# How to Save and Merge Multiple Ransack Search Queries in Rails

> Learn to save and merge multiple Ransack search queries in Rails using Ransack::Context and Ransack::Visitor. Persist searches across requests with session storage for efficient filtering.

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

---

**Ransack provides the `Ransack::Context` and `Ransack::Visitor` classes to combine independent searches into a single SQL query, while session storage handles persistence across requests.**

The `activerecord-hackery/ransack` library offers powerful building blocks to persist user filters across page loads and merge separate search conditions into unified ActiveRecord queries. When you need to maintain filter state during pagination or build complex OR-based queries across associations, you can leverage Ransack's low-level APIs to save and merge multiple Ransack search queries efficiently.

## Persisting Ransack Queries Across Requests

Ransack does not automatically store query parameters between requests, but you can implement session-based persistence using standard Rails controller patterns. This approach ensures that user-selected filters survive page reloads, pagination clicks, and navigation away from the index page.

### Session-Based Persistence Pattern

Store the search hash (`params[:q]`) in the session, falling back to stored values when fresh parameters are absent. In [`app/controllers/application_controller.rb`](https://github.com/activerecord-hackery/ransack/blob/main/app/controllers/application_controller.rb), implement a `search_params` method that synchronizes the session with incoming parameters:

```ruby
class ApplicationController < ActionController::Base
  private

  def search_params
    params[:q] ||= session[search_key]
    session[search_key] = params[:q] if params[:q]
    params[:q]
  end

  def clear_search_index
    return unless params[:search_cancel]

    params.delete(:search_cancel)
    session.delete(search_key)
  end

  def search_key
    "#{controller_name}_search".to_sym
  end
end

```

Use these methods in your resource controllers to maintain filter state:

```ruby
class PeopleController < ApplicationController
  def index
    clear_search_index if params[:search_cancel]

    @search = Person.ransack(search_params)
    @people = @search.result.distinct.page(params[:page])
  end
end

```

This works because `search_params` returns the same hash for every request within a controller scope, and the hash is mutated in-place by Ransack when you call `ransack`, ensuring subsequent calls like pagination carry identical filters.

### Alternative: Ransack Memory Gem

If you prefer a drop-in solution rather than custom controller code, the community-maintained **Ransack Memory** gem automates this persistence layer. See the documentation in [`docs/going-further/saving-queries.md`](https://github.com/activerecord-hackery/ransack/blob/main/docs/going-further/saving-queries.md) and the gem's README for implementation details.

## Merging Multiple Ransack Searches

When you need to combine several independent search conditions—such as filtering on two different associations with an OR relationship—you must share a single `Ransack::Context` across all searches to prevent table alias collisions.

### Combining Searches with Shared Context

In your model, create a method that instantiates one context via `Ransack::Context.for`, then passes that context to multiple `ransack` calls. Collect the resulting Arel nodes using `Ransack::Visitor` and combine them with `OR` or `AND`:

```ruby
class Person < ApplicationRecord
  def self.with_parent_or_child(name_for_parent, name_for_child)
    shared_context = Ransack::Context.for(self)

    parent_search = ransack({ parent_name_eq: name_for_parent }, context: shared_context)
    child_search  = ransack({ children_name_eq: name_for_child }, context: shared_context)

    conditions = [parent_search, child_search].map do |s|
      Ransack::Visitor.new.accept(s.base)
    end

    joins(shared_context.join_sources)
      .where(conditions.reduce(&:or))
  end
end

```

The resulting SQL generates unique aliases for each joined table (e.g., `"parents_people"` and `"children_people"`), preventing collisions when the query executes.

### How Ransack Builds Merged Queries

Understanding the internal pipeline helps debug complex merges:

- **Context creation**: `Ransack::Context.for` (defined in [`lib/ransack/context.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/context.rb) lines 24-44) initializes join dependencies and searchable keys, ensuring unique alias generation across searches.
- **Search building**: `Ransack::Search#initialize` (in [`lib/ransack/search.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/search.rb) lines 23-38) constructs the root `Grouping` node while storing the shared context reference.
- **Arel conversion**: `Ransack::Visitor#accept` (in [`lib/ransack/visitor.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/visitor.rb)) traverses the node tree and returns Arel-compatible where clauses.
- **Join compilation**: `Context#join_sources` (utilizing [`lib/polyamorous/join.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/polyamorous/join.rb)) aggregates all required `LEFT OUTER JOIN` statements before query execution.

Without a shared context, each `Ransack::Search` generates independent table aliases that conflict when combined, producing invalid SQL. The shared context guarantees consistent alias naming across all merged searches.

### Advanced Combination Techniques

For more than two queries, collect visitor results in an array and reduce them using Arel's boolean operators:

```ruby

# OR-combine multiple conditions

combined = conditions.reduce { |memo, cond| memo.or(cond) }

# AND-combine multiple conditions

combined = conditions.reduce { |memo, cond| memo.and(cond) }

```

## Reusable Controller Concern

Encapsulate both persistence and merging logic in a single concern to keep controllers tidy. Create [`app/controllers/concerns/ransack_searchable.rb`](https://github.com/activerecord-hackery/ransack/blob/main/app/controllers/concerns/ransack_searchable.rb):

```ruby
module RansackSearchable
  extend ActiveSupport::Concern

  included do
    helper_method :search_params, :clear_search_index
  end

  def search_params
    params[:q] ||= session[search_key]
    session[search_key] = params[:q] if params[:q]
    params[:q]
  end

  def clear_search_index
    return unless params[:search_cancel]

    params.delete(:search_cancel)
    session.delete(search_key)
  end

  def merge_ransack_queries(search_hashes, combine: :or, model: nil)
    raise ArgumentError, 'model required' unless model
    shared_context = Ransack::Context.for(model)

    visitors = search_hashes.map do |hash|
      Ransack::Visitor.new.accept(
        model.ransack(hash, context: shared_context).base
      )
    end

    combined = visitors.reduce do |memo, cond|
      combine == :or ? memo.or(cond) : memo.and(cond)
    end

    { arel: combined, context: shared_context }
  end

  private

  def search_key
    "#{controller_name}_search".to_sym
  end
end

```

Including this concern in any controller provides both `search_params` for persistence and `merge_ransack_queries` for combining multiple search hashes programmatically.

## Summary

- **Persist searches** by reading from and writing to the Rails session in a `search_params` controller method, ensuring filters survive pagination and navigation.
- **Merge searches** by sharing a `Ransack::Context` instance across multiple `ransack` calls, converting each to Arel nodes via `Ransack::Visitor`, and combining them with `or` or `and` before applying `join_sources`.
- **Avoid alias collisions** by always passing the shared context to every search operation; without it, independent searches generate conflicting table aliases.
- **Reference key files** including [`lib/ransack/context.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/context.rb), [`lib/ransack/visitor.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/visitor.rb), and [`lib/polyamorous/join.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/polyamorous/join.rb) when extending these patterns.

## Frequently Asked Questions

### Can I merge more than two Ransack searches at once?

Yes. Collect all visitor results into an array and use `reduce` to fold them together with either `or` or `and` operations. Each search hash must be processed using the same shared `Ransack::Context` instance to ensure consistent table aliasing across the entire query.

### Why does merging searches without a shared context fail?

Each `Ransack::Search` instance independently generates table aliases (e.g., `"parents_people"`). When you combine two searches with different contexts, they may generate identical alias names for different associations, causing SQL syntax errors. The shared context tracks alias generation globally, ensuring uniqueness.

### How do I clear saved Ransack filters from the session?

Implement a `clear_search_index` method that checks for a specific parameter (such as `params[:search_cancel]`), deletes that parameter, and removes the session key using `session.delete(search_key)`. Call this method at the beginning of your index action before initializing the search.

### Is there a gem that handles Ransack query persistence automatically?

Yes, the **Ransack Memory** gem (referenced in [`docs/going-further/saving-queries.md`](https://github.com/activerecord-hackery/ransack/blob/main/docs/going-further/saving-queries.md)) provides drop-in session persistence for Ransack queries. It eliminates the need for custom controller code by automatically storing and retrieving `params[:q]` across requests.