# How to Implement Custom Attribute Methods (Ransackers) for Advanced Data Manipulation in Rails

> Learn to implement custom attribute methods ransackers for advanced data manipulation in Rails. Generate complex SQL for computed values and subqueries within the Rails interface.

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

---

**Ransackers let you define custom searchable attributes in ActiveRecord models by wrapping Arel nodes, enabling complex SQL generation for computed values, JSON queries, and sub-queries without leaving the Rails query interface.**

The Ransack gem (`activerecord-hackery/ransack`) provides a powerful DSL for building search forms, but standard column-based searching often falls short when you need to query computed data or complex data structures. By implementing custom ransackers, you can expose arbitrary Arel expressions as searchable attributes, letting users filter by anything from reversed strings to JSONB keys and correlated subqueries.

## Understanding the Ransacker Architecture

A ransacker is essentially a lightweight wrapper object that instructs Ransack how to build SQL for a custom virtual attribute.

### Core Implementation in lib/ransack/ransacker.rb

The `Ransack::Ransacker` class, located in [`lib/ransack/ransacker.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/ransacker.rb), stores the configuration and callable logic that transforms user input into Arel nodes. Each instance tracks:

- **name**: The symbol used in search predicates (e.g., `:reversed_name`)
- **type**: The result type (`:string`, `:date`, `:integer`) for type casting—defaults to `:string`
- **args**: Arguments passed to the block, defaulting to `[:parent]` to receive the Arel table
- **formatter**: An optional proc that preprocesses user-supplied values before comparison
- **callable**: The block or method that constructs the Arel node

When initialized, the class delegates execution via `attr_from`, which invokes the callable with the appropriate arguments:

```ruby
def attr_from(bindable)
  call(*args.map { |arg| bindable.send(arg) })
end

```

### Registration via ActiveRecord Adapter

When you call `ransacker :foo` in a model, the `Ransack::Adapters::ActiveRecord::Base` module (loaded by [`lib/ransack/active_record.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/active_record.rb)) registers the definition in a class-level `_ransackers` hash. During a search, `Ransack::Search` looks up the ransacker by name, builds the Arel node via `attr_from`, applies any `formatter`, and combines it with the requested predicate (`_eq`, `_cont`, etc.).

## Implementing Custom Ransackers for Data Manipulation

Because ransackers can return any Arel expression, you can implement sophisticated query patterns directly in your models.

### Transforming Values with Formatters

Use a `formatter` proc to preprocess search values while defining the ransacker to transform the underlying column data. This example reverses the search input and matches against the database column:

```ruby

# app/models/person.rb

class Person < ApplicationRecord
  ransacker :reversed_name, formatter: proc { |v| v.reverse } do |parent|
    parent.table[:name]
  end
end

```

In your controller and view:

```ruby

# app/controllers/people_controller.rb

def index
  @q = Person.ransack(params[:q])
  @people = @q.result(distinct: true)
end

```

```erb
<!-- app/views/people/index.html.erb -->
<%= search_form_for @q do |f| %>
  <%= f.search_field :reversed_name_eq, placeholder: "Reverse of name" %>
  <%= f.submit "Search" %>
<% end %>

```

Users typing `"ecilA"` will match records where `name` equals `"Alice"`.

### Querying JSONB Keys (PostgreSQL)

Expose specific keys within JSONB columns as searchable attributes using Arel infix operations:

```ruby

# app/models/product.rb

class Product < ApplicationRecord
  ransacker :link_type do |parent|
    Arel::Nodes::InfixOperation.new(
      '->>', parent.table[:properties], Arel::Nodes.build_quoted('link_type')
    )
  end
end

```

This generates SQL like:

```sql
SELECT "products".* FROM "products"
WHERE "products"."properties" ->> 'link_type' = 'twitter';

```

### Concatenating Multiple Columns

Create a computed full-name search that is case-insensitive and handles multi-byte characters:

```ruby

# app/models/user.rb

class User < ApplicationRecord
  ransacker :full_name, formatter: proc { |v| v.mb_chars.downcase.to_s } do |parent|
    Arel::Nodes::NamedFunction.new(
      'LOWER',
      [
        Arel::Nodes::NamedFunction.new(
          'concat_ws',
          [
            Arel::Nodes::SqlLiteral.new("' '"),
            parent.table[:first_name],
            parent.table[:last_name]
          ]
        )
      ]
    )
  end
end

```

Users can now search `full_name_cont` to match across both first and last names as if they were a single normalized column.

### Accepting Dynamic Arguments with ransacker_args

For complex logic requiring runtime parameters, use the `args` option to accept `ransacker_args`:

