Elastic 向量搜索实战指南——Elastic中的模型管理与向量相关考量

本章内容简介

本章将概述 Hugging Face 生态系统、Elasticsearch 的 Eland Python 库,以及在 Elasticsearch 中使用嵌入模型的实用策略。

我们将从探索 Hugging Face 平台开始,介绍如何入门、选择合适模型,以及利用其庞大的数据集资源。同时深入讲解 Hugging Face 的 Spaces 功能及其高效使用方法。

接着,我们会介绍由 Elastic 开发的 Eland Python 库,并通过 Jupyter Notebook 示例演示其用法。

本章涵盖的主题包括:

  • Elastic 开发的 Eland Python 库
  • 索引映射(Index mappings)
  • 机器学习(ML)节点
  • 将 ML 模型集成到 Elasticsearch
  • 集群容量规划的关键要点
  • 存储效率策略,帮助优化 Elasticsearch 集群的性能与资源利用

技术需求

要实践本章所述内容,你需要具备以下环境:

Hugging Face

正如前言中简要提到的,Hugging Face 的主要目标是普及先进的自然语言处理(NLP)技术,使其在各行各业和各种应用中得到广泛采用。通过提供一个庞大的预训练模型库(截至撰写本文时超过12万个模型)、易用的 API 以及协作式的模型共享与微调环境,Hugging Face 赋能开发者和研究者轻松构建先进的语言处理应用。

在此基础上,Hugging Face 不仅提供丰富的模型资源,还确保了便捷的访问和高效的应用管理。其中一个突出特色便是 Model Hub

Model Hub

Hugging Face 针对研究人员和企业用户的需求,提供了包括 Model Hub 在内的多种资源和服务。Model Hub 是一个预训练模型的集中仓库,内置推理 API 方便用户轻松调用这些模型。

用户可以通过模型名称或任务类型搜索,并且可以根据任务类型、架构等多种条件筛选,快速找到适合自己应用场景的模型。图 3.1 展示了当我们搜索 "bert" 关键词时,返回的一部分可用模型列表:

选择某个模型后,你将获得该模型的详细信息。在页面顶部,你可以看到模型的任务类型、支持的各种架构、使用的库、支持的语言、许可协议等内容。图 3.2 展示了 bert-base-uncased 模型的相关信息:

大多数模型,尤其是较受欢迎的模型,都配有一个模型卡(Model Card)。模型卡由开发者提供,包含诸如模型的训练方法、详细描述、模型变体、相关模型、预期用途和限制、运行代码示例、已知偏差以及训练信息等内容。图 3.3 展示了 BERT 模型的训练相关信息:

在图 3.4 中,我们可以看到该模型在过去一个月的下载次数,以及通常最有用的功能------Hosted Inference API 的实时界面。Hosted Inference API 允许通过编程方式访问运行在 Hugging Face 共享基础设施上的模型,该接口接收输入,经过模型处理后返回输出。这个 API 界面让你可以通过提供输入来测试模型,并实时查看输出结果。

Hugging Face 数据集(Datasets)

正如前文提到的,Model Hub 非常重要,除此之外,我们还需关注支撑这些模型的基础数据。Hugging Face Datasets 是 NLP 和机器学习(ML)数据工具领域的基石。

Hugging Face Datasets 是一个简化下载、预处理和使用多样化数据集的库,适用于各种 NLP 和 ML 任务。该库提供了大量涵盖不同主题和语言的数据集,支持情感分析、翻译、摘要等多种应用场景。数据集来源包括科研论文、网页抓取和用户贡献,并以标准格式向社区开放。Datasets 库与 Transformers 库深度集成,方便用户将数据集与 NLP 模型结合进行训练、评估和部署。

使用 Hugging Face Datasets 十分简便。首先通过 pip install datasets 安装库。安装后,导入库并调用 load_dataset() 函数即可加载数据集。例如,加载 IMDb 影评数据集用于情感分析:

ini 复制代码
from datasets import load_dataset
imdb_dataset = load_dataset("imdb")

Hugging Face Datasets 还提供了过滤、映射和打乱等数据预处理功能,方便用户准备训练数据。下面示例展示如何使用 Transformers 库的预训练分词器对 IMDb 数据集进行分词处理:

ini 复制代码
from datasets import load_dataset
from transformers import AutoTokenizer

# 加载 IMDb 数据集的训练集前100条样本
imdb_dataset = load_dataset("imdb", split="train[:100]")

