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.
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
- 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.
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_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.
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
> 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
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