OpenSearchCon North America 2025のセッション「Implementing Piped Processing Language in OpenSearch Via Apache Calcite」をまとめます。

スピーカー

Lantao Jin氏はAWSのSenior Software Development Engineerで、OpenSearch上海チームに所属しています。データベース、OLAP、SQLクエリエンジンにおいて高いスケーラビリティを持つソリューションの構築で豊富な経験を持っています。
Heng Qian氏はAWSのSoftware Development Engineer IIで、同じくOpenSearch SQLとPPLに取り組んでいます。クエリパフォーマンスの最適化を専門とし、Hive、Spark、Hudi、Calcite、OpenSearchなど、様々なデータベースシステムにおいて高度にスケーラブルなソリューションを構築してきた経験を持っています。
OpenSearchにおけるクエリ
Query DSL

OpenSearchはQuery DSLと呼ばれるDSLを提供しています。DSLはJSONベースのインターフェースです。
SQL

OpenSearchはまた、SQLインターフェースも提供しています。SQLは、リレーショナルデータベースの概念とOpenSearchのドキュメント指向データの間のギャップを橋渡しする役割を果たします。これにより、既存のSQLの知識を活用してOpenSearchからデータをクエリし、分析することができます。
スライドは、CodeVersionでグループ化を行うSQLクエリの例です。このSQLクエリは、DSLクエリのスライドのクエリと機能的に同等のものとなっています。
PPL

さらに、PPLと呼ばれる言語もあります。PPLはPiped Processing Languageの略で、シーケンシャルなデータ処理に焦点を当てたクエリ言語です。パイプ演算子を使用して異なるコマンドを組み合わせることで、データを検索し取得します。PPLは特に、ログ、メトリック、トレースといったオブザーバビリティデータの分析に適しています。
スライドは、先ほどのSQLやDSLと同等の処理を行うPPLクエリの例です。PPLでは、よりシンプルで直感的な構文でデータ処理のパイプラインを表現できることがわかります。
それぞれの比較

これら3つのクエリ言語について比較してみましょう。
DSLは非常に強力で高速です。これは、ほとんどのDSLがLuceneの機能を直接活用しているためです。しかし一方で、DSLには学習曲線が急であるという課題があります。人間が読み書きするのが非常に難しく、また機能的にはユーザー定義関数がほとんどサポートされていません。
SQLは業界で広く使用されている言語であり、多くのSQL標準が存在します。OpenSearchでも、既存のSQLの知識を使って異なるデータベース間を行き来することができます。また、強力なデータ操作機能も提供しています。ただし、SQLにもいくつかの制限があります。SQLは標準であるがゆえに、複雑な問題を処理する際には制約が生じることがあります。また、SQLは基本的に構造化データに焦点を当てた言語です。
PPLはSQLに似ていますが、非常に使いやすく、書きやすいという特徴があります。ログのようなオブザーバビリティデータでも機能し、時系列データ分析にも使用できます。また、PPLの大きな強みは、複雑なデータ変換を処理するために、異なる演算子のチェーンを長くつなげることができる点です。しかし、PPLはユーザーにとってもAIにとっても非常に新しい言語であるという課題があります。
PPLの変換

ほとんどのPPLクエリはDSLに変換できます。PPLから内部的にどのようなDSLが生成されているかを理解するために、explainツールを提供しています。


PPLのexplainコマンドやexplainエンドポイントを使用すると、PPLの実行計画が出力されます。この物理計画の内部にはDSLが含まれており、それをコピーしてJSONとしてフォーマットすることで、実際のDSLを確認することができます。このツールは、PPLとDSLの違いを理解するのに役立ちます。

PPLはSQLにも翻訳可能です。explainコマンドをextendedモードで使用するか、エンドポイントにformat=extendedパラメーターを追加すると、PPLに対応するSQLが生成されます。

例として、シンプルなPPLコマンドと、それに対応するSQLを比較してみましょう。PPLでは非常にシンプルに書けるクエリが、SQLでは標準に従う必要があるため、かなり複雑な表現になることがわかります。これは、SQLが標準であるがゆえに、PPLのような強力で簡潔な機能を提供できないことを示しています。
AIアシスタントによるPPLの生成

PPLは比較的シンプルな言語ですが、それでも多くのコマンドが存在します。初心者がPPLを学ぶ際には、依然として課題があります。この課題を解決するため、Discover UIでAIアシスタンス機能を提供しています。
このT2PPLモデルは、LLMベースのText-to-PPL変換機能です。ユーザーが自然言語でクエリを入力すると、このモデルが自動的にPPLクエリに翻訳します。画面の例では、「How many error in my logs?」という自然言語の質問が、適切なPPLクエリに変換されている様子が示されています。
SQLプラグイン

SQLやPPLを使用するには、SQLプラグインをOpenSearchにインストールする必要があります。プラグインをインストールすると、2つのエンドポイントが利用可能になります。SQLクエリを実行する場合は/_plugins/_sqlエンドポイントを、PPLクエリを実行する場合は/_plugins/_pplエンドポイントを使用します。
画面には、curlコマンドを使用してクエリを実行する例が示されています。このように、馴染みのあるSQL構文やPPL構文を使って、OpenSearchからインサイトを抽出することができます。
クエリエンジンv3のアーキテクチャ
エンジンv3に至る経緯

これまでのクエリエンジンの歴史を振り返りましょう。v3と呼んでいるのは、v1とv2が存在したためです。
V1はSQLのみをサポートしていました。オリジナルのOpenSearch SQLプロジェクトは、NLPChina ES-SQLプロジェクトをベースに開発されましたが、このプロジェクトは数年前に廃止されています。
V2では、PPLとSQLの両方をサポートするエンジンを開発しました。V2では新しいデータ型システムを導入し、半構造化データのクエリをサポートしました。また、Push-downや複数のデータソースもサポートしています。図に示されているように、V2ではParser、Analyzer、Core Engine、Executionという4つの主要なコンポーネントで構成されるアーキテクチャを採用していました。

V2エンジンにも多くの課題がありました。
1つ目は実装の課題です。より複雑なクエリや新しいコマンド、関数を追加したいという要望がありましたが、V2エンジンでこれらを実装することは困難でした。
2つ目は最適化の課題です。V2エンジンでは成熟した最適化ルールを使用しておらず、コストベースのオプティマイザも備えていませんでした。
3つ目は堅牢性の課題です。5,000以上の統合テストがありますが、成熟したデータベースシステムと比較すると、テストカバレッジはまだ不十分でした。
Apache Calciteによるエンジンv3

v3での新しいソリューションは、Apache Calciteの統合です。Apache CalciteはSQLインターフェースとあらゆるクエリに対する高度な最適化を可能にするJavaフレームワークです。
Calciteは標準SQLをサポートし、クエリ最適化機能を提供します。また、サードパーティのデータソースもサポートしており、内部にはリレーショナル代数操作の仕組みがあります。これにより、クエリの書き換えやクエリの最適化が可能になります。
calcite.apache.org

Calciteを導入することで、先ほどの課題を解決できます。
まず、実装の課題についてです。Calciteは広く使用されているライブラリであり、複雑な式のサポートや多くの組み込み関数を提供しています。スクラッチからコードを書くよりも低コストで開発を進めることができます。
次に、最適化の課題についてです。Calciteの最も優れた点は、コストベースのオプティマイザを提供していることです。成熟した論理ルールを多数含んでおり、コストと統計を使用して最適な実行計画を見つけることができます。
最後に、堅牢性の課題についてです。Calciteは業界で広く採用されており、Apache Hive、Beam、Storm、Drill、Druid、Uber など、多くのプロジェクトで使用されています。このライブラリには膨大なテストが含まれており、独自開発よりもはるかに高い堅牢性を持つオペレーター実行を実現できます。
Calcite統合のアーキテクチャ

Calcite統合のアーキテクチャについて説明します。従来のSQLプラグインは、クエリプラグインという名前に変更されました。
このクエリプラグインはコーディネーターノード上にあります。DSL API、SQL API、PPL APIのすべてのリクエストがコーディネーターノードに送信されます。SQLとPPLはそれぞれ異なるエンドポイントを使用します。
受信したステートメントは、それぞれ対応するパーサーに渡されます。SQLパーサーとPPLパーサーは、共通のクエリAST(抽象構文木)を出力します。このASTがCalciteに供給され、Calciteが論理計画と物理計画を生成し、実行を支援します。
物理計画の中で、クエリの一部をDSLにPush-downできる場合は、そのDSL APIをTransportSearchServiceに送信します。これにより、処理をデータノードにPush-downすることができます。

PPLとSQLの処理フローはどちらもANTLR 4を使用してレキシカル分析とセマンティクス分析を行い、ASTを生成します。
このASTがCalciteに供給されると、CalciteはRelNodeを作成します。RelNodeは、Calcite内部で使用される論理式ツリーです。Calciteはこの論理計画に対してコストベースの最適化を実行し、物理計画を生成します。
現在、物理計画としてEnumerableRelというインターフェースを使用しています。これはCalciteのAPIの一つです。実行時に、物理計画の中でDSL APIにPush-downできるクエリが見つかれば、OpenSearch DSL APIを生成してデータノードに送信します。

ASTからRelNodeへの変換については、V2エンジンで既にASTが存在するため、それを再利用することにしました。そのため、Calciteのコアインターフェースから開始するのではなく、既存のASTを使用してRelNodeを生成するアプローチを取りました。
この変換にはCalciteのRelBuilderというツールを使用します。RelBuilderは、論理的なリレーショナル式であるRelNodeを作成するためのツールです。RelNodeには、Sort、Join、Project、Filter、Scanなど、多くの演算子の定義があります。RelBuilderは最終的に、これらのRelNodeを組み合わせて論理計画のツリーを構築します。
ASTノードをRelNodeに変換するために、CalciteRelNodeVisitorというビジターパターンを実装しました。画面には例として、visitRelation関数が示されています。この関数は入力としてRelation(テーブルやインデックスを表すASTノード)を受け取り、コンテキストからRelBuilderを取得してscan APIを呼び出し、RelNodeを作成して返します。このようなビジターを多数実装することで、ビジターパターンを使ってASTツリー全体をRelNodeツリーに変換します。
スキーママッピング

スキーマのバインディングについて検討すべきこととして、ここまでの処理ではRelBuilderを使用しているため、通常のSQLレイヤーが欠落しています。では、スキーマをどのようにマッピングしているのでしょうか。
実際のスキーマの扱いは、RelOptSchemaというインターフェースで行います。RelOptSchemaはオプティマイザレイヤーで使用されるインターフェースです。RelOptSchemaは内部にCalciteSchemaと呼ばれるコンテナを持っており、最終的にSchemaPlusというパブリックAPIを呼び出します。
そこで、OpenSearchのインデックススキーマをこのSchemaPlusに適合させる実装を行いました。このスキーマAPIがRelOptSchemaによって呼び出されます。左側のRelation(ASTノード)は、このパイプラインを経由してLogicalIndexScanと呼ばれる論理ノードに変換されます。スキーマ情報はこの論理スキャンノードの中に含まれています。

スキーママッピングができても、まだ解決すべき課題があります。それは、異なるシステム間での型のマッピングです。
OpenSearchでは、各インデックスにインデックスマッピングがあります。画面に表示されているように、boolean、float、double、integer、object、array、textなど、様々な動的マッピング型が存在します。しかし、これらの型はCalciteの型システムとは異なるため、両者を橋渡しする仕組みが必要になります。

そこで、ExprTypeと呼ばれるブリッジ型システムを導入しました。ExprTypeは実際にはV2エンジンで使用されていたものを再利用しており、インデックスマッピングとCalciteの型システムをマッピングするインターフェースとして機能します。ExprTypeは2つの型カテゴリを提供します。1つ目はExprCoreTypeで、longなどの一般的なデータベース型に簡単にマッピングできる型です。2つ目はOpenSearchDataTypeで、IPやポイントなど、OpenSearchでのみ定義されている固有の型用です。

型変換の全体的な流れを見てみましょう。まず、OpenSearchリクエストがOpenSearchのインデックスマッピングをクエリします。このマッピングがExprTypeに変換され、ExprTypeがインターフェースレイヤーとして機能してCalciteの型システムにマッピングされます。
分析ステージでは、論理計画を構築する際に各計画がRelDataTypeシステムを持ちます。RelDataTypeには、BasicSqlTypeとJavaTypeの2つのタイプがあります。IntegerやLongのような主要な型はBasicSqlTypeを使用しますが、IPやポイントのようなユーザー定義型はJavaTypeを使用します。実行時には、BasicSqlTypeとJavaTypeの両方がCalciteの内部実行でJavaクラスに変換され、最終的にJDBCインターフェースを通じて結果が返されます。
これが型システム全体の流れです。
オプティマイザ

