チュートリアル: デプロイされたモデルによるセマンティック検索

  • Elastic Stackでセマンティック検索を行う最も簡単な方法については、semantic_textのエンドツーエンドチュートリアルを参照してください。
  • このチュートリアルは、推論エンドポイントおよびsemantic_textフィールドタイプが導入される前に書かれました。今日では、セマンティック検索を行うためのより簡単なオプションがあります。

このガイドでは、Elasticsearchにデプロイされたモデルを使用してセマンティック検索を実装する方法を示します: NLPモデルの選択からクエリの作成まで。

NLPモデルの選択

Elasticsearchは、幅広いNLPモデルの使用を提供しており、密なベクトルモデルと疎なベクトルモデルの両方が含まれています。言語モデルの選択は、セマンティック検索を成功させるために重要です。

独自のテキスト埋め込みモデルを持ち込むことも可能ですが、モデルの調整を通じて良好な検索結果を得ることは難しいです。サードパーティモデルリストから適切なモデルを選択することが最初のステップです。独自のデータでモデルをトレーニングすることは、BM25のみを使用するよりも良い検索結果を保証するために不可欠です。しかし、モデルのトレーニングプロセスにはデータサイエンティストやML専門家のチームが必要であり、高価で時間がかかります。

この問題に対処するために、ElasticはElastic Learned Sparse EncodeR (ELSER)という事前トレーニングされた表現モデルを提供しています。ELSERは現在英語のみで利用可能で、ファインチューニングを必要としないドメイン外の疎なベクトルモデルです。この適応性により、さまざまなNLPユースケースにすぐに適用できます。ML専門家のチームがない限り、ELSERモデルの使用を強く推奨します。

疎なベクトル表現の場合、ベクトルは主にゼロ値で構成されており、非ゼロ値を含む小さなサブセットのみがあります。この表現はテキストデータに一般的に使用されます。ELSERの場合、インデックス内の各ドキュメントとクエリテキスト自体は高次元の疎なベクトルで表現されます。ベクトルの各非ゼロ要素は、モデルの語彙内の用語に対応します。ELSERの語彙には約30000の用語が含まれているため、ELSERによって作成された疎なベクトルには約30000の値が含まれ、その大部分はゼロです。実際、ELSERモデルは元のクエリ内の用語を、トレーニングデータセット内の元の検索用語と最も一致するドキュメントに存在することが学習された他の用語に置き換え、各用語の重要性を制御する重みを持っています。

モデルのデプロイ

セマンティック検索を実装するために使用するモデルを決定したら、そのモデルをElasticsearchにデプロイする必要があります。

ELSERをデプロイするには、ELSERのダウンロードとデプロイを参照してください。

サードパーティのテキスト埋め込みモデルをデプロイするには、テキスト埋め込みモデルのデプロイを参照してください。

テキスト埋め込みのためのフィールドのマッピング

デプロイされたモデルを使用して入力テキストに基づいて埋め込みを生成する前に、まずインデックスマッピングを準備する必要があります。インデックスのマッピングはモデルのタイプによって異なります。

ELSERは、入力テキストとクエリからトークン-重みペアを出力します。Elasticsearchのsparse_vectorフィールドタイプは、これらのトークン-重みペアを数値特徴ベクトルとして保存できます。インデックスには、ELSERが生成するトークンをインデックスするためにsparse_vectorフィールドタイプのフィールドが必要です。

ELSERインデックスのマッピングを作成するには、チュートリアルのインデックスマッピングの作成セクションを参照してください。この例では、my-indexのインデックスマッピングを作成する方法が示されており、my_embeddings.tokensフィールド - ELSERの出力を含む - をsparse_vectorフィールドとして定義します。

Python

  1. resp = client.indices.create(
  2. index="my-index",
  3. mappings={
  4. "properties": {
  5. "my_tokens": {
  6. "type": "sparse_vector"
  7. },
  8. "my_text_field": {
  9. "type": "text"
  10. }
  11. }
  12. },
  13. )
  14. print(resp)

Ruby

  1. response = client.indices.create(
  2. index: 'my-index',
  3. body: {
  4. mappings: {
  5. properties: {
  6. my_tokens: {
  7. type: 'sparse_vector'
  8. },
  9. my_text_field: {
  10. type: 'text'
  11. }
  12. }
  13. }
  14. }
  15. )
  16. puts response

Js

  1. const response = await client.indices.create({
  2. index: "my-index",
  3. mappings: {
  4. properties: {
  5. my_tokens: {
  6. type: "sparse_vector",
  7. },
  8. my_text_field: {
  9. type: "text",
  10. },
  11. },
  12. },
  13. });
  14. console.log(response);

