k-nearest neighbor (kNN) 検索は、類似度メトリックによって測定されたクエリベクトルに最も近い k ベクトルを見つけます。

kNNの一般的な使用例には、次のものが含まれます:

  • 自然言語処理 (NLP) アルゴリズムに基づく関連性ランキング
  • 製品推奨および推薦エンジン
  • 画像や動画の類似検索

Prerequisites

  • kNN 検索を実行するには、データを意味のあるベクトル値に変換できる必要があります。これらのベクトルは、Elasticsearch の自然言語処理 (NLP) モデルを使用して作成することができます、または Elasticsearch の外部で生成することができます。ベクトルは、dense_vector フィールド値としてドキュメントに追加できます。クエリは、同じ次元のベクトルとして表されます。
    ドキュメントのベクトルがクエリベクトルに近いほど、類似度メトリックに基づいてマッチが良くなるようにベクトルを設計してください。
  • このガイドの手順を完了するには、次の インデックス権限 が必要です:
    • create_index または manage を使用して dense_vector フィールドを持つインデックスを作成する
    • createindex、または write を使用して作成したインデックスにデータを追加する
    • read を使用してインデックスを検索する

kNN methods

Elasticsearch は kNN 検索のための 2 つのメソッドをサポートしています:

ほとんどの場合、近似 kNN を使用することをお勧めします。近似 kNN は、インデックス作成が遅く、精度が不完全である代わりに、低遅延を提供します。

正確なブルートフォース kNN は正確な結果を保証しますが、大規模データセットではスケールしません。このアプローチでは、script_score クエリが各一致するドキュメントをスキャンしてベクトル関数を計算する必要があり、検索速度が遅くなる可能性があります。ただし、クエリ を使用して関数に渡される一致するドキュメントの数を制限することで、遅延を改善できます。データを小さなサブセットのドキュメントにフィルタリングすると、このアプローチを使用して良好な検索パフォーマンスを得ることができます。

Approximate kNN

他のタイプの検索と比較して、近似 kNN 検索には特定のリソース要件があります。特に、すべてのベクトルデータは、効率的にするためにノードのページキャッシュに収まる必要があります。構成とサイズに関する重要な注意事項については、近似 kNN 検索の調整ガイド を参照してください。

近似 kNN 検索を実行するには、knn オプションを使用して、インデックス作成が有効な 1 つ以上の dense_vector フィールドを検索します。

  • 1. 1 つ以上の dense_vector フィールドを明示的にマッピングします。近似 kNN 検索には、次のマッピングオプションが必要です:
    • similarity 値。この値は、クエリとドキュメントベクトルの類似性に基づいてドキュメントをスコアリングするために使用される類似度メトリックを決定します。利用可能なメトリックのリストについては、similarity パラメータのドキュメントを参照してください。similarity 設定は cosine にデフォルト設定されています。

Python

  1. resp = client.indices.create(
  2. index="image-index",
  3. mappings={
  4. "properties": {
  5. "image-vector": {
  6. "type": "dense_vector",
  7. "dims": 3,
  8. "similarity": "l2_norm"
  9. },
  10. "title-vector": {
  11. "type": "dense_vector",
  12. "dims": 5,
  13. "similarity": "l2_norm"
  14. },
  15. "title": {
  16. "type": "text"
  17. },
  18. "file-type": {
  19. "type": "keyword"
  20. }
  21. }
  22. },
  23. )
  24. print(resp)

Ruby

  1. response = client.indices.create(
  2. index: 'image-index',
  3. body: {
  4. mappings: {
  5. properties: {
  6. "image-vector": {
  7. type: 'dense_vector',
  8. dims: 3,
  9. similarity: 'l2_norm'
  10. },
  11. "title-vector": {
  12. type: 'dense_vector',
  13. dims: 5,
  14. similarity: 'l2_norm'
  15. },
  16. title: {
  17. type: 'text'
  18. },
  19. "file-type": {
  20. type: 'keyword'
  21. }
  22. }
  23. }
  24. }
  25. )
  26. puts response

Js

  1. const response = await client.indices.create({
  2. index: "image-index",
  3. mappings: {
  4. properties: {
  5. "image-vector": {
  6. type: "dense_vector",
  7. dims: 3,
  8. similarity: "l2_norm",
  9. },
  10. "title-vector": {
  11. type: "dense_vector",
  12. dims: 5,
  13. similarity: "l2_norm",
  14. },
  15. title: {
  16. type: "text",
  17. },
  18. "file-type": {
  19. type: "keyword",
  20. },
  21. },
  22. },
  23. });
  24. console.log(response);

Console

  1. PUT image-index
  2. {
  3. "mappings": {
  4. "properties": {
  5. "image-vector": {
  6. "type": "dense_vector",
  7. "dims": 3,
  8. "similarity": "l2_norm"
  9. },
  10. "title-vector": {
  11. "type": "dense_vector",
  12. "dims": 5,
  13. "similarity": "l2_norm"
  14. },
  15. "title": {
  16. "type": "text"
  17. },
  18. "file-type": {
  19. "type": "keyword"
  20. }
  21. }
  22. }
  23. }
  • 2. データをインデックスします。

Console

  1. POST image-index/_bulk?refresh=true
  2. { "index": { "_id": "1" } }
  3. { "image-vector": [1, 5, -20], "title-vector": [12, 50, -10, 0, 1], "title": "moose family", "file-type": "jpg" }
  4. { "index": { "_id": "2" } }
  5. { "image-vector": [42, 8, -15], "title-vector": [25, 1, 4, -12, 2], "title": "alpine lake", "file-type": "png" }
  6. { "index": { "_id": "3" } }
  7. { "image-vector": [15, 11, 23], "title-vector": [1, 5, 25, 50, 20], "title": "full moon", "file-type": "jpg" }
  8. ...

Python

  1. resp = client.search(
  2. index="image-index",
  3. knn={
  4. "field": "image-vector",
  5. "query_vector": [
  6. -5,
  7. 9,
  8. -12
  9. ],
  10. "k": 10,
  11. "num_candidates": 100
  12. },
  13. fields=[
  14. "title",
  15. "file-type"
  16. ],
  17. )
  18. print(resp)

Ruby

  1. response = client.search(
  2. index: 'image-index',
  3. body: {
  4. knn: {
  5. field: 'image-vector',
  6. query_vector: [
  7. -5,
  8. 9,
  9. -12
  10. ],
  11. k: 10,
  12. num_candidates: 100
  13. },
  14. fields: [
  15. 'title',
  16. 'file-type'
  17. ]
  18. }
  19. )
  20. puts response

Js

  1. const response = await client.search({
  2. index: "image-index",
  3. knn: {
  4. field: "image-vector",
  5. query_vector: [-5, 9, -12],
  6. k: 10,
  7. num_candidates: 100,
  8. },
  9. fields: ["title", "file-type"],
  10. });
  11. console.log(response);

Console

  1. POST image-index/_search
  2. {
  3. "knn": {
  4. "field": "image-vector",
  5. "query_vector": [-5, 9, -12],
  6. "k": 10,
  7. "num_candidates": 100
  8. },
  9. "fields": [ "title", "file-type" ]
  10. }

ドキュメント _score は、クエリとドキュメントベクトルの類似性によって決定されます。kNN 検索のスコアがどのように計算されるかについての詳細は、similarity を参照してください。

近似 kNN 検索のサポートはバージョン 8.0 で追加されました。それ以前は、dense_vector フィールドはマッピングで index を有効にすることをサポートしていませんでした。8.0 より前に dense_vector フィールドを含むインデックスを作成した場合、近似 kNN 検索をサポートするには、index: true を設定する新しいフィールドマッピングを使用してデータを再インデックス化する必要があります。これはデフォルトオプションです。

Tune approximate kNN for speed or accuracy

結果を収集するために、kNN 検索 API は各シャードで num_candidates 数の近似最近傍候補を見つけます。検索は、これらの候補ベクトルとクエリベクトルの類似性を計算し、各シャードから最も類似した k 結果を選択します。検索は、各シャードからの結果をマージして、グローバルなトップ k 最近傍を返します。

num_candidates を増やすことで、検索速度が遅くなる代わりに、より正確な結果を得ることができます。num_candidates の高い値を持つ検索は、各シャードからより多くの候補を考慮します。これには時間がかかりますが、検索は真の k 最近傍を見つける可能性が高くなります。

同様に、num_candidates を減少させることで、潜在的に精度が低下する可能性のあるより速い検索を実現できます。

Approximate kNN using byte vectors

