Location based searching in Rails 5 - Part 2 (using PostGIS extension)

geosearch rails5 postgis

Continued from Part 1 (using geokit-rails)

Why?

Location-based search can bring user experience value to modern websites and mobile apps. Such as allowing users to find nearby drivers/doctors/restaurants/lawyers/etc. This post will talk about adding geo-search ability to Rails API backend that enabled the location-based searching.

Business example

Let’s say we want to build a mobile app where users can find doctors nearby. So this is the core business requirement:

Users want to find doctors within 5 miles nearby

Technical specs

We will focus on Rails backend development in this post.

Available tools in Rails community

After quick research, there are two ways to go:

This post will talk about on Option 2 - using PostGIS extension.
Option 1 is covered in Part 1 - using GeoKit.

What is PostGIS?

PostGIS is a spatial database extender for PostgreSQL object-relational database. It adds support for geographic objects allowing location queries to be run in SQL.

Simply put, it allows you to store location data (latitude+longitude) as a native PostgreSQL datatype which enable spatial operations such as distance function.

see more here

Installing PostGIS extension

Follow the guides here. It supports Windows, OSX, Ubuntu, and other unix distributions.

Installing Rails PostGIS Adapter
# Gemfile
gem 'activerecord-postgis-adapter'

> bundle

see more here

Model / Migration

Create model for doctor model like below.

class CreateDoctors < ActiveRecord::Migration[5.0]  
  def change
    create_table :doctors do |t|
      t.st_point :clinic_lonlat, geographic: true
      t.string :clinic_name

      t.timestamps
    end
    add_index :doctors, :clinic_lonlat, using: :gist
  end
end

Note: st_point is the geo-datatype we need here.

Let's add a scope in model to allow us perform geo-search handy.

class Doctor < ApplicationRecord  
  scope :within, -> (latitude, longitude, distance_in_mile = 1) {
    where(%{
     ST_Distance(clinic_lonlat, 'POINT(%f %f)') < %d
    } % [longitude, latitude, distance_in_mile * 1609.34]) # approx
  }
end  

Note: we are using ST_Distance spatial function in PostGIS enabled database.

Quick test

> rails console

# add some testing data
> Doctor.create!(clinic_name: "Doc 1", clinic_lonlat:"POINT(#{114.2219923} #{22.3129115})")
> Doctor.create!(clinic_name: "Doc 2", clinic_lonlat:"POINT(#{114.2219993} #{22.3129125})")
> Doctor.create!(clinic_name: "Doc 3 (too far)", clinic_lonlat:"POINT(#{114.5019993} #{22.9429125})")

# find nearby doctors
> Doctor.within(22.3129115, 114.2219923, 1)

# returned expected records, all good
#<ActiveRecord::Relation [#<Doctor id: 1, clinic_lonlat: #<RGeo::Geographic::SphericalPointImpl:0x3fd5a296f3e0 "POINT (114.2219923 22.3129115)">, clinic_name: "Doc 1", created_at: "2017-09-15 19:29:20", updated_at: "2017-09-15 19:29:20">, #<Doctor id: 2, clinic_lonlat: #<RGeo::Geographic::SphericalPointImpl:0x3fd5a296e4f4 "POINT (114.2219993 22.3129125)">, clinic_name: "Doc 2", created_at: "2017-09-15 19:29:27", updated_at: "2017-09-15 19:29:27">]> 

The generated query look like this

Controller

Last step is to replace latitude, longitude and radius with the parameters sent from the app.

# controllers/doctors_controller.rb

def search  
  ... other stuff ...

  Doctor.within(
    params[:lat],
    params[:lng]
    params[:radius]
  )

  ... other stuff ...
end

Heroku Support

If we host the Rails backend on Heroku, we get immediate support from Heroku out of the box.

PostGIS is available in public beta. The beta is available on all Production tier databases and currently supports PostGIS version 2.0.

see more PostGIS supports on Heroku