Global Full-Text Search Experiences in Rails with AWS Elasticsearch

Image credits: Unsplash

The story comes to during project development that is company website and our API. In Rails app on each page, there is one specific search form as if it’s required to searching records particularly.

But it’s not good enough to visitors who have no time, no ideas to walk-through pages or have to look at the sitemap carefully, therefore, the global search functionality should be offered.

Prerequisites

My project is not complex in business processes but it have normalized to many data models. Before dive into implementation details, it should be introduced system characteristics:

Known Problems

With reading documentation from Searchkick homepage, you can get started to searching with Elasticsearch quickly.

But it’s not issue in a real project for me. The challenge come with “global” search meaning on scalable-content website that allows to searching across multiple data models, inconsistent data fields between models. And provides instant search/autocomplete on typing.

For example, it has to provides search on Product, FAQ, Event, Recruitment Information models. And matches to multiple fields of the model, show highlight, order to results with specific indices.

I will illustrate my solutions into snippets follow to steps:

Install Elasticsearch

There are two options:

For this article, I focus to using AWS Elasticsearch service.

Install gems and external JS libraries

Make sure you have searchkick and elasticsearch in your Gemfile

gem ‘elasticsearch’, ‘>= 1.0.15’
gem ‘searchkick’
gem ‘faraday_middleware-aws-signers-v4’

You will need faraday_middleware-aws-signers-v4 for generate signed requests to AWS services.

Start gems installation with $bundle install

Config Rails work with AWS Elasticsearch

In config/initializers/, you should create a elasticsearch_config.rb :

ENV['ELASTICSEARCH_URL'] = "https://es-domain-1234.us-east-1.es.amazonaws.com"
Searchkick.aws_credentials = {
access_key_id: ENV['AWS_ACCESS_KEY_ID'],
secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'],
region: ENV['AWS_REGION']
}

Setup searchable models

Next, you should add searchkick functionality into models you want to search, for instance:

class AromaCartridge < ApplicationRecord
searchkick

// your codes...
end

Also, you can define your own advanced search with Elasticsearch DSL from its documentation.

For make sure it work, you go to the rails console rails c

> AromaCartridge.reindex

and do simple query in commands:

> AromaCartridge.search "orange"
> get some results here...

Create Global Search Partial View

You should create a partial view of the search form which you can easy to include to front-end codes

<%= form_for :term, url: global_search_path, method: :get do |form| %>
<%= text_field_tag :term, params[:term],
placeholder: "#{t ".search"}",
autocomplete: :off,
autocorrect: :off,
autocapitalize: :off %>
<%= submit_tag "", class: 'hide' %>
<% end %>

Add Global Search Controller and Route

In order to processing requests from the view, we create a global search controller that includes search functionality.

// app/controllers/global_search_controller.rb
class GlobalSearchController < ApplicationController
def search
if params[:term].blank?
@results = []
else
@term = params[:term].to_s
@results = Searchkick.search(@term,
fields: [:title, :description],
indices_boost: { Recruitment => 1,
Event => 2,
Faq => 3,
// in order models },
index_name: [Faq,
Event,
Recuitment,
...other models])
end
rescue => e
puts "Have problems: #{e.message}"
end

then define a route of controller action following:

get 'search', to: 'global_search#search', as: 'global_search'

Search Results Page

A view page displays search results from the controller.

Something like:

<% if @results.blank? %>
<div class="search_results">
<p>Your search did not match any documents.</p>
</div>
<% else %>
<ul class="search_results">
<% @results.with_details.each do |result, details| %>
           <li>
// display attributes of the result.
// display highlight, e.g: details[:highlight]
</li>
        <% end %>
</ul>
<% end %>

Global Autocomplete Search

You will usually want more enhancements, making the search experience faster and easier. Therefore, workaround is instant search with autocomplete predicts what a user will type.

First, easy-autocomplete is a jQuery plugin, so you should create a new folder named by easy-autocompletethen put it inside [rootProject]/vendor/assets/*

Don’t forget to include it in application.js:

//= require jquery.easy-autocomplete

and yourapplication.scss:

*= require easy-autocomplete
*= require easy-autocomplete.themes

Then, add a route and implement controller action:

// app/controllers/global_search_controller.rb
class GlobalSearchController < ApplicationController
// ...
def autocomplete
@query = params[:query].to_s
@recruitments = Recuitment.search(@query,
fields: %w(title^5 description^5),
limit: 10,
load: false,
misspellings: { below: 5 }).map(&:title)
@events = Event.search(@query,
fields: %w(title^5 description^5),
limit: 10,
load: false,
misspellings: { below: 5 }).map(&:title)
@products = AromaProduct.search(@query,
fields: %w(title^5 description^5),
limit: 10,
load: false,
misspellings: { below: 5 }).map(&:title)
render json: { recruitments: @recruitments,
events: @events,
products: @products }
end
end

Next, you need define the element id in the previous search box:

<%= form_for :term, url: global_search_path, method: :get do |form| %>
<%= text_field_tag :term, params[:term],
id: "global-search-input"
placeholder: "#{t ".search"}",
autocomplete: :off,
autocorrect: :off,
autocapitalize: :off %>
<%= submit_tag "", class: 'hide' %>
<% end %>

And front-end JavaScript codes to manipulate JSON data:

// app/assets/javascript/global_search.js
var ready = function(){
var options = {
url: function(query){ return '/search/autocomplete?query=' + query },
categories: [
{ //Category products
listLocation: "products",
header: "-- <strong>Aromajoin Products</strong> --"
},
{ //Category events
listLocation: "events",
header: "-- <strong>Events</strong> --"
},
{ //Category recruitments
listLocation: "recruitments",
header: "-- <strong>Recuitments</strong> --"
}
],
list: {
onChooseEvent: function () {
var keywords = $('#global-search-input').getSelectedItemData();
window.location.href = '/search?utf8=✓&term=' + encodeURIComponent(keywords);
}
}
};
$('#global-search-input').easyAutocomplete(options);
}
$(document).ready(ready);

let’s go through these options, there’s a lot of them, so we’re going to run through these fairly quickly, but you can look at their documentation for how all of this works.

  • url: This is going to take an endpoint function, it will give us our phrase that we typed in, and we have to return a JSON string for /search/autocomplete
  • categories: an array of categories. Each category is going to have a list location. You can also add your header option, so you can have a title.
  • list: We want to be able to handle the clicks on those events, by passing a list option.

So, that is a little experiences on a full-text search functionality with AWS Elasticsearch service and Instant search/Autocomplete. I hope that it was useful to someone.