Full-text search is a game-changer for any Rails app. It lets users find exactly what they’re looking for quickly and easily. I’ve implemented it on several projects, and it always gives the app that extra polish users love.
Let’s dive into how to set up full-text search with Rails and PostgreSQL using the pg_search gem. This combo is powerful and surprisingly simple to get going.
First things first, add the pg_search gem to your Gemfile:
gem 'pg_search'
Run bundle install to get it set up. Now, let’s say we have a Post model for our blog. We’ll add full-text search to this model.
In your post.rb file, include the PgSearch module and set up the search:
class Post < ApplicationRecord
include PgSearch::Model
pg_search_scope :search_full_text,
against: {
title: 'A',
content: 'B'
},
using: {
tsearch: { prefix: true }
}
end
This sets up a search_full_text method on your Post model. The ‘against’ option specifies which fields to search, and the letters ‘A’ and ‘B’ set the priority (A is higher priority than B). The ‘using’ option configures the search to use PostgreSQL’s full-text search with prefix matching.
Now, you can use this in your controller:
def index
@posts = params[:query].present? ? Post.search_full_text(params[:query]) : Post.all
end
This will search posts if a query is present, otherwise it’ll return all posts.
But what if you want to search across multiple models? Pg_search has you covered with multisearch. Let’s say we want to search posts and comments.
First, enable multisearch in an initializer:
# config/initializers/pg_search.rb
PgSearch.multisearch_options = {
using: { tsearch: { prefix: true } }
}
Then, in your models:
class Post < ApplicationRecord
include PgSearch::Model
multisearchable against: [:title, :content]
end
class Comment < ApplicationRecord
include PgSearch::Model
multisearchable against: [:content]
end
Now you can search across both models:
results = PgSearch.multisearch('ruby on rails')
This returns a collection of PgSearch::Document objects. You can access the original record with result.searchable.
One cool thing about pg_search is its support for different search features. For example, you can use trigram similarity for fuzzy matching:
pg_search_scope :search_full_text,
against: [:title, :content],
using: {
tsearch: { prefix: true },
trigram: { threshold: 0.1 }
}
This combines full-text search with trigram matching, which can catch misspellings and variations.
Another neat feature is the ability to rank results. You can customize this with a tsvector_column:
class AddSearchableToPosts < ActiveRecord::Migration[6.1]
def up
execute <<-SQL
ALTER TABLE posts
ADD COLUMN searchable tsvector GENERATED ALWAYS AS (
setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(content, '')), 'B')
) STORED;
SQL
end
def down
remove_column :posts, :searchable
end
end
Then in your model:
pg_search_scope :search_full_text,
against: 'searchable',
using: {
tsearch: {
dictionary: 'english',
tsvector_column: 'searchable'
}
}
This can significantly speed up your searches, especially for large datasets.
Now, let’s talk about making your search results more relevant. One way to do this is by using weights:
pg_search_scope :search_full_text,
against: {
title: 'A',
content: 'B',
author: 'C'
},
using: {
tsearch: {
dictionary: 'english',
any_word: true,
prefix: true
}
}
This setup gives more importance to matches in the title than in the content or author fields.
You can also boost the relevance of more recent posts:
pg_search_scope :search_full_text,
against: [:title, :content],
using: {
tsearch: { prefix: true }
},
order_within_rank: "posts.created_at DESC"
This ensures that, within posts with the same relevance score, more recent ones appear first.
One thing to keep in mind is performance. As your dataset grows, you might need to add indexes to speed things up:
class AddIndexesToPosts < ActiveRecord::Migration[6.1]
def change
add_index :posts, :title
add_index :posts, :content
execute "CREATE INDEX posts_full_text_search ON posts USING gin(to_tsvector('english', title || ' ' || content))"
end
end
This creates a GIN (Generalized Inverted Index) which can significantly speed up full-text searches.
Now, let’s talk about handling search results in the view. You might want to highlight the matching terms in your search results. Pg_search doesn’t do this out of the box, but you can achieve it with a bit of Ruby:
def highlight(text, query)
query.split.each do |word|
text.gsub!(/(#{Regexp.escape(word)})/i, '<mark>\1</mark>')
end
text.html_safe
end
Use this in your view like so:
<% @posts.each do |post| %>
<h2><%= highlight(post.title, params[:query]) %></h2>
<p><%= highlight(truncate(post.content, length: 200), params[:query]) %></p>
<% end %>
This will wrap matching terms in tags, which you can style with CSS to highlight them.
Another cool feature you might want to add is search suggestions. You can implement this with a bit of JavaScript and an additional endpoint in your controller:
# posts_controller.rb
def suggest
render json: Post.search_full_text(params[:term]).limit(5).pluck(:title)
end
Then in your JavaScript:
$('#search_input').autocomplete({
source: '/posts/suggest',
minLength: 2
});
This will show a dropdown of suggestions as the user types.
One thing I’ve found really useful is adding search analytics. You can log searches to understand what users are looking for:
class SearchLog < ApplicationRecord
belongs_to :user, optional: true
end
class PostsController < ApplicationController
def index
@posts = params[:query].present? ? Post.search_full_text(params[:query]) : Post.all
if params[:query].present?
SearchLog.create(
query: params[:query],
results_count: @posts.count,
user: current_user
)
end
end
end
This can give you valuable insights into what your users are searching for, which can inform your content strategy or help you identify gaps in your site’s information architecture.
Lastly, don’t forget about internationalization. If your site supports multiple languages, you might need to adjust your search setup:
pg_search_scope :search_full_text,
against: [:title, :content],
using: {
tsearch: {
dictionary: 'simple'
}
}
Using the ‘simple’ dictionary instead of ‘english’ can work better for multi-language content.
Implementing full-text search with Rails and PostgreSQL using pg_search is a powerful way to enhance your app’s user experience. It’s flexible, performant, and can be customized to fit a wide variety of use cases. Whether you’re building a blog, an e-commerce site, or any other kind of web application, good search functionality can make a huge difference to your users.
Remember, the key to great search is not just in the implementation, but in continually refining and improving it based on user behavior and feedback. Keep an eye on your search logs, talk to your users, and don’t be afraid to experiment with different configurations to find what works best for your specific use case.