# How Ransack Translates Search Parameters into SQL Using the Visitor/AST Pattern

> Discover how Ransack translates search parameters to SQL via the Visitor/AST pattern. Learn to build powerful search queries by understanding its internal mechanism for generating Arel predicates.

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

---

**Ransack converts search parameters into an abstract syntax tree (AST) of node objects, then uses a Visitor pattern to traverse the tree and generate Arel predicates that ActiveRecord compiles into SQL.**

The [activerecord-hackery/ransack](https://github.com/activerecord-hackery/ransack) gem provides a powerful search interface for Rails applications. Understanding how Ransack translates search parameters into SQL using the Visitor/AST pattern reveals how it supports complex queries, custom predicates, and multiple database adapters while maintaining a clean public API.

## Building the AST from Search Parameters

Ransack begins by transforming the incoming parameter hash into a tree of node objects. This AST represents the logical structure of the query, including conditions, groupings, and sort orders.

### The Search Class and Parameter Parsing

The entry point is `Ransack::Search`, which receives raw parameters in its initializer and delegates to `#build`.

```ruby

# lib/ransack/search.rb

def build(params)
  collapse_multiparameter_attributes!(params).each do |key, value|
    if base.attribute_method?(key)
      base.send("#{key}=", value)  # Creates Grouping/Condition nodes

    end
  end
end

```

This method iterates through the parameter hash, creating the appropriate node types based on the key names. Simple predicates like `name_cont` become `Condition` nodes, while the `g` key triggers nested `Grouping` creation.

### Grouping Nodes and Logical Structure

The base node is a `Ransack::Nodes::Grouping` instance that holds child conditions and sub-groupings. In [`lib/ransack/nodes/grouping.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/nodes/grouping.rb), the `conditions=` setter instantiates `Condition` objects:

```ruby

# lib/ransack/nodes/grouping.rb

def conditions=(conditions)
  conditions.each do |attrs|
    condition = Condition.new(@context).build(attrs)
    self.conditions << condition
  end
end

```

Groupings maintain a `combinator` property (`:and` or `:or`) that determines how to join their children when the Visitor traverses the tree.

### Condition Nodes and Predicates

The `Condition` class in [`lib/ransack/nodes/condition.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/nodes/condition.rb) represents a single predicate comparison. It stores the attribute name, predicate type (eq, cont, gt, etc.), and value. The critical method is `arel_predicate`, which constructs the actual Arel node:

```ruby

# lib/ransack/nodes/condition.rb

def arel_predicate
  attributes.map { |attribute|
    format_predicate(attribute)
  }.reduce(combinator_method)
end

```

This method maps the Ransack predicate to an Arel method (e.g., `cont` becomes `matches`), handles multiple attributes with the combinator, and returns an Arel predicate node ready for SQL generation.

### Sort Nodes for Ordering

Sort nodes handle the `ORDER BY` clause. Created via `Search#sorts=`, they wrap column names and direction. In [`lib/ransack/nodes/sort.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/nodes/sort.rb), the `to_arel` method returns an `Arel::Nodes::Ordering` instance:

```ruby

# lib/ransack/nodes/sort.rb

def to_arel
  # Returns Arel::Nodes::Ordering instance

  attr.direction == :asc ? attr.asc : attr.desc
end

```

## Traversing the AST with the Visitor Pattern

Once the AST is constructed, Ransack must convert the node tree into Arel objects. This happens through the Visitor pattern implemented in [`lib/ransack/visitor.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/visitor.rb).

### Dispatch Mechanism and Dynamic Method Resolution

The `Visitor` class uses a dynamic dispatch table to route nodes to specific handler methods:

```ruby

# lib/ransack/visitor.rb

DISPATCH = Hash.new do |hash, klass|
  hash[klass] = "visit_#{klass.name.gsub(Constants::TWO_COLONS, Constants::UNDERSCORE)}"
end

```

When `Visitor#accept` receives a node, it looks up the corresponding `visit_*` method name based on the node's class name. This allows the Visitor to handle new node types without modification, following the Open/Closed Principle.

### Converting Conditions to Arel Predicates

For `Condition` nodes, the Visitor delegates to the node's own Arel-building logic:

```ruby

# lib/ransack/visitor.rb

def visit_Ransack_Nodes_Condition(object)
  object.arel_predicate
end

```

This method simply returns the result of `Condition#arel_predicate`, which contains the formatted Arel predicate (e.g., `Arel::Nodes::Matches` for `cont` predicates).

### Handling Logical Combinations (AND/OR)

`Grouping` nodes require combining their children with the appropriate logical operator:

```ruby

# lib/ransack/visitor.rb

def visit_Ransack_Nodes_Grouping(object)
  send(DISPATCH[object.combinator], object)
end

def visit_and(object)
  nodes = object.values.map { |o| accept(o) }.compact
  # Combines nodes with Arel::Nodes::And

end

def visit_or(object)
  left, right = object.values.map { |o| accept(o) }
  Arel::Nodes::Or.new(left, right)
end

```

The Visitor recursively processes child nodes, then combines them using `Arel::Nodes::And` or `Arel::Nodes::Or`, preserving the logical structure defined in the search parameters.

### Processing Sort Nodes

For sorting, the Visitor handles both standard attribute sorting and custom scope-based sorting:

```ruby

# lib/ransack/visitor.rb

def visit_Ransack_Nodes_Sort(object)
  # Returns Arel ordering or scope symbol

  object.to_arel
end

```

## From Arel to Executable SQL

The Visitor returns Arel nodes, but the final SQL generation happens through ActiveRecord's query interface.

### The ActiveRecord Context Adapter

The `Context` class in [`lib/ransack/adapters/active_record/context.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/adapters/active_record/context.rb) serves as the bridge between Ransack's AST and ActiveRecord:

```ruby

# lib/ransack/adapters/active_record/context.rb

def evaluate(search, opts = {})
  viz = Visitor.new
  relation = @object.where(viz.accept(search.base))
  # Handles sorting and scoping...

end

```

This method instantiates the Visitor, accepts the root grouping node to generate the WHERE clause Arel, and applies it to the ActiveRecord relation.

### Compiling the Final Query

When `search.result` is called, the chain executes:

1. `Context#evaluate` creates the Visitor
2. `viz.accept(search.base)` traverses the AST and returns an Arel node
3. `@object.where(arel_node)` applies the predicate
4. ActiveRecord's SQL builder converts the Arel tree to a SQL string when the query executes

```ruby
relation = Article.where(viz.accept(search.base))
sql = relation.to_sql

# => "SELECT \"articles\".* FROM \"articles\" WHERE \"articles\".\"name\" ILIKE '%rails%'"

```

## Complete Example Walkthrough

Here is a comprehensive example demonstrating the full pipeline from parameters to SQL:

```ruby

# app/models/article.rb

class Article < ApplicationRecord; end

# Build a complex search

search = Article.ransack(
  name_cont: "rails",                    # Condition node

  published_eq: true,                    # Condition node

  g: {                                    # Nested Grouping (OR)

    0 => { title_start: "A", m: "or" },
    1 => { title_end: "Z", m: "or" }
  },
  s: "created_at desc"                   # Sort node

)

# Generate the relation and inspect SQL

relation = search.result
puts relation.to_sql

```

**Generated SQL:**

```sql
SELECT "articles".* FROM "articles"
WHERE ("articles"."name" ILIKE '%rails%')
  AND ("articles"."published" = TRUE)
  AND (("articles"."title" LIKE 'A%') OR ("articles"."title" LIKE '%Z'))
ORDER BY "articles"."created_at" DESC

```

**Internal transformation steps:**

| Parameter | Node Type | Visitor Method | Arel Output |
|-------------|-----------|----------------|-------------|
| `name_cont` | `Condition` | `visit_Ransack_Nodes_Condition` | `Arel::Nodes::Matches` |
| `published_eq` | `Condition` | `visit_Ransack_Nodes_Condition` | `Arel::Nodes::Equality` |
| `g` (group) | `Grouping` | `visit_Ransack_Nodes_Grouping` → `visit_or` | `Arel::Nodes::Or` |
| `s` (sort) | `Sort` | `visit_Ransack_Nodes_Sort` | `Arel::Nodes::Ordering` |

## Summary

- **Ransack** converts URL parameters into an **AST** composed of `Grouping`, `Condition`, and `Sort` nodes defined in `lib/ransack/nodes/`.
- The **`Search`** class orchestrates AST construction in [`lib/ransack/search.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/search.rb), parsing predicates like `name_cont` into structured node objects.
- The **`Visitor`** class in [`lib/ransack/visitor.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/visitor.rb) traverses the AST using dynamic dispatch, converting each node into **Arel** objects via methods like `visit_Ransack_Nodes_Condition`.
- **Arel** nodes are combined using `Arel::Nodes::And` and `Arel::Nodes::Or` to preserve logical groupings from the original parameters.
- The **`Context`** adapter in [`lib/ransack/adapters/active_record/context.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/adapters/active_record/context.rb) applies the generated Arel to ActiveRecord relations, which ultimately compile the tree into executable SQL.

## Frequently Asked Questions

### How does Ransack handle complex nested queries with AND/OR logic?

Ransack represents nested logic using `Grouping` nodes that store a `combinator` property (`:and` or `:or`). When the `Visitor` processes a `Grouping` via `visit_Ransack_Nodes_Grouping`, it dispatches to `visit_and` or `visit_or`, which combine child nodes using `Arel::Nodes::And` or `Arel::Nodes::Or`. This allows arbitrarily deep nesting of conditions while preserving the logical structure defined in the search parameters.

### What is the role of Arel in Ransack's query generation?

Arel serves as the intermediate representation between Ransack's AST and the final SQL string. Each `Condition` node implements `arel_predicate` to return an Arel predicate object (such as `Arel::Nodes::Matches` for `cont` queries). The `Visitor` traverses the tree and aggregates these Arel objects, which are then passed to ActiveRecord's `where` method. ActiveRecord ultimately compiles the Arel tree into database-specific SQL when the query executes.

### How does the Visitor pattern enable database adapter flexibility?

The `Visitor` class in [`lib/ransack/visitor.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/visitor.rb) uses a dynamic dispatch table (`DISPATCH`) to map node classes to handler methods like `visit_Ransack_Nodes_Condition`. This separation of traversal logic from node implementation means Ransack can support different database adapters without modifying the AST node classes. The adapter-specific logic (such as handling SQL syntax variations) resides in the `Context` class and Arel compilation, while the Visitor focuses solely on tree traversal and Arel construction.

### Where does the actual SQL string generation happen in the Ransack pipeline?

The final SQL generation occurs outside of Ransack itself, within ActiveRecord's Arel integration. In [`lib/ransack/adapters/active_record/context.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/adapters/active_record/context.rb), the `evaluate` method calls `viz.accept(search.base)` to obtain an Arel node, then passes it to `@object.where()`. When the resulting relation is executed (via `to_sql` or iteration), ActiveRecord's Arel visitors compile the Arel tree into the final SQL string specific to the connected database.