Simple named_scope Searching
Matt Swasey, Former Viget
Article Category:
Posted on
One of the coolest aspects of named_scopes in ActiveRecord is their ability to chain together. One can chain a number of named_scopes onto each other, and the result is a single SQL query. Using this feature, I've come up with an easy way to do some simple searching.
To begin I'll create a search form with fields for searching on 'Artist' and 'Song title.'
<div>
<% form_tag search_songs_path, :method => :get do %>
<p>
<%= label_tag "Artist" %><br/>
<%= text_field_tag "search[by_artist]", params[:search].try(:[], :by_artist) %>
</p>
<p>
<%= label_tag "Song title" %><br/>
<%= text_field_tag "search[by_name]", params[:search].try(:[], :by_name) %>
</p>
<p>
<%= submit_tag "Search" %>
</p>
<% end %>
</div>
This form will submit it's elements as GET parameters to a search action. This is a custom action, so we need to define it in our routes file:
map.resources :songs, :collection => {:search => :get}
And in app/controllers/songs_controller.rb
....
def search
@songs = Song.search(params[:search])
render :index
end
....
The search action will call Song.search, and pass the param values attached to :search.
Implementing the Search #
We need a class method in our Song model that will find records based on our params. What I want is to turn this:
{:by_name => "Twist and Shout", :by_artist => "The Beatles"}
Into this:
Song.by_name("Twist and Shout").by_artist("The Beatles")
Here's what I came up with:
def self.search(params)
params.keys.inject(scoped({})) do |found, k|
params[k].blank? ? found : found.send(:"#{k}", params[k])
end
end
And a named_scope for every key:
named_scope :by_artist, lambda {|name| {:conditions => {:artist => name}}}
named_scope :by_name, lambda {|name| {:conditions => {:name => name}}}
Let's step through what's happening in Song.search:
- Param's keys are iterated over, setting the initial value for inject to an anonymous scope.
- Iterating over the key names, we return the current collection 'found' if the current key has no value.
- If the key does have a value, the key name is called on the collection with it's corresponding value as the arguments. Since 'found' is always a scope, we can do this as many times as needed, creating our scope chain.
Conclusion #
If I had to preform searching of a more complex nature, I might choose an library like ThinkingSphinx. If you want something quick, simple, and relativly flexible, I think this approach is worth a try.