RESTful searching in Rails

26 Jun 2008

Over and over again I see people asking about how to do a search the “RESTful way” with Rails, and every time I do I just shake my head and wonder why we sometimes get so caught up in how we do something that it keeps from actually doing it. So, with that introduction, I want to present to you a really simple implementation of the most basic type of searching you can do in a Rails application. It just so happens that this method will also handle the bulk of your searching needs.

Let’s say you want to search through orders for a shopping cart. Instead of doing something fancy, let’s just assume you want to search on the customer’s name, the customer’s email, and also optionally restrict the search to a date range. First, here’s a snippet from the view, index.html.erb:

...
< % form_tag({:action => ‘index’}, :method => ‘get’) do %>
  < label>Name/Email< /label>
  < %= text_field_tag :q, h(params[:q]) %>
  < label>Date Range< /label>
  < %= text_field_tag :start_on, h(params[:start_on]) %>
  < %= text_field_tag :end_on, h(params[:end_on]) %>
  />
  < %= submit_tag ‘Search’ %>
< % end %>
...

What, throwing search right into the index action? You betcha. Oh, you want pagination with that? No problem:

...
< %= will_paginate @orders, :params => params >
...

Bam. Ok, on to the controller:

class OrdersController < ApplicationController

  def index
    @orders = Order.paginate(:all,
      :order => ‘orders.created_at’,
      :page => params[:page],
      :conditions => search_conditions,
      :joins => :user)
  end

  ...

  protected
 
    def search_conditions
      cond_params = {
        :q => "#{params[:q]}%",
        :start_on => Chronic.parse(params[:start_on]),
        :end_on => Chronic.parse(params[:end_on])
      }
     
      cond_strings = returning([]) do |strings|
        strings < < "(users.name like :q or users.email like :q)" unless params[:q].blank?
        if cond_params[:start_on] && cond_params[:end_on]
          strings << "orders.created_at between :start_on and :end_on"
        elsif cond_params[:start_on]
          strings << "orders.created_at >= :start_on"
        elsif cond_params[:end_on]
          strings < < "orders.created_at <= :end_on"
        end
      end
     
      cond_strings.any? ? [ cond_strings.join(and), cond_params ] : nil
    end
end

It just doesn’t get much easier than this. If there was no search, then the :conditions arg gets passed in as nil, which gets silently dropped. If there was one, then we build some conditions based on which parameters were specified. With a little bit of help from the chronic gem, we get easy date entry, to boot.

We’re done. And I just don’t care whether it’s RESTful or not. ;)


Actions

Informations

14 responses to “RESTful searching in Rails”

Scott Raymond (14:32:32) :

And fortunately, it’s perfectly RESTful to boot! I’ve never understood why that’s not obvious, but oh well.

Ben (16:28:39) :

Of course it’s RESTful – search results for a single resource type are really just filtered listing views, which is what you’re doing here. It gets a bit more complicated to stay RESTful when you’re searching across multiple resource types (say, posts and people), but even that’s not much of a challenge.

topfunky (17:59:06) :

I’ve been using this technique in my apps and it just makes sense. I’m not sure what the debate is about!

And in my experience, you don’t even need to pass params to will_paginate. It picks them up automatically.

Ben (19:19:43) :

@topfunky – Ah, cool, I didn’t even try it without. :)

A Fresh Cup » Blog Archive » Double Shot #237 (03:05:01) :

[...] RESTful searching in Rails – Ben Curtis argues for simplicity rather than an extra resource. I’ve done it both ways, and tend to agree. [...]

Ben Hughes (06:10:05) :

I would encourage everyone to check out Ryan Bates’ post on “Anonymous Scopes”:
http://railscasts.com/episodes/112

He presents a fairly elegant way of conditionally including different search conditions, IMO must better than manually constructing SQL strings then joining them with AND…

Tekhne (07:23:42) :

I’m sorry to pick nits (ignore me if you don’t care), but you wrote “if there was no search.” I think that’s grammatically incorrect. I think it should be “if there were no search.” See http://en.wikipedia.org/wiki/Subjunctive.

Ryan Bates (07:42:29) :

Thanks for posting this, I’ve often wondered why some struggle with RESTful searching as well. Using the index action just makes sense.

I’d recommend moving the search logic out of the controller and into the model, but beyond that looks great. :)

On a related note I have a couple screencasts on searching which some may find helpful.
http://railscasts.com/episodes/37
http://railscasts.com/episodes/111

Alex Vollmer (08:11:30) :

There seems to be a very common mis-conception that the presence of query parameters is somehow un-REST-ful. As the other commenters have pointed out, this isn’t really the case, and as Ben pointed out we still have resources at the core of the design which is quite REST-ful.

As the resident “REST-guy” at my company, I found I often have to debunk these strange myths people form in their heads. If we’re going explain REST to those who don’t get it, we better make damn sure we explain it right.

Great post, Ben.

Jonathan Tron (14:06:41) :

I second Ryan Bates about moving search logic in the model, anyway I found pretty useful to use an OpenStruct initialized with the search params :

@search = OpenStruct.new(params[:search])

Then in your view you can use the different FormHelper methods on the @search object.
If at any time you need/want to store queries (to speedup app, for later reuse or for statistics) you can easily swap your OpenStruct with your model.

Adam Meehan (22:31:41) :

I think Alex is right, that it was the query params that threw people. I continued doing it the same way as you have done it, but read enough posts about ‘the RESTful search problem’ I started doubt myself.

There are few solutions out there, but I found the criteria_query plugin (http://www.muermann.org/ruby/criteria_query/) good for building model find conditions from form params like your example.

RESTful Searching in Rails (06:20:59) :

[...] Ben Curtis demonstrates RESTful Searching in Rails. [...]

Dan Manges (13:06:42) :

+1 on moving the search logic to the model. For what it’s worth, you don’t need to wrap the params in an h call when passing to the form helpers.

Latest Bookmarks on Ma.gnolia.com at Ivan Enviroman (23:02:50) :

[...] BenCurtis.com ยป RESTful searching in Rails [...]