[翻訳] バケット集計の変革: 100 倍のパフォーマンス改善への道のり
OpenSearch は、特に時系列データを扱う際のデータ分析に広く使用されています。時系列分析のコア機能は、ドキュメントを月、週、日などの定義された間隔で日付またはタイムスタンプごとにグループ化する date histogram 集計です。このグループ化は、Web サイトへの 1 時間あたりの HTTP リクエスト数を表示するなど、トレンドやパターンを可視化するために重要です。
しかし、データ量が増加すると、これらの集計に必要な計算により、分析やダッシュボードの応答性が低下する可能性があります。従来、集計は関連するすべてのドキュメントを反復処理して正しい時間バケットに配置していましたが、この方法は大規模になると非効率になります。
約 1 年前、私たちは OpenSearch の date histogram 集計パフォーマンスを改善するという野心的な取り組みを開始しました。段階的な最適化として始まったものが、劇的な改善につながりました。場合によっては最大 100 倍高速なクエリレスポンスを実現しています。この記事では、これらの成果をどのように達成したか、そして最適化が時間とともにどのように進化したかをお伝えします。
なぜ date histogram を最適化するのか: ユースケースとメリット
パフォーマンスの最適化は一般的に価値がありますが、date histogram にとっては特に重要です。date histogram は、以下のようなさまざまなユースケースで広く使用されています。
- Web トラフィックパターンの分析
- アプリケーションメトリクスとログの監視
- 売上トレンドの可視化
- IoT センサーデータの追跡
date histogram パフォーマンスの最適化には、以下のメリットがあります。
- より高速な分析: 時系列可視化のダッシュボード読み込み時間の短縮
- スケーラビリティの向上: パフォーマンスを犠牲にすることなく、大量のデータを効率的に処理
これらの最適化の機会を十分に理解するには、基盤となるアーキテクチャを理解することが不可欠です。次のセクションでは、Lucene と OpenSearch のデータレイアウトとデフォルトの実行フローを探り、その後に続く重要な最適化の詳細を理解するために必要な基礎を提供します。
OpenSearch での数値データの保存方法
パフォーマンスの課題に効果的に対処するために、OpenSearch は日付フィールドの基盤となるインデックス構造を活用して集計パフォーマンスを大幅に高速化する最適化を導入しました。これらの最適化に入る前に、OpenSearch が Lucene を使用してタイムスタンプなどの数値データをどのように保存するかを見てみましょう。数値データは主に 2 つの構造に保存されます。
- ドキュメント値 (doc values): ソートや集計などの操作に最適化されたカラム型構造。従来の集計アルゴリズムはこれらの値を反復処理します。
- インデックスツリー (BKD ツリー): 高速な範囲フィルタリング用に設計された特殊なインデックス構造 (日付フィールドの場合は 1 次元 BKD ツリー)。インデックスツリーは内部ノードとリーフノードで構成されます。値はリーフノードにのみ保存され、内部ノードは子の境界範囲を保存します。この構造により、特定の範囲内のドキュメントを見つけるためにソートされた順序で効率的にトラバースできます。
OpenSearch での集計の仕組み
データストレージの理解を基に、OpenSearch が集計をどのように処理するかを見てみましょう。デフォルトでは、OpenSearch は最初に Lucene を使用して各シャードで各クエリ条件を評価し、一致するドキュメント ID を識別するイテレータを構築することで集計を処理します。これらのイテレータは結合され (例: 論理 AND)、すべてのクエリフィルターを満たすドキュメントを見つけます。結果として得られる一致するドキュメント ID のセットは集計フレームワークにストリーミングされ、各ドキュメントは 1 つ以上のアグリゲーターを通過します。これらのアグリゲーターは Lucene の doc values を使用して、計算に必要なフィールド値 (例: 平均やカウントの計算) を効率的に取得します。このストリーミングモデルは階層的です。ドキュメントはアグリゲーターのパイプラインを通過し、トップレベルとネストされたバケットに同時にグループ化できます。例えば、ドキュメントは最初に月ごとにバケット化され、その後その月内で HTTP ステータスコードごとにさらに集計できます。この設計により、OpenSearch は一致するドキュメントを 1 回のパスで複雑な多層集計を効率的に処理できます。
以下の図に、OpenSearch が一致するドキュメントの識別から結果の収集まで、集計をどのように処理するかを示します。

