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 查询中的 filtermust_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

相关推荐
蓝速科技1 分钟前
蓝速桌面 AI 双屏翻译机:重塑企业涉外接待门面与沟通效率
人工智能
浪兎兎2 分钟前
【微调】(案例)SpeedWolf 狼人杀策略分析模型
人工智能·pytorch
tachibana23 分钟前
RAGAS 指标解读
数据库·人工智能·算法·机器学习·架构·大模型·llm
能源革命8 分钟前
AI 日报 2026-08-23|AI Coding + 具身智能重点速览
人工智能
suaizai_22 分钟前
AI Agent如何懂你:四层能力拆解
java·前端·人工智能
BruceZong26 分钟前
人工智能领域常见名词及其解释
人工智能
FL162386312933 分钟前
电力场景变电站火灾检测数据集VOC+YOLO格式564张2类别
人工智能·yolo·机器学习
ZhengEnCi33 分钟前
DeepSeek V4 Flash Vision Exp 全面调研:好不好用、值不值得用、性价比高不高
人工智能
AI_Auto39 分钟前
中小企业数字化转型指南 | 拆解中小企业数字化四大转型特征
大数据·人工智能·制造
武子康43 分钟前
单卡 A6000 跑 27B FP8:我把“能跑”拆成了五组证据
人工智能·llm·agent