How to Save and Merge Multiple Ransack Search Queries in Rails

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, implement a search_params method that synchronizes the session with incoming parameters:

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:

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

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 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 lines 23-38) constructs the root Grouping node while storing the shared context reference.
  • Arel conversion: Ransack::Visitor#accept (in lib/ransack/visitor.rb) traverses the node tree and returns Arel-compatible where clauses.
  • Join compilation: Context#join_sources (utilizing 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:


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

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, lib/ransack/visitor.rb, and 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) 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.

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 →