教程:使用 ELSER 进行语义搜索

编辑

教程:使用 ELSER 进行语义搜索

编辑

Elastic Learned Sparse EncodeR(简称 ELSER)是由 Elastic 训练的 NLP 模型,它使您可以通过使用稀疏向量表示来执行语义搜索。语义搜索不是对搜索词进行字面匹配,而是基于搜索查询的意图和上下文含义来检索结果。

本教程中的说明将向您展示如何使用 ELSER 对您的数据执行语义搜索。

有关在 Elastic Stack 中执行语义搜索的最简单方法,请参阅 semantic_text 端到端教程。

在使用 ELSER 进行语义搜索时,仅考虑每个字段中提取的前 512 个标记。有关更多信息,请参阅 此页面

要求

编辑

要使用 ELSER 执行语义搜索,您的集群中必须部署 NLP 模型。请参阅 ELSER 文档,了解如何下载和部署模型。

如果 部署自动伸缩已关闭,则在 Elasticsearch Service 中部署和使用 ELSER 模型的最小专用 ML 节点大小为 4 GB。建议开启自动伸缩,因为它允许您的部署根据需求动态调整资源。通过使用更多分配或每个分配更多线程可以实现更好的性能,这需要更大的 ML 节点。自动伸缩会在需要时提供更大的节点。如果自动伸缩已关闭,则您必须自行提供大小合适的节点。

创建索引映射

编辑

首先,必须创建目标索引的映射,即包含模型根据您的文本创建的标记的索引。目标索引必须具有带有 sparse_vectorrank_features 字段类型的字段,以索引 ELSER 输出。

ELSER 输出必须摄取到具有 sparse_vectorrank_features 字段类型的字段中。否则,Elasticsearch 会将标记-权重对解释为文档中的大量字段。如果您收到类似于此的错误:"在添加新字段时,已超过总字段 [1000] 的限制",则表示 ELSER 输出字段未正确映射,并且它的字段类型不同于 sparse_vectorrank_features

resp = client.indices.create(
    index="my-index",
    mappings={
        "properties": {
            "content_embedding": {
                "type": "sparse_vector"
            },
            "content": {
                "type": "text"
            }
        }
    },
)
print(resp)
response = client.indices.create(
  index: 'my-index',
  body: {
    mappings: {
      properties: {
        content_embedding: {
          type: 'sparse_vector'
        },
        content: {
          type: 'text'
        }
      }
    }
  }
)
puts response
const response = await client.indices.create({
  index: "my-index",
  mappings: {
    properties: {
      content_embedding: {
        type: "sparse_vector",
      },
      content: {
        type: "text",
      },
    },
  },
});
console.log(response);
PUT my-index
{
  "mappings": {
    "properties": {
      "content_embedding": { 
        "type": "sparse_vector" 
      },
      "content": { 
        "type": "text" 
      }
    }
  }
}

包含生成的标记的字段的名称。必须在下一步的推理管道配置中引用它。

包含标记的字段是一个 sparse_vector 字段。

从中创建稀疏向量表示的字段的名称。在此示例中,字段的名称是 content。必须在下一步的推理管道配置中引用它。

在此示例中,字段类型为文本。

要了解如何优化空间,请参阅通过从文档源中排除 ELSER 标记来节省磁盘空间 部分。

创建带有推理处理器的摄取管道

编辑

创建带有 推理处理器摄取管道,以使用 ELSER 对正在管道中摄取的数据进行推理。

resp = client.ingest.put_pipeline(
    id="elser-v2-test",
    processors=[
        {
            "inference": {
                "model_id": ".elser_model_2",
                "input_output": [
                    {
                        "input_field": "content",
                        "output_field": "content_embedding"
                    }
                ]
            }
        }
    ],
)
print(resp)
response = client.ingest.put_pipeline(
  id: 'elser-v2-test',
  body: {
    processors: [
      {
        inference: {
          model_id: '.elser_model_2',
          input_output: [
            {
              input_field: 'content',
              output_field: 'content_embedding'
            }
          ]
        }
      }
    ]
  }
)
puts response
const response = await client.ingest.putPipeline({
  id: "elser-v2-test",
  processors: [
    {
      inference: {
        model_id: ".elser_model_2",
        input_output: [
          {
            input_field: "content",
            output_field: "content_embedding",
          },
        ],
      },
    },
  ],
});
console.log(response);
PUT _ingest/pipeline/elser-v2-test
{
  "processors": [
    {
      "inference": {
        "model_id": ".elser_model_2",
        "input_output": [ 
          {
            "input_field": "content",
            "output_field": "content_embedding"
          }
        ]
      }
    }
  ]
}

配置对象,它定义了推理过程的 input_field 和将包含推理结果的 output_field

加载数据

编辑

在此步骤中,您将加载稍后在推理摄取管道中使用的、从中提取标记的数据。

