title: Ruby elasticsearch-model - configuring multiple mappings
body: <pre><code>module Spree
      Product.class_eval do
         include Elasticsearch::Model
  
  index_name Spree::ElasticsearchSettings.index
  document_type 'spree_product'
  
    mapping _all: {"index_analyzer" =&gt; "nGram_analyzer", "search_analyzer" =&gt; "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: 'string', index_analyzer:'simple'
      end
  
      indexes :name_completion, type: 'multi_field' do
        indexes :untouched, type: 'string', include_in_all: false, index: 'not_analyzed'
      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" =&gt; "simple", "search_analyzer" =&gt; "whitespace_analyzer"} do
      indexes :name_completion, type: 'multi_field' do
        indexes :name, type: 'completion', index_analyzer:'simple', search_analyzer: 'simple', payloads: true
      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
  
  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(&amp;:self_and_ancestors).flatten.uniq.map(&amp;: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" =&gt; ["value_a","value_b"]},{"key2" =&gt; ["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 &lt;&lt; { terms: { properties: pair.map {|prop| prop.join("||")} } }
        end
      end
  
      sorting = case @sorting
      when "name_asc"
        [ {"name.untouched" =&gt; { order: "asc" }}, {"price" =&gt; { order: "asc" }}, "_score" ]
      when "name_desc"
        [ {"name.untouched" =&gt; { order: "desc" }}, {"price" =&gt; { order: "asc" }}, "_score" ]
      when "price_asc"
        [ {"price" =&gt; { order: "asc" }}, {"name.untouched" =&gt; { order: "asc" }}, "_score" ]
      when "price_desc"
        [ {"price" =&gt; { order: "desc" }}, {"name.untouched" =&gt; { order: "asc" }}, "_score" ]
      when "score"
        [ "_score", {"name.untouched" =&gt; { order: "asc" }}, {"price" =&gt; { order: "asc" }} ]
      else
        [ {"name.untouched" =&gt; { order: "asc" }}, {"price" =&gt; { 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 &lt;&lt; { terms: { taxon_ids: taxons } } unless taxons.empty?
      # only return products that are available
      #and_filter &lt;&lt; { range: { available_on: { lte: "now" } } }
      result[:query][:filtered][:filter] = { "and" =&gt; and_filter } unless and_filter.empty?
  
      # add price filter outside the query because it should have no effect on facets
      if price_min &amp;&amp; price_max &amp;&amp; (price_min &lt; 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
  </code></pre>
  
  <p>I have the following model with two mappings, one for conventional search the other for search suggestions. The latter mapping is required according to ElasticSearch docs to enable a Completion Suggestor - <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters-completion.html" rel="nofollow">https://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters-completion.html</a>.</p>
  
  <p>When indexing the above model I get the following error:
  [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 <code>__raise_transport_error'
  /usr/local/bundle/gems/elasticsearch-transport-1.0.12/lib/elasticsearch/transport/transport/base.rb:227:in</code>perform_request'
  /usr/local/bundle/gems/elasticsearch-transport-1.0.12/lib/elasticsearch/transport/transport/http/faraday.rb:20:in <code>perform_request'
  /usr/local/bundle/gems/elasticsearch-transport-1.0.12/lib/elasticsearch/transport/client.rb:119:in</code>perform_request'
  /usr/local/bundle/gems/elasticsearch-api-1.0.12/lib/elasticsearch/api/namespace/common.rb:21:in <code>perform_request'
  /usr/local/bundle/gems/elasticsearch-api-1.0.12/lib/elasticsearch/api/actions/indices/create.rb:77:in</code>create'
  /tmp/spree_elasticsearch/lib/tasks/load_products.rake:5:in `block (2 levels) in '
  Tasks: TOP => spree_elasticsearch:load_products</p>