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.

 
103
Kudos
 
103
Kudos

Now read this

Most Common Mistakes On Legacy Rails Apps

Lately I’ve been supporting several legacy projects. As many of you may know, working on legacy projects sucks most of the time, usually because most of the code is ugly and difficult to understand or read. I decided to make a list of... Continue →