Sphinx, Riddle, and will_paginate

17 Feb 2009

I'm a big fan of the excellent Sphinx full text engine, and I have some projects that use UltraSphinx and others that just use the Ruby API, Riddle, directly. Riddle supports limits and offsets but (understandably) doesn't do Railsy pagination - so Rich Kilmer wrote some code to do that.

The basic idea is to implement enough of will_paginate's WillPaginate::Collection methods to make things work. In this case we're searching a bunch of Book objects from my military reading list site:

class SearchResults

   def initialize(query_results, page, page_size)
     @query_results = query_results
     @page = page
     @page_size = page_size
     @books = Book.find(@query_results[:matches].map{|match| match[:doc]})
   end

   def previous_page
     @page == 1 ? @page : @page - 1
   end

   def next_page
     (@page == total_pages ? total_pages : @page + 1)
   end

   def current_page
     @page
   end

   def total_pages
     (@query_results[:total_found]/@page_size)+1
   end

   def each(&block)
     @books.each(&block)
   end

   def empty?
     @books.empty?
   end

 end

Here's the searching code; we can just put this in a class method on Book:

 def self.search(terms, options = {})
   client = Riddle::Client.new("localhost", 3312)
   page = options[:page].to_i || 1
   page_size = options[:page_size] || 20
   client.offset = (page - 1) * page_size
   client.limit = page_size
   SearchResults.new(client.query(terms), page, page_size)
 end

We also need a simple controller action:

def search
  @books = Book.search(params[:term], params)
end

And a route to get us there with nice URLs:

map.search "/search/:term/:page", :controller => 'books', :action => 'search'

And, finally, the standard will_paginate view code:

<%= will_paginate(@books)%>
<% @books.each do |b| %>
  <%= b.title %>
<% end %>

That's about it! I usually test this stuff by using Mocha to replace Riddle::Client.query with a stub that returns a Hash of search result information. Pretty standard stuff, really. Enjoy!