使用 msmarco-passagetest2019-top1000 数据集,它是 MS MARCO Passage Ranking 数据集的子集。它包含 200 个查询,每个查询都附带一个相关的文本段落列表。所有唯一的段落及其 ID 都已从该数据集中提取并编译到 tsv 文件中。

msmarco-passagetest2019-top1000 数据集未用于训练模型。我们在本教程中使用此示例数据集,因为它易于访问以进行演示。您可以使用不同的数据集来测试工作流程并熟悉它。

下载该文件并使用 UI 中的 文件上传器将其上传到您的集群。分析完数据后,单击覆盖设置。在编辑字段名称下,将 id 分配给第一列,将 content 分配给第二列。单击应用,然后单击导入。将索引命名为 test-data,然后单击导入。上传完成后,您将看到一个名为 test-data 的索引,其中包含 182,469 个文档。

通过推理摄取管道摄取数据

编辑

通过使用 ELSER 作为推理模型的推理管道重新索引数据,从文本创建标记。

resp = client.reindex(
    wait_for_completion=False,
    source={
        "index": "test-data",
        "size": 50
    },
    dest={
        "index": "my-index",
        "pipeline": "elser-v2-test"
    },
)
print(resp)
response = client.reindex(
  wait_for_completion: false,
  body: {
    source: {
      index: 'test-data',
      size: 50
    },
    dest: {
      index: 'my-index',
      pipeline: 'elser-v2-test'
    }
  }
)
puts response
const response = await client.reindex({
  wait_for_completion: "false",
  source: {
    index: "test-data",
    size: 50,
  },
  dest: {
    index: "my-index",
    pipeline: "elser-v2-test",
  },
});
console.log(response);
POST _reindex?wait_for_completion=false
{
  "source": {
    "index": "test-data",
    "size": 50 
  },
  "dest": {
    "index": "my-index",
    "pipeline": "elser-v2-test"
  }
}

重新索引的默认批次大小为 1000。将 size 减小到一个较小的数字可以加快重新索引过程的更新速度,从而使您能够密切关注进度并及早发现错误。

该调用返回一个任务 ID,用于监视进度

resp = client.tasks.get(
    task_id="<task_id>",
)
print(resp)
const response = await client.tasks.get({
  task_id: "<task_id>",
});
console.log(response);
GET _tasks/<task_id>

您还可以打开“训练模型”UI,选择 ELSER 下的“管道”选项卡以跟踪进度。

重新索引大型数据集可能需要很长时间。您可以使用数据集的子集来测试此工作流程。为此,请取消重新索引过程,并且仅为已重新索引的子集生成嵌入。以下 API 请求将取消重新索引任务

resp = client.tasks.cancel(
    task_id="<task_id>",
)
print(resp)
const response = await client.tasks.cancel({
  task_id: "<task_id>",
});
console.log(response);
POST _tasks/<task_id>/_cancel

使用 sparse_vector 查询进行语义搜索

编辑

要执行语义搜索,请使用 sparse_vector 查询,并提供查询文本和与您的 ELSER 模型关联的推理 ID。以下示例使用查询文本“How to avoid muscle soreness after running?”,content_embedding 字段包含生成的 ELSER 输出

resp = client.search(
    index="my-index",
    query={
        "sparse_vector": {
            "field": "content_embedding",
            "inference_id": "my-elser-endpoint",
            "query": "How to avoid muscle soreness after running?"
        }
    },
)
print(resp)
const response = await client.search({
  index: "my-index",
  query: {
    sparse_vector: {
      field: "content_embedding",
      inference_id: "my-elser-endpoint",
      query: "How to avoid muscle soreness after running?",
    },
  },
});
console.log(response);
GET my-index/_search
{
   "query":{
      "sparse_vector":{
         "field": "content_embedding",
         "inference_id": "my-elser-endpoint",
         "query": "How to avoid muscle soreness after running?"
      }
   }
}

结果是与 my-index 索引中查询文本的含义最接近的前 10 个文档,按相关性排序。结果还包含每个相关搜索结果的提取标记及其权重。标记是捕获相关性的学习关联,它们不是同义词。要了解有关标记的更多信息,请参阅 此页面。可以从源中排除标记,请参阅此部分以了解更多信息。

"hits": {
  "total": {
    "value": 10000,
    "relation": "gte"
  },
  "max_score": 26.199875,
  "hits": [
    {
      "_index": "my-index",
      "_id": "FPr9HYsBag9jXmT8lEpI",
      "_score": 26.199875,
      "_source": {
        "content_embedding": {
          "muscular": 0.2821541,
          "bleeding": 0.37929374,
          "foods": 1.1718726,
          "delayed": 1.2112266,
          "cure": 0.6848574,
          "during": 0.5886185,
          "fighting": 0.35022718,
          "rid": 0.2752442,
          "soon": 0.2967024,
          "leg": 0.37649947,
          "preparation": 0.32974035,
          "advance": 0.09652356,
          (...)
        },
        "id": 1713868,
        "model_id": ".elser_model_2",
        "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."
      }
    },
    (...)
  ]
}

