Ferret Pagination in Rails

Gregg Pollack put up a great guide to showcase the power of act_as_ferret plugin earlier today. On its own, Ferret is a Ruby port of the Lucene search engine library which offers fast full-text search with a number of great freebies and addons. Gregg did an excellent job of showcasing some of these features, so I won't go into the details here. Instead, I want to show you how to integrate paginating_find with Ferret. It's a great combination which I've been using at Graphics World for quite a while.

Extending paginating_find

If you haven't already, read 'Faster Pagination in Rails' to get up to speed with paginating_find and windowed pagination. Now, to integrate Ferret, we need to add the following two ClassMethods to paginating_find.rb (copy/paste):

def paginating_ferret_search(options)
   count = find_by_contents(options[:q], {:lazy => true}).total_hits

   PagingEnumerator.new(options[:page_size], count, false, options[:current], 1) do |page|
      offset = (options[:current].to_i - 1) * options[:page_size]
      limit = options[:page_size]

      res = find_by_contents(options[:q], {:offset => offset, :limit => limit})
    end
end

def paginating_ferret_multi_search(options)
   count = multi_search(options[:q], [List], {:limit => :all, :lazy => true}).total_hits

   PagingEnumerator.new(options[:page_size], count, false, options[:current], 1) do |page|
      offset = (options[:current].to_i - 1) * options[:page_size]
      limit = options[:page_size]

      multi_search(options[:q], [List], {:offset => offset, :limit => limit})
    end
end

You will notice that params[:q] refers to the search query, feel free to change it as you wish. More importantly, you will notice that my multi_search method asks Ferret to search for the 'List' model in addition to the default model (Tutorial). In my application, this value was hard-coded right into the plugin, you may want to generate this value dynamically.

Extending your controller

Now that our search query is wrapped in a PagingEnumerator, all we have to do is define a new search method in one of our controllers. Here is a sample from my own controller for Graphics World:

def search
 unless params[:q].blank?
  params[:q] = sanitize(params[:q])
  params[:p] = 1 unless params[:p]

  case params[:t]
    when 'tutorials'
      @collection = Tutorial.paginating_ferret_search({:q => params[:q], :page_size => 10, :current => params[:p]})
    when 'all'
      @collection = Tutorial.paginating_ferret_multi_search({:q => params[:q], :page_size => 10, :current => params[:p]})
  end
 else
    flash[:warning] = "Please provide a search query"
    redirect_to home_url
 end
end

Voila, with a little work you should have a nice-looking, full-text, paginated search component:


Ilya Grigorik

Ilya Grigorik is a web performance engineer and developer advocate on the Make The Web Fast team at Google, where he spends his days and nights on making the web fast and driving adoption of performance best practices.