Elasticsearch:语义搜索快速入门

这个交互式 Notebook 将介绍一些 Elasticsearch 基础操作,使用官方 Elasticsearch Python 客户端。你将使用 Sentence Transformers 对文本进行嵌入,并执行语义搜索。学习如何将传统基于文本的搜索与语义搜索结合,构建混合搜索系统。

如果你想使用本地部署,你可以参考文章 "如何在 Linux,MacOS 及 Windows 上进行安装 Elasticsearch" 来进行安装。并参考代码 https://github.com/liu-xiao-guo/semantic_search_getstarted

创建 Elastic Cloud 部署

如果你没有 Elastic Cloud 部署,可以在这里注册免费试用。

登录 Elastic Cloud 账户后,进入创建部署页面并选择创建部署(Create deployment)。保持所有设置为默认值。

安装软件包并导入模块

开始之前,我们需要使用 Python 客户端连接到我们的 Elastic 部署。由于我们使用的是 Elastic Cloud 部署,因此将使用 Cloud ID 来标识我们的部署。

首先,我们需要安装 Elasticsearch Python 客户端。

复制代码
!pip install -qU "elasticsearch<9" sentence-transformers==2.7.0

在此示例中,我们使用 sentence_transformers 库中的 all-MiniLM-L6-v2 模型。你可以在 Hugging Face 上阅读有关此模型的更多信息。

复制代码
from sentence_transformers import SentenceTransformer

model = SentenceTransformer("all-MiniLM-L6-v2")

初始化 Elasticsearch 客户端

现在,我们可以实例化 Elasticsearch Python 客户端,并提供部署中的 Cloud ID 和密码。

复制代码
from elasticsearch import Elasticsearch
from getpass import getpass

# https://www.elastic.co/search-labs/tutorials/install-elasticsearch/elastic-cloud#finding-your-cloud-id
ELASTIC_CLOUD_ID = getpass("Elastic Cloud ID: ")

# https://www.elastic.co/search-labs/tutorials/install-elasticsearch/elastic-cloud#creating-an-api-key
ELASTIC_API_KEY = getpass("Elastic Api Key: ")

# 创建客户端实例
client = Elasticsearch(
    # 用于本地开发
    # hosts=["http://localhost:9200"]
    cloud_id=ELASTIC_CLOUD_ID,
    api_key=ELASTIC_API_KEY,
)

如果你正在本地运行 Elasticsearch 或使用自托管部署,则可以改为传入 Elasticsearch 主机地址。阅读更多关于如何连接本地 Elasticsearch 的信息。

启用遥测

了解你正在使用此 Notebook,有助于我们决定将精力投入到哪些方面来改进我们的产品。

我们希望你运行以下代码,以便我们收集匿名使用统计信息。有关详细信息,请参阅 telemetry.py。谢谢!

复制代码
!curl -O -s https://raw.githubusercontent.com/elastic/elasticsearch-labs/main/telemetry/telemetry.py

from telemetry import enable_telemetry

client = enable_telemetry(client, "00-quick-start")

测试客户端

在继续之前,请通过此测试确认客户端已成功连接。

复制代码
print(client.info())

输出:

复制代码
{
  'name': 'instance-0000000000',
  'cluster_name': 'a72482be54904952ba46d53c3def7740',
  'cluster_uuid': 'g8BE52TtT32pGBbRzP_oKA',
  'version': {
    'number': '8.12.2',
    'build_flavor': 'default',
    'build_type': 'docker',
    'build_hash': '48a287ab9497e852de30327444b0809e55d46466',
    'build_date': '2024-02-19T10:04:32.774273190Z',
    'build_snapshot': False,
    'lucene_version': '9.9.2',
    'minimum_wire_compatibility_version': '7.17.0',
    'minimum_index_compatibility_version': '7.0.0'
  },
  'tagline': 'You Know, for Search'
}

索引测试数据

我们的客户端已经设置完成,并连接到了 Elastic 部署。现在,我们需要一些数据来测试 Elasticsearch 查询基础功能。我们将使用一个包含以下字段的图书小型索引:

  • title

  • authors

  • publish_date

  • num_reviews

  • publisher

创建索引

首先确保不存在之前创建的名为 book_index 的索引。

复制代码
client.indices.delete(index="book_index", ignore_unavailable=True)

输出:

复制代码
ObjectApiResponse({'acknowledged': True})

🔐 注意:你可以随时返回此部分并运行上面的删除函数,以删除索引并从头开始。

让我们创建一个 Elasticsearch 索引,并为测试数据配置正确的映射。

