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 need to build a mobile app and a backend server (Rails API)
- the mobile app will make AJAX call to backend with current location (latitude + longitude) and searching radius
- Rails the API backend will return a list of doctors (in JSON format) that match the location search query
We will focus on Rails backend development in this post.
Available tools in Rails community
Apparently there are two common ways to go:
- Option 1: Using gem geokit-rails (easy to setup & use, ideal for simple use cases)
- Option 2: Using PostGIS extension in PostgreSQL (more work on setup but suitable for complex SQL query and better performance)
We will talk about Option 1 in this post.
Option 2 will be covered in Part 2 - using PostGIS extension.
Setup
Follow geokit-rails' github for first time setup. It's fairly easy to incorporate geokit into Rails project.
Model / Migration
Create model for doctor model like below.
Note the index created on clinic_latitude
and clinic_longitude
for performance purpose.
class CreateDoctors < ActiveRecord::Migration[5.1]
def change
create_table :doctors do |t|
t.decimal :clinic_latitude, precision: 10, scale: 6
t.decimal :clinic_longitude, precision: 10, scale: 6
t.string :clinic_name
t.timestamps
end
add_index :doctors, [:clinic_latitude, :clinic_longitude]
end
end
Hook up GeoKit
Simply by adding acts_as_mappable
in the model. lat_column_name
and lng_column_name
should match the latitude and longitude columns created in the table.
# models/doctor.rb
class Doctor < ApplicationRecord
acts_as_mappable :default_units => :miles,
:default_formula => :sphere,
:distance_field_name => :distance,
:lat_column_name => :clinic_latitude,
:lng_column_name => :clinic_longitude
end
Quick Test
> rails console
# create some doctors record
> Doctor.create!(clinic_name: "Doc 1", clinic_latitude: 22.3129115, clinic_longitude: 114.2219923)
> Doctor.create!(clinic_name: "Doc 2", clinic_latitude: 22.3129125, clinic_longitude: 114.2219993)
> Doctor.create!(clinic_name: "Doc 3 (too far)", clinic_latitude: 22.9429125, clinic_longitude: 114.5019993)
> Doctor.within(5, :units => :miles, :origin => [22.3129115, 114.2219923])
# #<ActiveRecord::Relation [#<Doctor id: 9, clinic_latitude: 0.22312912e2, clinic_longitude: 0.114221992e3, clinic_name: "Doc 1", created_at: "2017-09-15 11:47:24", updated_at: "2017-09-15 11:47:24">, #<Doctor id: 10, clinic_latitude: 0.22312913e2, clinic_longitude: 0.114221999e3, clinic_name: "Doc 2", created_at: "2017-09-15 11:47:24", updated_at: "2017-09-15 11:47:24">]>
- NOTE: Under the hood, geokit handles the geo-search by some mathetical formulas, you can tell from the generated SQL query
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[:radius],
:units => :miles,
:origin => [
params[:lat],
params[:lng]
]
)
... other stuff ...
end