首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >使用 ES|QL 进行地理空间距离搜索

使用 ES|QL 进行地理空间距离搜索

原创
作者头像
点火三周
发布2025-06-17 14:26:08
发布2025-06-17 14:26:08
20210
代码可运行
举报
运行总次数:0
代码可运行

地理空间数据搜索

首先,回忆一下我们在上一篇博客中使用的主要搜索功能 ST_INTERSECTS。假设你有一组丹麦的兴趣点(POIs)数据,比如从 Geofabrik 下载的 OpenStreetMap 丹麦数据,并已将其导入到 Elasticsearch(例如,通过使用 Kibana 地图的功能导入 ESRI ShapeFiles),你可以使用 ES|QL 搜索特定区域内的兴趣点,例如哥本哈根市:

代码语言:javascript
代码运行次数:0
运行
复制
FROM denmark_pois
| WHERE name IS NOT NULL
| WHERE ST_INTERSECTS(
    geometry, 
    TO_GEOSHAPE(
      "POLYGON ((12.444077 55.606669, 12.681656 55.608996, 12.639084 55.720149, 12.593765 55.762282, 12.459869 55.747985, 12.417984 55.654735, 12.444077 55.606669))"
    )
  )
| LIMIT 10000

这个查询在我们用来描绘哥本哈根市的简单多边形内搜索所有点几何。

哥本哈根多边形和圆
哥本哈根多边形和圆

但是,这个查询使用了一个庞大的多边形表达式,不是特别直观。我们更有可能想查询的是所有在某个中心点一定距离范围内的点,比如我们当前位于哥本哈根中央火车站:

代码语言:javascript
代码运行次数:0
运行
复制
FROM denmark_pois
| WHERE name IS NOT NULL
| WHERE ST_DISTANCE(
    geometry, 
    TO_GEOPOINT("POINT (12.564926 55.672938)")
  ) < 10000
| LIMIT 10000

这个更简单的查询请求所有距离给定纬度 55.672938 和经度 12.564926 的点 10,000 米(10 公里)以内的点。

哥本哈根多边形和圆
哥本哈根多边形和圆

现在,将其与在 Elasticsearch 查询 DSL 中的等效查询进行比较:

代码语言:javascript
代码运行次数:0
运行
复制
POST denmark_pois/_search
{
  "size": 10000,
  "query": {
    "geo_distance": {
      "distance": "10km",
      "geometry": {
        "lat": 55.672938,
        "lon": 12.564926
      }
    }
  }
}

这两个查询在意图上都相当清晰。然而,请注意 ES|QL 查询与 SQL 非常相似。PostGIS 中相同的查询如下所示:

代码语言:sql
复制
SELECT * FROM denmark_pois WHERE ST_Distance(
    geometry::geography, 
    ST_SetSRID(ST_MakePoint(12.564926, 55.672938), 4326)::geography
) < 10000 LIMIT 10000;

看看 ES|QL 的例子,是不是很相似?实际上,ES|QL 查询甚至比 PostGIS 查询更简单,因为它不需要 ST_SetSRID 函数来设置点几何的坐标参考系统(CRS),也不需要 ::geography 类型转换来确保距离计算在球面坐标系上进行。这是因为 ES|QL 的 TO_GEOPOINT 使用 geo_point 类型,它总是使用 WGS84 CRS,并且确保所有距离计算都是在球面坐标系上完成。

距离计算

这引出了一个重要问题:距离计算是如何工作的?如上所述,ES|QL 的 geo_point 类型总是在 WGS84 坐标参考系统(CRS)中,这是一个球面 CRS。实际的距离计算是使用 Haversine 公式完成的,该公式可以根据两点的纬度和经度计算球面上的距离。这适用于 ES|QL 的 ST_DISTANCE 函数和 Query DSL 的 geo_distance 查询。

这又引出了另一个重要点。由于我们与 Query DSL 兼容,甚至可以利用相同的底层 Lucene 空间索引,因此距离计算也受限于 Lucene 空间索引中定义的相同精度。Lucene 使用量化函数将 64 位浮点数转换为 32 位整数,这意味着 Elasticsearch 中的所有空间函数,也包括 ES|QL,均受限于这种精度,约为 1 厘米。详细内容请参阅这篇博客:BKD-backed geo_shapes in Elasticsearch: precision + efficiency + speed

ST_DISTANCE 的其他用途

我们还可以在很多其他情况下使用 ST_DISTANCE 函数,甚至当结果不打算在地图上显示时:

