近似kNN検索の調整

Elasticsearchは、クエリベクトルに最も近いk個のベクトルを効率的に見つけるための近似k最近傍検索をサポートしています。近似kNN検索は他のクエリとは異なる動作をするため、そのパフォーマンスに関して特別な考慮が必要です。

これらの推奨事項の多くは、検索速度の向上に役立ちます。近似kNNでは、インデックス作成アルゴリズムが内部で検索を実行してベクトルインデックス構造を作成します。したがって、これらの推奨事項はインデックス作成速度にも役立ちます。

ベクトルのメモリフットプリントを削減

デフォルトのelement_typefloatです。しかし、これはインデックス作成時にquantizationを通じて自動的に量子化される可能性があります。量子化は必要なメモリを4倍削減しますが、ベクトルの精度も低下し、フィールドのディスク使用量が最大25%増加します。ディスク使用量の増加は、Elasticsearchが量子化されたベクトルと非量子化のベクトルの両方を保存するために発生します。たとえば、40GBの浮動小数点ベクトルを量子化すると、量子化されたベクトルのために追加で10GBのデータが保存されます。合計ディスク使用量は50GBになりますが、高速検索のためのメモリ使用量は10GBに削減されます。

floatの次元がdim以上のベクトルについては、quantizedインデックスの使用が強く推奨されます。

ベクトルの次元を削減

kNN検索の速度はベクトルの次元数に対して線形にスケールします。なぜなら、各類似度計算は2つのベクトルの各要素を考慮するからです。可能な限り、次元の低いベクトルを使用する方が良いです。一部の埋め込みモデルは異なる「サイズ」で提供され、低次元および高次元のオプションがあります。また、PCAのような次元削減技術を試すこともできます。異なるアプローチを試す際には、検索の質が依然として受け入れ可能であることを確認するために、関連性への影響を測定することが重要です。

ソースからベクトルフィールドを除外

Elasticsearchは、インデックス作成時に渡された元のJSONドキュメントを_sourceフィールドに保存します。デフォルトでは、検索結果の各ヒットには完全なドキュメント_sourceが含まれています。ドキュメントに高次元のdense_vectorフィールドが含まれている場合、_sourceはかなり大きく、読み込みに高コストになる可能性があります。これにより、kNN検索の速度が大幅に低下する可能性があります。

reindexupdate、およびupdate by query操作は一般的に_sourceフィールドを必要とします。フィールドの_sourceを無効にすると、これらの操作に対して期待される動作が得られる可能性があります。たとえば、reindexは新しいインデックスにdense_vectorフィールドを実際には含まないかもしれません。

excludesマッピングパラメータを通じて、_source内のdense_vectorフィールドの保存を無効にできます。これにより、検索中に大きなベクトルを読み込んで返すことを防ぎ、インデックスサイズも削減されます。_sourceから省略されたベクトルは、kNN検索で使用できます。なぜなら、検索を実行するために別のデータ構造に依存しているからです。excludesパラメータを使用する前に、_sourceからフィールドを省略することの欠点を確認してください。

別のオプションは、すべてのインデックスフィールドがサポートしている場合に合成_sourceを使用することです。

データノードに十分なメモリを確保

Elasticsearchは、近似kNN検索のためにHNSWアルゴリズムを使用します。HNSWは、ほとんどのベクトルデータがメモリに保持されているときにのみ効率的に機能するグラフベースのアルゴリズムです。データノードには、ベクトルデータとインデックス構造を保持するのに十分なRAMがあることを確認する必要があります。ベクトルデータのサイズを確認するには、インデックスディスク使用量を分析APIを使用できます。おおよその目安として、デフォルトのHNSWオプションを前提とすると、使用されるバイト数はnum_vectors * 4 * (num_dimensions + 12)です。byteelement_typeを使用する場合、必要なスペースはnum_vectors * (num_dimensions + 12)に近くなります。必要なRAMはファイルシステムキャッシュ用であり、Javaヒープとは別です。

データノードは、RAMが必要な他の方法のためにバッファを残す必要があります。たとえば、インデックスにはテキストフィールドや数値も含まれている可能性があり、これらもファイルシステムキャッシュを使用することで恩恵を受けます。特定のデータセットでベンチマークを実行して、良好な検索パフォーマンスを提供するのに十分なメモリがあることを確認することをお勧めします。夜間ベンチマークに使用するデータセットと構成の例は、こちらこちらで見つけることができます。

ファイルシステムキャッシュをウォームアップ

Elasticsearchを実行しているマシンが再起動されると、ファイルシステムキャッシュは空になります。そのため、オペレーティングシステムがインデックスのホット領域をメモリにロードするまでに時間がかかり、検索操作が遅くなります。ファイル拡張子に応じて、どのファイルをメモリに早期にロードするかをオペレーティングシステムに明示的に指示するには、index.store.preload設定を使用できます。

あまりにも多くのインデックスやファイルでデータをファイルシステムキャッシュに早期にロードすると、ファイルシステムキャッシュがすべてのデータを保持するのに十分でない場合、検索が遅くなります。注意して使用してください。

