217 lines
7.2 KiB
Ruby
217 lines
7.2 KiB
Ruby
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: 'multi_field' do
|
|
indexes :title, analyzer: 'snowball'
|
|
indexes :tokenized, analyzer: 'simple'
|
|
end
|
|
|
|
indexes :content, type: 'multi_field' do
|
|
indexes :content, analyzer: 'snowball'
|
|
indexes :tokenized, analyzer: 'simple'
|
|
end
|
|
|
|
indexes :published_on, type: 'date'
|
|
|
|
indexes :authors do
|
|
indexes :full_name, type: 'multi_field' do
|
|
indexes :full_name
|
|
indexes :raw, analyzer: 'keyword'
|
|
end
|
|
end
|
|
|
|
indexes :categories, analyzer: 'keyword'
|
|
|
|
indexes :comments, type: 'nested' do
|
|
indexes :body, analyzer: 'snowball'
|
|
indexes :stars
|
|
indexes :pick
|
|
indexes :user, analyzer: 'keyword'
|
|
indexes :user_location, type: 'multi_field' do
|
|
indexes :user_location
|
|
indexes :raw, analyzer: '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
|
|
|
|
# Return documents matching the user's query, include highlights and aggregations in response,
|
|
# and implement a "cross" faceted navigation
|
|
#
|
|
# @param q [String] The user query
|
|
# @return [Elasticsearch::Model::Response::Response]
|
|
#
|
|
def self.search(q, options={})
|
|
@search_definition = Elasticsearch::DSL::Search.search do
|
|
query do
|
|
|
|
# If a user query is present...
|
|
#
|
|
unless q.blank?
|
|
bool do
|
|
|
|
# ... search in `title`, `abstract` and `content`, boosting `title`
|
|
#
|
|
should do
|
|
multi_match do
|
|
query q
|
|
fields ['title^10', 'abstract^2', 'content']
|
|
operator 'and'
|
|
end
|
|
end
|
|
|
|
# ... search in comment body if user checked the comments checkbox
|
|
#
|
|
if q.present? && options[:comments]
|
|
should do
|
|
nested do
|
|
path :comments
|
|
query do
|
|
multi_match do
|
|
query q
|
|
fields 'body'
|
|
operator 'and'
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# ... otherwise, just return all articles
|
|
else
|
|
match_all
|
|
end
|
|
end
|
|
|
|
# Filter the search results based on user selection
|
|
#
|
|
post_filter do
|
|
bool do
|
|
must { term categories: options[:category] } if options[:category]
|
|
must { match_all } if options.keys.none? { |k| [:c, :a, :w].include? k }
|
|
must { term 'authors.full_name.raw' => options[:author] } if options[:author]
|
|
must { range published_on: { gte: options[:published_week], lte: "#{options[:published_week]}||+1w" } } if options[:published_week]
|
|
end
|
|
end
|
|
|
|
# Return top categories for faceted navigation
|
|
#
|
|
aggregation :categories do
|
|
# Filter the aggregation with any selected `author` and `published_week`
|
|
#
|
|
f = Elasticsearch::DSL::Search::Filters::Bool.new
|
|
f.must { match_all }
|
|
f.must { term 'authors.full_name.raw' => options[:author] } if options[:author]
|
|
f.must { range published_on: { gte: options[:published_week], lte: "#{options[:published_week]}||+1w" } } if options[:published_week]
|
|
|
|
filter f.to_hash do
|
|
aggregation :categories do
|
|
terms field: 'categories'
|
|
end
|
|
end
|
|
end
|
|
|
|
# Return top authors for faceted navigation
|
|
#
|
|
aggregation :authors do
|
|
# Filter the aggregation with any selected `category` and `published_week`
|
|
#
|
|
f = Elasticsearch::DSL::Search::Filters::Bool.new
|
|
f.must { match_all }
|
|
f.must { term categories: options[:category] } if options[:category]
|
|
f.must { range published_on: { gte: options[:published_week], lte: "#{options[:published_week]}||+1w" } } if options[:published_week]
|
|
|
|
filter f do
|
|
aggregation :authors do
|
|
terms field: 'authors.full_name.raw'
|
|
end
|
|
end
|
|
end
|
|
|
|
# Return the published date ranges for faceted navigation
|
|
#
|
|
aggregation :published do
|
|
# Filter the aggregation with any selected `author` and `category`
|
|
#
|
|
f = Elasticsearch::DSL::Search::Filters::Bool.new
|
|
f.must { match_all }
|
|
f.must { term 'authors.full_name.raw' => options[:author] } if options[:author]
|
|
f.must { term categories: options[:category] } if options[:category]
|
|
|
|
filter f do
|
|
aggregation :published do
|
|
date_histogram do
|
|
field 'published_on'
|
|
interval 'week'
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# Highlight the snippets in results
|
|
#
|
|
highlight do
|
|
fields title: { number_of_fragments: 0 },
|
|
abstract: { number_of_fragments: 0 },
|
|
content: { fragment_size: 50 }
|
|
|
|
field 'comments.body', fragment_size: 50 if q.present? && options[:comments]
|
|
|
|
pre_tags '<em class="label label-highlight">'
|
|
post_tags '</em>'
|
|
end
|
|
|
|
case
|
|
# By default, sort by relevance, but when a specific sort option is present, use it ...
|
|
#
|
|
when options[:sort]
|
|
sort options[:sort].to_sym => 'desc'
|
|
track_scores true
|
|
#
|
|
# ... when there's no user query, sort on published date
|
|
#
|
|
when q.blank?
|
|
sort published_on: 'desc'
|
|
end
|
|
|
|
# Return suggestions unless there's no query from the user
|
|
unless q.blank?
|
|
suggest :suggest_title, text: q, term: { field: 'title.tokenized', suggest_mode: 'always' }
|
|
suggest :suggest_body, text: q, term: { field: 'content.tokenized', suggest_mode: 'always' }
|
|
end
|
|
end
|
|
|
|
__elasticsearch__.search(@search_definition)
|
|
end
|
|
end
|
|
end
|