# 初始化分词器
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")

# 对数据集进行分词,自动截断与填充
tokenized_imdb_dataset = imdb_dataset.map(
    lambda x: tokenizer(x["text"], truncation=True, padding="max_length")
)

print(tokenized_imdb_dataset)

输出如下:

php 复制代码
Dataset({
    features: ['text', 'label', 'input_ids', 'token_type_ids', 'attention_mask'],
    num_rows: 100
})

可以获取第一条样本的分词 ID:

css 复制代码
first_row_tokens = tokenized_imdb_dataset[0]["input_ids"]

for token in first_row_tokens[:10]:
    print(f"Token: {token}, Word: {tokenizer.decode([token])}")

输出示例:

yaml 复制代码
Token: 101, Word: [CLS]
Token: 1045, Word: i
Token: 12524, Word: rented
Token: 1045, Word: i
Token: 2572, Word: am
Token: 8025, Word: curious
Token: 1011, Word: -
Token: 3756, Word: yellow
Token: 2013, Word: from
Token: 2026, Word: my

这表示当我们访问 imdb_dataset['text'][0] 时,得到了 IMDb 影评首条文本对应的前十个词元及其词义。

Hugging Face Spaces

Hugging Face Spaces 是一个便于部署、分享和协作 ML 模型的平台,尤其适合 NLP 领域。通过 Spaces,用户可以轻松将模型部署为基于 Gradio 或 Streamlit 的交互式网页应用,无需额外基础设施或复杂配置。Spaces 使得用户能展示作品、收集反馈,并与社区成员协作,是 ML 和 NLP 从业者和初学者的宝贵工具。

Spaces 与 Hugging Face 生态(包括 Transformers 库和 Model Hub)无缝集成,方便用户快速部署预训练或微调模型为交互式应用,让他人能够在线测试并深入了解模型能力。入门流程包括在 Hugging Face 官网注册账号,按照部署指南创建新 Space,通常涉及编写 Python 脚本,利用 Gradio 或 Streamlit 框架,从 Hugging Face Hub 导入模型,并定义交互界面。

以下是一个利用 Gradio 创建简单情感分析界面的示例代码:

ini 复制代码
import gradio as gr
from transformers import pipeline

sentiment_pipeline = pipeline("sentiment-analysis")

def sentiment_analysis(text):
    result = sentiment_pipeline(text)
    return result[0]["label"]

iface = gr.Interface(fn=sentiment_analysis, inputs="text", outputs="text")
iface.launch()

此示例中,使用 Hugging Face Transformers 库创建情感分析流水线,并定义 Gradio 界面接受文本输入,返回情感标签。将代码上传至 Hugging Face Space 后,用户即可通过网页浏览器与应用互动,使用自己的文本测试情感分析模型。Spaces 提供了简单而强大的方式,展示 ML 模型并促进社区协作。

虽然 Hugging Face Spaces 为 NLP 应用提供了极佳的平台,但 Hugging Face 并不是我们唯一可用的资源。接下来,让我们扩展工具箱,聚焦 Eland 及其在 Elasticsearch 与传统数据处理框架(如 pandas)之间搭建桥梁的作用。

Eland

Eland 是 Elastic 开发的一个 Python 库,允许用户无缝地与 Elasticsearch 交互,实现数据操作和分析。该库基于官方的 Elasticsearch Python 客户端,扩展了 pandas 的 API 到 Elasticsearch,使用户能用熟悉的 pandas 语法和惯例操作 Elasticsearch 中的数据,方便将 Elasticsearch 融入现有数据分析工作流和工具中。

Eland 对处理无法全部加载进内存、需要分布式处理的大型数据集尤为有用。Elasticsearch 可通过集群中多节点的水平扩展,支持远超单机能力的数据处理。以下是 Eland 的几个关键功能:

  • 查询 Elasticsearch 数据

    用户无需编写原生的 Elasticsearch 查询语句,而是用类似 pandas 的 Python 代码来过滤、排序和聚合数据。例如,从 Elasticsearch 索引中获取数据:

    ini 复制代码
    import eland as ed
    df = ed.DataFrame("http://localhost:9200", "my_index")
    filtered_df = df[df['some_field'] > 100]
  • 支持聚合操作

    可计算摘要统计或复杂聚合,帮助理解大数据集的分布和特征。例如计算某字段的平均值:

    ini 复制代码
    average_value = df['some_field'].mean()
  • 数据可视化集成

    Eland 无缝支持 Matplotlib、Seaborn 等常用 Python 可视化库。用户可轻松将数据转换为 pandas DataFrame,并制作图表。例如:

    ini 复制代码
    import seaborn as sns
    import pandas as pd
    
    filtered_df = df[df['some_field'] > 100]
    pandas_df = filtered_df.to_pandas()
    sns.boxplot(x='category', y='value', data=pandas_df)

最近,Eland 还扩展支持将 NLP Transformer 模型导入 Elasticsearch。Elasticsearch 目前支持部分基于 PyTorch 训练、符合标准 BERT 模型接口的架构,能够原生运行命名实体识别、零样本分类、问答等 NLP 任务。支持任务的最新列表见官方文档:www.elastic.co/guide/en/ma...

对本书最相关的是对文本嵌入模型的原生支持。Eland 可用于加载本地模型,或更常见的是直接从 Hugging Face 加载模型至 Elasticsearch。模型被下载并分块后,便可加载到 Elasticsearch 中。

接下来,我们将通过一个 Jupyter Notebook 演示如何配置 Elasticsearch 连接,加载 Hugging Face 模型,部署(启动)模型,并生成测试向量。

完整代码可在本书 GitHub 仓库第 3 章文件夹中的 Jupyter Notebook 查看:github.com/PacktPublis...。 在理解了 Eland 如何促进 Hugging Face 模型与 Elasticsearch 集成的基础上,我们将深入探讨代码的实际使用细节。接下来,将指导你如何高效地将 Hugging Face 的 Sentence Transformer 直接加载到 Elasticsearch,帮助你充分利用两者结合的强大能力。

从 Hugging Face 加载 Sentence Transformer 到 Elasticsearch

本段代码演示如何将 Hugging Face 上支持的嵌入模型加载到 Elastic Cloud(cloud.elastic.co/)上的 Elasticsearch 集群中。

首先,我们将安装并导入所需的 Python 库:

  • eland
  • elasticsearch
  • transformers
  • sentence_transformers
  • torch==1.11

Elastic 通过 Eland 库从 Hugging Face Hub 下载模型并加载到 Elasticsearch:

ini 复制代码
pip -q install eland elasticsearch transformers sentence_transformers torch==1.13
javascript 复制代码
from pathlib import Path
from eland.ml.pytorch import PyTorchModel
from eland.ml.pytorch.transformers import TransformerModel
from elasticsearch import Elasticsearch
from elasticsearch.client import MlClient

配置 Elasticsearch 认证

推荐使用 Elastic Cloud ID(详见:www.elastic.co/guide/en/cl...)和集群级API 密钥(详见:www.elastic.co/guide/en/ki...)进行认证。

你可以采用任何方式设置所需凭据。以下示例使用 getpass 交互式输入凭据,避免在 GitHub 上明文保存:

ini 复制代码
import getpass

es_cloud_id = getpass.getpass('Enter Elastic Cloud ID:  ')
es_api_id = getpass.getpass('Enter cluster API key ID:  ')
es_api_key = getpass.getpass('Enter cluster API key:  ')

# 连接到 Elastic Cloud
es = Elasticsearch(cloud_id=es_cloud_id, api_key=(es_api_id, es_api_key))

es.info()  # 返回集群信息,表示连接成功

从 Hugging Face Hub 加载模型

Elastic Stack 的机器学习功能支持符合标准 BERT 模型接口并使用 WordPiece 分词算法的 Transformer 模型。

关于 WordPiece 分词的深入介绍,可参见 Hugging Face NLP 课程的该章节:huggingface.co/learn/nlp-c...

通过模型页面旁边的复制图标获取模型链接,示例中使用的是 sentence-transformers/msmarco-MiniLM-L-12-v3 模型,链接为:huggingface.co/sentence-tr...

下载模型

在这里,我们需要指定 Hugging Face 的模型 ID。获取该 ID 最简便的方法是点击模型页面名称旁的复制图标,复制模型名称即可。

调用 TransformerModel 时,需要指定 Hugging Face 模型的 ID 和任务类型。你可以尝试指定 "auto",Eland 会根据模型配置中的信息自动推断任务类型。但这并非总是可行,因此可以显式指定任务类型,例如:

ini 复制代码
hf_model_id = 'sentence-transformers/msmarco-MiniLM-L-12-v3'
tm = TransformerModel(model_id=hf_model_id, task_type="text_embedding")

接下来,我们设置并确认模型 ID。为了让名称兼容 Elasticsearch,路径中的 / 会被替换为双下划线 __

