【RAG】Milvus 混合检索参数调优:ef / candidate_k / final_k 详解

Milvus 混合检索参数调优:ef / candidate_k / final_k 详解


一、核心概念

  • final_k --- 最终返回给调用方的结果条数,对应 hybrid_searchlimit 参数
  • candidate_k --- 每路 AnnSearchRequest 的候选条数,RRF 融合前的原始召回量
  • ef(exploration factor,探索因子)--- HNSW 算法查询时的搜索范围,控制图遍历时探索的邻居节点数

二、实现原理

三者的层级关系

复制代码
ef  >=  candidate_k  >=  final_k

示例:ef=40, candidate_k=20, final_k=5

结合代码理解数据流向

python 复制代码
from pymilvus import MilvusClient, AnnSearchRequest, RRFRanker
from langchain_openai import OpenAIEmbeddings

# ---------- 配置(按需修改) ----------
MILVUS_URI      = "http://localhost:19530"
COLLECTION_NAME = "my_col"
FIELD_DENSE     = "dense_vector"
FIELD_SPARSE    = "sparse_vector"
FIELD_TEXT      = "text"

final_k     = 5   # ① 最终返回给调用方的条数
candidate_k = 20  # ② 每路召回的候选量,建议 final_k 的 4~10 倍
ef          = candidate_k * 2  # ③ HNSW 搜索范围,必须 >= candidate_k

client     = MilvusClient(uri=MILVUS_URI)
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
# --------------------------------------

def hybrid_search(query: str) -> list[dict]:

    # ④ 稠密路:embed_query 把 query 转成向量,召回 candidate_k 条候选
    query_dense = embeddings.embed_query(query)
    req_dense = AnnSearchRequest(
        data=[query_dense],
        anns_field=FIELD_DENSE,
        param={
            "metric_type": "COSINE",
            "params": {"ef": ef}        # ← ef 只对稠密路(HNSW)有效
        },
        limit=candidate_k,              # ← 注意是 candidate_k,不是 final_k
    )

    # ⑤ 稀疏路:直接传原始字符串,Milvus 服务端内置 BM25 自动向量化
    req_sparse = AnnSearchRequest(
        data=[query],
        anns_field=FIELD_SPARSE,
        param={"metric_type": "BM25"}, # ← BM25 走倒排索引,不需要设置 ef
        limit=candidate_k,              # ← 同样是 candidate_k
    )

    # ⑥ RRF 融合:两路候选合并打分,截取 final_k 条返回
    results = client.hybrid_search(
        collection_name=COLLECTION_NAME,
        reqs=[req_dense, req_sparse],
        ranker=RRFRanker(),
        limit=final_k,                  # ← 这里才截取到 final_k
        output_fields=[FIELD_TEXT],
    )

    # ⑦ 解析原始结果(Milvus 结构 → 普通 dict)
    return [
        {
            "text":  hit.get("entity", {}).get(FIELD_TEXT),
            "score": hit.get("distance"),
        }
        for hit in results[0]
    ]


# 调用示例
hits = hybrid_search("Go 语言的并发模型是什么?")
for i, h in enumerate(hits, 1):
    print(f"[{i}] score={h['score']:.4f}  {h['text']}")

为什么 candidate_k 要大于 final_k

若两路各取 5 条,真正相关的结果 X 在稠密路排第 6,RRF 融合时根本看不到 X,

导致好结果被漏掉。candidate_k 放大后,X 参与融合,最终得分提升,有机会进入 top-k。

ef 的作用机制

ef 在 AnnSearchRequest 的 param 里配置,只对稠密路(HNSW)有效

python 复制代码
param={
    "metric_type": "COSINE",
    "params": {"ef": candidate_k * 2}  # ← 建议显式设置,默认值有时偏小
}
  • ef 控制 HNSW 第 0 层精细搜索时维护的候选集大小
  • ef 必须 >= candidate_k,否则 Milvus 报错或自动修正
  • ef 只在查询阶段 生效,efConstruction 只在建索引阶段生效,二者互不影响