コンソール

  1. PUT my-index
  2. {
  3. "mappings": {
  4. "properties": {
  5. "my_tokens": {
  6. "type": "sparse_vector"
  7. },
  8. "my_text_field": {
  9. "type": "text"
  10. }
  11. }
  12. }
  13. }
ELSERによって生成されるトークンを含むフィールドの名前。
トークンを含むフィールドはsparse_vectorフィールドでなければなりません。
疎なベクトル表現を作成するためのフィールドの名前。
この例では、フィールドの名前はmy_text_fieldです。
この例ではフィールドタイプはtextです。

Elasticsearch NLPと互換性のあるモデルは、出力として密なベクトルを生成します。dense_vectorフィールドタイプは、数値値の密なベクトルを保存するのに適しています。インデックスには、選択したサードパーティモデルが生成する埋め込みをインデックスするためにdense_vectorフィールドタイプのフィールドが必要です。モデルは特定の次元数の埋め込みを生成することを考慮してください。dense_vectorフィールドは、dimsオプションを使用して同じ次元数に設定する必要があります。埋め込みの次元数に関する情報は、各モデルのドキュメントを参照してください。

NLPモデルのインデックスのマッピングを確認するには、チュートリアルのテキスト埋め込みモデルをインジェスト推論パイプラインに追加するセクションのマッピングコードスニペットを参照してください。この例では、my_embeddings.predicted_valueフィールド - モデル出力を含む - をdense_vectorフィールドとして定義するインデックスマッピングを作成する方法が示されています。

Python

  1. resp = client.indices.create(
  2. index="my-index",
  3. mappings={
  4. "properties": {
  5. "my_embeddings.predicted_value": {
  6. "type": "dense_vector",
  7. "dims": 384
  8. },
  9. "my_text_field": {
  10. "type": "text"
  11. }
  12. }
  13. },
  14. )
  15. print(resp)

Ruby

  1. response = client.indices.create(
  2. index: 'my-index',
  3. body: {
  4. mappings: {
  5. properties: {
  6. 'my_embeddings.predicted_value' => {
  7. type: 'dense_vector',
  8. dims: 384
  9. },
  10. my_text_field: {
  11. type: 'text'
  12. }
  13. }
  14. }
  15. }
  16. )
  17. puts response

Js

  1. const response = await client.indices.create({
  2. index: "my-index",
  3. mappings: {
  4. properties: {
  5. "my_embeddings.predicted_value": {
  6. type: "dense_vector",
  7. dims: 384,
  8. },
  9. my_text_field: {
  10. type: "text",
  11. },
  12. },
  13. },
  14. });
  15. console.log(response);

コンソール

  1. PUT my-index
  2. {
  3. "mappings": {
  4. "properties": {
  5. "my_embeddings.predicted_value": {
  6. "type": "dense_vector",
  7. "dims": 384
  8. },
  9. "my_text_field": {
  10. "type": "text"
  11. }
  12. }
  13. }
  14. }
モデルによって生成される埋め込みを含むフィールドの名前。
埋め込みを含むフィールドはdense_vectorフィールドでなければなりません。
モデルは特定の次元数の埋め込みを生成します。dense_vectorフィールドは、dimsオプションによって同じ次元数に設定する必要があります。埋め込みの次元数に関する情報は、各モデルのドキュメントを参照してください。
密なベクトル表現を作成するためのフィールドの名前。
この例では、フィールドの名前はmy_text_fieldです。
この例ではフィールドタイプはtextです。

テキスト埋め込みの生成

インデックスのマッピングを作成したら、入力テキストからテキスト埋め込みを生成できます。これは、インジェストパイプラインを使用して推論プロセッサを使用することで行えます。インジェストパイプラインは、入力データを処理し、目的のインデックスにインデックスします。インデックス時に、推論インジェストプロセッサは、パイプラインを通じて取り込まれたデータに対してトレーニングされたモデルを使用して推論を行います。推論プロセッサを使用してインジェストパイプラインを作成した後、データを通じて取り込んでモデル出力を生成できます。

これは、ELSERモデルを使用するインジェストパイプラインの作成方法です:

Python

  1. resp = client.ingest.put_pipeline(
  2. id="my-text-embeddings-pipeline",
  3. description="Text embedding pipeline",
  4. processors=[
  5. {
  6. "inference": {
  7. "model_id": ".elser_model_2",
  8. "input_output": [
  9. {
  10. "input_field": "my_text_field",
  11. "output_field": "my_tokens"
  12. }
  13. ]
  14. }
  15. }
  16. ],
  17. )
  18. print(resp)

