206 lines
6 KiB
Ruby
206 lines
6 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
|
|
|
|
# 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][:and] ||= []
|
|
@search_definition[:post_filter][:and] |= [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: ['<em class="label label-highlight">'],
|
|
post_tags: ['</em>'],
|
|
fields: {
|
|
title: { number_of_fragments: 0 },
|
|
abstract: { number_of_fragments: 0 },
|
|
content: { fragment_size: 50 }
|
|
}
|
|
},
|
|
|
|
post_filter: {},
|
|
|
|
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
|