module Searchable extend ActiveSupport::Concern included do include Elasticsearch::Model # Customize the index name # index_name [Rails.application.engine_name, Rails.env].join('_') # Set up index configuration and mapping # settings index: { number_of_shards: 1, number_of_replicas: 0 } do mapping do indexes :title, type: 'text' do indexes :title, analyzer: 'snowball' indexes :tokenized, analyzer: 'simple' end indexes :content, type: 'text' do indexes :content, analyzer: 'snowball' indexes :tokenized, analyzer: 'simple' end indexes :published_on, type: 'date' indexes :authors do indexes :full_name, type: 'text' do indexes :full_name indexes :raw, type: 'keyword' end end indexes :categories, type: 'keyword' indexes :comments, type: 'nested' do indexes :body, analyzer: 'snowball' indexes :stars indexes :pick indexes :user, type: 'keyword' indexes :user_location, type: 'text' do indexes :user_location indexes :raw, type: 'keyword' end end end end # Set up callbacks for updating the index on model changes # after_commit lambda { Indexer.perform_async(:index, self.class.to_s, self.id) }, on: :create after_commit lambda { Indexer.perform_async(:update, self.class.to_s, self.id) }, on: :update after_commit lambda { Indexer.perform_async(:delete, self.class.to_s, self.id) }, on: :destroy after_touch lambda { Indexer.perform_async(:update, self.class.to_s, self.id) } # Customize the JSON serialization for Elasticsearch # def as_indexed_json(options={}) hash = self.as_json( include: { authors: { methods: [:full_name], only: [:full_name] }, comments: { only: [:body, :stars, :pick, :user, :user_location] } }) hash['categories'] = self.categories.map(&:title) hash end # Search in title and content fields for `query`, include highlights in response # # @param query [String] The user query # @return [Elasticsearch::Model::Response::Response] # def self.search(query, options={}) # Prefill and set the filters (top-level `post_filter` and aggregation `filter` elements) # __set_filters = lambda do |key, f| @search_definition[:post_filter][:bool] ||= {} @search_definition[:post_filter][:bool][:must] ||= [] @search_definition[:post_filter][:bool][:must] |= [f] @search_definition[:aggregations][key.to_sym][:filter][:bool][:must] ||= [] @search_definition[:aggregations][key.to_sym][:filter][:bool][:must] |= [f] end @search_definition = { query: {}, highlight: { pre_tags: [''], post_tags: [''], fields: { title: { number_of_fragments: 0 }, abstract: { number_of_fragments: 0 }, content: { fragment_size: 50 } } }, post_filter: { bool: { must: [ match_all: {} ] } }, aggregations: { categories: { filter: { bool: { must: [ match_all: {} ] } }, aggregations: { categories: { terms: { field: 'categories' } } } }, authors: { filter: { bool: { must: [ match_all: {} ] } }, aggregations: { authors: { terms: { field: 'authors.full_name.raw' } } } }, published: { filter: { bool: { must: [ match_all: {} ] } }, aggregations: { published: { date_histogram: { field: 'published_on', interval: 'week' } } } } } } unless query.blank? @search_definition[:query] = { bool: { should: [ { multi_match: { query: query, fields: ['title^10', 'abstract^2', 'content'], operator: 'and' } } ] } } else @search_definition[:query] = { match_all: {} } @search_definition[:sort] = { published_on: 'desc' } end if options[:category] f = { term: { categories: options[:category] } } __set_filters.(:authors, f) __set_filters.(:published, f) end if options[:author] f = { term: { 'authors.full_name.raw' => options[:author] } } __set_filters.(:categories, f) __set_filters.(:published, f) end if options[:published_week] f = { range: { published_on: { gte: options[:published_week], lte: "#{options[:published_week]}||+1w" } } } __set_filters.(:categories, f) __set_filters.(:authors, f) end if query.present? && options[:comments] @search_definition[:query][:bool][:should] ||= [] @search_definition[:query][:bool][:should] << { nested: { path: 'comments', query: { multi_match: { query: query, fields: ['comments.body'], operator: 'and' } } } } @search_definition[:highlight][:fields].update 'comments.body' => { fragment_size: 50 } end if options[:sort] @search_definition[:sort] = { options[:sort] => 'desc' } @search_definition[:track_scores] = true end unless query.blank? @search_definition[:suggest] = { text: query, suggest_title: { term: { field: 'title.tokenized', suggest_mode: 'always' } }, suggest_body: { term: { field: 'content.tokenized', suggest_mode: 'always' } } } end __elasticsearch__.search(@search_definition) end end end