Elasticsearch is an external service that offers powerful searching features. You can build a simple search feature for your Rails App using standard SQL queries. However when it comes to advanced features such as working with AND, OR statements and ranked results it is better to rely on a industrial strength external solution. Search is often at the core of any CRUD Rails App.

Set-up

Set up Elasticsearch locally

  • Install Java and Elasticsearch via Brew

    $ brew update
    $ brew install Caskroom/cask/java
    $ brew install elasticsearch
    
  • To run Elasticsearch simply type $ elasticsearch in your Terminal.

By default Elasticsearch runs on localhost:9200. You can test it with $ curl -X GET http://localhost:9200/

Set up Elasticsearch with your Rails App in development and test environments

  • Install the following gems from this Elasticsearch Rails resource:

    gem 'elasticsearch-model'
    gem 'elasticsearch-rails'
    
  • Run $ bundle install

  • Go into rails console and create the index

$ rails console
Post.__elasticsearch__.create_index!
Post.__elasticsearch__.import

Set up Elasticsearch in production

  • If you use CircleCI for continuous deployment add elasticsearch to your circle.yml file.

    machine:
      ruby:
        version: 2.1.6
      services:
        - elasticsearch
    deployment:
      ...
    
  • On Heroku you can use the SearchBox add-on. Install it with

    $ heroku addons:create searchbox

If you have an extra app for staging remember to install it there as well ($ heroku addons:create searchbox --app staging-app-name)

  • On your Heroku environment you also have to create the index
$ heroku run rails console
Post.__elasticsearch__.create_index!
Post.__elasticsearch__.import
  • Create an initializer in your Rails app config/initializers/elasticsearch.rb

    Elasticsearch::Model.client =
      if Rails.env.staging? || Rails.env.production?
        Elasticsearch::Client.new url: ENV['SEARCHBOX_URL']
      elsif Rails.env.development?
        Elasticsearch::Client.new log: true
      else
        Elasticsearch::Client.new
      end
    
  • If you use RSpec add the following in your RSpec.configure block

    config.before(:each, elasticsearch: true) do
      Video.__elasticsearch__.create_index! force: true
    end
    

    You will also have to tag all your Elasticsearch related specs with :elasticsearch

    describe ".search", :elasticsearch do
      let(:refresh_index) do
        Post.import
        Post.__elasticsearch__.refresh_index!
      end
      # ...
    

Use

  • Index the model you want to search in rails console

    Post.__elasticsearch__.create_index!
    Post.__elasticsearch__.import
    
  • Include the Elastic libraries in your model

    # app/models/post.rb
    
    class Post < ActiveRecord::Base
      include Elasticsearch::Model
      include Elasticsearch::Model::Callbacks
    
      index_name ["app_name", Rails.env].join("_")
      # ...
    
  • Add a search method to the same model and parse the search as json for Elasticsearch. All objects in Elasticsearch are in json.

    # app/models/post.rb
    # ... 
    
    def self.search(query)
      search_definition = {
        query: {
          multi_match: {
            query: query, 
            fields: ["title", "description"],
            operator: "and"
          }
        }
      }
      __elasticsearch__.search(search_definition)
    end
    
    def as_indexed_json(options={})
      as_json(only: [:title, :description])
    end
    
  • Add a search method to your controller

    # app/controllers/posts_controller.rb
    # ...
    
    def advanced_search
      if params[:query]
        @posts = Post.search(params[:query]).records.to_a
      else
        @posts = []
      end
    end
    
  • In the view your search form can perform a simple get request

    # app/views/posts/advanced_search.html.haml
    
    %section.advanced_search.container
      %form.form-horizontal
      = form_tag advanced_search_posts_path, class: "form-horizontal", method: :get do
        %header
          %h1 Advanced Search
        .form-group
          .col-sm-6
            = search_field_tag :query, params[:query], placeholder: "Search posts by title, description", class: "form-control"
        .form-group
          .col-sm-6
            = button_tag(type: "submit", class: "btn btn-primary") do
              %span.glyphicon.glyphicon-search
              Search
    
  • Don’t forget to add a route for the search action

    # routes.rb
    # ... 
    
    resources :posts, only: [:show, :index] do
      collection do
        get :advanced_search, to: 'videos#advanced_search'
      end
      resources :comments, only: [:create]
    end
    
    # ...
    

The example above only scratches the surface of what you can do with Elasticsearch. To learn more check out the documentation.