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 inlib/ransack/context.rblines 24-44) initializes join dependencies and searchable keys, ensuring unique alias generation across searches. - Search building:
Ransack::Search#initialize(inlib/ransack/search.rblines 23-38) constructs the rootGroupingnode while storing the shared context reference. - Arel conversion:
Ransack::Visitor#accept(inlib/ransack/visitor.rb) traverses the node tree and returns Arel-compatible where clauses. - Join compilation:
Context#join_sources(utilizinglib/polyamorous/join.rb) aggregates all requiredLEFT OUTER JOINstatements 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_paramscontroller method, ensuring filters survive pagination and navigation. - Merge searches by sharing a
Ransack::Contextinstance across multipleransackcalls, converting each to Arel nodes viaRansack::Visitor, and combining them withororandbefore applyingjoin_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, andlib/polyamorous/join.rbwhen 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →