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 theid
attribute is required.escapeMarkup
overrides the defaultescapeMarkup
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 classP
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 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.