首先,回忆一下我们在上一篇博客中使用的主要搜索功能 ST_INTERSECTS
。假设你有一组丹麦的兴趣点(POIs)数据,比如从 Geofabrik 下载的 OpenStreetMap 丹麦数据,并已将其导入到 Elasticsearch(例如,通过使用 Kibana 地图的功能导入 ESRI ShapeFiles),你可以使用 ES|QL 搜索特定区域内的兴趣点,例如哥本哈根市:
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
这个查询在我们用来描绘哥本哈根市的简单多边形内搜索所有点几何。
但是,这个查询使用了一个庞大的多边形表达式,不是特别直观。我们更有可能想查询的是所有在某个中心点一定距离范围内的点,比如我们当前位于哥本哈根中央火车站:
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 中的等效查询进行比较:
POST denmark_pois/_search
{
"size": 10000,
"query": {
"geo_distance": {
"distance": "10km",
"geometry": {
"lat": 55.672938,
"lon": 12.564926
}
}
}
}
这两个查询在意图上都相当清晰。然而,请注意 ES|QL 查询与 SQL 非常相似。PostGIS 中相同的查询如下所示:
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
函数,甚至当结果不打算在地图上显示时:
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
结果是一个按类别排序的“兴趣点”数量表,按最常见的类别排序:
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
接下来我们可能想专注于咖啡馆,找出距离中央车站最近的那些:
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 中的地图上色。
那么 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
这样的函数是否可以被优化取决于查询结构和底层数据。考虑以下查询:
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 总是会这样做)。SORT
和 LIMIT
合并为一个 TOPN
命令,这是 Lucene 特别支持的。WHERE
子句被分为两部分,一部分按距离过滤,另一部分按 scalerank
过滤,以便精化过程变为:scalerank
过滤,这是一个已知的索引字段,易于使用“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)"))
TOPN
命令下推到 Lucene,它原生支持 GeoDistanceSort
这将大大减少搜索返回的文档数量,从而减少 ES|QL 计算引擎需要执行的工作量。然后,过滤后的文档将按以下步骤处理:
location
字段。distance
的值)。KEEP
命令中请求的其他字段。如上所述,这类查询的性能令人印象深刻。在我们的基准测试中,在一个包含 6000 万个点的数据集上运行仅需 50 毫秒,而未应用索引优化时则需 30 秒。
如 上一篇博客 所述,Elasticsearch 8.14 引入了四个 OGC 空间搜索函数。随着在 8.15 中添加了 ST_DISTANCE
,我们现在有了一套完整的 OGC 函数,它们被认为是 ES|QL 中核心的“空间搜索”功能的一部分:
ST_INTERSECTS
的反义词。与 PostGIS 中的 ST_Disjoint 进行比较。ST_CONTAINS
的反义词。与 PostGIS 中的 ST_Within 进行比较。geo_point
,则使用球面计算,与现有的 Elasticsearch geo_distance 查询相同。与 PostGIS 中的 ST_Distance 进行比较。所有这些函数的行为都与其 PostGIS 对应函数相似,并且使用方式也相同。如果您按照上文中的文档链接查看,您可能会注意到所有的 ES|QL 示例都在 FROM
子句后的 WHERE
子句中,而所有的 PostGIS 示例都在使用字面几何。实际上,两个平台都支持在查询中任何语义合理的地方使用这些函数。
PostGIS 文档中 ST_DISTANCE
的第一个示例是:
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 的等效代码是:
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
之后,我们还增加了两个新的聚合函数:
ST_CENTROID_AGG
在 8.15 中添加ST_EXTENT_AGG
在 8.18 中添加这些是用于 STATS
命令的聚合函数,也是我们计划添加到 ES|QL 中的许多空间分析功能中的第一个。我们将在有更多成果展示时撰写相关博客!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。