使用 Python 中的 ELSER 进行Serverless 语义搜索:探索夏季奥运会历史

163 阅读11分钟

作者:来自 Elastic Essodjolo Kahanam

本博客介绍如何使用语义搜索以自然语言表达形式从 Elasticsearch 索引中获取信息。我们将创建一个无服务器 Elasticsearch 项目,将之前的奥运会数据集加载到索引中,使用推理处理器和 ELSER 模型生成推理数据(在稀疏向量场中),最后借助文本扩展(text expansion)查询以自然语言表达形式搜索历史奥运会比赛信息。

工具和数据集

对于这个项目,我们将使用 Elasticsearch serverless 项目和无服务器 Python 客户端 (elasticsearch_serverless) 与 Elasticsearch 交互。要创建 serverless 项目,只需按照 serverless 入门指南操作即可。有关 serverless 的更多信息(包括定价),可在此处找到。

设置 serverless 项目时,请务必选择 Elasticsearch 选项和用于本教程的通用选项。

使用的数据集是从 Kaggle (Athletes_summer_games.csv) 获得的 1896 年至 2020 年夏季奥运会参赛者的数据集。它包含有关比赛年份、比赛类型、参赛者姓名、他们是否赢得奖牌以及最终获得哪枚奖牌以及其他信息的信息。

对于数据集操作,我们将使用 Eland,这是一个用于 Elasticsearch 中的 DataFrames 和机器学习的 Python 客户端和工具包。

最后使用的自然语言处理 (NLP) 模型是 Elastic Learned Sparse EncodeR (ELSER),这是一个由 Elastic 训练的检索模型,允许通过语义搜索检索更相关的搜索结果。

在执行以下步骤之前,请确保你已安装 severless Python 客户端和 Eland。



1.  pip install elasticsearch_serverless
2.  pip install eland


请注意下面我使用的版本。如果你使用的不是同一个版本,则可能需要根据你使用的版本中的任何最终语法更改来调整代码。



1.  ➜  ~ python3 --version
2.  Python 3.9.6
3.  ➜  ~ pip3 list | grep -E 'elasticsearch-serverless|eland'
4.  eland                     8.14.0
5.  elasticsearch-serverless  0.3.0.20231031


下载并部署 ELSER 模型

我们将使用 Python 客户端下载并部署 ELSER 模型。在此之前,让我们先确认我们可以连接到我们的 serverless 项目。下面的 URLAPI 密钥是从环境变量中读取的;你需要根据自己的情况使用适当的值,或者使用你喜欢的任何方法来读取凭据。



1.  from elasticsearch_serverless import Elasticsearch
2.  from os import environ

5.  serverless_endpoint = environ.get("SERVERLESS_ENDPOINT_URL")
6.  serverless_api_key = environ.get("SERVERLESS_API_KEY")

9.  client = Elasticsearch(
10.   serverless_endpoint,
11.   api_key=serverless_api_key
12.  )

15.  client.info()


如果一切配置正确,你应该得到如下输出:

ObjectApiResponse({'name': 'serverless', 'cluster_name': 'd6c6698e28c34e58b6f858df9442abac', 'cluster_uuid': 'hOuAhMUPQkumEM-PxW_r-Q', 'version': {'number': '8.11.0', 'build_flavor': 'serverless', 'build_type': 'docker', 'build_hash': '00000000', 'build_date': '2023-10-31', 'build_snapshot': False, 'lucene_version': '9.7.0', 'minimum_wire_compatibility_version': '8.11.0', 'minimum_index_compatibility_version': '8.11.0'}, 'tagline': 'You Know, for Search'}) 

现在我们已经确认 Python 客户端已成功连接到无服务器 Elasticsearch 项目,让我们下载并部署 ELSER 模型。我们将检查该模型是否之前已部署,并将其删除以执行全新安装。此外,由于部署阶段可能需要几分钟,我们将不断检查模型配置信息,以确保在进入下一阶段之前模型定义存在。有关更多信息,请查看Get trained models API



1.  from elasticsearch_serverless import Elasticsearch, exceptions
2.  import time

