【学习笔记】Milvus 如何完成一次向量相似度检索,Milvus 索引到底应该怎么选?-15

在 RAG 项目里,"向量检索"经常被一句 search() 代码带过。但要真正排查召回为空、结果不准和延迟升高,必须理解一次检索内部发生了什么。

可以把它概括为:查询向量进入 Milvus,经过集合和索引定位候选,再计算距离、执行过滤,最后返回 Top K 结果。

如果把一次查询放回部署架构中,关键路径可以这样看:

复制代码
PyMilvus / REST 客户端
          ↓
Milvus Proxy:接收请求、校验参数、路由请求
          ↓
Query Coordinator / Query Node:确定数据范围并执行检索
          ├── 从 etcd 获取集合、分区、索引和节点状态等元数据
          └── 从 MinIO / 本地缓存加载数据文件和索引文件
          ↓
返回 Top K 结果

这里的 etcd 和 MinIO 不在应用的 search() 调用里直接出现,但它们会影响 Milvus 是否知道"该查什么"和"到哪里读取索引"。因此,检索报错时不能只盯着 Python 参数,也要检查依赖服务和数据卷。

一、一次检索的完整链路

复制代码
用户问题
   ↓
Embedding 模型生成 query_vector
   ↓
Proxy 接收请求并校验参数
   ↓
Query Coordinator 确定目标数据范围
   ↓
Query Node 加载索引并搜索候选向量
   ↓
执行标量过滤与 Top K 合并
   ↓
返回 id、distance 和 output_fields

不同部署模式下组件名称和内部实现会有所差异,但从应用开发者视角,最重要的是确认四件事:集合是否存在、数据是否已加载、向量维度是否正确、索引与距离指标是否匹配。

二、相似度到底如何计算?

2.1. L2 距离

L2 是欧氏距离,距离越小通常表示越相近:

复制代码
d(x, y) = sqrt(sum((x_i - y_i)^2))

2.2. 内积 IP

内积常用于已经做过归一化或对方向相似度有要求的向量。不同模型对向量长度的含义不同,不能只凭经验选择。

2.3. 余弦相似度 COSINE

余弦相似度关注向量方向,常用于文本语义检索。使用时要保证写入和查询采用一致的向量预处理方式。

最重要的原则是:

写入向量、查询向量、索引 metric_type 必须属于同一套约定。

三、一个可复现的搜索示例

复制代码
from pymilvus import MilvusClient

client = MilvusClient(uri="http://localhost:19530")

query_vector = [0.01, 0.12, 0.34]  # 仅示例,维度必须匹配集合

results = client.search(
    collection_name="knowledge_chunks",
    data=[query_vector],
    anns_field="embedding",
    limit=5,
    filter='tenant_id == "tenant-a" and status == "published"',
    output_fields=["document_id", "chunk_id", "text"],
    search_params={"params": {"ef": 64}},
)

for hits in results:
    for hit in hits:
        print({
            "id": hit["id"],
            "distance": hit["distance"],
            "entity": hit.get("entity", {}),
        })

不同索引的 search_params 不同,ef 是 HNSW 的搜索参数,不能照搬到 IVF 或其他索引。代码示例用于说明调用结构,实际参数要与创建的索引一致。

四、Milvus 会先做过滤还是先做向量搜索?

应用通常会同时提供向量和标量过滤条件,例如:

复制代码
相似于"差旅报销"的内容
并且 tenant_id = tenant-a
并且 status = published

过滤和向量搜索的执行方式受索引、数据规模和查询计划影响。工程上不要假定"过滤一定先执行"或"向量一定先执行",而应通过真实数据压测观察:

  • 过滤字段的选择性;

  • 过滤后候选数量;

  • Top K 大小;

  • 查询延迟和资源使用;

  • 跨分区搜索数量。

如果过滤字段决定权限范围,必须确保过滤表达式由可信服务生成,不能直接拼接用户输入。

五、Top K 不是越大越好

RAG 经常把 Top K 设置成 5、10 或 20,但 K 越大并不一定提高答案质量:

  • 召回内容太少,可能遗漏关键事实;

  • 召回内容太多,会挤压大模型上下文;

  • 相似但重复的 Chunk 会降低信息密度;

  • 低相关片段会增加模型误判机会。