近似 kNN 検索 API は、byte 値ベクトルに加えて float 値ベクトルをサポートしています。knn オプション を使用して、dense_vector フィールドbyte に設定し、インデックス作成を有効にして検索します。

  • 1. element_typebyte に設定し、インデックス作成を有効にして、1 つ以上の dense_vector フィールドを明示的にマッピングします。

Python

  1. resp = client.indices.create(
  2. index="byte-image-index",
  3. mappings={
  4. "properties": {
  5. "byte-image-vector": {
  6. "type": "dense_vector",
  7. "element_type": "byte",
  8. "dims": 2
  9. },
  10. "title": {
  11. "type": "text"
  12. }
  13. }
  14. },
  15. )
  16. print(resp)

Ruby

  1. response = client.indices.create(
  2. index: 'byte-image-index',
  3. body: {
  4. mappings: {
  5. properties: {
  6. "byte-image-vector": {
  7. type: 'dense_vector',
  8. element_type: 'byte',
  9. dims: 2
  10. },
  11. title: {
  12. type: 'text'
  13. }
  14. }
  15. }
  16. }
  17. )
  18. puts response

Js

  1. const response = await client.indices.create({
  2. index: "byte-image-index",
  3. mappings: {
  4. properties: {
  5. "byte-image-vector": {
  6. type: "dense_vector",
  7. element_type: "byte",
  8. dims: 2,
  9. },
  10. title: {
  11. type: "text",
  12. },
  13. },
  14. },
  15. });
  16. console.log(response);

Console

  1. PUT byte-image-index
  2. {
  3. "mappings": {
  4. "properties": {
  5. "byte-image-vector": {
  6. "type": "dense_vector",
  7. "element_type": "byte",
  8. "dims": 2
  9. },
  10. "title": {
  11. "type": "text"
  12. }
  13. }
  14. }
  15. }
  • 2. データをインデックスし、すべてのベクトル値が [-128, 127] の範囲内の整数であることを確認します。

Python

  1. resp = client.bulk(
  2. index="byte-image-index",
  3. refresh=True,
  4. operations=[
  5. {
  6. "index": {
  7. "_id": "1"
  8. }
  9. },
  10. {
  11. "byte-image-vector": [
  12. 5,
  13. -20
  14. ],
  15. "title": "moose family"
  16. },
  17. {
  18. "index": {
  19. "_id": "2"
  20. }
  21. },
  22. {
  23. "byte-image-vector": [
  24. 8,
  25. -15
  26. ],
  27. "title": "alpine lake"
  28. },
  29. {
  30. "index": {
  31. "_id": "3"
  32. }
  33. },
  34. {
  35. "byte-image-vector": [
  36. 11,
  37. 23
  38. ],
  39. "title": "full moon"
  40. }
  41. ],
  42. )
  43. print(resp)

Ruby

  1. response = client.bulk(
  2. index: 'byte-image-index',
  3. refresh: true,
  4. body: [
  5. {
  6. index: {
  7. _id: '1'
  8. }
  9. },
  10. {
  11. "byte-image-vector": [
  12. 5,
  13. -20
  14. ],
  15. title: 'moose family'
  16. },
  17. {
  18. index: {
  19. _id: '2'
  20. }
  21. },
  22. {
  23. "byte-image-vector": [
  24. 8,
  25. -15
  26. ],
  27. title: 'alpine lake'
  28. },
  29. {
  30. index: {
  31. _id: '3'
  32. }
  33. },
  34. {
  35. "byte-image-vector": [
  36. 11,
  37. 23
  38. ],
  39. title: 'full moon'
  40. }
  41. ]
  42. )
  43. puts response

Js

  1. const response = await client.bulk({
  2. index: "byte-image-index",
  3. refresh: "true",
  4. operations: [
  5. {
  6. index: {
  7. _id: "1",
  8. },
  9. },
  10. {
  11. "byte-image-vector": [5, -20],
  12. title: "moose family",
  13. },
  14. {
  15. index: {
  16. _id: "2",
  17. },
  18. },
  19. {
  20. "byte-image-vector": [8, -15],
  21. title: "alpine lake",
  22. },
  23. {
  24. index: {
  25. _id: "3",
  26. },
  27. },
  28. {
  29. "byte-image-vector": [11, 23],
  30. title: "full moon",
  31. },
  32. ],
  33. });
  34. console.log(response);

Console

  1. POST byte-image-index/_bulk?refresh=true
  2. { "index": { "_id": "1" } }
  3. { "byte-image-vector": [5, -20], "title": "moose family" }
  4. { "index": { "_id": "2" } }
  5. { "byte-image-vector": [8, -15], "title": "alpine lake" }
  6. { "index": { "_id": "3" } }
  7. { "byte-image-vector": [11, 23], "title": "full moon" }
  • 3. knn オプション を使用して検索を実行し、query_vector 値が [-128, 127] の範囲内の整数であることを確認します。

Python

  1. resp = client.search(
  2. index="byte-image-index",
  3. knn={
  4. "field": "byte-image-vector",
  5. "query_vector": [
  6. -5,
  7. 9
  8. ],
  9. "k": 10,
  10. "num_candidates": 100
  11. },
  12. fields=[
  13. "title"
  14. ],
  15. )
  16. print(resp)

Ruby

  1. response = client.search(
  2. index: 'byte-image-index',
  3. body: {
  4. knn: {
  5. field: 'byte-image-vector',
  6. query_vector: [
  7. -5,
  8. 9
  9. ],
  10. k: 10,
  11. num_candidates: 100
  12. },
  13. fields: [
  14. 'title'
  15. ]
  16. }
  17. )
  18. puts response

Js

  1. const response = await client.search({
  2. index: "byte-image-index",
  3. knn: {
  4. field: "byte-image-vector",
  5. query_vector: [-5, 9],
  6. k: 10,
  7. num_candidates: 100,
  8. },
  9. fields: ["title"],
  10. });
  11. console.log(response);

Console

  1. POST byte-image-index/_search
  2. {
  3. "knn": {
  4. "field": "byte-image-vector",
  5. "query_vector": [-5, 9],
  6. "k": 10,
  7. "num_candidates": 100
  8. },
  9. "fields": [ "title" ]
  10. }

: 標準のバイト配列に加えて、query_vector パラメータに対して16進エンコードされた文字列値を提供することもできます。たとえば、上記の検索リクエストは、次のように表現することもでき、同じ結果が得られます。

Python

  1. resp = client.search(
  2. index="byte-image-index",
  3. knn={
  4. "field": "byte-image-vector",
  5. "query_vector": "fb09",
  6. "k": 10,
  7. "num_candidates": 100
  8. },
  9. fields=[
  10. "title"
  11. ],
  12. )
  13. print(resp)

Ruby

  1. response = client.search(
  2. index: 'byte-image-index',
  3. body: {
  4. knn: {
  5. field: 'byte-image-vector',
  6. query_vector: 'fb09',
  7. k: 10,
  8. num_candidates: 100
  9. },
  10. fields: [
  11. 'title'
  12. ]
  13. }
  14. )
  15. puts response

Js

  1. const response = await client.search({
  2. index: "byte-image-index",
  3. knn: {
  4. field: "byte-image-vector",
  5. query_vector: "fb09",
  6. k: 10,
  7. num_candidates: 100,
  8. },
  9. fields: ["title"],
  10. });
  11. console.log(response);

Console

  1. POST byte-image-index/_search
  2. {
  3. "knn": {
  4. "field": "byte-image-vector",
  5. "query_vector": "fb09",
  6. "k": 10,
  7. "num_candidates": 100
  8. },
  9. "fields": [ "title" ]
  10. }

float ベクトルを提供したいが、byte ベクトルのメモリ節約を望む場合は、量子化 機能を使用できます。量子化を使用すると、float ベクトルを提供できますが、内部的には byte ベクトルとしてインデックスされます。さらに、元の float ベクトルはインデックスに保持されます。

dense_vector のデフォルトインデックスタイプは int8_hnsw です。

量子化を使用するには、int8_hnsw または int4_hnsw オブジェクトを dense_vector マッピングで使用できます。

Python

  1. resp = client.indices.create(
  2. index="quantized-image-index",
  3. mappings={
  4. "properties": {
  5. "image-vector": {
  6. "type": "dense_vector",
  7. "element_type": "float",
  8. "dims": 2,
  9. "index": True,
  10. "index_options": {
  11. "type": "int8_hnsw"
  12. }
  13. },
  14. "title": {
  15. "type": "text"
  16. }
  17. }
  18. },
  19. )
  20. print(resp)

