Search and Autocomplete in Rails Apps

Share this article

Adding search to Rails apps
Adding search to Rails apps

Searching is one of the most common features found on virtually any website. There are numerous solutions out there for easily incorporating search into your application, but in this article I’ll discuss Postgres’ native search in Rails applications powered by the pg_search gem. On top of that, I’ll show you how to add an autocomplete feature with the help of the select2 plugin.

I’ll explore three examples of employing search and autocomplete features in Rails applications. Specifically, this article covers:

  • building a basic search feature
  • discussing additional options supported by pg_search
  • building autocomplete functionality to display matched user names
  • using a third-party service to query for geographical locations based on the user’s input and powering this feature with autocomplete.

The source code can be found at GitHub.

Getting Started

Go ahead and create a new Rails application. I’ll be using Rails 5.0.1, but most of the concepts explained in this article apply to older versions as well. As long as we’re going to use Postgres’ search, the app should be initialized with the PostgreSQL database adapter:

rails new Autocomplete --database=postgresql

Create a new PG database and setup config/database.yml properly. To exclude my Postgres username and password from the version control system, I’m using the dotenv-rails gem:

Gemfile

# ...
group :development do
  gem 'dotenv-rails'
end

To install it, run the following:

$ bundle install

and create a file in the project’s root:

.env

PG_USER: 'user'
PG_PASS: 'password'

Then exclude this file from version control:

.gitignore

.env

Your database configuration may look like this:

config/database.yml

development:
  adapter: postgresql
  database: autocomplete
  host: localhost
  user: < %= ENV['PG_USER'] %>
  password: < %= ENV['PG_PASS'] %>

Now let’s create a table and populate it with sample data. Rather than invent anything complex here, I’ll simply generate a users table with name and surname columns:

$ rails g model User name:string surname:string
$ rails db:migrate

Our sample users should have distinct names so that we can test the search feature. So I’ll use the Faker gem:

Gemfile

# ...
group :development do
  gem 'faker'
end

Install it by running this:

$ bundle install

Then tweak the seeds.rb file to create 50 users with random names and surnames:

db/seeds.rb

50.times do
  User.create({name: Faker::Name.first_name,
              surname: Faker::Name.last_name})
end

Run the script:

$ rails db:seed

Lastly, introduce a root route, controller with the corresponding action and a view. For now, it will only display all the users:

config/routes.rb

# ...
resources :users, only: [:index]
root to: 'users#index'

users_controller.rb

class UsersController < ApplicationController
  def index
    @users = User.all
  end
end

views/users/index.html.erb

<ul>
  < %= render @users %>
</ul>

views/users/_user.html.erb

<li>
  < %= user.name %> < %= user.surname %>
</li>

That’s it; all preparations are done! Now we can proceed to the next section and add a search feature to the app.

Searching for Users

What I’d like to do is display a search field at the top of the root page saying “Enter user’s name or surname” with a “Search!” button. When the form is submitted, only the users matching the entered term should be displayed.

Start off with the form:

views/users/index.html.erb

< %= form_tag users_path, method: :get do %>
  < %= text_field_tag 'term', params[:term], placeholder: "Enter user's name or surname" %>
  < %= submit_tag 'Search!' %>
< % end %>

Now we need to grab the entered term and do the actual search. That’s when the pg_search gem comes into play:

Gemfile

# ...
gem 'pg_search'

Don’t forget to install it:

$ bundle install

Pg_search supports two modes: multisearchable and pg_search_scope. In this article, I’ll use the latter option (multisearchable supports the same options, but allows global search to be crafted on multiple models). The idea is quite simple: we create a search scope that’s similar to ActiveRecord scopes. It has a name and a variety of options, like what fields to perform search against. Before doing that, the PgSearch module must be included in the model:

models/user.rb

# ...
include PgSearch
pg_search_scope :search_by_full_name, against: [:name, :surname]

In this example, the :name and :surname columns will be used for searching.

Now tweak the controller’s action and utilize the new search scope:

users_controller.rb

# ...
if params[:term]
  @users = User.search_by_full_name(params[:term])
else
  @users = User.all
end

The view itself doesn’t require any additional changes. You can now boot the server, visit the root page and enter a name or a surname of some user, and everything should be working properly.

Additional Options

Our searching works, but it’s not very convenient. For example, if I enter only a part of the name or a surname, it won’t return any results. This can be easily fixed by providing additional parameters.

First of all, you need to choose which searching technique to employ. The default one is Postgres’ full text search, which I’ll use in this article. Two others are trigram and metaphone searches, which require additional PG extensions to be installed. In order to set the technique, employ the :using option:

pg_search_scope :search_by_full_name, against: [:name, :surname], using: [:tsearch]

Each technique also accepts various options. For example, let’s enable searching for partial words:

models/user.rb

class User < ApplicationRecord
  include PgSearch
  pg_search_scope :search_by_full_name, against: [:name, :surname],
    using: {
      tsearch: {
        prefix: true
      }
    }

Now if you enter “jay” as a search term, you’ll see all users whose full name contains “jay”. Note that this feature is only supported starting from Postgres version 8.4.

Another interesting option is :negation, which allows exclusion criteria to be provided by adding the ! symbol. For example, Bob !Jones. Enable this option as follows:

models/user.rb

class User < ApplicationRecord
  include PgSearch
  pg_search_scope :search_by_full_name, against: [:name, :surname],
    using: {
      tsearch: {
        prefix: true,
        negation: true
      }
    }

Note, however, that sometimes this feature may provide unexpected results. (Read more here.)

It would be also cool to visually highlight the found match somehow. This is possible with :highlight option:

models/user.rb

# ...
include PgSearch
pg_search_scope :search_by_full_name, against: [:name, :surname],
  using: {
    tsearch: {
      prefix: true,
      negation: true,
      highlight: {
        start_sel: '<b>',
        stop_sel: '</b>',
      }
    }
  }

Here we are saying that the selection should be wrapped with the b tags. The highlight feature has some other options available, which can be seen here.

In order for the highlighting feature to work, we need to make two more changes. First, chain the with_pg_search_highlight method like this:

users_controller.rb

# ...
@users = User.search_by_full_name(params[:term]).with_pg_search_highlight

Second, tweak the partial:

views/users/_user.html.erb

<li>
  < % if user.respond_to?(:pg_search_highlight) %>
    < %= user.pg_search_highlight.html_safe %>
  < % else %>
    < %= user.name %> < %= user.surname %>
  < % end %>
</li>

The pg_search_highlight method returns the concatenated values of our search fields (:name and :surname) and highlights the found match.

Adding an Autocomplete Feature

Another popular feature found on many sites is autocomplete. Let’s see how it can be crafted with pg_search and the select2 plugin. Suppose we have a “Send message” page where users can select whom to write to. We won’t actually build the messaging logic, but if you’re interested in how this can be done, read my article Build a Messaging System with Rails and ActionCable.

Add two gems into the Gemfile:

Gemfile

# ...
gem 'select2-rails'
gem 'underscore-rails'

Select2 is a plugin to turn generic select fields into beautiful and powerful controls, whereas Underscore.js will provide some convenient methods. Hook the necessary files:

javascripts/application.js

//= require underscore
//= require select2
//= require messages

The messages.coffee file will be created in a moment. Select2 also requires some styles to function properly:

stylesheets/application.scss

@import 'select2';

Now let’s quickly add a new route, controller and view:

config/routes.rb

# ...
resources :messages, only: [:new]

messages_controller.rb

class MessagesController < ApplicationController
  def new
  end
end

views/messages/new.html.erb

<%= form_tag '' do %>
  < %= label_tag 'to' %>
  < %= select_tag 'to', nil, style: 'width: 100%' %>
< % end %>

The form is not finished, because it won’t be sent anywhere. Note that initially the select field doesn’t have any values; they’ll be displayed dynamically based on the user’s input.

Now the CoffeeScript:

javascripts/messages.coffee

jQuery(document).on 'turbolinks:load', ->
  $('#to').select2
    ajax: {
      url: '/users'
      data: (params) ->
        {
          term: params.term
        }
      dataType: 'json'
      delay: 500
      processResults: (data, params) ->
        {
          results: _.map(data, (el) ->
            {
              id: el.id
              name: "#{el.surname}, #{el.name}"
            }
          )
        }
      cache: true
    }
    escapeMarkup: (markup) -> markup
    minimumInputLength: 2
    templateResult: (item) -> item.name
    templateSelection: (item) -> item.name

I won’t dive into all the details explaining what this code does, as this article is not devoted to Select2 and JavaScript in general. However, let’s quickly review the main parts:

  • ajax says that the entered data should be asynchronously sent to the /users URL awaiting JSON in response (dataType).
  • delay: 500 means that the request will be delayed to 0.5 seconds after a user has finished typing.
  • processResults explains how to process the data received from the server. I’m constructing an array of objects containing users’ ids and full names. Note that the id attribute is required.
  • escapeMarkup overrides the default escapeMarkup function to prevent any escaping from happening.
  • minimumInputLength stipulates that a user should enter at least 2 symbols.
  • templateResult defines the look of the options displayed in dropdown.
  • templateSelection defines the look of the selected option.

