Sunspot 1.2 with Spatial Solr Plugin 2.0

Currently I am porting an application from Rails 2.x to 3.x. The old version uses a plugin called acts_as_solr but is no longer actively maintained and has been stale for a while. Fortunately I found Sunspot, an elegant and clean solution to adding SOLR to Rails applications.

For geo-location based searches, Sunpot uses a really cool technique called Geohashing as Apache Solr does not provide built in support for location searches (a feature expected in the 4.0 release). However, in my case, I needed the ability to bound searches with finer granularity then the 10 or so precision bounds provided by geohashing. I opted to use Spatial Solr Plugin to provide the spatial component for search.

1. Download SSP and add the jar file to your Solr lib directory. Sunspot should have created this directory in your project root project_root/solr/lib.

2. Modify Rails.root/solr/conf/solrconfig.xml and add the following

...
<!-- Configuration for using Spatial Solr Plugin (SSP) 2.0 -->
<queryParser name="spatial" class="nl.jteam.search.solrext.spatial.SpatialQParserPlugin">
  <str name="latField">lat</str>
  <str name="lngField">lng</str>
</queryParser>
 
<searchComponent name="geoDistance" class="nl.jteam.search.solrext.spatial.GeoDistanceComponent"/>

<requestHandler name="standard" class="solr.SearchHandler" default="true">
  <!-- default values for query parameters -->
  <lst name="defaults">
    <str name="echoParams">explicit</str>
    <str name="distanceField">distance</str>
  </lst>
  <arr name="last-components">
    <str>geoDistance</str>
  </arr>
</requestHandler>
...

Note: The requestHandler with the name attribute as “standard” already exists so you just need to add the new fields to your current definition.

The two fields “latField” and “lngField” in the queryParser element defines the field name that SSP will use for its spatial search. For simplicity, I used the default names lat and lng.

3. Modify project_root/solr/conf/schema.xml and add the field definitions for lat and lng. In Sunspot’s configuration, it seems to already come with a lat and lng field that isn’t being used anymore so I decided to reuse them and just modified the type attribute to “tdouble”, a Trie Double.

...
<!-- *** This field is used by Sunspot! *** -->
<field name="lat" stored="true" type="tdouble" multiValued="false" indexed="true"/>
<!-- *** This field is used by Sunspot! *** -->
<field name="lng" stored="true" type="tdouble" multiValued="false" indexed="true"/>
...

4. In your ActiveRecord model, modify your searchable block to add indexing for lat and lng. For example, if your model attributes were latitude and longitude…

searchable do
...
  double :latitude, :as => "lat"
  double :longitude, :as => "lng"
...
end

5. Restart Solr server and reindex using

rake sunspot:solr:stop
rake sunspot:solr:start
rake sunspot:solr:reindex

If you browse to your solr schema browser (default: 127.0.0.1:8982/admin/solr/schema.jsp), you should see the lat and lng fields are indexed.

6. Next we need to add support to get the “distance” field from the results. To do this, I added the following into a file in project_root/conf/initializers

module Sunspot
  
  class Search::Hit
    def distance
      @stored_values['distance'] # distance_field_name
    end
  end
  
  class Query::Sort::DistanceSort < Query::Sort::Abstract
    def to_param
      "distance #{direction_for_solr}" # distance_field_name
    end
  end
  
end

Edit: Sunspot requires that a valid class name be defined for sorting on fields that are not explicitly defined in the searchable clause. I’ve added an additional class into the module Sunspot::Query::Sort. Koodos to Craig for pointing this out.

This defines a new method in the Hit object that returns the field ‘distance’ from the solr results. If you named the distanceField in solrconfig.xml differently, you will need to modify this accordingly.

7. Now you can define a search using the adjust_solr_params. Use the order_by with :distance to sort on the distance.

@results = Model.search do
...
  order_by(:distance, :desc) # Order by distance from farthest to closest

  adjust_solr_params do |params|
    params[:q] = "{!spatial qtype=dismax boost=#{some_boost_val} circles=#{search_lat},#{search_lng},#{search_radius}}" + "#{params[:q]}"
  end
end

The adjust_solr_params adjusts the solr query parameter right before its sent off to solr. We prepend the spatial search syntax to the beginning and leave the built query intact. That way, we can still make use of the other DSL defined search parameters. Since Sunspot uses dismax by default, i’ve set the qtype parameter so that the text portion of the search is passed to the dismax parser.

8. Access the distance field like

@results.hits do |hit, result|
  puts hit.distance
end

Note: I haven’t tested this with complex queries using faceting but I think it should work. The only thing is that I’m not sure what will happen if you use this alongside geohashing, not that you would need to.