Easy multi-table full-text search with Rails

Jun 20, 2014 · 5 min read
rails
activerecord
postgresql

In my new personal project I've been working for the past months I wanted the user to have the ability to search for certain records of a given model across multiple of it's fields and relations using a simple form with just one text field. This is known as full-text search and there are many standalone services you can use to handle it such as Elasticsearch, Sphinx or Solr, but I didn't wanted to add and additional complexity layer to the project so I decided to choose PostgreSQL as my database of choice and take advantage of it's power.

The goal

So imagine we have a model named Person which looks like this:

# == Schema Information
#
# Table name: people
#
#  id                      :integer          not null, primary key
#  first_name              :string(255)
#  last_name               :string(255)
#  location                :string(255)
#  headline                :string(255)
#  summary                 :text

class Person < ActiveRecord::Base
  validates :first_name, presence: true
end

We also want to add this Person a couple of tag lists for interests and skills. To achieve this I decided to use the ActsAsTaggableOn gem which I have already used before and I like it because of it's simplicity of use. So we add it to our gemfile:

gem 'acts-as-taggable-on'

After running bundle install we have to create the needed migrations to create the needed tables for the gem to work properly:

rake acts_as_taggable_on_engine:install:migrations
rake db:migrate

Now we can add the desired tag contexts to our Person model:

# == Schema Information
#
# Table name: people
#
#  id                      :integer          not null, primary key
#  first_name              :string(255)
#  last_name               :string(255)
#  location                :string(255)
#  headline                :string(255)
#  summary                 :text

class Person < ActiveRecord::Base
  acts_as_taggable_on :skills, :interests
  
  validates :first_name, presence: true
end

By adding this acts_as_taggable_on we are providing the model with some very useful functionality to manage it's tags lists (for skills and interests), so if we want to search for people with a certain skill we can do the following:

Person.tagged_with(["surf", "longboard"]), on: :interests

And it will return all the people with those interests in common. Take a look at the gem's documentation to learn more about the wide range of methods and helpers it offers us.

At first this could be enough to find people by their skills or interests, but what if someone didn't add a skill or interests in those fields but he did it in his headline or summary? Or what if the user wants to search also for other people living in a certain place with similar skills or interests by using a simple form with a single text input? Well, here is where full-text search comes into play.

The solution

There are many ways activating and taking advantage of PostgreSQL's full-text search but I think that using the pg_search gem it is indeed one the easiest ones. First, add it to you gemfile and run the corresponding bundle:

gem 'pg_search'

The gem offers us two different techniques for searching, multi-search and search-scopes. As I just wanted to search in a specific model I chose to implement the search scopes approach.


# == Schema Information
#
# Table name: people
#
#  id                      :integer          not null, primary key
#  first_name              :string(255)
#  last_name               :string(255)
#  location                :string(255)
#  headline                :string(255)
#  summary                 :text

class Person < ActiveRecord::Base
  acts_as_taggable_on :skills, :interests
  
  pg_search_scope :quick_search, against: [:headline, :location, :summary]
  
  validates :first_name, presence: true
end

So we are adding a new scope named quick_search which will perform a full-text search within the headline, location and summary fields:


search_text = 'spain rails surf'
@people = Person.quick_search(search_text)

But how can we make the scope to search also inside a person's skills and interests to see if any of them matches? As easy as pie:


# == Schema Information
#
# Table name: people
#
#  id                      :integer          not null, primary key
#  first_name              :string(255)
#  last_name               :string(255)
#  location                :string(255)
#  headline                :string(255)
#  summary                 :text

class Person < ActiveRecord::Base
  acts_as_taggable_on :skills, :interests
  
  pg_search_scope :quick_search, 
                  against: [:headline, :location, :summary],
                  associated_against: {
                    skills: [:name],
                    interests: [:name]
                  }
  
  validates :first_name, presence: true
end

Just adding the associated_against parameter and specifying which relations you want to include and in which of their columns to search.

Final words

Though pg_search full-text search is very powerful, this is for sure the most simple example of what can be done. So go ahead and check the pg_search documentation and give your next project the power of PostgreSQL's full-text search.

Happy coding!