目录
-
- 一、向量搜索基础概念
- [二、Elasticsearch 向量字段配置](#二、Elasticsearch 向量字段配置)
-
- [1、`dense_vector` 类型映射](#1、
dense_vector类型映射) - 1)参数详解
- [2)HNSW 索引的高级参数](#2)HNSW 索引的高级参数)
- [1、`dense_vector` 类型映射](#1、
- [三、KNN 查询的核心参数:`k` 与 `num_candidates`](#三、KNN 查询的核心参数:
k与num_candidates) - [四、HNSW 算法中的 `ef_construction` 与 `ef_search`](#四、HNSW 算法中的
ef_construction与ef_search) - 五、混合搜索:向量与文本并列查询
-
- [1、语法结构(ES 8.x 及以上)](#1、语法结构(ES 8.x 及以上))
- 1)分数计算:累加关系
- 2)查看分数构成
- 六、实际案例:水煮鱼查询的分数分析
- 七、常见问题与解决方案
-
- [1、`knn` 嵌套在 `bool` 中报错](#1、
knn嵌套在bool中报错) - [2、`query_vector` 维度不匹配](#2、
query_vector维度不匹配) - [3、RRF 需要商业许可证](#3、RRF 需要商业许可证)
- 4、如何排除未命中文本的文档?
- [1、`knn` 嵌套在 `bool` 中报错](#1、
- 八、总结与最佳实践
一、向量搜索基础概念
1、什么是向量搜索
向量搜索将非结构化数据(文本、图像、行为等)转换为高维浮点数数组,通过计算向量间距离(余弦相似度、欧氏距离等)找到最相似的内容。Elasticsearch 自 7.x 版本引入 dense_vector 类型与 knn 查询,原生支持大规模近似最近邻搜索。
1)应用场景举例
在直播推荐系统中,每个用户可抽象为高维行为向量。给定一个查询用户,需要从海量用户中快速找出最相似的若干人。这就用到了 knn 查询。
二、Elasticsearch 向量字段配置
1、dense_vector 类型映射
json
"frame_vector": {
"type": "dense_vector",
"dims": 512, // 向量维度
"index": true, // 启用 HNSW 索引
"similarity": "cosine" // 距离度量
}
1)参数详解
dims:固定维度,一旦创建不可修改。若实际向量为 32 维,必须重建索引。index:是否构建近似索引。true时使用 HNSW 图,查询毫秒级;false时仅能暴力搜索,大规模数据下查询不可接受。similarity:支持cosine、l2_norm、dot_product。需注意余弦相似度原始范围[-1,1],ES 内部会做线性变换,使得分数可能大于 1。
2)HNSW 索引的高级参数
json
"index_options": {
"type": "hnsw",
"m": 16, // 每个节点最大连接数(默认 16)
"ef_construction": 100 // 建图时的候选队列大小(默认 100)
}
增大 m 和 ef_construction 可提升召回率,但会增加内存和建索引时间。
三、KNN 查询的核心参数:k 与 num_candidates
1、定义与区别
k:最终返回给用户的结果数量。num_candidates:每个分片在 HNSW 图搜索中预先检索的候选向量数量,从中挑选最优k个。
1)为什么需要 num_candidates?
HNSW 是近似算法,贪心路径可能遗漏真正的最近邻。通过增大 num_candidates(即 HNSW 中的 ef_search),先粗捞一批候选再精排,可显著提高召回率。公式:num_candidates 越大 → 召回越接近精确搜索,但查询耗时增加。
2)典型设置建议
| 数据规模 | k 值 |
num_candidates 建议 |
说明 |
|---|---|---|---|
| 小数据集(千级) | 10 | 总数(或暴力搜索) | 数据量很小,可直接精确计算 |
| 海量数据(千万级以上) | 10 | 500 ~ 10000 | ES 上限 10000,从 500 开始测试,逐步上调 |
四、HNSW 算法中的 ef_construction 与 ef_search
1、作用阶段对比
| 参数 | 阶段 | 本质 | 对性能/召回的影响 |
|---|---|---|---|
ef_construction |
索引构建 | 插入节点时的动态候选列表大小 | 越大 → 图质量越高(查询召回提升),但建索引越慢 |
ef_search |
在线查询 | 搜索时的动态候选列表大小(即 num_candidates) |
越大 → 每次查询召回提升,但延迟增加 |
1)关系与调优口诀
- 建图用大
ef_construction(如 200~500),一次成本换高质量。 - 查询时用合适的
num_candidates(如 500~5000),按 SLA 平衡。
2)代码示例
json
// 建索引时设置
"index_options": { "ef_construction": 300 }
// 查询时设置
{ "knn": { "num_candidates": 1000 } }
五、混合搜索:向量与文本并列查询
1、语法结构(ES 8.x 及以上)
json
{
"query": { "multi_match": { "query": "手机", "fields": ["title^2"] } },
"knn": {
"field": "frame_vector",
"query_vector": [0.1, -0.2, ...],
"k": 10,
"num_candidates": 500
}
}
knn 与 query 必须平级 ,不可嵌套在 bool 内部。
1)分数计算:累加关系
从实际 _explanation 可知,最终分数为:
\\text{final_score} = \\text{score}*{\\text{query}} + \\text{score}* {\\text{knn}}
未命中 query 的文档,score_query = 0,但仍然可能因高向量分数而出现。
2)查看分数构成
添加 "explain": true 并解析 _explanation:
json
"explanation": {
"description": "sum of:",
"details": [
{ "description": "score from multi_match query", "value": 2.03 },
{ "description": "within top k documents", "value": 5.02 }
]
}
若某部分为 0,则可能被省略。
六、实际案例:水煮鱼查询的分数分析
1、查询语句
bash
curl -X GET "http://127.0.0.1:9200/live_room/_search" -H 'Content-Type: application/json' -d'
{
"query": { "multi_match": { "query": "水煮鱼", "fields": ["anchor_name^2", "room_title"] } },
"knn": {
"field": "frame_vector",
"query_vector": [32维向量],
"k": 3,
"num_candidates": 100
},
"explain": true
}'
1)返回的三篇文档(简化)
| ID | room_title | query_score | knn_score | final_score | 说明 |
|---|---|---|---|---|---|
| 1 | iPhone促销 | 0 | 9.99 | 9.99 | 纯向量匹配(向量极相似) |
| 4 | 川菜水煮鱼 | 2.04 | 5.03 | 7.07 | 文本+向量双命中 |
| 2 | 王者荣耀 | 0 | 4.62 | 4.62 | 纯向量中等相似 |
2)关键观察
- 纯向量结果依然出现 ,因为
query得分 0 时knn仍贡献总分。 - 文本命中加分明显 :
_id:4因文本得分 2.04,排名超过了向量分数更高的_id:2(4.62 vs 5.03?实际上_id:2向量 4.62 低于_id:4的 5.03,所以顺序正确)。 - 向量分数可能大于 2 ,取决于
similarity的内置变换和可能的boost。
七、常见问题与解决方案
1、knn 嵌套在 bool 中报错
错误示例:
json
{ "bool": { "should": [ { "knn": ... } ] } }
原因 :ES 8.x 中 knn 必须是顶级参数。
修正 :使用 knn.filter 实现"且"逻辑,或使用 query + knn 并列实现"或"逻辑。
2、query_vector 维度不匹配
现象 :illegal_argument_exception,提示向量维度不一致。
解决 :检查 mapping 中 dims,重建索引以修改维度(无法在线变更)。
3、RRF 需要商业许可证
错误 :current license is non-compliant for [Reciprocal Rank Fusion (RRF)]
替代方案:
- 使用
query+knn并列(简单累加,无需许可证)。 - 使用
knn.filter实现前置过滤。 - 在应用层手动实现 RRF(方案代码见附录)。
4、如何排除未命中文本的文档?
使用 knn.filter:
json
{
"knn": {
"field": "frame_vector",
"query_vector": [...],
"k": 10,
"filter": { "multi_match": { "query": "水煮鱼", "fields": [...] } }
}
}
此时 kNN 只在满足文本条件的文档中搜索,纯向量文档不会出现。
八、总结与最佳实践
1、参数速查表
| 目标 | 参数 | 推荐值(海量数据) |
|---|---|---|
| 索引质量 | ef_construction |
200~500 |
| 索引内存 | m |
16~32 |
| 查询召回 | num_candidates |
500~10000 |
| 最终结果数 | k |
业务决定(如10) |
2、查询逻辑选择
| 业务需求 | 正确语法 |
|---|---|
| 必须满足文本条件 | knn.filter |
| 文本与向量分别打分后累加 | query + knn 并列 |
| 复杂的融合排序(如 RRF) | 需商业许可证或应用层实现 |
3、调优流程
- 根据实际向量维度配置 mapping(
index: true)。 - 建索引时设
ef_construction=300,m=24。 - 测试查询:从
num_candidates=500开始,逐步增加,监控召回率与 P99 延迟。 - 利用
explain分析分数构成,调整boost平衡文本与向量权重。 - 若需排除纯向量结果,改用
knn.filter。
附录:应用层手动 RRF 示例(Python)
python
def rrf_score(rank1, rank2, k=60):
return 1/(rank1+k) + 1/(rank2+k)
text_hits = es.search(...) # 全文检索结果
knn_hits = es.search(...) # knn 结果
merged = {}
for r, hit in enumerate(text_hits['hits']['hits']):
merged[hit['_id']] = rrf_score(r+1, 1000) # 未出现在 knn 中的设置大 rank
for r, hit in enumerate(knn_hits['hits']['hits']):
if hit['_id'] in merged:
merged[hit['_id']] += rrf_score(1000, r+1)
else:
merged[hit['_id']] = rrf_score(1000, r+1)
sorted_ids = sorted(merged, key=merged.get, reverse=True)