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 GrigorikIlya Grigorik is a web ecosystem engineer, author of High Performance Browser Networking (O'Reilly), and Principal Engineer at Shopify — follow on Twitter.