Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Creating multiple mappings for one model/index #429

Closed
vmatekole opened this issue Jun 19, 2015 · 3 comments
Closed

Creating multiple mappings for one model/index #429

vmatekole opened this issue Jun 19, 2015 · 3 comments

Comments

@vmatekole
Copy link

I want to create two mappings, one for conventional search and the other using a Completion Suggestor. According to elasticsearch the latter requires a separate mapping - https://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters-completion.html

I have the following code:

module Spree
  Product.class_eval do
    include Elasticsearch::Model

    index_name Spree::ElasticsearchSettings.index
    document_type 'spree_product'

      mapping _all: {"index_analyzer" => "nGram_analyzer", "search_analyzer" => "whitespace_analyzer"} do
        indexes :name, type: 'multi_field' do
          indexes :name, type: 'string', analyzer:'nGram_analyzer', boost: 100
          indexes :untouched, type: 'string', include_in_all: false, index: 'not_analyzed'
        end

        indexes :name_whole, type: 'multi_field' do
          indexes :name, type: 'completion', index_analyzer:'simple', search_analyzer: 'simple', payloads: true          
          indexes :name, type: 'string', index_analyzer:'simple'
        end

        indexes :taxon_ids, type: 'multi_field' do
          indexes :taxon_ids, type:'string', analyzer: 'simple'
          indexes :taxon_ids_ngram, type:'string', analyzer:'nGram_analyzer'
        end
        indexes :description, analyzer: 'snowball', include_in_all:false
        indexes :available_on, type: 'date', format: 'dateOptionalTime', include_in_all: false
        indexes :price, type: 'double', include_in_all:false
        indexes :sku, type: 'string', index: 'not_analyzed', include_in_all:false
        indexes :properties, type: 'string', index: 'not_analyzed', include_in_all:false
     end

     mapping _suggest: {"index_analyzer" => "simple", "search_analyzer" => "whitespace_analyzer"} do
        indexes :name_completion, type: 'multi_field' do
          indexes :name, type: 'completion', index_analyzer:'simple', search_analyzer: 'simple', payloads: true
        end
     end

    def as_indexed_json(options={})
      result = as_json({
        methods: [:price, :sku],
        only: [:available_on, :description, :name],
        include: {
          variants: {
            only: [:sku],
            include: {
              option_values: {
                only: [:name, :presentation]
              }
            }
          }
        }
      })
      result[:properties] = property_list unless property_list.empty?
      result[:taxon_ids] = taxons.map(&:self_and_ancestors).flatten.uniq.map(&:id) unless taxons.empty?
      result
    end

    # Inner class used to query elasticsearch. The idea is that the query is dynamically build based on the parameters.
    class Product::ElasticsearchQuery
      include ::Virtus.model

      attribute :from, Integer, default: 0
      attribute :price_min, Float
      attribute :price_max, Float
      attribute :properties, Hash
      attribute :query, String
      attribute :taxons, Array
      attribute :browse_mode, Boolean
      attribute :sorting, String

      # When browse_mode is enabled, the taxon filter is placed at top level. This causes the results to be limited, but facetting is done on the complete dataset.
      # When browse_mode is disabled, the taxon filter is placed inside the filtered query. This causes the facets to be limited to the resulting set.

      # Method that creates the actual query based on the current attributes.
      # The idea is to always to use the following schema and fill in the blanks.
      # {
      #   query: {
      #     filtered: {
      #       query: {
      #         query_string: { query: , fields: [] }
      #       }
      #       filter: {
      #         and: [
      #           { terms: { taxons: [] } },
      #           { terms: { properties: [] } }
      #         ]
      #       }
      #     }
      #   }
      #   filter: { range: { price: { lte: , gte: } } },
      #   sort: [],
      #   from: ,
      #   facets:
      # }
      def to_hash
        q = { match_all: {} }
        unless query.blank? # nil or empty
          q = { query_string: { query: query, fields: ['name^5','description','sku'], default_operator: 'AND', use_dis_max: true } }
        end
        query = q

        and_filter = []
        unless @properties.nil? || @properties.empty?
          # transform properties from [{"key1" => ["value_a","value_b"]},{"key2" => ["value_a"]}
          # to { terms: { properties: ["key1||value_a","key1||value_b"] }
          #    { terms: { properties: ["key2||value_a"] }
          # This enforces "and" relation between different property values and "or" relation between same property values
          properties = @properties.map {|k,v| [k].product(v)}.map do |pair|
            and_filter << { terms: { properties: pair.map {|prop| prop.join("||")} } }
          end
        end

        sorting = case @sorting
        when "name_asc"
          [ {"name.untouched" => { order: "asc" }}, {"price" => { order: "asc" }}, "_score" ]
        when "name_desc"
          [ {"name.untouched" => { order: "desc" }}, {"price" => { order: "asc" }}, "_score" ]
        when "price_asc"
          [ {"price" => { order: "asc" }}, {"name.untouched" => { order: "asc" }}, "_score" ]
        when "price_desc"
          [ {"price" => { order: "desc" }}, {"name.untouched" => { order: "asc" }}, "_score" ]
        when "score"
          [ "_score", {"name.untouched" => { order: "asc" }}, {"price" => { order: "asc" }} ]
        else
          [ {"name.untouched" => { order: "asc" }}, {"price" => { order: "asc" }}, "_score" ]
        end

        # facets
        facets = {
          price: { statistical: { field: "price" } },
          properties: { terms: { field: "properties", order: "count", size: 1000000 } },
          taxon_ids: { terms: { field: "taxon_ids", size: 1000000 } }
        }

        # basic skeleton
        result = {
          min_score: 0.1,
          query: { filtered: {} },
          sort: sorting,
          from: from,
          facets: facets
        }

        # add query and filters to filtered
        result[:query][:filtered][:query] = query
        # taxon and property filters have an effect on the facets
        and_filter << { terms: { taxon_ids: taxons } } unless taxons.empty?
        # only return products that are available
        #and_filter << { range: { available_on: { lte: "now" } } }
        result[:query][:filtered][:filter] = { "and" => and_filter } unless and_filter.empty?

        # add price filter outside the query because it should have no effect on facets
        if price_min && price_max && (price_min < price_max)
          result[:filter] = { range: { price: { gte: price_min, lte: price_max } } }
        end
        result
      end
    end

    private

    def property_list
      product_properties.map{|pp| "#{pp.property.name}||#{pp.value}"}
    end
  end