复制代码
# 定义映射
mappings = {
    "properties": {
        "title_vector": {
            "type": "dense_vector",
            "dims": 384,
            "index": "true",
            "similarity": "cosine",
        }
    }
}

# 创建索引
client.indices.create(index="book_index", mappings=mappings)

输出:

复制代码
ObjectApiResponse({'acknowledged': True, 'shards_acknowledged': True, 'index': 'book_index'})

索引测试数据

运行以下命令上传一些测试数据,其中包含来自该数据集的 10 本热门编程书籍的信息。model.encode 会使用我们之前初始化的模型,将文本实时编码为向量。

复制代码
import json
from urllib.request import urlopen

url = "https://raw.githubusercontent.com/elastic/elasticsearch-labs/main/notebooks/search/data.json"
response = urlopen(url)
books = json.loads(response.read())

operations = []
for book in books:
    operations.append({"index": {"_index": "book_index"}})
    # 使用模型将标题转换为嵌入向量
    book["title_vector"] = model.encode(book["title"]).tolist()
    operations.append(book)

client.bulk(index="book_index", operations=operations, refresh=True)

输出:

复制代码
ObjectApiResponse({'errors': False, 'took': 88, 'items': [{'index': {'_index': 'book_index', '_id': 'caRpvY4BKY8PuI1qPluy', '_version': 1, 'result': 'created', 'forced_refresh': True, '_shards': {'total': 2, 'successful': 2, 'failed': 0}, '_seq_no': 0, '_primary_term': 1, 'status': 201}}, {'index': {'_index': 'book_index', '_id': 'cqRpvY4BKY8PuI1qPluy', '_version': 1, 'result': 'created', 'forced_refresh': True, '_shards': {'total': 2, 'successful': 2, 'failed': 0}, '_seq_no': 1, '_primary_term': 1, 'status': 201}}, {'index': {'_index': 'book_index', '_id': 'c6RpvY4BKY8PuI1qPluy', '_version': 1, 'result': 'created', 'forced_refresh': True, '_shards': {'total': 2, 'successful': 2, 'failed': 0}, '_seq_no': 2, '_primary_term': 1, 'status': 201}}, {'index': {'_index': 'book_index', '_id': 'dKRpvY4BKY8PuI1qPluy', '_version': 1, 'result': 'created', 'forced_refresh': True, '_shards': {'total': 2, 'successful': 2, 'failed': 0}, '_seq_no': 3, '_primary_term': 1, 'status': 201}}, {'index': {'_index': 'book_index', '_id': 'daRpvY4BKY8PuI1qPluy', '_version': 1, 'result': 'created', 'forced_refresh': True, '_shards': {'total': 2, 'successful': 2, 'failed': 0}, '_seq_no': 4, '_primary_term': 1, 'status': 201}}, {'index': {'_index': 'book_index', '_id': 'dqRpvY4BKY8PuI1qPluy', '_version': 1, 'result': 'created', 'forced_refresh': True, '_shards': {'total': 2, 'successful': 2, 'failed': 0}, '_seq_no': 5, '_primary_term': 1, 'status': 201}}, {'index': {'_index': 'book_index', '_id': 'd6RpvY4BKY8PuI1qPluy', '_version': 1, 'result': 'created', 'forced_refresh': True, '_shards': {'total': 2, 'successful': 2, 'failed': 0}, '_seq_no': 6, '_primary_term': 1, 'status': 201}}, {'index': {'_index': 'book_index', '_id': 'eKRpvY4BKY8PuI1qPluy', '_version': 1, 'result': 'created', 'forced_refresh': True, '_shards': {'total': 2, 'successful': 2, 'failed': 0}, '_seq_no': 7, '_primary_term': 1, 'status': 201}}, {'index': {'_index': 'book_index', '_id': 'eaRpvY4BKY8PuI1qPluy', '_version': 1, 'result': 'created', 'forced_refresh': True, '_shards': {'total': 2, 'successful': 2, 'failed': 0}, '_seq_no': 8, '_primary_term': 1, 'status': 201}}, {'index': {'_index': 'book_index', '_id': 'eqRpvY4BKY8PuI1qPluy', '_version': 1, 'result': 'created', 'forced_refresh': True, '_shards': {'total': 2, 'successful': 2, 'failed': 0}, '_seq_no': 9, '_primary_term': 1, 'status': 201}}]})

附带说明:格式化 Elasticsearch 响应