セットアップの理解
集計実行モデルを検証し、実際のシナリオでの最適化の影響を測定するために、opensearch-benchmark-workloads の nyc_taxis ワークロードを使用してパフォーマンステストを実施しました。具体的には、以下のクエリを使用して date histogram 集計パフォーマンスの分析に焦点を当てました。
{
"size": 0,
"query": {
"range": {
"dropoff_datetime": {
"gte": "2015-01-01 00:00:00",
"lt": "2016-01-01 00:00:00"
}
}
},
"aggs": {
"dropoffs_over_time": {
"date_histogram": {
"field": "dropoff_datetime",
"calendar_interval": "month"
}
}
}
}
このクエリは、1 年間の降車時間に対する range クエリを使用してドキュメントをフィルタリングし、一致するドキュメントに対して月次バケットで date histogram を計算します。クラスター内のノード数は、パフォーマンスのボトルネックがシャードレベルで発生するため、結果に大きな影響を与えません。そのため、単一ノードクラスターを使用してテストを実施しました。このクラスターには、レプリカなしの単一シャードに分散された nyc_taxis データセット全体が含まれており、コアの集計パフォーマンスに焦点を当てることができました。
パフォーマンスのボトルネック
テスト環境を整えた後、主要なパフォーマンスボトルネックの特定に注目しました。まず、単一の date histogram 集計クエリをループで実行し、実行中にフレームグラフを収集しました。これはこの issue で紹介されています。この分析により、2 つの主要な制限が明らかになりました。
- データ量への依存: クエリレイテンシはデータ量に直接比例していました。例えば、1 ヶ月の集計に 1 秒かかる場合、1 年分のデータでは 12 秒かかります。
- バケット数の影響: 多数のバケット (例: 分単位の集計) を使用すると、ハッシュ衝突が発生し、パフォーマンスがさらに低下しました。
これらの発見は、最適化が必要な領域に関する重要な洞察を提供し、改善への取り組みの舞台を整えました。
最適化の道のり
パフォーマンスボトルネックを明確に理解した上で、date histogram 集計パフォーマンスを向上させる取り組みを開始しました。以下のセクションでは、OpenSearch 2.12 での初期の機能強化から OpenSearch 3.0 でのより広範なサポートまで、最適化の取り組みがどのように進化したかを概説します。
初期の試み
特定されたボトルネックは集計実行フローの理解と一致していましたが、パフォーマンス問題の程度には驚かされました。結局のところ、1 年間の各月のドキュメント数を単純にカウントするだけで、12 個のカウント値を返すのに 7〜8 秒もかかるべきではありません! この不一致に興味をそそられ、以下のセクションで説明するいくつかの素朴な最適化の試みを開始しました。
データパーティショニング
最初の試みは、12 ヶ月のクエリを並行する 6 ヶ月の操作に分割することでした。2 つの操作からのレスポンスは簡単にマージできるためです。これによりクエリ時間は 8 秒から 4 秒に短縮されました (このコメントを参照) が、コミュニティからのフィードバックでは、これはゼロサムゲームであると指摘されました。実際には CPU 使用量を削減しておらず、タスクを並列で実行しているだけでした。並行セグメント検索はすでにこれらのメリットを提供していたため、このアプローチの価値は限定的でした。
データスライシング
最初の試みからの学びを基に、別の戦略であるデータスライシングを探求しました。カウントを取得するために集計クエリを使用する代わりに、track_total_hits を有効にした通常の range クエリを使用して単一月のドキュメントをカウントするようにアプローチを再構築しました。
{
"size": 0,
"track_total_hits": "true",
"query": {
"range": {
"dropoff_datetime": {
"gte": "2015-01-01 00:00:00",
"lt": "2015-02-01 00:00:00"
}
}
}
}
結果は劇的でした。クエリ時間は 1 ヶ月で約 150 ms に低下しました (このコメントを参照)。これは、順次の月次クエリでも 1 年全体で約 2 秒で完了することを意味し、並行性や全体的な CPU 使用量を増やすことなく、元の 8 秒から大幅に改善されました。
この顕著なパフォーマンスの違いにより、このクエリが同等の集計クエリよりもなぜこれほど高速に実行されるのかをより深く理解するようになりました。私たちの調査は、次の最適化アプローチを形作る重要な洞察をもたらしました。
フェーズ 1: フィルターリライト
range クエリの優れたパフォーマンスの分析から得た知見を基に、フィルターリライトアプローチを開発しました。フィルターリライトは、要求された date histogram の各バケットに対して一連の範囲フィルターを事前に作成することで機能します。例えば、1 年間の月次集計は以下のようにリライトできます。
{
"size": 0,
"aggs": {
"dropoffs_over_time ": {
"filters": {
"1420070400000": {
"range": {
"dropoff_datetime": {
"gte": "2015-01-01 00:00:00",
"lt": "2015-02-01 00:00:00"
}
}
},
"1422748800000": {
"range": {
"dropoff_datetime": {
"gte": "2015-02-01 00:00:00",
"lt": "2015-03-01 00:00:00"
}
}
},
...
}
}
}
}
この date histogram 集計クエリは、各バケットに対応するフィルターを生成し、BKD ツリーに基づく Lucene の Points Index を使用して集計を大幅に最適化します。このツリーベースの構造は、データを値の範囲と関連するドキュメント数を表すノードに整理し、効率的なトラバースを可能にします。無関係なサブツリーをスキップし、早期終了を使用することで、システムは不要なディスク読み取りを削減し、個々のドキュメントへのアクセスを回避します。各バケットのカウントは、ドキュメント値を反復処理するよりも高速な range クエリと同様に、インデックスツリーを使用して決定されます。このアプローチは、auto-date histogram、date histogram ソースでの composite 集計、そして後に重複しない範囲での numeric range 集計にも適用しました。以下のグラフに、OpenSearch 2.7 および 2.11 と比較した日次および時間次の date histogram 集計のパフォーマンス改善を示します。また、分単位の集計 (グラフの灰色のバー) は当初この最適化の恩恵を受けられなかったことに注意してください。これについては次のセクションで詳しく説明します。

