Complex Object Searching in Rails with Ransack

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:

  1. search across multiple fields
  2. be able to traverse ActiveRecord relations
  3. involve no raw SQL for basic queries
  4. 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