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.