【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 只对稠密路有效
相关推荐
codedx3 小时前
LangChain 和 LangGraph 构建的 Agent 项目模版
后端·langchain·agent
颜酱18 小时前
LangGraph 入门指南
langchain
武子康2 天前
调查研究-186 LangChain 和 LangGraph 的区别:从快速构建 Agent 到生产级工作流编排
人工智能·langchain·llm
葫芦和十三5 天前
渐进发现|代码库不是文档库
langchain·agent·ai编程
柒和远方5 天前
LangGraph 深度解析:从增强型 LLM 到生产级 Agent
langchain·llm·agent
网络研究院6 天前
2026年网络安全
网络·安全·法律·法规·趋势·发展
酣大智6 天前
ARP代理--工作原理
运维·网络·arp·arp代理
treesforest6 天前
AI安全系统如何识别异常访问?IP风险识别正在成为关键能力
网络·人工智能·tcp/ip·安全·web安全
shushangyun_6 天前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化
2601_961845156 天前
粉笔行测题库|系统班|刷题
网络·百度·微信·微信公众平台·facebook·新浪微博