Add Robust Search functionality in your Rails 4 app using Elasticsearch and Typeahead JS


Install ElasticSearch :
— For Ubuntu 14.04

Install Java Default Runtime & JDK

sudo apt-get install default-jre
sudo apt-get install default-jdk

Install ElasticSearch from Debian software package

wget https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-0.90.7.deb

dpkg -i elasticsearch-0.90.7.deb

Test ElasticSearch install

curl -X GET 'http://localhost:9200'

We will get a response like this:

{
  "ok" : true,
  "status" : 200,
  "name" : "Destroyer, The",
  "version" : {
    "number" : "0.90.7",
    "build_hash" : "36897d07dadcb70886db7f149e645ed3d44eb5f2",
    "build_timestamp" : "2013-11-13T12:06:54Z",
    "build_snapshot" : false,
    "lucene_version" : "4.5.1"
  },
  "tagline" : "You Know, for Search"
}

We will be using Searchkick – a ruby gem which works as a wrapper on top of ElasticSearch and provides robust searching as well as many other facilities.

Add Searchkick in your Gemfile

...
gem 'searchkick'
...

Add Searchkick in your Model

# app/models/book.rb

class Book < ActiveRecord::Base
  searchkick
end

We need to build the index for Book class so that already existing books from database are added into the index.

rake searchkick:reindex CLASS=Book

Now we can try Searchkick in our console

>> books = Book.search('rails').map(&:title)
["Rails Recipes", "Crafting Rails 4 Applications, 2nd Edition"]

Since Searchkick is working, lets integrate it in out index action of books controller

# app/controllers/books_controller.rb

def index
  if params[:query].present?
    @books = Book.search(params[:query])
  else
    @books = []
  end
end

Let’s add the Search form in the view

# app/views/books/index.html.erb
<div class='books-search'>
<h1>Search Books</h1>
<%= form_tag books_path, method: :get do %>
<div class='form-group'>
<%= text_field_tag :query, params[:query], class: 'form-control' %>
<%= submit_tag 'Search', class: 'btn btn-primary' %></div>
<% end %></div>

Now search functionality will be working.

Search engines are awesome because of their autocomplete feature. Now, lets integrate the Autocomplete functionality using Twitter Typeahead JS and Bloodhound Suggestion Engine in our app.

After downloading TypeaheadJS Bundle with BloodhoundJS we will add typeahead.bundle.min.js into our assets/javascripts/ folder and require it after jquery in our application.js file.

# app/assets/javascripts/application.js

//= require jquery
//= require jquery_ujs
//= require turbolinks
//= require bootstrap-sprockets
//= require typeahead.bundle.min
//= require_tree .

Now we need to specify autocomplete field in our Book model

# app/models/book.rb
class Book < ActiveRecord::Base
  searchkick word_start: [:title]
end

Lets rebuild the search index

rake searchkick:reindex CLASS=Book

Let’s add autocomplete action in our BooksController and change the route file

def autocomplete
  results = Book.search(params[:query], autocomplete: false, limit: 10).map do |book|
    { title: book.title, value: book.id }
    end
  render json: results
end
# config/routes.rb
resources :books do
  collection do
    get :autocomplete
  end
end

‘typeahead’ class name needs to be added in our text_field_tag

# app/views/books/index.html.erb
<div class='books-search'>
...
<%= text_field_tag :query, params[:query], class: 'form-control typeahead' %>
...</div>

Let’s add the bloodhound suggestion engine and initiate typeahead in a new js file and require it after typeahead.bundle.min.js in application.js

# app/assets/javascripts/books.js

var ready = function() {
  var engine = new Bloodhound({
    datumTokenizer: function(d) {
      return Bloodhound.tokenizers.whitespace(d.title);
    },
    queryTokenizer: Bloodhound.tokenizers.whitespace,
    remote: {
      url: '../books/autocomplete?query=%QUERY',
      wildcard: '%QUERY'
    }
  });

  var promise = engine.initialize();

  promise
    .done(function() { console.log('success!'); })
    .fail(function() { console.log('err!'); });

  $('.typeahead').typeahead(null, {
    name: 'engine',
    displayKey: 'title',
    source: engine.ttAdapter()
  });
}

$(document).ready(ready);
$(document).on('page:load', ready);
# app/assets/javascripts/application.js

//= require jquery
//= require jquery_ujs
//= require turbolinks
//= require bootstrap-sprockets
//= require typeahead.bundle.min
//= require books
//= require_tree .

Add a new file typeahead.css.scss and include it assets/stylesheets folder

