# How to Integrate Ransack with acts-as-taggable-on for Tag-Based Search

> Learn how to integrate Ransack with acts-as-taggable-on for powerful tag based search. Query tag names directly with simple predicates without custom SQL.

- Repository: [ActiveRecord Hackery/ransack](https://github.com/activerecord-hackery/ransack)
- Tags: how-to-guide
- Published: 2026-02-23

---

**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`](https://github.com/activerecord-hackery/ransack/blob/main/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.

```ruby

# 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.

```ruby

# 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`).

```erb
<%= 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.

```ruby

# 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`](https://github.com/activerecord-hackery/ransack/blob/main/spec/ransack/adapters/active_record/base_spec.rb), Ransack handles HABTM associations by constructing LEFT OUTER JOINs across the join tables.

```sql
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`](https://github.com/activerecord-hackery/ransack/blob/main/spec/support/schema.rb) file (lines 141-148) demonstrates this pattern.

```ruby
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?` in [`lib/ransack/context.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/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_in` without adding custom ransackers.
- **Permit tag parameters** using the singular context name with `_list` (e.g., `project_list` for the `:projects` context).
- **Always use `distinct: true`** in `@q.result` to prevent duplicate records caused by LEFT OUTER JOINs on the `taggings` and `tags` tables.
- **Implement custom ransackers** only when you need complex logic beyond standard association searches, referencing patterns in [`spec/support/schema.rb`](https://github.com/activerecord-hackery/ransack/blob/main/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`](https://github.com/activerecord-hackery/ransack/blob/main/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`](https://github.com/activerecord-hackery/ransack/blob/main/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.