OpenSearchCon North America 2025のセッション「Derived Source: Slash Storage Costs Without Losing Data in OpenSearch」をまとめます。

スピーカー
このセッションは、Amazon Web ServicesでSenior Software Development Engineerを務めるMohit Godwani氏と、Software Development EngineerのTanik Pansuriya氏によって行われました。
Godwani氏は2021年からOpenSearchプロジェクトのコントリビュータとして活動しており、主にインデックス作成の分野を専門としています。分散システム、システムパフォーマンス、データベースに興味を持ち、インデックス作成領域における様々な機能開発とパフォーマンス改善に貢献してきました。
Pansuriya氏は2年以上にわたってAmazon OpenSearch Serviceで勤務しており、マネージドOpenSearchサービスの監視とリカバリを担当するチームの一員として活動しています。最近ではインデックス作成の分野にも貢献を広げています。
OpenSearchのデータ構造
基本構造

Derived Sourceとは何か、そしてそれがどのようにストレージコストを削減するのかを理解する前に、まずOpenSearchがインデックスのデータをどのように保存しているかを見ていく必要があります。
OpenSearchインデックスにデータを投入すると、舞台裏では複数の基礎となるデータ構造が作成されます。まず、ドキュメント内にどのような用語が含まれているかを理解するためのタームベクトルがあります。次に、数値に対するポイントクエリを実行するための数値インデックスがあります。これはLucene内で作成されるBKDインデックスであり、例えばレイテンシが5秒より大きいすべてのドキュメントを取得したい場合など、ドキュメントをフィルタリングするための主要なソースとなります。
さらに、転置インデックスも作成されます。これはテキストインデックスで、分析されたすべての用語を保持し、各用語がどのドキュメントに属しているか、その位置、頻度などの情報を提供します。そして列形式のデータ、つまりDoc Valuesもあります。Doc Valuesは基本的にソートと集計の目的で使用され、ドキュメント全体のフィールドを列形式で保存します。
加えて、すべてのKNN関連フィールドを処理するベクトルデータも存在します。最後に、Luceneでセグメントがどのように構成されているか、どのようなフィールドが存在するか、フィールド名、削除されたドキュメント、コミットポイントなどの情報を含むメタデータとコミット関連ファイルがあります。
OpenSearchは舞台裏でこれらのデータ構造を大量に作成しており、それらすべてに独自のストレージコスト、インデックス作成コスト、そしてクエリコストが発生します。
Stored Fields

次に、Stored Fieldsが何であるかについて少し深く掘り下げていきます。Stored FieldsはOpenSearchがデータの行指向表現を持つために利用する、Luceneによって提供される形式です。
ドキュメントの特定のフィールドを実際に取得したい場合、すべてのフィールドが単一の場所に保存されている方が効率的です。そうすれば、取得を実行したいときに単一のルックアップで迅速に実行できます。構造上、Stored Fieldsはクエリのfetchフェーズ、つまりドキュメントの実際のソースなどを取得するフェーズに役立ちます。
Stored Fieldsの圧縮

OpenSearchとその基盤となるLuceneは、すべてのStored Fieldsを圧縮して保存します。
Luceneはデフォルトで2つの圧縮モードを提供しています。1つは高速モード(LZ4)で、もう1つは高圧縮モード(DEFLATE)です。高速モードはインデックス作成スループットを優先していますが、圧縮率はそれほど高くありません。一方、高圧縮モードは優れた圧縮率を実現しますが、インデックス作成スループットは低下します。
この2つの中間として、OpenSearchはLuceneのStored Fields形式の上にZSTDという圧縮アルゴリズムを導入しました。ZSTDは高いインデックス作成スループットと優れた圧縮率の両方を実現するスイートスポットとなっており、複数のレベルで調整も可能です。
OpenSearchでのStored Fieldsの使用
次に、OpenSearchがこれらのStored Fieldsをどのように使用するかを見ていきます。
インデックス作成時にOpenSearchに渡すドキュメント全体は、Stored Fields形式で保存されます。自動生成されたIDや提供されたID、ルーティングパラメータなどのフィールドはすべて、特定のドキュメントの_sourceとともにStored Fieldsに格納されます。
また、不正な形式のフィールドを無視する設定をしている場合、そのメタ情報もStored Fieldsに格納され、取得時にその情報を活用できます。これとは別に、マッピングで特定のフィールドに対してstoreをtrueに設定することもできます。こうすることで、個別にフィールドを保存したい場合にも対応可能です。
このセッションの目的において、最も重要なStored Fieldsは_sourceです。
_source field

では、_source Fieldとは何かを見ていきます。_source Fieldは、インデックス作成時にDoc APIまたはBulk APIを通じて提供した入力バイト全体を保存します。ドキュメントがJSON、YAML、CBOR形式のいずれであっても、OpenSearchは基本的に形式と提供されたバイトをそのまま保存します。
_source fieldは無効にすることができます。無効にするといくつかの欠点が生じますが、ストレージコストの削減につながります。また、_sourceフィールドでは、マッピングで指定できるincludeフィルターとexcludeフィルターを通じて、保存するものと除外するものを選択できます。
_source Fieldが必要な理由

では、なぜ_source Fieldが必要なのかを見ていきます。
まず、すでに説明したように、データを行指向で保存するため、クエリのヒット取得に非常に効率的です。
次に、データをあるインデックスから別のインデックスに転送したいユースケースがあります。再インデックス作成では、ソースインデックスでデータを検索し、ドキュメントの_sourceを取得して、それを新しいインデックスに投入します。これはOpenSearchの2つのバージョンをまたいでインデックスを実際にアップグレードする必要がある場合に役立ちます。この目的のためには、_sourceがなければ再インデックス作成を行うことができません。
また、部分的な更新とスクリプトによる更新のユースケースがあります。ドキュメントに部分的な更新を適用したり、既存のデータを使用して更新するスクリプトを実行したい場合、OpenSearchはドキュメントの既存バージョンの_sourceを取得し、その上にスクリプトまたはパッチを適用して、新しいバージョンのドキュメントを作成します。この場合も_sourceが必要です。
最後のユースケースは、OpenSearchが内部的に使用するLuceneベースのリカバリです。例えば、既存のインデックスに新しいレプリカを追加する場合など、ピアリカバリに依存している場合、プライマリからレプリカに操作がリプレイされ、ブートストラップの一部としてプライマリと同期が保たれます。このときLuceneベースのリカバリを使用しますが、これもLuceneに保存されている_source Fieldに依存します。
_source Fieldのフットプリント

OpenSearchのいくつかの重要なユースケースに_sourceが必要であることを理解したところで、それがどのようなフットプリントをもたらすかを見ていきます。
まず、インデックス作成中のCPU使用率についてです。先ほど説明したように、インデックス作成時にはStored Fieldsも圧縮されます。ドキュメントがディスクにフラッシュされ、Stored Fields形式が永続化されるインデックス作成アクティビティ中に、圧縮が実行されドキュメントが圧縮されます。この処理によりCPUが消費され、場合によってはCPU使用率が20%に達することもあります。

ストレージコストについても見ていきます。例えば、分析したログデータの円グラフでは、Stored Fieldsがインデックスの合計サイズのほぼ60%を占めています。合計約2900MBのうち2076MBです。これはStored Fields、特に_sourceフィールドがストレージに非常に大きな影響を与えていることを示しています。
Derived Source
Derived Sourceのアイデア

OpenSearchでは多くのデータ構造を作成しています。転置インデックス、タームベクトル、Doc Values、KNNベクトル、そしてStored Fieldsです。その上でOpenSearchは_sourceも保存しています。では、分析クエリのためにすでに作成している既存のデータ構造を使用して、クエリ中に_sourceを再生成する方法があったらどうでしょうか。
Doc Valuesには多くのフィールドの値が含まれており、それを使用して特定のフィールドの値を取得できます。すべてのフィールドのDoc Valuesを何らかの方法でフェッチし、JSON、YAMLなどの必要な形式で結合して返せば、Stored Fields、つまり_sourceが不要になる可能性があります。
Doc Valuesを活用した_sourceの再生成

例えば、元のデータに整数型のフィールドAがあり、そこに1、2、1、3などの複数の値を追加しているとします。同様に、日付フィールドB、キーワードCがるとします。これらはすべて列形式のDoc Valuesに変換されます。

これらの列形式のDoc Valuesから、各フィールドの列構造を順に処理しながら、A、B、Cの値を取り出して_sourceを再構築できます。つまり、既存のDoc Valuesを活用すれば、元のドキュメント構造を復元できるということです。
つまり、_source Field全体を削除できる余地が生まれるわけです。これがDerived Sourceのアイデアです。
Derived Sourceのストレージへの影響
この変更だけで、Luceneに存在していた_sourceまたはStored Fields形式のサイズが2076MBからわずか330MBに減少しました。これはストレージコストで60%の改善に相当します。
Derived Sourceのインデックス作成とマージへの影響

ストレージの改善だけではありません。インデックス作成とマージのパフォーマンスも向上しました。

従来はインデックス作成中には圧縮処理が発生していましたが、Derived Sourceではそれがなくなります。インデックス作成のすべての作業が、分析クエリに必要な実際のデータ構造を作成するためだけに使われるようになるのです。
マージについても改善が見られます。セグメントサイズが小さくなるため、Stored Fieldsを使用せず、Stored Fieldsに多くのストレージを消費することもなくなり、マージの回数が大幅に減少します。これにより、コンピューティングで発生するバックグラウンドアクティビティも改善され、クエリなどの他のアクティビティのためのリソースが増えることになります。
Derived Sourceの詳細

Derived Sourceの有効化は簡単で、indexの設定で有効にするだけです。
Derived Sourceをサポートするデータ型

現時点で、使用されているほとんどのフィールドタイプをサポートしています。
まず、boolean型、すべての数値型フィールド(byte、double、float、half_float、integer、long、unsigned_long、short)、date型、date_nanos型、そしてgeo_point型があります。分析ユースケースでは、Doc ValuesがStored Fieldsと比較してデータを読み取りやすいため、Doc Valuesを優先するようにしました。したがって、これらすべてのフィールドでは、Doc Valuesが分析ユースケースのためにデフォルトで保存されます。
日付フィールドについては、すべての形式をサポートしており、ユーザーが提供した形式を最終的に再生成される_sourceに保持しようとします。地理フィールドタイプについては、現在geo_pointのみをサポートしており、geo_shapeはまだサポートしていません。geo_pointには、geohash、point形式など、多くの投入形式がありますが、緯度経度形式に変換され、再構築された_sourceが提供されます。

次に、ip型、keyword型、scaled_float型、text型、wildcard型です。text型については、テキストフィールドのソートと集計をサポートしていないため、Stored FieldsやDoc Valuesを保存してもあまり意味がありません。これはタームベースであるため、Doc ValuesではなくStored Fieldsから_sourceを派生して再生成します。
wildcard型は従来のkeyword型やtext型とは異なります。まず部分文字列に分解し、その上にターム辞書を作成するため、そこから元の値を派生させるのは困難です。そのため、最終的に再生成するには、Stored FieldsまたはDoc Valuesを明示的に有効にする必要があります。
Derived Sourceの拡張性

サポートされているフィールドタイプについて見てきましたが、次は他のフィールドタイプやプラグインにも拡張する方法を見ていきます。
例えば、KNNプラグインがあり、_sourceフィールドに保存する代わりに、そのフィールドを_sourceの再生成でサポートしたいとします。この場合、フィールドマッパーレベルで提供されているメソッドをオーバーライドするだけで実現できます。OpenSearchは既に、ソートされた数値、ソートされたセット、またはStored Fieldsから値を再生成するなどのユーティリティをサポートしています。
特定のフィールドタイプでどのようにサポートされているか、そして例えば日付フィールドが長いタイムスタンプ形式で保存されている場合など、最終的な目的の形式にどのように変換し直すかを定義するだけです。それをISO形式またはユーザーが提供した形式に変換し直す実装が必要になります。それ以外では、フィールドタイプへの拡張は非常に簡単です。
Derived Sourceの実装インターフェース

インターフェースがどのように定義されているか、そしてそれらのメソッドがフィールドマッパーレベルでどのように定義されているかを見ていきます。
主に2つのメソッドがあります。canDeriveSourceとderiveSourceです。canDeriveSource関数内で、どのフィールドマッパータイプまたはマッパータイプがサポートされているかを定義します。deriveSourceジェネレーターでは、Doc ValuesやStored Fieldsなどの複数のデータ構造を使用して、元の形式に再生成する方法を定義しています。
したがって、これらのメソッドを拡張することで、現在サポートされていない他のフィールドタイプにも拡張できます。
Index作成と更新の流れ

