It’s often desirable to allow a user to search for records in an application with a “simple” form which searches across multiple relevant fields, rather than forcing them to fill out an advanced search form.
A possible solution must:
- search across multiple fields
- be able to traverse ActiveRecord relations
- involve no raw SQL for basic queries
- support multiple search types depending on use-case
Typically one would write a lengthy SQL query (in Rails this may be a raw query or a bit of arel). Instead my proposal is to abstract the query-building part away from the application into a dedicated library–enter ransack.
The advantage of this is that we may use ransack to generate advanced search forms too (this being notionally what it’s for).
Considering the following example app/models/ticket.rb
snippet:
class Ticket < ActiveRecord::Base
# ...
def self.search(query = nil)
if query
params = {
subject_cont: query,
id_eq: query
}
end
ransack(params).result(distinct: true)
end
end
The Ticket.search
method defines what the search will be for, but leaves the
how to ransack, which will build the query and return the result (a standard
ActiveRecord::Relation
).
This satisfies all the requirements (we could have multiple methods on Ticket
for different types of search if required).
Usage
A corresponding controller (in a json-only application at least) may contain the following method:
# app/controllers/tickets_controller.rb
class TicketsController < ApplicationController
# ...
def index
@tickets = Ticket.search(params[:search])
render json: @tickets
end
# ...
end
Searching on composite/calculated column
Sometimes you’ll want to apply a database function to transform a column (or columns) for searching. Ransack provides a method for doing this using a custom ransacker to return an arel node representing the composite column.
I could search for people based on their full name (e.g.: by using
full_name_cont
) and the following ransacker:
class Person < ActiveRecord::Base
# ...
ransacker :full_name do |parent|
Arel::Nodes::NamedFunction.new('CONCAT_WS', [
Arel::Nodes::SqlLiteral.new('" "'),
parent.table[:first_name], parent.table[:last_name]
])
end
end
And even traverse relations with these complex fields:
class Ticket < ActiveRecord::Base
belongs_to :person
def self.search(query = nil)
if query
params = {
full_name_cont: query,
subject_cont: query,
id_eq: query
}
end
ransack(params).result(distinct: true)
end
end