Skip to content
ExponaBLOG
RUBY ON RAILS · DAY 061 / 100

Search in Rails: Ruby on Rails best practices explained

From simple to powerful.

Expona Admin7 min read

Introduction

Search functionality is often an afterthought in web development, yet it's one of the first features users reach for when exploring your application. Building effective search in Rails means understanding when to reach for simple SQL queries and when to invest in dedicated search infrastructure. Search in Rails: Ruby on Rails best practices explained covers the spectrum from straightforward LIKE queries to Elasticsearch-backed solutions, helping you choose the right tool for your scale and requirements.

LIKE Queries — Fine for Small Tables

For many Rails applications, especially those starting out or with modest data volumes, SQL LIKE queries are sufficient. They're simple to implement, require no external dependencies, and fit naturally into your existing ActiveRecord patterns.

The approach is straightforward: scope your queries with LIKE operators and let PostgreSQL handle the pattern matching. Here's how a typical product search might look:

ruby
class Product < ApplicationRecord
  def self.search(query)
    return all if query.blank?
    where("name ILIKE ? OR description ILIKE ?", "%#{query}%", "%#{query}%")
  end
end

This works well when you're searching through hundreds or even low thousands of records. The ILIKE operator (case-insensitive) is usually what you want for user-facing search. However, there are real limitations here.

LIKE queries don't handle typos, synonyms, or relevance ranking. A search for "lapto" won't find "laptop." More importantly, as your table grows beyond tens of thousands of rows, performance degrades noticeably because PostgreSQL must scan a significant portion of the table on each query. You also can't easily weight results — there's no native way to say "matches in the title should rank higher than matches in the description."

When should you use LIKE? When your product catalog is under 10,000 items, your users expect exact or close matches only, and you're willing to accept query times that creep toward a few hundred milliseconds during peak load. It's also fine as a fallback mechanism when your more sophisticated search system is temporarily unavailable.

When LIKE queries start feeling slow, PostgreSQL's native full-text search capabilities offer a dramatic improvement without leaving the database. The pg_search gem wraps PostgreSQL's tsvector and tsquery functionality in a Rails-friendly interface, giving you ranked, relevance-aware results while staying within your existing stack.

Full-text search tokenizes text into searchable units and creates an inverted index. A search for "fast laptop" understands that it should find documents containing both "fast" and "laptop," and it handles word forms — searching for "running" might also find "run" or "runs" depending on your language settings.

Here's a practical setup for a product model with full-text search:

ruby
class Product < ApplicationRecord
  include PgSearch::Model

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

The weights (A, B, C) control relevance ranking. Results with matches in the name rank highest. Prefix search allows partial word matching — searching for "lap" will match "laptop." Then in your controller:

ruby
class ProductsController < ApplicationController
  def index
    @products = Product.search_by_text(params[:q]).limit(20)
  end
end

Performance improves significantly over LIKE because PostgreSQL maintains a full-text index, and queries are optimized for this specific workload. You get stemming (word form reduction), phrase searching, and boolean operators out of the box. Searching for "high-performance laptop" or "laptop NOT gaming" works naturally.

pg_search is excellent for catalogs with 10,000 to a few hundred thousand items. It requires no additional infrastructure — PostgreSQL is already running. The trade-off is that full-text search on very large datasets or with complex ranking rules can still be slower than dedicated search engines. It's also PostgreSQL-specific; if you're using MySQL or another database, you'll need a different approach.

A real scenario: an e-commerce team uses pg_search to serve 80,000 SKUs across five categories. Queries complete in under 50ms, users get relevant results with typo tolerance via stemming, and the team avoids the operational overhead of running Elasticsearch. Perfect fit.

Searchkick — Elasticsearch or OpenSearch Integration

When your search demands grow beyond what PostgreSQL can deliver — multiple indexes, complex faceting, sub-millisecond response times, or terabytes of product data — Searchkick brings Elasticsearch or OpenSearch to Rails with minimal friction.

Searchkick abstracts away much of the Elasticsearch complexity. You define searchable attributes, boost rules, and filters in Ruby, and Searchkick handles indexing, updates, and query construction.

Here's a practical implementation for a product search in a mid-market e-commerce platform:

ruby
class Product < ApplicationRecord
  searchkick text_start: [:name, :sku], 
             callbacks: :async,
             synonyms: [["laptop", "notebook"], ["fast", "quick"]]

  def search_data
    {
      name: name,
      sku: sku,
      description: description,
      category: category.name,
      price: price,
      in_stock: in_stock?,
      popularity: view_count
    }
  end
end

Searchkick's callbacks: :async means indexing happens in the background (via Active Job) instead of blocking the request. When you create or update a product, Searchkick queues the index operation. This prevents search latency from affecting create/update performance.

Querying is simple and powerful:

ruby
products = Product.search("gaming laptop", 
  where: { price: { lte: 1500 } },
  boost: { popularity: { factor: 10 } },
  limit: 20
)

This searches for "gaming laptop," filters to products under $1500, boosts popular items ten times, and returns 20 results. Elasticsearch handles typos, stemming, and relevance ranking out of the box.

Searchkick works with both Elasticsearch (self-hosted or AWS-managed) and OpenSearch (AWS's Elasticsearch fork). For teams on AWS, managed OpenSearch is often simpler than running Elasticsearch yourself. You'll need an Elasticsearch/OpenSearch cluster running, which adds operational overhead but enables powerful features: faceted search (filter by category, brand, price range), autocomplete with typo correction, advanced analytics on search queries, and performance at scale (millions of searchable items).

Real scenario: a travel booking platform serves 500,000+ accommodation listings across 50 countries. They use Searchkick with Elasticsearch to deliver sub-100ms search results with complex faceting (location, price, amenities), autocomplete for city names, and per-user result personalization. This scale would be painful or impossible with pg_search alone.

Meilisearch offers a compelling alternative to Elasticsearch for teams that want great search performance without Elasticsearch's operational complexity. It's purpose-built to be simple to deploy and manage while delivering fast, typo-tolerant search with minimal configuration.

The Meilisearch gem for Rails is straightforward to set up:

ruby
class Product < ApplicationRecord
  include MeiliSearch::Rails

  meilisearch do |config|
    config.primary_key = :id
  end

  meilisearch_index :searchable_index do
    attributes_to_index [:name, :description, :category]
    searchable_attributes [:name, :description]
    filterable_attributes [:category, :price, :in_stock]
    sortable_attributes [:price, :created_at]
  end
end

Then search with a single method call:

ruby
results = Product.search("laptop")

By default, Meilisearch returns results sorted by relevance. You get typo tolerance, stemming, and phrase search automatically. Filtering is clean:

ruby
results = Product.search("laptop", 
  filter: "category = 'Electronics' AND price < 1500"
)

Meilisearch shines for smaller to mid-sized deployments (millions rather than billions of documents). You can self-host it with Docker or use their managed cloud offering. Unlike Elasticsearch, which requires significant tuning and memory overhead, Meilisearch runs on modest hardware and works well out of the box. Configuration is minimal — no complex mapping definitions or analyzer tweaking required.

The trade-offs: Meilisearch is less mature than Elasticsearch and the ecosystem is smaller. Advanced features like custom scoring functions or real-time analytics are less developed. For massive datasets (100M+ documents) or highly specialized search requirements, Elasticsearch remains more powerful.

Real scenario: a SaaS directory with 50,000 companies uses Meilisearch for search. They deployed it with a single Docker container, integrated it in an afternoon, and now deliver search results in 30ms with instant typo correction. The simplicity and performance mean they never think about search infrastructure.

Common Mistakes

  • Overengineering for the wrong stage: Deploying Elasticsearch for 5,000 products wastes time and money. Start with LIKE queries, graduate to pg_search, only move to Elasticsearch when you truly need it. Most Rails applications never reach that scale.
  • Ignoring async indexing: Synchronous indexing with Searchkick or Meilisearch can make create/update endpoints slow. Always use async callbacks and monitor your job queue to ensure indexes stay current.
  • Forgetting about stale indexes: After deploying search infrastructure, indexes can drift from your database if background jobs fail silently. Build monitoring and periodic reindexing jobs into your deployment.
  • Not handling empty queries: A blank search should return either nothing or a sensible default (like featured products), not every record. Always validate and scope queries explicitly.
  • Neglecting user experience: Powerful search means nothing without autocomplete, facets, and clear result presentation. Search is product work, not just backend work. Collaborate with your frontend team on query timing, debouncing, and result display.

Key Takeaways

  • LIKE queries work for small datasets (under 10K items) but degrade quickly. Use them only when simplicity matters more than performance.
  • pg_search brings PostgreSQL full-text capabilities to Rails with no additional infrastructure. It's the sweet spot for most mid-sized applications with 10K–100K searchable items.
  • Searchkick with Elasticsearch/OpenSearch scales to millions of items and enables advanced features like faceting and analytics, but adds operational overhead.
  • Meilisearch offers simplicity and great default performance for smaller deployments without Elasticsearch's complexity. Excellent for bootstrapped teams or simple use cases.
  • Always start simple and only add complexity when metrics show you need it. Search is often a premature optimization target.
  • Async indexing, monitoring, and stale-index handling are critical for any search system in production. Don't treat search infrastructure as "set it and forget it."
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles