Search Filters and Object Oriented Design

Search filters are a very common feature in any application.

I’ve used two gems for several projects I worked on: has_scope and searchlight.

Both gems work out of the box, but they are not as flexible as I would like them to be, and that’s why I decided to create my gem, Lupa.

I decided to write an overview about these 3 gems:

 HasScope

has_scope will map your controller filters to your model scopes.

# app/controllers/products_controller.rb

class ProductsController < ApplicationController
  has_scope :featured, type: :boolean
  has_scope :by_price
  has_scope :by_period, using: [:started_at, :ended_at], type: :hash

  def index
    @products = apply_scopes(Product).all
  end
end
# app/models/product.rb

class Product < ActiveRecord::Base
  scope :featured, -> { where(featured: true) }
  scope :by_price, -> price { where(price: price) }
  scope :by_period, -> started_at, ended_at { where("started_at = ? AND ended_at = ?", started_at, ended_at) }
end

 Pros

 Cons

 Searchlight

Searchlight does a similar work as HasScope but with a different approach by moving all the logic to its own class.

# app/controllers/products_controller.rb

class ProductsController < ApplicationController

  def index
    @products = ProductSearch.search(search_params).results
  end

  protected

    def search_params
      params.require(:product_search).permit(:featured, :by_price, :by_period)
    end
end
# app/searches/product_search.rb

class ProductSearch < Searchlight::Search

  search_on Product.all

  searches :featured, :by_price, :by_period

  def search_featured
    search.where(featured: true)
  end

  def search_by_price
    search.where(price: price)
  end

  def search_by_period
    started_at = by_period[:started_at]
    ended_at   = by_period[:end_at]

    search.where("started_at = ? AND ended_at = ?", started_at, ended_at)
  end
end

 Pros

 Cons

 Lupa

Lupa works similar to Searchlight but only uses POROs and has no DSL.

# app/controllers/products_controller.rb

class ProductsController < ApplicationController

  def index
    @products = ProductSearch.new(Product.all).search(search_params)
  end

  protected

    def search_params
      params.permit(:featured, :by_price, :by_period)
    end
end
# app/searches/product_search.rb

class ProductSearch < Lupa::Search
  class Scope

    def featured
      scope.where(featured: true)
    end

    def by_price
      scope.where(price: search_attributes[:price])
    end

    def search_by_period
      started_at = by_period[:started_at]
      ended_at   = by_period[:end_at]

      scope.where("started_at = ? AND ended_at = ?", started_at, ended_at)
    end

  end
end

 Pros

 Benchmarks

Besides the pros and cons of each gem, I decided benchmark this gems. I used benchmark-ips to perform the benchmarks.

 Lupa vs HasScope

Calculating -------------------------------------
                lupa   265.000  i/100ms
           has_scope   254.000  i/100ms
-------------------------------------------------
                lupa      3.526k (±24.7%) i/s -     67.045k
           has_scope      3.252k (±24.8%) i/s -     61.976k

Comparison:
                lupa:     3525.8 i/s
           has_scope:     3252.0 i/s - 1.08x slower

 Lupa vs Searchlight

Calculating -------------------------------------
                lupa   480.000  i/100ms
         searchlight   232.000  i/100ms
-------------------------------------------------
                lupa      7.273k (±25.1%) i/s -    689.280k
         searchlight      2.665k (±14.1%) i/s -    260.072k

Comparison:
                lupa:     7273.5 i/s
         searchlight:     2665.4 i/s - 2.73x slower

 Conclusion

The 3 gems have a lot a cool features that I did not mention in this article. If you don’t know some of the gems, I suggest you to check out the documentation on the repository of them.

 
83
Kudos
 
83
Kudos

Now read this

Strong Parameters The Right Way

StrongParameters is a great gem and it comes with Rails 4 by default. Currently, there are two patterns to work with your attributes. 1. Creating a private method on your controller which returns the whitelisted attributes. #... Continue →