ini 复制代码
es_model_id = tm.elasticsearch_model_id()

将模型导出为 Elasticsearch 使用的 TorchScript 格式:

ini 复制代码
tmp_path = "models"
Path(tmp_path).mkdir(parents=True, exist_ok=True)
model_path, config, vocab_path = tm.save(tmp_path)

将模型加载到 Elasticsearch

此时,模型会被序列化并加载进 Elasticsearch,以便在 ML 节点启动:

ini 复制代码
ptm = PyTorchModel(es, es_model_id)
ptm.import_model(model_path=model_path, config_path=None, vocab_path=vocab_path, config=config)

启动模型

此步骤会输出模型名称和状态等信息:

ini 复制代码
# 列出 Elasticsearch 中的模型
m = MlClient.get_trained_models(es, model_id=es_model_id)

部署模型

以下代码将在 ML 节点加载模型并启动服务,使其可用于 NLP 任务:

ini 复制代码
s = MlClient.start_trained_model_deployment(es, model_id=es_model_id)

接下来,验证模型是否正常启动:

scss 复制代码
stats = MlClient.get_trained_models_stats(es, model_id=es_model_id)
stats.body['trained_model_stats'][0]['deployment_stats']['nodes'][0]['routing_state']

你应该看到如下输出,表示模型已成功部署到 ML 节点:

arduino 复制代码
{'routing_state': 'started'}

为查询生成向量

在执行 kNN 查询前,需要将查询字符串转换为向量。

示例:创建一个测试查询句子以验证配置:

ini 复制代码
docs = [
    {
        "text_field": "Last week I upgraded my iOS version and ever since then my phone has been overheating whenever I use your app."
    }
]

调用 _infer 端点,传入模型 ID 和需要向量化的文档:

ini 复制代码
z = MlClient.infer_trained_model(es, model_id=es_model_id, docs=docs)

响应中可通过如下方式获取第一个文档的向量:

scss 复制代码
doc_0_vector = z['inference_results'][0]['predicted_value']
print(doc_0_vector)

模型加载并测试成功后,即可在生产环境中使用该嵌入模型。

在 Elasticsearch 中生成向量

向量可以在文档被索引(写入)到 Elasticsearch 之前,通过 ingest pipeline(摄取管道)生成。摄取管道中的每个处理器执行不同的任务,比如丰富数据、修改数据或删除数据。其中关键处理器是 inference 处理器,它会将文本传递给嵌入模型,接收向量表示,然后将该向量添加到原始文档载荷中,之后将文档继续传递至索引存储。这样可以确保文档的向量表示在摄取后即可用于索引和搜索。

下面是一个使用 inference 处理器的 Elasticsearch ingest pipeline 配置示例,该管道使用之前加载的 sentence-transformers/msmarco-MiniLM-L-12-v3 模型。该管道从输入文档中获取名为 summary 的字段,利用嵌入模型生成向量,并将结果存储到名为 vector 的字段中。

此处演示使用 HTTP 请求而非 Python 代码,模型假定已经加载到集群中(此前示例已完成该步骤):

bash 复制代码
PUT _ingest/pipeline/embedding_book_pipeline
{
  "description": "Ingest pipeline using bert-base-uncased model",
  "processors": [
    {
      "inference": {
        "target_field": "vector",
        "model_id": "sentence-transformers__msmarco-minilm-l-12-v3",
        "field_map": {
          "summary": "text_field"
        }
      }
    }
  ]
}

使用该摄取管道索引文档时,在索引请求中包含 pipeline 参数:

bash 复制代码
PUT my_index/_doc/1?pipeline=embedding_book_pipeline
{
  "summary": "This is an example text for the embedding model."
}

该请求将索引包含文本字段的文档,同时增加由 bert-base-uncased 模型生成的向量字段。

_infer 端点也可以直接调用,用于在查询时(见下一节)或其它临时需求时生成向量:

bash 复制代码
POST _ml/trained_models/sentence-transformers__msmarco-minilm-l-12-v3/_infer
{
  "docs": [
    {"text_field": "How do I load an NLP model from Hugging Face"}
  ]
}

返回示例(HTTP 200,含文本向量表示):

json 复制代码
{
  "inference_results": [
    {
      "predicted_value": [
        0.012806695885956287,
        -0.045506563037633896,
        0.014663844369351864,
        ...
        0.002125605707988143,
        0.06371472030878067
      ]
    }
  ]
}