5.  # delete model if already downloaded and deployed
6.  try:
7.     client.ml.delete_trained_model(model_id=".elser_model_2", force=True)
8.     print("Model deleted successfully, We will proceed with creating one")
9.  except exceptions.NotFoundError:
10.     print("Model doesn't exist, but We will proceed with creating one")

13.  # Creates the ELSER model configuration. Automatically downloads the model if it doesn't exist.
14.  client.ml.put_trained_model(
15.     model_id=".elser_model_2",
16.     input={
17.         "field_names": [
18.             "concatenated_textl"
19.         ]
20.     }
21.  )

24.  # Check the download and deploy progress
25.  while True:
26.     status = client.ml.get_trained_models(
27.         model_id=".elser_model_2", include="definition_status"
28.     )

31.     if status["trained_model_configs"][0]["fully_defined"]:
32.         print("ELSER Model is downloaded and ready to be deployed.")
33.         break
34.     else:
35.         print("ELSER Model is downloaded but not ready to be deployed.")
36.     time.sleep(5)


一旦我们确认模型已下载并准备部署,我们就可以继续启动 ELSER。完全准备好部署可能需要一点时间。



1.  # A function to check the model's routing state
2.  # https://www.elastic.co/guide/en/elasticsearch/reference/current/get-trained-models-stats.html
3.  def get_model_routing_state(model_id=".elser_model_2"):
4.     try:
5.         status = client.ml.get_trained_models_stats(
6.             model_id=".elser_model_2",
7.         )
8.         return status["trained_model_stats"][0]["deployment_stats"]["nodes"][0]["routing_state"]["routing_state"]
9.     except:
10.         return None

13.  # If ELSER is already started, then we are fine.
14.  if get_model_routing_state(".elser_model_2") == "started":
15.     print("ELSER Model has been already deployed and is currently started.")

18.  # Otherwise, we will deploy it, and monitor the routing state to make sure it is started.
19.  else:
20.     print("ELSER Model will be deployed.")

23.     # Start trained model deployment
24.     client.ml.start_trained_model_deployment(
25.         model_id=".elser_model_2",
26.         number_of_allocations=16,
27.         threads_per_allocation=4,
28.         wait_for="starting"
29.     )

32.     while True:
33.         if get_model_routing_state(".elser_model_2") == "started":
34.             print("ELSER Model has been successfully deployed.")
35.             break
36.         else:
37.             print("ELSER Model is currently being deployed.")
38.         time.sleep(5)


使用 Eland 将数据集加载到 Elasticsearch

eland.csv_to_eland 允许将逗号分隔值 (csv) 文件读入存储在 Elasticsearch 索引中的数据框中。我们将使用它将奥运会数据 (Athletes_summer_games.csv) 加载到 Elasticsearch 中。es_type_overrides 允许覆盖默认映射。



1.  import eland as ed

4.  index="elser-olympic-games"
5.  csv_file="Athletes_summer_games.csv"

8.  ed.csv_to_eland(
9.     csv_file,
10.     es_client=client,
11.     es_dest_index=index,
12.     es_if_exists='replace',
13.     es_dropna=True,
14.     es_refresh=True,
15.     index_col=0,
16.     es_type_overrides={
17.         "City": "text",
18.         "Event": "text",
19.         "Games": "text",
20.         "Medal": "text",
21.         "NOC": "text",
22.         "Name": "text",
23.         "Season": "text",
24.         "Sport": "text",
25.         "Team": "text"
26.     }
27.  )


执行上述代码后,数据将写入索引 elser-olympic-games。你还可以将生成的数据框 (eland.DataFrame) 检索到变量中,以供进一步操作。

基于 ELSER 创建用于推理的摄取管道

我们使用语义搜索探索过去奥运会比赛数据的下一步是创建一个包含运行 ELSER 模型的 inference processor 的摄取管道。已选择一组字段并将其串联成推理处理器将在其上工作的单个字段。根据你的用例,你可能需要使用另一种策略。

串联是使用 script processor 完成的。推理处理器使用先前部署的 ELSER 模型,将串联字段作为输入,并将输出存储在稀疏向量类型字段中(参见以下要点)。



1.  client.ingest.put_pipeline(
2.     id="elser-ingest-pipeline",
3.     description="Ingest pipeline for ELSER",
4.     processors=[
5.         {
6.             "script": {
7.             "description": "Concatenate some selected fields value into `concatenated_text` field",
8.             "lang": "painless",
9.             "source": """
10.                 ctx['concatenated_text'] = ctx['Name'] + ' ' + ctx['Team'] + ' ' + ctx['Games'] + ' ' + ctx['City'] + ' ' + ctx['Event'];
11.             """
12.             }
13.         },
14.         {
15.             "inference": {
16.                 "model_id": ".elser_model_2",
17.                 "ignore_missing": True,
18.                 "input_output": [
19.                     {
20.                         "input_field": "concatenated_text",
21.                         "output_field": "concatenated_text_embedding"
22.                     }
23.                 ]
24.             }
25.         }
26.     ]
27.  )


准备索引

这是使用自然语言表达查询过去奥运会比赛数据之前的最后一个阶段。我们将更新之前创建的索引的映射,添加一个 sparse vector 类型字段。

更新映射:添加稀疏向量字段

我们将通过添加一个用于保存串联数据(concatenated data)的字段和一个用于保存推理处理器使用 ELSER 模型计算出的推断信息的稀疏向量字段来更新索引映射。



1.  index="elser-olympic-games"

4.  mappings_properties={
5.     "concatenated_text": {
6.         "type": "text"
7.     },
8.     "concatenated_text_embedding": {
9.         "type": "sparse_vector"
10.     }
11.  }

14.  client.indices.put_mapping(
15.     index=index,
16.     properties=mappings_properties
17.  )


填充稀疏向量字段

我们将通过运行 update by query 来调用之前创建的摄取管道,以便填充每个文档中的稀疏向量字段。



1.  client.update_by_query(
2.     index="elser-olympic-games",
3.     pipeline="elser-ingest-pipeline",
4.     wait_for_completion=False
5.  )


该请求将需要一些时间,具体取决于文档数量以及用于部署 ELSER 的分配数量和每个分配的线程数。完成此步骤后,我们现在可以开始使用语义搜索探索过去的奥运会数据集。

让我们使用语义搜索探索奥运会数据集

现在我们将使用 text expansion 查询,使用自然语言表达来检索有关过去奥运会比赛的信息。在进行演示之前,让我们创建一个函数来检索和格式化搜索结果。



1.  def semantic_search(search_text):
2.     response = client.search(
3.         index="elser-olympic-games",
4.         size=3,
5.         query={
6.             "bool": {
7.             "must": [
8.                 {
9.                     "text_expansion": {
10.                         "concatenated_text_embedding": {
11.                         "model_id": ".elser_model_2",
12.                         "model_text": search_text
13.                         }
14.                     }
15.                 },
16.                 {
17.                     "exists": {
18.                         "field": "Medal"
19.                     }
20.                 }
21.             ]
22.             }
23.         },
24.         source_excludes="*_embedding, concatenated_text"
25.     )

28.     for hit in response["hits"]["hits"]:
29.         doc_id = hit["_id"]
30.         score = hit["_score"]
31.         year = hit["_source"]["Year"]
32.         event = hit["_source"]["Event"]
33.         games = hit["_source"]["Games"]
34.         sport = hit["_source"]["Sport"]
35.         city = hit["_source"]["City"]
36.         team = hit["_source"]["Team"]
37.         name = hit["_source"]["Name"]
38.         medal = hit["_source"]["Medal"]

41.         print(f"Score: {score}\nDocument ID: {doc_id}\nYear: {year}\nEvent: {event}\nName: {name}\nCity: {city}\nTeam: {team}\nMedal: {medal}\n")


上述函数将接收有关往届奥运会比赛获胜者的问题,并使用 Elastic 的 text expansion 查询执行语义搜索。检索到的结果将被格式化并打印出来。请注意,我们强制查询中存在奖牌,因为我们只对获胜者感兴趣。我们还将结果的大小限制为 3,因为我们预计会有三名获胜者(金牌、银牌、铜牌)。同样,根据你的用例,你可能不一定会做同样的事情。

🏌️‍♂️ “Who won the Golf competition in 1900?”

请求:



1.  semantic_search("Who won the Golf competition in 1900?")




响应:



1.  Score: 18.184263
2.  Document ID: 206566
3.  Year: 1900
4.  Event: Golf Men's Individual
5.  Name: Walter Mathers Rutherford
6.  City: Paris
7.  Team: Great Britain
8.  Medal: Silver

10.  Score: 17.443663
11.  Document ID: 209892
12.  Year: 1900
13.  Event: Golf Men's Individual
14.  Name: Charles Edward Sands
15.  City: Paris
16.  Team: United States
17.  Medal: Gold

19.  Score: 16.939331
20.  Document ID: 192747
21.  Year: 1900
22.  Event: Golf Women's Individual
23.  Name: Myra Abigail "Abbie" Pratt (Pankhurst-, Wright-, -Karageorgevich)
24.  City: Paris
25.  Team: United States
26.  Medal: Bronze


🏹 “Women archery winners of 1908”

请求:



1.  semantic_search("Women archery winners of 1908")




响应:



1.  Score: 21.876282
2.  Document ID: 96010
3.  Year: 1908
4.  Event: Archery Women's Double National Round
5.  Name: Beatrice Geraldine Hill-Lowe (Ruxton-, -Thompson)
6.  City: London
7.  Team: Great Britain
8.  Medal: Bronze

10.  Score: 21.0998
11.  Document ID: 170250
12.  Year: 1908
13.  Event: Archery Women's Double National Round
14.  Name: Sybil Fenton Newall
15.  City: London
16.  Team: Great Britain
17.  Medal: Gold

19.  Score: 21.079535
20.  Document ID: 56686
21.  Year: 1908
22.  Event: Archery Women's Double National Round
23.  Name: Charlotte "Lottie" Dod
24.  City: London
25.  Team: Great Britain
26.  Medal: Silver


🚴‍♂️ “Who won the individual cycling competition in 1972?”

请求:



1.  semantic_search("Who won the cycling competition in 1972?")




响应:



1.  Score: 20.554308
2.  Document ID: 215559
3.  Year: 1972
4.  Event: Cycling Men's Road Race, Individual
5.  Name: Kevin "Clyde" Sefton
6.  City: Munich
7.  Team: Australia
8.  Medal: Silver

10.  Score: 20.267525
11.  Document ID: 128598
12.  Year: 1972
13.  Event: Cycling Men's Road Race, Individual
14.  Name: Hendrikus Andreas "Hennie" Kuiper
15.  City: Munich
16.  Team: Netherlands
17.  Medal: Gold

19.  Score: 19.108923
20.  Document ID: 19225
21.  Year: 1972
22.  Event: Cycling Men's Team Pursuit, 4,000 metres
23.  Name: Michael John "Mick" Bennett
24.  City: Munich
25.  Team: Great Britain
26.  Medal: Bronze


结论

本博客展示了如何使用 serverlss 的 Python 编程语言,通过 Elastic Learned Sparse EncodeR (ELSER) NLP 模型执行语义搜索。运行本教程后,你需要确保关闭 serverless,以避免任何额外费用。要进一步了解,请随时查看我们的 Elasticsearch 相关性引擎 (ESRE​​) 工程师课程,你可以在其中学习如何利用 Elasticsearch 相关性引擎 (ESRE​​) 和大型语言模型 (LLMs) 构建高级 RAG(检索增强生成)应用程序,将 Elasticsearch 的存储、处理和搜索功能与 LLM 的生成能力相结合。

本文中描述的任何特性或功能的发布和时间均由 Elastic 自行决定。任何当前不可用的特性或功能可能无法按时交付或根本无法交付。

准备好自己尝试一下了吗?开始免费试用

想要获得 Elastic 认证?了解下一期 Elasticsearch 工程师培训何时开始!

更多关于奥运的数据分析,请阅读文章 :

  • 使用 Elastic Stack 来分析奥运数据()()(

原文:Serverless semantic search with ELSER in Python — Search Labs