Simple named_scope Searching

Matt Swasey, Former Viget

Article Category: #Code

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:

  1. Param's keys are iterated over, setting the initial value for inject to an anonymous scope.
  2. Iterating over the key names, we return the current collection 'found' if the current key has no value.
  3. 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.

Related Articles