查询时生成向量并执行 kNN 搜索

查询时,可调用 _infer 端点将输入文本转换为向量,再将该向量作为 kNN 查询的一部分。以下示例使用之前生成的向量执行 kNN 查询:

sql 复制代码
GET search-elastic-docs-vectorized/_search
{
  "knn": {
    "field": "body_content-vector",
    "k": 1,
    "num_candidates": 20,
    "query_vector": [
      0.012806695885956287,
      -0.045506563037633896,
      0.014663844369351864,
      ...
      0.002125605707988143,
      0.06371472030878067
    ]
  },
  "fields": ["body_content", "headings"],
  "_source": false
}

从 Elasticsearch 8.7 开始,可以直接在 _knn 查询中指定嵌入模型,Elasticsearch 会先生成查询向量,再执行后续 kNN 查询。这样能更简化向量搜索流程,实现更顺畅的集成:

sql 复制代码
GET search-elastic-docs-vectorized/_search
{
  "knn": {
    "field": "dense-vector-field",
    "k": 10,
    "num_candidates": 100,
    "query_vector_builder": {
      "text_embedding": {
        "model_id": "my-text-embedding-model",
        "model_text": "The opposite of blue"
      }
    }
  }
}

接下来,我们将探讨如何规划生产环境中的集群容量和资源。

集群容量和资源规划

在任何生产环境中,尤其是实现大规模向量搜索时,规划足够的集群容量和资源至关重要。为了确保最佳性能和效率,必须进行细致的考虑、规划和测试。

在下一章,我们将深入探讨负载测试,这是微调和优化 Elasticsearch 部署的重要环节。但在此之前,我们先了解在 Elasticsearch 中的 ML 节点上运行嵌入模型所需的条件,重点讨论如何在性能与资源利用之间找到平衡。本节将介绍 CPU、内存和磁盘等关键资源的需求,为理解 Elasticsearch 资源管理奠定基础。

CPU 和内存需求

Elasticsearch 中向量搜索的 CPU 需求与传统搜索相差不大,主要区别在于近似 kNN 查询时对向量数据的访问方式。

在传统搜索和暴力/精确匹配向量搜索中,随着文档数量增加,磁盘空间会水平扩展,CPU 也会扩充以支持更高吞吐。

kNN 搜索中同理,但为了获得最快响应,理想情况是有足够的堆外(off-heap)内存将所有向量嵌入加载至内存。若 Elasticsearch JVM 以外的堆外内存不足以容纳所有嵌入,则需频繁从磁盘读取,导致响应变慢。这种数据访问方式转变带来了性能提升,因为观察到当向量必须从磁盘读取而非直接从内存时,热线程(hot threads)更容易出现。通过直接访问堆外内存,向量搜索减少了热线程的影响,查询过程更高效。

备注
hot_threads 是一个诊断 API,用于识别 Elasticsearch 节点中当前占用 CPU 资源异常高的线程,有助于发现性能瓶颈。

由于页缓存内存对 kNN 搜索尤为重要,用户需要关注单节点可用的页缓存内存能容纳多少向量。假设使用默认的分层可导航小世界(HNSW)配置,所需内存粗略估算公式为:

scss 复制代码
num_vectors * 4 * (num_dimensions + 12) = 总 RAM 字节数

举例:使用输出 512 维向量的模型,预期向 Elasticsearch 加载 2000 万向量,计算如下:

scss 复制代码
20,000,000 * 4 * (512 + 12) = 41,920,000,000 字节 ≈ 39 GB 内存

2000 万个嵌入字段粗略估算需要约 39GB 堆外内存,以实现 kNN 搜索的最佳性能。若单个 Elasticsearch 节点配置为 64GB 内存(其中约一半给 JVM,若干 GB 给操作系统及其他进程),则初步估计需要至少 2 个数据节点来处理这些向量。此估算未包含副本数量。

此为起始估计,适合测试初期参考,非精确内存需求。后续章节的测试将为你提供更准确的生产环境数据。

磁盘需求

保证有足够的堆外内存存储向量是高效 kNN 搜索的关键,但向量本身仍然存储于磁盘。因此,需要确保磁盘容量充足。不过,由于磁盘容量通常远大于内存容量,磁盘容量一般不会成为集群容量规划中的瓶颈。

向量数据的磁盘容量估算可用与内存估算相同的计算方式,但需考虑与向量一同存储的其它字段,如文本、附加文本字段及元数据。

