Saturday, March 1, 2014


Note: This docs assumes you have installed ElasticSearch Engine and server is running on port-9200(In case ElasticSearch is not installed yet please follow - ElasticSearch Installation


Steps to use Tire for ElasticSearch Engine in Rails Application:


  1. Include gem “tire” in the Gemfile and bundle install
  2. 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”


  1. we have to include these for search and auto index after calling callbacks
  • include Tire::Model::Search
  • include Tire::Model::Callbacks
  1. 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.




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