• I swear, just go and read this right now; it might look like it's about games, but really, it's about space, and memory, and Memory Palaces, and wrapped around a retrospective of a marvellous game, and a little bit about how games make us who we are, in ways their creators might never have imagined.
  • "We already know the decapitated Statue of Liberty in Deus Ex can tell a story; perhaps I want to know if a building can tell me a poem.

    In that vein, "Butte, Montana. 1973" is a game where you dig around in a box of dirt."

    This is marvellous; thoughtful, interesting, perhaps not entirely successful, but the trick of the rain at the end is a very, very nice touch.

  • "At this moment of awards-giving and back-patting, however, we can all agree to love movies again, for a little while, because we're living within a mirage that exists for only about six or eight weeks around the end of each year. Right now, we can argue that any system that allows David Fincher to plumb the invention of Facebook and the Coen brothers to visit the old West, that lets us spend the holidays gorging on new work by Darren Aronofsky and David O. Russell, has got to mean that American filmmaking is in reasonably good health. But the truth is that we'll be back to summer—which seems to come sooner every year—in a heartbeat. And it's hard to hold out much hope when you hear the words that one studio executive, who could have been speaking for all her kin, is ready to chisel onto Hollywood's tombstone: "We don't tell stories anymore."" This is good, and sad.
  • "If you’re like us, your knowledge is spread across several places: Gmail, Google Docs, Basecamp, and more. Redwood makes it easy to search across these sources, right from your desktop." Clever.

So, Matt pointed out that when you do things like that, the scores for different Models are all ranked seperately because they’re coming out of different indexes. The trick, obviously, is to use the multi_search method, which generates a multi-model-index, and that’s probably a better approach. This approach is very well documented in the RDoc. So all it shows is: probably should have RTFM. The thing below isn’t by any means bad, it’s just a little like re-inventing the wheel.

Never mind, eh?

(This mail from Jonas is also worth a read on this matter).

If you’re building a Rails applciation with search, chances are you’ve run across acts_as_ferret. And if you haven’t, check it out – it allows any model to be searchable by Ferret, the Ruby port of Lucene. Ferret’s a pretty nifty search engine – reasonably fast, pretty accurate – and it’s nice to be able to use it so simply in your Rails app.

Of course, what makes acts_as_ferret really handy is that it’s neatly designed to perform multi-model searches. Or, rather, the interface to do so is there, you just have to glue the results together. After some rough stumbling about, here’s what I came up with.

Firstly: don’t use find_by_contents, that’s not much use. find_by_contents is a wrapper around find_id_by_contents. find_id_by_contents is useful because it returns you only three things: a relative score, a model name, and a model id. That means you can merge everything into a big list, and then perform individual queries on the relevant models.

So how did I implement this?

My first stab was this (borrowing some real code from what I’m working on):

articles = Article.find_id_by_contents(query)
authors = Author.find_id_by_contents(query)
matches = (articles + authors).sort_by {|match| match[:score] }

This gives you an array of model/score/id hashes, which you can then do lookups on. The problem is it’s not very DRY, and it requires editing every time you make a new model that’s Ferretable. This is my more final solution, which I came up with. This time, I’ll walk you through each step.

First, somewhere like environment.rb, define a constant array:

FERRETABLE_MODELS = %w[Article Author]

That means we can update things at a later date easily. Then, in your search controller action, start with this:

klasses = FERRETABLE_MODELS.collect {|klass| Kernelt.const_get klass}

That should give you an array containing the Article and Author objects – or whatever ActiveRecord objects you’ve chosen in your constant array. Next, let’s get the array that we arrived at last time:

matches = klasses.inject([]) {|out, klass| out << klass.find_id_by_contents(query)}.flatten

Bit more complex, but also more succinct. All this does is iterate over each klass, using an inject method with an empty array passed in, and tells each class to call find_id_by_contents, passing in the query. It then flattens that lot, so that the array is only one-deep. We're now where we were before.

Finally: let's generate an array of the actual objects we referred to, sorted by ranking. I'm going to generate an array of hashes. Each hash has two keys: :object, the actual data object we want; and :score, the rank that ferret assigned it. We get that out like so:

results = matches.collect {|match|
   :score => match[:score], 
   :object => (Kernel.const_get(match[:model]).find(match[:id]))
}.sort_by {|o| o[:score]}.reverse

Again, possibly a bit ugly. :score remains the same; we run find on the appropriate ActiveRecord model, passing in the appropriate id to obtain the :object. Finally, we sort the array by :score and flip it, so that results[0] is the most popular search record. Obviously, you can pass extra parameters into that "find" method.

All that remains is to display that lot in your view, perhaps paginate it, and build a conditional to determine how to display each kind of object.

And that's it. I made a few changes to my dummy code when typing this up, so if something's broken, tell me and I'll fix it. I think that's a more maintainable way of searching across a range of models with ferret, and it takes advantage of some useful Ruby dynamics - finding objects through Kernel.const_get, in particular. That's my Ruby fun for today, then.

Update

My colleague Ben proposes this much tidier (but untested) solution:

results = []

FERRETABLE_MODELS.each  do |klass|
  k = Kernel.const_get klass
  k.find_id_by_contents(query).each do |m|
    results.push {
       :score => m[:score], 
       :object => k.find(match[:id]) 
     }
  end
end

results = results.sort{ |a,b| b[:score] <=> a[:score] }

I like the nested loop much more - should have thought of that myself - and will admit to being lazy wrt the sort_by and .reverse trick.