Skip to Content
Course content

208: Full-Text Search with pg_search

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Look, we've all been there. You're building a feature, and the requirement is simple: "The user needs to be able to search for something." Your first instinct is probably to throw a WHERE title LIKE '%query%' at the database and call it a day. For a few hundred records, that's fine. But as soon as you're dealing with thousands of rows or multiple columns, that approach falls apart. It's slow, it doesn't handle stemming (searching for "run" won't find "running"), and it's frankly a blunt instrument.

I've got a project here—a recipe manager—and I want to be able to search for recipes by their title, their description, and the ingredients list. Let's see what happens when I try to do this the "naive" way first.

The frustration of the manual query

# In my Recipe controller
def index
  @recipes = Recipe.where("title ILIKE ? OR description ILIKE ?", "%#{params[:q]}%", "%#{params[:q]}%")
end

It works, technically. But it's clunky. If I add a third column, the query gets longer. If I want to prioritize results where the keyword is in the title over the description, I'm basically out of luck without writing some nightmare of a SQL CASE statement. Plus, PostgreSQL is just sitting there with powerful full-text search (FTS) capabilities that I'm completely ignoring.

Enter pg_search. It's a wrapper that makes Postgres's FTS features feel like native Ruby. Let's pull it in and see if we can clean this up.

Setting up the basic scope

After adding the gem and running the bundle, I'll move the search logic out of the controller and into the model where it belongs. I'll use pg_search_scope because it gives me a reusable scope I can chain with other queries.

class Recipe < ApplicationRecord
  include PgSearch::Model

  pg_search_scope :search_by_text, 
                  against: [:title, :description]
end

Now, in my controller, I can just do Recipe.search_by_text(params[:q]). This is already cleaner, but I noticed something during testing. If I search for "Chocolate Cake," and I have a recipe titled "Chocolate Cake" and another description that mentions "chocolate" and "cake" in two different sentences, they're both showing up. But they aren't being sorted by relevance. The "perfect match" isn't necessarily at the top.

Tuning the relevance with weights

This is where I usually start scratching my head until I remember that pg_search allows for weighting. I want a match in the title to be significantly more important than a match in the description. In Postgres, we use weights (A, B, C, D), where A is the highest.

Let's adjust the model to tell the database that the title is the priority:

class Recipe < ApplicationRecord
  include PgSearch::Model

  pg_search_scope :search_by_text, 
                  against: {
                    title: 'A',
                    description: 'B'
                  }
end

Now, when I run a search for "Sourdough," the recipe actually titled "Sourdough Bread" jumps to the top, while the "Daily Bread" recipe that just mentions sourdough in the description drops down. That feels much more like a professional search experience.

Handling the 'No Results' awkwardness

I tried searching for "Zucchini" and got exactly what I expected: nothing. But as I keep playing with the search bar, I realize that the default behavior is very strict. If I typo "Zuccini", I get zero results. In a real app, users typo things constantly.

I'll add using: { tsearch: { prefix: true } } to the scope. This allows for prefix matching, so searching "Zucc" will find "Zucchini". It's a small tweak, but it makes the search feel "softer" and more forgiving.

pg_search_scope :search_by_text, 
                  against: {
                    title: 'A',
                    description: 'B'
                  },
                  using: {
                    tsearch: { prefix: true }
                  }
end

One final thought: keep in mind that pg_search is leaning heavily on PostgreSQL. If you're using SQLite for development and Postgres for production, this code will blow up in your dev environment. I always make sure my local environment mirrors production exactly when I'm touching FTS features, otherwise, you'll spend an hour debugging a "method not found" error that only exists on your laptop.




📋 Practical Task

Building a Weighted Book Catalog Search

You are tasked with adding a search feature to a Book model. The Book model has three attributes: title, author, and summary.

  • Install and integrate the pg_search gem.
  • Create a pg_search_scope named search_books.
  • Configure the weights so that a match in the title is the most important (Weight A), the author is second (Weight B), and the summary is the least important (Weight C).
  • Enable prefix matching so that searching for "Hem" returns "Hemingway".
  • Test your scope in the Rails console by searching for a term that appears in both a title and a summary to verify the title result ranks higher.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.