你的 API 调用会返回难以阅读的嵌套 JSON。我们将创建一个名为 pretty_response 的小函数,用于从示例中返回清晰、易读的输出。

复制代码
def pretty_response(response):
    if len(response["hits"]["hits"]) == 0:
        print("Your search returned no results.")
    else:
        for hit in response["hits"]["hits"]:
            id = hit["_id"]
            publication_date = hit["_source"]["publish_date"]
            score = hit["_score"]
            title = hit["_source"]["title"]
            summary = hit["_source"]["summary"]
            publisher = hit["_source"]["publisher"]
            num_reviews = hit["_source"]["num_reviews"]
            authors = hit["_source"]["authors"]
            pretty_output = f"\nID: {id}\nPublication date: {publication_date}\nTitle: {title}\nSummary: {summary}\nPublisher: {publisher}\nReviews: {num_reviews}\nAuthors: {authors}\nScore: {score}"
            print(pretty_output)

创建查询

现在,我们已经对图书进行了索引,希望对与给定查询相似的图书执行语义搜索。我们会对查询进行嵌入,然后执行搜索。

复制代码
response = client.search(
    index="book_index",
    knn={
        "field": "title_vector",
        "query_vector": model.encode("javascript books"),
        "k": 10,
        "num_candidates": 100,
    },
)

pretty_response(response)

结果:

得分:0.8042828

标题:JavaScript:优良部分

简介:深入探索 JavaScript 中对于编写可维护代码至关重要的部分

出版社:oreilly

得分:0.6989136

标题:你不知道的 JavaScript:入门

简介:JavaScript 和整体编程的介绍

出版社:oreilly

得分:0.6796988

标题:精通 JavaScript

简介:现代编程入门

出版社:no starch press

(其余结果略)

过滤

过滤上下文主要用于过滤结构化数据。例如,可以使用过滤上下文回答以下问题:

复制代码
Does this timestamp fall into the range 2015 to 2016?
Is the status field set to "published"?

当查询子句传递给过滤参数时,例如 bool 查询中的 filter 或 must_not 参数,过滤上下文就会生效。

更多信息请参阅 Elasticsearch 文档中的过滤上下文。

示例:关键词过滤

下面示例展示如何向查询添加关键词过滤。

该示例根据标题向量检索与 "javascript books" 相似的热门图书,同时限制出版社为 Addison-Wesley。

复制代码
response = client.search(
    index="book_index",
    knn={
        "field": "title_vector",
        "query_vector": model.encode("javascript books"),
        "k": 10,
        "num_candidates": 100,
        "filter": {"term": {"publisher.keyword": "addison-wesley"}},
    },
)

pretty_response(response)

结果:

得分:0.6206549

标题:The Pragmatic Programmer:Your Journey to Mastery

简介:面向软件工程师和开发者的实用编程指南

出版社:addison-wesley

得分:0.56499225

标题:设计模式:可复用面向对象软件的基础

简介:适用于任何面向对象语言的设计模式指南

出版社:addison-wesley

相关推荐
Dawson Zhu6 分钟前
《Agentic Design Patterns》第 1 章导读:提示词链(Prompt Chaining)
人工智能·语言模型·架构·aigc·agi
DP DPharness8 分钟前
StudyMate 从安装到第一节课的完整操作路径
人工智能·websocket·网络协议·智能手机·dpharness
lingchen190615 分钟前
MATLAB 单引号 `‘` 和双引号 `“`
人工智能
hhzz22 分钟前
【YOLO 入门到精通 04】理解任务与模型家族:7 大 CV 任务与 YOLO26 选型指南
人工智能·python·深度学习·yolo·计算机视觉
2501_9336707929 分钟前
医药市场分析岗位适合应用统计学学生吗,需要考什么证
人工智能
水獭比特32 分钟前
Agent 工具审批不止一个按钮:用 v0.22.1 复盘入口、恢复和副作用
人工智能·python
网络毒刘33 分钟前
Token 用量观测实战:从会话结构拆前缀/工具/生成,建立个人降本仪表盘
人工智能·性能优化·token·cursor
库拉镜像AI牛牛34 分钟前
短剧内容自动化生产:知漫剧工作室落地教程
大数据·服务器·前端·人工智能·语音识别
IamZJT_34 分钟前
Agent 系统工程 09|Agent 的可靠性怎么测?成功率、恢复能力与成本
人工智能
AI你一生一世39 分钟前
当AI在数学中“失准”:一场关于形式化、直觉与工程取舍的深度复盘
人工智能·大语言模型·数学推理·ai对齐·形式化验证·推理模型·语义漂移