通常应把 Milvus 的 Top K、去重、Reranker 和最终送入大模型的上下文数量分开设计:

复制代码
Milvus 召回 Top 20
   ↓
按文档或章节去重
   ↓
Reranker 重排
   ↓
选 Top 5 进入 Prompt

六、为什么返回 distance 不能直接当答案可信度?

距离是向量空间中的数值,不等于事实正确率。它会受以下因素影响:

  • Embedding 模型;

  • 文本切分方式;

  • 距离指标;

  • 文档领域;

  • 查询表达方式;

  • 向量是否归一化。

因此,阈值需要用标注数据校准。不要直接认为"距离小于某个固定值就一定相关"。

七、检索不到数据时的排查顺序

7.1第一步:确认集合和字段

检查 Collection 名称、向量字段名、主键字段和输出字段是否与代码一致。

7.2 第二步:确认向量维度

把 Embedding 模型输出的 len(vector) 与 Schema 中的 dim 比较。维度不一致时,插入和搜索都会失败或无法得到预期结果。

7.3 第三步:确认数据已经加载

Milvus 搜索前需要让目标 Collection 或 Partition 处于可搜索状态。未加载时,客户端通常会返回相应错误。

7.4 第四步:去掉过滤条件做对照

先搜索全量范围,再逐个加上租户、版本和状态条件。如果去掉过滤后有结果,问题往往出在表达式、字段值或数据类型上。

7.5第五步:检查距离指标和模型

确认写入向量和查询向量来自同一 Embedding 模型,且索引使用的 metric 与查询预期一致。

7.6第六步:检查 Milvus 依赖服务

在 Standalone Docker Compose 部署中,可先查看:

复制代码
docker compose ps
docker compose logs --tail 100 standalone
docker compose logs --tail 100 etcd
docker compose logs --tail 100 minio

常见判断方式如下:

  • etcd 不健康:Milvus 可能无法读取集合、索引或节点状态等元数据;

  • MinIO 不健康或数据卷不可用:Milvus 可能无法读取索引文件或持久化数据;

  • Milvus Standalone 不健康:客户端无法正常建立连接或执行查询;

  • 三者都正常但仍然空结果:继续检查集合加载、过滤条件、向量模型和一致性设置。

不要直接修改 etcd 数据,也不要只删除 MinIO 中看起来"没用"的文件。它们由 Milvus 的元数据和文件布局共同管理,手工删除可能造成元数据与对象不一致。

八、如何看待一致性?

刚写入的数据不一定在所有查询节点上立即可见。Milvus 支持 Strong、Bounded、Session 和 Eventually 等一致性级别。默认级别和具体版本有关,生产项目应明确配置并测试。

例如,知识库上传后立即要求用户搜索到新文档,可以在这个确认链路使用更高的一致性级别;推荐系统等可接受短暂延迟的场景,则可以使用更低延迟的策略。

不要把"搜索不到刚写入的记录"直接判断为写入失败,先检查写入结果、刷新状态和一致性设置。

九、检索质量如何验证?

建立一组有标准答案的问题,记录:

  • Recall@K:相关片段是否被召回;

  • MRR 或 NDCG:相关结果排名是否靠前;

  • P50、P95、P99 延迟;

  • 过滤条件下的有效召回数量;

  • 最终回答的引用正确率。

把"检索正确"和"回答正确"分开评估,才能知道问题出在 Embedding、Milvus、Reranker 还是大模型生成环节。

向量检索慢,很多人的第一反应是"换一个更高级的索引"。但索引不是越复杂越好。它本质上是在搜索精度、查询延迟、内存占用、构建时间和更新成本之间做取舍。

本文先理解 FLAT、IVF_FLAT 和 HNSW,再给出适合 RAG 的选择方法。

先补一个部署层面的认识:索引不是只存在于 Milvus 进程的内存里。Milvus 会在索引节点或相关服务中构建索引,并将持久化索引文件写入配置的对象存储,例如 MinIO 或 S3;集合、字段和索引状态等元数据由 Milvus 通过 etcd 管理。Standalone 会把这些服务角色集中到单机部署中,Distributed 则允许查询、数据和索引计算分别扩展。

这意味着"索引创建成功"至少要包含三个层面:API 请求成功、索引元数据状态正确、索引文件已经能够被查询节点加载。只看创建接口返回值,不足以证明生产检索已经使用了新索引。