代码语言:javascript
代码运行次数:0
运行
复制
FROM denmark_pois
| WHERE name IS NOT NULL
| WHERE ST_DISTANCE(geometry, TO_GEOPOINT("POINT (12.564926 55.672938)")) < 10000
| STATS count=COUNT() BY fclass
| SORT count DESC
| LIMIT 16

结果是一个按类别排序的“兴趣点”数量表,按最常见的类别排序:

代码语言:plaintext
复制
count     |    fclass
----------+---------------
1528      | fast_food
930       | cafe
842       | restaurant
492       | clothes
490       | bar
457       | hairdresser
368       | artwork
364       | supermarket
326       | convenience
258       | bakery
255       | bicycle_shop
184       | kiosk
135       | beverages
133       | jeweller
120       | butcher
113       | pub

接下来我们可能想专注于咖啡馆,找出距离中央车站最近的那些:

代码语言:javascript
代码运行次数:0
运行
复制
FROM denmark_pois
| WHERE name IS NOT NULL AND fclass == "cafe"
| EVAL distance = ST_DISTANCE(geometry, TO_GEOPOINT("POINT (12.564926 55.672938)"))
| WHERE distance < 2000
| SORT distance ASC
| LIMIT 100

这个查询不仅过滤结果以仅包含咖啡馆,还计算并返回距离,最近的咖啡馆会排在最前面。我们甚至可以使用报告的距离来为 Kibana 中的地图上色。

哥本哈根多边形和圆
哥本哈根多边形和圆
为什么不用 SQL?

那么 Elasticsearch SQL 呢?它已经存在了一段时间,并且具有一些地理空间功能。然而,Elasticsearch SQL 是作为原始查询 API 之上的包装器编写的,这意味着只有可以转换为原始 API 的查询才受支持。ES|QL 没有这种限制。作为一个全新的架构,它允许许多在 SQL 中不可能进行的优化。它甚至允许一些在查询 API 中不可能的功能,比如 EVAL 命令,它允许你计算表达式并返回结果。我们的基准测试显示,ES|QL 通常比查询 API 更快,特别是在聚合方面!

显然,从之前的例子中可以看出,ES|QL 与 SQL 非常相似,但也有一些重要的区别,我们在之前的博客中详细讨论过:ES|QL 中的地理空间搜索

ST_DISTANCE 性能

一个显而易见的问题是 ST_DISTANCE 函数的性能如何?乍一看,它似乎很慢,因为它需要计算索引中每个点的距离。然而,事实并非如此。ST_DISTANCE 函数被优化为使用与 Query DSL 中的 geo_distance 查询相同的空间索引。事实上,甚至 SORT distance ASC 命令也被优化为使用相同的空间索引,因此非常快。

去年,当我们首次实现 ST_DISTANCE 函数时,它在我们的基准数据集上运行大约需要 30 秒。然后我们进行了一个名为“Lucene 推送”的优化,通过确保适当的查询在可能的情况下充分利用底层 Lucene 索引,之后相同的查询只需要 50 毫秒即可完成。

距离排序基准
距离排序基准

那么,这些优化是如何实现的?通常,对于像 ES|QL 这样的声明性查询语言,查询引擎可以分析查询并确定执行它的最佳方式。像 ST_DISTANCE 这样的函数是否可以被优化取决于查询结构和底层数据。考虑以下查询:

代码语言:javascript
代码运行次数:0
运行
复制
FROM airports
| EVAL distance = ST_DISTANCE(location, TO_GEOPOINT("POINT(12.565 55.673)"))
| WHERE distance < 1000000 AND scalerank < 6 AND distance > 10000
| SORT distance ASC
| KEEP distance, abbrev, name, location, country, city

此查询计算从哥本哈根中央车站到所有机场的距离,过滤结果仅包括重要机场(scalerank 小于 6),且距离在 10 公里到 1000 公里之间,这排除了哥本哈根机场本身,并按距离对结果进行排序。要完成的工作相当多。我们如何加快速度?查询引擎包含一组规则,每个规则执行特定的优化。通过对查询反复应用这些规则,可以逐步将查询转变为语义上等价但快得多的查询。