将语义搜索与其他查询结合使用

编辑

您可以将 sparse_vector复合查询中的其他查询结合使用。例如,在 布尔查询中使用筛选子句,或者使用与 sparse_vector 查询相同(或不同)的查询文本进行全文查询。这使您可以合并来自两个查询的搜索结果。

sparse_vector 查询的搜索匹配结果的得分往往高于其他 Elasticsearch 查询。可以通过使用 boost 参数增加或减少每个查询的相关性得分来调整这些得分。sparse_vector 查询在存在大量不太相关的结果时可能会有很高的召回率。使用 min_score 参数来修剪这些不太相关的文档。

resp = client.search(
    index="my-index",
    query={
        "bool": {
            "should": [
                {
                    "sparse_vector": {
                        "field": "content_embedding",
                        "inference_id": "my-elser-endpoint",
                        "query": "How to avoid muscle soreness after running?",
                        "boost": 1
                    }
                },
                {
                    "query_string": {
                        "query": "toxins",
                        "boost": 4
                    }
                }
            ]
        }
    },
    min_score=10,
)
print(resp)
const response = await client.search({
  index: "my-index",
  query: {
    bool: {
      should: [
        {
          sparse_vector: {
            field: "content_embedding",
            inference_id: "my-elser-endpoint",
            query: "How to avoid muscle soreness after running?",
            boost: 1,
          },
        },
        {
          query_string: {
            query: "toxins",
            boost: 4,
          },
        },
      ],
    },
  },
  min_score: 10,
});
console.log(response);
GET my-index/_search
{
  "query": {
    "bool": { 
      "should": [
        {
          "sparse_vector": {
            "field": "content_embedding",
            "inference_id": "my-elser-endpoint",
            "query": "How to avoid muscle soreness after running?",
            "boost": 1 
          }
        },
        {
          "query_string": {
            "query": "toxins",
            "boost": 4 
          }
        }
      ]
    }
  },
  "min_score": 10 
}

sparse_vectorquery_string 查询都在 bool 查询的 should 子句中。

sparse_vector 查询的 boost 值为 1,这是默认值。这意味着此查询结果的相关性得分不会被提升。

query_string 查询的 boost 值为 4。此查询结果的相关性得分会提高,从而使其在搜索结果中排名更高。

仅显示得分等于或高于 10 的结果。

优化性能

编辑

通过从文档源中排除 ELSER 标记来节省磁盘空间

编辑

ELSER 生成的词元必须被索引,以便在sparse_vector 查询中使用。但是,没有必要将这些词项保留在文档源中。您可以使用源排除映射来删除文档源中的 ELSER 词项,从而节省磁盘空间。

重新索引使用文档源来填充目标索引。一旦从源中排除了 ELSER 词项,就无法通过重新索引恢复它们。从源中排除词元是一种节省空间的优化,只有当您确定将来不需要重新索引时才应应用!务必仔细考虑这种权衡,并确保从源中排除 ELSER 词项符合您的特定需求和用例。请仔细查看禁用 _source 字段_source 中包含/排除字段部分,以了解更多关于从 _source 中排除词元可能造成的后果。

可以通过以下 API 调用创建从 _source 字段中排除 content_embedding 的映射

resp = client.indices.create(
    index="my-index",
    mappings={
        "_source": {
            "excludes": [
                "content_embedding"
            ]
        },
        "properties": {
            "content_embedding": {
                "type": "sparse_vector"
            },
            "content": {
                "type": "text"
            }
        }
    },
)
print(resp)
response = client.indices.create(
  index: 'my-index',
  body: {
    mappings: {
      _source: {
        excludes: [
          'content_embedding'
        ]
      },
      properties: {
        content_embedding: {
          type: 'sparse_vector'
        },
        content: {
          type: 'text'
        }
      }
    }
  }
)
puts response
const response = await client.indices.create({
  index: "my-index",
  mappings: {
    _source: {
      excludes: ["content_embedding"],
    },
    properties: {
      content_embedding: {
        type: "sparse_vector",
      },
      content: {
        type: "text",
      },
    },
  },
});
console.log(response);
PUT my-index
{
  "mappings": {
    "_source": {
      "excludes": [
        "content_embedding"
      ]
    },
    "properties": {
      "content_embedding": {
        "type": "sparse_vector"
      },
      "content": {
        "type": "text"
      }
    }
  }
}

根据您的数据,使用 track_total_hits: false 时,sparse_vector 查询可能会更快。

延伸阅读

编辑

互动示例

编辑