ef(exploration factor)底层原理:HNSW 图是怎么搜索的

ef 全称 exploration factor(探索因子) ,名称来自 HNSW 原作者的开源实现 hnswlib,后被 Milvus、Faiss 等向量数据库沿用。部分文档也写作 efSearch,含义相同。

HNSW(Hierarchical Navigable Small World)把所有向量组织成多层图结构

复制代码
第 2 层(最稀疏):  A --------------------------- F              ← 少量节点,用于快速粗定位
第 1 层(中等):    A --- C --- E --- F --- H
第 0 层(最密):    A-B-C-D-E-F-G-H-I-J        ← 所有节点,用于精细搜索

查询过程分两阶段:

复制代码
阶段一:逐层下降(粗定位)
  从最高层入口节点出发
  → 贪心走向离 query 最近的邻居
  → 到达当前层局部最优后下降一层
  → 重复,直到抵达第 0 层
  (这个阶段不受 ef 控制)

阶段二:第 0 层扩展搜索(精细搜索)
  维护一个大小为 ef 的候选集
  → 不断从候选集里取出最近的节点,探索它的邻居
  → 若邻居比候选集中最远的节点更近,则替换进去
  → 候选集满 ef 个且无更近节点时停止
  → 从候选集里取 top candidate_k 返回
  (ef 就在这里起作用)

ef 大小的影响:

复制代码
ef = 10  → 候选集只维护 10 个节点 → 探索范围小 → 快,但容易漏掉真正最近的向量
ef = 100 → 候选集维护 100 个节点 → 探索范围大 → 慢,但召回更准确

与 efConstruction 的区别:

参数 作用阶段 控制什么
efConstruction 建索引时 构建图时每个节点找邻居的搜索范围,越大图质量越好,入库越慢
ef 查询时 检索时第 0 层的候选集大小,越大召回越准,查询越慢

两个参数互不影响,改查询时的 ef 不会重建图结构。


三、经验参考值

参数 推荐值 说明
final_k 3 ~ 8 给 LLM 的上下文,太多反而干扰生成
candidate_k 20 ~ 50 通常取 final_k 的 4~10 倍
ef candidate_k * 2 经验值,保证搜索范围充足
python 复制代码
final_k     = 5
candidate_k = 20               # 建议 final_k 的 4~10 倍
ef          = candidate_k * 2  # 对应 AnnSearchRequest param 里的 ef

四、注意事项

  • 三者成本递增candidate_k 越大召回越准,但每路向量计算量线性增长;ef 越大图遍历越深,延迟越高
  • 不要无脑调大candidate_k=200, ef=400 在数据量大时会显著拖慢查询
  • 调参顺序建议 :先固定 final_k,再根据召回质量调 candidate_k,最后对齐 ef
  • efefConstruction 是两个不同阶段的参数,不要混淆
  • 稀疏路不需要设置 ef :BM25 走的是倒排索引,不是 HNSW 图结构,ef 只对稠密路有效
相关推荐
夜月yeyue1 小时前
KCP 与 UDP 可靠传输
linux·网络·单片机·网络协议·udp·php
一个向上的运维者1 小时前
Docker 自定义网络中容器无法通过宿主机 IP 访问服务的完整排障记录
网络·tcp/ip·docker
utf8mb4安全女神2 小时前
子网划分【概念+实操+理解】
运维·服务器·网络
码语智行2 小时前
拦截器、接口限流、过滤器、防重发/幂等性功能说明
开发语言·网络·python
志栋智能2 小时前
超自动化安全:构建智能安全运营的神经系统
大数据·运维·网络·人工智能·安全·自动化
华普微HOPERF2 小时前
LoRa模块,如何通过卫星通信补齐地面网络的覆盖盲区?
网络·嵌入式硬件·模块·卫星通信
淇奥73 小时前
【LangChain】LangChain 学习笔记
langchain·agent
lhxcc_fly3 小时前
4.LangChain--Prompt提示词
langchain·llm·prompt
兆。3 小时前
LangChain实验跟踪集成指南:面向ML研究员
数据库·langchain