Recently, during the development of the very first feature from RecAtlas, I had to find a simple way to filter and order by distance, which is very useful when looking for activities near your home.
The Problem
I needed to filter and sort records by distance from a user's location. Most tutorials will tell you to reach for geocoder or similar gems, but PostgreSQL already has killer geospatial support through PostGIS. Why add dependencies when your database can do the heavy lifting?
Installing PostGIS
First, you need to install the PostGIS extension in your PostgreSQL database. Add this migration:
class EnablePostgis < ActiveRecord::Migration[8.0]
def change
enable_extension 'postgis'
end
end
Then make sure your model has latitude and longitude columns (use :decimal type with precision 10 and scale 6):
class AddCoordinatesToLocations < ActiveRecord::Migration[8.0]
def change
add_column :locations, :latitude, :decimal, precision: 10, scale: 6
add_column :locations, :longitude, :decimal, precision: 10, scale: 6
end
end
The Solution: Two Simple Scopes
Here's the magic - two scopes that leverage PostGIS functions directly:
# Filter: Find all records within X kilometers
scope :within_km_of, ->(lat, lng, radius_km = 5) do
lat_f = Float(lat) rescue nil
lng_f = Float(lng) rescue nil
rad_f = Float(radius_km) rescue 5.0
next all if lat_f.nil? || lng_f.nil?
meters = (rad_f.to_f * 1000.0)
origin_sql = "ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography"
record_sql = "ST_SetSRID(ST_MakePoint(#{table_name}.longitude, #{table_name}.latitude), 4326)::geography"
where.not(latitude: nil, longitude: nil)
.where("ST_DWithin(#{origin_sql}, #{record_sql}, :meters)",
lat: lat_f, lng: lng_f, meters: meters)
end
# Order: Sort by distance from a point
scope :by_distance, ->(lat, lng, direction = "ASC") do
lat_f = Float(lat) rescue nil
lng_f = Float(lng) rescue nil
next all if lat_f.nil? || lng_f.nil?
origin_sql = "ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography"
record_sql = "ST_SetSRID(ST_MakePoint(#{table_name}.longitude, #{table_name}.latitude), 4326)::geography"
distance_column = Arel.sql("distance_meters")
ordering = direction.downcase == "desc" ? distance_column.desc.nulls_last : distance_column.asc.nulls_last
select(Arel.sql("#{table_name}.*, ST_Distance(#{origin_sql}, #{record_sql}) AS distance_meters"))
.reorder(ordering)
end
The key ingredients: ST_DWithin for radius filtering (faster than calculating every distance), ST_Distance for ordering, and converting decimal coordinates to PostGIS geography objects with SRID 4326 (WGS 84 - the standard GPS coordinate system).
Using It
Now you can chain these scopes like any ActiveRecord query:
# Find stores within 10km of user location
Store.within_km_of(user.latitude, user.longitude, 10)
# Find nearest restaurants, ordered by distance
Restaurant.by_distance(49.2827, -123.1207, "ASC").limit(5)
# Combine with other scopes
Event.where(active: true).within_km_of(lat, lng, 25).by_distance(lat, lng)
Other Use Cases
This pattern works brilliantly for any location-based features: - Store locators - Find nearest retail locations - Real estate listings - Search homes within commute distance - Service providers - Match users with nearby freelancers - Event discovery - Show concerts happening close to you - Delivery zones - Validate if an address is within service area
No gems, no fuss, just solid SQL and PostGIS doing what it does best.
Built for Rec Atlas, a Rails 8 app aggregating recreational activities across Metro Vancouver.