Using shared indexes with acts_as_ferret

ferret | rails | search April 29 2008

So by now we all know how to do wicked-cool search with acts_as_ferret. (If not, the RailsEnvy guys can lend a hand, but the tutorial is a little outdated for the latest trunk version of acts_as_ferret. Just replace #find_by_contents with #find_with_ferret and you should be good.)

But searching a single model is so last year. All the cool kids are getting promiscuous with their searches and involving multiple models. Fortunately for us, recent revisions of acts_as_ferret makes this easy-peasy.

The key to making this happen is a shared index: we want all of our models indexed in one place so ferret only has to do one search. We can do it without a shared index, but then we have to do a ferret search and thus a SQL select for each model. Plus, if we want your search results interspersed and sorted by rank, we have to have a shared index.

Enough chit-chat, show us how!

Ok, I’m getting to it. Grab the latest version of acts as ferret from trunk:

script/plugin install svn://projects.jkraemer.net/acts_as_ferret/trunk/plugin/acts_as_ferret

Now, instead of defining acts_as_ferret in our models, we define them all in config/aaf.rb

ActsAsFerret::define_index('shared',
 :models => {
   Person  => {:fields => [:first_name, :last_name, :phone, :bio]},
   Company => {:fields => [:name, :description]},
   Post    => {:fields => [:title, :body]}
 },
 :ferret   => {
   :default_fields => [:first_name, :last_name, :phone, :bio, :name, :description, :title, :body]
 }
)

This defines a new index, called “shared”, and then defines the acts_as_ferret configuration for each model.

To get this to load, and to make it so we don’t lose our configuration in development when the models reload, we need to add a bit in config/environment.rb. I’ve submitted a patch so we won’t have to do this, but for now: Update: my patch was accepted, so you can skip this.

config.after_initialize do
  ActsAsFerret.initialize
end

Now for the fun part: searching our shiny new index.

def search
  @results = ActsAsFerret.find(params[:q], 'shared')
end

This will give us one array with any models that matched the search query, ordered by rank. And for those times when we only want to search one model, we can still do that.

def search
  @people = Person.find_with_ferret(params[:q])
end

How do we display the results?

If we’re using map.resources in our routes, it’s really easy:

<%= render :partial => @results %>

This is going to look for a partial for each record based on the model name. So for Person, it would look for people/_person.html.erb. If we’re not using map.resources, or want to display our search results differently, we could just do something like this:

<% @results.each do |result| %>
  <%= render :partial => "search/#{dom_class(result)}" %>
<% end %>

This will just look for a partial for each model (like search/_person.html.erb).

So there you have it. Now you too can have promiscuous searching.

posted by brandon | updated April 30th 10:20 AM
comments feed

16 comments

  1. That’s fantastic, thanks for the writeup. Last time I look at Ferret it felt a bit contorted – this looks great though!

    Any ideas about memory usage? I seem to remember it using a lot last time I used it…

    Ed Spencer Ed Spencer
    April 29, 2008 at 01:18 PM
  2. Are you using Ferret on any production apps? If so, how has it been? I considered Ferret a few months ago, but went for Sphinx when a lot of people were saying Ferret isn’t as stable.

    Derek Derek
    April 29, 2008 at 07:44 PM
  3. I have to report stability issues with Ferret. Sphinx is serving well and exposes some advanced features Ferret is lacking.

    ynw ynw
    April 29, 2008 at 08:38 PM
  4. Hi, I’m following your pattern but having problems with rendering. I have my models mapped as resources but when I call

    <%= render :partial => @results %>

    I get an exception, “couldn’t find template array/_arrays”

    Instead of a SearchResults object, I appear to be getting a plain array returned. Any ideas?

    Damon Damon
    May 07, 2008 at 02:28 PM
  5. you’ve gotten farther then I damon and brandon.

    i get

    undefined method `load_config' for ActsAsFerret:Module (NoMethodError)
    when i reload the server. I did get the same message as Damon however, when i tried this yesterday without restarting the server. i never should have restarted my server.

    I’m thinking that something in trunk is borked, right now.

    P jam P jam
    May 07, 2008 at 04:12 PM
  6. also, branden, in the paragraph where you say to skip the patch , your tutorial/post goes right to more code.

    Was your intent for ‘us’ to skip that bit of code too? it kind of just appears and then you say… now for the fun part.

    1000 thank you’s if you get this working for me boss.

    P jam P jam
    May 07, 2008 at 04:25 PM
  7. P jam, all the code after
    config.after_initialize....<pre>
    needs to be in your controller(s) / view(s).
                
    Damon Damon
    May 07, 2008 at 06:25 PM
  8. (try again) P jam, all the code after
    config.after_initialize....
    needs to be in your controller(s) / view(s).
    Damon Damon
    May 07, 2008 at 06:27 PM
  9. It’s actually the

    config.after_initialize....
    that i am having the trouble with.

    Where did you put that? did you put that in your environment.rb? and if so, where?

    i get a message saying that load_config cannot be found. load_config is part of the plugin… but for some reason it can’t be found when initializing.

    p jam p jam
    May 07, 2008 at 08:36 PM
  10. I have the latest aaf and didn’t need to do anything to my environment.rb.

    Once I made the changes, I did restart ferret_server.

    Damon Damon
    May 07, 2008 at 11:08 PM
  11. Sorry, half asleep here…

    To answer your question, I am not using
    config.after_initialize....
    anywhere. Once I restarted my ferret server, everything worked fine. If you’re not using a ferret server, you might want to look at reloading your index files (but I’m just guessing).
    Damon Damon
    May 07, 2008 at 11:11 PM
  12. the blog post that became a forum…. i bet if we help each other, this posting will go down as the posting that changed searching with ferret forever. First, thanks damon. for some reason it’s working now. i got rid of all reference to aaf in my app and started with a new trunk plugin. so far so good. at least my server starts now.

    So what do you use in your view then?

    i keep getting blank views.

    here is my searches/index.html.erb
    <h1>Search Results</h1>
    <%= params.inspect %>
    <% @results.each do |result| %>
      <%= render :partial => "searches/#{dom_class(result)}" %>
    <% end %>
    and let’s say i have a _user.html.erb partial for the user’s name i want to search. here is the corresponding partial
    <%= link_to result.login, root_url %>
    What am i doing wrong? What does yours look like? THIS is the part that is screwing with my head. p.s., i just wanted something to show up when i searched, hence the lame partial. i’ll pimp it up when i get it working.
    p jam p jam
    May 08, 2008 at 10:46 AM
  13. The view is where things get ugly for me.
    <%= render :partial => @results %>
    Doesn’t look for other partials based on the model name (e.g., activities/_activity partial if the model is Activity), so I threw something together that’s very ugly in a _result partial:
    <%= link_to_if(result.instance_of?(Activity), result.name, activity_path(result)){ link_to result.name, organization_path(result) } %>. . . .
    I am sure there is a better way to do this but I don’t know how. However, once I know what object I’m dealing with, I access it normally.
    Damon Damon
    May 08, 2008 at 01:42 PM
  14. i don’t know where my last comment post went… but, i was saying, if we can talk about this on railsforum, it may be a better spot for us.

    You’re the only dude i know with a similar issue, hope you don’t mind.

    http://www.railsforum.com/viewtopic.php?id=18060
    p jam p jam
    May 08, 2008 at 04:13 PM
  15. Hi Brandon,

    Is it possible to display your views, and search fields please?

    When i pass params[:q], all i get is the name in which i typed.

    Somehow, params[:q] doesn’t work it’s way through the ‘shared’ index? it must be how i’m passing the search term.

    Perhaps you could do another post just devoted to the little bits, like the views and search forms, that were skipped in this post. Much appreciated, and keep up the good work.

    p jam p jam
    May 09, 2008 at 10:39 AM
  16. Very nice code, I am not quite clear about passing these parameters, but it make some sense for me.

    Thanks, Eddie Funeral

    Funeral Funeral
    May 11, 2008 at 05:45 PM

Speak your mind:

(Required)

(Required)


(You may use textile in your comments.)

About

I'm Brandon Keepers, a web application developer that likes beautiful code, valid markup and adherence to standards. As a part of Collective Idea in Holland, Michigan, I practice Agile software development primarily using Ruby on Rails.

-86.103171 42.785037

Contact:

more ยป

Syndicate