Ruby

  1. response = client.indices.create(
  2. index: 'quantized-image-index',
  3. body: {
  4. mappings: {
  5. properties: {
  6. "image-vector": {
  7. type: 'dense_vector',
  8. element_type: 'float',
  9. dims: 2,
  10. index: true,
  11. index_options: {
  12. type: 'int8_hnsw'
  13. }
  14. },
  15. title: {
  16. type: 'text'
  17. }
  18. }
  19. }
  20. }
  21. )
  22. puts response

Js

  1. const response = await client.indices.create({
  2. index: "quantized-image-index",
  3. mappings: {
  4. properties: {
  5. "image-vector": {
  6. type: "dense_vector",
  7. element_type: "float",
  8. dims: 2,
  9. index: true,
  10. index_options: {
  11. type: "int8_hnsw",
  12. },
  13. },
  14. title: {
  15. type: "text",
  16. },
  17. },
  18. },
  19. });
  20. console.log(response);

Console

  1. PUT quantized-image-index
  2. {
  3. "mappings": {
  4. "properties": {
  5. "image-vector": {
  6. "type": "dense_vector",
  7. "element_type": "float",
  8. "dims": 2,
  9. "index": true,
  10. "index_options": {
  11. "type": "int8_hnsw"
  12. }
  13. },
  14. "title": {
  15. "type": "text"
  16. }
  17. }
  18. }
  19. }
  • 1. float ベクトルをインデックスします。

Python

  1. resp = client.bulk(
  2. index="quantized-image-index",
  3. refresh=True,
  4. operations=[
  5. {
  6. "index": {
  7. "_id": "1"
  8. }
  9. },
  10. {
  11. "image-vector": [
  12. 0.1,
  13. -2
  14. ],
  15. "title": "moose family"
  16. },
  17. {
  18. "index": {
  19. "_id": "2"
  20. }
  21. },
  22. {
  23. "image-vector": [
  24. 0.75,
  25. -1
  26. ],
  27. "title": "alpine lake"
  28. },
  29. {
  30. "index": {
  31. "_id": "3"
  32. }
  33. },
  34. {
  35. "image-vector": [
  36. 1.2,
  37. 0.1
  38. ],
  39. "title": "full moon"
  40. }
  41. ],
  42. )
  43. print(resp)

Ruby

  1. response = client.bulk(
  2. index: 'quantized-image-index',
  3. refresh: true,
  4. body: [
  5. {
  6. index: {
  7. _id: '1'
  8. }
  9. },
  10. {
  11. "image-vector": [
  12. 0.1,
  13. -2
  14. ],
  15. title: 'moose family'
  16. },
  17. {
  18. index: {
  19. _id: '2'
  20. }
  21. },
  22. {
  23. "image-vector": [
  24. 0.75,
  25. -1
  26. ],
  27. title: 'alpine lake'
  28. },
  29. {
  30. index: {
  31. _id: '3'
  32. }
  33. },
  34. {
  35. "image-vector": [
  36. 1.2,
  37. 0.1
  38. ],
  39. title: 'full moon'
  40. }
  41. ]
  42. )
  43. puts response

Js

  1. const response = await client.bulk({
  2. index: "quantized-image-index",
  3. refresh: "true",
  4. operations: [
  5. {
  6. index: {
  7. _id: "1",
  8. },
  9. },
  10. {
  11. "image-vector": [0.1, -2],
  12. title: "moose family",
  13. },
  14. {
  15. index: {
  16. _id: "2",
  17. },
  18. },
  19. {
  20. "image-vector": [0.75, -1],
  21. title: "alpine lake",
  22. },
  23. {
  24. index: {
  25. _id: "3",
  26. },
  27. },
  28. {
  29. "image-vector": [1.2, 0.1],
  30. title: "full moon",
  31. },
  32. ],
  33. });
  34. console.log(response);

Console

  1. POST quantized-image-index/_bulk?refresh=true
  2. { "index": { "_id": "1" } }
  3. { "image-vector": [0.1, -2], "title": "moose family" }
  4. { "index": { "_id": "2" } }
  5. { "image-vector": [0.75, -1], "title": "alpine lake" }
  6. { "index": { "_id": "3" } }
  7. { "image-vector": [1.2, 0.1], "title": "full moon" }
  • 2. knn オプション を使用して検索を実行します。検索時に、float ベクトルは自動的に byte ベクトルに量子化されます。

Python

  1. resp = client.search(
  2. index="quantized-image-index",
  3. knn={
  4. "field": "image-vector",
  5. "query_vector": [
  6. 0.1,
  7. -2
  8. ],
  9. "k": 10,
  10. "num_candidates": 100
  11. },
  12. fields=[
  13. "title"
  14. ],
  15. )
  16. print(resp)

Ruby

  1. response = client.search(
  2. index: 'quantized-image-index',
  3. body: {
  4. knn: {
  5. field: 'image-vector',
  6. query_vector: [
  7. 0.1,
  8. -2
  9. ],
  10. k: 10,
  11. num_candidates: 100
  12. },
  13. fields: [
  14. 'title'
  15. ]
  16. }
  17. )
  18. puts response

Js

  1. const response = await client.search({
  2. index: "quantized-image-index",
  3. knn: {
  4. field: "image-vector",
  5. query_vector: [0.1, -2],
  6. k: 10,
  7. num_candidates: 100,
  8. },
  9. fields: ["title"],
  10. });
  11. console.log(response);

Console

  1. POST quantized-image-index/_search
  2. {
  3. "knn": {
  4. "field": "image-vector",
  5. "query_vector": [0.1, -2],
  6. "k": 10,
  7. "num_candidates": 100
  8. },
  9. "fields": [ "title" ]
  10. }

元の float ベクトルはインデックスに保持されているため、再スコアリングにオプションで使用できます。つまり、int8_hnsw インデックスを使用してすべてのベクトルを迅速に検索し、トップ k 結果のみを再スコアリングできます。これにより、迅速な検索と正確なスコアリングの両方を実現できます。

Python

  1. resp = client.search(
  2. index="quantized-image-index",
  3. knn={
  4. "field": "image-vector",
  5. "query_vector": [
  6. 0.1,
  7. -2
  8. ],
  9. "k": 15,
  10. "num_candidates": 100
  11. },
  12. fields=[
  13. "title"
  14. ],
  15. rescore={
  16. "window_size": 10,
  17. "query": {
  18. "rescore_query": {
  19. "script_score": {
  20. "query": {
  21. "match_all": {}
  22. },
  23. "script": {
  24. "source": "cosineSimilarity(params.query_vector, 'image-vector') + 1.0",
  25. "params": {
  26. "query_vector": [
  27. 0.1,
  28. -2
  29. ]
  30. }
  31. }
  32. }
  33. }
  34. }
  35. },
  36. )
  37. print(resp)

Ruby

  1. response = client.search(
  2. index: 'quantized-image-index',
  3. body: {
  4. knn: {
  5. field: 'image-vector',
  6. query_vector: [
  7. 0.1,
  8. -2
  9. ],
  10. k: 15,
  11. num_candidates: 100
  12. },
  13. fields: [
  14. 'title'
  15. ],
  16. rescore: {
  17. window_size: 10,
  18. query: {
  19. rescore_query: {
  20. script_score: {
  21. query: {
  22. match_all: {}
  23. },
  24. script: {
  25. source: "cosineSimilarity(params.query_vector, 'image-vector') + 1.0",
  26. params: {
  27. query_vector: [
  28. 0.1,
  29. -2
  30. ]
  31. }
  32. }
  33. }
  34. }
  35. }
  36. }
  37. }
  38. )
  39. puts response

Js

  1. const response = await client.search({
  2. index: "quantized-image-index",
  3. knn: {
  4. field: "image-vector",
  5. query_vector: [0.1, -2],
  6. k: 15,
  7. num_candidates: 100,
  8. },
  9. fields: ["title"],
  10. rescore: {
  11. window_size: 10,
  12. query: {
  13. rescore_query: {
  14. script_score: {
  15. query: {
  16. match_all: {},
  17. },
  18. script: {
  19. source:
  20. "cosineSimilarity(params.query_vector, 'image-vector') + 1.0",
  21. params: {
  22. query_vector: [0.1, -2],
  23. },
  24. },
  25. },
  26. },
  27. },
  28. },
  29. });
  30. console.log(response);