Ruby

  1. response = client.ingest.put_pipeline(
  2. id: 'my-text-embeddings-pipeline',
  3. body: {
  4. description: 'Text embedding pipeline',
  5. processors: [
  6. {
  7. inference: {
  8. model_id: '.elser_model_2',
  9. input_output: [
  10. {
  11. input_field: 'my_text_field',
  12. output_field: 'my_tokens'
  13. }
  14. ]
  15. }
  16. }
  17. ]
  18. }
  19. )
  20. puts response

Js

  1. const response = await client.ingest.putPipeline({
  2. id: "my-text-embeddings-pipeline",
  3. description: "Text embedding pipeline",
  4. processors: [
  5. {
  6. inference: {
  7. model_id: ".elser_model_2",
  8. input_output: [
  9. {
  10. input_field: "my_text_field",
  11. output_field: "my_tokens",
  12. },
  13. ],
  14. },
  15. },
  16. ],
  17. });
  18. console.log(response);

コンソール

  1. PUT _ingest/pipeline/my-text-embeddings-pipeline
  2. {
  3. "description": "Text embedding pipeline",
  4. "processors": [
  5. {
  6. "inference": {
  7. "model_id": ".elser_model_2",
  8. "input_output": [
  9. {
  10. "input_field": "my_text_field",
  11. "output_field": "my_tokens"
  12. }
  13. ]
  14. }
  15. }
  16. ]
  17. }
推論プロセスのためのinput_fieldを定義し、推論結果を含むoutput_fieldを定義する構成オブジェクト。

ELSERを使用してトークンを生成するためにデータをパイプラインを通じて取り込むには、チュートリアルの推論インジェストパイプラインを通じてデータを取り込むセクションを参照してください。パイプラインを使用してドキュメントを正常に取り込んだ後、インデックスにはELSERによって生成されたトークンが含まれます。トークンは関連性を捉える学習された関連付けであり、同義語ではありません。トークンについての詳細はこのページを参照してください。

これは、テキスト埋め込みモデルを使用するインジェストパイプラインの作成方法です:

Python

  1. resp = client.ingest.put_pipeline(
  2. id="my-text-embeddings-pipeline",
  3. description="Text embedding pipeline",
  4. processors=[
  5. {
  6. "inference": {
  7. "model_id": "sentence-transformers__msmarco-minilm-l-12-v3",
  8. "target_field": "my_embeddings",
  9. "field_map": {
  10. "my_text_field": "text_field"
  11. }
  12. }
  13. }
  14. ],
  15. )
  16. print(resp)

Ruby

  1. response = client.ingest.put_pipeline(
  2. id: 'my-text-embeddings-pipeline',
  3. body: {
  4. description: 'Text embedding pipeline',
  5. processors: [
  6. {
  7. inference: {
  8. model_id: 'sentence-transformers__msmarco-minilm-l-12-v3',
  9. target_field: 'my_embeddings',
  10. field_map: {
  11. my_text_field: 'text_field'
  12. }
  13. }
  14. }
  15. ]
  16. }
  17. )
  18. puts response

Js

  1. const response = await client.ingest.putPipeline({
  2. id: "my-text-embeddings-pipeline",
  3. description: "Text embedding pipeline",
  4. processors: [
  5. {
  6. inference: {
  7. model_id: "sentence-transformers__msmarco-minilm-l-12-v3",
  8. target_field: "my_embeddings",
  9. field_map: {
  10. my_text_field: "text_field",
  11. },
  12. },
  13. },
  14. ],
  15. });
  16. console.log(response);

コンソール

  1. PUT _ingest/pipeline/my-text-embeddings-pipeline
  2. {
  3. "description": "Text embedding pipeline",
  4. "processors": [
  5. {
  6. "inference": {
  7. "model_id": "sentence-transformers__msmarco-minilm-l-12-v3",
  8. "target_field": "my_embeddings",
  9. "field_map": {
  10. "my_text_field": "text_field"
  11. }
  12. }
  13. }
  14. ]
  15. }
使用したいテキスト埋め込みモデルのモデルID。
field_mapオブジェクトは、入力ドキュメントフィールド名(この例ではmy_text_field)をモデルが期待するフィールドの名前(常にtext_field)にマッピングします。

