Rails with full-text Search

Rails with full-text Search

Rails with full-text Search

Your no-BS weekly brief on software engineering.
Join 100,000+ developers

When building a search feature for your application, integrating full-text search functionality can be challenging. With the pg_search gem, however, you can add powerful and flexible search capabilities with minimal effort. Let’s dive into an example of how we use pg_search to create a robust search scope and explore what it would look like if you didn’t have this gem in your toolbox.

Defining a Search Scope with pg_search

Here’s how we define a search scope in our Rails model using pg_search:

pg_search_scope :search,
  against: {
    title: 'A',
    excerpt: 'B',
    summary: 'C'
  },
  using: {
    tsearch: {
      prefix: true,
      dictionary: "english",
      any_word: true
    }
  }

Capabilities Explained:

Ranked Search: Fields like title, excerpt, and summary are assigned weights (A,B, and C) to influence the ranking of search results. These weights are part of PostgreSQL’s full-text search system, where A represents the highest importance, followed by B, and then C. For example, matches in title are considered more important than matches in excerpt, which in turn are prioritized over matches in summary. This allows you to fine-tune how search results are ranked, ensuring that the most relevant content surfaces first.

Prefix Matching: Enables searching for partial words. For example, searching for dev will match developer or development. This is handled by PostgreSQL’s to_tsquery under the hood.

English Dictionary: Uses PostgreSQL’s english dictionary to handle stemming and stopwords (e.g., walking matches walk) and removes stopwords like and or the.

Any Word Matching: Returns results if any word in the query matches, instead of requiring all words to match.

A query like Model.search("quick brown") returns results ranked by relevance, considering these features.

What Would You Do Without pg_search?

Without pg_search, implementing this functionality manually in PostgreSQL involves more effort and complexity. Here's how it could be done:

Setting Up a tsvector Column

Add a tsvector column to store a precomputed search index:

ALTER TABLE models ADD COLUMN search_vector tsvector;
CREATE INDEX search_vector_idx ON models USING gin(search_vector);

Creating a Trigger to Update tsvector

CREATE OR REPLACE FUNCTION update_search_vector() RETURNS trigger AS $$
BEGIN
  NEW.search_vector :=
    setweight(to_tsvector('english', NEW.title), 'A') ||
    setweight(to_tsvector('english', NEW.excerpt), 'B') ||
    setweight(to_tsvector('english', NEW.summary), 'C');
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER update_search_vector_trigger
BEFORE INSERT OR UPDATE ON models
FOR EACH ROW EXECUTE FUNCTION update_search_vector();

This ensures the search_vector column remains up-to-date, but introduces additional database overhead during writes.

Writing a Raw SQL Query for Search

Performing a search involves writing SQL queries manually:

Model.where("search_vector @@ plainto_tsquery(?)", query)
     .order("ts_rank(search_vector, plainto_tsquery(?)) DESC", query)

Handling Advanced Features'

  1. Prefix Matching: Use to_tsquery with wildcards:
to_tsquery('quick:* & brown:*')

This allows partial word matching but requires constructing queries manually.

  1. Field weights: Manage weights using setweight in the tsvector trigger function as shown above. Adjust the weights as needed for each field.

This approach is time-consuming, prone to error, and harder to maintain. Each change to your search requirements could involve updating database triggers and raw SQL, making iteration slower and more complex.

Why pg_search is a Game-Changer

The pg_search gem simplifies all of the above by abstracting PostgreSQL’s complexity into Rails model methods. Key advantages include:

  • Avoid Boilerplate: No need to manually manage tsvector columns, triggers, or SQL queries.
  • Iterate Quickly: Update search logic directly in your Rails models without touching the database schema or triggers.
  • Leverage Advanced Features: Easily enable prefix matching, dictionaries, and weighted fields without custom SQL.
  • Stay Focused on Business Logic: Spend less time on plumbing and more time delivering features that matter.

Conclusion

pg_search makes full-text search in Rails both powerful and accessible. It handles the heavy lifting of integrating PostgreSQL’s search capabilities, allowing you to focus on building great user experiences. Without it, you’d need to dive deep into PostgreSQL’s internals, adding significant complexity to your application.

If your application requires robust and flexible search functionality, pg_search is an invaluable tool in your stack.