# How to Create Custom Predicates and Ransackers for Complex Search Logic in Ransack

> Discover how to build custom predicates and ransackers in Ransack for advanced search logic. Extend Ransack with specialized Arel operations and virtual attributes.

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

---

**You can extend Ransack by registering custom predicates via `Ransack.configure` to handle specialized Arel operations, and by defining model-level ransackers that return arbitrary Arel nodes for virtual attributes.**

The Ransack gem (activerecord-hackery/ransack) provides a flexible search interface for Rails applications, but real-world scenarios often require database-specific operations or complex SQL logic. By leveraging **custom predicates** for value formatting and validation, and **ransackers** for virtual column definitions, you can implement advanced queries while maintaining Ransack's declarative API.

## Understanding Ransack's Extension Architecture

Ransack's search engine is built around two primary extensibility points that work together to transform URL parameters into executable SQL.

### Predicates vs. Ransackers

- **Predicates** are low-level building blocks defined in [`lib/ransack/configuration.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/configuration.rb) that map suffixes like `_eq` or `_cont` to Arel predicates (e.g., `=`, `LIKE`). When you call `add_predicate`, Ransack stores the definition in a `PredicateCollection` accessible via `Ransack.predicates`.

- **Ransackers** are model-level methods defined in [`lib/ransack/adapters/active_record/base.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/adapters/active_record/base.rb) that expose virtual attributes. The `Ransacker` class (defined in [`lib/ransack/ransacker.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/ransacker.rb)) wraps a callable that receives a parent Arel table binding and returns any Arel node—whether a column reference, SQL function, or sub-query.

When processing a search parameter like `name_reversed_cont`, Ransack first strips the predicate suffix (`_cont`) using `Predicate.detect_from_string!`, then retrieves the corresponding ransacker to obtain the virtual column before applying the predicate's Arel operation.

## Creating Custom Predicates in Ransack

Custom predicates allow you to introduce specialized formatting and database-specific operators that ship with Ransack.

### Adding a CSV-Processing Predicate

In [`lib/ransack/configuration.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/configuration.rb), the `add_predicate` method registers new predicates with options for `arel_predicate`, `formatter`, and `validator`. Here is how to implement a `not_in_csv` predicate that splits comma-separated values:

```ruby

# config/initializers/ransack.rb

Ransack.configure do |c|
  c.add_predicate(
    "not_in_csv",
    arel_predicate: "not_in",
    formatter: proc { |v| v.split(",") },
    validator: proc { |v| v.is_a?(Array) && v.any? }
  )
end

```

This configuration leverages the `Predicate` class (from [`lib/ransack/predicate.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/predicate.rb)) to handle value transformation. When Ransack encounters a parameter ending in `_not_in_csv`, it applies the formatter to split the string, validates the result is a non-empty array, then constructs an Arel `NOT IN` clause.

### Database-Specific Array Predicates

For PostgreSQL applications, you can expose the `&&` (overlap) operator:

```ruby
Ransack.configure do |c|
  c.add_predicate(
    "overlap",
    arel_predicate: "overlap",
    formatter: proc { |v| v.split(",").map(&:to_i) },
    validator: proc { |v| v.is_a?(Array) && v.size >= 2 }
  )
end

```

## Defining Custom Ransackers for Virtual Attributes

Ransackers bridge the gap between your search forms and complex SQL expressions. Defined via the `ransacker` class method in your models (added by [`lib/ransack/adapters/active_record/base.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/adapters/active_record/base.rb)), these virtual attributes are stored in the model's `_ransackers` hash.

### Simple String Manipulation

The following ransacker, similar to examples in [`spec/support/schema.rb`](https://github.com/activerecord-hackery/ransack/blob/main/spec/support/schema.rb) (lines 89-91), exposes a reversed name column:

```ruby

# app/models/person.rb

class Person < ApplicationRecord
  ransacker :reversed_name do |parent|
    Arel::Nodes::NamedFunction.new('REVERSE', [parent[:name]])
  end
end

```

The block receives `parent`, which represents the Arel table for `people`. The `attr_from` method in [`lib/ransack/ransacker.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/ransacker.rb) invokes this block during query construction to resolve the virtual column before predicates are applied.

### Ransackers Accepting Arguments

For more complex scenarios requiring runtime parameters, use the `:args` option to pass additional values through the search context:

```ruby

# app/models/person.rb

class Person < ApplicationRecord
  ransacker :articles_body_length, args: [:parent, :ransacker_args] do |parent, args|
    min, max = args
    subquery = <<-SQL.squish
      (SELECT MAX(articles.title)
         FROM articles
        WHERE articles.person_id = people.id
          AND LENGTH(articles.body) BETWEEN #{min} AND #{max}
        GROUP BY articles.person_id)
    SQL
    Arel.sql(subquery)
  end
end

```

This pattern, demonstrated in [`spec/support/schema.rb`](https://github.com/activerecord-hackery/ransack/blob/main/spec/support/schema.rb) (lines 27-39), enables searches like:

```ruby
Person.ransack(articles_body_length_gteq: 10, ransacker_args: [5, 100])

```

## Combining Predicates and Ransackers

The true power emerges when combining custom predicates with ransackers. Given the `reversed_name` ransacker and `not_in_csv` predicate defined earlier, you can construct forms that filter on transformed data:

```erb
<%= search_form_for @q, url: people_path do |f| %>
  <%= f.label :reversed_name_not_in_csv, "Exclude Reversed Names (CSV)" %>
  <%= f.text_field :reversed_name_not_in_csv %>
  <%= f.submit "Search" %>
<% end %>

```

When submitted with `reversed_name_not_in_csv=abc,def`, Ransack executes:

1. **Attribute Resolution**: Retrieves the `reversed_name` ransacker and calls `attr_from` to get `REVERSE("people"."name")`.
2. **Value Formatting**: Applies the `not_in_csv` formatter to produce `["abc", "def"]`.
3. **Validation**: Confirms the array contains elements.
4. **Query Construction**: Generates SQL resembling `WHERE REVERSE("people"."name") NOT IN ('abc', 'def')`.

This workflow demonstrates how `Ransack::Predicate` handles the operator logic while `Ransack::Ransacker` provides the column context, both orchestrated by the search builder in the core library.

## Summary

- **Custom predicates** are registered via `Ransack.configure` in [`lib/ransack/configuration.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/configuration.rb), allowing you to define formatters, validators, and Arel operators for specialized search suffixes.
- **Ransackers** are defined on ActiveRecord models using the `ransacker` class method from [`lib/ransack/adapters/active_record/base.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/adapters/active_record/base.rb), returning Arel nodes for virtual columns.
- The `Ransacker` class in [`lib/ransack/ransacker.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/ransacker.rb) provides the `attr_from` interface that receives the parent table binding and optional arguments.
- Complex queries combining both features can handle sub-queries, database functions, and array operations while maintaining clean controller and view code.

## Frequently Asked Questions

### How do I access the parent table inside a ransacker block?

The `parent` parameter passed to the ransacker block is an Arel table instance representing the model's table. According to [`lib/ransack/ransacker.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/ransacker.rb), this binding allows you to reference columns via `parent[:column_name]` or construct more complex Arel expressions using `Arel::Nodes` classes.

### Can I pass multiple arguments to a custom ransacker?

Yes. Specify the `:args` option as an array including `:parent` and `:ransacker_args`. When invoking the search, provide the `ransacker_args` key with an array of values. The block receives these arguments after the parent parameter, as shown in the `articles_body_length` example in [`spec/support/schema.rb`](https://github.com/activerecord-hackery/ransack/blob/main/spec/support/schema.rb).

### What is the difference between a predicate's formatter and validator?

The **formatter** (defined in [`lib/ransack/predicate.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/predicate.rb)) transforms the incoming search value before it is passed to the Arel predicate—useful for splitting CSV strings or typecasting. The **validator** determines whether the condition should be included in the query at all, typically used to skip empty strings or nil values.

### Where should I register custom predicates in a Rails application?

Register custom predicates in an initializer file such as [`config/initializers/ransack.rb`](https://github.com/activerecord-hackery/ransack/blob/main/config/initializers/ransack.rb). This ensures `Ransack.configure` runs during application boot, populating the global `PredicateCollection` accessed by `Ransack.predicates` before any searches execute.