混合检索:语义搜索与词汇搜索
为什么要将词汇搜索与语义搜索结合使用?因为二者各有优势与局限。以 BM25 为代表的词汇搜索依赖精确匹配关键词,擅长识别低频专有术语、特定名称与固定短语;但当用户查询与文档使用了不同的同义词或表述时,便可能检索失败。而我们在上一个 Sprint 使用过的纯语义搜索,通过稠密 embedding 捕捉上下文含义与同义改写,能够召回概念相关的文本,却容易忽略精确关键词或专有术语是否真实出现。
可以这样对比二者的特性:
- 词汇搜索:适合精确字面匹配(例如查找代码中的类名
PyTorchLightning),但较难处理同义词与句式改写,可能遗漏缺乏字面重合的相关上下文,或返回虽包含关键词但实际语境无关的文档。 - 语义搜索:擅长捕捉概念含义与同义改写(例如将
CEO与executive关联起来),有助于提升召回率与语义理解;但它可能忽略某个关键罕见词是否真正出现,返回概念相关却缺失核心关键词的文档(例如当询问“微调机器学习模型”时,若向量搜索误判语境,甚至可能返回与“烘焙”相关的内容)。
混合检索(Hybrid Retrieval)并行执行语义搜索与词汇搜索,再将两路结果合并,使返回内容既包含关键术语、又契合查询意图,从而提升整体检索相关性。
合并两路检索结果通常需要借助融合算法。一种直观的做法是先对两个系统的原始分数做归一化,再按权重线性相加(例如 70% 向量分数 + 30% BM25 分数)。由于 BM25 分数(某次查询中可能落在 0~12)与余弦相似度(通常落在 0~1)量纲不同,合并前必须先转换到可比较的数值范围或分布。常见的归一化方法包括:
- Min-max scaling(最小—最大归一化):将数值线性缩放到固定区间(通常为
[0, 1]),即每个分数减去最小值后再除以极差(最大值减最小值),使不同尺度的分数能够按比例直接比较。 - Z-score(标准分数归一化):每个分数减去均值后再除以标准差,转化为均值为
0、标准差为1的分布,极端值会落在距离均值较远的位置。由于均值和标准差本身易受异常值拉动,使用时仍需留意极端异常值对归一化结果的影响。
归一化之后,可以采用线性组合,并调节语义和词汇分数各自的权重。
简单加权和往往不如基于排名的融合方法稳定。Reciprocal Rank Fusion(RRF,倒数排名融合)已成为常用方法。它使用排名位置计算组合分数:将文档在各结果列表中的 1 / (k + rank) 相加,k 通常取 60,以平缓最高排名的影响。排名第一的贡献为 1 / (60 + 1) ≈ 0.0164,第二名为 1 / (60 + 2),以此类推。排名越高,贡献越大。相加后,多个方法都认为重要的文档更容易居前;只在一种方法中排名很高的文档,也有机会进入合并结果。RRF 无需监督训练,研究中曾优于更复杂的学习型融合方法。
这种方式简单且稳健:每个排名较高的结果都获得一票,排名较低时权重递减。许多向量搜索引擎已内置 RRF 支持。
混合检索的取舍
主要成本来自同时执行稠密与稀疏两类查询,可能增加延迟与基础设施复杂性。Pinecone 混合索引、Qdrant 多向量搜索等方案通过同一引擎处理两类搜索,能够减轻这一问题。收益通常包括更高召回率,找到单一方法遗漏的信息,以及更高精确率,排除另一方法可能引入的无关结果。长尾查询和专业领域搜索尤其适合混合检索,例如企业资料中的专业术语,或同时包含一般概念与专有名称的查询。动态调整语义与词汇权重,可以让某些查询更重视准确关键词匹配,形成关注关键词的语义搜索。
检索结果的重排序策略
即便采用了混合检索,初步召回的前 100 个候选文本块中仍可能夹杂无关内容,或者相关性最高的片段未能排在最前面。重排序(Re-ranking)利用表达能力更强的深度模型,对粗召回的少量候选结果重新打分排序。其核心思路是仅对初筛出的小规模候选集运行高成本深度模型,避免在全量语料库上直接计算——这与传统推荐系统长期采用的“粗排召回 → 精排重排 → 终排”多阶段漏斗架构一脉相承(例如先用快速检索召回 1000 项,再用强模型重排前 50 项,最后对前 10 项做精细处理)。
在 RAG 流水线中,常用的重排序方案主要分为 Bi-encoder、Cross-encoder 和 基于 LLM 的评分三类:
Bi-encoder(双编码器)与初检阶段的向量模型结构相同,但在重排序阶段可以选用参数规模更大或经过垂直领域微调的模型。例如,先用快速的 BM25 初筛出 100 篇文档,再用 Sentence Transformer 分别编码查询和这 100 篇文档,计算余弦相似度后重新排序。
特点与取舍:Bi-encoder 速度快、易于扩展且支持并行计算,文档向量还可预先离线计算并缓存,通常比 Cross-encoder 成本更低,能在几乎不增加明显延迟的情况下提升基线指标(如 MRR)。但由于查询与文档仍是独立编码、仅在最后通过点积或余弦比较整体相似度,它无法捕捉查询词与文档词之间的细粒度交叉注意力,仍可能遗漏微妙的相关性信号。因此,Bi-encoder 更适合用于广泛召回与初步重排;若对最终排序精度有更高要求,可进一步引入 Cross-encoder。
Cross-encoder(交叉编码器)将查询与单篇候选文档拼接后一同送入 Transformer 模型(如 BERT 及其变体,输入格式形如 [CLS] Query [SEP] Document [SEP]),直接输出一个相关性得分(如 0~1 之间的连续值)或相关/不相关分类结果。由于模型在每一层自注意力中都能同时联合分析查询与文档中的每一个词,它能精准捕捉细微的语义关联、上下文语境以及短语匹配,在 MS MARCO 等检索基准上取得了领先的排序效果。
在实际应用中,可将粗召回的前 50 篇候选文档逐一与查询配对送入 Cross-encoder 打分并按分值重排。训练良好的 Cross-encoder 能够有效修正双编码器的误判(例如压低虽然凑齐了关键词但整体语境无关的文档排名,或结合上下文准确区分水果“Apple”与科技公司“Apple”)。
代价与优化:Cross-encoder 无法预先离线生成可复用的独立文档向量,每次查询都必须对全部“查询—候选文档”对实时运行完整的 Transformer 前向计算;未经优化时可能增加数百毫秒延迟,高并发或候选集较大时也会消耗大量 GPU 算力。工程上常采用蒸馏后的轻量模型(如 cross-encoder/ms-marco-MiniLM-L-6-v2)、限制单篇文档最大长度(如截断至 512 个 token)、开启批处理(Batching)以及对高频查询结果做缓存。凭借出色的精度,Cross-encoder 被广泛用于法律检索等高精度场景,以及需要剔除无关上下文以减少生成幻觉的 RAG 系统。
其优势是准确性高,能够识别微妙相关性,适合法律检索等高精度任务,以及希望减少无关上下文和模型幻觉的问答系统。Cross-encoder 重排序被广泛用于改进 RAG,因为它能够减少进入答案生成器的无关信息。
直接将用户查询与一篇或多篇候选文档放入提示词,调用 GPT-4.1 等大语言模型评估文档对回答问题的帮助程度(例如提示:“给定用户问题和文档,请输出 1~5 的相关性分数,表示文档对回答该问题有多大帮助”),或要求模型直接选出最相关的文档列表。
优势:灵活性高。LLM 不仅理解复杂语义,还能直接遵循用自然语言描述的复合业务排序规则——例如在提示词中要求:“请重新排序这些结果,优先考虑最新发布、来自权威域名的内容”,从而将语义理解与难以用简单数学相似度表达的业务规则结合起来(后续课程还会探讨相关机制)。
限制:调用延迟高且累计 token 费用昂贵,通常难以承受高并发实时搜索的延迟要求,更适合请求量较低、单次查询价值较高的场景。此外,LLM 评分对提示词措辞与候选文档排列顺序较为敏感,微小的输入变化可能改变同一批文档的排序结果,需要精心设计提示词并严格评估稳定性。工程上通常将其作为多阶段漏斗的最后一环——例如先用双编码器或混合搜索广度召回,再用 Cross-encoder 精排候选子集,最后仅在预算与场景价值允许时,调用 LLM 检查排在最前面的极少数(如前 5 项)结果。
推荐采用多阶段排序:快速检索器,例如 bi-encoder 或混合搜索,取得较广的相关候选;cross-encoder 对候选子集精确排序;有充分价值和预算时,再让 LLM 检查最后少数结果。
按业务需要调整分数与排序
在真实生产搜索中,“相关性”不仅取决于文本语义匹配度,还受业务优先级与元数据条件制约(例如企业搜索需优先展示官方规范文档,新闻搜索看重发布时效,电商搜索则倾向提权有现货、高评分或处于特定价格区间的商品)。这些元数据可存储在向量数据库的 payload 或外部业务库中,供查询阶段过滤或调整排序得分。
常见的业务排序信号与规则包括:
- 来源与权威性:提权经过认证的官方知识库文档,压低来源不明的博客或非正式讨论;在多部门共建知识库中,优先展示提问用户所属部门的内部资料。
- 时效性(Recency):在新闻、技术文档等快速迭代的领域优先展示较新文档,例如引入时间衰减函数(Time Decay)使附加分随文档发布时长递减,或将基础相关性得分乘以新鲜度系数。
- 质量与受欢迎程度:结合平均星级评分、点赞数或历史相似查询下的高点击率信号提权优质内容。
- 个性化与用户上下文:结合提问者身份调整权重——例如当已知提问者是研发工程师时,搜索“故障排查指南”可优先返回深度技术手册而非入门 FAQ;搜索“本地办公室”时则优先匹配用户当前所在地或常驻地的文档。
- 字段与文档类型权重:在食谱搜索中赋予标题匹配高于配料表匹配的权重;在企业搜索中赋予标签或摘要匹配高于正文偶发词匹配的权重;若 PDF 正式手册通常比自动生成的 API 文档更有参考价值,也可整体提权手册类文档。
- 显式业务与合规规则:例如满足“查询包含关键词 X 且文档属于分类 Y”时强制置顶;按合规要求对特定查询优先展示指定声明文档;或在电商大促期间提权特定品牌商品。
- 降权低质或过期属性:主动下调陈旧归档资料或未经核实的社区发帖得分。
在具体实现中,自托管的 Elasticsearch / OpenSearch 可通过字段权重(Field Boost)与 function_score 自定义排序公式;Qdrant 则通过 payload 过滤(划定权限与准入候选集)配合向量相似度完成语义检索。对于用户无权访问的机密资料或明确要求屏蔽的缺货商品(如 product.in_stock == true 或 document.language == "EN"),必须通过硬性过滤(Filter)直接排除,再对剩余合规候选集做软性分数调权(Boost)。
明确的排除规则也可以通过过滤实现。例如,用户无权查看的内容,或业务要求不展示的缺货商品,可以完全排除。Qdrant 等向量数据库支持在向量查询中加入过滤条件,例如 product.in_stock == true 或 document.language == "EN"。过滤决定是否纳入,分数调整则表达相对优先程度。实际应用经常结合二者:先排除不允许展示的内容,再调整其余结果。
引擎不原生支持某种元数据加权时,可以执行多次搜索并合并。例如,一次普通搜索,一次仅针对高优先级子集搜索,再合并结果,让部分前排位置由优先内容占据。始终测量调整对相关性指标和用户满意度的影响。过度加权容易让字段符合条件、却实际不相关的内容排在前面。可以对不同权重进行 A/B 测试,或利用小型标注查询集评估。排序调整应支持业务目标,同时保持搜索提供相关答案的基本目的。
在授权范围内做混合检索
课程演示环境使用本地 Qdrant 执行向量搜索,配合自托管的 Elasticsearch / OpenSearch 执行关键词检索。查询时必须先按当前同一用户的可读权限过滤资料范围,再对候选结果执行融合、重排并二次复核权限;若调用外部重排服务,还需明确资料传输的物理地域与合规边界。最后使用同一份基准 JSONL 数据集对比混合检索与重排前后的召回率及引用准确性,严禁将无权限资料混入重排上下文。
练习:记录本课所用配置、输入、预期结果与失败分支,并用脱敏数据复现。
连接原生重排接口
本例选用百炼文本排序中的 qwen3-rerank 模型。原生接口接收 input.query、input.documents 与 parameters.top_n,并在 output.results 中返回重排后的 index 与 relevance_score;RERANK_ENDPOINT 与对应 API Key 需独立配置。
import math
def rerank(client, endpoint, api_key, model, principal, acl, question, contexts, top_n=4):
candidates = [c for c in contexts if acl.allowed(principal, c['source_id'])]
if not candidates:
return []
if not 1 <= top_n <= 500 or len(candidates) > 500:
raise ValueError('rerank_request_size_invalid')
result = client.post(endpoint, headers={'Authorization': f'Bearer {api_key}'}, json={
'model': model, 'input': {'query': question, 'documents': [c['text'] for c in candidates]},
'parameters': {'top_n': min(top_n, len(candidates)), 'return_documents': False}})
result.raise_for_status()
data = result.json()
if 'output' not in data or 'results' not in data['output']:
raise ValueError('rerank_response_invalid')
ordered, seen = [], set()
for item in data['output']['results']:
index, score = item['index'], item['relevance_score']
if type(index) is not int or not 0 <= index < len(candidates) or index in seen or not math.isfinite(score):
raise ValueError('rerank_index_or_score_invalid')
seen.add(index)
candidate = candidates[index]
if acl.allowed(principal, candidate['source_id']):
ordered.append({**candidate, 'rerank_score': score})
return ordered
向重排接口发送请求前,仅保留当前用户获准访问的候选材料;收到响应后,按返回的 index 映射回本地分块 ID,严格校验是否存在重复索引、越界索引或非有限浮点数得分(NaN/Inf),并在返回前再次复核用户权限。请求侧需同时限制 top_n 与候选文档总数上限(<= 500);若单次输入 token 超限,接口会返回 HTTP 400 错误,需相应缩减分块大小或候选数量。重排得分(rerank_score)与初始向量相似度应分别记录。
离线测试通过 HTTP fixture 校验授权资料过滤范围与返回索引映射;对接真实云端服务时,需核验服务地域、模型开通状态、并发限流及原始材料外发传输合规性。若改用本地开源 Cross-encoder,则按所选模型的官方输入格式部署并开展独立评估。
练习:模拟在重排请求发出期间撤回用户对某篇资料的访问权限,确认该资料不会在重排返回后进入生成上下文;随后分别构造越界 index、重复 index、HTTP 429 限流以及输入长度超限等异常响应,验证拦截逻辑。