最近邻搜索
Sedona 通过提供地理空间 k 近邻(kNN)连接方法来支持对地理空间数据进行最近邻搜索。该方法基于地理临近性识别给定空间点或区域的 k 个最近邻,通常使用空间坐标以及合适的距离度量(如欧氏距离或大圆距离)。
ST_KNN¶
简介:用于在空间数据集中查找一个点或区域的 k 个最近邻的连接操作。
格式:ST_KNN(R: Table, S: Table, k: Integer, use_spheroid: Boolean)
其中 R 是查询侧表,S 是对象侧表,K 是邻居数量。use_spheroid 是一个布尔值,决定是否使用椭球距离。
查询侧表包含用于在对象侧表中查找 k 近邻的几何对象。
当查询数据或对象数据中存在非点几何(其他几何类型)时,会取每个几何对象的质心。
当距离上出现并列时,只有在下面的 sedona 配置被设置为 true 时,结果才会包含所有并列的几何对象:
关于内连接的说明:
ST_KNN连接仅支持 left inner join。- 它只返回那些至少存在一个在 k 近邻范围内的匹配邻居的对。
- 如果某个查询点没有有效邻居(例如 k 设得太大),它会被从结果中排除。
spark.sedona.join.knn.includeTieBreakers=true
谓词下推注意事项:¶
在 ST_KNN 之后对结果 DataFrame 施加的过滤条件,部分可能会被下推到 kNN 连接的对象侧。这意味着这些过滤会在 kNN 连接执行之前应用到对象侧的读取阶段。如果你希望过滤在 kNN 连接之后再生效,请先将 kNN 连接的结果物化,然后再应用过滤条件。
例如,可以使用以下方式:
Scala 示例:
val knnResult = knnJoinDF.cache()
val filteredResult = knnResult.filter(condition)
SQL 示例:
CREATE OR REPLACE TEMP VIEW knnResult AS
SELECT * FROM (
-- Your KNN join SQL here
) AS knnView;
CACHE TABLE knnResult;
SELECT * FROM knnResult WHERE condition;
优化屏障¶
使用 barrier 函数可以阻止谓词下推,并在复杂的空间连接中控制谓词的求值顺序。该函数通过在运行时对布尔表达式求值来创建一个优化屏障。
barrier 函数接收一个作为字符串的布尔表达式,之后是若干对变量名及其取值,这些取值会在该表达式中被替换:
barrier(expression, var_name1, var_value1, var_name2, var_value2, ...)
过滤条件相对于 KNN 连接的放置位置会改变查询的语义:
- 在 KNN 之前过滤:先对数据进行过滤,再在过滤后的子集上查找 K 个最近邻。回答的是“评分高的餐厅中,最近的 K 家是哪几家?”
- 在 KNN 之后过滤:先在全部数据中查找 K 个最近邻,再对结果进行过滤。回答的是“最近的 K 家餐厅中,哪几家是高评分的?”
示例¶
查找距离每家豪华酒店最近的 3 家高评分餐厅,并确保 KNN 连接先完成,再进行过滤。
SELECT
h.name AS hotel,
r.name AS restaurant,
r.rating
FROM hotels AS h
INNER JOIN restaurants AS r
ON ST_KNN(h.geometry, r.geometry, 3, false)
WHERE barrier('rating > 4.0 AND stars >= 4',
'rating', r.rating,
'stars', h.stars)
借助 barrier 函数,这条查询会先为每家酒店找到 3 家最近的餐厅(不考虑评分),随后再过滤,仅保留餐厅评分大于 4.0 且酒店星级不低于 4 的配对。如果不使用 barrier,优化器可能会将过滤下推,将查询改写为先过滤出高评分餐厅与豪华酒店,再在这些过滤后的子集中找最近的 3 个。
在 ST_KNN 连接中处理 SQL 定义的表:¶
在 Sedona 中,如果使用硬编码的 SQL select 语句创建 DataFrame,并随后在 ST_KNN 连接中使用它们,Sedona 可能会以绕过 kNN 连接逻辑的方式来优化查询。具体地说,如果你像下面这样用硬编码 SQL 创建 DataFrame:
val df1 = sedona.sql("SELECT ST_Point(0.0, 0.0) as geom1")
val df2 = sedona.sql("SELECT ST_Point(0.0, 0.0) as geom2")
val df = df1.join(df2, expr("ST_KNN(geom1, geom2, 1)"))
Sedona 可能会把这次连接优化为类似下面的形式:
SELECT ST_KNN(ST_Point(0.0, 0.0), ST_Point(0.0, 0.0), 1)
结果是,ST_KNN 函数被当作用户自定义函数(UDF)处理,而非一个连接操作,从而阻止 Sedona 走 kNN 连接的执行路径。与典型的 UDF 不同,ST_KNN 函数会跨 DataFrame 作用在多行上,而不是仅作用在单行上。当出现这种情况时,查询会以 UnsupportedOperationException 失败,提示不支持该 KNN 谓词。
解决方法:
为防止 Spark 的优化绕过 kNN 连接逻辑,必须先将由硬编码 SQL select 创建的 DataFrame 物化,再执行连接。可以通过缓存 DataFrame 告诉 Spark 不要进行这种不希望的优化:
val df1 = sedona.sql("SELECT ST_Point(0.0, 0.0) as geom1").cache()
val df2 = sedona.sql("SELECT ST_Point(0.0, 0.0) as geom2").cache()
val df = df1.join(df2, expr("ST_KNN(geom1, geom2, 1)"))
通过 .cache() 物化 DataFrame 后,Spark 逻辑计划中会走正确的 kNN 连接路径,避免将 ST_KNN 当成普通 UDF 处理的优化。
SQL 示例¶
假设我们有两张表 QUERIES 与 OBJECTS,数据如下:
QUERIES 表:
ID GEOMETRY NAME
1 POINT(1 1) station1
2 POINT(10 10) station2
3 POINT(-0.5 -0.5) station3
OBJECTS 表:
ID GEOMETRY NAME
1 POINT(11 5) bank1
2 POINT(12 1) bank2
3 POINT(-1 -1) bank3
4 POINT(-3 5) bank4
5 POINT(9 8) bank5
6 POINT(4 3) bank6
7 POINT(-4 -5) bank7
8 POINT(4 -2) bank8
9 POINT(-3 1) bank9
10 POINT(-7 3) bank10
11 POINT(11 5) bank11
12 POINT(12 1) bank12
13 POINT(-1 -1) bank13
14 POINT(-3 5) bank14
15 POINT(9 8) bank15
16 POINT(4 3) bank16
17 POINT(-4 -5) bank17
18 POINT(4 -2) bank18
19 POINT(-3 1) bank19
20 POINT(-7 3) bank20
SELECT
QUERIES.ID AS QUERY_ID,
QUERIES.GEOMETRY AS QUERIES_GEOM,
OBJECTS.GEOMETRY AS OBJECTS_GEOM
FROM QUERIES JOIN OBJECTS ON ST_KNN(QUERIES.GEOMETRY, OBJECTS.GEOMETRY, 4, FALSE)
输出:
+--------+-----------------+-------------+
|QUERY_ID|QUERIES_GEOM |OBJECTS_GEOM |
+--------+-----------------+-------------+
|3 |POINT (-0.5 -0.5)|POINT (-1 -1)|
|3 |POINT (-0.5 -0.5)|POINT (-1 -1)|
|3 |POINT (-0.5 -0.5)|POINT (-3 1) |
|3 |POINT (-0.5 -0.5)|POINT (-3 1) |
|1 |POINT (1 1) |POINT (-1 -1)|
|1 |POINT (1 1) |POINT (-1 -1)|
|1 |POINT (1 1) |POINT (4 3) |
|1 |POINT (1 1) |POINT (4 3) |
|2 |POINT (10 10) |POINT (9 8) |
|2 |POINT (10 10) |POINT (9 8) |
|2 |POINT (10 10) |POINT (11 5) |
|2 |POINT (10 10) |POINT (11 5) |
+--------+-----------------+-------------+