spodlecki/elasticsearch-facetedsearch

View on GitHub
README.md

Summary

Maintainability
Test Coverage
# Elasticsearch::FacetedSearch

[![Code Climate](https://codeclimate.com/github/spodlecki/elasticsearch-facetedsearch/badges/gpa.svg)](https://codeclimate.com/github/spodlecki/elasticsearch-facetedsearch)
[![Test Coverage](https://codeclimate.com/github/spodlecki/elasticsearch-facetedsearch/badges/coverage.svg)](https://codeclimate.com/github/spodlecki/elasticsearch-facetedsearch/coverage)
[![Build Status](https://travis-ci.org/spodlecki/elasticsearch-facetedsearch.svg)](https://travis-ci.org/spodlecki/elasticsearch-facetedsearch)

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

- [Elasticsearch::Model](https://github.com/elastic/elasticsearch-rails/tree/master/elasticsearch-model)
- [Rails > 3.2.8](http://rubyonrails.org/)
- [Elasticsearch Server > 1.0.1](http://www.elastic.co)

## 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