Articles tagged with ferret

Using shared indexes with acts_as_ferret

ferret April 28 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.

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?

Update: Sorry, originally I had an example using resources, but that doesn’t work as-is; I was doing something a little different in the app that this example came from.

To display our search results, we just render a partial for each model in the result:

<% @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.

Update: I’ve put together an example rails app that uses the shared index. It uses sqlite and has some date pre-populated. Start up script/server and do a search for “John”.

posted by brandon | updated May 11th 10:32 PM | 24 comments

Hack for partial matches in Ferret

ferret December 12 2007

I love ferret (the ruby port of Lucene, not the fuzzy little creatures, you sicko). But something I fight on every project is that ferret turns into a bear when you try to get it to do partial matches, like "ferr" matching "ferret" and "ferrari".

Ferret allows you to append an asterisk to your search query ("ferr*"), which works great, but we can’t expect our users to do that because damn Google 1 has set the expectation that search just works; I don’t need to use any funky syntax to find my pogs, Harry Potter gossip or BRATZ 2.

So, we can do this manually in code by appending an asterisk to anything users enter and problem solved, right? Not quite.

  • It breaks if you’re using the StemFilter, which allows you to match variations of words ("happy" would match "happiness" and "happiest")
  • It will only match partials on the last word that the user entered ("Ed Brad" won’t find "Edward Bradley")
  • Apparently the asterisk tells ferret that there has to be more characters, because full matches no longer work ("ferret*" won’t match "ferret")

So, here’s my hack.

Book.find_by_contents "(#{term})^2 OR (#{term.split.map {|t| t + "*" }.join(' ')})"

This ugly little thing will match exactly what the user entered (making use of stemming and all the magic that comes from it) and give it a little boost in the ranking, or match any part of any of the words entered, giving me partial matches.

I acknowledge that this is an ugly hack at the moment, and will break miserably if the user is any kind of a wizard that knows how to do advanced searches, but it works for now. I have no idea what kind of consequences this will have as far as search performance and such. The goal is to wrap this into a filter.

Any one else have any cleaver ideas for doing partial matches?

  1. Yes, Google, we love and hate you for raising the bar.
  2. We’re talking normal users here, which excludes anyone that is reading this.
posted by brandon | updated December 12th 01:11 PM | 3 comments

acts_as_ferret will_paginate

ferret August 17 2007

Here’s a little nugget to add to acts_as_ferret to make your searches paginate with will_paginate.

module ActsAsFerret
  module ClassMethods
    def paginate_search(query, options = {})
      page, per_page, total = wp_parse_options(options)
      pager = WillPaginate::Collection.new(page, per_page, total)
      options.merge!(:offset => pager.offset, :limit => per_page)
      result = find_by_contents(query, options)
      returning WillPaginate::Collection.new(page, per_page, result.total_hits) do |pager|
        pager.replace result
      end
    end
  end
end

Updated from Behrang’s comment based on changes to will_paginate.

There was a slight challenge in that will_paginate expects that you do one query to get the count, create a new collection object based on that count, and then perform the actual search. But acts_as_ferret does it all in one method call, so I have to create a temporary collection object to get the offset, then do the search and create the collection object. It’s a little messier than it needs to be, but it works.

Product.paginate_search params[:q], :page => params[:page]
posted by brandon | updated March 14th 10:55 AM | 44 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