近似kNN検索に使用されるファイル拡張子は次のとおりです:

  • vecおよびveqはベクトル値用
  • vexはHNSWグラフ用
  • vemvemf、およびvemqはメタデータ用

インデックスセグメントの数を削減

Elasticsearchのシャードは、インデックス内の内部ストレージ要素であるセグメントで構成されています。近似kNN検索のために、Elasticsearchは各セグメントのベクトル値を別々のHNSWグラフとして保存するため、kNN検索は各セグメントをチェックする必要があります。最近のkNN検索の並列化により、複数のセグメントを横断する検索が大幅に高速化されましたが、セグメントが少ない場合、kNN検索は数倍速くなる可能性があります。デフォルトでは、Elasticsearchは定期的に小さなセグメントをバックグラウンドのマージプロセスを通じて大きなセグメントにマージします。これが不十分な場合は、インデックスセグメントの数を減らすために明示的な手順を取ることができます。

最大セグメントサイズを増加

Elasticsearchは、マージプロセスを制御するための多くの調整可能な設定を提供します。重要な設定の1つはindex.merge.policy.max_merged_segmentです。これは、マージプロセス中に作成されるセグメントの最大サイズを制御します。値を増加させることで、インデックス内のセグメントの数を減らすことができます。デフォルト値は5GBですが、これは大きな次元のベクトルには小さすぎる可能性があります。この値を10GBまたは20GBに増加させることを検討すると、セグメントの数を減らすのに役立ちます。

バルクインデックス作成中に大きなセグメントを作成

一般的なパターンは、最初に初期のバルクアップロードを実行し、その後検索可能なインデックスを作成することです。強制的にマージする代わりに、Elasticsearchがより大きな初期セグメントを作成するようにインデックス設定を調整できます:

  • バルクアップロード中に検索が行われないことを確認し、index.refresh_interval-1に設定して無効にします。これにより、リフレッシュ操作が防止され、余分なセグメントの作成を回避します。
  • Elasticsearchに大きなインデックスバッファを与え、フラッシュする前により多くのドキュメントを受け入れられるようにします。デフォルトでは、indices.memory.index_buffer_sizeはヒープサイズの10%に設定されています。32GBのようなかなりのヒープサイズでは、これで十分なことがよくあります。完全なインデックスバッファを使用できるようにするには、index.translog.flush_threshold_sizeの制限も増加させる必要があります。

検索中の重いインデックス作成を避ける

ドキュメントを積極的にインデックス作成すると、近似kNN検索のパフォーマンスに悪影響を及ぼす可能性があります。なぜなら、インデックス作成スレッドが検索から計算リソースを奪うからです。インデックス作成と検索を同時に行うと、Elasticsearchは頻繁にリフレッシュを行い、いくつかの小さなセグメントが作成されます。これにより、セグメントが多いと近似kNN検索が遅くなるため、検索パフォーマンスが低下します。

可能であれば、近似kNN検索中に重いインデックス作成を避けるのが最善です。すべてのデータを再インデックス作成する必要がある場合、たとえばベクトル埋め込みモデルが変更された場合は、インデックスをその場で更新するのではなく、新しいドキュメントを別のインデックスに再インデックス作成する方が良いです。これにより、上記の遅延を回避し、頻繁なドキュメント更新による高コストのマージ操作を防ぐことができます。

Linuxで控えめなリードアヘッド値を使用してページキャッシュのスラッシングを避ける

検索は多くのランダムな読み取りI/Oを引き起こす可能性があります。基盤となるブロックデバイスのリードアヘッド値が高いと、特にファイルがメモリマッピングを使用してアクセスされる場合に、多くの不必要な読み取りI/Oが行われる可能性があります(ストレージタイプを参照)。

ほとんどのLinuxディストリビューションは、単一のプレーンデバイスに対して128KiBの妥当なリードアヘッド値を使用しますが、ソフトウェアRAID、LVM、またはdm-cryptを使用する場合、結果として得られるブロックデバイス(Elasticsearchのパス.dataをバックアップする)は非常に大きなリードアヘッド値(数MiBの範囲)を持つことがあります。これにより、ページ(ファイルシステム)キャッシュのスラッシングが発生し、検索(または更新)パフォーマンスに悪影響を及ぼすことがよくあります。

現在の値はKiBlsblk -o NAME,RA,MOUNTPOINT,TYPE,SIZEを使用して確認できます。この値を変更する方法については、ディストリビューションのドキュメントを参照してください(たとえば、再起動を跨いで持続するためのudevルールや、一時設定としてblockdev —setraを使用するなど)。リードアヘッドには128KiBの値を推奨します。

blockdevは512バイトセクターで値を期待し、lsblkKiBで値を報告します。たとえば、/dev/nvme0n1128KiBにリードアヘッドを一時的にblockdev --setra 256 /dev/nvme0n1に設定するには、blockdev --setra 256 /dev/nvme0n1を指定します。