ブログ

ES|QL を使用した Elasticsearch 地理空間検索

Elasticsearch クエリ言語 (ES|QL) での地理空間検索。Elasticsearch には強力な地理空間検索機能があり、これが ES|QL にも導入され、使いやすさと OGC への親しみやすさが大幅に向上しました。

シリーズの一部:
Elasticsearch 地理空間検索

Elasticsearch は長年にわたって強力な地理空間検索および分析機能を提供してきましたが、その API は一般的な GIS ユーザーが使い慣れたものとはまったく異なっていました。過去 1 年間で、SQL と同じくらい、あるいは SQL よりも簡単なパイプ クエリ言語である ES|QL クエリ言語を追加しました。これは、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 で視覚化する方法も紹介します。ここで説明する機能はすべて「テクニカル プレビュー」段階であるため、改善方法について皆様からのフィードバックをお待ちしています。

地理空間データの検索

クエリの例から始めましょう:

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) の周囲の長方形の検索ポリゴンと交差する都市境界ポリゴンの検索を実行します。

ESQL 地理空間検索

空港、都市、都市境界のサンプル データセットでは、この検索により交差するポリゴンが検索され、一致するドキュメントから必要なフィールドが返されます。

略語

空港

地域

都市の場所

シックス

三亜フェニックス国際空港

天外区

三亜

点(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から始めるのではなく、1 つ以上の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
)

ポストGIS

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))")
)

ポストGIS

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 では、次の 4 つの OGC 空間検索関数が導入されています。

ES|QL

ポストGIS

説明

ST_INTERSECTS

ST_交差

2 つのジオメトリが交差する場合は true を返し、そうでない場合は false を返します。

ST_DISJOINT

ST_分離

2 つのジオメトリが交差しない場合は true を返し、そうでない場合は false を返します。ST_INTERSECTS の逆。

ST_CONTAINS

ST_Contains

1 つのジオメトリに別のジオメトリが含まれている場合は true を返し、含まれていない場合は false を返します。

ST_WITHIN

ST_以内

1 つのジオメトリが別のジオメトリ内にある場合は true を返し、そうでない場合は false を返します。ST_CONTAINS の逆。

これらの関数は PostGIS の対応する関数と同様に動作し、同じように使用されます。たとえば、 ST_INTERSECTS 2 つのジオメトリが交差する場合は true を返し、そうでない場合は false を返します。上記の表のドキュメント リンクに従うと、すべての ES|QL の例がFROM句の後のWHERE句内にあるのに対し、すべての PostGIS の例はリテラル ジオメトリを使用していることに気付くでしょう。実際、どちらのプラットフォームでも、意味のあるクエリのどの部分でも関数の使用がサポートされています。

ST_INTERSECTSの PostGIS ドキュメントの最初の例は次のとおりです。

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 でgeometryタイプを使用する場合、すべての計算が平面座標系で実行されるため、両方のジオメトリが同じ SRID を持つ場合、SRID が何であるかは問題にならないためです。Elasticsearch でも、これはほとんどの関数に当てはまりますが、 geo_shapegeo_point球面計算が使用されるという例外があります。これについては、空間距離検索に関する次のブログで説明します。

ES|QLの汎用性

上記では、 WHERE句とROWコマンドで空間関数を使用する例を見てきました。他にどこで意味を成すのでしょうか?非常に便利な場所の 1 つは、 EVALコマンドです。このコマンドを使用すると、式を評価して結果を返すことができます。たとえば、国名ごとにグループ化されたすべての空港の重心が、国の境界内にあるかどうかを判断します。

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

結果は予想通りで、英国の空港の重心は英国の境界内にあり、アイスランドの境界内にはなく、その逆も同様です。

重心

カウント

英国

アイスランド

英国内

アイスランド内

ポイント (-21.94663446396589364.13187285885215)

1

間違い

true

間違い

true

ポイント (-2.597342072712148 54.33551226578214)

17

true

間違い

true

間違い

ポイント (0.04453958108176276 23.74658354606057)

873

間違い

間違い

間違い

間違い

実際、これらの関数は、そのシグネチャが意味を成すクエリのどの部分でも使用できます。これらはすべて、リテラル空間オブジェクトまたは空間型のフィールドのいずれかである 2 つの引数を取り、ブール値を返します。重要な考慮事項の 1 つは、ジオメトリの座標参照システム (CRS) が一致している必要があることです。一致していない場合はエラーが返されます。つまり、同じ関数呼び出しでgeo_shape型とcartesian_shape型を混在させることはできません。ただし、 geo_pointタイプはgeo_shapeタイプの特殊なケースであり、両方とも同じ座標参照系を共有しているため、 geo_pointタイプとgeo_shapeタイプを混在させることができます。上記で定義された各関数のドキュメントには、サポートされている型の組み合わせがリストされています。

さらに、どちらの引数も、空間リテラルまたはフィールドを任意の順序で指定できます。2 つのフィールド、2 つのリテラル、フィールドとリテラル、またはリテラルとフィールドを指定することもできます。唯一の要件は、タイプに互換性があることです。たとえば、次のクエリは同じインデックス内の 2 つのフィールドを比較します。

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

このクエリは基本的に、都市の場所が都市の境界内にあるかどうかを尋ねます。これは通常当てはまるはずですが、常に例外があります。

基数

カウント

市内

少し

29

間違い

多くの

740

true

さらに興味深い質問は、空港の場所が、その空港がサービスを提供する都市の境界内にあるかどうかです。ただし、空港の位置は、都市の境界を含むインデックスとは異なるインデックスに存在します。これには、これら 2 つの個別のインデックスからデータを効果的にクエリして相関させる方法が必要です。