オプティマイザとPush-downルールについて見ていきましょう。
実装した主なPush-downルールには、OpenSearchAggScanRule、OpenSearchProjectScanRule、OpenSearchSortScanRule、OpenSearchFilterScanRule、OpenSearchLimitScanRuleなどがあります。
これらのルールがどのように機能するか、具体例で見てみましょう。元の計画は、2つのテーブルに対するJoinと集約を含む複雑なものです。段階的に多くのPush-downルールが適用されることで、クエリ計画が簡素化されていきます。まず、FilterがScanにPush-downされ、次にSortがScanにPush-downされます。最終的には、2つのScanと1つのJoin演算子だけが残る、非常にシンプルな計画になります。
このように、複雑なクエリ計画も、適切な最適化ルールを適用することで、大幅に簡素化できることがわかります。

Push-downルールによってクエリは大幅に簡素化されますが、まだ課題があります。Joinなど、DSLにPush-downできない演算子が残っているのです。これらの演算子を実行するための仕組みが必要になります。
そこで、CalciteのEnumerableインターフェースを使用しています。EnumerableConventionは、結果をEnumerableとして返す規約です。EnumerableRelインターフェースは、RelNodeをEnumerable演算子、つまりJavaコードに変換します。これらのEnumerable演算子はJavaでインメモリに存在するため、行ごとに実行することができます。
特定の演算子を物理演算子に変換するためのルールも提供しています。例えば、EnumerableIndexScanRuleは、Logical Index ScanをEnumerableRelインスタンスに変換し、OpenSearchからデータを検索します。同様に、EnumerableAggregateRule、EnumerableJoinRule、EnumerableSortRule、EnumerableFilterRuleなどが、それぞれの論理演算子を実行可能な物理演算子に変換します。

EnumerableRelノードを持つツリーは、プログラムのフレームワークを構築しますが、まだ実行すべき具体的な処理が残っています。それは式の評価です。
例えば、フィルター条件として「aがbを10で割った値より大きい」という式があるとします。このような式は、式ツリーとして表現されます。Calciteでは、この式ツリーをLinq4j Expressionに変換します。
Linq4j Expressionと実行計画が組み合わされて、BlockStatementが生成されます。このBlockStatementは、Janinoコンパイラを使用してJavaバイトコードに変換されます。こうして、クエリは最終的に実行可能なJavaコードとなり、実行時にコンパイルされて実行されます。これがExpression Codegenの仕組みです。
UDF

もう1つ重要な要素として、CalciteでUDFなどの関数を登録する仕組みがあります。Calciteには多くのビルトイン関数が用意されており、これらを再利用できますが、OpenSearch固有の用途のためにカスタムUDFを構築する必要もあります。
そこで、このプラグインを拡張し、関数を登録するためのPPLFuncImplTableを作成しました。Calciteの関数テーブルは階層構造になっています。最上位にReflectiveSqlStdOperatorTableがあり、その下にCalciteのビルトイン関数を含むSqlStdOperatorTableがあります。OpenSearch SqlPluginでは、PPLBuiltinOperatorTableを通じてPPL固有のビルトイン関数を登録し、さらにPPLFuncImplTableを使ってカスタムUDFを登録します。この階層構造により、Calciteの標準関数を活用しながら、OpenSearch固有の関数を柔軟に追加できるようになっています。

UDFの開発について見ていきましょう。
新しいUDFを作成するには、本来4つの基本要素が必要です。SqlUserDefinedFunction APIの拡張、SqlReturnTypeInferenceの提供(戻り型の推論のため)、SqlTypeCheckerの提供(コンパイル段階で型チェックを行うため)、そしてImplementableFunctionの提供(実装の詳細)です。
しかし、これらを毎回実装するのは煩雑です。そこで、これらの要素を簡略化した基本クラスImplementorUDFを提供しています。
CRC32関数を例に見てみましょう。このUDFでは、ImplementorUDFを継承し、getReturnTypeInference()とgetOperandMetadata()の2つのメソッドをオーバーライドするだけです。getReturnTypeInference()では戻り型をBIGINTと指定し、getOperandMetadata()ではパラメータ型をSTRINGと定義しています。
さらに、implement()メソッドでは、RexCallをLinq4jのExpressionに変換してCodegen用の式を生成します。最後に、静的メソッドとして実際のロジックを実装します。この例では、CRC32オブジェクトを作成してチェックサムを計算する処理を記述しています。
このフレームワークにより、UDFをCalciteに統合し、PPLクエリで使用できるようになります。詳細なコード例はGitHubのリンク先で確認できます。
最適化の仕組み
コストベースオプティマイザ

V3エンジンの重要な改善点の一つは、Calciteのコストベースオプティマイザを活用したクエリ最適化です。コストベースオプティマイザは、与えられたクエリに対して最も低いコストで正しい実行計画を見つける仕組みです。
コスト見積もりは、入力行数や各演算子の計算ロジックなどのメタデータに基づいて行われます。また、新しい実行計画を継続的に生成するために、多くのヒューリスティックな最適化ルールが用意されています。
最適化プロセスは反復的に動作します。まず元のルートプランで初期化され、VolcanoPlannerがすべてのルールを適用して複数の候補プランを生成します。それぞれのプランに対してコストが計算され、最終的に最もコストの低いプランが選択されます。このプロセスは、新しいルールが新しいプランをトリガーしなくなるまで継続されます。
Calcite統合により、V3エンジンには約300以上の最適化ルールが導入されました。これには、Predicate Push-down、Column Pruning、Constant Folding、Join戦略など、最も一般的に使用され、かつ非常に効率的な最適化ルールが含まれています。

Calcite統合により、約300以上の最適化ルールがPPLの最適化プロセスに導入されました。これには、Predicate Push-down、Column Pruning、Constant Folding、Join戦略など、最も一般的に使用される非常に効率的な最適化ルールが含まれています。これらのルールがクエリ計画のコストにどのように影響するか、いくつかの具体例で見ていきましょう。
Predicate Pushdown

最初の例として、Predicate Pushdownを見てみましょう。会社のデータから若くて高給の従業員を見つけたいというシナリオを考えます。この才能ある人材を見つけるために、深く考えずにPPLクエリを素早く書いたとします。
画面左側の基本計画を見ると、実際には非効率なクエリになっています。両側のすべてのレコードに対してクロスジョインを実行した後、フィルター条件を適用する形になっています。しかし、オプティマイザがこれを改善します。オプティマイザはJoinの直上にあるフィルターを見つけ、それを2つの適切なフィルター条件に分割し、Joinを通過させて両側の子ノードにPush-downします。
この最適化により、Joinへの入力行数が大幅に削減されます。コスト見積もりの観点から見ると、Joinのコストは「左側の行数 × 右側の行数 × selectivity」で計算されます。フィルターをJoinを通してPush-downすることで、両側の行数が大幅に減少するため、最適化された計画のコストは基本計画よりも大幅に小さくなります。
Column Pruning

次の例はColumn Pruningです。このPPLクエリにはJoinコマンドとSTATSコマンドが含まれています。
画面左側の基本計画を見ると、Joinの各子ノードがインデックスからすべてのカラムをフェッチしています。log-peopleからはid、age、name、gender、addressなど、log-occupationからはid、occupation、salary、country、startなど、すべてのフィールドを取得しています。しかし実際には、これらすべてのカラムは必要ありません。
オプティマイザは、このクエリで実際に必要なカラムを正確に特定し、2つのLogicalProject演算子に対してColumn Pruningを実行します。最適化後の計画では、log-peopleからはidとageのみ、log-occupationからはidとsalaryのみを取得するようになっています。
Projectのコストは「入力行数 × 式のサイズ」で計算されます。各側でカラム数を必要最小限の2つに削減したため、最適化された計画のコストは基本計画よりも小さくなります。
カスタマイズルール

Calciteに既存の300以上のルールに加えて、カスタマイズされた最適化ルールを開発することもできます。これにより、様々なシナリオでパフォーマンスを向上させる機会が増え、特定のパフォーマンス課題に対処できるようになります。
ClickBenchからのクエリを例に見てみましょう。このクエリは、90種類の異なる式に対して集約を実行することを目的としています。集約は非常に重い演算子であり、集約が多すぎるとクラスタに大きなオーバーヘッドをかけることになります。
そこで、合計変換の数学的特性に基づいて最適化する方法を考えました。具体的には、SUM(FIELD + X) = SUM(FIELD) + X * COUNT(FIELD)という数学的性質を利用します。この性質により、元の90の集約関数を、フィールドの合計とカウントを含むわずか2つの集約関数に削減できます。元の90の関数は、この2つの集約関数の出力から計算できるのです。
コストへの影響を見てみましょう。集約のコストは「入力行数 × 集約関数のサイズ × その他の要因」で計算されます。集約関数のサイズを90から2に削減したため、コストも大幅に削減されます。
この種のカスタマイズルール以外にも、PPLのパフォーマンスを向上させるために多くのルールを実装しました。それらのほとんどは、演算子をIndexScan演算子にPush-downすることに関するものです。
OpenSearchを活かしたプッシュダウン

