Use PostGIS GiST index for spatial bounds queries

This commit is contained in:
Graham McIntire 2026-02-20 07:26:29 -06:00
parent 6c2b664333
commit bcb3556bf6
No known key found for this signature in database
2 changed files with 49 additions and 37 deletions

View file

@ -121,17 +121,18 @@ defmodule Aprsme.Packets.QueryBuilder do
@spec within_bounds(Ecto.Query.t(), list(number())) :: Ecto.Query.t() @spec within_bounds(Ecto.Query.t(), list(number())) :: Ecto.Query.t()
def within_bounds(query, [west, south, east, north]) def within_bounds(query, [west, south, east, north])
when is_number(west) and is_number(south) and is_number(east) and is_number(north) do when is_number(west) and is_number(south) and is_number(east) and is_number(north) do
# Handle antimeridian crossing (e.g., west=170, east=-170) # Use ST_MakeEnvelope with && operator to leverage GiST spatial index on location column
if west > east do if west > east do
# Handle antimeridian crossing with two envelopes: [west, 180] and [-180, east]
from p in query, from p in query,
where: p.has_position == true, where: p.has_position == true,
where: p.lat >= ^south and p.lat <= ^north, where:
where: p.lon >= ^west or p.lon <= ^east fragment("? && ST_MakeEnvelope(?, ?, 180, ?, 4326)", p.location, ^west, ^south, ^north) or
fragment("? && ST_MakeEnvelope(-180, ?, ?, ?, 4326)", p.location, ^south, ^east, ^north)
else else
from p in query, from p in query,
where: p.has_position == true, where: p.has_position == true,
where: p.lat >= ^south and p.lat <= ^north, where: fragment("? && ST_MakeEnvelope(?, ?, ?, ?, 4326)", p.location, ^west, ^south, ^east, ^north)
where: p.lon >= ^west and p.lon <= ^east
end end
end end

View file

@ -5,26 +5,22 @@ defmodule Aprsme.Packets.QueryBuilderTest do
alias Aprsme.Packet alias Aprsme.Packet
alias Aprsme.Packets.QueryBuilder alias Aprsme.Packets.QueryBuilder
alias Aprsme.PacketsFixtures
describe "within_bounds/2" do describe "within_bounds/2" do
test "filters packets within normal bounds" do test "filters packets within normal bounds" do
# Create test packets inside =
{:ok, inside} = PacketsFixtures.packet_fixture(%{
Aprsme.Repo.insert(%Packet{
sender: "TEST-1", sender: "TEST-1",
lat: 40.5, lat: Decimal.new("40.5"),
lon: -73.5, lon: Decimal.new("-73.5")
has_position: true,
received_at: DateTime.truncate(DateTime.utc_now(), :second)
}) })
{:ok, _outside} = _outside =
Aprsme.Repo.insert(%Packet{ PacketsFixtures.packet_fixture(%{
sender: "TEST-2", sender: "TEST-2",
lat: 50.0, lat: Decimal.new("50.0"),
lon: -73.5, lon: Decimal.new("-73.5")
has_position: true,
received_at: DateTime.truncate(DateTime.utc_now(), :second)
}) })
# Test normal bounds [west, south, east, north] # Test normal bounds [west, south, east, north]
@ -36,32 +32,25 @@ defmodule Aprsme.Packets.QueryBuilderTest do
end end
test "handles antimeridian crossing bounds" do test "handles antimeridian crossing bounds" do
# Create test packets west_side =
{:ok, west_side} = PacketsFixtures.packet_fixture(%{
Aprsme.Repo.insert(%Packet{
sender: "WEST-1", sender: "WEST-1",
lat: 0.0, lat: Decimal.new("0.0"),
lon: 175.0, lon: Decimal.new("175.0")
has_position: true,
received_at: DateTime.truncate(DateTime.utc_now(), :second)
}) })
{:ok, east_side} = east_side =
Aprsme.Repo.insert(%Packet{ PacketsFixtures.packet_fixture(%{
sender: "EAST-1", sender: "EAST-1",
lat: 0.0, lat: Decimal.new("0.0"),
lon: -175.0, lon: Decimal.new("-175.0")
has_position: true,
received_at: DateTime.truncate(DateTime.utc_now(), :second)
}) })
{:ok, _middle} = _middle =
Aprsme.Repo.insert(%Packet{ PacketsFixtures.packet_fixture(%{
sender: "MIDDLE-1", sender: "MIDDLE-1",
lat: 0.0, lat: Decimal.new("0.0"),
lon: 0.0, lon: Decimal.new("0.0")
has_position: true,
received_at: DateTime.truncate(DateTime.utc_now(), :second)
}) })
# Test antimeridian crossing bounds [west=170, south=-10, east=-170, north=10] # Test antimeridian crossing bounds [west=170, south=-10, east=-170, north=10]
@ -87,4 +76,26 @@ defmodule Aprsme.Packets.QueryBuilderTest do
assert QueryBuilder.within_bounds(original_query, ["a", "b", "c", "d"]) == original_query assert QueryBuilder.within_bounds(original_query, ["a", "b", "c", "d"]) == original_query
end end
end end
describe "weather_only/1" do
test "filters to weather packets using has_weather column" do
_weather =
PacketsFixtures.packet_fixture(%{
sender: "WX-QBT",
temperature: 72.0,
has_weather: true
})
_non_weather =
PacketsFixtures.packet_fixture(%{
sender: "NOWX-QBT",
comment: "No weather"
})
query = QueryBuilder.weather_only(Packet)
results = Aprsme.Repo.all(query)
assert Enum.all?(results, fn p -> p.has_weather == true end)
end
end
end end