チュートリアル: ELSERを用いたセマンティックサーチ

Elastic Learned Sparse EncodeR - またはELSER - は、ElasticによってトレーニングされたNLPモデルで、スパースベクトル表現を使用してセマンティックサーチを実行することを可能にします。検索用語の文字通りの一致ではなく、セマンティックサーチは検索クエリの意図と文脈的意味に基づいて結果を取得します。

このチュートリアルの指示では、ELSERを使用してデータに対してセマンティックサーチを実行する方法を示します。

Elastic Stackでセマンティックサーチを最も簡単に実行する方法については、semantic_textのエンドツーエンドチュートリアルを参照してください。

ELSERを使用したセマンティックサーチでは、各フィールドから抽出された最初の512トークンのみが考慮されます。詳細については、このページを参照してください。

要件

ELSERを使用してセマンティックサーチを実行するには、クラスターにNLPモデルがデプロイされている必要があります。モデルのダウンロードとデプロイ方法については、ELSERドキュメントを参照してください。

ELSERモデルをデプロイして使用するための最小限の専用MLノードサイズは、Elasticsearch Serviceで4GBです。デプロイメントのオートスケーリングがオフの場合、オートスケーリングをオンにすることをお勧めします。これにより、需要に基づいてリソースを動的に調整できます。より多くの割り当てや、割り当てごとのスレッド数を増やすことで、より良いパフォーマンスが得られますが、これにはより大きなMLノードが必要です。オートスケーリングは、必要に応じてより大きなノードを提供します。オートスケーリングがオフの場合は、適切なサイズのノードを自分で提供する必要があります。

インデックスマッピングの作成

まず、モデルがあなたのテキストに基づいて作成したトークンを含む宛先インデックスのマッピングを作成する必要があります。宛先インデックスには、ELSER出力をインデックスするためにsparse_vectorまたはrank_featuresフィールドタイプを持つフィールドが必要です。

ELSER出力は、sparse_vectorまたはrank_featuresフィールドタイプのフィールドに取り込まれる必要があります。そうしないと、Elasticsearchはトークン-重みペアをドキュメント内の大量のフィールドとして解釈します。このようなエラーが発生した場合: "Limit of total fields [1000] has been exceeded while adding new fields"、ELSER出力フィールドが正しくマッピングされておらず、フィールドタイプがsparse_vectorまたはrank_featuresとは異なります。

Python

  1. resp = client.indices.create(
  2. index="my-index",
  3. mappings={
  4. "properties": {
  5. "content_embedding": {
  6. "type": "sparse_vector"
  7. },
  8. "content": {
  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. content_embedding: {
  7. type: 'sparse_vector'
  8. },
  9. content: {
  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. content_embedding: {
  6. type: "sparse_vector",
  7. },
  8. content: {
  9. type: "text",
  10. },
  11. },
  12. },
  13. });
  14. console.log(response);

コンソール

  1. PUT my-index
  2. {
  3. "mappings": {
  4. "properties": {
  5. "content_embedding": {
  6. "type": "sparse_vector"
  7. },
  8. "content": {
  9. "type": "text"
  10. }
  11. }
  12. }
  13. }
生成されたトークンを含むフィールドの名前です。
それは次のステップの推論パイプライン構成で参照される必要があります。
トークンを含むフィールドはsparse_vectorフィールドです。
スパースベクトル表現を作成するためのフィールドの名前です。
この例では、フィールドの名前はcontentです。
それは次のステップの推論パイプライン構成で参照される必要があります。
この例ではテキストのフィールドタイプです。

スペースを最適化する方法については、ELSERトークンをドキュメントソースから除外してディスクスペースを節約するセクションを参照してください。

推論プロセッサを使用したインジェストパイプラインの作成

ELSERを使用して、パイプラインで取り込まれるデータに対して推論を行うインジェストパイプラインを作成します。

Python

  1. resp = client.ingest.put_pipeline(
  2. id="elser-v2-test",
  3. processors=[
  4. {
  5. "inference": {
  6. "model_id": ".elser_model_2",
  7. "input_output": [
  8. {
  9. "input_field": "content",
  10. "output_field": "content_embedding"
  11. }
  12. ]
  13. }
  14. }
  15. ],
  16. )
  17. print(resp)

Ruby

  1. response = client.ingest.put_pipeline(
  2. id: 'elser-v2-test',
  3. body: {
  4. processors: [
  5. {
  6. inference: {
  7. model_id: '.elser_model_2',
  8. input_output: [
  9. {
  10. input_field: 'content',
  11. output_field: 'content_embedding'
  12. }
  13. ]
  14. }
  15. }
  16. ]
  17. }
  18. )
  19. puts response

Js

  1. const response = await client.ingest.putPipeline({
  2. id: "elser-v2-test",
  3. processors: [
  4. {
  5. inference: {
  6. model_id: ".elser_model_2",
  7. input_output: [
  8. {
  9. input_field: "content",
  10. output_field: "content_embedding",
  11. },
  12. ],
  13. },
  14. },
  15. ],
  16. });
  17. console.log(response);

コンソール

  1. PUT _ingest/pipeline/elser-v2-test
  2. {
  3. "processors": [
  4. {
  5. "inference": {
  6. "model_id": ".elser_model_2",
  7. "input_output": [
  8. {
  9. "input_field": "content",
  10. "output_field": "content_embedding"
  11. }
  12. ]
  13. }
  14. }
  15. ]
  16. }
推論プロセスのためのinput_fieldを定義する構成オブジェクトと、推論結果を含むoutput_field

データのロード

このステップでは、後で推論インジェストパイプラインでトークンを抽出するために使用するデータをロードします。

MS MARCO Passage Rankingデータセットのサブセットであるmsmarco-passagetest2019-top1000データセットを使用します。これは、200のクエリと、それぞれに関連するテキストパッセージのリストが付属しています。すべてのユニークなパッセージとそのIDは、そのデータセットから抽出され、tsvファイルにまとめられています。

  1. ファイルをダウンロードし、UIの[ファイルアップローダー](https://www.elastic.co/guide/en/kibana/8.15/connect-to-elasticsearch.html#upload-data-kibana)を使用してクラスターにアップロードします。データが分析された後、**設定を上書き**をクリックします。**フィールド名の編集**の下で、最初の列に`````id`````、2番目の列に`````content`````を割り当てます。**適用**をクリックし、次に**インポート**をクリックします。インデックスに`````test-data`````という名前を付け、**インポート**をクリックします。アップロードが完了すると、182,469ドキュメントを持つ`````test-data`````という名前のインデックスが表示されます。
  2. ## 推論インジェストパイプラインを通じてデータを取り込む
  3. ELSERを推論モデルとして使用する推論パイプラインを通じてデータを再インデックス化することで、テキストからトークンを作成します。
  4. #### Python
  5. ``````python
  6. resp = client.reindex(
  7. wait_for_completion=False,
  8. source={
  9. "index": "test-data",
  10. "size": 50
  11. },
  12. dest={
  13. "index": "my-index",
  14. "pipeline": "elser-v2-test"
  15. },
  16. )
  17. print(resp)
  18. `

Ruby

  1. response = client.reindex(
  2. wait_for_completion: false,
  3. body: {
  4. source: {
  5. index: 'test-data',
  6. size: 50
  7. },
  8. dest: {
  9. index: 'my-index',
  10. pipeline: 'elser-v2-test'
  11. }
  12. }
  13. )
  14. puts response

Js

  1. const response = await client.reindex({
  2. wait_for_completion: "false",
  3. source: {
  4. index: "test-data",
  5. size: 50,
  6. },
  7. dest: {
  8. index: "my-index",
  9. pipeline: "elser-v2-test",
  10. },
  11. });
  12. console.log(response);

コンソール

  1. POST _reindex?wait_for_completion=false
  2. {
  3. "source": {
  4. "index": "test-data",
  5. "size": 50
  6. },
  7. "dest": {
  8. "index": "my-index",
  9. "pipeline": "elser-v2-test"
  10. }
  11. }
再インデックス化のデフォルトバッチサイズは1000です。sizeを小さい数に減らすことで、再インデックス化プロセスの更新が迅速になり、進捗を密接に追跡し、早期にエラーを検出できます。

呼び出しは進捗を監視するためのタスクIDを返します:

Python

  1. resp = client.tasks.get(
  2. task_id="<task_id>",
  3. )
  4. print(resp)

Js

  1. const response = await client.tasks.get({
  2. task_id: "<task_id>",
  3. });
  4. console.log(response);

コンソール

  1. GET _tasks/<task_id>

トレーニング済みモデルUIを開き、ELSERの下のパイプラインタブを選択して進捗を追跡することもできます。

大規模なデータセットの再インデックス化には時間がかかる場合があります。このワークフローをデータセットのサブセットのみを使用してテストできます。再インデックス化プロセスをキャンセルし、再インデックス化されたサブセットの埋め込みのみを生成します。次のAPIリクエストは再インデックス化タスクをキャンセルします:

Python

  1. resp = client.tasks.cancel(
  2. task_id="<task_id>",
  3. )
  4. print(resp)

Js

  1. const response = await client.tasks.cancel({
  2. task_id: "<task_id>",
  3. });
  4. console.log(response);

コンソール

  1. POST _tasks/<task_id>/_cancel

スパースベクトルクエリを使用したセマンティックサーチ

セマンティックサーチを実行するには、sparse_vectorクエリを使用し、クエリテキストとELSERモデルに関連付けられた推論IDを提供します。以下の例では、クエリテキスト「ランニング後の筋肉痛を避ける方法は?」を使用し、content_embeddingフィールドには生成されたELSER出力が含まれています:

Python

  1. resp = client.search(
  2. index="my-index",
  3. query={
  4. "sparse_vector": {
  5. "field": "content_embedding",
  6. "inference_id": "my-elser-endpoint",
  7. "query": "How to avoid muscle soreness after running?"
  8. }
  9. },
  10. )
  11. print(resp)

Js

  1. const response = await client.search({
  2. index: "my-index",
  3. query: {
  4. sparse_vector: {
  5. field: "content_embedding",
  6. inference_id: "my-elser-endpoint",
  7. query: "How to avoid muscle soreness after running?",
  8. },
  9. },
  10. });
  11. console.log(response);

コンソール

  1. GET my-index/_search
  2. {
  3. "query":{
  4. "sparse_vector":{
  5. "field": "content_embedding",
  6. "inference_id": "my-elser-endpoint",
  7. "query": "How to avoid muscle soreness after running?"
  8. }
  9. }
  10. }

結果は、my-indexインデックスからのクエリテキストに最も意味的に近い上位10ドキュメントです。結果には、関連する検索結果ごとの抽出されたトークンとその重みも含まれています。トークンは関連性を捉える学習された関連付けであり、同義語ではありません。トークンについて詳しく知りたい場合は、このページを参照してください。ソースからトークンを除外することも可能で、このセクションを参照して詳細を学んでください。

コンソール-結果

  1. "hits": {
  2. "total": {
  3. "value": 10000,
  4. "relation": "gte"
  5. },
  6. "max_score": 26.199875,
  7. "hits": [
  8. {
  9. "_index": "my-index",
  10. "_id": "FPr9HYsBag9jXmT8lEpI",
  11. "_score": 26.199875,
  12. "_source": {
  13. "content_embedding": {
  14. "muscular": 0.2821541,
  15. "bleeding": 0.37929374,
  16. "foods": 1.1718726,
  17. "delayed": 1.2112266,
  18. "cure": 0.6848574,
  19. "during": 0.5886185,
  20. "fighting": 0.35022718,
  21. "rid": 0.2752442,
  22. "soon": 0.2967024,
  23. "leg": 0.37649947,
  24. "preparation": 0.32974035,
  25. "advance": 0.09652356,
  26. (...)
  27. },
  28. "id": 1713868,
  29. "model_id": ".elser_model_2",
  30. "content": "For example, if you go for a run, you will mostly use the muscles in your lower body. Give yourself 2 days to rest those muscles so they have a chance to heal before you exercise them again. Not giving your muscles enough time to rest can cause muscle damage, rather than muscle development."
  31. }
  32. },
  33. (...)
  34. ]
  35. }

他のクエリとのセマンティックサーチの組み合わせ

sparse_vectorを他のクエリと複合クエリで組み合わせることができます。たとえば、[sparse_vector]クエリと同じ(または異なる)クエリテキストを持つブールまたは全文検索クエリでフィルター句を使用します。これにより、両方のクエリからの検索結果を組み合わせることができます。

  1. #### Python
  2. ``````python
  3. resp = client.search(
  4. index="my-index",
  5. query={
  6. "bool": {
  7. "should": [
  8. {
  9. "sparse_vector": {
  10. "field": "content_embedding",
  11. "inference_id": "my-elser-endpoint",
  12. "query": "How to avoid muscle soreness after running?",
  13. "boost": 1
  14. }
  15. },
  16. {
  17. "query_string": {
  18. "query": "toxins",
  19. "boost": 4
  20. }
  21. }
  22. ]
  23. }
  24. },
  25. min_score=10,
  26. )
  27. print(resp)
  28. `

Js

  1. const response = await client.search({
  2. index: "my-index",
  3. query: {
  4. bool: {
  5. should: [
  6. {
  7. sparse_vector: {
  8. field: "content_embedding",
  9. inference_id: "my-elser-endpoint",
  10. query: "How to avoid muscle soreness after running?",
  11. boost: 1,
  12. },
  13. },
  14. {
  15. query_string: {
  16. query: "toxins",
  17. boost: 4,
  18. },
  19. },
  20. ],
  21. },
  22. },
  23. min_score: 10,
  24. });
  25. console.log(response);

コンソール

  1. GET my-index/_search
  2. {
  3. "query": {
  4. "bool": {
  5. "should": [
  6. {
  7. "sparse_vector": {
  8. "field": "content_embedding",
  9. "inference_id": "my-elser-endpoint",
  10. "query": "How to avoid muscle soreness after running?",
  11. "boost": 1
  12. }
  13. },
  14. {
  15. "query_string": {
  16. "query": "toxins",
  17. "boost": 4
  18. }
  19. }
  20. ]
  21. }
  22. },
  23. "min_score": 10
  24. }
sparse_vectorquery_stringの両方のクエリがshould句のboolクエリにあります。
boostの値は1で、sparse_vectorクエリのデフォルト値です。
これは、このクエリの結果の関連性スコアがブーストされないことを意味します。
boostの値は4で、query_stringクエリの値です。
このクエリの結果の関連性スコアが増加し、検索結果でのランクが上がります。
10以上のスコアを持つ結果のみが表示されます。

パフォーマンスの最適化

ELSERトークンをドキュメントソースから除外してディスクスペースを節約する

ELSERによって生成されたトークンは、スパースベクトルクエリで使用するためにインデックス化される必要があります。ただし、ドキュメントソースにそれらの用語を保持する必要はありません。ソース除外マッピングを使用して、ドキュメントソースからELSER用語を削除することでディスクスペースを節約できます。

再インデックスは、ドキュメントソースを使用して宛先インデックスをポピュレートします。ELSER用語がソースから除外された後は、再インデックス化を通じて回復できません。ソースからトークンを除外することは、将来的に再インデックス化が必要ないと確信している場合にのみ適用すべきスペース節約の最適化です!このトレードオフを慎重に考慮し、ELSER用語をソースから除外することが特定の要件やユースケースに合致していることを確認することが重要です。_sourceフィールドの無効化や、_sourceからのフィールドの含める/除外するセクションを注意深く確認して、_sourceからトークンを除外することの可能な結果について詳しく学んでください。

  1. #### Python
  2. ``````python
  3. resp = client.indices.create(
  4. index="my-index",
  5. mappings={
  6. "_source": {
  7. "excludes": [
  8. "content_embedding"
  9. ]
  10. },
  11. "properties": {
  12. "content_embedding": {
  13. "type": "sparse_vector"
  14. },
  15. "content": {
  16. "type": "text"
  17. }
  18. }
  19. },
  20. )
  21. print(resp)
  22. `

Ruby

  1. response = client.indices.create(
  2. index: 'my-index',
  3. body: {
  4. mappings: {
  5. _source: {
  6. excludes: [
  7. 'content_embedding'
  8. ]
  9. },
  10. properties: {
  11. content_embedding: {
  12. type: 'sparse_vector'
  13. },
  14. content: {
  15. type: 'text'
  16. }
  17. }
  18. }
  19. }
  20. )
  21. puts response

Js

  1. const response = await client.indices.create({
  2. index: "my-index",
  3. mappings: {
  4. _source: {
  5. excludes: ["content_embedding"],
  6. },
  7. properties: {
  8. content_embedding: {
  9. type: "sparse_vector",
  10. },
  11. content: {
  12. type: "text",
  13. },
  14. },
  15. },
  16. });
  17. console.log(response);

コンソール

  1. PUT my-index
  2. {
  3. "mappings": {
  4. "_source": {
  5. "excludes": [
  6. "content_embedding"
  7. ]
  8. },
  9. "properties": {
  10. "content_embedding": {
  11. "type": "sparse_vector"
  12. },
  13. "content": {
  14. "type": "text"
  15. }
  16. }
  17. }
  18. }

データに応じて、sparse_vectorクエリはtrack_total_hits: falseでより速くなる場合があります。

さらなる読み物

インタラクティブな例