【学习笔记】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 如何完成一次向量相似度检索?

相关推荐
Rabitebla26 分钟前
【Linux 底层原理】进程(十):进程等待详解---从僵尸进程到 wait、waitpid 与 status
linux·c++·笔记·学习·visual studio
小lu飞2 小时前
2026小红书小工具怎么制作:纯前端 ZIP 打包、沙箱限制与笔记挂载流程
前端·笔记·ai·ai编程
无心水5 小时前
24、系统架构师论文写作专题 核心考点与学习笔记
笔记·学习·系统架构·架构师·安全架构·软考架构师·软考系统架构设计师
蒸蒸yyyyzwd5 小时前
邓明 Agent 系统面试-意图识别学习笔记
笔记·学习
Zhao-Jichao9 小时前
AI Prompting for Everyone 课程笔记 - P21 总结 Conclusion
人工智能·笔记
欢欢李9 小时前
【LTE L3 笔记 05】LTE同步信号
笔记·无线通信·物理层·lte·空口协议
La Pulga10 小时前
【FreeRTOS源码学习笔记】空闲任务与阻塞延时
笔记·mcu·学习·嵌入式·freertos·实时操作系统
蒸蒸yyyyzwd11 小时前
秋招学习笔记 day106
笔记·学习
小李不想当小白12 小时前
WDG看门狗(STM32标准库学习笔记)
笔记·stm32·单片机·嵌入式硬件·学习·嵌入式
欢欢李12 小时前
【LTE L3 笔记 04】LTE帧结构
笔记·无线通信·物理层·lte·空口协议