Console

  1. POST quantized-image-index/_search
  2. {
  3. "knn": {
  4. "field": "image-vector",
  5. "query_vector": [0.1, -2],
  6. "k": 15,
  7. "num_candidates": 100
  8. },
  9. "fields": [ "title" ],
  10. "rescore": {
  11. "window_size": 10,
  12. "query": {
  13. "rescore_query": {
  14. "script_score": {
  15. "query": {
  16. "match_all": {}
  17. },
  18. "script": {
  19. "source": "cosineSimilarity(params.query_vector, 'image-vector') + 1.0",
  20. "params": {
  21. "query_vector": [0.1, -2]
  22. }
  23. }
  24. }
  25. }
  26. }
  27. }
  28. }

kNN 検索 API は、フィルターを使用して検索を制限することをサポートしています。検索は、フィルタークエリにも一致するトップ k ドキュメントを返します。

次のリクエストは、file-type フィールドでフィルタリングされた近似 kNN 検索を実行します:

Python

  1. resp = client.search(
  2. index="image-index",
  3. knn={
  4. "field": "image-vector",
  5. "query_vector": [
  6. 54,
  7. 10,
  8. -2
  9. ],
  10. "k": 5,
  11. "num_candidates": 50,
  12. "filter": {
  13. "term": {
  14. "file-type": "png"
  15. }
  16. }
  17. },
  18. fields=[
  19. "title"
  20. ],
  21. source=False,
  22. )
  23. print(resp)

Ruby

  1. response = client.search(
  2. index: 'image-index',
  3. body: {
  4. knn: {
  5. field: 'image-vector',
  6. query_vector: [
  7. 54,
  8. 10,
  9. -2
  10. ],
  11. k: 5,
  12. num_candidates: 50,
  13. filter: {
  14. term: {
  15. "file-type": 'png'
  16. }
  17. }
  18. },
  19. fields: [
  20. 'title'
  21. ],
  22. _source: false
  23. }
  24. )
  25. puts response

Js

  1. const response = await client.search({
  2. index: "image-index",
  3. knn: {
  4. field: "image-vector",
  5. query_vector: [54, 10, -2],
  6. k: 5,
  7. num_candidates: 50,
  8. filter: {
  9. term: {
  10. "file-type": "png",
  11. },
  12. },
  13. },
  14. fields: ["title"],
  15. _source: false,
  16. });
  17. console.log(response);

Console

  1. POST image-index/_search
  2. {
  3. "knn": {
  4. "field": "image-vector",
  5. "query_vector": [54, 10, -2],
  6. "k": 5,
  7. "num_candidates": 50,
  8. "filter": {
  9. "term": {
  10. "file-type": "png"
  11. }
  12. }
  13. },
  14. "fields": ["title"],
  15. "_source": false
  16. }

フィルターは、k に一致するドキュメントが返されることを保証するために、近似 kNN 検索 中に 適用されます。これは、近似 kNN 検索が完了した 後に フィルターが適用されるポストフィルタリングアプローチとは対照的です。ポストフィルタリングには、十分な一致するドキュメントがある場合でも、k 結果未満を返すことがあるという欠点があります。

Approximate kNN search and filtering

従来のクエリフィルタリングとは異なり、より制限的なフィルターが通常はクエリを高速化するのに対し、HNSW インデックスを使用した近似 kNN 検索でフィルターを適用すると、パフォーマンスが低下する可能性があります。これは、HNSW グラフを検索するために、フィルター基準を満たす num_candidates を取得するために追加の探索が必要だからです。

重大なパフォーマンスの低下を避けるために、Lucene はセグメントごとに次の戦略を実装しています:

  • フィルターされたドキュメント数が num_candidates 以下の場合、検索は HNSW グラフをバイパスし、フィルターされたドキュメントに対してブルートフォース検索を使用します。
  • HNSW グラフを探索している間、探索されたノードの数がフィルターを満たすドキュメントの数を超えた場合、検索はグラフの探索を停止し、フィルターされたドキュメントに対してブルートフォース検索に切り替えます。

Combine approximate kNN with other features

ハイブリッド検索 を実行するには、knn オプションquery の両方を提供できます:

Python

  1. resp = client.search(
  2. index="image-index",
  3. query={
  4. "match": {
  5. "title": {
  6. "query": "mountain lake",
  7. "boost": 0.9
  8. }
  9. }
  10. },
  11. knn={
  12. "field": "image-vector",
  13. "query_vector": [
  14. 54,
  15. 10,
  16. -2
  17. ],
  18. "k": 5,
  19. "num_candidates": 50,
  20. "boost": 0.1
  21. },
  22. size=10,
  23. )
  24. print(resp)

Ruby

  1. response = client.search(
  2. index: 'image-index',
  3. body: {
  4. query: {
  5. match: {
  6. title: {
  7. query: 'mountain lake',
  8. boost: 0.9
  9. }
  10. }
  11. },
  12. knn: {
  13. field: 'image-vector',
  14. query_vector: [
  15. 54,
  16. 10,
  17. -2
  18. ],
  19. k: 5,
  20. num_candidates: 50,
  21. boost: 0.1
  22. },
  23. size: 10
  24. }
  25. )
  26. puts response

Js

  1. const response = await client.search({
  2. index: "image-index",
  3. query: {
  4. match: {
  5. title: {
  6. query: "mountain lake",
  7. boost: 0.9,
  8. },
  9. },
  10. },
  11. knn: {
  12. field: "image-vector",
  13. query_vector: [54, 10, -2],
  14. k: 5,
  15. num_candidates: 50,
  16. boost: 0.1,
  17. },
  18. size: 10,
  19. });
  20. console.log(response);

Console

  1. POST image-index/_search
  2. {
  3. "query": {
  4. "match": {
  5. "title": {
  6. "query": "mountain lake",
  7. "boost": 0.9
  8. }
  9. }
  10. },
  11. "knn": {
  12. "field": "image-vector",
  13. "query_vector": [54, 10, -2],
  14. "k": 5,
  15. "num_candidates": 50,
  16. "boost": 0.1
  17. },
  18. "size": 10
  19. }

この検索は、グローバルなトップ k = 5 ベクトルマッチを見つけ、それを match クエリからのマッチと組み合わせ、最終的にトップ 10 のスコア結果を返します。knnquery のマッチは、論理和のように組み合わされます。すなわち、両者の間にブールの または を取るかのように。トップ k ベクトル結果は、すべてのインデックスシャードにわたるグローバルな最近傍を表します。

各ヒットのスコアは、knnquery スコアの合計です。合計の各スコアに重みを与える boost 値を指定できます。上記の例では、スコアは次のように計算されます:

  1. score = 0.9 * match_score + 0.1 * knn_score

knn オプションは、aggregations とともに使用することもできます。一般に、Elasticsearch は検索に一致するすべてのドキュメントに対して集計を計算します。したがって、近似 kNN 検索の場合、集計はトップ k 最近のドキュメントに対して計算されます。検索に query が含まれている場合、集計は knnquery のマッチの組み合わせセットに対して計算されます。

kNN 検索を使用すると、以前にデプロイされた テキスト埋め込みモデル を使用してセマンティック検索を実行できます。検索用語のリテラルマッチの代わりに、セマンティック検索は検索クエリの意図と文脈的意味に基づいて結果を取得します。

内部では、テキスト埋め込み NLP モデルは、提供された入力クエリ文字列から model_text と呼ばれる密なベクトルを生成します。次に、それは同じテキスト埋め込み機械学習モデルで作成された密なベクトルを含むインデックスに対して検索されます。検索結果は、モデルによって学習されたセマンティックに類似しています。

セマンティック検索を実行するには:

  • 検索対象の入力データの密なベクトル表現を含むインデックスが必要です。
  • 入力データから密なベクトルを作成するために使用したのと同じテキスト埋め込みモデルを検索に使用する必要があります。
  • テキスト埋め込み NLP モデルのデプロイメントを開始する必要があります。

デプロイされたテキスト埋め込みモデルまたは query_vector_builder オブジェクト内のモデルデプロイメントを参照し、検索クエリを model_text として提供します:

Js

  1. (...)
  2. {
  3. "knn": {
  4. "field": "dense-vector-field",
  5. "k": 10,
  6. "num_candidates": 100,
  7. "query_vector_builder": {
  8. "text_embedding": {
  9. "model_id": "my-text-embedding-model",
  10. "model_text": "The opposite of blue"
  11. }
  12. }
  13. }
  14. }
  15. (...)
実行する自然言語処理タスク。text_embedding である必要があります。
クエリ文字列から密なベクトルを生成するために使用するテキスト埋め込みモデルの ID。検索対象のインデックス内の入力テキストから埋め込みを生成したのと同じモデルを使用します。deployment_id の値を model_id 引数に代わりに使用できます。
モデルが密なベクトル表現を生成するクエリ文字列。