在我们的示例中,进行了以下更改:

  • 在末尾添加了 LIMIT 1000(如果您自己没有添加,ES|QL 总是会这样做)。
  • SORTLIMIT 合并为一个 TOPN 命令,这是 Lucene 特别支持的。
  • WHERE 子句被分为两部分,一部分按距离过滤,另一部分按 scalerank 过滤,以便精化过程变为:
    • 首先按 scalerank 过滤,这是一个已知的索引字段,易于使用“Lucene 推送”优化。
    • 仅对剩余文档计算距离,这是一组小得多的文档。
    • 按距离(包括上下界)过滤,这也可能适合后续优化。
  • 最后,将所有可以下推到 Lucene 的内容下推:
    • scalerank 过滤器下推到 Lucene。
    • 将距离过滤器转换为两个空间交集过滤器:
      • ST_INTERSECTS(location, TO_GEOSHAPE("CIRCLE(12.565 55.673, 1000000)"))
      • ST_DISJOINT(location, TO_GEOSHAPE("CIRCLE(12.565 55.673, 10000)"))
    • 也将这些下推到 Lucene,它将使用空间索引快速过滤掉不符合标准的文档。
    • TOPN 命令下推到 Lucene,它原生支持 GeoDistanceSort

这将大大减少搜索返回的文档数量,从而减少 ES|QL 计算引擎需要执行的工作量。然后,过滤后的文档将按以下步骤处理:

  • 从剩余文档中提取 location 字段。
  • 计算每个文档的距离(因为查询仍然期望返回 distance 的值)。
  • 提取 KEEP 命令中请求的其他字段。
  • 将所有数据节点的数据发送回协调节点。
  • 在协调节点上对合并结果进行最终的距离排序。

如上所述,这类查询的性能令人印象深刻。在我们的基准测试中,在一个包含 6000 万个点的数据集上运行仅需 50 毫秒,而未应用索引优化时则需 30 秒。

OGC 函数

上一篇博客 所述,Elasticsearch 8.14 引入了四个 OGC 空间搜索函数。随着在 8.15 中添加了 ST_DISTANCE,我们现在有了一套完整的 OGC 函数,它们被认为是 ES|QL 中核心的“空间搜索”功能的一部分:

  • ST_INTERSECTS:如果两个几何相交则返回 true,否则返回 false。与 PostGIS 中的 ST_Intersects 进行比较。
  • ST_DISJOINT:如果两个几何不相交则返回 true,否则返回 false。是 ST_INTERSECTS 的反义词。与 PostGIS 中的 ST_Disjoint 进行比较。
  • ST_CONTAINS:如果一个几何包含另一个则返回 true,否则返回 false。与 PostGIS 中的 ST_Contains 进行比较。
  • ST_WITHIN:如果一个几何在另一个几何内部则返回 true,否则返回 false。是 ST_CONTAINS 的反义词。与 PostGIS 中的 ST_Within 进行比较。
  • ST_DISTANCE:返回两个几何之间的距离。如果字段类型为 geo_point,则使用球面计算,与现有的 Elasticsearch geo_distance 查询相同。与 PostGIS 中的 ST_Distance 进行比较。

所有这些函数的行为都与其 PostGIS 对应函数相似,并且使用方式也相同。如果您按照上文中的文档链接查看,您可能会注意到所有的 ES|QL 示例都在 FROM 子句后的 WHERE 子句中,而所有的 PostGIS 示例都在使用字面几何。实际上,两个平台都支持在查询中任何语义合理的地方使用这些函数。

限制

PostGIS 文档中 ST_DISTANCE 的第一个示例是:

代码语言:sql
复制
SELECT ST_Distance(
    'SRID=4326;POINT(-72.1235 42.3521)'::geometry,
    'SRID=4326;LINESTRING(-72.1260 42.45, -72.123 42.1546)'::geometry
);

ES|QL 的等效代码是:

代码语言:javascript
代码运行次数:0
运行
复制
ROW ST_DISTANCE(
    "POINT(-72.1235 42.3521)"::geo_point,
    "LINESTRING(-72.1260 42.45, -72.123 42.1546)"::geo_shape
)

然而,我们在 ES|QL 中尚不支持 geo_shape。目前,您只能计算两个 geo_point 几何或两个 cartesian_point 几何之间的距离。

接下来的发展

自从我们添加 ST_DISTANCE 之后,我们还增加了两个新的聚合函数:

这些是用于 STATS 命令的聚合函数,也是我们计划添加到 ES|QL 中的许多空间分析功能中的第一个。我们将在有更多成果展示时撰写相关博客!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 地理空间数据搜索
  • 距离计算
  • ST_DISTANCE 的其他用途
    • 为什么不用 SQL?
  • ST_DISTANCE 性能
  • OGC 函数
    • 限制
  • 接下来的发展
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档