Archives

Older posts coming eventually.

When I started blogging, it had everything: links, short posts, long posts, pictures. Web 2.0 has brought new ways of creating that content, but it feels scattered. This site brings my multiple streams of content back to one place, like we did in the good old days.

Tuesday, July 31, 2007

RESTful search across multiple models

A guy I know from the Rails mailing list called me this morning. He’s working on some kind of forum application, I think, and is thinking hard about how to do search in a RESTful way.

Normally, I just use the index action to search across a single resource, but in his case he wants to search Posts, which are nested inside Topics, which are nested inside Forums.

# routes.rb

map.resources :forums do |forum|
  forum.resources :topics do |topic|
    topic.resources :posts
  end
end

He wants to search Posts across all Forums and Topics. One option would be to make PostsController serve double-duty

# routes.rb

map.resources :forums do |forum|
  forum.resources :topics do |topic|
    topic.resources :posts
  end
end

map.resources :posts

and make it figure out whether or not it’s called in a scoped way or an unscoped way.

# posts_controller

def index
  if params.include?(:forum_id)
    # act normal, we're scoped
  else
    # we're special and top-level, so ... do something different
  end
end

In this case, I suggested he go a different way. Even though he says he only wants to search Posts, I feel pretty sure he wants to search Topics (by title) and Forums (by name), too.

And even if he doesn’t, I do. I’m about to add a general-purpose, cross-resource search to SOLIS, the registration and conference management system I’m writing for SUUSI and this seemed like an ideal way to help out a friend, make a much-needed blog post, and get some code done in the meantime.

Before we get started, though…this is one way to do RESTful search. It’s not the only way. It is probably not the best way. I’m not sure it’s a good way, since I just thought of it on the porch this morning and haven’t used it yet. I’m pretty sure it’s an OK way, though, and a good place to start thinking about it. YMMV.

OK, that’s out of the way.

I already know what I want: a singleton resource called /search. So let’s add it to the routes

map.resource :search

I can never remember whether singleton resources want singular (SearchController) or plural (SearchesController) names. I’m pretty sure this has changed in edge, and possibly changed back. So, I add the resource and load it up, and see what Rails tells me. When I load /search, I get ‘uninitialized constant SearchController’. Cool, it did what I wanted, and used singular. (I just checked, and on edge it uses plural. Bleh. I guess I can see why, but…anyway.) Now we need that controller.


$ script/generate controller -c Search
      exists  app/controllers/
      exists  app/helpers/
      create  app/views/search
A         app/views/search
      exists  test/functional/
      create  app/controllers/search_controller.rb
A         app/controllers/search_controller.rb
      create  test/functional/search_controller_test.rb
A         test/functional/search_controller_test.rb
      create  app/helpers/search_helper.rb
A         app/helpers/search_helper.rb

Because it’s a singleton resource, the actions are different; requesting /search calls the show action.

Now, showing a search with no query string doesn’t make sense. /search/new is an ugly URL. I want to search as a GET not a POST, so you can bookmark it. So, here’s what I ended up with.

# SearchController
  def show
    if params.include?(:q)
      # we have a query, so call create to actually do the search
      # don't redirect, though, there's no need, and bookmarkable
      # search URLs are handy
      create
      render :action => "create"
    else
      # we don't have a query, so do the new action
      new
      render :action => "new"
    end
  end

  def create
    @search_results = returning [] do |results|
      results << Person.search(params[:q])
      results << Event.search(params[:q])
      # ... add other models here, or use whatever searching you need
    end.flatten
  end
# new.rhtml
<h1>Search for stuff (events, workshops, trips, people...)</h1>
<% form_tag search_path, :method => :get do %>
  <%= text_field_tag :q %>
  <%= submit_tag "Search", :name => nil  %>
<% end %>

# create.rhtml
<% @search_results.each do |result| %>
  <p><%= result %></p>
<% end %>

Obviously you’ll format your search results more prettily, with links and such. And you’ll need search() methods on all the classes you’re going to search. Or, make a Searcher model and let it do all the work.

Monday, July 09, 2007

Rails 1.2.3 eTag patch

Rails Edge changeset 6158 added automatic eTag support. I needed it in a Rails 1.2.3 project, so I made a little patch to shoehorn 6158 into 1.2.3.

Here’s the patch, just drop it into lib and require it.

Updated: includes 7580 too.

Updated again: I’m having some issues with this, so looks like I blogged it pre-maturely.