インデックス作成のフローを見ていきます。インデックスマッピングを作成または更新しようとしている場合、まずドキュメントマッピングが解析されます。各マッピングフィールドについて、Derived Sourceがサポートされているかどうかの実現可能性を確認するために、canDeriveSourceメソッドが呼び出されます。
このメソッドは構成された各フィールドマッパーに対して繰り返し呼び出され、これらのフィールドマッパーは、それぞれのフィールドタイプがサポートされておりインデックスを作成できる場合にユーザーに応答を返します。サポートされていない場合は、特定のフィールドについて例外がスローされます。例えば、wildcard型の場合、StoreまたはDoc Valuesがtrueに構成されていない場合、例外がスローされます。
検索時のフロー

検索パスでドキュメントを再生成する方法を見ていきます。fetchフェーズ中に、どのドキュメントをクエリに対して返す必要があるかが判明します。
各ドキュメントについて、そのインデックス用に定義されたサブフィールドマッパーをそれぞれ呼び出します。各サブフィールドについて、提供されたDoc ValuesまたはStored Fieldsを使用して、それぞれのデータ構造から_sourceを再生成します。そして最終的な結果ドキュメントを、ユーザーが提供したコンテンツタイプ(JSONやYAMLなど)で返します。
これで、ほとんどのフィールドタイプでどのようにサポートされているかがわかりました。次に、検索パフォーマンスやリカバリ面での課題を見ていきます。
制約と考慮事項
検索パフォーマンスへの影響

検索パフォーマンスについて見ていきます。_sourceを再生成する際には、すべてのフィールドのDoc Valuesを順に処理し、最終結果を結合する必要があります。そのため、異なるディスクロケーションにアクセスする必要があり、_sourceフィールドが単一の場所にある場合と比較して、より多くのI/Oを使用する可能性があります。
ただし、ほとんどの場合、デフォルトでトップ10を返すため、パフォーマンスの問題は見られません。むしろ一部のケースではパフォーマンスの改善が見られました。これは、小規模なシャードまたは小規模なセグメントから集計されたDoc Valuesを読み取るためです。一方で、10,000件のドキュメントをフェッチしたり、スクロールクエリを実行したりすると、待ち時間が増加する可能性があります。
Doc Valuesは解凍を必要とせず、直接mmapされ、メモリから直接フェッチされることがほとんどです。従来の_sourceの場合には、まずディスクからフェッチし、次に解凍されるため、CPUサイクルの消費もありました。結果として、ほとんどの検索ワークロードではパフォーマンスの低下は見られませんでした。
リカバリパフォーマンスへの影響

リカバリ面でのパフォーマンスについて見ていきます。
リカバリには2つのタイプがあります。ファイルベースリカバリとオペレーションベースリカバリです。ファイルベースリカバリは、セグメントの数がはるかに少なく、それらのセグメントのサイズも通常のフローと比較して非常に小さいため、はるかに高速になります。これは、かなりの量のスペースを占めていた_sourceフィールドを削除したためです。
オペレーションベースリカバリには、Luceneベースリカバリとトランザクションログベースリカバリの2つのタイプがあります。Luceneベースリカバリでは、異なる場所から_sourceを再構築する必要があるため、パフォーマンスが低下する可能性があります。多くのドキュメントでは時間がかかる場合があります。
トランザクションログベースリカバリへの影響

トランザクションログベースリカバリでも同様の課題が見られる可能性がありますが、それを回避する方法があります。
トランザクションログベースリカバリでは、元の_sourceが存在します。セグメントから_sourceを派生させるのと同様の体験を実現したい場合は、まずトランザクションログから一時的な_sourceをインメモリバッファに投入し、その上で_sourceを派生させる必要があります。そのため、表示面での最終出力は、セグメントから読み取るときとほぼ同じように見えます。
トランザクションログから_sourceをフェッチしたり、トランザクションログベースのリカバリを実行したりする際の遅延を短縮するために、設定で無効にすることができます。ただし、この場合、トランザクションログからは元の_sourceが取得され、セグメントから読み取るときにはDerived Sourceが使用されるという違いが生じます。
keywordフィールドの取り扱い