選択したモデルでテキスト埋め込みを生成するためにデータをパイプラインを通じて取り込むには、推論インジェストパイプラインにテキスト埋め込みモデルを追加するセクションを参照してください。この例では、推論プロセッサを使用してパイプラインを作成し、パイプラインを通じてデータを再インデックスする方法が示されています。パイプラインを使用してドキュメントを正常に取り込んだ後、インデックスにはモデルによって生成されたテキスト埋め込みが含まれます。

さあ、セマンティック検索を行う時です!

データの検索

デプロイしたモデルのタイプに応じて、疎なベクトルクエリで特徴をランク付けするか、kNN検索で密なベクトルを使用できます。

ELSERテキスト埋め込みは、疎なベクトルクエリを使用してクエリできます。疎なベクトルクエリを使用すると、使用したいNLPモデルに関連付けられた推論IDとクエリテキストを提供することで、疎なベクトルフィールドをクエリできます:

Python

  1. resp = client.search(
  2. index="my-index",
  3. query={
  4. "sparse_vector": {
  5. "field": "my_tokens",
  6. "inference_id": "my-elser-endpoint",
  7. "query": "the query string"
  8. }
  9. },
  10. )
  11. print(resp)

Js

  1. const response = await client.search({
  2. index: "my-index",
  3. query: {
  4. sparse_vector: {
  5. field: "my_tokens",
  6. inference_id: "my-elser-endpoint",
  7. query: "the query string",
  8. },
  9. },
  10. });
  11. console.log(response);

コンソール

  1. GET my-index/_search
  2. {
  3. "query":{
  4. "sparse_vector": {
  5. "field": "my_tokens",
  6. "inference_id": "my-elser-endpoint",
  7. "query": "the query string"
  8. }
  9. }
  10. }

テキスト埋め込みは、密なベクトルモデルによって生成され、kNN検索を使用してクエリできます。knn句では、密なベクトルフィールドの名前を提供し、query_vector_builder句ではモデルIDとクエリテキストを提供します。

Python

  1. resp = client.search(
  2. index="my-index",
  3. knn={
  4. "field": "my_embeddings.predicted_value",
  5. "k": 10,
  6. "num_candidates": 100,
  7. "query_vector_builder": {
  8. "text_embedding": {
  9. "model_id": "sentence-transformers__msmarco-minilm-l-12-v3",
  10. "model_text": "the query string"
  11. }
  12. }
  13. },
  14. )
  15. print(resp)

Ruby

  1. response = client.search(
  2. index: 'my-index',
  3. body: {
  4. knn: {
  5. field: 'my_embeddings.predicted_value',
  6. k: 10,
  7. num_candidates: 100,
  8. query_vector_builder: {
  9. text_embedding: {
  10. model_id: 'sentence-transformers__msmarco-minilm-l-12-v3',
  11. model_text: 'the query string'
  12. }
  13. }
  14. }
  15. }
  16. )
  17. puts response

Js

  1. const response = await client.search({
  2. index: "my-index",
  3. knn: {
  4. field: "my_embeddings.predicted_value",
  5. k: 10,
  6. num_candidates: 100,
  7. query_vector_builder: {
  8. text_embedding: {
  9. model_id: "sentence-transformers__msmarco-minilm-l-12-v3",
  10. model_text: "the query string",
  11. },
  12. },
  13. },
  14. });
  15. console.log(response);

コンソール

  1. GET my-index/_search
  2. {
  3. "knn": {
  4. "field": "my_embeddings.predicted_value",
  5. "k": 10,
  6. "num_candidates": 100,
  7. "query_vector_builder": {
  8. "text_embedding": {
  9. "model_id": "sentence-transformers__msmarco-minilm-l-12-v3",
  10. "model_text": "the query string"
  11. }
  12. }
  13. }
  14. }

ハイブリッド検索によるセマンティック検索の超越

いくつかの状況では、レキシカル検索がセマンティック検索よりも優れたパフォーマンスを発揮することがあります。たとえば、製品番号のような単語やIDを検索する場合です。

セマンティック検索とレキシカル検索を逆順位融合を使用して1つのハイブリッド検索リクエストに組み合わせることで、両方の利点を得ることができます。それだけでなく、逆順位融合を使用したハイブリッド検索は一般的により良いパフォーマンスを示すことが分かっています

セマンティッククエリとレキシカルクエリの間のハイブリッド検索は、検索リクエストの一部としてrrfリトリーバーを使用することで実現できます。sparse_vectorクエリと、standardリトリーバーとしての全文検索クエリを提供します。rrfリトリーバーは、逆順位融合を使用して上位のドキュメントをランク付けします。

