Searching, sorting, and filtering in Rails controllers can be a pain. ElasticSearch and Solr are great, high-powered solutions, but are really big dependencies for a small app.

Luckily, Rails includes scopes, which can provide you with a lot of what you need for simple searching, filtering, and sorting. If you take advantage of scope chaining, you can build the features you want without taking on big dependencies or writing a bunch of repetitive search code yourself.

Searching with scopes

Imagine an #index method on your RESTful controller that shows a table of products. The products can be active, pending, or inactive, are available in a single location, and have a name.

If you want to be able to filter these products, you can write some scopes:

app/models/product.rb
1
2
3
4
5
class Product < ActiveRecord::Base
  scope :status, -> (status) { where status: status }
  scope :location, -> (location_id) { where location_id: location_id }
  scope :starts_with, -> (name) { where("name like ?", "#{name}%")}
end

Each of these scopes defines a class method on Product that you can use to limit the results you get back.

1
@products = Product.status("active").starts_with("Ruby") # => All active products whose names start with 'Ruby'

Your controller can use these scopes to filter your results:

app/controllers/product_controller.rb
1
2
3
4
5
6
def index
  @products = Product.where(nil) # creates an anonymous scope
  @products = @products.status(params[:status]) if params[:status].present?
  @products = @products.location(params[:location]) if params[:location].present?
  @products = @products.starts_with(params[:starts_with]) if params[:starts_with].present?
end

And now you can show just the active products with names that start with ‘Ruby’.

http://example.com/products?status=active&starts_with=Ruby

Clearly, this needs some cleanup

You can see how this code starts to get unwieldy and repetitive! Of course, you’re using Ruby, so you can stuff this in a loop:

app/controllers/product_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
def index
  @products = Product.where(nil)
  filtering_params(params).each do |key, value|
    @products = @products.public_send(key, value) if value.present?
  end
end

private

# A list of the param names that can be used for filtering the Product list
def filtering_params(params)
  params.slice(:status, :location, :starts_with)
end

A more reusable solution

You can move this code into a module and include it into any model that supports filtering:

app/models/concerns/filterable.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
module Filterable
  extend ActiveSupport::Concern

  module ClassMethods
    def filter(filtering_params)
      results = self.where(nil)
      filtering_params.each do |key, value|
        results = results.public_send(key, value) if value.present?
      end
      results
    end
  end
end
app/models/product.rb
1
2
3
4
class Product
  include Filterable
  ...
end
app/controllers/product_controller.rb
1
2
3
def index
  @products = Product.filter(params.slice(:status, :location, :starts_with))
end

You now have filtering and searching of your models with one line in the controller and one line in the model. How easy is that? You can also get built-in sorting by using the built-in order class method, but it’s probably a better idea to write your own scopes for sorting. That way you can sanity-check the input.

To save you some effort, I put Filterable into a gist. Give it a try in your own project, It’s saved me a lot of time and code.

Don't miss out on my next essay

Sign up below to get my free weekly Ruby column. I'll send you original articles and advice every Friday to help make you a smarter, better Ruby developer. Drop your name in the box!

Did you like this post? You should read these:

Comments