作者:来自 Elastic Craig Taverner
多年来,Elasticsearch 一直具有强大的地理空间搜索和分析功能,但其 API 与典型的 GIS 用户习惯的 API 截然不同。在过去的一年中,我们添加了 ES|QL 查询语言,这是一种管道查询语言,与 SQL 一样简单,甚至更简单。它特别适合 Elastic 擅长的搜索、安全性和可观察性用例。我们还在 ES|QL 中添加了对地理空间搜索和分析的支持,使其使用起来更加容易,尤其是对于来自 SQL 或 GIS 社区的用户而言。
Elasticsearch 8.12 和 8.13 为 ES|QL 提供了对地理空间类型的基本支持。8.14 中地理空间搜索功能的添加大大增强了这一功能。更重要的是,这种支持旨在与 PostGIS 等其他空间数据库使用的开放地理空间联盟 (Open Geospatial Consortium - OGC) 的简单功能访问(Simple Feature Access)标准紧密一致,让熟悉这些标准的 GIS 专家更容易使用。
在本篇博文中,我们将向你展示如何使用 ES|QL 执行地理空间搜索,以及它与 SQL 和查询 DSL 等价物的比较。我们还将向你展示如何使用 ES|QL 执行空间连接,以及如何在 Kibana Maps 中可视化结果。请注意,此处描述的所有功能均处于 “technial preview - 技术预览” 状态,我们很乐意听取你对如何改进这些功能的反馈。
搜索地理空间数据
让我们从一个示例查询开始:
FROM airport_city_boundaries
| WHERE ST_INTERSECTS(city_boundary,"POLYGON((109.4 18.1, 109.6 18.1, 109.6 18.3, 109.4 18.3, 109.4 18.1))"::geo_shape)
| KEEP abbrev, airport, region, city, city_location
这将搜索与三亚凤凰国际机场 (SYX) 周围的矩形搜索多边形相交的任何城市边界多边形。
在机场、城市和城市边界的样本数据集中,此搜索找到相交多边形并从匹配的文档中返回所需的字段:
abbrev | airport | region | city | city_location |
---|---|---|---|---|
SYX | Sanya Phoenix Int'l | 天涯区 | Sanya | POINT(109.5036 18.2533) |
这很简单!现在将其与相同查询的经典 Elasticsearch 查询 DSL 进行比较:
GET /airport_city_boundaries/_search
{"_source": ["abbrev", "airport", "region", "city", "city_location"],"query": {"geo_shape": {"city_boundary": {"shape": {"type": "polygon","coordinates" : [[[109.4, 18.1],[109.6, 18.1],[109.6, 18.3],[109.4, 18.3],[109.4, 18.1]]]}}}}
}
这两个查询的目的都相当明确,但 ES|QL 查询与 SQL 非常相似。PostGIS 中的相同查询如下所示:
SELECT abbrev, airport, region, city, city_location
FROM airport_city_boundaries
WHERE ST_INTERSECTS(city_boundary,'SRID=4326;POLYGON((109.4 18.1, 109.6 18.1, 109.6 18.3, 109.4 18.3, 109.4 18.1))'::geometry
);
回顾一下 ES|QL 示例。很相似,对吧?
FROM airport_city_boundaries
| WHERE ST_INTERSECTS(city_boundary,"POLYGON((109.4 18.1, 109.6 18.1, 109.6 18.3, 109.4 18.3, 109.4 18.1))"::geo_shape)
| KEEP abbrev, airport, region, city, city_location
我们发现 Elasticsearch API 的现有用户发现 ES|QL 更易于使用。我们现在预计现有 SQL 用户(尤其是 Spatial SQL 用户)会发现 ES|QL 与他们习惯使用的内容非常相似。
为什么不使用 SQL?
Elasticsearch SQL 怎么样?它已经存在了一段时间,并且具有一些地理空间功能。但是,Elasticsearch SQL 是作为原始查询 API 之上的包装器编写的,这意味着只有可以转换为原始 API 的查询才受支持。ES|QL 没有这个限制。作为一个全新的堆栈,它允许进行许多在 SQL 中无法实现的优化。我们的基准测试显示 ES|QL 通常比查询 API 更快,尤其是在聚合方面!
与 SQL 的区别
显然,从前面的示例可以看出,ES|QL 与 SQL 有些相似,但也存在一些重要的区别。例如,ES|QL 是一种管道查询语言,以 FROM 之类的源命令开始,然后用管道 | 字符将所有后续命令链接在一起。这使得很容易理解每个命令如何接收数据表并对该表执行某些操作,例如使用 WHERE 进行过滤、使用 EVAL 添加列或使用 STATS 执行聚合。不是从 SELECT 开始定义最终输出列,而是可以有一个或多个 KEEP 命令,最后一个命令指定最终输出结果。这种结构简化了查询的推理。
关注上例中的 WHERE 命令,我们可以看到它与 PostGIS 示例非常相似:
ES|QL
WHERE ST_INTERSECTS(city_boundary,"POLYGON((109.4 18.1, 109.6 18.1, 109.6 18.3, 109.4 18.3, 109.4 18.1))"::geo_shape
)
PostGIS
WHERE ST_INTERSECTS(city_boundary,'SRID=4326;POLYGON((109.4 18.1, 109.6 18.1, 109.6 18.3, 109.4 18.3, 109.4 18.1))'::geometry
)
除了字符串引号字符的差异之外,最大的区别在于我们如何将字符串强制转换为空间类型。在 PostGIS 中,我们使用 ::geometry 后缀,而在 ES|QL 中,我们使用 ::geo_shape 后缀。这是因为 ES|QL 在 Elasticsearch 中运行,并且类型转换运算符 :: 可用于将字符串转换为任何受支持的 ES|QL 类型,在本例中为 geo_shape。此外,Elasticsearch 中的 geo_shape 和 geo_point 类型暗示了称为 WGS84 的空间坐标系,通常使用 SRID 编号 4326 来表示。在 PostGIS 中,这需要明确说明,因此在 WKT 字符串中使用 SRID=4326; 前缀。如果删除该前缀,SRID 将设置为 0,这更像 Elasticsearch 类型 cartesian_point 和 cartesian_shape,它们不与任何特定坐标系绑定。
ES|QL 和 PostGIS 都提供类型转换函数语法:
ES|QL
WHERE ST_INTERSECTS(city_boundary,TO_GEOSHAPE("POLYGON((109.4 18.1, 109.6 18.1, 109.6 18.3, 109.4 18.3, 109.4 18.1))")
)
PostGIS
WHERE ST_INTERSECTS(city_boundary,ST_SetSRID(ST_GeomFromText('POLYGON((109.4 18.1, 109.6 18.1, 109.6 18.3, 109.4 18.3, 109.4 18.1))'),4326)
)
OGC 函数
Elasticsearch 8.14 引入了以下四个 OGC 空间搜索函数:
ES|QL | PostGIS | Description |
---|---|---|
ST_INTERSECTS | ST_Intersects | 如果两个几何图形相交,则返回 true,否则返回 false。 |
ST_DISJOINT | ST_Disjoint | 如果两个几何图形不相交,则返回 true,否则返回 false。与 ST_INTERSECTS 相反。 |
ST_CONTAINS | ST_Contains | 如果一个几何图形包含另一个几何图形,则返回 true,否则返回 false。 |
ST_WITHIN | ST_Within | 如果一个几何图形位于另一个几何图形内,则返回 true,否则返回 false。ST_CONTAINS 的逆运算。 |
这些函数的行为与 PostGIS 对应函数类似,使用方式也相同。例如,如果两个几何图形相交,ST_INTERSECTS 返回 true,否则返回 false。如果你点击上表中的文档链接,你可能会注意到所有 ES|QL 示例都在 FROM 子句后的 WHERE 子句中,而所有 PostGIS 示例都使用文字几何图形。事实上,这两个平台都支持在查询的任何部分使用这些函数。
PostGIS 文档中 ST_INTERSECTS 的第一个示例是:
SELECT ST_Intersects('POINT(0 0)'::geometry,'LINESTRING ( 2 0, 0 2 )'::geometry
);
ES|QL 中与此等价的版本为:
ROW ST_INTERSECTS("POINT(0 0)"::geo_point,"LINESTRING ( 2 0, 0 2 )"::geo_shape
)
请注意,我们在 PostGIS 示例中没有指定 SRID。这是因为在 PostGIS 中使用几何类型时,所有计算都是在平面坐标系上进行的,因此如果两个几何具有相同的 SRID,则 SRID 是什么并不重要。在 Elasticsearch 中,大多数函数也是如此,但是,也存在例外,其中 geo_shape 和 geo_point 使用球面计算,我们将在下一篇关于空间距离搜索的博客中看到。
ES|QL 多功能性
所以,我们已经看到了上面在 WHERE 子句和 ROW 命令中使用空间函数的示例。它们还能在哪些地方发挥作用?一个非常有用的地方是在 EVAL 命令中。此命令允许你评估表达式并返回结果。例如,让我们确定按国家名称分组的所有机场的质心(centroids)是否在勾勒出该国轮廓的边界内:
FROM airports
| EVAL in_uk = ST_INTERSECTS(location, TO_GEOSHAPE("POLYGON((1.2305 60.8449, -1.582 61.6899, -10.7227 58.4017, -7.1191 55.3291, -7.9102 54.2139, -5.4492 54.0078, -5.2734 52.3756, -7.8223 49.6676, -5.0977 49.2678, 0.9668 50.5134, 2.5488 52.1065, 2.6367 54.0078, -0.9668 56.4625, 1.2305 60.8449))"))
| EVAL in_iceland = ST_INTERSECTS(location, TO_GEOSHAPE("POLYGON ((-25.4883 65.5312, -23.4668 66.7746, -18.4131 67.4749, -13.0957 66.2669, -12.3926 64.4159, -20.1270 62.7346, -24.7852 63.3718, -25.4883 65.5312))"))
| EVAL within_uk = ST_WITHIN(location, TO_GEOSHAPE("POLYGON((1.2305 60.8449, -1.582 61.6899, -10.7227 58.4017, -7.1191 55.3291, -7.9102 54.2139, -5.4492 54.0078, -5.2734 52.3756, -7.8223 49.6676, -5.0977 49.2678, 0.9668 50.5134, 2.5488 52.1065, 2.6367 54.0078, -0.9668 56.4625, 1.2305 60.8449))"))
| EVAL within_iceland = ST_WITHIN(location, TO_GEOSHAPE("POLYGON ((-25.4883 65.5312, -23.4668 66.7746, -18.4131 67.4749, -13.0957 66.2669, -12.3926 64.4159, -20.1270 62.7346, -24.7852 63.3718, -25.4883 65.5312))"))
| STATS centroid = ST_CENTROID_AGG(location), count=COUNT() BY in_uk, in_iceland, within_uk, within_iceland
| SORT count ASC
结果符合预期,英国机场的质心位于英国边界内,而不是冰岛边界内,反之亦然:
centroid | count | in_uk | in_iceland | within_uk | within_iceland |
---|---|---|---|---|---|
POINT (-21.946634463965893 64.13187285885215) | 1 | false | true | false | true |
POINT (-2.597342072712148 54.33551226578214) | 17 | true | false | true | false |
POINT (0.04453958108176276 23.74658354606057) | 873 | false | false | false | false |
实际上,这些函数可以在查询的任何部分使用,只要它们的签名有意义。它们都接受两个参数,要么是文字空间对象,要么是空间类型的字段,并且它们都返回布尔值。一个重要的考虑因素是几何图形的坐标参考系 (coordinate reference system - CRS) 必须匹配,否则将返回错误。这意味着你不能在同一个函数调用中混合 geo_shape 和 cartesian_shape 类型。但是,你可以混合 geo_point 和 geo_shape 类型,因为 geo_point 类型是 geo_shape 类型的特殊情况,并且两者共享相同的坐标参考系。上面定义的每个函数的文档列出了支持的类型组合。
此外,任一参数都可以是空间文字或字段,顺序任意。你甚至可以指定两个字段、两个文字、一个字段和一个文字,或者一个文字和一个字段。唯一的要求是类型兼容。例如,此查询比较同一索引中的两个字段:
FROM airport_city_boundaries
| EVAL in_city = ST_INTERSECTS(city_location, city_boundary)
| STATS count=COUNT(*) BY in_city
| SORT count ASC
| EVAL cardinality = CASE(count < 10, "very few", count < 100, "few", "many")
| KEEP cardinality, count, in_city
该查询基本上询问城市位置是否在城市边界内,这通常应该是正确的,但总是有例外:
cardinality | count | in_city |
---|---|---|
few | 29 | false |
many | 740 | true |
一个更有趣的问题是机场位置是否位于机场服务的城市边界内。但是,机场位置位于与包含城市边界的索引不同的索引中。这需要一种方法来有效地查询和关联这两个独立索引中的数据。
空间 joins
ES|QL 不支持 JOIN 命令,但你可以使用 ENRICH 命令实现连接的特殊情况,其行为类似于 SQL 中的 “left join - 左连接”。此命令的操作类似于 SQL 中的 “左连接”,允许你根据两个数据集之间的空间关系使用来自另一个索引的数据来丰富来自一个索引的结果。
例如,让我们通过查找包含机场位置的城市边界来丰富机场表的结果,其中包含有关机场所服务城市的其他信息,然后对结果进行一些统计:
FROM airports
| ENRICH city_boundaries ON city_location WITH airport, region, city_boundary
| MV_EXPAND city_boundary
| EVAL boundary_wkt_length = LENGTH(TO_STRING(city_boundary))
| STATS centroid = ST_CENTROID_AGG(location), count = COUNT(city_location), min_wkt = MIN(boundary_wkt_length), max_wkt = MAX(boundary_wkt_length) BY region
| SORT count DESC
| LIMIT 5
这将返回拥有最多机场的前 5 个地区,以及所有具有匹配区域的机场的质心,以及这些区域内城市边界的 WKT 表示的长度范围:
centroid | count | min_wkt | max_wkt | region |
---|---|---|---|---|
POINT (-32.56093470960719 32.598117914802714) | 90 | 207 | 207 | null |
POINT (-73.94515332765877 40.70366442203522) | 9 | 438 | 438 | City of New York |
POINT (-83.10398317873478 42.300230911932886) | 9 | 473 | 473 | Detroit |
POINT (-156.3020245861262 20.176383580081165) | 5 | 307 | 803 | Hawaii |
POINT (-73.88902732171118 45.57078813901171) | 4 | 837 | 837 | Montréal |
那么,这里到底发生了什么?所谓的 JOIN 发生在哪里?查询的关键在于 ENRICH 命令:
FROM airports
| ENRICH city_boundaries ON city_location WITH airport, region, city_boundary
此命令指示 Elasticsearch 丰富从 airports 索引检索到的结果,并在原始索引的 city_location 字段和 airport_city_boundaries 索引的 city_boundary 字段之间执行intersects 连接,我们在之前的几个示例中使用过该字段。但其中一些信息在此查询中并不清晰可见。我们看到的是丰富策略 city_boundaries 的名称,缺失的信息封装在该策略定义中。
{"geo_match": {"indices": "airport_city_boundaries","match_field": "city_boundary","enrich_fields": ["city", "airport", "region", "city_boundary"]}
}
在这里我们可以看到它将执行 geo_match 查询(默认为 intersects),要匹配的字段是 city_boundary,而 enrich_fields 是我们要添加到原始文档的字段。其中一个字段,即区域,实际上被用作 STATS 命令的分组键,如果没有这个 “左连接” 功能,我们就无法做到这一点。有关丰富策略的更多信息,请参阅 enrich documentation。在阅读这些文档时,你会注意到它们描述了使用丰富索引在索引时丰富数据,方法是配置摄取管道。这对于 ES|QL 不是必需的,因为 ENRICH 命令在查询时起作用。使用必要的数据和丰富策略准备丰富索引,然后在 ES|QL 查询中使用 ENRICH 命令就足够了。
你可能还注意到最常见的区域为 null。这意味着什么?回想一下,我曾将此命令比作 SQL 中的 “左连接”,这意味着如果未找到机场的匹配城市边界,则仍会返回该机场,但 airport_city_boundaries 索引中的字段值为 null。结果发现有 89 个机场未找到匹配的城市边界,而一个机场的匹配项区域字段为 null。这导致结果中有 90 个机场没有 region。另一个有趣的细节是需要 MV_EXPAND 命令。这是必要的,因为 ENRICH 命令可能会为每个输入行返回多个结果,而 MV_EXPAND 有助于将这些结果分成多行,每个结果一行。这也解释了为什么 “Hawaii” 显示不同的 min_wkt 和 max_wkt 结果:有多个区域(regions)具有相同的名称但不同的边界。
Kibana 地图
Kibana 在地图应用程序中添加了对 Spatial ES|QL 的支持。这意味着你现在可以使用 ES|QL 在 Elasticsearch 中搜索地理空间数据,并在地图上可视化结果。
添加图层菜单中有一个新的图层选项,称为 “ES|QL”。与迄今为止描述的所有地理空间功能一样,该选项处于 “technical preview - 技术预览” 状态。选择此选项可让你根据 ES|QL 查询的结果向地图添加图层。例如,你可以向地图添加一个显示世界上所有机场的图层。
或者你可以添加一个显示 airport_city_boundaries 索引中的多边形的图层,或者更好的是,上面的复杂 ENRICH 查询如何生成每个地区有多少个机场的统计数据?
接下来是什么?
你可能已经注意到,在上面的两个示例中,我们又挤进了另一个空间函数 ST_CENTROID_AGG。这是 STATS 命令中使用的聚合函数,也是我们计划添加到 ES|QL 的众多空间分析功能中的第一个。当我们有更多内容要展示时,我们会在博客中介绍它!
在此之前,我们想告诉你更多有关我们开发的一项特别令人兴奋的功能的信息:执行空间距离搜索的能力,这是 Elasticsearch 最常用的空间搜索功能之一。你能想象距离搜索的语法可能是什么样子吗?也许类似于 OGC 函数?请继续关注本系列的下一篇博客以了解详情!
剧透警告:Elasticsearch 8.15 刚刚发布,其中包括使用 ES|QL 进行空间距离搜索!
准备好自己尝试一下了吗?开始免费试用。
想要获得 Elastic 认证吗?了解下一次 Elasticsearch 工程师培训何时开始!
原文:Geospatial search with Elasticsearch ES|QL — Search Labs