Skip to content

Commit bffad1d

Browse files
committed
feat: add K-Nearest Neighbor (<->) operator for blazing fast spatial searches
- Added the PostGIS <-> operator (K-Nearest Neighbor) to Arel - This operator uses spatial indexes for incredibly fast 'find nearest' queries - Much faster than ST_Distance + ORDER BY for finding closest geometries - Added comprehensive tests for the new operator - Updated documentation with space combat examples showing tactical uses The <-> operator is a game-changer for performance when finding nearest: - Restaurants, stores, or locations - Ships, satellites, or space stations - Any spatial objects where you need the N closest items Example usage: Location.order(Location.arel_table[:position].distance_operator(origin)).limit(10)
1 parent d61181f commit bffad1d

5 files changed

Lines changed: 181 additions & 1 deletion

File tree

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,13 @@ Location.where(
101101
Location.arel_table[:coordinates].st_distance(point).lt(1000)
102102
)
103103

104-
# NEW: Advanced spatial predicates
104+
# NEW: K-Nearest Neighbor - Lightning fast "find nearest" queries
105+
# Uses spatial index for incredible performance!
106+
nearest_locations = Location
107+
.order(Location.arel_table[:coordinates].distance_operator(my_position))
108+
.limit(10)
109+
110+
# Advanced spatial predicates
105111
# Find intersecting routes
106112
Route.where(Route.arel_table[:path].st_intersects(restricted_zone))
107113

@@ -255,6 +261,7 @@ end
255261
🔍 **Spatial Query Methods**
256262
- Core methods: `st_distance`, `st_contains`, `st_within`, `st_length`
257263
- **NEW:** Advanced spatial operations:
264+
- `<->` (distance_operator) - K-Nearest Neighbor search (blazing fast!)
258265
- `st_intersects` - Detect geometry intersections
259266
- `st_dwithin` - Efficient proximity queries (index-optimized!)
260267
- `st_buffer` - Create buffer zones around geometries

docs/COOKBOOK.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,62 @@
1616

1717
*Your starships are scattered across the galaxy like atoms in the void. Time to coordinate their movements with military precision.*
1818

19+
### The Quantum Leap: K-Nearest Neighbor Search
20+
21+
The `<->` operator is your secret weapon for finding the nearest anything at warp speed:
22+
23+
```ruby
24+
# app/models/starship.rb
25+
class Starship < ApplicationRecord
26+
# Find nearest starships using KNN (blazing fast with spatial index!)
27+
scope :nearest_to, ->(position, limit = 10) {
28+
order(arel_table[:current_coordinates].distance_operator(position))
29+
.limit(limit)
30+
}
31+
32+
# Combine KNN with distance calculations when you need actual distances
33+
scope :nearest_with_distances, ->(position, limit = 10) {
34+
select(
35+
"*",
36+
"ST_Distance(current_coordinates, ST_GeomFromText('#{position.as_text}', 4326)) as distance_meters"
37+
)
38+
.order(arel_table[:current_coordinates].distance_operator(position))
39+
.limit(limit)
40+
}
41+
42+
# Emergency protocols: Find nearest friendly ships
43+
def nearest_allies(count = 5)
44+
self.class
45+
.where(faction: faction)
46+
.where.not(id: id)
47+
.order(
48+
self.class.arel_table[:current_coordinates].distance_operator(current_coordinates)
49+
)
50+
.limit(count)
51+
end
52+
53+
# Tactical assessment: Nearest threats
54+
def incoming_threats(scan_limit = 20)
55+
HostileVessel
56+
.active
57+
.select(
58+
"*",
59+
"ST_Distance(position, ST_GeomFromText('#{current_coordinates.as_text}', 4326)) as threat_distance"
60+
)
61+
.order(
62+
HostileVessel.arel_table[:position].distance_operator(current_coordinates)
63+
)
64+
.limit(scan_limit)
65+
.having("threat_distance < ?", sensor_range)
66+
end
67+
end
68+
69+
# The Navigation AI notes:
70+
# "Traditional distance queries are like checking every star in the galaxy.
71+
# The <-> operator is like having a sorted list already waiting for you.
72+
# Always use <-> for 'find nearest' queries—your CPU will thank you."
73+
```
74+
1975
```ruby
2076
# app/models/starship.rb
2177
class Starship < ApplicationRecord

docs/SPATIAL_WARFARE.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,66 @@ Welcome to the Spatial Warfare Division, pilot. You've been equipped with Active
88

99
## 🎯 The Advanced Weapons Systems
1010

11+
### The <-> Operator - Quantum Targeting System
12+
13+
Before we dive into the standard weapons, let me introduce you to the most powerful targeting system in the fleet: the K-Nearest Neighbor operator, `<->`. While other systems calculate distances in real-time (burning precious CPU cycles), this quantum targeting array uses spatial indexes to lock onto targets at warp speed.
14+
15+
```ruby
16+
# Traditional targeting (slow, scans entire space)
17+
NearbyShips.order(
18+
Arel.sql("ST_Distance(position, ST_GeomFromText('POINT(-73.5 40.7)', 4326))")
19+
).limit(10)
20+
21+
# Quantum targeting with <-> (lightning fast, uses spatial index)
22+
NearbyShips.order(
23+
NearbyShips.arel_table[:position].distance_operator(command_ship_position)
24+
).limit(10)
25+
26+
# Even cleaner with the alias
27+
NearbyShips.order(
28+
NearbyShips.arel_table[:position].send(:'<->', command_ship_position)
29+
).limit(10)
30+
31+
# Real combat scenario: Find 5 nearest enemy vessels
32+
class HostileVessel < ActiveRecord::Base
33+
scope :nearest_threats, ->(our_position, limit = 5) {
34+
order(arel_table[:coordinates].distance_operator(our_position))
35+
.limit(limit)
36+
}
37+
38+
# Emergency evasion: Find escape routes
39+
scope :find_escape_vectors, ->(current_position) {
40+
safe_zones = SpaceSector.neutral
41+
.order(
42+
SpaceSector.arel_table[:center_point].distance_operator(current_position)
43+
)
44+
.limit(3)
45+
}
46+
end
47+
48+
# The Tactical Computer explains:
49+
# "The <-> operator doesn't calculate distances—it uses the spatial index
50+
# to teleport directly to the answer. It's the difference between searching
51+
# every star in the galaxy versus knowing exactly which ones are closest."
52+
```
53+
54+
**Critical Intelligence**: The `<->` operator returns results ordered by distance but doesn't give you the actual distance value. If you need both speed AND distance:
55+
56+
```ruby
57+
# Get nearest ships with their distances
58+
Starship
59+
.select(
60+
"*",
61+
"ST_Distance(position, ST_GeomFromText('#{origin.as_text}', 4326)) as distance_meters"
62+
)
63+
.order(
64+
arel_table[:position].distance_operator(origin)
65+
)
66+
.limit(10)
67+
```
68+
69+
## 🎯 Standard Weapons Arsenal
70+
1171
### ST_Intersects - The Collision Detection Array
1272

1373
Every good pilot knows: space is big, but not big enough when two fleets converge on the same coordinates.

lib/arel/visitors/postgis.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ def st_area
3636
end
3737
end
3838
class SpatialArea < Unary; end
39+
40+
# K-Nearest Neighbor distance operator
41+
class SpatialDistanceOperator < Binary
42+
def initialize(left, right)
43+
super
44+
end
45+
end
3946

4047
# Wrapper for spatial values that need special handling
4148
class SpatialValue < Node
@@ -76,6 +83,12 @@ def st_transform(srid)
7683
def st_area
7784
SpatialArea.new(self)
7885
end
86+
87+
def distance_operator(other)
88+
SpatialDistanceOperator.new(self, other)
89+
end
90+
91+
alias :'<->' :distance_operator
7992
end
8093
end
8194

@@ -116,6 +129,12 @@ def st_transform(srid)
116129
def st_area
117130
Arel::Nodes::SpatialArea.new(self)
118131
end
132+
133+
def distance_operator(other)
134+
Arel::Nodes::SpatialDistanceOperator.new(self, other)
135+
end
136+
137+
alias :'<->' :distance_operator
119138
end
120139
end
121140

@@ -211,6 +230,12 @@ def visit_Arel_Nodes_SpatialArea(node, collector)
211230
visit(node.expr, collector)
212231
collector << ")"
213232
end
233+
234+
def visit_Arel_Nodes_SpatialDistanceOperator(node, collector)
235+
visit(node.left, collector)
236+
collector << " <-> "
237+
visit_spatial_operand(node.right, collector)
238+
end
214239

215240
private
216241

test/arel/visitors/postgis_test.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,38 @@ def test_where_clause_with_st_dwithin
9292
assert_sql_includes where_clause, "ST_DWithin"
9393
assert_sql_includes where_clause, "1000) = TRUE"
9494
end
95+
96+
def test_knn_distance_operator
97+
table = Arel::Table.new(:spatial_models)
98+
query_point = factory(srid: 3785).point(1, 2)
99+
100+
# Test the <-> operator
101+
node = table[:location].distance_operator(query_point)
102+
103+
assert_sql_includes node, '"spatial_models"."location" <-> ST_GeomFromEWKT'
104+
assert_sql_includes node, "SRID=3785;POINT (1 2)"
105+
end
106+
107+
def test_knn_operator_alias
108+
table = Arel::Table.new(:spatial_models)
109+
query_point = factory(srid: 3785).point(1, 2)
110+
111+
# Test the <-> alias
112+
node = table[:location].send(:'<->', query_point)
113+
114+
assert_sql_includes node, '"spatial_models"."location" <-> ST_GeomFromEWKT'
115+
end
116+
117+
def test_knn_in_order_clause
118+
table = Arel::Table.new(:spatial_models)
119+
query_point = factory(srid: 4326).point(-72.1, 42.1)
120+
121+
# Simulate ORDER BY with KNN
122+
order_node = table[:location].distance_operator(query_point).asc
123+
124+
assert_sql_includes order_node, '<-> ST_GeomFromEWKT'
125+
assert_sql_includes order_node, 'ASC'
126+
end
95127

96128
private
97129

0 commit comments

Comments
 (0)