OpenSearchのプラグインとして、PPLはコーディネーターノードでのみ実行できるという制限があります。実質的には、クエリの単一ノード逐次処理となるため、特に大量のデータをフェッチして処理する必要がある場合、パフォーマンスに重大な制限が生じる可能性があります。
一方、OpenSearchの検索クエリは、Query PhaseとFetch Phaseの両方で並列処理が含まれています。したがって、この並列処理を活用するために、PPLをOpenSearchクエリ(DSL)としてPush-downすることが有効な解決策となります。

Push-downの実装は、最適化フェーズと実行フェーズの2つに分かれています。
最適化フェーズでは、すべての基本的な演算子をLogicalIndexScan演算子のPushDownContextにPush-downするための関連最適化ルールを実装しました。これらは実際には、コンテキスト内の多くのPush-downアクションとして構築されており、各アクションには演算子自体のダイジェストと、OpenSearch QueryBuilderを構築する方法に関するラムダ関数が含まれています。図からわかるように、Project、Filter、Sort、Aggregateなど、ほとんどすべての演算子をPush-downできます。ただし、Joinはできません。Joinは複数のインデックスにわたるクロスジョインを実行する演算子ですが、DSLは現時点でこの機能をサポートしていないためです。
実行フェーズでは、PushDownContextを、既にPush-downしたアクションに基づいて検索リクエストに構築します。そして、ノードクライアントを介してリクエストを送信し、インデックスから既に処理されたデータをフェッチします。

例として、Project演算子のPush-downを見てみましょう。これは単純に、DSLのソースステートメント内でプロジェクションフィールドを指定するだけです。

次にFilter Push-downです。PPLのWHEREコマンドをOpenSearch DSLのqueryステートメントにPush-downします。PPLで使用される関数に基づいて、適切な検索クエリタイプを選択し、正しい位置にパラメーターを配置する必要があります。このために、Predicate Analyzerというツールを実装しました。


Sort Push-downでは、PPLのSORTコマンドをDSLのsortステートメントにマッピングします。両方のクエリがソート方向をサポートしているため、完璧に対応します。さらに、Sort Push-downはJoinプロセスの強化にも役立ちます。Joinには通常、Hash JoinとSort Merge Joinの2種類の物理実装がありますが、Sort演算子を常にScanにPush-downできるため、両側のSort演算子がPush-downされたSort Merge Joinを優先的に選択します。その時間複雑度はO(m+n)となり、既存のBlock Hash JoinのO(mlogm + nlogn)よりもはるかに高速になります。

最後にAggregation Push-downです。PPLのSTATSコマンドをDSLのaggregationsステートメントで実装します。画面を見ると、PPLが同等のDSLよりもかなりシンプルであることがわかります。
スクリプトエンジン

多くの演算子をPush-downできますが、まだ課題が残っています。PPLのUDFは、DSLがほとんどのUDFに関連する実装を持っていないため、Push-downの障害となります。しかし、幸いにもOpenSearchはスクリプトクエリをサポートしており、プラグイン向けにカスタマイズされたスクリプトエンジンを許可しています。

そこで、Calcite Expressionのためにカスタマイズされた新しいスクリプトエンジンを登録し、Push-downプロセスをさらに強化するためのスクリプトクエリを構築しました。プッシュダウンプロセスでプリミティブなDSLステートメントを使用して演算子や式をPush-downできない場合、スクリプトPush-downにフォールバックします。異なる演算子は対応するスクリプトコンテキストを生成する必要がありますが、同じスクリプト言語と実行ロジックを共有できます。
スクリプトを含む検索リクエストが送信されると、事前登録されたスクリプトエンジンは、スクリプトを逆シリアル化して元のCalcite Expressionに変換します。その後、式用のコードを生成し、生成されたJavaコードをコンパイルして実行し、最終結果を返します。