The next step is to tweak our users#index action allowing it to respond with JSON:

users_controller.rb

# ...
def index
  respond_to do |format|
    if params[:term]
      @users = User.search_by_full_name(params[:term]).with_pg_search_highlight
    else
      @users = User.all
    end
    format.json
    format.html
  end
end

Lastly, the view:

views/users/index.json.jbuilder

json.array! @users do |user|
  json.id user.id
  json.name user.name
  json.surname user.surname
end

Note that in order for this to work, the jBuilder gem is needed. For Rails 5 apps, it’s present in the Gemfile by default.

So far, so good, but can’t we do a bit better? Remember that highlight feature that was added in the previous section? Let’s employ it here as well! What I want to do is highlight the found match in the dropdown, but not when the option is already selected. Therefore, we’ll need to keep the name and surname fields in the JSON response and also introduce the full_name field:

views/users/index.json.jbuilder

json.array! @users do |user|
  json.id user.id
  json.full_name user.pg_search_highlight.html_safe
  json.name user.name
  json.surname user.surname
end

Now tweak the CoffeeScript changing the processResults to the following:

processResults: (data, params) ->
  {
    results: _.map(data, (el) ->
      {
        name_highlight: el.full_name
        id: el.id
        name: "#{el.surname}, #{el.name}"
      }
    )
  }

Also change templateResult to this:

templateResult: (item) -> item.name_highlight

Note that if you don’t override the escapeMarkup function, all output will be escaped and the b tags used for highlighting will be rendered as plain text.

That’s it. Feel free to play around with this feature and extend it further!

Autocomplete with a Third-party Service

Students sometimes ask me how to build a field with an autocomplete feature that allows users to select their location. As a bonus, I’m going to show you one of the possible solutions in this section. In order to search for cities, I’ll use geonames.org, a free service that has information about virtually anything: big cities and small towns, countries, postal codes, various historical locations etc. All you need to do in order to start using it is register and enable API access inside your profile.

Start off with adding a new route, controller and view:

config/routes.rb

# ...
resources :cities, only: [:index]

cities_controller.rb

class CitiesController < ApplicationController
  def index
  end
end

views/cities/index.html.erb

<%= form_tag '' do %>
  < %= label_tag 'city' %>
  < %= select_tag 'city', '', style: 'width: 100%;' %>
< % end %>

The CoffeeScript code will be very similar to the code for adding the autocomplete feature for the messaging page:

cities.coffee

template = (item) -> item.text

jQuery(document).on 'turbolinks:load', ->
  $('#city').select2
    ajax: {
      url: 'http://ws.geonames.org/searchJSON'
      data: (params) ->
        {
          name: params.term,
          username: 'your_name',
          featureClass: 'p',
          cities: 'cities1000'
        }
      dataType: 'json'
      delay: 500
      processResults: (data, params) ->
        {
          results: _.map(data.geonames, (el) ->
            name = el.name + ' (' + el.adminName1 + ', ' + el.countryName + ')'
            {
              text: name
              id: name
            }
          )
        }
      cache: true
    }
    escapeMarkup: (markup) -> markup
    minimumInputLength: 2
    templateResult: template
    templateSelection: template

The three parts that have changes are url, data and processResults. As for url, we’re using the searchJSON API method. This method supports various parameters to define what objects, and of what type, we want to search for. Their options are passed inside the data attribute:

  • name is a required parameter that can contain the place’s full or partial name.
  • username indicates your GeoNames account.
  • featureClass: as long as GeoNames has a huge database with various geographical objects, we want to narrow the searching scope. Feature class P means that we wish to search only for cities, towns and villages. Other feature classes can be found here.
  • cities: I’ve limited the search only to cities populated with at least 1000 people. This is done because there are too many small towns and villages with identical names. For example, I was able to find at least four places named “Moscow”.

In the processResults we take the returned geonames and construct the string that will be displayed in the dropdown. For each city we display its name, area and country.

That’s pretty much it, so now you can observe this feature in action!

Conclusion

In this article, we have seen the pg_search gem and select2 plugins in action. With their help, we constructed search functionality and added an autocomplete feature into our application, making it a bit more user friendly. Pg_search has a lot more more exciting stuff to offer, so be sure to browse its docs.

What searching solutions do you use? What are their key features? Share your experience in the comments!

Frequently Asked Questions (FAQs) about Autocomplete in Rails Apps

How can I implement autocomplete in a Rails application using Webpack?

Webpack is a popular module bundler for JavaScript applications, and it can be used to implement autocomplete in a Rails application. First, you need to install the necessary packages such as ‘jquery-ui’ and ‘webpacker’. Then, you can create a new JavaScript file where you will write the code for the autocomplete feature. You can use the ‘autocomplete’ method provided by jQuery UI and pass the source data as an array. Finally, you need to import this JavaScript file in your application.js file and ensure that Webpack is correctly configured in your Rails application.

