Today I Learned

Working with ElasticSearch and Rails; part 3

“You can pass any object which implements a to_hash method, which is called automatically, so you can use a custom class or your favourite JSON builder to build the search definition”

The aforementioned sentence lies somewhere in the middle of the lengthy Readme file for the elasticsearch-model gem and can be easily overlooked, however it let’s you to create abstraction for some standard elements of the user intefrace.

Imagine you have a listing displaying records, which can be filtered by the user. There are different types of filters depending if the actual field is a date, string, boolean and so on. User interacts with the filters and the request is being fired up to the backend.

Then on the backend, you translate the payload, matching the params contents against in the the info specified in your META for a given search class, generating an array of filtering directives as the one below:

module Search
 module Filters
   class Range
     def initialize(name:, min:, max:)
       raise ::Search::Error::RangeFilterMissing.new(filter: name) if min.blank? && max.blank?

       @name = name
       @min  = min
       @max  = max
     end

     def to_hash
       {}.tap do |range|
         range[:range] = Hash[name, {}]
         range[:range][name][:gte] = min if min.present?
         range[:range][name][:lte] = max if max.present?
       end
     end

     private

     attr_accessor :name, :min, :max
   end
 end
end

Which can be easily passed as an input for the Model.search as:

def search_query
  { query: query, sort: sort, aggs: aggregations }
end

As long as we organized it with the following interface:

def query
  filter.presence || { match_all: {} }
end

def filter
  return if filter_options.blank?

  filter = { bool: { filter: [] } }
  filter_options.map do |f|
    filter[:bool][:filter].push(f)
  end

  filter
end

where the filter_options contains already mangled instances of various filter classes abstracitons.