引言
在上篇中,我们发现了KNN结果通过SubSearch机制被保留的关键事实。本篇将继续深入分析混合搜索的执行机制,揭示完整的处理流程,并解答之前的所有疑惑。
深入源码分析
1. SubSearch的执行机制
1.1 KnnScoreDocQueryBuilder的实现
KNN结果被转换为KnnScoreDocQueryBuilder
,这个类负责在查询阶段重新执行KNN搜索:
// server/src/main/java/org/elasticsearch/index/query/KnnScoreDocQueryBuilder.java
public class KnnScoreDocQueryBuilder extends AbstractQueryBuilder<KnnScoreDocQueryBuilder> {
private final String field;
private final List<ScoreDoc> scoreDocs;
@Override
protected Query doToQuery(SearchExecutionContext context) throws IOException {
// 创建KnnScoreDocQuery,包含DFS阶段找到的文档ID和分数
return new KnnScoreDocQuery(field, scoreDocs);
}
}
1.2 KnnScoreDocQuery的核心逻辑
// 简化版的KnnScoreDocQuery实现
public class KnnScoreDocQuery extends Query {
private final String field;
private final List<ScoreDoc> scoreDocs;
@Override
public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) {
return new Weight(this) {
@Override
public Scorer scorer(LeafReaderContext context) {
// 只对DFS阶段找到的文档进行打分
return new KnnScoreDocScorer(this, context, scoreDocs);
}
};
}
}
2. 分数计算与合并机制
2.1 主查询分数计算
主查询对10000+文档进行打分,然后应用boost:
// 主查询分数计算
float queryScore = calculateQueryScore(document);
float finalQueryScore = queryScore * 0.05; // 应用boost
2.2 KNN分数计算
KNN SubSearch只对50篇候选文档进行打分:
// KNN分数计算
float knnScore = calculateKnnScore(document);
float finalKnnScore = knnScore * 1.0; // 应用boost
2.3 分数合并逻辑
通过OR逻辑合并所有文档的分数:
// 分数合并逻辑(简化版)
Map<String, Float> finalScores = new HashMap<>();
// 处理主查询结果
for (ScoreDoc scoreDoc : queryResults) {
String docId = getDocId(scoreDoc);
float score = scoreDoc.score * 0.05;
finalScores.put(docId, score);
}
// 处理KNN结果
for (ScoreDoc scoreDoc : knnResults) {
String docId = getDocId(scoreDoc);
float score = scoreDoc.score * 1.0;
// OR逻辑:取最大值
if (finalScores.containsKey(docId)) {
finalScores.put(docId, Math.max(finalScores.get(docId), score));
} else {
finalScores.put(docId, score);
}
}
3. Filter的影响路径
3.1 Filter的传递过程
KNN的filter通过以下路径传递到向量搜索:
// KnnVectorQueryBuilder.java
public class KnnVectorQueryBuilder extends AbstractQueryBuilder<KnnVectorQueryBuilder> {
private final String field;
private final float[] queryVector;
private final int k;
private final int numCandidates;
private final QueryBuilder filter; // 关键:filter字段
@Override
protected Query doToQuery(SearchExecutionContext context) throws IOException {
Query filterQuery = null;
if (filter != null) {
filterQuery = filter.toQuery(context);
}
// 创建KnnVectorQuery,传入filter
return new KnnVectorQuery(field, queryVector, k, numCandidates, filterQuery);
}
}
3.2 向量搜索中的Filter应用
// DenseVectorFieldMapper.java 第903行
case FLOAT -> parentFilter != null
? new DiversifyingChildrenFloatKnnVectorQuery(name(), queryVector, filter, numCands, parentFilter)
: new KnnFloatVectorQuery(name(), queryVector, numCands, filter);
Filter直接限制向量搜索的候选文档范围,这就是为什么KNN的filter会影响最终结果的原因。
完整执行流程图
时序图
数据流图
关键问题解答
问题1:KNN结果是否被丢弃?
答案: 不会。KNN结果通过SubSearch机制被保留在最终查询中。
问题2:Filter如何影响结果?
答案: Filter直接影响向量搜索的候选范围,限制KNN只在满足条件的文档中搜索。
问题3:分数如何合并?
答案: 通过OR逻辑合并,取每个文档的query分数×0.05和knn分数×1的最大值。
问题4:from/size的作用?
答案: 在最终分数排序后应用,选取总分最高的前50篇文档。
性能优化建议
1. 参数调优
1.1 num_candidates优化
{
"knn": {
"field": "q_vec",
"k": 50,
"num_candidates": 100 // 根据数据量调整
}
}
- 小数据集: num_candidates = k * 2
- 大数据集: num_candidates = k * 10
- 性能与精度平衡: 根据实际需求调整
1.2 boost值调优
{
"query": {
"bool": {
"boost": 0.05 // 根据业务需求调整
}
},
"knn": {
"boost": 1.0 // 根据业务需求调整
}
}
2. 索引优化
2.1 向量索引优化
{
"mappings": {
"properties": {
"q_vec": {
"type": "dense_vector",
"dims": 768,
"index": true,
"similarity": "cosine"
}
}
}
}
2.2 文本字段优化
{
"mappings": {
"properties": {
"title_tks": {
"type": "text",
"analyzer": "standard",
"search_analyzer": "standard"
}
}
}
}
3. 查询优化
3.1 Filter优化
{
"knn": {
"filter": {
"bool": {
"must": [
{
"term": {
"category": "research" // 使用精确匹配
}
}
]
}
}
}
}
3.2 字段权重优化
{
"query_string": {
"fields": [
"title_tks^10", // 标题权重最高
"important_kwd^30", // 关键词权重最高
"content_ltks^2" // 内容权重较低
]
}
}
监控与调试
1. 查询性能监控
{
"query": {...},
"knn": {...},
"profile": true // 启用查询分析
}
2. 分数调试
{
"query": {...},
"knn": {...},
"_source": false,
"explain": true // 启用分数解释
}
总结
通过深入源码分析,我们完全理解了Elasticsearch混合搜索的执行机制:
关键发现
- KNN结果不会被丢弃: 通过SubSearch机制保留
- Filter直接影响向量搜索: 限制候选文档范围
- 分数通过OR逻辑合并: 取query和knn分数的最大值
- boost值影响最终排序: 0.05 vs 1.0的权重差异
执行流程
- DFS阶段: KNN查询执行,返回候选文档
- DfsQueryPhase: KNN结果转换为SubSearch
- 主查询执行: 独立执行query和knn sub_search
- 分数合并: 通过OR逻辑和boost值合并分数
- 最终排序: 按总分排序并应用分页
实际应用
这个理解帮助我们:
- 正确配置混合搜索参数
- 优化查询性能
- 调试查询问题
- 设计更好的搜索策略
经验总结
1. 源码分析的价值
直接查看源码是理解复杂系统的最佳方式,比文档更准确、更深入。
2. 系统性思维的重要性
不能孤立地看某个组件,要理解整个系统的协作机制。
3. 实践验证的必要性
理论认知需要通过实际测试来验证,避免被表面现象误导。
4. 持续学习的态度
技术不断发展,要保持好奇心和学习热情。
参考资料
- Elasticsearch 8.11 源码
server/src/main/java/org/elasticsearch/action/search/DfsQueryPhase.java
server/src/main/java/org/elasticsearch/search/SearchService.java
docs/reference/search/search-your-data/knn-search.asciidoc
- Lucene 向量搜索文档
本文档基于Elasticsearch 8.11源码分析,如有疑问或发现错误,欢迎讨论交流。