磁盘容量的粗略初始估算公式:

复制代码
单个文档大小 * 文档数 * 副本数 * 1.2

其中,1.2 是 JSON 扩展与磁盘压缩之间的净乘数。请注意,这只是非常粗略的估计。

真正了解数据在 Elasticsearch 中行为的唯一方法是加载样本数据集。你可以免费试用 Elastic Cloud,快速搭建小型集群并导入代表性数据集。试用启动快速,仅需邮箱注册:cloud.elastic.co/registratio...

你将获得一个包含 ML 节点及企业级功能的全功能小型 Elasticsearch 集群。Elastic 官网的入门指南(www.elastic.co/getting-sta...)可帮助你快速上手。

加载数据后,你可准确看到数据实际扩展的大小,这是规划生产需求的唯一可靠方式。即使试用期结束,也可以通过灵活的按月付费方式逐步扩展集群规模。

磁盘使用分析 API

加载向量及其它数据后,可使用分析索引磁盘使用情况 API 来计算占用的磁盘空间。关于该 API 详情,请参阅官方文档:www.elastic.co/guide/en/el...

示例中,search-elastic-docs 索引含多个相对较小的数据向量字段,其中之一为 title-vector

ini 复制代码
POST /search-elastic-docs/_disk_usage?run_expensive_tasks=true&pretty

返回(API 全响应较长,此处仅展示部分相关内容):

json 复制代码
{
  "_shards": {
    "total": 2,
    "successful": 2,
    "failed": 0
  },
  "search-elastic-docs": {
    "store_size": "989.5mb",
    "store_size_in_bytes": 1037592858,
    "all_fields": {
      "total": "987.6mb",
      "total_in_bytes": 1035596641,
      "inverted_index": {
        "total": "69.8mb",
        "total_in_bytes": 73269048
      },
      ...
      "knn_vectors": "83.4mb",
      "knn_vectors_in_bytes": 87462952
    },
    "fields": {
      ...
      "title-vector": {
        "total": "40mb",
        "total_in_bytes": 42046916,
        "inverted_index": {
          "total": "0b",
          "total_in_bytes": 0
        },
        ...
        "knn_vectors": "40mb",
        "knn_vectors_in_bytes": 42046916
      },
      ...
    }
  }
}

从中可见向量占用约 83.4 MB 磁盘空间。完成磁盘使用分析后,我们将转向 ML 节点容量规划。

ML 节点容量规划

在 Elasticsearch 中运行嵌入模型的 ML 节点上,您需要规划确保节点在推理时拥有足够的容量。Elastic Cloud 支持基于 CPU 需求的 ML 节点自动扩缩容,当需要更多计算资源时节点会自动扩展,需求降低时则缩减。

我们将在下一章更详细地讲解 ML 节点的推理调优,但至少您需要一个具有足够内存的 ML 节点,以加载至少一个嵌入模型实例。随着性能需求的增加,您可以增加单个模型的分配数以及每个分配的线程数。

要检查模型大小和加载模型所需内存,您可以调用获取已训练模型统计的 API(详情见:www.elastic.co/guide/en/el...):

bash 复制代码
GET _ml/trained_models/dslim__bert-base-ner-uncased/_stats?human

返回示例包含模型大小和内存需求:

json 复制代码
{
  "model_id": "dslim__bert-base-ner-uncased",
  "model_size_stats": {
    "model_size": "415.4mb",
    "model_size_bytes": 435663717,
    "required_native_memory": "1gb",
    "required_native_memory_bytes": 1122985674
  }
}

当模型运行时,也可在 Kibana 的"Machine Learning > Model Management > Trained Models"页面查看模型状态。

规划 ML 节点容量时需考虑两种情况:

  • 索引时:通常会有一批大量文档批量导入到 Elasticsearch 索引,这时需要大量推理以生成向量。能灵活扩缩 ML 节点和模型分配数,方便快速响应批量导入负载,导入完成后可缩减资源。
  • 搜索时推理:用户发送查询时,需要使用相同嵌入模型生成查询向量。根据平均查询负载规划正常运行容量;遇到负载高峰(如电商大促),可提前增加模型分配和扩容 ML 节点。

测试这两种情况的唯一可靠方法是负载测试和性能调优,下一章会详细介绍。

存储效率策略

随着向量搜索生产数据集规模增长,存储和及时搜索这些向量所需的资源也增加。本节介绍几种减小资源需求的策略,每种都有权衡,需谨慎测试后再投入生产。

