Sunspot, Solr, Rails: Working with Results
Having worked with Sunspot and Solr in several large Rails projects now, I’ve gained some knowledge about working with result sets optimally. Here’s a brief explanation on working with results or hits from a search object.
MVC Setup
When working with Sunspot, searchable fields are defined in the model:
class Thing < ActiveRecord::Base
searchable do
text :field1, :stored => true
text :field2
string :field3, :stored => true
integer :field4, :multiple => true
end
end
The code block above will include field1, field2, field3, and field4 in the search index of things . A keyword or text search on things will search field1 and field2 for matches. field3 and field4 may be used for scoping, or limiting the search result set based to specific values of field3 or field4.
In your controller, a new search object is created with the appropriate scoping and keyword values, shown below. Pagination is also added inside the search block.
class ThingsController < ApplicationController
def index
@search = Sunspot.search(Thing) do
#fulltext search
fulltext params[:keyword]
#scoping
if params.has_key?(:field3)
with :field3, params[:field3]
end
if params.has_key?(:field4)
with :field3, params[:field4]
end
paginate :page => params[:page], :per_page => 25
end
@search.execute!
end
end
In the view, one can iterate through the result set, where results is an array of Thing instances.
<% @search.results.each do |result| -%>
<h2><%= result.field3 %></h2>
<%= result.field1 %>
<% end -%>
Working with Hits
The above code works. It works nicely until you display many results on one page where instantiation of things is not expensive. But the above code will call the query below for every search, and subsequently instantiate Ruby objects for each of the things found. This can become sluggish when the result set is large or the items themselves are expensive to instantiate.
# development.log
Thing Load (0.9ms) SELECT "things".* FROM "things" WHERE "things"."id" IN (6, 12, 7, 13, 8, ...)
An optimized way to work with search results sets is working directly with hits. @search.hits is an array of Sunspot::Search::Hits, which represent the raw information returned by Solr for a single returned item. Hit objects provide access to stored field values, identified by the :stored option in the model’s searchable definition. The model definition looks the same. The controller may now look like this:
class ThingsController < ApplicationController
def index
search = Sunspot.search(Thing) do
#fulltext search
fulltext params[:keyword]
#scoping
if params.has_key?(:field3)
with :field3, params[:field3]
end
if params.has_key?(:field4)
with :field3, params[:field4]
end
end
search.execute!
@hits = search.hits.paginate :page => params[:page], :per_page => 25
end
end
And working with the data in the view may look like this:
<% @hits.each do |result| -%>
<h2><%= hit.stored(:field3) %></h2>
<%= hit.stored(:field1) %>
<% end -%>
In some cases, you may want to introduce an additional piece of logic prior pagination, which is the case with the most recent Rails application I’ve been working on:
...
search.execute!
filtered_results = []
search.hits.each do |hit|
if hit.stored(:field3) == "some arbitrary value"
filtered_results << hit
elsif hit.stored(:field1) == "some other arbitrary value"
filtered_results << hit
end
end
@hits = filtered_results.paginate :page => params[:page], :per_page => 25
Sunspot and Solr are rich with functionality and features that can add value to a Rails application, but it’s important to identify areas of the application where database calls can be minimized and lazy loading can be optimized for better performance. The standard log file and database log file are good places to start looking.
Comments