Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Elasticsearch rails/ Elasticsearch model search model association

I have a model named Movie that looks like this:

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

  has_many :actors, after_add: [ lambda {|a,c| a.__elasticsearch__.index_document}],
                    after_remove: [ lambda {|a,c| a.__elasticsearch__.index_document}]

  settings index: {number_of_shards: 1} do
    mappings dynamic: 'false' do
      indexes :title, analyzer: 'snowball', boost: 100
      indexes :actors
    end
  end

   def as_indexed_json(options={})
    self.as_json(
      include: {
          actors: { only: :name}
      }
    )
  end
end

When i do Movie.first.as_indexed_json , I get:

{"id"=>6, "title"=>"Back to the Future ", 
"created_at"=>Wed, 03 Dec 2014 22:21:24 UTC +00:00, 
"updated_at"=>Fri, 12 Dec 2014 23:40:03 UTC +00:00, 
"actors"=>[{"name"=>"Michael J Fox"}, {"name"=>"Christopher Lloyd"}, 
{"name"=>"Lea Thompson"}]}

but when i do Movie.search("Christopher Lloyd").records.first i get: => nil .

What changes can i make to the index to search movies associated with the searched actor?

like image 444
Optimus Pette Avatar asked Dec 20 '14 16:12

Optimus Pette


3 Answers

I used filtering query to solve this, first I created an ActiveSupport::Concern called searchable.rb, the concern looks like this:

module Searchable
  extend ActiveSupport::Concern
  included do
    include Elasticsearch::Model
    include Elasticsearch::Model::Callbacks

    index_name [Rails.application.engine_name, Rails.env].join('_')

    settings index: { number_of_shards: 3, number_of_replicas: 0} do
    mapping do
      indexes :title, type: 'multi_field' do
        indexes :title, analyzer: 'snowball'
        indexes :tokenized, analyzer: 'simple'
      end
      indexes :actors, analyzer: 'keyword'
    end
    def as_indexed_json(options={})
      hash = self.as_json()
      hash['actors'] = self.actors.map(&:name)
      hash
    end

    def self.search(query, options={})
      __set_filters = lambda do |key, f|
        @search_definition[:post_filter][:and] ||= []
        @search_definition[:post_filter][:and]  |= [f]
      end

      @search_definition = {
        query: {},

        highlight: {
          pre_tags: ['<em class="label label-highlight">'],
          post_tags: ['</em>'],
          fields: {
            title: {number_of_fragments: 0}
          }
        },
        post_filter: {},

        aggregations: {
          actors: {
            filter: {bool: {must: [match_all: {}]}},
            aggregations: {actors: {terms: {field: 'actors'}}}
          }
        }
      }

        unless query.blank?
        @search_definition[:query] = {
          bool: {
            should: [
              {
                multi_match: {
                  query: query,
                  fields: ['title^10'],
                  operator: 'and'
                }
              }
            ]
          }
        }
      else
        @search_definition[:query] = { match_all: {} }
        @search_definition[:sort] = {created_at: 'desc'}
       end

       if options[:actor]
        f = {term: { actors: options[:taxon]}}
       end

       if options[:sort]
        @search_definition[:sort] = { options[:sort] => 'desc'}
        @search_definition[:track_scores] = true
       end
       __elasticsearch__.search(@search_definition)
    end
  end
end

I have the above concern in the models/concerns directory. In movies.rb I have:

class Movie < ActiveRecord::Base
  include Searchable
end

In movies_controller.rb I am doing searching on the index action and the action looks like this:

def index 
  options = {
  actor: params[:taxon],
    sort: params[:sort]
  }
  @movies = Movie.search(params[q], options).records
end 

Now when i go to http://localhost:3000/movies?q=future&actor=Christopher I get all records which have the word future on their title and has an actor with a name Christopher. You can have more than one filter as shown by the expert template of the example application templates found here .

like image 54
Optimus Pette Avatar answered Oct 28 '22 04:10

Optimus Pette


You can try add method search to your model like this:

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

  # ...

  def self.search(query, options = {})
    es_options =
      {
        query: {
          query_string: {
            query:            query,
            default_operator: 'AND',
        }
      },
      sort:  '_score',
    }.merge!(options)
    __elasticsearch__.search(es_options)
  end

  # ...
end

Here is some examples of method search: http://www.sitepoint.com/full-text-search-rails-elasticsearch/

And now you can search in all your indexed fields.

like image 39
Štefan Bartoš Avatar answered Oct 28 '22 04:10

Štefan Bartoš


You need to specify the fields in the the search method, like:

def self.search query
  __elasticsearch__.search(
    query: {
      multi_match: {
        query: query,
        fields: %w[title actor.name]
      }
    }
  )
end
like image 36
Jinbo Avatar answered Oct 28 '22 03:10

Jinbo