No commit activity in last 3 years
No release in over 3 years
Add faceted searching with ElasticSearch to your Models
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

Runtime

 Project Readme

Elasticsearch::FacetedSearch

Code Climate Test Coverage Build Status

Quickly add faceted searching to your Rails app. This gem is opinionated as to how faceted searching works. Filters are applied to the counts so the counts themselves will change while different filters are applied.

Installation

Add this line to your application's Gemfile:

gem 'elasticsearch-facetedsearch'

And then execute:

$ bundle

Create config/initializers/elasticsearch.rb. We normally namespace our indexed like below.

ELASTICSEARCH_INDEX = [
  Rails.env.development? && `whoami`.strip,
  Rails.env,
  Rails.application.class.to_s.split("::").first.downcase
].reject(&:blank?).join('_')

# Optional concepts to help with indexing / connection
ELASTICSEARCH_MODELS = []
ELASTICSEARCH_SERVER = 'http://lalaland.com:9200'

Elasticsearch::Model.client = Elasticsearch::Client.new({
  log: false,
  host: ELASTICSEARCH_SERVER,
  retry_on_failure: 5,
  reload_connections: true
})

Dependencies

Usage

Controller

# Good idea to prefilter the params with strong_params
#
def search
  @search = FruitFacetsSearch.new(params)

  # Fetch results
  # @search.results

  # Fetch facets
  # @search.facets
end

Facet Search Class

class FruitFacetsSearch
  include Elasticsearch::FacetedSearch::FacetBase

  # ... include other facet classes (examples below) ...
  include Elasticsearch::FacetedSearch::FacetColor

  # *required
  # The type to search for (Elasticsearch Type)
  # Can be an Array or String
  def type
    'fruit'
  end

  # Use this to add a query search or something
  # Probably best to cache the results
  def query
    @query ||= super
  end

  # Apply additional pre-filters
  # If overwriting this method, ensure to call super, and ensure to cache the results
  # => require color to be 'blue'
  def filter_query
    @filter_query ||= begin
      fq = super
      fq << { term: { color: 'blue' } }
      fq
    end
  end

  # Force specific limit or allow changable #s
  #
  def limit
    33
  end

  # Whitelisted collection of sortable options
  def sorts
    [
      {
        label: "Relevant",
        value: "relevant",
        search: [
          "_score"
        ],
        default: true
      }
    ]
  end

  # Want to always keep facet counts the same regardless of filters applied?
  # pass true to keep counts scoped to search, & false to remove filters entirely
  #
  def build_facets
    super(false)
  end
end

Facet Creation Class

module Elasticsearch
  module FacetedSearch
    module FacetColor
      extend ActiveSupport::Concern

      included do
        # Adds the facet to the class.facets collection
        #
        # Available types:
        # => facet_multivalue
        # => facet_multivalue_and(:ident, 'elasticsearch_field', 'Human String')
        #     - Allows multiple values, but filters with :and execution
        # => facet_multivalue_or(:ident, 'elasticsearch_field', 'Human String')
        #     - Allows multiple values, but filters with :or execution
        # => facet_exclusive_or(:ident, 'elasticsearch_field', 'Human String')
        #     - Allows single value only
        #
        facet_multivalue_or(:color, 'color_field', 'Skin Color')
      end

      # *required
      # Should we apply the filter for this facet?
      # __Replace 'color' with the :ident value of your facet key
      def filter_color?
        valid_color?
      end

      # *required
      # Returns the array of selected values
      # __Replace 'color' with the :ident value of your facet key
      # __You should really take this time to whitelist the values and remove any noise. Elasticsearch can be picky if you're searching a number field and pass it an alpha character
      def color_value
        return unless valid_color?
        search_params[:color].split( operator_for(:color) )
      end

      # (optional)
      # By default, Elasticsearch only returns terms that are pertaining to the specific search. If a result was filtered out, that term would not show up.
      # Normally this isn't optimal... create this method and return an array of hashes with id and term keys.
      #
      # __Replace 'color' with the :ident value of your facet key
      def color_collection
        @color_collection ||= begin
          Color.all.map{|x| {id: x.id, term: x.name}}
        end
      end

      # (concept)
      # Use these type of helper methods to validate the information given to you by users
      #
      def valid_color?
        search_params[:color].present? && !!(search_params[:color] =~ /[\|[0-9]+]/)
      end
    end
  end
end

Using Sortable

The only requirement to change sort options is to apply a :sort http param

Pagniation

Pagination is supported, but only tested with Kaminari.

HTML

Facets

%ul
  -@search.facets.each do |group|
    %li.title=group.title
    -group.items.each do |item|
      - # Assuming you have some dynamic urls, you can use the url_for and merge in the params
      %li=item.link_to("#{item.term} (#{item.count})", url_for(params.merge(item.params_for)))

Results

%ul
  -@search.results.each do |item|
    - # item is now direct reference to the elastic search _source
    - # item also contains item._type that displays the Elasticsearch type field
    %li=item.id

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

TODO

  • Setup facet generator