Adding PostGIS support to Rails 6
By: Ben Stacey
Tags: Geospatial, Postgis, Engineering, Postgres, Rails

Here at Biggerpockets Engineering, we have been busy building the first and subsequent iterations of the new Property Insights feature.

We have more than 200 million rented/sold property records containing fields such as street address, ZIP code, property type; as well as associated records containing rental and sale price data, when the price data was recorded, valuation data and much more.

One of the first features to implement was to show a subset of comparable properties related to the current report page with a known longitudinal/latitudinal location.We decided to preferentially show properties with a similar number of user selected bedrooms, that were within a fixed distance of a location/property.

Geokit vs PostGIS

The Geocoder Ruby gem has by far the most downloads in the domain of street address/IP location encoding, and it possesses ActiveRecord support for a bunch of distance based database queries. The Biggerpockets app already uses the Geokit Ruby gem (in several areas including the Marketplace, Forums and Premium Listings). So why bother adding new gems, database extensions and a few workarounds to make use of PostGIS?

We knew Property Insights was going to be a location-heavy product from the outset. We already had a firm use case for basic spatial querying and wanted to be able to reach for as many spatial functions as possible in the future, as well as have the ability to ingest a range of spatial datatypes. It was also highly likely we would have to source more data in the future to provide further useful context when a user is finding or analysing a deal.

Heroku serves our application and has a proven track record of working with PostGIS, plus we use Postgres already — if you don’t use Heroku and/or Postgres please check out their documentation carefully for any issues or caveats with the use of PostGIS columns in your application.

That’s not to say we recommend PostGIS for every project that requires distance-based querying — see this excellent article on why you shouldn’t use PostGIS:

The Code

Postgres Extension Migration

class EnablePostgisExtension < ActiveRecord::Migration[6.0]

def change

enable_extension "postgis"

end

end

Database schema migration

def change

add_column :insights_properties, :lonlat, :geometry,

geographic: true, srid: 4326

add_index :insights_properties, :lonlat, using: :gist

end

The addition of the gist index is recommended by the PostGIS authors to improve performance when querying large tables, see:

https://postgis.net/docs/PostGIS_FAQ.html

Gemfile

gem "rgeo"

Core component to support geospatial objects, handle geometry, parse additional datatypes (WKT, WKB, Multipolygons).

gem 'activerecord-postgis-adapter'

Enables PostGIS database features to work within ActiveRecord — provides additional migrations, allows spatial data in queries etc.

gem 'rgeo-activerecord'

Provides additional extensions and helpers for ActiveRecord.

database.yml

Our databases are provisioned by Heroku, so we followed the recommended procedure to alter the value of the DATABASE_URL env variable (https://devcenter.heroku.com/articles/rails-database-connection-behavior) in production . Replace:

url: <%= ENV.fetch('DATABASE_URL') %>

with:

url: <%= ENV.fetch('DATABASE_URL', '').sub(/^postgres/, "postgis") %>

For the remaining environments we replaced instances of:

postgres:///

with:

postgis:///

To solve an issue with the pre-existing geokit methods throwing uninitialized constant errors after this change, we also added a new file lib/geokit-rails/adapters/postgis.rb

require File.join("geokit-rails", "adapters", "postgresql") module Geokit

module Adapters

class PostGIS < PostgreSQL

end

end

end

CI Config

For your CI, you will need to amend the source database Docker image that your CI instances spin up from. We use CircleCI, so in our config file .circleci/config.yml we replaced:

- image: circleci/postgres:10-alpine-ram

with:

- image: circleci/postgres:10-postgis-ram

Database cleaner

Our database cleaning strategy was truncating the spatial_ref_sys table. This table stores constants and reference values required for various PostGIS calculations. If starting a Rails instance throws an unable to find table: spatial_ref_sys error you should add an exception condition to the clean_with expression like the following:

DatabaseCleaner.clean_with(:deletion, cache_tables: false, except: %w(spatial_ref_sys))

Usage Examples

The following example expression can be used to insert a geographical point type into our freshly created column:

Insights::Property.update(

lonlat: RGeo::Geographic.spherical_factory(srid:4326).point("-104.9816", "39.5142"))

To find properties, whose coordinates are within a certain number of miles as another, we selected the PostGIS function ST_DWithin which has the following signature:

ST_DWithin(geometry1, geometry2, distance)

We also used the ST_POINT function to convert given coordinates to usable points for ST_DWithin .

The following methods were then used to implement this function as part of a WHERE clause. We wrapped the clause strings in Arel functions so as to avoid Dangerous query method errors being thrown: