Steps to use Tire for ElasticSearch Engine in Rails Application:
- Include gem “tire” in the Gemfile and bundle install
- Create a initializer for Tire to define the ElasticSearch server URL
- Provide following server and index settings in confg/initializer/tire.rb
Tire.configure {
url “localhost:9200”
logger "#{Rails.root}/log/es.log"
}
# Index name -- Add the Application name and Environment name prefix to the indexed model.
Tire::Model::Search.index_prefix “ANY INDEX NAME”
- we have to include these for search and auto index after calling callbacks
- include Tire::Model::Search
- include Tire::Model::Callbacks
- Mapping, JSON Serialization, and Search
Follow the detailed explanation for mapping, JSON Serialization and Search given below:
How to Index or search a model and associated models attributes?
Let’s take the complete example indexing and searching for a model and models associated to this:
Class Book < ActiveRecord::Base
has_many :book_authors, :dependent => :destroy
has_many :authors, :through => :book_authors,
:after_add => :touch_book_for_indexing
has_one :epub
has_one :publisher
has_one :book_info
# For search and auto index after calling callbacks
include Tire::Model::Search
include Tire::Model::Callbacks
end
Note: Mapping of attributes should be done before the indexing, because once the data got indexed mapping can not be changed without re-indexing.
How to map and provide the attribute type, analyzers and custom analyzer:
# Settings analysis for defining the custom analyzer i.e "asciifolding_analyzer" which is required # to converts alphabetic, numeric, and symbolic Unicode characters which are not in the first 127 # ASCII characters (the "Basic Latin" Unicode block) into their ASCII equivalents, if one exists.
# For more details of analyzers please follow : http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping-analyzer-field.html
settings :analysis => {
:analyzer => {
:asciifolding_analyzer => {
"tokenizer" => 'whitespace',
"filter" => ['lowercase', 'asciifolding'],
"type" => "custom" }
}
} do
mapping do
# Mapping of Book Model Attributes
indexes :name, :type => 'string', :analyzer => "asciifolding_analyzer"
indexes :sort_name, type: 'string', index: 'not_analyzed'
indexes :image_url, type: 'string'
indexes :place_holder, type: 'string', index: 'not_analyzed'
indexes :price, type: 'float'
# has_many association attributes - authors
indexes :authors do
indexes :first_name, :type => 'string', :analyzer => "asciifolding_analyzer"
indexes :last_name, :type => 'string', :analyzer => "asciifolding_analyzer"
indexes :full_name, type: 'string', index: 'not_analyzed'
end
# has_one association attributes - book_info
indexes :book_info do
indexes :isbn, :type => 'string'
indexes :isbn13, :type => 'string'
# indexes :gtin13, :type => 'string'
indexes :description, :type => 'string'
indexes :release_date, type: 'date'
indexes :plan_type, :type => 'string', :analyzer => 'keyword'
end
# has_one association attributes - epub
indexes :epub do
indexes :epub_id, index: :not_analyzed
end
# has_one association attributes - publisher
indexes :publisher do
indexes :publisher_name, type: 'string', index: :not_analyzed
end
end
end
After Adding the mapping of attributes Book model looks like:
Class Book < ActiveRecord::Base
has_many :book_authors, :dependent => :destroy
has_many :authors, :through => :book_authors, :after_add => :touch_book_for_indexing
has_one :epub
has_one :publisher
has_one :book_info
# we have to include these for search and auto index after calling callbacks
include Tire::Model::Search
include Tire::Model::Callbacks
# mapiing of attributes and custom analyzer settings
settings :analysis => {
:analyzer => {
:asciifolding_analyzer => {
"tokenizer" => 'whitespace',
"filter" => ['lowercase', 'asciifolding'],
"type" => "custom" }
}
} do
mapping do
# Mapping of Book Model Attributes
indexes :name, :type => 'string', :analyzer => "asciifolding_analyzer"
indexes :sort_name, type: 'string', index: 'not_analyzed'
indexes :image_url, type: 'string'
indexes :place_holder, type: 'string', index: 'not_analyzed'
indexes :price, type: 'float'
# has_many association attributes - authors
indexes :authors do
indexes :first_name, :type => 'string', :analyzer => "asciifolding_analyzer"
indexes :last_name, :type => 'string', :analyzer => "asciifolding_analyzer"
indexes :full_name, type: 'string', index: 'not_analyzed'
end
# has_one association attributes - book_info
indexes :book_info do
indexes :isbn, :type => 'string'
indexes :isbn13, :type => 'string'
# indexes :gtin13, :type => 'string'
indexes :description, :type => 'string'
indexes :release_date, type: 'date'
indexes :plan_type, :type => 'string', :analyzer => 'keyword'
end
# has_one association attributes - epub
indexes :epub do
indexes :epub_id, index: :not_analyzed
end
# has_one association attributes - publisher
indexes :publisher do
indexes :publisher_name, type: 'string', index: :not_analyzed
end
end
end
end
# Define the JSON serialization for the documents/models
# This way documents get indexed on ElasticSearch Engine
def to_indexed_json
# Need to look another way if book info not present
if book_info.blank?
options = { :isbn => nil,
:isbn13 => nil,
# :gtin13 => nil,
:description => nil,
:release_date => nil,
:plan_type => nil
}
else
options = { :isbn => book_info.isbn,
:isbn13 => book_info.isbn13,
# :gtin13 => book_info.gtin13,
:description => book_info.description,
:release_date => book_info.publishing_date,
:plan_type => get_plan_type_name
}
end
epub_id = epub.blank? ? nil : epub.id
publisher_name = publisher.blank? ? nil : publisher.name
# This method will get the place holder and image_url of a book
get_book_cover_image_url
{:id => id,
:name => name,
:image_url => self["image_url"],
:place_holder => self["place_holder"],
:sort_name => name,
:book_info => options,
:epub_id => epub_id,
:price => price,
:publisher_name => publisher_name,
:authors => authors.map { |c| { :id => c.id, :first_name => c.first_name, :last_name => c.last_name, :full_name => "#{c.first_name.to_s} #{c.last_name.to_s}".strip } },
}.to_json
end
# Method called inside the to_indexed_json method:
def get_book_cover_image_url
if self.images.blank?
self["image_url"] = DEFAULT_BOOK_COVER
self["place_holder"] = true
else
self["image_url"] = self.images.first.attachment.url
self["place_holder"] = false
end
end
Book Model with mapping and JSON serialization looks like:
Class Book < ActiveRecord::Base
has_many :book_authors, :dependent => :destroy
has_many :authors, :through => :book_authors, :after_add => :touch_book_for_indexing
has_one :epub
has_one :publisher
has_one :book_info
# we have to include these for search and auto index after calling callbacks
include Tire::Model::Search
include Tire::Model::Callbacks
# mapiing of attributes and custom analyzer settings
settings :analysis => {
:analyzer => {
:asciifolding_analyzer => {
"tokenizer" => 'whitespace',
"filter" => ['lowercase', 'asciifolding'],
"type" => "custom" }
}
} do
mapping do
# Mapping of Book Model Attributes
indexes :name, :type => 'string', :analyzer => "asciifolding_analyzer"
indexes :sort_name, type: 'string', index: 'not_analyzed'
indexes :image_url, type: 'string'
indexes :place_holder, type: 'string', index: 'not_analyzed'
indexes :price, type: 'float'
# has_many association attributes - authors
indexes :authors do
indexes :first_name, :type => 'string', :analyzer => "asciifolding_analyzer"
indexes :last_name, :type => 'string', :analyzer => "asciifolding_analyzer"
indexes :full_name, type: 'string', index: 'not_analyzed'
end
# has_one association attributes - book_info
indexes :book_info do
indexes :isbn, :type => 'string'
indexes :isbn13, :type => 'string'
# indexes :gtin13, :type => 'string'
indexes :description, :type => 'string'
indexes :release_date, type: 'date'
indexes :plan_type, :type => 'string', :analyzer => 'keyword'
end
# has_one association attributes - epub
indexes :epub do
indexes :epub_id, index: :not_analyzed
end
# has_one association attributes - publisher
indexes :publisher do
indexes :publisher_name, type: 'string', index: :not_analyzed
end
end
end
def to_indexed_json
# Need to look another way if book info not present
if book_info.blank?
options = { :isbn => nil,
:isbn13 => nil,
# :gtin13 => nil,
:description => nil,
:release_date => nil,
:plan_type => nil
}
else
options = { :isbn => book_info.isbn,
:isbn13 => book_info.isbn13,
# :gtin13 => book_info.gtin13,
:description => book_info.description,
:release_date => book_info.publishing_date,
:plan_type => get_plan_type_name
}
end
epub_id = epub.blank? ? nil : epub.id
publisher_name = publisher.blank? ? nil : publisher.name
# This method will get the place holder and image_url of a book
get_book_cover_image_url
{:id => id,
:name => name,
:image_url => self["image_url"],
:place_holder => self["place_holder"],
:sort_name => name,
:book_info => options,
:epub_id => epub_id,
:price => price,
:publisher_name => publisher_name,
:authors => authors.map { |c| { :id => c.id, :first_name => c.first_name, :last_name => c.last_name, :full_name => "#{c.first_name.to_s} #{c.last_name.to_s}".strip } },
}.to_json
end
# Method called inside to_indexed json
def get_book_cover_image_url
if self.images.blank?
self["image_url"] = DEFAULT_BOOK_COVER
self["place_holder"] = true
else
self["image_url"] = self.images.first.attachment.url
self["place_holder"] = false
end
end
end
Search - With Boost, Filters, Sorting, Exact match(By Default for those attributes which are mapped with not analyzed option)
Note1: By default to separate or filters behaves as AND for each other but as OR for the conditions wriiten inside them.
Note2: Boosting will results in high score and will give the more priority .
# search query(come from params) by using tire.
def self.search(search_query, page_no, page_size, sort_type, plan_type, is_app = true)
page_size ||= DEFAULT_PAGE_SIZE
curr_date = Date.today
search_query = self.sanitize_string_for_elasticsearch_query(search_query)
load = is_app ? false : true
tire.search(:load => load, :per_page => page_size, :page => page_no) do
if search_query.present?
query do
boolean do
# This will boost the exact matched result.
should { match :name, search_query, :type => :text, :boost => 120}
should { string "name:#{search_query}", {:boost => 100} }
should { string "full_name:#{search_query}", {:boost => 120} }
should { string "description:#{search_query}"}
should {string "first_name:#{search_query}", {:boost => 50}}
should {string "last_name:#{search_query}", {:boost => 50}}
should {string "#{search_query}"}
end
end
if is_app
if plan_type.present?
@plan_rules = BookPlanRule.where(:plan_type_id => PlanType.find_by_name(plan_type))
plan_type = [plan_type.downcase]
else
plan_type = ['basic','premium']
@plan_rules = BookPlanRule.all
end
filter :or,[{exists: {field: :epub_id}}, {range: {release_date:{gt: curr_date}}}]
if @plan_rules.present?
cond_arr = Array.new
# Create the array of publisher speicific book plan rules. for filtering the search results
# create the cond_arr with AND option because all the conditions specific to a publisher required to be satisfied while filtering
# Provide the AND conditions array to OR filter whatever publisher specific condition meets the criteria will come in the search result.
@plan_rules.each do |p|
cond_arr << {:and => [{:range => {:price => {:gte => p.min_price, :lt => p.max_price}}}, {:term => {:publisher_source_id => p.publisher_source_id}}, {missing: {field: :plan_type}}]}
end
# If plan rules are not configured then only those books which have plan type will come in the search result
cond_arr << {:term => {:plan_type => plan_type}}
filter :or, cond_arr
end
end
# facet ('authors') {terms "first_name"}
# highlight :name, :description, :options => { :tag => '<strong>' }
# Sorting of search results
# Attribute sort_name is not analyzed because it should sort the results with the exact match.
case sort_type
when SORT_BY_ALPHABETICALLY_ASC
sort {by :sort_name, 'asc'}
when SORT_BY_ALPHABETICALLY_DESC
sort { by :sort_name, 'desc'}
when SORT_BY_RELEASE_DATE_ASC
sort { by :release_date, 'asc' }
when SORT_BY_RELEASE_DATE_DESC
sort { by :release_date, 'desc' }
end
end
end
end