空間結合

ES|QL はJOINコマンドをサポートしていませんが、SQL の「左結合」と同様に動作するENRICHコマンドを使用して、特殊な結合を実現できます。このコマンドは SQL の「左結合」に似た動作をし、2 つのデータセット間の空間関係に基づいて、1 つのインデックスの結果を別のインデックスのデータで強化することができます。

たとえば、空港の場所を含む都市の境界を見つけることで、空港のテーブルからの結果に、空港がサービスを提供する都市に関する追加情報を付加し、結果に対していくつかの統計を実行してみましょう。

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 表現の長さの範囲を返します。

重心

カウント

最小wkt

最大wkt

地域

ポイント (-32.5609347096071932.598117914802714)

90

207

207

ヌル

ポイント (-73.9451533276587740.70366442203522)

9

438

438

ニューヨーク市

ポイント (-83.1039831787347842.300230911932886)

9

473

473

デトロイト

ポイント (-156.302024586126220.176383580081165)

5

307

803

ハワイ

ポイント (-73.8890273217111845.57078813901171)

4

837

837

モントリオール

それで、ここで実際に何が起こったのでしょうか?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が元のドキュメントに追加するフィールドであることがわかります。これらのフィールドの 1 つであるregionは、実際にはSTATSコマンドのグループ化キーとして使用されていましたが、これはこの「左結合」機能がなければ実行できませんでした。エンリッチ ポリシーの詳細については、エンリッチのドキュメントを参照してください。これらのドキュメントを読んでいくと、取り込みパイプラインを構成することによって、インデックス作成時にデータを拡充するための拡充インデックスの使用について説明されていることに気付くでしょう。ES|QL では、 ENRICHコマンドがクエリ時に機能するため、これは必要ありません。必要なデータとエンリッチポリシーを使用してエンリッチインデックスを準備し、ES|QL クエリでENRICHコマンドを使用するだけで十分です。

また、最も頻繁に見つかった地域はnullであったことにも気付くでしょう。これは何を意味するのでしょうか?このコマンドを SQL の「左結合」に例えたことを思い出してください。つまり、空港に一致する都市境界が見つからない場合でも、その空港は返されますが、 airport_city_boundariesインデックスのフィールドにはnull値が含まれます。一致するcity_boundaryが見つからなかった空港が 89 か所あり、 regionフィールドがnullである一致する空港が 1 か所あることがわかりました。これにより、結果にregionが含まれない空港が 90 件見つかりました。もう 1 つの興味深い点は、 MV_EXPANDコマンドの必要性です。これが必要なのは、 ENRICHコマンドが入力行ごとに複数の結果を返す場合があり、 MV_EXPANDこれらの結果を結果ごとに 1 つずつ複数の行に分割するのに役立つためです。これにより、「ハワイ」が異なるmin_wktmax_wkt結果を表示する理由も明らかになります。同じ名前でありながら境界が異なる複数の地域が存在したためです。

Kibanaマップ

Kibana は、マップ アプリケーションに Spatial ES|QL のサポートを追加しました。つまり、ES|QL を使用して Elasticsearch で地理空間データを検索し、その結果をマップ上に視覚化できるようになりました。

Kibana レイヤー ES|QL

レイヤー追加メニューに、「ES|QL」という新しいレイヤー オプションがあります。これまで説明したすべての地理空間機能と同様に、これは「技術プレビュー」段階です。このオプションを選択すると、ES|QL クエリの結果に基づいてマップにレイヤーを追加できます。たとえば、世界中のすべての空港を表示するレイヤーをマップに追加できます。

キバナ ES|QL - 空港

または、 airport_city_boundariesインデックスからポリゴンを表示するレイヤーを追加することもできます。さらに良い方法として、各地域に空港がいくつあるかという統計を生成する、上記の複雑なENRICHクエリはどうでしょうか。

Kibana ES|QL - 地域統計

新しいエクスペリエンス

上記の 2 つの例では、さらに別の空間関数ST_CENTROID_AGGが組み込まれていることに気付いたかもしれません。これは、 STATSコマンドで使用される集計関数であり、ES|QL に追加する予定の多くの空間分析機能の最初のものです。もっと詳しく紹介できるようになったら、ブログで紹介します!

その前に、私たちが取り組んできた特に興味深い機能、つまり Elasticsearch で最もよく使用される空間検索機能の 1 つである空間距離検索を実行する機能について詳しく説明したいと思います。距離検索の構文がどのようになるか想像できますか?おそらく OGC 関数に似ていますか?詳細については、このシリーズの次のブログをお読みください。

ネタバレ注意: Elasticsearch 8.15 がリリースされました。ES|QL による空間距離検索が含まれています。

関連記事

検索関連性の評価パート1-BEIRベンチマーク

Thanos Papaoikonomou

AI盗作:Elasticsearchによる盗作検出

Priscilla Parodi

ユーザー行動データに基づく判断リストを使用して Elasticsearch で LTR モデルをトレーニングする

Alexander Dávila

Elasticsearch と SigLIP-2 による山頂のマルチモーダル探索

Navneet Kumar

Elasticsearch による語彙検索と意味検索

Priscilla Parodi

最先端の検索体験を構築する準備はできましたか?

十分に高度な検索は 1 人の努力だけでは実現できません。Elasticsearch は、データ サイエンティスト、ML オペレーター、エンジニアなど、あなたと同じように検索に情熱を傾ける多くの人々によって支えられています。ぜひつながり、協力して、希望する結果が得られる魔法の検索エクスペリエンスを構築しましょう。

はじめましょう