引言
最近在生产环境中遇到了一个令人困惑的问题:一个包含主查询(query)和KNN查询(knn)的混合搜索请求,当移除KNN部分后,查询结果发生了显著变化。这违背了我对Elasticsearch混合搜索机制的认知,于是决定深入源码一探究竟。
问题背景
查询场景
我们的业务场景是学术文献搜索,需要同时支持关键词匹配和语义相似性搜索。查询结构如下:
{
"query": {
"bool": {
"must": [
{
"query_string": {
"fields": [
"title_tks^10",
"title_sm_tks^5",
"important_kwd^30",
"important_tks^20",
"content_ltks^2",
"content_sm_ltks"
],
"type": "best_fields",
"query": "research^0.0345 domain^0.0345 filler^0.0345...",
"boost": 1
}
}
],
"boost": 0.05
}
},
"from": 0,
"size": 50,
"_source": ["doc_id"],
"knn": {
"field": "q_vec",
"k": 50,
"similarity": 0.01,
"num_candidates": 100,
"query_vector": [...],
"filter": {
"bool": {
"must": [
{
"query_string": {
"fields": [...],
"query": "research^0.0345 domain^0.0345...",
"boost": 1
}
}
],
"boost": 0.05
}
},
"boost": 1
}
}
关键参数说明
- 主查询boost: 0.05(权重较低)
- KNN查询boost: 1(权重较高)
- KNN参数: k=50, num_candidates=100
- 分页: from=0, size=50
问题现象
- 有KNN查询: 返回50条结果,包含特定的学术文献
- 无KNN查询: 返回50条结果,但内容完全不同
- 结果差异: 两种查询的结果重叠度极低,几乎完全不同
初步假设与困惑
我的初始理解
基于对Elasticsearch混合搜索的认知,我认为:
- KNN和主查询独立执行
- 无rank配置时,KNN结果被丢弃
- 最终结果只来自主查询
- KNN的filter不会影响主查询
实际观察的矛盾
但实际观察到的现象与我的理解完全矛盾:
- KNN的filter确实影响了最终结果
- 有无KNN查询结果差异巨大
- 这无法用"KNN结果被丢弃"来解释
开始源码探索
第一步:定位关键类
首先在Elasticsearch 8.11源码中搜索混合搜索相关的类:
# 搜索混合搜索相关类
find . -name "*.java" -exec grep -l "hybrid\|knn.*query\|query.*knn" {} \;
第二步:找到DfsQueryPhase
通过搜索发现,混合搜索的关键处理逻辑在DfsQueryPhase.java
中:
// server/src/main/java/org/elasticsearch/action/search/DfsQueryPhase.java
public class DfsQueryPhase {
private ShardSearchRequest rewriteShardSearchRequest(ShardSearchRequest request) {
// 这里是关键逻辑
}
}
第三步:分析DFS阶段
DFS(Distributed Fetch Search)阶段是混合搜索的第一个关键阶段:
// DFS阶段处理KNN查询
for (DfsKnnResults dfsKnnResults : knnResults) {
// 收集KNN结果
knnResults.add(dfsKnnResults);
}
第四步:发现SubSearch机制
在DfsQueryPhase.rewriteShardSearchRequest()
方法中,发现了关键逻辑:
// 关键发现:KNN结果被转换为SubSearch
for (DfsKnnResults dfsKnnResults : knnResults) {
KnnScoreDocQueryBuilder query = new KnnScoreDocQueryBuilder(
dfsKnnResults.getField(),
dfsKnnResults.getKnnResults()
);
subSearchSourceBuilders.add(new SubSearchSourceBuilder(query));
}
// 清空knnSearch,保留sub_searches
source = source.shallowCopy()
.subSearches(subSearchSourceBuilders)
.knnSearch(List.of());
关键发现
发现1:KNN结果不会被丢弃
重要发现: 即使没有rank配置,KNN结果也不会被丢弃,而是通过SubSearch机制保留在最终查询中。
发现2:SubSearch的作用机制
通过进一步分析SearchService.java
,发现SubSearch的处理逻辑:
// SearchService.java 第1458-1464行
if (source.rankBuilder() != null) {
List<Query> queries = new ArrayList<>();
for (SubSearchSourceBuilder subSearchSourceBuilder : source.subSearches()) {
queries.add(subSearchSourceBuilder.toSearchQuery(context));
}
// rank处理逻辑
} else {
// 无rank时的处理逻辑
}
发现3:分数合并机制
从文档中确认了分数合并的公式:
// docs/reference/search/search-your-data/knn-search.asciidoc 第325行
score = 0.9 * match_score + 0.1 * knn_score
初步结论
通过源码分析,我开始理解问题的根源:
- KNN结果确实被保留:通过SubSearch机制
- 分数合并机制:query分数×0.05 + knn分数×1
- Filter影响:KNN的filter直接影响向量搜索范围
下一步探索
虽然有了初步发现,但还有很多细节需要深入:
- SubSearch的具体执行机制
- 分数合并的详细实现
- Filter的传递路径
- 最终排序和分页的处理
这些问题将在下篇中继续深入分析。
经验总结
1. 不要轻信文档的表面描述
官方文档可能简化了复杂的内部机制,需要结合源码才能真正理解。
2. 源码是最好的老师
当遇到无法解释的现象时,直接查看源码往往能找到答案。
3. 保持怀疑精神
即使是对"常识"的认知,也要敢于质疑和验证。
下篇预告
在下篇中,我将继续深入分析:
- SubSearch的详细执行机制
- 分数计算和合并的具体实现
- Filter的影响路径
- 完整的执行流程图
- 性能优化建议
敬请期待!