混合检索
混合检索结合关键词检索和向量检索,兼顾精确匹配和语义理解,提升召回质量。
为什么需要混合检索?
| 检索方式 | 优势 | 劣势 |
|---|---|---|
| 关键词检索 | 精确匹配、专有名词 | 无法理解语义 |
| 向量检索 | 语义理解、同义匹配 | 可能遗漏精确词 |
场景示例:
用户查询: "Redis SETNX 命令如何实现分布式锁?"
- 仅向量检索 — 可能返回语义相关但不包含 "SETNX" 的分片
- 仅关键词检索 — 可能遗漏使用 "SET if Not eXists" 描述的分片
- 混合检索 — 两者互补,召回更全面
架构
特殊逻辑
启用重排序时: 跳过融合得分计算,直接去重即可。因为重排序模型会重新打分,初检的融合得分没有意义。
关键词检索
Elasticsearch (推荐)
生产环境首选的关键词检索方案:
yaml
molandev:
rag:
elasticsearch:
enabled: true
uris: http://localhost:9200
username: elastic
password: ${ES_PASSWORD}优势:
- ✅ 功能强大,支持复杂查询
- ✅ 分布式架构,可扩展
- ✅ 生产环境成熟
劣势:
- ❌ 内存占用高 (建议 2GB+)
- ❌ 需独立部署维护
Lucene (备选方案)
低配服务器或 Demo 测试的轻量级方案:
yaml
molandev:
rag:
lucene:
enabled: true
index-dir: ./lucene-index # 索引存储路径优势:
- ✅ 内存占用低 (100MB 内)
- ✅ 内嵌运行,无需独立部署
- ✅ 部署简单
劣势:
- ❌ 单机架构,不可扩展
- ❌ 功能相对简单
- ❌ 索引管理需自行处理
⚠️ 注意: Lucene 仅建议用于低配服务器或 Demo 测试场景,不建议生产环境使用。
关键词检索实现
使用 BM25 算法进行全文检索:
java
// 伪代码 - ES 关键词检索
public List<SearchDocument> keywordSearch(String query, KeywordSearchOptions options) {
// 构建 ES 查询
NativeQuery esQuery = NativeQuery.builder()
.withQuery(q -> q.multiMatch(m -> m
.query(query)
.fields("content", "section_title", "tags")
.minimumShouldMatch("75%")
))
.withMaxResults(options.getTopK())
.build();
// 执行查询
SearchHits<ChunkDocument> hits = elasticsearchTemplate.search(esQuery, ChunkDocument.class);
// 转换为 SearchDocument
return convertToSearchDocuments(hits);
}融合策略
混合检索需要将两路召回的结果进行融合,Knowledge 支持两种融合策略:
1. 加权得分融合 (weighted_score)
将关键词检索和向量检索的得分加权求和:
java
// 伪代码 - 加权得分融合
private List<RetrievedDocument> fuseByWeightedScore(
List<SearchDocument> keywordResults,
List<SearchDocument> vectorResults,
double vectorWeight,
double keywordWeight) {
Map<String, SearchDocument> mergedMap = new LinkedHashMap<>();
Map<String, Double> scoreMap = new HashMap<>();
// 关键词检索得分
for (int i = 0; i < keywordResults.size(); i++) {
SearchDocument doc = keywordResults.get(i);
double score = doc.getScore() * keywordWeight;
mergedMap.put(doc.getChunkId(), doc);
scoreMap.merge(doc.getChunkId(), score, Double::sum);
}
// 向量检索得分 (去重合并)
for (int i = 0; i < vectorResults.size(); i++) {
SearchDocument doc = vectorResults.get(i);
double score = doc.getScore() * vectorWeight;
if (mergedMap.containsKey(doc.getChunkId())) {
// 已有该分片,累加得分
scoreMap.merge(doc.getChunkId(), score, Double::sum);
} else {
mergedMap.put(doc.getChunkId(), doc);
scoreMap.merge(doc.getChunkId(), score, Double::sum);
}
}
// 按融合得分排序
return sortByScore(mergedMap, scoreMap);
}配置:
yaml
molandev:
rag:
hybrid-search:
vector-weight: 0.5 # 向量检索权重
keyword-weight: 0.5 # 关键词检索权重
fusion-strategy: weighted_score权重调优:
| 场景 | vector-weight | keyword-weight | 说明 |
|---|---|---|---|
| 语义优先 | 0.7 | 0.3 | 查询偏向概念理解 |
| 精确匹配优先 | 0.3 | 0.7 | 查询包含专有名词 |
| 平衡 | 0.5 | 0.5 | 通用场景 |
2. RRF 倒数排名融合 (rrf)
基于排名的融合算法,不依赖具体得分值:
java
// 伪代码 - RRF 融合
private List<RetrievedDocument> fuseByRRF(
List<SearchDocument> keywordResults,
List<SearchDocument> vectorResults,
double vectorWeight,
double keywordWeight) {
int k = 60; // RRF 参数
Map<String, Double> scoreMap = new HashMap<>();
// 关键词检索 RRF 得分
for (int i = 0; i < keywordResults.size(); i++) {
double rrfScore = keywordWeight / (k + i + 1);
scoreMap.merge(keywordResults.get(i).getChunkId(), rrfScore, Double::sum);
}
// 向量检索 RRF 得分
for (int i = 0; i < vectorResults.size(); i++) {
double rrfScore = vectorWeight / (k + i + 1);
scoreMap.merge(vectorResults.get(i).getChunkId(), rrfScore, Double::sum);
}
// 按 RRF 得分排序
return sortByScore(mergedMap, scoreMap);
}RRF 公式:
score(d) = Σ 1 / (k + rank(d))其中 k 是常数 (默认 60),rank(d) 是文档在某路检索结果中的排名。
配置:
yaml
molandev:
rag:
hybrid-search:
fusion-strategy: rrf优势:
- ✅ 不依赖得分量纲,通用性强
- ✅ 对不同检索引擎的得分有公平性
- ✅ 生产环境推荐
融合策略对比
| 策略 | 特点 | 适用场景 |
|---|---|---|
weighted_score | 加权求和,依赖原始得分 | 得分可比较时 |
rrf | 基于排名,不依赖得分值 | 通用场景,推荐 |
完整配置
yaml
molandev:
rag:
# Elasticsearch 配置
elasticsearch:
enabled: true
uris: http://localhost:9200
username: elastic
password: ${ES_PASSWORD}
# Lucene 配置 (二选一)
lucene:
enabled: false
index-dir: ./lucene-index
# 混合检索配置
hybrid-search:
vector-weight: 0.5
keyword-weight: 0.5
fusion-strategy: rrf混合检索警告
启用混合检索时,如果关键词检索不可用,会返回警告:
json
{
"type": "hybridSearchWarning",
"content": "关键词检索不可用,仅使用向量检索"
}前端可选择是否向用户展示此警告。
性能优化
| 优化项 | 建议 | 说明 |
|---|---|---|
| ES 分片规划 | 按知识库分索引 | 减少检索范围 |
关键词检索 top-k | 10-15 | 不需要太大,后续会融合 |
向量检索 top-k | 20 | 保证覆盖度 |