SearchFlip: Tutorial how to easily integrate Elasticsearch into your Rails project
If you aren’t familiar with Elasticsearch and want to integrate a full search in your Ruby on Rails project, this article will show you how easy it is.
Fortunately, now we have plenty of different gems and articles on how to integrate Elasticsearch in your project, but today I want to introduce you to the search_flip gem (https://github.com/mrkamel/search_flip) which offers a chainable DSL similar to ActiveRecord.
Create our simple Rails app
Generate a Rails project using a PostgreSQL database, but feel free to use whatever DB you prefer:
rails new kino_flip --database=postgresql
We are going to use the Open Movie Database as sample data for our movie project.
Let's create a model with migration and define some data that we're going to use for our search:
rails g model Movie name:string subtitle:string released_date:date rating_avg:float rating_count:integer
And run the migration:
rails db:migrate
We expect that all fields will be present, let's add a validation:
Now, let’s create seed data by parsing the OMDB CSV sample with 5000 movies and import to our movies
table.
Example of CSV (we only need certain parts of the data provided by OMDB):
Add the respective code for parsing and importing the CSV to seeds.rb
:
Now we have almost 5000 records in the movies
table.
Let's create a simple page where users can find the last 50 released
movies together with their ratings.
- Create
home_controller.rb
and addroutes to:'home#index'
toroutes.rb
- Add an
index
method toHomeController
3. Add the last_movies
scope to movie.rb
4. Display the movies via home/index.html.erb
template
Integrate Elasticsearch with the search_flip gem
Now we should add Elasticsearch to our project and integrate search_flip to make fast and full-text searches of movies.
Install Elasticsearch e.g. via brew:
Now if you go to the browser and type http://localhost:9200
and should see Elasticsearch running on your machine:
- Add
gem 'search_flip'
toGemfile
and runbundle install
- Create the file
config/initializers/search_flip.rb
and add settings:
3. Create folder indices
inside the app
directory and addmovie_index.rb
file there
In MovieIndex
we will keep all index settings and help Elasticsearch to understand which fields we want to put in the index.
There are at least 3 class methods you should define: index_name
to provide the name of the index in Elasticsearch, model
the name of the ActiveRecord model which we are going to use to import data or read the records and serialize
to specify the mapping between our model and index in ES.
Usually, real indices look much more complicated and have different names of mapping, setting etc. But our example is simple to just demonstrate the basic features of search_flip. You always can take a look at the last documentation in the search_flip repository.
4. Go to rails console
and manually import our movies to Elasticsearch.
That will import all movies using batches and Bulk API, so you don't need to worry about performance issues for now.
🎉 That's enough to start implementing full-text search in KinoFlip.
Add full-text search to KinoFlip
We already have a search bar on the top of our page, let's create a search_controller.rb
to communicate with Elasticsearch.
With MovieIndex.search('query')
we will make a query search for all fields in our index, which means we only need a single line of code to search by name, subtitle and release date. That's pretty cool, especially in our case.
You can always inspect the raw Elasticsearch request via MovieIndex.search('query').request
and it will return:
{:query=>{:bool=>{:must=>[{:query_string=>{:query=>”query”, :default_operator=>:AND}}]}}, :from=>0, :size=>30}
The method
MovieIndex.search('query').records
will return an array ofActiveRecord#Movie
objects.You can also use
MovieIndex.search('query').results
which returns an array ofSearchFlip::Result
and you can access the Elasticsearch result properties by using accessor methods e.g.result.name
. It's pretty awesome when you don’t need the AR object and it will improve the performance as the results are loaded from ES solely, i.e. it will not query the database.
MovieIndex.search('query').total_entries
returns the total count of search results.
Now we create our template in app/views/search/search.html.erb
And add to routes.rb
get ‘/search’, to: ‘search#search’
Let's try to search for "man" and see the result:
Congrats, we have our first full-text search results. By default, the results are sorted by Elasticsearch’s score value. But we still have a few problems: you can see only the first 30
results even though total_entries
says there’d be 184
results. Moreover, our idea of KinoFlip provides the best movies, so we should sort it by rating and define a range with a good rating. Let's implement it! With search_flip it's also pretty easy!
Add pagination
SearchFlip, for now, supports will_paginate and kaminari compatible pagination. For our example, we will use kaminari
Add to Gemfile
and run bundle install
:
gem 'kaminari'
And add to our query .page(params[:page).per(50)
And in our search.html.erb
template put the line:
It will return 50
records depending on which page you are right now.
Add sorting
We would like to show the top-rated movies on the top, so let’s sort it by rating_avg
and refactor our code to use of benefits of search_flip chainable DSL:
You can apply the sorting as you’re used to from ActiveRecord MovieIndex.search('query').sort(name: :asc)
. It's really cool!
Add a rating range
And the last feature we will add in this example: we want to return movies with a good rating only. For this, we need to provide a range in search_flip.
As you can use, that’s also pretty easy with seach_flip and similar to how you’d do it with ActiveRecord :
MovieIndex.search('query').where(rating_avg: 6..10)
I tried to keep this example of usage as simple as I can, but in a real project consider to use Query Object and specify all important settings like per page, rating range somewhere in Constants.
Sync your model with Elasticsearch
One problem is still left: if you try to add a new movie, you won't find it in our search, because there is no automatic synchronizing between our Movie
model and MovieIndex
index. For now, we use data in Elasticsearch, that we manually imported via rails console
. Let's fix it!
The only thing you need to add in movie.rb
to accomplish that is:
include SearchFlip::Model
notifies_index(MovieIndex)
If any changes happen (like an update, delete, create), search_flip will re-index the changed records.
Our final Movie
model looks like:
You will find all the tutorial source code of KinoFlip in my public repository https://github.com/vermaxik/kino_flip.
I hope this tutorial showed how easy it is to integrate Elasticsearch with search_flip in an application (and it's not necessary to be a Rails app).
If you have some questions feel free to ask via comments or on twitter.
Links
- https://github.com/mrkamel/search_flip (SearchFlip gem repository)
- https://github.com/vermaxik/kino_flip (KinoFlip repository containing the tutorial app)
- https://www.rubydoc.info/github/mrkamel/search_flip (SearchFlip documentation)
- https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html (Elasticsearch documentation)