十一、FLAT:最直接的精确搜索

FLAT 不建立近似索引,而是将查询向量与候选向量逐一计算距离,再取 Top K:

复制代码
查询向量 q
   ↓
与所有向量计算距离
   ↓
排序并返回 Top K

它的优点是结果精确、行为容易理解、参数少,适合:

  • 数据规模较小;

  • 评测基线;

  • 对召回精度要求极高;

  • 需要验证近似索引损失了多少召回率。

缺点也很明显:向量越多,每次查询需要比较的候选越多,延迟和资源消耗会上升。

十二、IVF_FLAT:先聚类,再搜索部分区域

IVF_FLAT 会先把向量划分到多个聚类中心附近。搜索时,先找到最接近查询向量的若干个聚类,再只在这些聚类中计算精确距离。

复制代码
全部向量
   ↓ 建立 nlist 个聚类
聚类中心 1、2、3......
   ↓ 查询时选择 nprobe 个聚类
在候选聚类中精确计算距离

常见参数:

  • nlist:聚类数量,影响索引构建和搜索空间;

  • nprobe:每次搜索探测的聚类数量,越大通常召回率越高但延迟也更高。

一个示例:

复制代码
index_params.add_index(
    field_name="embedding",
    index_type="IVF_FLAT",
    metric_type="COSINE",
    params={"nlist": 1024},
)

search_params = {
    "params": {"nprobe": 16}
}

nlist 和 nprobe 不是固定公式。应该用真实数据和问题集压测,而不是直接套用某篇文章的参数。

十三、HNSW:基于图的近似搜索

HNSW 将向量组织成分层邻近图。搜索时从较高层开始快速接近目标,再在较低层细化结果。

常见构建和搜索参数:

  • M:每个节点维护的连接数量,影响内存和图质量;

  • efConstruction:构建索引时考虑的候选数量;

  • ef:搜索时考虑的候选数量,通常越大召回率越高,但查询更慢。

示例:

复制代码
index_params.add_index(
    field_name="embedding",
    index_type="HNSW",
    metric_type="COSINE",
    params={"M": 16, "efConstruction": 200},
)

search_params = {
    "params": {"ef": 64}
}

HNSW 的优势是较低延迟和较好的召回表现,代价是更高的内存占用、构建成本和参数复杂度。

十四、三种索引如何比较?

索引 搜索方式 主要优点 主要代价 常见用途
FLAT 全量比较 精确、简单 数据大时慢 基线、小数据
IVF_FLAT 聚类后搜索部分区域 参数直观、可控 需要调 nlist/nprobe 中等规模检索
HNSW 邻近图搜索 低延迟、召回较好 内存和构建成本较高 低延迟在线检索

这里的"中等规模"和"低延迟"不能脱离机器配置、向量维度和并发量理解。

十五、RAG 项目应该怎么选?

15.1. 先用 FLAT 建立基线

先用 FLAT 得到近似"理想召回结果",再对比 IVF 或 HNSW 的 Recall@K。没有基线,就无法知道换索引后到底损失了多少相关片段。

15.2. 低并发和小数据不必过度优化

知识库只有几万条 Chunk 时,索引优化可能带来的收益不大,反而增加调参和运维成本。先把文本切分、权限过滤和答案引用做好。

15.3. 对 P95 延迟敏感时考虑 HNSW

在线客服、搜索助手等场景通常更关注稳定低延迟。HNSW 可以作为候选方案,但必须关注内存、构建时间和更新模式。

15.4. 数据规模和负载变化明显时考虑 IVF

IVF 的搜索范围可以通过 nprobe 调节,适合需要在召回率和延迟之间做显式控制的场景。

十六、不要只看索引名称

一个真实的压测矩阵至少包括:

复制代码
索引类型
× Top K
× 向量规模
× 查询并发
× 过滤条件
× 向量维度
× 写入更新比例

每组记录:

  • Recall@K;

  • P50/P95/P99 延迟;

  • CPU、内存和磁盘使用;

  • 索引构建时间;

  • 新数据可搜索延迟;

  • 更新和删除后的检索质量。