.tt-dropdown-menu {
  width: 100%;
  min-width: 160px;
  margin-top: 2px;
  padding: 5px 0;
  background-color: #fff;
  border: 1px solid #ccc;
  border: 1px solid rgba(0, 0, 0, 0.2);
  *border-right-width: 2px;
  *border-bottom-width: 2px;
  -webkit-border-radius: 6px;
  -moz-border-radius: 6px;
  border-radius: 6px;
  -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
  -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
  box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
  -webkit-background-clip: padding-box;
  -moz-background-clip: padding;
  background-clip: padding-box;
}

.tt-suggestion {
  display: block;
  padding: 3px 20px;
}

.tt-suggestion.tt-is-under-cursor {
  color: #fff;
  background-color: #0081c2;
  background-image: -moz-linear-gradient(top, #0088cc, #0077b3);
  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3));
  background-image: -webkit-linear-gradient(top, #0088cc, #0077b3);
  background-image: -o-linear-gradient(top, #0088cc, #0077b3);
  background-image: linear-gradient(to bottom, #0088cc, #0077b3);
  background-repeat: repeat-x;
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0);
}

.tt-suggestion.tt-is-under-cursor a {
  color: #fff;
}

.tt-suggestion p {
  margin: 0;
}

.books-search {
  .twitter-typeahead {
    width: 100%;
  }

  .btn {
    vertical-align: top;
  }
}

You are done!

Here is the github repository if you missed anything compare your code with mine. 🙂

21 responses to “Add Robust Search functionality in your Rails 4 app using Elasticsearch and Typeahead JS”

  1. Hi the post is quite clear but i didnt get how you downloaded TypeaheadJS.
    Did you use Bower?
    or use a gem?

  2. Ok i am using the above js file now.
    I was checking your project on Github however it doesnt seem to be latest since it doesnt have this file-> app/assets/javascripts/books.js.
    Thanks.

    1. Thanks! I will update my github repo or you can send me a PR if you want to contribute? 🙂

    2. You need to specifically add the wildcard in newer versions of typeahead:
      e.g.:

      remote: {
      url: ‘../books/autocomplete?query=%QUERY’,
      wildcard: ‘%QUERY’
      }

  3. Hi Sharvy,alls good in development mode,but when im pushing to heroku im facing several problems.
    If you have done this can you document it?

    1. Hi Rnjai, are you using Heroku free account, if yes then you should know, in free mode you can not use ElasticSearch. If you go through the Heroku documentation, you will see that.

  4. Abderrahmenn Avatar
    Abderrahmenn

    Hi Sharvy, good job for this tutorial. But it doesn’t work for autocomplete. Can you help me please? thanks for advance.

  5. Awesome tutorial, Worked like a charm! For those who are struggling, make sure to ‘require book.js’ in your application.js file.

  6. hey man, really enjoyed this. so i got search working fine buy autocomplete is doing nothing. i doubled checked the code twice. any ideas?

    1. Hi Richie, have you required books.js in application.js? I have updated my post.

  7. Anyone else getting malformed request?

    1. What kind of malformed request?

  8. Sounds like a great tutorial! I’ll try to figure out how this could work without elasticsearch because i don’t need it 😉 I’m unsure what to do at the point where you add searchkick autocomplete: [‘title’] to your model?!

  9. HI Sharvy, nice post! I was able to get searchkick working. But after downloading typeahead js files and including the autocomplete path inside the model, when I try to rebuild the search Index (rake searchkick:reindex CLASS=City) , I get in my console the message : “Could not find class: City”

    any idea? the rake command works when i remove the autocomplete part…

    1. Did you try City.reindex ?

      1. Hi .. sorry the issue was because i had not included bootstrap-sprockets inside my application.js

        I got it reindexed .. but now when i add autocomplete: [‘name’] inside my model, i get an error when i load my page :

        “Unknown keyword : autocomplete”

      2. Hi Sharvy, I did check and they both match (i Use the path “//= require cities” instead of books, as i’m adding search for cities)
        I am not sure why only autocomplete is not working. searchkick works. but adding autocomplete[‘name’] to the model throws this same error : “unknown keywords : autocomplete”

        I have copied into my app/assets/javascripts folder the files typeahead.bundle.js, typeahead.bundle.min.js, typeahead.jquery.js, bloodhound.js

        I’ve also created the file app/assets/javascripts/cities.js, by replacing the ‘title’ field with ‘name’

        It must have something to do with copying the typeahead files or linking them to the js file.. but am not sure what’s going wrong 😦

Leave a comment