トレーニングされたモデルをデプロイし、それを使用してテキスト埋め込みを作成する方法についての詳細は、この エンドツーエンドの例 を参照してください。

Search multiple kNN fields

ハイブリッド検索に加えて、同時に複数の kNN ベクトルフィールドを検索できます:

Python

  1. resp = client.search(
  2. index="image-index",
  3. query={
  4. "match": {
  5. "title": {
  6. "query": "mountain lake",
  7. "boost": 0.9
  8. }
  9. }
  10. },
  11. knn=[
  12. {
  13. "field": "image-vector",
  14. "query_vector": [
  15. 54,
  16. 10,
  17. -2
  18. ],
  19. "k": 5,
  20. "num_candidates": 50,
  21. "boost": 0.1
  22. },
  23. {
  24. "field": "title-vector",
  25. "query_vector": [
  26. 1,
  27. 20,
  28. -52,
  29. 23,
  30. 10
  31. ],
  32. "k": 10,
  33. "num_candidates": 10,
  34. "boost": 0.5
  35. }
  36. ],
  37. size=10,
  38. )
  39. print(resp)

Ruby

  1. response = client.search(
  2. index: 'image-index',
  3. body: {
  4. query: {
  5. match: {
  6. title: {
  7. query: 'mountain lake',
  8. boost: 0.9
  9. }
  10. }
  11. },
  12. knn: [
  13. {
  14. field: 'image-vector',
  15. query_vector: [
  16. 54,
  17. 10,
  18. -2
  19. ],
  20. k: 5,
  21. num_candidates: 50,
  22. boost: 0.1
  23. },
  24. {
  25. field: 'title-vector',
  26. query_vector: [
  27. 1,
  28. 20,
  29. -52,
  30. 23,
  31. 10
  32. ],
  33. k: 10,
  34. num_candidates: 10,
  35. boost: 0.5
  36. }
  37. ],
  38. size: 10
  39. }
  40. )
  41. puts response

Js

  1. const response = await client.search({
  2. index: "image-index",
  3. query: {
  4. match: {
  5. title: {
  6. query: "mountain lake",
  7. boost: 0.9,
  8. },
  9. },
  10. },
  11. knn: [
  12. {
  13. field: "image-vector",
  14. query_vector: [54, 10, -2],
  15. k: 5,
  16. num_candidates: 50,
  17. boost: 0.1,
  18. },
  19. {
  20. field: "title-vector",
  21. query_vector: [1, 20, -52, 23, 10],
  22. k: 10,
  23. num_candidates: 10,
  24. boost: 0.5,
  25. },
  26. ],
  27. size: 10,
  28. });
  29. console.log(response);

k近傍法 (kNN) 検索

k-nearest neighbor (kNN) 検索は、クエリベクトルに最も近い k 個のベクトルを、類似度メトリックによって測定します。

kNNの一般的な使用例には、次のものが含まれます:

  • 自然言語処理 (NLP) アルゴリズムに基づく関連性ランキング
  • 製品推奨および推薦エンジン
  • 画像や動画の類似検索

Search kNN with expected similarity

kNN は強力なツールですが、常に k 最近傍を返そうとします。したがって、knnfilter とともに使用すると、関連するすべてのドキュメントをフィルタリングし、検索するのは無関係なものだけになる可能性があります。その場合、knn は、ベクトル空間で遠く離れた k 最近傍を返すために最善を尽くします。

この懸念を軽減するために、similarity パラメータが knn 句で利用可能です。この値は、ベクトルがマッチと見なされるために必要な最小類似度です。このパラメータを使用した knn 検索フローは次のようになります:

  • ユーザー提供の filter クエリを適用します
  • ベクトル空間を探索して k ベクトルを取得します
  • 設定された similarity よりも遠くにあるベクトルは返しません

similarity は、_score に変換され、ブーストが適用される前の真の 類似度 です。

