(SQL statement in the console via a little .irbrc hack that maps the Rails logger to standard out.) Notice that the LIKE condition from the contains clause and the last name condition from find_by_last_name have been merged into the SQL statement.

This works because named scopes are just wrappers around Active Record’s with_scope method — essentially the named scopes create a series of nested scopes that last until the end of the method call chain — the find statement at the end is effectively executed inside all the nested scopes.

In this case, the find_all_by_last_name is called normally, but the resulting array does not, of course, have a contains method.

On the plus side, the last slot isn’t limited to find methods created by Rails — any method you put last in the chain is evaluated as though inside all of the named scopes. For all you will_paginate fans, the paginate method will work here too — but you do need to be careful that limit or offset options are not set on any of the earlier scopes.

Named Scopes and Advanced Search

Last time I had mentioned using advanced scopes to support search. Ryan Bates does a version of this in Railscast 112. Here’s an even more general solution.

Let’s say you have a dynamic search form that lets the users specify options in something sort of like an iTunes smart playlist structure:

The user can specify an arbitrary number of options, to keep this a little simple, we’ll limit this to string fields.

The first step in using named scopes to support this kind of search is to create individual scopes for each possible operator, as I did for contains last time. A little bit of metaprogramming does this neatly. I put this code in configinitializersglobal_named_scopes.rb so it is automatically loaded when Rails starts. The .code is monkey patched inside ActiveRecord::Base

The constant hash keys are the scopes being created, and the values are the operator and also whether the operand needs a LIKE wild card at the beginning or end of the text.

The loop gives creates each named scope, creating the correct :conditions list for it, and along the way ensuring that everything is converted to lower case to support case insensitive search. Now any ActiveRecord can manage things like:

User.does_not_start_with(:first_name, "f")

Which actually has a fighting chance of being useful in general, I think.

The next step is to be able to dynamically generate one of these scopes from arguments

The first line allows the display to say “starts with” and the resulting scope to be starts_with, then the send just calls the appropriate named scope. This can be called individually.

Person.search_by_criteria(:first_name, "starts with", "fr")

But in order to get the search to work full, you need to combine the scopes. And in order to do that, you need to take advantage of a special scope called scoped. The scoped scope lets you create anonymous scopes on the fly, and is defined for all ActiveRecords as follows:

named_scope :scoped, lambda { |scope| scope }

That’s pretty minimalist — all it does is take the arguments passed to it.

The code starts by creating a blank anonymous scope, then inside the loop, the scope is composed together by creating a scope for the individual criterion then taking those options and adding them to the scope being built up — building an anonymous scope each time. The scope.scoped(inside_scope.proxy_options) is somewhat hacky, in that you’re building a scope, then tearing it apart for the options. You need to do something like that because you can’t compose search_by_criteria directly — since it’s not, in and of itself, a scope (or at least, I get SQL errors when I try). A less hacky version might have a separate method that just returned an option hash for each criterion, although I think it’s nice to have everything as a separate scope.

The nice thing about this scope-based solution is that it’s very easy to compose it with global search conditions, such as limiting to active objects, or limiting based on access. They can be composed explicity:

Person.search_by(...).active.for_current_user

Or alternately, conditions can be added to the initial scope in the search_by method. (The production version of this puts some :include options there because some of the search columns are from joined tables…)

Scope And Search Issues

There are a couple of things you should be aware of when using scopes. Merging scopes can be kind of slow, in a url_for kind of way. Especially worth noting is that even if ActiveRecord is caching the result of the SQL query, the scope merge still happens because that’s what generates the SQL code in the first place.

This particular search implementation has a couple of limitations at the moment. Most notable is that since scope condition merge is always via AND, doing a search with OR logic is not possible yet. I’d like to have a clean option for this, but I’m not sure what the best API is. Right now, you can work around it a bit by grabbing the conditions for each clause and doing the OR connecting yourself, as in the following code that does a LIKE search over multiple columns:

Cool. My has_browser plugin does a very similar thing to search_by_criteria, except that it allows you to choose which scopes should be exposed (a little bit like attr_accessible), for security reasons.

The power of named_scopes and of the scoped method is just incredible, and this technique in particular looks really interesting. I figured I should mention that we recently got squirrel working with scoped, which makes complex finders like this both easy to read and build.

Useful stuff. It’s good to see that the chained scopes will get rolled up into a single SQL statement (avoiding the normal AR foo.bar.baz problem, where missing includes cause lots of trips to the DB).

The first example did trip a buzzer for me, tho. IIRC MySQL, LIKEs need to start with a non-wildcard (‘foo%’) to take advantage of the indexes, which are usually btrees built from the left side of string. (http://dev.mysql.com/doc/refman/5.0/en/mysql-indexes.html) Not sure if FULLTEXT indexes help this, haven’t gotten to dig in.

So if this makes it easy to do searches with ‘%foo’ and ‘%foo%’, there’s a noticeable DB impact.

Nice, I’m just learning named_scope and used it in a project this week. I figured there was a way to generate anonymous scopes on the fly to simplify all my crazy conditional logic. This example really helps alot. cheers mate.

James and Ben — in the actual version of this, the user is limited to a specific subset (I think delete_all is excluded because it’ll cause an error when composed with other scopes), plus there are some additional scopes tacked on to all the searches to limit users to objects that they have access to see. Although I probably could do a tighter job of limiting the searches.

Tammer — thanks for the kind words, we use a lot of thoughtbot tools on a daily basis here..

There seems to be a bug in the code, if I do a :
Model.some_scoped_proxy.search_by(@searches)
search_by will ignore the previous named_scopes.
but if I do
Model.search_by(@searches).some_scoped_proxy
that will generate the proper sql.

Also I had a seperate question, I tried making my own simple example based off your code. I want a order named_proxy on all AR models, but my code doesn’t work because when it calls column_names the lambda thinks its calling it on ActiveRecord::Base instead of the subclassed model. Why does this not work?

Also one other thing I noticed about scopes is that they dont override previous scopes, so for example Car.order(‘a’).order(‘b’)
I would think that following the “ruby way” of least suprise, the order would on b, but its actually on a

well any thing you are thinking of building a mobile site? If yes, here are some guidelines for creating the mobile site. While initiating with this, you are advised to consider a lot of points, which includes both hardware and software. Though you will be able to create but knowledge over the two aspects is going to give you a clear and better platform. There are thousands of companies and hundreds of sets entering this mobile world every now and then. Every handset has specific features, when it comes to size of screen, operating system or the resolution. So just for your convenience, described below are the points to enhance both the looks and functionality of the website.
The foremost point while building mobile sites is to consider the platform compatibility check, which urges you to design or create your website compatible enough with all of the operating systems as Windows, mobile Linux, iphone Symbian Os, Blackberry etc. So, build your mobile site compatible with all kinds of platforms.