I really like how Lovetastic’s search works. Instead of a plethora of select boxes, users can simply use expressions to create searches. We did something similar with Tryst’s searching. One text field and you enter stuff like age ranges (30-45), things like smoke-yes or smoke-no, cities, zipcodes, etc.

To make this whole thing more Rails like, I did not want to just use Regex to parse out search terms and plug them into a complicated SQL query. That would be soooooo PHP.
Named Scopes are awesome. You can use them to built decently formatted and fairly complicated SQL queries in a nice, no bullshit, manner. You can do cool stuff like this:
@users = User.active.males.in_zipcode(params[:zip_code]).nonsmokers.top_ten
Just keep stacking scopes on. Rails generates the query and it usually doesn’t suck.
Go read the API docs for the basics on Named Scopes. We’re gonna do some not basic stuff. Check out Ryan Bate’s Anonymous Scopes Railscast, which introduced me to this method of generating Scopes.
We wanted to add a Class Method called search and have it define the Scopes. We pass in the params hash, anything else we need (like the current user), and let it handle the Scoping and determining what we’re looking for from the search params.
To start, here is our search class method.
# app/models/user.rb
def self.search(params, current_user)
page = params[:page] || 1
search_terms = params[:search]
end
Yeah, using Will Paginate and assigning my search terms to a local variable. I’ve also written another Class Method that does the parsing of the params.
# app/models/user.r.b
def self.search(params, current_user)
page = params[:page] || 1
search_terms = params[:search]
#parse params
max_age, min_age, zip_codes, gender, smoke, drink = self.get_parameters(search_terms.to_s, current_user)
end
Now, we’re going to define some dynamic scopes. This is awesome. For it to work, we’ll add an initializer.
#config/initializers/scopes.rb
class ActiveRecord::Base
named_scope :conditions, lambda { |*args| {:conditions => args} }
end
Now, lets make the scopes.
def self.search(params, current_user)
page = params[:page] || 1
search_terms = params[:search]
max_age, min_age, zip_codes, gender, smoke, drink = self.get_parameters(search_terms.to_s, current_user)
scope = User.scoped({})
scope = scope.conditions "users.age >= ? and users.age <= ?", min_age, max_age unless min_age.blank?
scope = scope.conditions "users.zip_code in (?)", zip_codes unless zip_codes.blank?
scope = scope.conditions "users.smoker = ?", smoke unless smoke.blank?
scope = scope.conditions "users.drink = ?", drink unless drink.blank?
scope = scope.conditions "users.gender = ?", gender unless gender.blank?
scope.paginate :page => page, :order => "created_at DESC"
end
Pretty slick, huh? So now, in my controller I just need
# app/controllers/search.rb
def users
@users = User.search(params, current_user)
end
Assuming your class method for parsing out scope parameters doesn’t suck, you should have very clean, cuddly, and concise searching. No more worrying about breaking some huge crappy SQL query.
You’ll notice I used
users.zip_codes in (?)
This is because I can pass an array of zipcodes and return all the users in those zipcodes. This is because I’ve implemented radial distance searching base on zipcodes. Users can search like ” 60606 25miles” and return all users who are within 25 miles of 60606. We’ll go into how this works in the next post.