降维

降维是将高维数据转换成低维表示的过程,常用于缓解"维度灾难"(en.wikipedia.org/wiki/Curse_...)带来的挑战。常见方法有主成分分析(PCA)和 t-SNE。降维可以提高 kNN 向量搜索的效率和效果,但也存在利弊。

优势包括:

  • 降低存储、内存和计算资源需求。高维向量数据占用大量 RAM 和 CPU,降维后减少堆外内存和计算,提升 kNN 搜索效率,适合资源受限环境。
  • 缓解维度灾难。高维空间中数据点变得稀疏,基于距离的算法性能下降。降维后保留重要信息,使距离搜索更有效。

劣势包括:

  • 信息丢失。降维常伴随压缩或近似,可能导致细微关系丢失,影响 kNN 搜索准确度。
  • 增加复杂度和计算开销。降维需额外计算,参数调优复杂,需根据数据特性和问题领域选择合适方法。

示例:使用四维的鸢尾花(Iris)数据集,通过 PCA 将维度降至二维,并作图展示:

ini 复制代码
import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.decomposition import PCA

# 加载鸢尾花数据集
iris = datasets.load_iris()
X = iris.data
y = iris.target

# 应用 PCA 降维
pca = PCA(n_components=2)
X_reduced = pca.fit_transform(X)

# 原始数据散点图
plt.scatter(X[:, 0], X[:, 1], c=y, cmap=plt.cm.Set1, edgecolor='k')
plt.xlabel('Sepal length')
plt.ylabel('Sepal width')
plt.title('Original Iris dataset')
plt.show()

# 降维后数据散点图
plt.scatter(X_reduced[:, 0], X_reduced[:, 1], c=y, cmap=plt.cm.Set1, edgecolor='k')
plt.xlabel('First Principal Component')
plt.ylabel('Second Principal Component')
plt.title('Iris dataset after PCA')
plt.show()

图 3.7 中展示了原始数据与降维数据的分布。降维会带来一定信息损失,但能简化数据处理,提升基于距离的算法(如 kNN 搜索)性能。

或者,有些预训练模型会提供不同维度大小的版本。如果你刚开始使用带有这些选项的预训练模型,建议先测试较小维度的版本,再考虑自己做降维。

量化(Quantization)

量化是一种在向量搜索中用来减少高维数据大小和复杂度的技术,同时保持执行有意义搜索的能力。量化过程通常是将连续的向量值(通常为浮点数)转换成更小、更紧凑的表示形式,比如整数。这样转换后,存储和计算需求都会减少,从而实现更高效的搜索和检索。

从浮点数表示转换到 8 位向量,能显著减小数据大小和存储需求。浮点数一般用 32 或 64 位表示一个值,而 8 位向量每个值仅用 8 位。这种体积缩减能大幅节省存储和内存,便于处理大规模向量搜索。但代价是精度和准确性的降低,因为量化过程是用较少的离散级别来近似原始浮点值,可能导致量化向量间的距离不能完美反映高维空间中数据点的真实关系,从而影响搜索相关性。

下面用 scikit-learn 库中的 Public Digits 数据集(MNIST 的小版本,包含 8x8 像素的手写数字图像)演示量化过程。该数据集足够小,可在笔记本电脑上运行,无需大量内存或计算资源。我们先用 PCA 降维,再对降维后的向量做量化:

ini 复制代码
import numpy as np
from sklearn import datasets
from sklearn.decomposition import PCA
from sklearn.preprocessing import MinMaxScaler, QuantileTransformer

# 加载 digits 数据集
digits = datasets.load_digits()
X = digits.data

print("Original dataset (first example):\n", X[0])

# PCA 降维到 10 维
pca = PCA(n_components=10)
X_reduced = pca.fit_transform(X)

print("\nReduced dataset after PCA (first example):\n", X_reduced[0])

# 归一化到 [0, 255] 区间
scaler = MinMaxScaler((0, 255))
X_scaled = scaler.fit_transform(X_reduced)

print("\nScaled dataset after normalization (first example):\n", X_scaled[0])

# 量化成 8 位整数
X_quantized = np.round(X_scaled).astype(np.uint8)

print("\nQuantized dataset (first example):\n", X_quantized[0])

输出示例:

