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 #
- Simple to use.
- Minimal DSL.
Cons #
- Only works with Rails.
- Couples logic to controllers.
- Controller filters only can be applied to one resource at the time.
- Architecture doesn’t encourage object-oriented design.
- Hard to test.
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 #
- You can use it with any Framework, ORM or Object.
- Decouples logic from the controller. You can use your search class wherever you want.
- You can use different search classes inside your controller at the same time.
- Easy to test.
Cons #
- Search classes are attached to the defined target by using search_on method. This means that you can’t reuse this class with another resource.
- Search methods are not encapsulated; they are accessible from outside of the class itself.
- Search class definition is complex.
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 #
- You can use it with any Framework, ORM or Object.
- Decouples logic from the controller. You can use your search class wherever you want.
- You can use different search classes inside your controller at the same time.
- Easy to test.
- Search classes are not attached to a specific resource. You can use any resource with your search class.
- Search methods are encapsulated; they are not accessible from outside the class itself.
- No DSL used to define your search class, only POROs.
- Encourages Object Oriented Design and the use of Design Patterns.
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.