Implementing Multi-Table Full Text Search with Postgres in Rails

This post originally published on thoughtbot's Giant Robots blog.

Easily searching across an application's data is a pervasive need. If you are lucky, you can get away with simple sorting or searching on a single column, but it is more likely that you need full text search across multiple models, all from a single search field.

There are many standalone services, some hosted and some not, offering full text search. We looked at a few of these, but decided that adding too much to our classes or having an external service running during tests weren't things we wanted for this project.

Thanks to the power of Postgres' full text search, rolling your own search isn't too difficult.

If all you need is to search over a few models' text and string fields, this approach is probably the simplest thing you can do.

SQL Changes

We'll need to construct a database view which presents a polymorphic relationship to the individual result and the text column being searched.


  SELECT AS searchable_id,
    'Status' AS searchable_type,
    comments.body AS term
  FROM statuses
  JOIN comments ON = comments.status_id


  SELECT AS searchable_id,
    'Status' AS searchable_type,
    statuses.body AS term
  FROM statuses


  SELECT AS searchable_id,
    'User' AS searchable_type, AS term
  FROM users

From here, we add gin indices to the columns on which we are searching. In our case similar indices to these made the difference between a 3-5 second lookup and ~100ms.

CREATE INDEX index_statuses_on_body ON statuses USING gin(to_tsvector('english', body));
CREATE INDEX index_comments_on_body ON comments USING gin(to_tsvector('english', body));
CREATE INDEX index_users_on_name ON users USING gin(to_tsvector('english', name));


Aaron Patterson's Textacular is the only non-standard dependency we'll introduce here:

gem 'textacular'

Textacular will manage searching over all text and varchar columns.

We follow Rails' conventions in our database view, which makes hooking a model up to it as simple as any table-backed model. The Search class below automatically hooks into the searches view we created.

Luckily, ActiveRecord already presents us with a solution to polymorphic associations in the form of the <ASSOCIATION>_id and <ASSOCIATION>_type columns.

All we have to do is tell Search about its searchable relationship, define the results method to perform the search, and extend Textacular in the model.

class Search < ActiveRecord::Base
  extend Textacular

  belongs_to :searchable, polymorphic: true

  def results
    if @query.present?!(&:searchable).uniq

The call to preload is used instead of include because Rails can't include polymorphic associations. Calling preload still loads the related models in as few SQL statements as possible, but does not allow for querying on the related models. This isn't something we need since we immediately map to searchable.

Since we could potentially get a result for a model multiple times, for example if the same term appeared in a status and a comment, we also call uniq.

The interface for Search look like this: 'books').results.


  # config/application.rb
  config.active_record.schema_format = :sql


Overall, we've enjoyed working with this method of full text search. Because it's awkward to change the view we'd recommend not approaching the search too iteratively; add the tables you know you'll want to search up front and save yourself some headache.

Written by and .