end

But get this error when indexing my model:
Elasticsearch::Transport::Transport::Errors::BadRequest: [400] {"error":"MapperParsingException[mapping [spree_product]]; nested: MapperParsingException[Root type mapping not empty after parsing! Remaining fields: [_suggest : {search_analyzer=whitespace_analyzer, index_analyzer=simple}]]; ","status":400}
/usr/local/bundle/gems/elasticsearch-transport-1.0.12/lib/elasticsearch/transport/transport/base.rb:135:in __raise_transport_error' /usr/local/bundle/gems/elasticsearch-transport-1.0.12/lib/elasticsearch/transport/transport/base.rb:227:inperform_request'
/usr/local/bundle/gems/elasticsearch-transport-1.0.12/lib/elasticsearch/transport/transport/http/faraday.rb:20:in perform_request' /usr/local/bundle/gems/elasticsearch-transport-1.0.12/lib/elasticsearch/transport/client.rb:119:inperform_request'
/usr/local/bundle/gems/elasticsearch-api-1.0.12/lib/elasticsearch/api/namespace/common.rb:21:in perform_request' /usr/local/bundle/gems/elasticsearch-api-1.0.12/lib/elasticsearch/api/actions/indices/create.rb:77:increate'
/tmp/spree_elasticsearch/lib/tasks/load_products.rake:5:in `block (2 levels) in <top (required)>'
Tasks: TOP => spree_elasticsearch:load_products
(See full trace by running task with --trace)

@vmatekole vmatekole changed the title Creating multiple mappings for one model Creating multiple mappings for one model/index Jun 19, 2015
@karmi
Copy link
Contributor

karmi commented Jun 20, 2015

Hmm, not sure about nesting the completion type in multi field, can you try the following example:

require 'ansi'
require 'active_record'
require 'elasticsearch/model'

ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT)
ActiveRecord::Base.establish_connection( adapter: 'sqlite3', database: ":memory:" )

ActiveRecord::Schema.define(version: 1) do
  create_table :articles do |t|
    t.string :title
    t.date   :published_at
    t.timestamps
  end
end

class Article < ActiveRecord::Base
  include Elasticsearch::Model
  include Elasticsearch::Model::Callbacks

  mapping do
    indexes :title
    indexes :title_suggest, type: 'completion', payloads: true
  end

  def as_indexed_json(options={})
    as_json.merge \
    title_suggest: {
      input:  title,
      output: title,
      payload: { url: "/articles/#{id}" }
    }
  end
end

Article.__elasticsearch__.client = Elasticsearch::Client.new log: true

# Create index

Article.__elasticsearch__.create_index! force: true

# Store data

Article.delete_all
Article.create title: 'Foo'
Article.create title: 'Bar'
Article.create title: 'Foo Foo'
Article.__elasticsearch__.refresh_index!

# Search and suggest

response_1 = Article.search 'foo';

puts "Article search:".ansi(:bold), response_1.to_a.map(&:title).inspect.ansi(:bold, :yellow)

response_2 = Article.__elasticsearch__.client.suggest \
  index: Article.index_name,
  body: {
    articles: {
      text: 'foo',
      completion: { field: 'title_suggest', size: 25 }
    }
  };

puts "Article suggest:".ansi(:bold), response_2['articles'].inspect.ansi(:bold, :green)

require 'pry'; binding.pry;

@vmatekole
Copy link
Author

@karmi sorry for prolonged response, I will test later today and close issue once resolved. Thanks so much for your support!

@vmatekole
Copy link
Author

@karmi Awesome. That worked, thank you for your support!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants