ES kNN 性能调优实战:num_candidates、ef_search、混合检索权重怎么调?
问题:为什么我的向量检索慢?
你接入了 ES 8.x 的 kNN 检索做 RAG,写了第一个版本:
1 | { |
跑通。但生产上拿到百万级文档后,开始抖:
- P99 延迟 800ms,业务方反馈”问答卡顿”
- 单次查询 CPU 飙到 80%
- 加机器不解决问题(瓶颈是单 shard 的 ANN 搜索时间)
问题不在于 ES 不行,在于你没调参。 ES 的 HNSW 索引有 4-5 个核心参数,每个都直接影响性能和召回率。这篇文章讲清楚每个参数怎么调。
ES kNN 检索的执行流程
ES 用的也是 HNSW(图算法)。一次 kNN 查询的过程:
1 | 1. 加载 HNSW 索引(如果没缓存) |
**关键点:num_candidates 是你”愿意让 ES 搜多少候选”,k 是”最终返回多少”**。num_candidates >= k,且通常大很多(10-100 倍)。
HNSW 算法的两个核心参数:
M:每个节点的连接数(索引创建时定)ef_construction:构建时的搜索深度(索引创建时定)ef_search(或num_candidates):查询时的搜索深度
参数 1:num_candidates(查询时最重要的参数)
1 | { |
含义:ES 从 HNSW 图中至少访问 100 个候选节点,再从中选 10 个最近的。
| num_candidates | 性能 | 召回率 |
|---|---|---|
| 50 | 极快 | 可能漏掉真正相关的(召回 < 0.9) |
| 100 | 较快 | 召回通常 > 0.95 |
| 500 | 慢一些 | 召回 > 0.99 |
| 1000 | 显著慢 | 几乎 100% 召回 |
经验值:
- 通用 RAG 场景:
num_candidates = max(k * 10, 100) - 召回率要求 > 0.99:
num_candidates = max(k * 50, 500) - 延迟敏感(如实时推荐):
num_candidates = max(k * 5, 50),配合 rerank 兜底
调优方法:固定 k=10,从 50 拉到 1000,跑 100 次查询看 latency 和召回率曲线。选曲线拐点附近的值。
参数 2:ef_search(HNSW 内部参数)
ef_search 是 HNSW 算法自己的搜索深度参数,和 num_candidates 含义类似但层级不同:
num_candidates是 ES 暴露给用户的 API 参数ef_search是底层 HNSW 算法的搜索宽度
ES 8.x 默认 ef_search = num_candidates,但你可以显式覆盖:
1 | PUT my-index/_settings |
一般不需要手动改 ef_search,除非:
- 你的 num_candidates 已经被 RAG 应用层逻辑锁死
- 想要全局调整所有查询的 HNSW 搜索宽度
参数 3:M(创建索引时定)
M 是 HNSW 图中每个节点的最大连接数。值越大:
- 索引越准(召回率高)
- 索引越大(占用内存多)
- 索引构建越慢
1 | PUT my-index |
经验值:
- 默认
m=16适用于大多数场景 - 召回率要求高:
m=32或m=48 - 内存紧张:
m=8(牺牲召回率换内存)
注意:M 改不动。改 M 必须 reindex。所以建索引时就要想清楚。
参数 4:ef_construction(创建索引时定)
构建 HNSW 图时的搜索深度。值越大:
- 索引质量越高(搜索时召回率高)
- 索引构建越慢
- 索引文件略大
经验值:
ef_construction=100:默认,绝大多数场景够用ef_construction=200:质量要求高、构建时间可接受ef_construction=300+:通常是过度优化
混合检索:BM25 + kNN 怎么融合权重?
RAG 场景下,纯向量检索经常漏掉关键词明确匹配的文档。比如用户问”Redis 的 RDB 持久化机制”,向量能找到语义相关的,但 top 结果可能是”AOF 持久化”。
混合检索(Hybrid Search) 同时跑 BM25 + kNN,融合排序。ES 8.x 的写法:
1 | { |
两种融合方式
1. RRF(Reciprocal Rank Fusion)
ES 8.8+ 默认方式。boost 不再是简单的”加权求和”,而是基于排名的融合:
1 | score(d) = sum(1 / (rank_constant + rank_i(d))) |
- 文档在 BM25 中排第 1 → 加 1/61 分
- 文档在 kNN 中排第 1 → 加 1/61 分
- 文档同时在两者排前 → 总分高
优点:不需要归一化分数(不同检索系统的分数不可比)
缺点:无法直接控制”更偏 BM25 还是更偏 kNN”
2. 加权求和(手动)
1 | { |
0.3 是 BM25 权重,0.7 是向量权重。这两个值需要根据业务调。
我的经验值
| 场景 | BM25 权重 | kNN 权重 |
|---|---|---|
| 内部知识库问答 | 0.3 | 0.7 |
| 客服 RAG(用户口语化提问) | 0.2 | 0.8 |
| 文档精确检索(用户用专业术语) | 0.6 | 0.4 |
| 代码搜索 | 0.7 | 0.3 |
调优方法:
- 准备 50-100 个测试 query,每个 query 标注”相关文档”列表
- 用不同权重组合跑 NDCG@10 / MRR@10 指标
- 找指标最高的一组
性能调优的 4 个工程实践
1. 用过滤(filter)缩小 kNN 范围
1 | { |
这是最有效的加速手段。如果你的 RAG 检索在某个分类下,filter 提前缩小数据集,kNN 搜索范围从百万级降到十万级,速度提升 5-10 倍。
2. 减少 vector 维度
1 | "embedding": { |
768 → 384 维:召回率下降 1-3%,内存和检索时间减少 50%。
如果用 bge-small-zh 这类小模型,384 维对中文 RAG 完全够用。
3. 量化压缩
ES 8.12+ 支持 int8 量化:
1 | "index_options": { |
- 内存减少 4 倍
- 召回率下降 1-2%
- 检索速度提升 1.5-2 倍
生产强烈推荐开量化。
4. 监控和告警
1 | GET _nodes/stats/indices/search |
关注:
indices.search.query_time_in_millisknn.query_time_in_millis(ES 8.x 单独统计)knn.cached_queries(缓存命中率)
如果 P99 > 200ms 持续,先看是不是某个 shard 数据量过大——rebalance 或加 shard。
实战:一个完整的 RAG 检索请求
1 | POST rag_documents/_search |
关键设计:
filter同时应用到 BM25 和 kNN,确保两者都在同一集合内召回num_candidates=200是k=10的 20 倍,召回率足够window_size=50让 RRF 融合时只看前 50 个候选rank_constant=60是 RRF 经典值
意味着什么?
ES kNN 不是”建索引就能用”,需要根据业务调:
num_candidates是查询时最重要的调参旋钮M和ef_construction是建索引时定的,不能改- 混合检索的权重需要有评测集调,不能拍脑袋
- 量化(int8_hnsw)是性价比最高的优化
- filter 是性能优化的”银弹”,能提前缩小数据集就提前
数据密集型 AI 后端的工程师,对向量检索的理解深度决定了 RAG 系统的质量。这套参数调过一遍,下次新项目你就有直觉——知道瓶颈在哪、往哪个方向调。
