29 混合检索:结合语义召回和关键词精确匹配
混合检索:结合语义召回和关键词精确匹配
混合检索把向量检索与关键词检索结合起来。向量检索理解语义,关键词检索擅长错误码、接口路径、产品编号和专有名词,融合后可以减少单一路线的召回盲区。
[TOC]
1. 为什么需要混合检索
向量检索适合:
同义表达
自然语言问题
概念解释
语义相近但字面不同的内容
关键词检索适合:
ERR_10027
/api/upload/max-size
QWEN_MODEL
订单号
类名和函数名
版本号
人名和产品名
例如:
用户:ERR_FILE_TOO_LARGE 是什么意思?
向量模型可能把错误码看作普通符号,关键词检索可以精确命中包含该字符串的 chunk。
混合检索目标:
向量召回语义相关结果
关键词召回精确匹配结果
合并、去重、融合排序
返回稳定候选集
2. 整体流程
graph TD;
A["用户问题"] --> B["生成问题向量"];
A --> C["关键词和全文检索查询"];
B --> D["向量检索候选"];
C --> E["关键词检索候选"];
D --> F["统一候选结构"];
E --> F; F --> G["按 chunkId 去重"];
G --> H["融合排名"];
H --> I["取最终 TopK"];
第一版参数示例:
vectorTopK = 20
keywordTopK = 20
finalTopK = 8
后续如果增加 Rerank:
混合检索取20到50个候选
Rerank后取5到8个上下文
3. 统一候选结构
public record SearchCandidate(
String chunkId, String documentId, String content, double vectorScore, double keywordScore, Integer vectorRank, Integer keywordRank, String hitType, Map<String, Object> metadata) {
}
hitType:
VECTOR
KEYWORD
BOTH
不要只保留最终分数。调试混合检索时必须知道:
候选来自哪一路
原始排名是多少
原始分数是多少
融合后排名如何变化
4. PostgreSQL 简单关键词检索
最简单的包含查询:
SELECT
c.id, c.document_id, c.contentFROM rag_chunk c
JOIN rag_document d ON d.id = c.document_id
WHERE d.knowledge_base_id = :knowledge_base_id
AND d.status = 'EMBEDDED' AND c.content ILIKE '%' || :keyword || '%'LIMIT :top_k;
适合学习流程,但存在:
大数据量性能差
无法良好分词和排序
前导百分号难以使用普通B-tree索引
可以启用 pg_trgm 改善模糊匹配:
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX rag_chunk_content_trgm_idx
ON rag_chunk
USING gin (content gin_trgm_ops);
仍需要根据中文、英文标识符和数据规模测试效果。
5. PostgreSQL 全文检索
可以增加搜索向量列:
ALTER TABLE rag_chunk
ADD COLUMN search_vector tsvector;
更新:
UPDATE rag_chunk
SET search_vector = to_tsvector('simple', coalesce(title_path, '') || ' ' || content);
索引:
CREATE INDEX rag_chunk_search_vector_gin_idx
ON rag_chunk
USING gin (search_vector);
查询:
WITH query AS (
SELECT websearch_to_tsquery('simple', :question) AS value)
SELECT
c.id, c.document_id, c.content, ts_rank_cd(c.search_vector, query.value) AS keyword_scoreFROM rag_chunk c
CROSS JOIN query
JOIN rag_document d ON d.id = c.document_id
WHERE d.knowledge_base_id = :knowledge_base_id
AND d.status = 'EMBEDDED' AND c.search_vector @@ query.valueORDER BY keyword_score DESC
LIMIT :top_k;
websearch_to_tsquery 适合处理普通用户输入,并支持部分搜索语法。
中文全文检索需要额外评估分词方案。simple 配置不会自动提供成熟中文分词。
第一版可以组合:
错误码、接口路径、英文标识符:ILIKE或trigram
自然语言:向量检索
6. 关键词提取
第一版可以从问题中提取明显的精确标识符:
大写错误码:ERR_[A-Z0-9_]+
接口路径:/api/...
版本号:v1.2.3
配置名:大写字母和下划线
订单号:符合业务格式的数字或字母串
不要让聊天模型随意改写精确标识符。
例如:
原问题:ERR_10027是什么?
关键词:ERR_10027
停用词、标点和大小写处理要可配置。
7. 为什么不能直接相加分数
向量分数可能是:
cosine similarity:0到1附近,越大越好
cosine distance:0开始,越小越好
全文检索分数可能是:
ts_rank_cd:没有与向量分数相同的量纲
直接计算:
vectorScore + keywordScore
可能让某一路长期压制另一路。
推荐先使用排名融合。
8. Reciprocal Rank Fusion
RRF 只关心各路排名,不要求原始分数同尺度。
公式:
RRF(d) = Σ 1 / (k + rank_i(d))
其中:
d:候选chunk
rank_i:候选在第i路检索中的排名,从1开始
k:平滑常数,常见取60,也应通过评测调整
示例:
chunk A:向量第1,关键词第4
chunk B:向量第3,关键词第1
A = 1/(60+1) + 1/(60+4)
B = 1/(60+3) + 1/(60+1)
Java 示例:
double rrfScore(Integer vectorRank, Integer keywordRank, int k) {
double score = 0.0; if (vectorRank != null) { score += 1.0 / (k + vectorRank); } if (keywordRank != null) { score += 1.0 / (k + keywordRank); } return score;}
同一 chunk 两路都命中,通常会获得更高融合排名。
9. 归一化加权
另一种做法是先把分数归一化:
normalizedVectorScore
normalizedKeywordScore
再计算:
finalScore = 0.7 * normalizedVectorScore
+ 0.3 * normalizedKeywordScore
需要明确:
距离如何转换成相似度
每一路如何归一化
某一路没有结果时怎么办
权重如何通过评测确定
学习阶段优先 RRF,解释更简单,也不容易受分数量纲影响。
10. 去重
两路可能返回同一个 chunk。
必须按稳定 ID 去重:
chunkId
不要只按内容文本去重,因为不同文档可以合法包含相同段落。
合并时保留:
vectorRank
keywordRank
vectorScore
keywordScore
hitType
如果同一文档相邻 chunk 高度重叠,可以在最终上下文阶段再做相邻合并,不要在候选融合阶段过早删除。
11. 查询路由
第一版可以所有问题都执行两路检索。
后续可以根据问题特征路由:
包含错误码、路径、编号 -> 提高关键词权重
自然语言解释 -> 提高向量权重
短而精确的标识符 -> 关键词优先
长问题或同义表达 -> 向量优先
路由不能只有单一路径且没有回退。
如果分类错误,可能完全丢失正确候选。
稳妥方式:
保留两路检索
根据问题类型调整候选数或融合权重
12. 权限过滤
向量和关键词两路都必须使用同一授权范围。
不能:
向量检索带tenant过滤
关键词检索却查询全库
统一查询条件:
tenantId
knowledgeBaseId
documentStatus
用户可访问范围
融合前的每一条候选都必须已经授权。
13. 返回结构
调试接口可以返回:
{
"query": "ERR_FILE_TOO_LARGE是什么意思?",
"candidates": [ { "chunkId": "chunk_003", "hitType": "BOTH", "vectorRank": 4, "keywordRank": 1, "vectorScore": 0.81, "keywordScore": 0.72, "fusionScore": 0.032, "content": "ERR_FILE_TOO_LARGE表示文件超过大小限制。"
} ]}
普通问答响应不必暴露全部内部排名,但日志和评测报告应保留这些信息。
14. 超时与降级
两路检索可能独立失败。
降级策略:
向量检索失败,关键词成功 -> 可降级到关键词结果
关键词检索失败,向量成功 -> 可降级到向量结果
两路都失败 -> 返回检索失败,不调用模型自由回答
降级必须记录:
degraded=true
failedChannel=VECTOR或KEYWORD
错误原因
不能悄悄降级,否则团队会误以为混合检索一直正常。
15. 评测
准备不同问题类型:
同义表达
错误码
接口路径
产品编号
自然语言事实
中英文混合术语
分别比较:
仅向量检索 Recall@K仅关键词检索 Recall@K混合检索 Recall@K最终来源命中率
查询P95延迟
不要只看最终答案。必须知道提升来自哪一路。
16. 可观测性
记录:
vectorCandidateCount
keywordCandidateCount
overlapCount
vectorOnlyCount
keywordOnlyCount
fusionTopK
两路耗时
降级状态
异常信号:
重合率长期为0
关键词结果从不进入最终TopK
向量结果总被关键词压制
某一路候选长期为空
这些通常意味着分词、索引、归一化或融合配置存在问题。
17. 常见问题
17.1 混合检索一定更好吗
不一定。
融合策略错误会把不相关关键词结果排到前面,所以必须评测。
17.2 第一版必须使用 Elasticsearch 吗
不必。
可以先用 PostgreSQL 的向量、trigram 和全文检索理解流程。数据规模和搜索需求增加后再评估 Elasticsearch/OpenSearch。
17.3 中文全文检索怎么处理
需要专门的中文分词或外部搜索引擎评估。不能假设 PostgreSQL 默认配置已经具备理想中文分词效果。
17.4 混合检索能替代 Rerank 吗
不能完全替代。
混合检索提高召回覆盖,Rerank 对候选做更精细的 query-passage 相关性排序。
17.5 RRF 的 k 固定为60吗
不是。
60 是常见起点,最终应由评测决定。
18. 练习清单
实现向量和关键词两路检索
提取错误码和接口路径关键词
为关键词检索建立合适索引
统一两路候选结构
按chunkId去重
实现RRF融合
记录VECTOR、KEYWORD、BOTH命中类型
两路使用相同权限过滤
实现单路失败降级
用固定问题集比较三种检索效果
19. 小结
混合检索链路:
query
-> vector retrieval -> keyword retrieval -> merge -> deduplicate -> rank fusion -> final candidates
本节最重要的结论:
向量检索解决“意思相近”,关键词检索解决“字面必须命中”。混合检索的关键不只是两路查询,而是可解释、可评测的融合排序。