設定された各 類似度 に対して、対応する逆 _score 関数があります。これは、_score の観点からフィルタリングしたい場合に、無関係な結果を正しく拒否するためにこの小さな変換を行うことができるようにするためです。

  • l2_norm: sqrt((1 / _score) - 1)
  • cosine: (2 * _score) - 1
  • dot_product: (2 * _score) - 1
    1. - `````_score < 1`````: `````1 - (1 / _score)
    • _score >= 1: _score - 1

ここに例があります。この例では、query_vector に対して k 最近傍を検索します。ただし、filter が適用され、見つかったベクトルが少なくとも提供された similarity を持つことが要求されます。

Python

  1. resp = client.search(
  2. index="image-index",
  3. knn={
  4. "field": "image-vector",
  5. "query_vector": [
  6. 1,
  7. 5,
  8. -20
  9. ],
  10. "k": 5,
  11. "num_candidates": 50,
  12. "similarity": 36,
  13. "filter": {
  14. "term": {
  15. "file-type": "png"
  16. }
  17. }
  18. },
  19. fields=[
  20. "title"
  21. ],
  22. source=False,
  23. )
  24. print(resp)

Ruby

  1. response = client.search(
  2. index: 'image-index',
  3. body: {
  4. knn: {
  5. field: 'image-vector',
  6. query_vector: [
  7. 1,
  8. 5,
  9. -20
  10. ],
  11. k: 5,
  12. num_candidates: 50,
  13. similarity: 36,
  14. filter: {
  15. term: {
  16. "file-type": 'png'
  17. }
  18. }
  19. },
  20. fields: [
  21. 'title'
  22. ],
  23. _source: false
  24. }
  25. )
  26. puts response

Js

  1. const response = await client.search({
  2. index: "image-index",
  3. knn: {
  4. field: "image-vector",
  5. query_vector: [1, 5, -20],
  6. k: 5,
  7. num_candidates: 50,
  8. similarity: 36,
  9. filter: {
  10. term: {
  11. "file-type": "png",
  12. },
  13. },
  14. },
  15. fields: ["title"],
  16. _source: false,
  17. });
  18. console.log(response);

Console

  1. POST image-index/_search
  2. {
  3. "knn": {
  4. "field": "image-vector",
  5. "query_vector": [1, 5, -20],
  6. "k": 5,
  7. "num_candidates": 50,
  8. "similarity": 36,
  9. "filter": {
  10. "term": {
  11. "file-type": "png"
  12. }
  13. }
  14. },
  15. "fields": ["title"],
  16. "_source": false
  17. }

データセット内で、png のファイルタイプを持つ唯一のドキュメントは [42, 8, -15] のベクトルを持っています。l2_norm[42, 8, -15][1, 5, -20] の間の距離は 41.412 であり、これは設定された類似度 36 よりも大きいです。つまり、この検索はヒットを返しません。

テキストが特定のモデルのトークン制限を超えることは一般的であり、個々のチャンクの埋め込みを構築する前にチャンク化が必要です。nesteddense_vector とともに使用すると、トップレベルのドキュメントメタデータをコピーせずに最近のパッセージの取得を実現できます。

ここに、ベクトルとフィルタリング用のいくつかのトップレベルメタデータを保存するシンプルなパッセージベクトルインデックスがあります。

Python

  1. resp = client.indices.create(
  2. index="passage_vectors",
  3. mappings={
  4. "properties": {
  5. "full_text": {
  6. "type": "text"
  7. },
  8. "creation_time": {
  9. "type": "date"
  10. },
  11. "paragraph": {
  12. "type": "nested",
  13. "properties": {
  14. "vector": {
  15. "type": "dense_vector",
  16. "dims": 2,
  17. "index_options": {
  18. "type": "hnsw"
  19. }
  20. },
  21. "text": {
  22. "type": "text",
  23. "index": False
  24. }
  25. }
  26. }
  27. }
  28. },
  29. )
  30. print(resp)

Ruby

  1. response = client.indices.create(
  2. index: 'passage_vectors',
  3. body: {
  4. mappings: {
  5. properties: {
  6. full_text: {
  7. type: 'text'
  8. },
  9. creation_time: {
  10. type: 'date'
  11. },
  12. paragraph: {
  13. type: 'nested',
  14. properties: {
  15. vector: {
  16. type: 'dense_vector',
  17. dims: 2,
  18. index_options: {
  19. type: 'hnsw'
  20. }
  21. },
  22. text: {
  23. type: 'text',
  24. index: false
  25. }
  26. }
  27. }
  28. }
  29. }
  30. }
  31. )
  32. puts response

Js

  1. const response = await client.indices.create({
  2. index: "passage_vectors",
  3. mappings: {
  4. properties: {
  5. full_text: {
  6. type: "text",
  7. },
  8. creation_time: {
  9. type: "date",
  10. },
  11. paragraph: {
  12. type: "nested",
  13. properties: {
  14. vector: {
  15. type: "dense_vector",
  16. dims: 2,
  17. index_options: {
  18. type: "hnsw",
  19. },
  20. },
  21. text: {
  22. type: "text",
  23. index: false,
  24. },
  25. },
  26. },
  27. },
  28. },
  29. });
  30. console.log(response);

Console

  1. PUT passage_vectors
  2. {
  3. "mappings": {
  4. "properties": {
  5. "full_text": {
  6. "type": "text"
  7. },
  8. "creation_time": {
  9. "type": "date"
  10. },
  11. "paragraph": {
  12. "type": "nested",
  13. "properties": {
  14. "vector": {
  15. "type": "dense_vector",
  16. "dims": 2,
  17. "index_options": {
  18. "type": "hnsw"
  19. }
  20. },
  21. "text": {
  22. "type": "text",
  23. "index": false
  24. }
  25. }
  26. }
  27. }
  28. }
  29. }

上記のマッピングを使用すると、個々のパッセージテキストを保存しながら、複数のパッセージベクトルをインデックスできます。

Python

  1. resp = client.bulk(
  2. index="passage_vectors",
  3. refresh=True,
  4. operations=[
  5. {
  6. "index": {
  7. "_id": "1"
  8. }
  9. },
  10. {
  11. "full_text": "first paragraph another paragraph",
  12. "creation_time": "2019-05-04",
  13. "paragraph": [
  14. {
  15. "vector": [
  16. 0.45,
  17. 45
  18. ],
  19. "text": "first paragraph",
  20. "paragraph_id": "1"
  21. },
  22. {
  23. "vector": [
  24. 0.8,
  25. 0.6
  26. ],
  27. "text": "another paragraph",
  28. "paragraph_id": "2"
  29. }
  30. ]
  31. },
  32. {
  33. "index": {
  34. "_id": "2"
  35. }
  36. },
  37. {
  38. "full_text": "number one paragraph number two paragraph",
  39. "creation_time": "2020-05-04",
  40. "paragraph": [
  41. {
  42. "vector": [
  43. 1.2,
  44. 4.5
  45. ],
  46. "text": "number one paragraph",
  47. "paragraph_id": "1"
  48. },
  49. {
  50. "vector": [
  51. -1,
  52. 42
  53. ],
  54. "text": "number two paragraph",
  55. "paragraph_id": "2"
  56. }
  57. ]
  58. }
  59. ],
  60. )
  61. print(resp)

Ruby

  1. response = client.bulk(
  2. index: 'passage_vectors',
  3. refresh: true,
  4. body: [
  5. {
  6. index: {
  7. _id: '1'
  8. }
  9. },
  10. {
  11. full_text: 'first paragraph another paragraph',
  12. creation_time: '2019-05-04',
  13. paragraph: [
  14. {
  15. vector: [
  16. 0.45,
  17. 45
  18. ],
  19. text: 'first paragraph',
  20. paragraph_id: '1'
  21. },
  22. {
  23. vector: [
  24. 0.8,
  25. 0.6
  26. ],
  27. text: 'another paragraph',
  28. paragraph_id: '2'
  29. }
  30. ]
  31. },
  32. {
  33. index: {
  34. _id: '2'
  35. }
  36. },
  37. {
  38. full_text: 'number one paragraph number two paragraph',
  39. creation_time: '2020-05-04',
  40. paragraph: [
  41. {
  42. vector: [
  43. 1.2,
  44. 4.5
  45. ],
  46. text: 'number one paragraph',
  47. paragraph_id: '1'
  48. },
  49. {
  50. vector: [
  51. -1,
  52. 42
  53. ],
  54. text: 'number two paragraph',
  55. paragraph_id: '2'
  56. }
  57. ]
  58. }
  59. ]
  60. )
  61. puts response

Js

  1. const response = await client.bulk({
  2. index: "passage_vectors",
  3. refresh: "true",
  4. operations: [
  5. {
  6. index: {
  7. _id: "1",
  8. },
  9. },
  10. {
  11. full_text: "first paragraph another paragraph",
  12. creation_time: "2019-05-04",
  13. paragraph: [
  14. {
  15. vector: [0.45, 45],
  16. text: "first paragraph",
  17. paragraph_id: "1",
  18. },
  19. {
  20. vector: [0.8, 0.6],
  21. text: "another paragraph",
  22. paragraph_id: "2",
  23. },
  24. ],
  25. },
  26. {
  27. index: {
  28. _id: "2",
  29. },
  30. },
  31. {
  32. full_text: "number one paragraph number two paragraph",
  33. creation_time: "2020-05-04",
  34. paragraph: [
  35. {
  36. vector: [1.2, 4.5],
  37. text: "number one paragraph",
  38. paragraph_id: "1",
  39. },
  40. {
  41. vector: [-1, 42],
  42. text: "number two paragraph",
  43. paragraph_id: "2",
  44. },
  45. ],
  46. },
  47. ],
  48. });
  49. console.log(response);

Console

  1. POST passage_vectors/_bulk?refresh=true
  2. { "index": { "_id": "1" } }
  3. { "full_text": "first paragraph another paragraph", "creation_time": "2019-05-04", "paragraph": [ { "vector": [ 0.45, 45 ], "text": "first paragraph", "paragraph_id": "1" }, { "vector": [ 0.8, 0.6 ], "text": "another paragraph", "paragraph_id": "2" } ] }
  4. { "index": { "_id": "2" } }
  5. { "full_text": "number one paragraph number two paragraph", "creation_time": "2020-05-04", "paragraph": [ { "vector": [ 1.2, 4.5 ], "text": "number one paragraph", "paragraph_id": "1" }, { "vector": [ -1, 42 ], "text": "number two paragraph", "paragraph_id": "2" } ] }

クエリは、典型的な kNN 検索に非常に似ているように見えます:

Python

  1. resp = client.search(
  2. index="passage_vectors",
  3. fields=[
  4. "full_text",
  5. "creation_time"
  6. ],
  7. source=False,
  8. knn={
  9. "query_vector": [
  10. 0.45,
  11. 45
  12. ],
  13. "field": "paragraph.vector",
  14. "k": 2,
  15. "num_candidates": 2
  16. },
  17. )
  18. print(resp)

Ruby

  1. response = client.search(
  2. index: 'passage_vectors',
  3. body: {
  4. fields: [
  5. 'full_text',
  6. 'creation_time'
  7. ],
  8. _source: false,
  9. knn: {
  10. query_vector: [
  11. 0.45,
  12. 45
  13. ],
  14. field: 'paragraph.vector',
  15. k: 2,
  16. num_candidates: 2
  17. }
  18. }
  19. )
  20. puts response

Js

  1. const response = await client.search({
  2. index: "passage_vectors",
  3. fields: ["full_text", "creation_time"],
  4. _source: false,
  5. knn: {
  6. query_vector: [0.45, 45],
  7. field: "paragraph.vector",
  8. k: 2,
  9. num_candidates: 2,
  10. },
  11. });
  12. console.log(response);

Console

  1. POST passage_vectors/_search
  2. {
  3. "fields": ["full_text", "creation_time"],
  4. "_source": false,
  5. "knn": {
  6. "query_vector": [
  7. 0.45,
  8. 45
  9. ],
  10. "field": "paragraph.vector",
  11. "k": 2,
  12. "num_candidates": 2
  13. }
  14. }

以下に注意してください。合計 4 つのベクトルがあるにもかかわらず、2 つのドキュメントが返されます。ネストされた dense_vectors に対する kNN 検索は、常にトップレベルのドキュメントに対してトップ結果を多様化します。つまり、"k" トップレベルのドキュメントが返され、最も近いパッセージベクトル (例: "paragraph.vector") によってスコア付けされます。

Console-Result

  1. {
  2. "took": 4,
  3. "timed_out": false,
  4. "_shards": {
  5. "total": 1,
  6. "successful": 1,
  7. "skipped": 0,
  8. "failed": 0
  9. },
  10. "hits": {
  11. "total": {
  12. "value": 2,
  13. "relation": "eq"
  14. },
  15. "max_score": 1.0,
  16. "hits": [
  17. {
  18. "_index": "passage_vectors",
  19. "_id": "1",
  20. "_score": 1.0,
  21. "fields": {
  22. "creation_time": [
  23. "2019-05-04T00:00:00.000Z"
  24. ],
  25. "full_text": [
  26. "first paragraph another paragraph"
  27. ]
  28. }
  29. },
  30. {
  31. "_index": "passage_vectors",
  32. "_id": "2",
  33. "_score": 0.9997144,
  34. "fields": {
  35. "creation_time": [
  36. "2020-05-04T00:00:00.000Z"
  37. ],
  38. "full_text": [
  39. "number one paragraph number two paragraph"
  40. ]
  41. }
  42. }
  43. ]
  44. }
  45. }

トップレベルのドキュメントメタデータでフィルタリングしたい場合は、filterknn 句に追加することでこれを行うことができます。

filter は常にトップレベルのドキュメントメタデータに対して行われます。これは、nested フィールドメタデータに基づいてフィルタリングできないことを意味します。

Python

  1. resp = client.search(
  2. index="passage_vectors",
  3. fields=[
  4. "creation_time",
  5. "full_text"
  6. ],
  7. source=False,
  8. knn={
  9. "query_vector": [
  10. 0.45,
  11. 45
  12. ],
  13. "field": "paragraph.vector",
  14. "k": 2,
  15. "num_candidates": 2,
  16. "filter": {
  17. "bool": {
  18. "filter": [
  19. {
  20. "range": {
  21. "creation_time": {
  22. "gte": "2019-05-01",
  23. "lte": "2019-05-05"
  24. }
  25. }
  26. }
  27. ]
  28. }
  29. }
  30. },
  31. )
  32. print(resp)

Ruby

  1. response = client.search(
  2. index: 'passage_vectors',
  3. body: {
  4. fields: [
  5. 'creation_time',
  6. 'full_text'
  7. ],
  8. _source: false,
  9. knn: {
  10. query_vector: [
  11. 0.45,
  12. 45
  13. ],
  14. field: 'paragraph.vector',
  15. k: 2,
  16. num_candidates: 2,
  17. filter: {
  18. bool: {
  19. filter: [
  20. {
  21. range: {
  22. creation_time: {
  23. gte: '2019-05-01',
  24. lte: '2019-05-05'
  25. }
  26. }
  27. }
  28. ]
  29. }
  30. }
  31. }
  32. }
  33. )
  34. puts response

Js

  1. const response = await client.search({
  2. index: "passage_vectors",
  3. fields: ["creation_time", "full_text"],
  4. _source: false,
  5. knn: {
  6. query_vector: [0.45, 45],
  7. field: "paragraph.vector",
  8. k: 2,
  9. num_candidates: 2,
  10. filter: {
  11. bool: {
  12. filter: [
  13. {
  14. range: {
  15. creation_time: {
  16. gte: "2019-05-01",
  17. lte: "2019-05-05",
  18. },
  19. },
  20. },
  21. ],
  22. },
  23. },
  24. },
  25. });
  26. console.log(response);

Console

  1. POST passage_vectors/_search
  2. {
  3. "fields": [
  4. "creation_time",
  5. "full_text"
  6. ],
  7. "_source": false,
  8. "knn": {
  9. "query_vector": [
  10. 0.45,
  11. 45
  12. ],
  13. "field": "paragraph.vector",
  14. "k": 2,
  15. "num_candidates": 2,
  16. "filter": {
  17. "bool": {
  18. "filter": [
  19. {
  20. "range": {
  21. "creation_time": {
  22. "gte": "2019-05-01",
  23. "lte": "2019-05-05"
  24. }
  25. }
  26. }
  27. ]
  28. }
  29. }
  30. }
  31. }

現在、トップレベルの "creation_time" に基づいてフィルタリングされており、その範囲内に該当するドキュメントは 1 つだけです。

Console-Result

  1. {
  2. "took": 4,
  3. "timed_out": false,
  4. "_shards": {
  5. "total": 1,
  6. "successful": 1,
  7. "skipped": 0,
  8. "failed": 0
  9. },
  10. "hits": {
  11. "total": {
  12. "value": 1,
  13. "relation": "eq"
  14. },
  15. "max_score": 1.0,
  16. "hits": [
  17. {
  18. "_index": "passage_vectors",
  19. "_id": "1",
  20. "_score": 1.0,
  21. "fields": {
  22. "creation_time": [
  23. "2019-05-04T00:00:00.000Z"
  24. ],
  25. "full_text": [
  26. "first paragraph another paragraph"
  27. ]
  28. }
  29. }
  30. ]
  31. }
  32. }

Nested kNN Search with Inner hits

さらに、一致したドキュメントの最近のパッセージを抽出したい場合は、knn 句に inner_hits を提供できます。

inner_hits と複数の knn 句を使用する場合は、inner_hits.name フィールドを指定してください。そうしないと、名前の衝突が発生し、検索リクエストが失敗する可能性があります。

Python

  1. resp = client.search(
  2. index="passage_vectors",
  3. fields=[
  4. "creation_time",
  5. "full_text"
  6. ],
  7. source=False,
  8. knn={
  9. "query_vector": [
  10. 0.45,
  11. 45
  12. ],
  13. "field": "paragraph.vector",
  14. "k": 2,
  15. "num_candidates": 2,
  16. "inner_hits": {
  17. "_source": False,
  18. "fields": [
  19. "paragraph.text"
  20. ],
  21. "size": 1
  22. }
  23. },
  24. )
  25. print(resp)

Js

  1. const response = await client.search({
  2. index: "passage_vectors",
  3. fields: ["creation_time", "full_text"],
  4. _source: false,
  5. knn: {
  6. query_vector: [0.45, 45],
  7. field: "paragraph.vector",
  8. k: 2,
  9. num_candidates: 2,
  10. inner_hits: {
  11. _source: false,
  12. fields: ["paragraph.text"],
  13. size: 1,
  14. },
  15. },
  16. });
  17. console.log(response);

Console

  1. POST passage_vectors/_search
  2. {
  3. "fields": [
  4. "creation_time",
  5. "full_text"
  6. ],
  7. "_source": false,
  8. "knn": {
  9. "query_vector": [
  10. 0.45,
  11. 45
  12. ],
  13. "field": "paragraph.vector",
  14. "k": 2,
  15. "num_candidates": 2,
  16. "inner_hits": {
  17. "_source": false,
  18. "fields": [
  19. "paragraph.text"
  20. ],
  21. "size": 1
  22. }
  23. }
  24. }

検索時に最近見つかった段落が結果に含まれます。

Console-Result

  1. {
  2. "took": 4,
  3. "timed_out": false,
  4. "_shards": {
  5. "total": 1,
  6. "successful": 1,
  7. "skipped": 0,
  8. "failed": 0
  9. },
  10. "hits": {
  11. "total": {
  12. "value": 2,
  13. "relation": "eq"
  14. },
  15. "max_score": 1.0,
  16. "hits": [
  17. {
  18. "_index": "passage_vectors",
  19. "_id": "1",
  20. "_score": 1.0,
  21. "fields": {
  22. "creation_time": [
  23. "2019-05-04T00:00:00.000Z"
  24. ],
  25. "full_text": [
  26. "first paragraph another paragraph"
  27. ]
  28. },
  29. "inner_hits": {
  30. "paragraph": {
  31. "hits": {
  32. "total": {
  33. "value": 2,
  34. "relation": "eq"
  35. },
  36. "max_score": 1.0,
  37. "hits": [
  38. {
  39. "_index": "passage_vectors",
  40. "_id": "1",
  41. "_nested": {
  42. "field": "paragraph",
  43. "offset": 0
  44. },
  45. "_score": 1.0,
  46. "fields": {
  47. "paragraph": [
  48. {
  49. "text": [
  50. "first paragraph"
  51. ]
  52. }
  53. ]
  54. }
  55. }
  56. ]
  57. }
  58. }
  59. }
  60. },
  61. {
  62. "_index": "passage_vectors",
  63. "_id": "2",
  64. "_score": 0.9997144,
  65. "fields": {
  66. "creation_time": [
  67. "2020-05-04T00:00:00.000Z"
  68. ],
  69. "full_text": [
  70. "number one paragraph number two paragraph"
  71. ]
  72. },
  73. "inner_hits": {
  74. "paragraph": {
  75. "hits": {
  76. "total": {
  77. "value": 2,
  78. "relation": "eq"
  79. },
  80. "max_score": 0.9997144,
  81. "hits": [
  82. {
  83. "_index": "passage_vectors",
  84. "_id": "2",
  85. "_nested": {
  86. "field": "paragraph",
  87. "offset": 1
  88. },
  89. "_score": 0.9997144,
  90. "fields": {
  91. "paragraph": [
  92. {
  93. "text": [
  94. "number two paragraph"
  95. ]
  96. }
  97. ]
  98. }
  99. }
  100. ]
  101. }
  102. }
  103. }
  104. }
  105. ]
  106. }
  107. }

Indexing considerations

近似 kNN 検索の場合、Elasticsearch は各セグメントの密なベクトル値を HNSW グラフ として保存します。近似 kNN 検索のためのベクトルのインデックス作成には、これらのグラフを構築するのにかかるコストのため、かなりの時間がかかる場合があります。インデックスおよびバルクリクエストのクライアントリクエストタイムアウトを増やす必要があるかもしれません。近似 kNN 調整ガイド には、インデックス作成パフォーマンスに関する重要なガイダンスと、インデックス構成が検索パフォーマンスにどのように影響するかが含まれています。

検索時の調整パラメータに加えて、HNSW アルゴリズムには、グラフの構築コスト、検索速度、および精度のトレードオフを持つインデックス時のパラメータがあります。dense_vector マッピングを設定する際に、これらのパラメータを調整するために index_options 引数を使用できます。

Python

  1. resp = client.indices.create(
  2. index="image-index",
  3. mappings={
  4. "properties": {
  5. "image-vector": {
  6. "type": "dense_vector",
  7. "dims": 3,
  8. "similarity": "l2_norm",
  9. "index_options": {
  10. "type": "hnsw",
  11. "m": 32,
  12. "ef_construction": 100
  13. }
  14. }
  15. }
  16. },
  17. )
  18. print(resp)

Ruby

  1. response = client.indices.create(
  2. index: 'image-index',
  3. body: {
  4. mappings: {
  5. properties: {
  6. "image-vector": {
  7. type: 'dense_vector',
  8. dims: 3,
  9. similarity: 'l2_norm',
  10. index_options: {
  11. type: 'hnsw',
  12. m: 32,
  13. ef_construction: 100
  14. }
  15. }
  16. }
  17. }
  18. }
  19. )
  20. puts response

Js

  1. const response = await client.indices.create({
  2. index: "image-index",
  3. mappings: {
  4. properties: {
  5. "image-vector": {
  6. type: "dense_vector",
  7. dims: 3,
  8. similarity: "l2_norm",
  9. index_options: {
  10. type: "hnsw",
  11. m: 32,
  12. ef_construction: 100,
  13. },
  14. },
  15. },
  16. },
  17. });
  18. console.log(response);

Console

  1. PUT image-index
  2. {
  3. "mappings": {
  4. "properties": {
  5. "image-vector": {
  6. "type": "dense_vector",
  7. "dims": 3,
  8. "similarity": "l2_norm",
  9. "index_options": {
  10. "type": "hnsw",
  11. "m": 32,
  12. "ef_construction": 100
  13. }
  14. }
  15. }
  16. }
  17. }
  • クロスクラスタ検索 で kNN 検索を使用する場合、ccs_minimize_roundtrips オプションはサポートされていません。
  • Elasticsearch は、効率的な kNN 検索をサポートするために HNSW アルゴリズム を使用します。ほとんどの kNN アルゴリズムと同様に、HNSW は結果の精度を犠牲にして検索速度を向上させる近似手法です。これは、返される結果が常に真の k 最近傍ではないことを意味します。

近似 kNN 検索は常に、シャード全体でグローバルなトップ k マッチを収集するために dfs_query_then_fetch 検索タイプを使用します。kNN 検索を実行する際に search_type を明示的に設定することはできません。

Exact kNN

正確な kNN 検索を実行するには、ベクトル関数を使用した script_score クエリを使用します。

  • 1. 1 つ以上の dense_vector フィールドを明示的にマッピングします。近似 kNN を使用するつもりがない場合は、index マッピングオプションを false に設定します。これにより、インデックス作成速度が大幅に向上する可能性があります。

Python

  1. resp = client.indices.create(
  2. index="product-index",
  3. mappings={
  4. "properties": {
  5. "product-vector": {
  6. "type": "dense_vector",
  7. "dims": 5,
  8. "index": False
  9. },
  10. "price": {
  11. "type": "long"
  12. }
  13. }
  14. },
  15. )
  16. print(resp)

Ruby

  1. response = client.indices.create(
  2. index: 'product-index',
  3. body: {
  4. mappings: {
  5. properties: {
  6. "product-vector": {
  7. type: 'dense_vector',
  8. dims: 5,
  9. index: false
  10. },
  11. price: {
  12. type: 'long'
  13. }
  14. }
  15. }
  16. }
  17. )
  18. puts response

Js

  1. const response = await client.indices.create({
  2. index: "product-index",
  3. mappings: {
  4. properties: {
  5. "product-vector": {
  6. type: "dense_vector",
  7. dims: 5,
  8. index: false,
  9. },
  10. price: {
  11. type: "long",
  12. },
  13. },
  14. },
  15. });
  16. console.log(response);

Console

  1. PUT product-index
  2. {
  3. "mappings": {
  4. "properties": {
  5. "product-vector": {
  6. "type": "dense_vector",
  7. "dims": 5,
  8. "index": false
  9. },
  10. "price": {
  11. "type": "long"
  12. }
  13. }
  14. }
  15. }
  • 2. データをインデックスします。

Console

  1. POST product-index/_bulk?refresh=true
  2. { "index": { "_id": "1" } }
  3. { "product-vector": [230.0, 300.33, -34.8988, 15.555, -200.0], "price": 1599 }
  4. { "index": { "_id": "2" } }
  5. { "product-vector": [-0.5, 100.0, -13.0, 14.8, -156.0], "price": 799 }
  6. { "index": { "_id": "3" } }
  7. { "product-vector": [0.5, 111.3, -13.0, 14.8, -156.0], "price": 1099 }
  8. ...
  • 3. 検索 API を使用して、ベクトル関数を含む script_score クエリを実行します。
    一致するドキュメントの数をベクトル関数に渡すのを制限するには、script_score.query パラメータにフィルタークエリを指定することをお勧めします。必要に応じて、このパラメータに match_all クエリ を使用してすべてのドキュメントに一致させることができます。ただし、すべてのドキュメントに一致させると、検索の遅延が大幅に増加する可能性があります。

Python

  1. resp = client.search(
  2. index="product-index",
  3. query={
  4. "script_score": {
  5. "query": {
  6. "bool": {
  7. "filter": {
  8. "range": {
  9. "price": {
  10. "gte": 1000
  11. }
  12. }
  13. }
  14. }
  15. },
  16. "script": {
  17. "source": "cosineSimilarity(params.queryVector, 'product-vector') + 1.0",
  18. "params": {
  19. "queryVector": [
  20. -0.5,
  21. 90,
  22. -10,
  23. 14.8,
  24. -156
  25. ]
  26. }
  27. }
  28. }
  29. },
  30. )
  31. print(resp)

Ruby

  1. response = client.search(
  2. index: 'product-index',
  3. body: {
  4. query: {
  5. script_score: {
  6. query: {
  7. bool: {
  8. filter: {
  9. range: {
  10. price: {
  11. gte: 1000
  12. }
  13. }
  14. }
  15. }
  16. },
  17. script: {
  18. source: "cosineSimilarity(params.queryVector, 'product-vector') + 1.0",
  19. params: {
  20. "queryVector": [
  21. -0.5,
  22. 90,
  23. -10,
  24. 14.8,
  25. -156
  26. ]
  27. }
  28. }
  29. }
  30. }
  31. }
  32. )
  33. puts response

Js

  1. const response = await client.search({
  2. index: "product-index",
  3. query: {
  4. script_score: {
  5. query: {
  6. bool: {
  7. filter: {
  8. range: {
  9. price: {
  10. gte: 1000,
  11. },
  12. },
  13. },
  14. },
  15. },
  16. script: {
  17. source: "cosineSimilarity(params.queryVector, 'product-vector') + 1.0",
  18. params: {
  19. queryVector: [-0.5, 90, -10, 14.8, -156],
  20. },
  21. },
  22. },
  23. },
  24. });
  25. console.log(response);

Console

  1. POST product-index/_search
  2. {
  3. "query": {
  4. "script_score": {
  5. "query" : {
  6. "bool" : {
  7. "filter" : {
  8. "range" : {
  9. "price" : {
  10. "gte": 1000
  11. }
  12. }
  13. }
  14. }
  15. },
  16. "script": {
  17. "source": "cosineSimilarity(params.queryVector, 'product-vector') + 1.0",
  18. "params": {
  19. "queryVector": [-0.5, 90.0, -10, 14.8, -156.0]
  20. }
  21. }
  22. }
  23. }
  24. }