以下の図に、インデックスツリーを使用してヒストグラムバケットごとにドキュメントがどのようにカウントされるかを示します。範囲 (例: 351〜771) に一致するドキュメントを効率的にカウントするために、トラバースはルートから開始し、ターゲット範囲がノードの範囲と交差するかどうかをチェックします。交差する場合、アルゴリズムは左右のサブツリーを再帰的に探索します。

重要な最適化として、サブツリー全体をスキップすることがあります。ノードの範囲がクエリ範囲の完全に外側にある場合 (例: 1〜200)、それは無視されます。逆に、ノードの範囲がクエリ範囲内に完全に含まれている場合 (例: 401〜600)、アルゴリズムは子をトラバースせずにそのノードからドキュメント数を直接返します。これにより、エンジンはすべてのリーフノードを訪問することを回避し、部分的な重複があるノードにのみ焦点を当てることができます。その結果、階層構造を使用してツリーの大部分の無関係な部分をスキップし、カウントを効率的に集計することで、操作が大幅に高速化されます。以下の図に最適化ワークフローを示します。

フェーズ 2: マルチレンジトラバーサルによるスケーラビリティへの対応 (OpenSearch 2.14)
初期のツリートラバーサルアプローチは効果的でしたが、多数の集計バケットを扱う際に制限がありました。アルゴリズムは各バケットに対して個別のツリートラバーサルを実行していたため、バケット数が増加するとパフォーマンスが低下し始めました。例えば、月次ログデータの集計 (12 バケット) では強力なパフォーマンス改善が見られましたが、長期間 (例: 1 年) にわたる分または時間単位の集計では数万のバケットが含まれる可能性があります。このような場合、各バケットに対してルートから深いツリーを繰り返しトラバースする累積コストにより、レイテンシとスケーラビリティの問題が増加しました (この issue を参照)。このアプローチは、バケット数が比較的少ない日次または時間次の集計では依然として有益で、OpenSearch 2.12 で最大 50 倍の速度向上をもたらしました。しかし、分単位の集計はボトルネックのままであり、よりスケーラブルなソリューションの必要性が生じました。これにより、マルチレンジトラバーサルと呼ばれる新しい方法の開発につながりました。この方法は、単一のツリーパスで複数のバケットを処理し、冗長な作業を削減し、高カーディナリティ集計のパフォーマンスを大幅に改善することを目的としています。
このアプローチは、従来の方法ではスケールが困難だった分単位の集計に特に効果的でした。その結果、日次および時間次の集計では最大 50 倍のパフォーマンス改善が見られ、分単位の集計では 100 倍以上の改善が見られ、クエリ時間が秒からミリ秒に短縮されました。以下のグラフに示します。

