TL;DR
We see search functionality as a crucial point for user experience and engagement.
The activerecord-mysql-search
gem provides a powerful, easy-to-use solution for implementing full-text search in Ruby on Rails applications using MySQL’s native capabilities.
It offers fast, scalable and flexible search without the complexity of any external services.
Common text search problems we faced in Rails
When we see the implementation of search in Rails applications, many default to ActiveRecord and SQL queries with the LIKE
operator. This approach seems simple and quick but it also comes with a side effect. It does not scale well.
LIKE
queries do not use full-text indexes, scan entire tables and are slow with tens or hundreds of thousands of records
- standard
LIKE
only matches exact substrings, lacks relevance ranking and does not support morphology or synonyms LIKE
ignores language nuances and doesn’t support word forms- searching across multiple fields, associations, filters, and sorting quickly leads to messy, unmaintainable code
As data grows, search slows down, and database load becomes critical.
Our pain might prevent you from suffering as well
Our main concerns that brought us all in to the full text search approach:
- we needed a solution that can scale with our clients systems
- we prefer a maintainable structure that can be taken over by anyone in the team
- speed is an important key aspect
After integrating our full-text search into our projects and learning from trial and error, we created the
activerecord-mysql-search
gem. Our goal was to give Rails devs not another wrapper for FULLTEXT
, but a tool that integrates seamlessly, reduces maintenance and unlocks MySQL’s built-in search potential.
Elasticsearch is powerful, sure. But for most projects, it is an overkill. Too complex, too costly and way more than you need if you are already using MySQL.
Key features
- uses MySQL
FULLTEXT
indexing for fast and relevant searches across large datasets - search data updates automatically when objects change. It works with flexible strategies, synchronous or background indexing via ActiveJob
- define which fields to index—simple fields, nested associations, dates, calendar weeks or custom transformations via
Proc
: all declaratively in source classes - create separate search columns for different user roles (e.g. buyer/seller/admin): critical for multi-tenant apps and data privacy
- comes with generators for quick setup, migrations, rake tasks for bulk reindexing and scopes for search—all in the Rails way
- indexed fields, formatting and sources are moved to separate source classes: it improves code transparency and maintainability
How our approach differs from others
- complex SQL and Arel magic are hidden: you work with familiar scopes and Ruby classes
- tools like synchronous/background indexing, scheduled tasks and SQL triggers ensure search data stays current
- source classes let you quickly add fields, associations or custom indexing rules without rewriting migrations or SQL
- index any associated data alongside the main entity, e.g. product reviews or related orders
- multi-column architecture ensures privacy—different users see different results and indexes don’t store unnecessary data
- our full-text-search approach can be added to large apps in hours
Implementing full-text search in 5 minutes
We designed activerecord-mysql-search
to make integration as fast and “Rails-like” as possible. Here’s how it works:
Step 1: Install the gem
Add to your Gemfile
:
gem 'activerecord-mysql-search'
Run:
bundle install
Step 2: Generate configuration and migrations
Run:
rails generate mysql:search:install
This creates:
config/initializers/mysql_search.rb
: Search configuration.app/models/search_index.rb
: Search index model.- A migration for the search index table.
Step 3: Run the migration
rails db:migrate
Step 4: Enable search in your model
Add to your model (e.g., Article
):
class Article < ApplicationRecord
include MySQL::Search::Searchable
belongs_to :news_digest
end
Step 5: Define the indexing schema (source class)
Create app/search_sources/article_source.rb
:
class ArticleSource < MySQL::Search::Source
schema content: {
title: :text,
content: :text,
type: ->(value) { I18n.t("article.types.#{value}") },
news_digest: {
title: :text,
published_at: [:date, :calendar_week]
}
}
end
Step 6: Index existing data
rails mysql:search:reindex
Step 7: Use search in controllers or services
results = Article.full_text_search("Ruby on Rails")
That is it! Users now get fast, scalable and relevant search.
Advanced scenarios: multi-column search for roles and contexts
Real projects rarely need “single-column search.” Business logic often requires showing different data to different users, supporting flexible filters and ensuring privacy. activerecord-mysql-search
supports this out of the box. For example, clients, sellers and admins each need their own “view of the world.” The gem lets you create separate indexes per role:
class ProductSource < MySQL::Search::Source
schema content: {
name: :text,
description: :text,
brand: :text,
reviews: { content: :text, rating: :text }
},
seller_extra: {
sku: :text,
internal_notes: :text,
supplier: { name: :text, contact_info: :text },
},
admin_extra: {
created_by: { name: :text, email: :text }
}
end
Add columns and indexes to SearchIndex
:
class ExtraContentForSearchIndices < ActiveRecord::Migration[7.1]
def change
add_column :search_indices, :seller_extra, :text
add_column :search_indices, :admin_extra, :text
add_index :search_indices, [:content, :seller_extra], type: :fulltext
add_index :search_indices, [:content, :seller_extra, :admin_extra], type: :fulltext
end
end
Now, sellers search with:
results = Product.full_text_search("Ruby on Rails", search_column: [:content, :seller_extra])
Admins use:
results = Product.full_text_search("Ruby on Rails", search_column: [:content, :seller_extra, :admin_extra])
You can completely separate search contexts for different roles. In this case, there is no need to create combined indexes, just use different columns and separate indexes for each role.
What if I use methods that don’t trigger ActiveRecord callbacks?
Using #update_column
and other methods that don’t trigger ActiveRecord callbacks can lead to search index desynchronization. Solution: use #update
or #save
to update records to ensure indexes remain current. If you don’t have this option, the gem provides the following tool to maintain index consistency.
In this case, the gem relies on the updated_at
column. You can delegate keeping this column up-to-date to the database itself using a trigger. Create a migration using the generator:
rails generate mysql:search:create_triggers
This migration will create a trigger in each table that will update the updated_at
column when records are modified, and will also add a monkey-patch to ActiveRecord’s #timestamps
method in migrations (to automatically add this trigger to future tables). This allows maintaining search index relevance using one or more of the following tools:
- Rake task
rails mysql:search:actualize[1.hour]
- periodically checks and updates indexes, syncing them with the current database state. You can configure it to run via cron. MySQL::Search::Jobs::ScheduledUpdaterJob
- a background job that periodically checks and updates indexes. Example for Solid Queue:
# config/recurring.yml
actualize_search_indices:
class: MySQL::Search::Jobs::ScheduledUpdaterJob
args: [:daily]
schedule: every day at noon
- Full reindexing via rake task
mysql:search:reindex
- if you want to completely refresh indexes, for example after migrations or schema changes. In this case, adding triggers isn’t required.
Why this gem just works: for you, your users and your team
It’s fast, easy to use and built for modern Rails. No black boxes, no vendor lock-in, just clean, open-source search that respects your data and your time. We built this because we were tired of clunky, over-engineered search. It is battle-tested, shaped by real pain and already making teams faster. Open-source and well-documented.
Search is essential, but it should not be a burden. activerecord-mysql-search
gives Rails devs a simple, powerful way to build fast and flexible search right on top of MySQL.
Give it a try, plug it into your app and let us know what you think. Questions, feedback or ideas are always welcome.
Stay in shape 🦄!