Python

  1. resp = client.search(
  2. index="my-index",
  3. retriever={
  4. "rrf": {
  5. "retrievers": [
  6. {
  7. "standard": {
  8. "query": {
  9. "match": {
  10. "my_text_field": "the query string"
  11. }
  12. }
  13. }
  14. },
  15. {
  16. "standard": {
  17. "query": {
  18. "sparse_vector": {
  19. "field": "my_tokens",
  20. "inference_id": "my-elser-endpoint",
  21. "query": "the query string"
  22. }
  23. }
  24. }
  25. }
  26. ]
  27. }
  28. },
  29. )
  30. print(resp)

Js

  1. const response = await client.search({
  2. index: "my-index",
  3. retriever: {
  4. rrf: {
  5. retrievers: [
  6. {
  7. standard: {
  8. query: {
  9. match: {
  10. my_text_field: "the query string",
  11. },
  12. },
  13. },
  14. },
  15. {
  16. standard: {
  17. query: {
  18. sparse_vector: {
  19. field: "my_tokens",
  20. inference_id: "my-elser-endpoint",
  21. query: "the query string",
  22. },
  23. },
  24. },
  25. },
  26. ],
  27. },
  28. },
  29. });
  30. console.log(response);

コンソール

  1. GET my-index/_search
  2. {
  3. "retriever": {
  4. "rrf": {
  5. "retrievers": [
  6. {
  7. "standard": {
  8. "query": {
  9. "match": {
  10. "my_text_field": "the query string"
  11. }
  12. }
  13. }
  14. },
  15. {
  16. "standard": {
  17. "query": {
  18. "sparse_vector": {
  19. "field": "my_tokens",
  20. "inference_id": "my-elser-endpoint",
  21. "query": "the query string"
  22. }
  23. }
  24. }
  25. }
  26. ]
  27. }
  28. }
  29. }

セマンティッククエリとレキシカルクエリの間のハイブリッド検索は、次のように提供することで実現できます:

  • 逆順位融合を使用して上位のドキュメントをランク付けするためのrrfリトリーバー
  • standardリトリーバーを子リトリーバーとしてquery句を使用して全文検索クエリ
  • knnリトリーバーを子リトリーバーとして、密なベクトルフィールドをクエリするkNN検索

Python

  1. resp = client.search(
  2. index="my-index",
  3. retriever={
  4. "rrf": {
  5. "retrievers": [
  6. {
  7. "standard": {
  8. "query": {
  9. "match": {
  10. "my_text_field": "the query string"
  11. }
  12. }
  13. }
  14. },
  15. {
  16. "knn": {
  17. "field": "text_embedding.predicted_value",
  18. "k": 10,
  19. "num_candidates": 100,
  20. "query_vector_builder": {
  21. "text_embedding": {
  22. "model_id": "sentence-transformers__msmarco-minilm-l-12-v3",
  23. "model_text": "the query string"
  24. }
  25. }
  26. }
  27. }
  28. ]
  29. }
  30. },
  31. )
  32. print(resp)

Js

  1. const response = await client.search({
  2. index: "my-index",
  3. retriever: {
  4. rrf: {
  5. retrievers: [
  6. {
  7. standard: {
  8. query: {
  9. match: {
  10. my_text_field: "the query string",
  11. },
  12. },
  13. },
  14. },
  15. {
  16. knn: {
  17. field: "text_embedding.predicted_value",
  18. k: 10,
  19. num_candidates: 100,
  20. query_vector_builder: {
  21. text_embedding: {
  22. model_id: "sentence-transformers__msmarco-minilm-l-12-v3",
  23. model_text: "the query string",
  24. },
  25. },
  26. },
  27. },
  28. ],
  29. },
  30. },
  31. });
  32. console.log(response);

コンソール

  1. GET my-index/_search
  2. {
  3. "retriever": {
  4. "rrf": {
  5. "retrievers": [
  6. {
  7. "standard": {
  8. "query": {
  9. "match": {
  10. "my_text_field": "the query string"
  11. }
  12. }
  13. }
  14. },
  15. {
  16. "knn": {
  17. "field": "text_embedding.predicted_value",
  18. "k": 10,
  19. "num_candidates": 100,
  20. "query_vector_builder": {
  21. "text_embedding": {
  22. "model_id": "sentence-transformers__msmarco-minilm-l-12-v3",
  23. "model_text": "the query string"
  24. }
  25. }
  26. }
  27. }
  28. ]
  29. }
  30. }
  31. }