様々なフィールドタイプをサポートする上での課題を見ていきます。
keywordフィールドはデフォルトでDoc Valuesが有効になっています。keywordには、ignore_aboveやnormalizerといったマッピングパラメータがあります。まず、ignore_aboveについて見てみましょう。多くの場合、256文字を上限として設定されています。フィールド値がこの長さを超えると、そのフィールド値は転置インデックスには保持されますが、Doc Valuesには保存されません。その結果、検索でターム集計を行う際に、その値は集計結果に現れなくなります。
normalizerの場合も問題があります。例えば小文字化normalizerを使用すると、すべての文字が小文字に変換されてからLuceneのDoc Valuesに保存されます。このため、インデックス作成時に投入した元の値の情報が失われてしまいます。
現在、Derived Sourceはこのようなマッピングパラメータがあるkeywordフィールドをサポートしていません。ただし、このような場合でも対応策があります。マッピングパラメータを使用したい特定のフィールドに対して、Stored Fieldsを明示的に有効にすることで、そのフィールドに対してもDerived Sourceを利用できるようになります。
textフィールドの取り扱い

最も一般的に使用されるtextフィールドについて見ていきます。
textフィールドではDoc Valuesがデフォルトで無効になっています。これは、textフィールドに対してDoc Valuesを持つことに意味がないためです。textフィールドは集計やソートをサポートしておらず、タームベースの処理が行われます。textフィールドの場合、転置インデックスを投稿(postings)と頻度(frequency)の形式でのみ保存しており、これを使用して_sourceを再生成するのは非常に困難でコストもかかります。
そのため、Derived SourceではtextフィールドのStored Fieldsを暗黙的に有効にしています。これにより、常にStored Fieldsから_sourceを再構築できるようになっています。
マッピングパラメータの制限事項

現在存在するマッピングパラメータの制限について見ていきます。
まず、copy_toがあります。これは複数のフィールドの値を1つのフィールドにコピーする機能ですが、Doc Valuesでは実際のソースコンテキストが失われてしまうため、再構築が困難になります。
同様に、null_valueにも課題があります。これはすべてのnull値を、null_valueで定義した値に置き換える機能です。例えば、null_value=xと設定すると、Doc Valuesではnull値がすべてxに置き換えられてしまいます。そのため、インデックス作成時にどのドキュメントの特定のフィールドが元々nullであったかという情報が失われてしまいます。
nested型の場合も同様の複雑さがあり、現在Derived Sourceをサポートしていません。
また、先ほどの例で見たように、Doc Valuesにはsorted numericとsorted setという2つの形式があります。sorted numericでは、複数の値を持つフィールドがある場合、値の順序をソートしてDoc Valuesに保存します。同様に、sorted setでは重複を削除し、ソートしてからDoc Valuesに保存します。したがって、複数の値を持つフィールドを使用する場合、最終的に再構築された_sourceで値の順序や重複に関する変化が見られる可能性があります。
今後の展望

Derived SourceはOpenSearch 3.2でリリースされました。これは機能の最初のリリースであり、ログ分析と可観測性に関するユースケースで最も一般的と考えられるフィールドタイプの大部分を実装しました。
今後は、より多くのフィールドのサポート、さらなるパフォーマンスの向上、そしてこれまで説明してきた制限の削減に積極的に取り組んでいます。近い将来の課題としては、rangeやgeo_shapeのようなフィールドのサポートが挙げられます。また、Doc Valuesがディスクにどのように保存されるか、そしてそれらのアクセスパターンを活用して_sourceを生成することで、ドキュメントの取得全体をどのように最適化できるかについても検討しています。アクセスパターンに基づいて、ドキュメントソースの取得時間をさらに短縮するのに役立つ、より多くの最適化に取り組んでいます。
リカバリ速度についても、オペレーションベースのリカバリは遅くなる可能性があります。そのため、バッチ処理や、現在見られるリカバリ速度を向上させるのに役立つ、より高速なドキュメント取得について、さらに検討を進めています。これらすべてにより、この機能のより高度な実装が実現され、現在の状態からさらに改善されることになります。
今後のロードマップ