スクリプトPush-downの威力を示す例を見てみましょう。OpenTelemetryログに関する分析クエリで、HTTPステータス統計を生成します。このクエリワークフローには、ログ本文からHTTPステータスコードをパースし、様々なスコープでこれらのコードを評価し、最終的に統計集計を計算する処理が含まれます。PPLクエリはある程度複雑ですが、この実行全体を埋め込みスクリプトを含む単一の集計DSLにPush-downすることで最適化できます。
ベンチマーク

V3エンジンと元のV2エンジンのパフォーマンスを比較したベンチマーク結果を紹介します。
まず、Big5ワークロードでのベンチマークです。V2が141.814秒、V3が100.227秒で、約30%のパフォーマンス向上を示しています。しかし、この改善は期待よりも劇的ではありません。これは、Big5のクエリが通常単純で分かりやすいため、V2オプティマイザでも効果的に処理できることが理由です。

V3エンジンの真価をよりよく評価するために、より包括的で挑戦的なベンチマークであるClickBenchも実施しました。テスト結果は非常に印象的で、V2が1006.465秒かかったのに対し、V3は303.800秒で完了しました。V3エンジンはV2よりも3倍高速という結果です。この大幅な改善は、V3エンジンが複雑なクエリに対してCalciteの最適化とPush-downを効果的に活用できることを示しています。
その他V3エンジンの新機能

パフォーマンス最適化以外にも、V3エンジンには多くの新機能を構築しました。既存のコマンドと関数をV2からV3に移行した後、多くの新しいコマンドと関数を実装しています。

まず、Subsearchについては、In Subsearch、Exists Subsearch、Scalar Subsearch、Relational Subsearchの4種類のサブ検索をすべてサポートしています。これにより、柔軟なクエリの組み立てが可能になります。

Joinについては、INNER、LEFT、RIGHT、FULL、CROSS、SEMI、ANTIといった一般的な結合タイプをすべてサポートしています。さらに、overwrite機能や最大一致行数の指定(max=n)などの拡張機能も提供しています。

Lookupコマンドもサポートしており、ルックアップインデックスからのデータを追加または置換することで、検索データを強化できます。Eventstatsコマンドでは、計算された概要統計でイベントデータを強化することができます。


新しい関数も多数追加しました。JSON関連関数としてJSON、JSON_VALID、JSON_EXTRACTなど、コレクション関連関数としてARRAY、ARRAY_LENGTH、FOR_ALL、EXITSなどを実装しています。また、集約関数としてPERCENTILE、PERCENTILE_APPROX、DISTINCT_COUNT_APPROXを、ウィンドウ関数としてMAX/MIN、AVG/SUM/COUNT、VAR_POP/VAR_SAMP、STDDEV_POP/STDDEV_SAMPなどを追加しました。

これらの新機能の詳細については、OpenSearch PPL Reference Manualを参照してください。
github.com
また、セッション「OTel/Piped Processing Language: Simplifying Observability Queries With OpenTelemetry Query Semantics」でも、PPLの実践的な使用方法について紹介されています。
www.youtube.com
ロードマップ

今後のロードマップについて説明します。

まず、PPL向けのコマンドと関数をさらに充実させていきます。現在開発中のコマンドとして、BIN、TIMECHART、REX、APPENDは既にマージ済みで、STREAMSTATS、SPATHなどが進行中です。関数については、EARLIEST/LATEST、FIRST/LAST、PER_SECOND、MVJOINなどを開発しています。

さらなる最適化ルールとPush-down機能の拡充も計画しています。具体的には、単一のgroup-by式に対する集約Push-downの高速化、派生フィールドを含むProject演算子のPush-down、派生フィールドに対するSort演算子のPush-down、集約値に対するSortのDSLへのPush-down、バッチフレンドリーなLimit演算子とMerge Joinのサポート、集約Push-down後のProject演算子のScanへのPush-down、auto_date_histogram集約を使用したSpan()のPush-down、Range集約へのCaseコマンドのPush-downなどがあります。

実行メカニズムについても進化を続けます。現在はメモリ実行を使用していますが、より多様な実行方式を提供したいと考えています。エンジンの進化を振り返ると、V1とV2はVolcano方式のIteratorモデルを使用していました。V3ではCalciteを統合したCodegen方式を採用しています。今後は、さらに高速なVectorized実行バックエンドの統合を目指しています。具体的には、DataFusionやVeloxといったプラガブルな実行バックエンドの統合を検討しています。
また、オプションとしてスキーマレスサポートも提供したいと考えています。これらの機能強化により、PPLのさらなる性能向上と柔軟性の向上を実現していきます。