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

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 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 that expose virtual attributes. The Ransacker class (defined in 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, 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:


# 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) 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:

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), 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 (lines 89-91), exposes a reversed name column:


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


# 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 (lines 27-39), enables searches like:

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:

<%= 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, 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, returning Arel nodes for virtual columns.
  • The Ransacker class in 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, 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.

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

The formatter (defined in 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. This ensures Ransack.configure runs during application boot, populating the global PredicateCollection accessed by Ransack.predicates before any searches execute.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →