Elasticsearch は、近似 knn 検索に HNSW アルゴリズムを使用します。HNSW アルゴリズムはグラフベースであるため、ほとんどのベクトルデータがメモリに保存されている場合に最も効果的です。そのため、データノードがベクトルデータとインデックススキーマを保存するのに十分なオフヒープメモリを持っていることを確認する必要があります。このトピックでは、ベクトルエンジンの主要なパフォーマンス最適化手法について説明します。
適切なパラメーターの設定
適切な m および ef_construction パラメーターを設定します。これらは、インデックス作成時に設定する dense_vector 型の高度なパラメーターです。詳細については、「Dense vector field type」をご参照ください。
HNSW は近似 k-NN 検索手法であり、最も隣接したすべてのデータポイントの取得を保証するものではありません。取得率に影響を与える主なパラメーターは m と ef_construction です。
パラメーター | 内容 |
| ノードの隣接数を指定します。デフォルト値は 16 です。隣接数を増やすと取得率は向上しますが、パフォーマンスに大きな影響を与え、メモリ使用量が増加します。取得率に厳密な要件がある場合は、このパラメーターを 64 以上に設定してください。 |
|
|
メモリ使用量の削減
Elasticsearch は、メモリ使用量を削減するために量子化を使用します。量子化により、ベクトルのメモリを 4、8、または 32 分の 1 に削減できます。たとえば、デフォルトの float 型では、ベクトル値は 4 バイトを使用します。int8 量子化を使用すると、各値は 1 バイトのみを使用します。int4 量子化を使用すると、各値は半バイトを使用します。Better Binary Quantization (BBQ) を使用すると、各値は 1 ビットのみを使用し、8 つの値で合計 1 バイトを使用します。これにより、メモリ要件は非量子化ベクトルの 1/32 に削減されます。
ベクトルデータのメモリを計算するには:
ベクトルデータと HNSW グラフインデックスの両方に必要なメモリを考慮する必要があります。ベクトルが非量子化されているか、int8 量子化を使用している場合、グラフインデックスは総メモリのごく一部を使用します。ただし、bbq 量子化を使用すると、グラフインデックスが使用するメモリの割合が大幅に増加します。したがって、ベクトルデータが使用するメモリを計算する際には、グラフインデックスも考慮する必要があります。
ベクトルデータメモリを計算する数式:
element_type: float:num_vectors * num_dimensions * 4element_type: floatとquantization: int8:num_vectors * (num_dimensions + 4)element_type: floatとquantization: int4:num_vectors * (num_dimensions/2 + 4)element_type: floatとquantization: bbq:num_vectors * (num_dimensions/8 + 12)element_type: byte:num_vectors * num_dimensionselement_type: bit:num_vectors * (num_dimensions/8)
flat 型を使用し、HNSW インデックスを作成しない場合、ベクトルデータのメモリ使用量は上記の数式を使用して計算されます。HNSW 型を使用する場合、グラフインデックスのサイズも計算する必要があります。グラフインデックスのサイズは、次の数式を使用して推定できます。
num_vectors * 4 * HNSW.m。HNSW.m のデフォルト値は 16 です。したがって、デフォルトサイズは num_vectors * 4 * 16 です。
ベクトルデータの総メモリは、これら 2 つの部分の合計です。
さらに、number_of_replicas (インデックスレプリカの数) を考慮してください。上記の計算は単一のデータコピーに対するものです。必要な総メモリは、すべてのレプリカコピーも考慮に入れます。たとえば、number_of_replicas のデフォルト値が 1 の場合、必要な総メモリは単一のデータコピーのメモリの 2 倍になります。
量子化を有効にすると、Elasticsearch は元のベクトルに加えて量子化されたベクトルデータを保存するため、ディスク上のインデックスサイズが増加します。たとえば、40 GB の浮動小数点ベクトルに int8 量子化を適用すると、量子化されたベクトル用にさらに 10 GB のデータが保存されます。総ディスク使用量は 50 GB になりますが、高速検索に必要なメモリは 10 GB に削減されます。
オフヒープメモリ容量は十分か
メモリ容量を計算し、ノードに十分なメモリがあるかどうかを確認する際には、ノードのオフヒープメモリに焦点を当てる必要があります。
オフヒープメモリを決定するには、ノードが Java ヒープ用に十分なメモリを予約する必要があることに注意してください。メモリが 64 GB 以下のノードの場合、オフヒープメモリは通常、総メモリの半分です。メモリが 64 GB を超えるノードの場合、オフヒープメモリはデフォルトで総メモリから 31 GB を引いた値です。正確な量を計算するには、次のコマンドを実行します。
GET _nodes/stats?human&filter_path=**.os.mem.total,**.jvm.mem.heap_maxノードの特定のオフヒープメモリ容量は、os.mem.total - jvm.mem.heap_max として計算されます。
ベクトルインデックスのメモリ計算
例:
1,000 万個のデータエントリがあり、それぞれ 1,024 ディメンションを持つと仮定します。デフォルトのベクトル設定を使用し、int8 量子化を有効にし、m=16 を設定し、デフォルトの number_of_replicas 値 1 を使用します。ベクトルデータに必要な総メモリは次のように計算されます。
2 × (10,000,000 × (1024 + 4) + 10,000,000 × 4 × 16) = 20.34 GB。
このインデックスを保存するために、それぞれ 16 GB のメモリを持つ 2 つのデータノードを使用する場合、ノードの総オフヒープメモリは (16 / 2) × 2 = 16 GB です。これはベクトルデータを保存するのに十分ではありません。
このインデックスを保存するために、それぞれ 32 GB のメモリを持つ 2 つのデータノードを使用する場合、ノードの総オフヒープメモリは (32 / 2) × 2 = 32 GB です。これはベクトルデータを保存するのに十分です。
必要な実際のオフヒープメモリを計算する際には、他のインデックス、ソースドキュメント、およびデータ読み書き操作からのネットワークトラフィックのために、一部のメモリを予約する必要もあります。本番環境では、オフヒープメモリの不足がディスクI/O使用率の高さと大量のランダム読み取りトラフィックを引き起こすことがよくあります。
ファイルシステムバッファーのプリフェッチ
Elasticsearch を実行しているマシンが再起動すると、ファイルシステムバッファーがクリアされます。その後、オペレーティングシステムは、高速検索を保証するために、インデックスの頻繁にアクセスされる部分をメモリにロードするのに時間が必要です。index.store.preload 設定を使用して、ファイル名拡張子に基づいて特定のファイルをメモリに即座にロードするようにオペレーティングシステムに明示的に指示できます。
ファイルシステムバッファーがすべてのデータを保持するのに十分な大きさでない場合、あまりにも多くのインデックスやファイルに対してデータを積極的にロードすると、検索が遅くなる可能性があります。この設定は注意して使用してください。
例:
PUT /my_vector_index
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1,
"index.store.preload": ["vex", "veq"]
},
"mappings": {
"properties": {
"my_vector": {
"type": "dense_vector",
"dims": 3
},
"my_text" : {
"type" : "keyword"
}
}
}
}index.store.preload がロードすべきインデックスファイルを決定するには、ファイル名拡張子の次の説明を参照してください。
次のファイル名拡張子は、近似 knn 検索に適用されます。各拡張子は量子化タイプによって細分化されます。
vexは HNSW グラフ構造を保存するファイルです。vecは、すべての非量子化ベクトル値を表します。これには、浮動小数点数、バイト、ビットなど、すべての要素タイプが含まれます。veqは、量子化 (int4またはint8) を使用するインデックス内の量子化されたベクトル用です。vebは、量子化 (bbq) を使用するインデックス内のバイナリベクトル用です。vem、vemf、vemq、およびvembはメタデータ用です。これらは通常小さく、プリロードする必要はありません。
通常、量子化されたインデックスを使用する場合、関連する量子化された値と HNSW グラフのみをプリロードします。元のベクトルをプリロードすることは不要であり、逆効果になる可能性があります。
構成例:
hnsw: "index.store.preload": ["vex", "vec"]
int8, int4: "index.store.preload": ["vex", "veq"]
bbq: "index.store.preload": ["vex", "veb"]
既存のインデックスにこの設定を構成する場合、index.store.preload は静的パラメーターであり、インデックス作成後に直接変更できないことに注意してください。一時的なインデックスの可用性の低下を許容できる場合は、次の手順に従ってください。まず、インデックスを閉じます。次に、パラメーターを構成します。最後に、インデックスを再度開きます。次の例は、これらの手順を実行する方法を示しています。
POST my_vector_index/_close
PUT my_vector_index/_settings
{
"index.store.preload": ["vex", "veq"]
}
POST my_vector_index/_open数を削減:インデックスの セグメント
Elasticsearch のシャードはセグメント (segment) で構成されており、これらはインデックス内の内部ストレージ要素です。近似 knn 検索の場合、Elasticsearch は各セグメントのベクトル値を個別の HNSW グラフとして保存します。結果として、knn 検索はすべてのセグメントを調べる必要があります。最近の knn 検索の並列化により、複数のセグメント間でのパフォーマンスが向上しましたが、セグメント数が少ない場合でも、knn 検索のパフォーマンスは数倍向上する可能性があります。デフォルトでは、Elasticsearch はバックグラウンドマージプロセスを通じて、より小さなセグメントをより大きなセグメントに定期的にマージします。これが不十分な場合は、インデックスセグメントの数を減らすために次の明示的な手順を実行できます。
1. 最大セグメントサイズの増加
Elasticsearch は、マージプロセスを制御するための多くの設定を提供します。重要な設定の 1 つは index.merge.policy.max_merged_segment です。この設定は、マージ中に作成されるセグメントの最大サイズを制御します。この値を増やすことで、インデックス内のセグメント数を減らすことができます。デフォルト値は 5 GB ですが、これはディメンションの大きいベクトルには小さすぎる可能性があります。セグメント数を減らすために、この値を 10 GB または 20 GB に増やすことを検討してください。例:
PUT my_vector_index/_settings
{
"index.merge.policy.max_merged_segment": "10gb"
}2. バッチインデックス作成時の大規模セグメントの作成
一般的なワークフローは、最初のバッチアップロードを実行し、その後インデックスを検索可能にすることです。Elasticsearch がマージを強制するのではなく、より大きな初期セグメントを作成するようにインデックス設定を調整できます。バッチアップロード中に、index.refresh_interval を -1 に設定することで無効にできます。これにより、リフレッシュ操作が防止され、余分なセグメントの作成が回避されます。また、Elasticsearch のインデックスバッファーを大きく構成して、リフレッシュが発生する前により多くのドキュメントを受け入れるようにすることもできます。デフォルトでは、indices.memory.index_buffer_size はヒープサイズの 10% に設定されています。32 GB のような大きなヒープサイズの場合、これは通常十分です。インデックスバッファー全体を使用できるようにするには、index.translog.flush_threshold_size の制限も増やす必要があります。
ベクターフィールドを _source から除外する
Elasticsearch は、インデックス作成時に提供された元の JSON ドキュメントを _source フィールドに保存します。デフォルトでは、検索結果の各ヒットには完全な _source ドキュメントが含まれます。ドキュメントに高次元密ベクトルフィールドが含まれている場合、_source は非常に大きく、ロードにコストがかかる可能性があります。これにより、knn 検索が大幅に遅くなる可能性があります。
インデックスの再作成、更新、および クエリによる更新 操作では、多くの場合 _source フィールドが必要です。_source からフィールドを除外すると、これらの操作が予期しない動作をする可能性があります。たとえば、インデックスを再作成する際に、dense_vector フィールドが新しいインデックスに含まれない場合があります。
excludes マッピングパラメーターを使用して、密ベクトルフィールドを _source に保存しないように除外できます。これにより、大量の生のベクトルデータが検索中にロードおよび返されるのを防ぎ、インデックスサイズも削減されます。_source から除外されたベクトルは、検索プロセスが別のデータ構造に依存しているため、引き続き knn 検索で使用できます。ただし、excludes パラメーターを使用する前に、_source フィールドを除外することの潜在的な欠点を確認する必要があります。欠点の詳細については、上記の注記をご参照ください。
PUT /my_vector_index
{
"mappings": {
"_source": {
"excludes": [
"my_vector"
]
},
"properties": {
"my_vector": {
"type": "dense_vector",
"dims": 3
},
"my_text": {
"type": "keyword"
}
}
}
}Elasticsearch のバージョンが 8.17 以降の場合、doc 内のベクトルコンテンツを表示するには、次のコマンドを使用できます。
GET my_vector_index/_search
{
"docvalue_fields": ["my_vector"]
}
他のバージョンも使用できます。
GET my_vector_index/_search
{
"script_fields": {
"vector_field": {
"script": {
"source" : "doc['my_vector'].vectorValue"
}
}
}
}_source フィールドからベクトルフィールドを除外する代替策については、「synthetic _source」をご参照ください。
インスタンスタイプ構成のアップグレード
ベクトル類似度計算は計算集約型タスクであるため、高い CPU パフォーマンスが必要です。そのため、Turbo インスタンスタイプを選択することで、パフォーマンスを 2 倍以上に向上させることができます。同じ仕様の Turbo インスタンスタイプにアップグレードするには、ブルーグリーンデプロイメントを使用できます。