```ruby

# app/models/person.rb

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

```

Invoke with explicit arguments:

```ruby
Person.ransack(
  conditions: [{
    attributes: {
      '0' => {
        name: 'author_max_title_of_article_where_body_length_between',
        ransacker_args: [10, 100]
      }
    },
    predicate_name: 'cont',
    values: ['Rails']
  }]
).result

```

This returns people whose longest article title (among articles with body length between 10-100 characters) contains "Rails".

### Boolean Existence Checks via Sub-queries

Implement boolean filters based on the existence of related records using raw SQL sub-queries:

```ruby

# app/models/book.rb

class Book < ApplicationRecord
  ransacker :price_exists do |parent|
    Arel.sql("(SELECT EXISTS (SELECT 1 FROM prices WHERE prices.book_id = books.id))")
  end
end

```

In the view:

```erb
<%= f.select :price_exists_true, [["Any", 2], ["No", 0], ["Yes", 1]] %>

```

This pattern filters books based on whether associated price records exist, generating efficient SQL `EXISTS` clauses.

## How Ransackers Generate SQL

The lifecycle from form parameter to SQL clause follows a specific path through the Ransack internals:

1. **Lookup**: `Ransack::Search` queries the model's `_ransackers` hash (populated by [`lib/ransack/active_record.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/active_record.rb)) to find the named ransacker.
2. **Binding**: The search object calls `attr_from`, passing the bindable context (typically the parent model's Arel table).
3. **Execution**: The ransacker's `callable` block executes, returning an Arel node—whether a simple column reference, a function call, or a sub-query.
4. **Formatting**: If a `formatter` is defined, the user-supplied value passes through it before comparison.
5. **Composition**: Ransack combines the generated Arel node with the predicate method (like `eq` or `matches`) to produce the final SQL condition.

As implemented in [`lib/ransack/ransacker.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/ransacker.rb), this architecture separates the concerns of SQL generation from search logic, allowing you to define complex database operations declaratively while maintaining full access to Arel's expressive power.

## Summary

- **Ransackers** are custom searchable attributes defined via the `ransacker` DSL in ActiveRecord models, backed by the `Ransack::Ransacker` class in [`lib/ransack/ransacker.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/ransacker.rb).
- Each ransacker stores a **callable** (block or method) that builds Arel nodes, an optional **formatter** for input transformation, and a **type** for casting.
- Registration occurs in the `_ransackers` class attribute via [`lib/ransack/active_record.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/active_record.rb), making custom attributes available to `Ransack::Search`.
- You can query **JSONB keys**, **concatenate columns**, accept **dynamic arguments** via `ransacker_args`, and execute **sub-queries** by returning appropriate Arel nodes or raw SQL.
- The `formatter` option preprocesses search values, enabling transformations like case normalization or string reversal before database comparison.

## Frequently Asked Questions

### What is the difference between a ransacker and a regular ActiveRecord scope?

A ransacker exposes a virtual column that can be used with any Ransack predicate (`_eq`, `_cont`, `_gteq`, etc.) in search forms, whereas a scope requires predefined logic and cannot be dynamically combined with other search parameters. Ransackers appear as fields in `search_form_for` helpers and generate SQL through the `attr_from` method, while scopes are method-based filters you call directly on the relation.

### How do I specify the return type for proper casting in my ransacker?

Set the `:type` option when defining the ransacker to match your database operation's result. According to the source in [`lib/ransack/ransacker.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/ransacker.rb), valid types include `:string`, `:date`, `:datetime`, `:integer`, `:float`, and `:boolean`. The type determines how Ransack casts user input before generating the comparison. For example, `ransacker :custom_date, type: :date` ensures date-specific predicates work correctly.

### Can I use associations inside a ransacker block?

Yes. While the default `args: [:parent]` provides the base table's Arel reference, you can construct joins or sub-queries manually using Arel. The fourth example above demonstrates querying an associated `articles` table via a correlated subquery. For simple associations, ensure you preload or join the association in your controller's `result` call to avoid N+1 queries, as ransackers only affect the `WHERE` clause generation.

### Why does my ransacker raise an error when I use it with the `_cont` predicate?

The `_cont` predicate attempts to use SQL `LIKE` patterns, which requires the ransacker to return a string-compatible Arel node. If your ransacker returns a boolean, integer, or complex expression incompatible with `LIKE`, Ransack will generate invalid SQL. Either use predicate aliases compatible with your return type (like `_eq` for booleans) or wrap your ransacker logic in a string conversion function such as `CAST` or `COALESCE` within the Arel node construction.