What are some good resources for autocomplete in Rails development?

There are several resources available online for learning about autocomplete in Rails development. Some of the most popular ones include the official Rails Guides, Stack Overflow, and GitHub. These platforms provide a wealth of information, from basic tutorials to advanced topics, and you can also find many examples and sample code snippets that you can use in your own projects.

How can I add autocomplete suggestions to an input field in a Rails application?

Adding autocomplete suggestions to an input field in a Rails application involves several steps. First, you need to create a new controller action that will handle the autocomplete requests. This action should return a JSON response with the suggestions. Then, you need to add a route for this action in your routes.rb file. Finally, you can use JavaScript (or a JavaScript library like jQuery UI) to add the autocomplete functionality to the input field. When the user starts typing in the field, a request is sent to the autocomplete action, and the suggestions are displayed in a dropdown menu.

How can I use the ‘rails-autocomplete’ gem in my Rails application?

The ‘rails-autocomplete’ gem is a powerful tool for adding autocomplete functionality to your Rails application. To use it, you first need to add it to your Gemfile and run the ‘bundle install’ command. Then, you can use the ‘autocomplete’ method provided by the gem in your controller to specify which model and field should be used for the autocomplete suggestions. Finally, you need to add the necessary JavaScript code to your views to enable the autocomplete feature.

How can I implement autocomplete for a form association in a Rails application?

Implementing autocomplete for a form association in a Rails application can be done using JavaScript or a JavaScript library like jQuery UI. First, you need to create a new controller action that will return the autocomplete suggestions as a JSON response. This action should query the associated model based on the user’s input. Then, you can use JavaScript to add the autocomplete functionality to the form field. When the user starts typing in the field, a request is sent to the autocomplete action, and the suggestions are displayed in a dropdown menu.

How can I customize the appearance of the autocomplete dropdown menu in a Rails application?

The appearance of the autocomplete dropdown menu in a Rails application can be customized using CSS. You can style the dropdown menu itself, as well as the individual suggestions. For example, you can change the background color, font size, and padding of the suggestions. You can also add hover effects to highlight the suggestion that the user is currently hovering over.

How can I handle multiple autocomplete fields in a single form in a Rails application?

Handling multiple autocomplete fields in a single form in a Rails application requires a bit more setup. You need to create a separate controller action for each autocomplete field, and each action should return a JSON response with the appropriate suggestions. Then, you can use JavaScript to add the autocomplete functionality to each field. Each field should send a request to its corresponding autocomplete action when the user starts typing.

How can I improve the performance of the autocomplete feature in a Rails application?

There are several ways to improve the performance of the autocomplete feature in a Rails application. One way is to limit the number of suggestions that are returned by the autocomplete action. This can reduce the amount of data that needs to be transferred and processed. Another way is to add a delay to the autocomplete feature, so that a request is only sent after the user has stopped typing for a certain amount of time. This can reduce the number of unnecessary requests.

How can I test the autocomplete feature in a Rails application?

Testing the autocomplete feature in a Rails application can be done using a combination of unit tests and integration tests. Unit tests can be used to test the controller action that returns the autocomplete suggestions. You can check that the action returns the correct suggestions based on the input. Integration tests can be used to test the autocomplete feature from the user’s perspective. You can simulate user input and check that the correct suggestions are displayed in the dropdown menu.

How can I handle errors in the autocomplete feature in a Rails application?

Handling errors in the autocomplete feature in a Rails application involves two steps. First, you need to handle any errors that occur on the server side, such as database errors. This can be done in the controller action that returns the autocomplete suggestions. You can use a rescue block to catch any exceptions and return an error message as a JSON response. Second, you need to handle any errors that occur on the client side, such as network errors. This can be done in the JavaScript code that implements the autocomplete feature. You can use a callback function to handle any errors that occur during the AJAX request.

Ilya Bodrov-KrukowskiIlya Bodrov-Krukowski
View Author

Ilya Bodrov is personal IT teacher, a senior engineer working at Campaigner LLC, author and teaching assistant at Sitepoint and lecturer at Moscow Aviations Institute. His primary programming languages are Ruby (with Rails) and JavaScript. He enjoys coding, teaching people and learning new things. Ilya also has some Cisco and Microsoft certificates and was working as a tutor in an educational center for a couple of years. In his free time he tweets, writes posts for his website, participates in OpenSource projects, goes in for sports and plays music.

appautocompletegeolocationrailsRails AppRalphMsearch
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week
Loading form