Derived Sourceの他の今後の開発計画について見ていきます。
まず、rangeやgeo_shapeといった追加のフィールドタイプのサポートが予定されています。また、copy_to、ignore_above、normalizerといったマッピングパラメータのサポート、そしてより多くのマッピングタイプへの対応も進められています。
パフォーマンス面では、列形式構造における空間的局所性を活用することで、大量のドキュメントを一度に取得する際のドキュメント取得の最適化に取り組んでいます。アクセスパターンに基づいて、ドキュメントソースの取得時間をさらに短縮する最適化が進行中です。
さらに、リカバリ速度の改善も重要な課題です。バッチ処理の導入や、より高速なドキュメント取得メカニズムの実装により、現在のリカバリ速度を向上させる検討が進められています。これらすべての改善により、Derived Source機能はより高度で実用的なものになり、現在の状態からさらに大きく進化することが期待されます。

この機能の提供とOpenSearchでの公開に協力してくれたコミュニティ全体と関係者に感謝します。何人かの関係者を挙げましたが、コミュニティや皆さんからの絶え間ないフィードバックがあったおかげで、これを実現できました。
QA
質問1:Derived Sourceの検索パフォーマンスについて
回答:LZ4圧縮と比較した検索パフォーマンスへの影響について説明します。ヒット数が比較的少ないほとんどのユースケースでは、検索レイテンシに悪化は見られませんでした。しかし、検索クエリで返されるドキュメント数が多い場合、検索レイテンシで10%から100%の悪化が見られました。ただし、これはLZ4との比較です。最高圧縮率(DEFLATE)と比較すると、解凍自体に多くの時間がかかるため、Derived Sourceでの_source取得とほぼ同じ範囲でした。検索パフォーマンス、インデックス作成パフォーマンスなどについて詳しく説明するブログを近日中に公開予定です。
質問2:どのような検索ユースケースでDerived Sourceを使用すべきか
回答:検索パフォーマンスへの影響を考慮したDerived Source機能の推奨事項についてお答えします。_sourceの再生成が行われるため、特定のケースではパフォーマンスが劣化する可能性があります。検索レイテンシに非常に敏感な場合は、高速アルゴリズム(LZ4)で保存された従来の_sourceに依存する方が良いでしょう。また、カタログのように継続的に更新される検索ユースケースで、大量のスクリプトによる更新や部分的な更新を行う場合も、パフォーマンスに影響がある可能性があります。一方で、ログ分析や可観測性に関するユースケースでは、ほぼ同じ検索パフォーマンスでストレージを大幅に節約できます。
質問3:列指向ストレージの最適化について
回答:Doc Valuesのストレージ構造と取得の最適化についてご質問いただきました。Luceneでは単一のファイルですが、データは列形式でシーケンシャルに保存されます。そのため、ドキュメントについて複数の異なるフィールドを取得したい場合、ディスク上の異なるブロックにアクセスする必要があります。この点については、すでにパフォーマンスの改善を行っています。ドキュメントごとに各フィールドを取得するのではなく、フィールドごとにアクセスし、ドキュメントにアクセス済みとマークし続けることで、効率化を図っています。理想的には、列ファミリーのような概念で、論理的に一緒にアクセスされる列をディスク上でまとめて配置できればさらに良いのですが、現在のLuceneの構造では完全に列指向であり、そのような制御はできません。
質問4:メモリとディスクのアクセス比率について
回答:ベンチマークとパフォーマンスの設定について、特にメモリとディスクのアクセス比率についてのご質問です。まず、従来の_sourceで使用するStored FieldsはOpenSearchがmmapしない形式で、仮想メモリを使用しません。一方、Doc Valuesはmmapを使用します。Derived Sourceへの変更により、仮想メモリにすでに存在するものを基本的にフェッチできるようになります。ベンチマークに関しては、ページスラッシングなどについて正確な数値は持っていませんが、同じコンピューティングリソースが両方の構造で利用可能な場合にどのように動作するかというレベルで分析を行っています。
質問5:KNNなどのベクトル検索での使用可能性について
回答:はい、使用可能です。フィールドマッパーで対応する拡張を行う必要があります。ベクトル検索機能を提供するプラグインで、適切な実装を行えば使用可能になります。KNNプラグインに関しては、すでにDerived Sourceに対応する機能が存在し、OpenSearch 3.0以降で利用可能です