Milvus 如何完成一次向量相似度检索?
码海寻道 · 大模型、智能体与 RAG 工程组件系列第 17 篇

在 RAG 项目里,"向量检索"经常被一句 search() 代码带过。但要真正排查召回为空、结果不准和延迟升高,必须理解一次检索内部发生了什么。
可以把它概括为:查询向量进入 Milvus,经过集合和索引定位候选,再计算距离、执行过滤,最后返回 Top K 结果。
如果把一次查询放回部署架构中,关键路径可以这样看:
text
PyMilvus / REST 客户端
↓
Milvus Proxy:接收请求、校验参数、路由请求
↓
Query Coordinator / Query Node:确定数据范围并执行检索
├── 从 etcd 获取集合、分区、索引和节点状态等元数据
└── 从 MinIO / 本地缓存加载数据文件和索引文件
↓
返回 Top K 结果
这里的 etcd 和 MinIO 不在应用的 search() 调用里直接出现,但它们会影响 Milvus 是否知道"该查什么"和"到哪里读取索引"。因此,检索报错时不能只盯着 Python 参数,也要检查依赖服务和数据卷。
一、一次检索的完整链路
text
用户问题
↓
Embedding 模型生成 query_vector
↓
Proxy 接收请求并校验参数
↓
Query Coordinator 确定目标数据范围
↓
Query Node 加载索引并搜索候选向量
↓
执行标量过滤与 Top K 合并
↓
返回 id、distance 和 output_fields
不同部署模式下组件名称和内部实现会有所差异,但从应用开发者视角,最重要的是确认四件事:集合是否存在、数据是否已加载、向量维度是否正确、索引与距离指标是否匹配。
二、相似度到底如何计算?
1. L2 距离
L2 是欧氏距离,距离越小通常表示越相近:
text
d(x, y) = sqrt(sum((x_i - y_i)^2))
2. 内积 IP
内积常用于已经做过归一化或对方向相似度有要求的向量。不同模型对向量长度的含义不同,不能只凭经验选择。
3. 余弦相似度 COSINE
余弦相似度关注向量方向,常用于文本语义检索。使用时要保证写入和查询采用一致的向量预处理方式。
最重要的原则是:
写入向量、查询向量、索引 metric_type 必须属于同一套约定。
三、一个可复现的搜索示例
python
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 会先做过滤还是先做向量搜索?
应用通常会同时提供向量和标量过滤条件,例如:
text
相似于"差旅报销"的内容
并且 tenant_id = tenant-a
并且 status = published
过滤和向量搜索的执行方式受索引、数据规模和查询计划影响。工程上不要假定"过滤一定先执行"或"向量一定先执行",而应通过真实数据压测观察:
- 过滤字段的选择性;
- 过滤后候选数量;
- Top K 大小;
- 查询延迟和资源使用;
- 跨分区搜索数量。
如果过滤字段决定权限范围,必须确保过滤表达式由可信服务生成,不能直接拼接用户输入。
五、Top K 不是越大越好
RAG 经常把 Top K 设置成 5、10 或 20,但 K 越大并不一定提高答案质量:
- 召回内容太少,可能遗漏关键事实;
- 召回内容太多,会挤压大模型上下文;
- 相似但重复的 Chunk 会降低信息密度;
- 低相关片段会增加模型误判机会。
通常应把 Milvus 的 Top K、去重、Reranker 和最终送入大模型的上下文数量分开设计:
text
Milvus 召回 Top 20
↓
按文档或章节去重
↓
Reranker 重排
↓
选 Top 5 进入 Prompt
六、为什么返回 distance 不能直接当答案可信度?
距离是向量空间中的数值,不等于事实正确率。它会受以下因素影响:
- Embedding 模型;
- 文本切分方式;
- 距离指标;
- 文档领域;
- 查询表达方式;
- 向量是否归一化。
因此,阈值需要用标注数据校准。不要直接认为"距离小于某个固定值就一定相关"。
七、检索不到数据时的排查顺序
第一步:确认集合和字段
检查 Collection 名称、向量字段名、主键字段和输出字段是否与代码一致。
第二步:确认向量维度
把 Embedding 模型输出的 len(vector) 与 Schema 中的 dim 比较。维度不一致时,插入和搜索都会失败或无法得到预期结果。
第三步:确认数据已经加载
Milvus 搜索前需要让目标 Collection 或 Partition 处于可搜索状态。未加载时,客户端通常会返回相应错误。
第四步:去掉过滤条件做对照
先搜索全量范围,再逐个加上租户、版本和状态条件。如果去掉过滤后有结果,问题往往出在表达式、字段值或数据类型上。
第五步:检查距离指标和模型
确认写入向量和查询向量来自同一 Embedding 模型,且索引使用的 metric 与查询预期一致。
第六步:检查 Milvus 依赖服务
在 Standalone Docker Compose 部署中,可先查看:
bash
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 还是大模型生成环节。
结语
一次向量检索不是简单的"传入向量、返回结果",而是模型、Schema、索引、过滤、一致性和资源状态共同作用的结果。掌握这条链路,才能在出现空结果、错结果和慢查询时快速定位。
下一篇将重点比较 FLAT、IVF 和 HNSW,回答"Milvus 索引到底应该怎么选"。
参考资料
- Milvus 官方文档:Single-Vector Search
- Milvus 官方文档:Consistency
- Milvus 官方文档:Run Milvus with Docker Compose
- Milvus 官方文档:Product FAQ
本文为"码海寻道"原创技术文章。检索参数、返回结构和一致性默认值请结合目标 Milvus 与 PyMilvus 版本确认。