The forty_facets gem
One Gem to rule them all, One gem to find them; One gem to bring them all
This week we released a shiny, tiny gem to get some serious searching action into your Rails application.
The Problem
If you develop rails applications on a regular basis you’ve done this before:
- Hooray new models!
- Hooray new CRUD controller!
- Oh my - that’s a lot of data - lets add some pagination
- How am I supposed to know that X is on page Y - Let’s just quickly add some filtering…
.. and the the tinkering begins. It starts out with a controller action getting bigger and bigger by adding elsif statements inspecting presence and absence of certain parameters. Scopes are being added and eventually the whole thing gets neatly tucked away in a model method.
Then the day comes where you have to do pretty much the same thing for another model - so there is some refactoring to do. Yet another $time_span later, you have another project with different models but the same problem. Having jumped through these hoops often enough to build up enough courage to release this as a gem we built forty_facets
The solution
So what the dogmatic rails developer wants is a simple API to offload all the ugly parameter fiddling to harness all the mighty power ActiveRecord offers to find the desired record in my data.
So in the manner of our all beloved ActiveRecord we created a base class with a few simple class methods to declare your own search class.
So consider having a ActiveRecord model called Movie:
class Movie < ActiveRecord::Base
belongs_to :genre
belongs_to :studio
# with columns for title(string), price(float) and year(int)
end
A matching custom search might look like this:
class MovieSearch < FortyFacets::FacetSearch
model 'Movie'
text :title, name: 'Title'
range :price, name: 'Price'
facet :genre, name: 'Genre'
facet :year, order: Proc.new { |year| -year }
facet :studio, name: 'Studio', order: :name
orders 'Title' => :title,
'price, cheap first' => "price asc",
'price, expensive first' => {price: :desc, title: :desc}
end
This definition creates a class that is capable of doing all the parameter handling, link generation and ActiveRecord filtering to create a search interface like this:
In the controller you simply hand the params hash to the constructor to create a new instance that is able to do the matching filtering and calculations of the facet values. The result collection returned by the search is a native ActiveRecord collection and can be combined with other gems, like will_pagine, just the way you’re used to.
def index
@search = MovieSearch.new(params)
@movies = @search.result.paginate(page: params[:page], per_page: 9)
end
The API of the @seach object offers a simple API to create links to searches with more or less filter values applied. Since there is no fancy ajax action going on here, it’s very easy to render your filters with the markup you want.
It’s as easy as the following five lines to add links to add or remove values for, in this case, the ‘studio’ filter.
- @search.filter(:studio).facet.each do |facet_value|
- if facet_value.slected
= link_to "#{facet_value.name}(facet_value.count)", filter.add(entity).path
- else
= link_to display, filter.remove(entity).path
Performance
But Perfomance …! and search engines … !
.. I hear you scream. Well of course there are some limitations doing all this on the database. But I think I do not have to give you the premature Knuth - and every devop who has to maintain a production environment is happy about every infrastructure component, he has not to keep running.
Don’t get me wrong, I don’t discourage the use of search engines like Solr or Elastic Search. But unless you have that 2.5Million product online shop, use the power the SQL offers you.
Every page request is made with a constant amount of queries - depending on the amount of filters you declare - not on the amount of models in your database. In the demo application we imported 26K Dvd titles into a SQLite database and the response times are absolutely usable in production. So before rolling in the big guns you might consider this little bad boy to enhance your app.