How to Integrate Ransack with acts-as-taggable-on for Tag-Based Search
Ransack integrates seamlessly with acts-as-taggable-on by treating tag associations as standard searchable associations, allowing you to query tag names directly using predicates like tags_name_eq or projects_name_cont without custom SQL or ransackers.
The activerecord-hackery/ransack gem provides advanced search capabilities for Rails applications, and integrating it with acts-as-taggable-on enables powerful filtering by tags. Because acts-as-taggable-on creates standard has_and_belongs_to_many or has_many :through associations between your model, the taggings join table, and the tags table, Ransack recognizes these relationships as searchable out of the box.
How Ransack Detects Searchable Associations
Ransack determines whether an association is searchable through Ransack::Context#ransackable_association? in lib/ransack/context.rb (lines 166-168). This method checks if the association exists in the model's ransackable associations list. When acts-as-taggable-on declares a tag context (e.g., acts_as_taggable_on :projects), it dynamically creates associations that Ransack treats as standard searchable relationships, enabling predicates like {context}_name_eq without additional configuration.
Step-by-Step Integration Guide
1. Configure the Model with acts-as-taggable-on
Declare your tag context in the model. The gem automatically creates the necessary join tables and associations.
# app/models/task.rb
# Reference: docs/docs/going-further/acts-as-taggable-on.md
class Task < ApplicationRecord
# Declares :projects as the tag context
acts_as_taggable_on :projects
# Optional: Enable multitenancy if needed
# acts_as_taggable_tenant :language
end
2. Permit Tag Parameters in Strong Parameters
When accepting tag input through forms, use the singular form of the tag context with the _list suffix.
# app/controllers/tasks_controller.rb
private
def task_params
params.require(:task).permit(:name, :description, :project_list)
end
3. Build the Search Form
Use Ransack's association-based predicates to search within the tag name. Replace {context} with your declared context name (e.g., projects).
<%= search_form_for @q, url: tasks_path, method: :get do |f| %>
<!-- Search for tags containing text -->
<%= f.label :projects_name_cont, 'Project (contains)' %>
<%= f.text_field :projects_name_cont %>
<!-- Search for specific tags from a dropdown -->
<%= f.label :projects_name_in, 'Project (choose from list)' %>
<%= f.select :projects_name_in,
ActsAsTaggableOn::Tag.distinct.order(:name).pluck(:name),
{}, multiple: true %>
<%= f.submit 'Search' %>
<% end %>
4. Execute the Search in the Controller
Instantiate the Ransack search object and retrieve results. Always use distinct: true to prevent duplicate records when joining tag tables.
# app/controllers/tasks_controller.rb
def index
@q = Task.ransack(params[:q])
@tasks = @q.result(distinct: true)
end
Understanding the Generated SQL
When you execute a search like projects_name_eq: 'Home', Ransack automatically generates the appropriate SQL joins. As shown in spec/ransack/adapters/active_record/base_spec.rb, Ransack handles HABTM associations by constructing LEFT OUTER JOINs across the join tables.
SELECT DISTINCT "tasks".* FROM "tasks"
LEFT OUTER JOIN "taggings"
ON "taggings"."taggable_id" = "tasks"."id"
AND "taggings"."taggable_type" = 'Task'
AND "taggings"."context" = 'projects'
LEFT OUTER JOIN "tags"
ON "tags"."id" = "taggings"."tag_id"
WHERE "tags"."name" = 'Home'
This automatic join generation eliminates the need for manual SQL or custom scopes when filtering by tags.
Advanced: Custom Ransackers for Complex Tag Logic
For scenarios requiring specialized logic—such as tenant-scoped tags or complex array operations—define a custom ransacker in your model. The spec/support/schema.rb file (lines 141-148) demonstrates this pattern.
class Task < ApplicationRecord
acts_as_taggable_on :projects
ransacker :projects_name, formatter: proc { |name|
ActsAsTaggableOn::Tag.where(name: name).select(:id)
} do |parent|
parent.table[:id]
end
end
Custom ransackers are only necessary when the default association predicates (_eq, _cont, _in) do not satisfy your specific query requirements, such as requiring records to match ALL specified tags rather than ANY.
Summary
- Ransack automatically recognizes acts-as-taggable-on associations via
Ransack::Context#ransackable_association?inlib/ransack/context.rb(lines 166-168), treating them as standard searchable relationships. - Query using association predicates like
{context}_name_eq,{context}_name_cont, or{context}_name_inwithout adding custom ransackers. - Permit tag parameters using the singular context name with
_list(e.g.,project_listfor the:projectscontext). - Always use
distinct: truein@q.resultto prevent duplicate records caused by LEFT OUTER JOINs on thetaggingsandtagstables. - Implement custom ransackers only when you need complex logic beyond standard association searches, referencing patterns in
spec/support/schema.rb.
Frequently Asked Questions
Can I search for records that match ANY of the specified tags?
Yes. Use the _in predicate with an array of tag names, such as projects_name_in. Ransack generates an SQL IN clause that returns records associated with any tag in the provided list. For example, params[:q][:projects_name_in] = ['Home', 'Work'] finds tasks tagged with either Home or Work.
Do I need to manually add tags to ransackable_attributes?
No. Because acts-as-taggable-on creates standard ActiveRecord associations (has_many :tags through :taggings), Ransack includes them automatically. As implemented in lib/ransack/adapters/active_record/base.rb, the gem provides fallback ransackable_* methods that expose these associations without explicit whitelisting in your model.
How do I search for records that have ALL specified tags, not just any?
The default _in predicate performs an OR query. To require ALL tags (AND logic), you must either chain multiple Ransack conditions (one per tag) or implement a custom ransacker. Refer to the custom ransacker example in spec/support/schema.rb (lines 141-148) for a pattern that handles intersection logic using subqueries or array aggregation.
Why does my tag search return duplicate results?
Duplicate records occur because Ransack generates LEFT OUTER JOINs on the taggings table. When a task has multiple tags, the join creates multiple rows for that single task. Always call @q.result(distinct: true) to ensure unique records in your result set, as demonstrated in the controller examples throughout the activerecord-hackery/ransack documentation.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →