引言
在上篇中,我們發現了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;@Overrideprotected 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;@Overridepublic Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) {return new Weight(this) {@Overridepublic 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字段@Overrideprotected Query doToQuery(SearchExecutionContext context) throws IOException {Query filterQuery = null;if (filter != null) {filterQuery = filter.toQuery(context);}// 創建KnnVectorQuery,傳入filterreturn 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源碼分析,如有疑問或發現錯誤,歡迎討論交流。