以下の図に、マルチレンジトラバーサルを使用してヒストグラムバケットごとにドキュメントがどのようにカウントされるかを示します。各バケットに対してトップからトラバースを再開する代わりに、マルチレンジトラバーサルは 2 ポインターアプローチを使用します。1 つのポインターはツリー内の現在位置を追跡し、もう 1 つはアクティブなバケットを追跡します。

ツリーはソートされた順序でトラバースされるため、アルゴリズムは現在の値がアクティブなバケットの範囲内にあるかどうかをチェックします。範囲内であれば、ドキュメントが収集されます (上の図を参照)。値がバケットの上限を超えている場合、ポインターは次のバケットに進みます (下の図を参照)。このバケット間のシームレスな遷移により、トラバースの再開が回避され、冗長な作業が削減されます。例えば、ノードの範囲がどのターゲットバケットとも重複しない場合 (例: 300〜400)、完全にスキップされます。

同様に、バケット内に完全に含まれるノード (例: 401〜600) は、以下の図に示すように、さらなる探索なしに直接カウントされます。この方法は、分単位の集計のような数千の細かいバケットを扱う場合に特に強力で、不要な操作を最小限に抑えることで処理時間を劇的に削減します。

フェーズ 3: サブ集計のサポート拡張 (OpenSearch 3.0)
当初、最適化はトップレベルの date histogram にのみ適用されていました。しかし、ユーザーは時間バケット内でサブ集計を頻繁に必要とします。例えば、平均メトリクスの計算や個別値のカウント (例: 1 時間あたりの平均ネットワーク帯域幅や HTTP ステータスコードのカウント) などです。OpenSearch 3.0 では、これらのサブ集計のサポートを追加しました。さらに、テスト中に特定されたリグレッションを防ぐための保護を開発中に実装しました。
パフォーマンス結果
最適化により、大幅なパフォーマンス改善が得られました。
- フィルターリライト (バージョン 2.12): ベースラインと比較して、特定の date histogram クエリで 10 倍から 50 倍の改善が観察されました。
-
マルチレンジトラバーサル (バージョン 2.14): リグレッションを解決し、フィルターリライト方式と比較して、
http_logsワークロードで最大 70%、nyc_taxisで 20〜40% のさらなる改善を達成しました。 -
サブ集計サポート (バージョン 3.0):
big5ワークロードの関連操作で 30〜40% の改善が観察されました。
制限事項
これらの改善は強力なパフォーマンス向上を提供しますが、適用されない場合やオーバーヘッドが発生する可能性がある場合を理解することが重要です。
- フィルターリライト最適化は主に
match_allクエリまたはインデックスツリー計算と互換性のある単純なrangeクエリに適用されます。任意のトップレベルクエリはサポートされていません。セグメントレベルのmatch_allは実装されていますが、複雑なクエリの相互作用によりフィルターリライトの適用性が制限される可能性があります。 - マルチレンジトラバーサルによりオーバーヘッドは大幅に削減されましたが、スパースなデータセットに対する非常に細かいヒストグラムでは、パフォーマンスのリグレッションが発生する可能性があります。
まとめ
OpenSearch の date histogram 集計に対するインデックスベースの最適化は、時系列分析と可視化のパフォーマンスを大幅に向上させます。これは対象となる集計に自動的に適用され、手動の作業を追加することなくワークフローを効率化します。OpenSearch が進化するにつれて、これらの改善により、スケールをあまり気にすることなく、データから効率的に洞察を得ることができます。
この道のりは、反復的な改善、システムの深い理解、コミュニティとのコラボレーションが、画期的なパフォーマンス向上につながることを示しています。当初はこれほど劇的な結果を期待していませんでしたが、継続的な最適化への取り組みが想像もしなかった形で報われました。
今後の予定
これらの最適化は Lucene のアップストリームに貢献されており (この pull request を参照)、Elasticsearch や Solr などの他の検索システムもこれらの改善の恩恵を受けることができます。今後も、特に以下の領域でさらなるパフォーマンス向上を探求・実装し続けています。
- ネストされた集計
- マルチフィールドクエリ
- 削除されたドキュメントのより効率的な処理
これらのトピックについてさらにコラボレーションや議論に興味がある場合は、GitHub や Slack でお気軽にお問い合わせください。ぜひつながりましょう!
OpenSearch Project(OSS) の Publicationです。 OpenSearch Tokyo User Group : meetup.com/opensearch-project-tokyo/
Discussion