ini 复制代码
Original dataset (first example): [ 0. 0. 5. 13. 9. 1. 0. 0. ...]
Reduced dataset after PCA (first example): [-1.25943119 21.27487713 -9.4630716 13.01409073 ...]
Scaled dataset after normalization (first example): [121.31621096 215.95604417 81.58062648 168.0966397 ...]
Quantized dataset (first example): [121 216 82 168 78 74 133 136 ...]

正如前面所述,这个过程虽能大幅提升存储和性能,但也可能导致精度和搜索相关性降低。用户应在投入生产前,测试量化前后的搜索结果相关性,确保可接受。

降维与量化的区别

降维和量化都是优化嵌入模型和向量表示的技术,但针对不同方面。降维主要减少向量的维度数量;量化则降低向量中每个元素的数值精度。两者都能提升效率,减少内存占用,但可能引入信息或精度损失,影响搜索准确度和质量。

dense_vector 与 _source 字段的关系及影响

系统效率不仅取决于数据本身,还依赖于数据的存储与访问方式。在 Elasticsearch 向量搜索中,_source 字段尤为关键,特别是处理高维数据时。

从 _source 中排除 dense_vector

Elasticsearch 默认会把索引时传入的原始 JSON 文档存储在 _source 字段中,搜索结果会返回完整文档。但若文档含有高维 dense_vector 字段,_source 体积会非常大,导致加载开销显著,影响 kNN 搜索速度。

为缓解这一问题,可通过 mapping 中的 excludes 参数禁用 dense_vector 字段的 _source 存储。这样搜索时不会加载和返回该向量字段,减小处理开销和索引体积。但搜索时仍可用向量执行 kNN 查询,因为搜索使用的是独立的数据结构。

禁用 _source 存储的缺点是,某些基于原始字段值的文档操作(如部分更新)可能受限。

示例 mapping 设置如下:

json 复制代码
PUT my_index
{
  "mappings": {
    "_source": {
      "excludes": [
        "vector_field"
      ]
    },
    "properties": {
      "vector_field": {
        "type": "dense_vector",
        "dims": 384,
        "index": true,
        "similarity": "dot_product"
      },
      "text_field": {
        "type": "text"
      }
    }
  }
}

除非你计划未来重建索引,否则对于含有 dense_vector 字段的索引,强烈建议开启 excludes,既节省空间又提升 kNN 搜索性能。

总结:从 _source 中排除 dense_vector 字段能显著提升 kNN 搜索速度,减少索引大小。但需权衡对未来索引操作的影响,选择最适合业务需求的方案。

总结

本章深入探讨了 Hugging Face 生态系统及 Elasticsearch 的 Eland Python 库的强大功能,提供了在 Elasticsearch 中使用嵌入模型的实用示例。我们介绍了 Hugging Face 平台,重点讲解了其数据集、模型选择以及 Spaces 平台的潜力。此外,通过实操演示了 Eland 库的用法,涵盖了索引映射、机器学习节点(ML nodes)以及模型集成等关键环节。我们还探讨了集群容量规划的细节,强调了内存(RAM)、磁盘容量和 CPU 资源的考量。最后,介绍了多种存储优化策略,聚焦于降维、量化及映射配置,以确保 Elasticsearch 集群在性能和资源利用上的最佳平衡。

下一章,我们将进入数据操作的实际阶段,学习如何调优推理和查询性能。

相关推荐
6confim15 分钟前
超越代码的未来:对话Cursor CEO,AI时代工程师的终极进化
llm·cursor·trae
阿里云大数据AI技术1 小时前
DistilQwen-ThoughtX蒸馏模型在PAI-ModelGallery的训练、评测、压缩及部署实践
大数据·开源·llm
Smaller、FL4 小时前
介绍MCP的背景及流程
人工智能·自然语言处理·llm·deepseek·mcp
玩转AGI5 小时前
面试篇-一文搞定 Agent
人工智能·程序员·llm
DeepSeek忠实粉丝6 小时前
Deepseek篇--开源技术3FS & smallpond详解
人工智能·程序员·llm
亲爱的非洲野猪15 小时前
基于ElasticSearch的法律法规检索系统架构实践
大数据·elasticsearch·系统架构
XW17 小时前
java mcp client调用 (modelcontextprotocol)
java·llm
从零开始学习人工智能20 小时前
Doris 与 Elasticsearch:谁更适合你的数据分析需求?
大数据·elasticsearch·数据分析
用户7112839284720 小时前
什么?大模型删库跑路了?
langchain·llm
代码搬运媛1 天前
ES Modules 与 CommonJS 的核心区别详解
大数据·elasticsearch·搜索引擎