同时记录索引构建期间的对象存储读写、节点 CPU/内存和磁盘压力。MinIO 或 S3 延迟过高时,可能表现为索引构建慢、加载慢或查询节点反复等待;etcd 延迟或不可用时,则可能表现为集合、索引状态读取失败。此时单纯调大 nprobe 或 ef 并不能解决根因。

十七、索引与距离指标必须匹配

索引类型解决"怎么快速搜索",距离指标解决"什么叫相似"。二者是不同维度的问题。

例如,使用余弦距离时,应确保 Embedding 模型和预处理策略适配余弦相似度;如果写入时做了归一化、查询时没有做归一化,结果就可能发生偏移。

十八、几个常见误区

18.1 误区一:HNSW 一定比 IVF 快

实际结果取决于数据、参数、内存和并发。HNSW 可能在延迟上占优,也可能因为内存压力导致整体服务变慢。

18.2 误区二:nprobe 越大越好

nprobe 增大通常会扩大候选范围,但也会增加查询成本。它应以 Recall 与延迟的平衡点为目标。

18.3 误区三:索引能修复 Embedding 质量问题

如果模型不理解领域术语、文本切分错误或查询改写不合理,换索引不会从根本上解决召回问题。

18.4 误区四:只用平均延迟做判断

在线系统更应该关注 P95、P99 和高峰期尾延迟。平均值可能掩盖偶发的长查询。

十九、推荐的落地顺序

复制代码
FLAT 建立召回基线
   ↓
选择 IVF_FLAT 或 HNSW
   ↓
用真实问题集调参
   ↓
压测过滤、并发和更新
   ↓
观察生产指标
   ↓
必要时重新评估索引

索引不是一次性配置。随着数据规模、模型版本和业务流量变化,参数也可能需要重新评估。

一个可执行的索引验证流程

复制代码
1. 用 FLAT 建立 Recall@K 基线
2. 创建 IVF/HNSW 索引
3. 等待索引状态可用
4. 重新加载集合或确认查询节点已加载索引
5. 用同一批问题比较召回率与 P95 延迟
6. 记录 MinIO/S3、etcd、Milvus 节点的资源指标

在本地 Standalone 环境,这个流程适合验证代码和参数;它不能代表 Distributed 集群的最终容量。上线前仍要在接近生产的数据规模、过滤条件和并发下复测。

二十、结语

1、一次向量检索不是简单的"传入向量、返回结果",而是模型、Schema、索引、过滤、一致性和资源状态共同作用的结果。掌握这条链路,才能在出现空结果、错结果和慢查询时快速定位。

2、FLAT 适合作为精确基线,IVF_FLAT 适合通过候选聚类控制搜索范围,HNSW 适合对低延迟和较高召回有要求的在线检索。真正的选型方式是建立评测集和压测矩阵,用数据决定,而不是凭"高级感"决定。

参考资料

  1. Milvus 官方文档:Single-Vector Search

  2. Milvus 官方文档:Consistency

  3. Milvus 官方文档:Run Milvus with Docker Compose

  4. Milvus 官方文档:Product FAQ

  5. Milvus 官方文档:Index Vector Fields

  6. Milvus 官方文档:HNSW

  7. Milvus 官方文档:Architecture Overview

  8. Milvus 官方文档:Product FAQ

参考文献:17.Milvus 如何完成一次向量相似度检索?

相关推荐
Hhuhkt1 小时前
3D 云养宠小游戏
学习
派大鑫wink3 小时前
纯前端实现软考中级·软件设计师备考学习网站
前端·学习
smartpi_ai4 小时前
51 单片机写好了,大模型语音怎么接?17T 全驱动的现成方案、双板串口分工的边界与画板前的三项硬件确认
单片机·嵌入式硬件·学习
wujiuhsu4 小时前
图片里的简体中文怎么翻译成繁体?商品图、详情页和海报批量转换方法
经验分享·笔记
辣知5 小时前
辣知·化智82 人类文明演化规律文化传承
学习
小雪崩6 小时前
嵌入式学习:Linux驱动开发总结
linux·驱动开发·学习
keyipatience7 小时前
JS学习笔记(2)数组,函数,作用域,对象
前端·javascript·笔记·学习·html
你想知道什么?8 小时前
词向量总结-学习笔记
人工智能·笔记·学习
He BianGu8 小时前
【笔记】WPF中RenderOptions类的详细功能
笔记·wpf