# Ransack
> Ransack is a Ruby gem that adds searching and sorting to Rails apps. It turns request params like name_cont=ann into Active Record queries, with form and sort-link helpers for your views, and needs no search server.

Ransack is a Ruby gem that adds searching and sorting to a Rails app by turning request parameters into Active Record queries. A param named `email_cont` becomes `WHERE email LIKE '%value%'`, a param named `s` becomes an `ORDER BY`, and the gem ships view helpers that build the matching form and the clickable column headers. It runs inside your database: no search server, no index to keep in sync. Ransack works on your [models](https://avohq.io/glossary/models) and the joins between them, which makes it a common choice for admin screens, filter bars and internal tools.

The current release is Ransack 6.1.0, published in November 2026, and it requires Rails 8.1 and Ruby 3.3 or later (checked in November 2026). Apps on Rails 8.0 or older stay on the 6.0 line.

## The search object

Everything starts with `ransack`, a class method Ransack adds to every model. You pass it a hash of conditions, usually `params[:q]`, and get back a search object. Calling `result` on that object returns an ordinary relation, so you can chain `includes`, pagination or scopes onto it.

```ruby
# Gemfile
gem "ransack"
```

```ruby
# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
  def index
    @q = Order.ransack(params[:q])
    @orders = @q.result.includes(:customer).order(created_at: :desc)
  end
end
```

The search object also remembers what was searched, which is how the form re-fills itself and how sort links know the current direction.

## Predicates

Each condition key is an attribute name followed by a predicate. The predicate decides the SQL operator. The ones you reach for most often:

- `_eq` and `_not_eq`: equal and not equal (`status_eq=paid`).
- `_cont` and `_not_cont`: contains, a `LIKE '%value%'` match.
- `_i_cont`: contains, ignoring case on every database.
- `_start` and `_end`: starts with and ends with.
- `_gt`, `_gteq`, `_lt`, `_lteq`: comparisons for numbers and dates.
- `_in` and `_not_in`: match any or none of a list of values.
- `_null`, `_not_null`, `_present`, `_blank`: empty checks.
- `_true` and `_false`: boolean columns.
- `_matches`: a raw `LIKE` pattern you write yourself.

You can join several attributes with `_or_` (or `_and_`) to search them with one value, and add `_any` or `_all` to many predicates to pass several values.

```ruby
Order.ransack(
  status_in: ["paid", "shipped"],
  total_gteq: 100,
  created_at_gteq: 30.days.ago,
  number_or_notes_i_cont: "refund"
).result
```

Two details changed recently and trip up older code. Since Ransack 5.0, `%` and `_` typed into a `cont`, `start` or `end` search are matched as literal characters on every database; use `_matches` when you want a pattern. Since 6.0, plain `cont` on PostgreSQL is case-sensitive like PostgreSQL's own `LIKE`, so use `_i_cont` where users expect case not to matter.

## search_form_for and sort_link

`search_form_for` wraps Rails' form builder around the search object. Field names are the condition keys, and the form submits them under `q` with a GET request. `sort_link` renders a column header that toggles the sort and shows an arrow for the current direction.

```erb
<%= search_form_for @q do |f| %>
  <%= f.label :number_or_notes_i_cont, "Order number or notes" %>
  <%= f.search_field :number_or_notes_i_cont %>

  <%= f.label :total_gteq, "Minimum total" %>
  <%= f.number_field :total_gteq %>

  <%= f.submit "Search" %>
<% end %>

<table>
  <tr>
    <th><%= sort_link(@q, :number) %></th>
    <th><%= sort_link(@q, :total, "Amount", default_order: :desc) %></th>
    <th><%= sort_link(@q, :customer_name, "Customer") %></th>
  </tr>
</table>
```

Ransack also has `turbo_search_form_for` for apps that want the search to submit through Turbo, and `sort_url` when you need the URL without the link. If your views use `sort_link` or `sort_url`, run 6.0.1, 5.0.3, 4.4.4 or later: those October 2026 releases fixed a flaw where a crafted query string could make a sort link run script or point to another site.

## Searching across associations

To reach into an association, prefix the attribute with the association name. `customer_name_cont` searches the `name` column of an order's customer, and `line_items_sku_eq` searches its line items. Ransack adds the join for you.

```ruby
class Order < ApplicationRecord
  belongs_to :customer
  has_many :line_items

  def self.ransackable_attributes(auth_object = nil)
    %w[number status total notes created_at]
  end

  def self.ransackable_associations(auth_object = nil)
    %w[customer line_items]
  end
end

class Customer < ApplicationRecord
  has_many :orders

  def self.ransackable_attributes(auth_object = nil)
    %w[name email]
  end
end
```

```ruby
Order.ransack(customer_email_cont: "@acme.com", line_items_sku_eq: "TSHIRT-M")
  .result(distinct: true)
```

Two things to know. Every model on the path is asked for its own allowlist, so the association must be listed on `Order` and the column must be listed on `Customer`. And a `has_many` join can return the same order once per matching line item, which is why the example asks for `result(distinct: true)`. More on how Rails wires these up in [associations](https://avohq.io/glossary/associations).

## The Ransack 4 allowlist, and why it exists

Up to version 3, Ransack let a search touch every column and every association on a model. In January 2023 the security firm Positive Security published [research](https://positive.security/blog/ransack-data-exfiltration) showing what that meant for any page that passed `params[:q]` straight to `ransack`. An attacker could follow associations to a sensitive column and guess it one character at a time with `_start`. On a blog's post index, a request like `?q[user_reset_password_token_start]=2` returns posts only if some author's reset token starts with `2`. Repeat for each character and the full token comes out, which is enough to take over the account. The same trick works on password hashes, API keys and any other column a model can reach.

Ransack 4.0.0, released on 9 February 2023, changed the default. Nothing is searchable until the model says so. Calling `ransack` on a model with no `ransackable_attributes` raises an error that begins "Ransack needs Order attributes explicitly allowlisted as searchable", and keys outside the list are ignored.

The four methods you can define:

- `ransackable_attributes`: columns and ransackers that can be searched.
- `ransackable_associations`: associations a search can follow.
- `ransortable_attributes`: what can be sorted. Defaults to the searchable attributes.
- `ransackable_scopes`: model scopes a search can call. Empty by default.

Each takes an optional `auth_object`, so the list can depend on who is asking.

```ruby
class User < ApplicationRecord
  def self.ransackable_attributes(auth_object = nil)
    if auth_object == :admin
      %w[name email role created_at last_sign_in_at]
    else
      %w[name]
    end
  end
end
```

```ruby
@q = User.ransack(params[:q], auth_object: current_user.admin? ? :admin : nil)
```

If you are upgrading an old app, Ransack offers `authorizable_ransackable_attributes`, which returns every column. It is a convenient base for an exclusion list (`authorizable_ransackable_attributes - %w[encrypted_password reset_password_token]`), but returning it unfiltered from `ApplicationRecord` restores the pre-4.0 exposure.

Since 6.0 there is a second option. A model that defines none of the lists can be searched from permitted strong parameters, so the controller's `permit` call becomes the allowlist. A plain hash, or unpermitted params, still needs the model lists. A model that defines a list always applies it on top of the controller's.

## Using Ransack in an Avo app

Your team can put Ransack behind its admin with Avo. With the [Dynamic Filters](https://avohq.io/addons/dynamic-filters) add-on, you mark a field `filterable: true` and the filter bar's queries run through Ransack, so each filterable attribute goes in the model's `ransackable_attributes` list, the same allowlist you keep for your own controllers. For search, each resource's `self.search` holds a query, and Ransack is the default the Avo docs show, not a requirement. The [Global Search](https://avohq.io/addons/global-search) add-on runs that same query from a Cmd+K palette across every resource.

## Frequently asked questions

### What is Ransack in Rails?

Ransack is a gem that adds search and sort to Rails apps by translating params such as `title_cont` or `price_gteq` into Active Record queries. It includes `search_form_for` and `sort_link` view helpers and runs entirely in your database, with no separate search server.

### Why does Ransack 4 require ransackable_attributes?

Before 4.0, any column and any association was searchable by default, and researchers showed attackers could guess secrets such as password reset tokens one character at a time through search params. Ransack 4.0 made searching opt-in, so you now list the searchable attributes and associations on each model.

### Is it safe to pass params[:q] to Ransack?

It is safe when the allowlists only contain columns you would be happy to show users, because a search on a column leaks its contents one character at a time. Keep secrets such as tokens, password hashes and API keys out of `ransackable_attributes` on every model a search can reach, and stay on a patched release.

### How do I search across associations with Ransack?

Prefix the attribute with the association name, as in `customer_name_cont` or `line_items_sku_eq`. The association must be in the parent model's `ransackable_associations` and the column in the associated model's `ransackable_attributes`. Add `distinct: true` to `result` when a `has_many` join could repeat rows.

### Is Ransack a full-text search engine?

No. It builds SQL conditions such as `LIKE`, equality and comparisons on your existing tables, which suits filtering and admin search. For ranked full-text search, use your database's full-text features (for example through pg_search) or a dedicated search service.
