Geo-Searching

One of the neat features of Sphinx is the ability to sort and filter by a calculated geographical distance, from latitude and longitude values. It’s quite easy to get set up, as well.

Setting up the Indexes

Firstly, you’ll need to be storing latitude and longitude values as attributes for each relevant document. So, in your index definition, you’ll need something like this, if you’ve already got the columns in your model:

has latitude, longitude

You can name your attributes to be whatever you like - Thinking Sphinx will automatically use them if they’re called latitude, longitude, lat or lng.

Keep in mind, though, that Sphinx needs these values to be floats, and tracking positions by radians instead of degrees.

Real-time Indices

For real-time indices, if you’re using degrees in your database you’ll want to have the conversion to radians happening in your model:

def latitude_in_radians
  Math::PI * latitude / 180.0
end

def longitude_in_radians
  Math::PI * longitude / 180.0
end

And then use these methods in your index definition:

has latitude_in_radians, as: :latitude, type: :float
has longitude_in_radians, as: :longitude, type: :float

SQL-backed Indices

For SQL-backed indices, you can have the degrees-to-radians conversion take place in your index definition instead:

has "RADIANS(latitude)",  :as => :latitude,  :type => :float
has "RADIANS(longitude)", :as => :longitude, :type => :float

# If you're using PostgreSQL:
group_by 'latitude', 'longitude'

Once this is done, you’ll need to rebuild your Sphinx indexes:

rake ts:rebuild

Searching

Once your indexes are set up, then you can begin searching. You need to make sure you’re doing two things:

  • Provide a geographical reference point
  • Filter or sort by the calculated distance

For the first, you can provide an array of two arguments (latitude and longitude, again in radians) to the :geo option. For the second, you’ll need to refer to Sphinx’s generated attribute geodist in a filter and/or a sort argument.

# Searching for places within 10km
Place.search "pancakes", :geo => [@lat, @lng],
  :with => {:geodist => 0.0..10_000.0}
# Searching for places sorted by closest first
Place.search "pancakes", :geo => [@lat, @lng],
  :order => "geodist ASC, @relevance DESC"

If you do not provide any reference to geodist, then the lat/lng values will be ignored by Sphinx.

Note: Sphinx expects the latitude and longitude values to be in radians - so you will probably need to convert the values when searching.

Thinking Sphinx v1/v2

Note: If you are using an older version of Thinking Sphinx, then the generated geodist attribute needs to be referenced with an @ prefix:

# Searching for places within 10km
Place.search "pancakes", :geo => [@lat, @lng],
  :with => {'@geodist' => 0.0..10_000.0}
# Searching for places sorted by closest first
Place.search "pancakes", :geo => [@lat, @lng],
  :order => "@geodist ASC, @relevance DESC"

Displaying Results

Thinking Sphinx since 3.0.0

When you provide a :geo option to your search, the distance pane is automatically added to search results, and so you can access the calculated Sphinx distance through either the distance or geodist methods (your model’s own methods of those names take precedence if they exist):

<% @places.each do |place| %>
  <li><%= place.name %>, <%= place.distance %></li>
<% end %>

It’s worth noting that the distance is in metres - so those stuck on the Imperial system (Americans, that’s you), you might want to convert to less archaic measurements.

Thinking Sphinx before 3.0.0

There’s two ways to access the calculated distance. You can either enumerate through the collection using each_with_geodist:

<% @places.each_with_geodist do |place, distance| %>
  <li><%= place.name %>, <%= distance %></li>
<% end %>

Or, you can access the distance as part of the @sphinx_attributes@ collection:

<% @places.each do |place| %>
  <li>
    <%= place.name %>,
    <%= place.sphinx_attributes['@geodist'] %>
  </li>
<% end %>