在 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 适合对低延迟和较高召回有要求的在线检索。真正的选型方式是建立评测集和压测矩阵,用数据决定,而不是凭"高级感"决定。
参考资料
-
Milvus 官方文档:Single-Vector Search
-
Milvus 官方文档:Consistency
-
Milvus 官方文档:Run Milvus with Docker Compose
-
Milvus 官方文档:Product FAQ
-
Milvus 官方文档:Index Vector Fields
-
Milvus 官方文档:HNSW
-
Milvus 官方文档:Architecture Overview
-
Milvus 官方文档:Product FAQ