信息检索与搜索


文档摘要

信息检索与搜索 本节摘要:BM25 精确但脆弱,稠密检索撒大网却漏关键词,混合检索是 2026 的默认。其余都是调参。用户输入「有人为了钱撒谎会怎样」,期待找到真正覆盖该情形的法条「印度刑法典第 420 条」。关键词检索完全错过(词汇不重叠),语义检索若嵌入没在法律文本上训过也会漏。真实搜索必须同时处理两类问题。信息检索是每个 RAG 系统、每个搜索框、每个文档站模糊查找底下的流水线。

信息检索与搜索

本节摘要:BM25 精确但脆弱,稠密检索撒大网却漏关键词,混合检索是 2026 的默认。其余都是调参。用户输入「有人为了钱撒谎会怎样」,期待找到真正覆盖该情形的法条「印度刑法典第 420 条」。关键词检索完全错过(词汇不重叠),语义检索若嵌入没在法律文本上训过也会漏。真实搜索必须同时处理两类问题。信息检索是每个 RAG 系统、每个搜索框、每个文档站模糊查找底下的流水线。2026 在生产中有效的架构不是单一方法,而是一条由互补方法串成的链,每一环接住上一环的失败:稀疏检索(BM25)跑倒排索引,毫秒级精确命中实体与代号,但不懂语义;稠密检索把查询与文档编码成向量做最近邻,抓住改写与语义相似,却漏掉差一个字符的精确匹配;融合用倒数排名融合(RRF)把两份排名合并,无需分数标定;交叉编码器重排只对前 30 条做查询+文档联合打分,慢但准,留前 5 条。本节逐层从零搭建,并点明每层接住哪种失败。

对应原课程:Phase 5 · Lesson 14 · information-retrieval-search(原英文 phases/05-nlp-foundations-to-advanced/14-information-retrieval-search/docs/en.md)。前置依赖:第 02 节(词袋与 TF-IDF)、第 04 节(GloVe、FastText、子词)。

学习目标

阅读完本节,你应当能够:

  1. 说清稀疏(BM25)、稠密、重排四层检索各自接住的失败模式。
  2. 从零实现 BM25、稠密双编码器检索、RRF 融合与交叉编码器重排,组装成混合检索。
  3. 选择正确的评估指标(Recall@k、MRR、nDCG),并把 Recall@k 锁定为 RAG 检索器的首要数字。
  4. 给定语料规模与查询模式,选定 2026 生产检索栈(纯 BM25 / 纯稠密 / 混合 + 重排 / 三路检索)。

一、问题与直觉

用户输入「有人为了钱撒谎会怎样」,期待找到真正覆盖该情形的法条「印度刑法典第 420 条」。关键词检索完全错过(词汇不重叠);语义检索若嵌入没在法律文本上训过也会漏。真实搜索必须同时处理这两类问题。

信息检索是每个 RAG 系统、每个搜索框、每个文档站模糊查找底下的流水线。2026 在生产中有效的架构不是单一方法,而是一条由互补方法串成的链,每一环接住上一环的失败:

四层,按需取用:

  1. 稀疏检索(BM25)。 快、精确匹配强、语义上糟糕。跑在倒排索引上,百万文档量级单查询不到 10 毫秒。法条引用、产品代号、错误信息、命名实体的命中靠它。
  2. 稠密检索。 把查询和文档编码成向量,做最近邻搜索。抓住改写与语义相似。漏掉差一个字符的精确关键词匹配。配 FAISS 或向量库,单查询 50~200 毫秒。
  3. 融合。 合并稀疏与稠密两份排名。倒数排名融合(Reciprocal Rank Fusion, RRF) 是省心的默认,因为它忽略原始分数(两者尺度不同)、只用排名位置。当已知某信号在你领域占主导时,加权融合是个选项。
  4. 交叉编码器重排。 取融合后的前 30 条,跑交叉编码器(查询 + 文档一起,逐对打分),留前 5。交叉编码器逐对比双编码器慢,但准得多。靠只在前 30 条上跑来摊薄成本。

💡 三路检索(BM25 + 稠密 + SPLADE 之类的学习稀疏)在 2026 基准上优于两路,但需要学习稀疏索引的基础设施。对多数团队,两路 + 交叉编码器重排是甜点。

二、从零实现

第 1 步:从零写 BM25

import math, re from collections import Counter TOKEN_RE = re.compile(r"[a-z0-9]+") def tokenize(text): return TOKEN_RE.findall(text.lower()) class BM25: def __init__(self, corpus, k1=1.5, b=0.75): if not corpus: raise ValueError("corpus must not be empty") self.corpus = [tokenize(d) for d in corpus] self.k1, self.b = k1, b self.n_docs = len(self.corpus) self.avg_dl = sum(len(d) for d in self.corpus) / self.n_docs self.df = Counter() for doc in self.corpus: for term in set(doc): self.df[term] += 1 def idf(self, term): n = self.df.get(term, 0) return math.log(1 + (self.n_docs - n + 0.5) / (n + 0.5)) def score(self, query, doc_idx): doc = self.corpus[doc_idx] dl, freq = len(doc), Counter(doc) s = 0.0 for term in tokenize(query): f = freq.get(term, 0) if f == 0: continue num = f * (self.k1 + 1) den = f + self.k1 * (1 - self.b + self.b * dl / self.avg_dl) s += self.idf(term) * num / den return s def rank(self, query, top_k=10): scored = [(self.score(query, i), i) for i in range(self.n_docs)] scored.sort(reverse=True) return scored[:top_k]

两个参数值得记住。k1=1.5 控制词频饱和:越大,词重复的权重越重。b=0.75 控制长度归一化:0 完全忽略文档长度,1 完全归一化。这两个默认值是 Robertson 在原论文里的推荐,基本不用调。

BM25 打分直觉(单个查询词 t 对文档 d 的贡献):

第 2 步:双编码器稠密检索

from sentence_transformers import SentenceTransformer import numpy as np def build_dense_index(corpus, model_id="sentence-transformers/all-MiniLM-L6-v2"): encoder = SentenceTransformer(model_id) embeddings = encoder.encode(corpus, normalize_embeddings=True) return encoder, embeddings def dense_search(encoder, embeddings, query, top_k=10): q_emb = encoder.encode([query], normalize_embeddings=True) sims = (embeddings @ q_emb.T).flatten() order = np.argsort(-sims)[:top_k] return [(float(sims[i]), int(i)) for i in order]

L2 归一化嵌入,使点积等于余弦。all-MiniLM-L6-v2 是 384 维,快,对多数英文检索够强。多语用 paraphrase-multilingual-MiniLM-L12-v2;要顶配准确率用 bge-large-en-v1.5e5-large-v2

第 3 步:倒数排名融合(RRF)

def reciprocal_rank_fusion(rankings, k=60): scores = {} for ranking in rankings: for rank, (_, doc_idx) in enumerate(ranking): scores[doc_idx] = scores.get(doc_idx, 0.0) + 1.0 / (k + rank + 1) fused = sorted(scores.items(), key=lambda x: x[1], reverse=True) return [(score, doc_idx) for doc_idx, score in fused]

k=60 常数来自原 RRF 论文。k 越大越抹平排名差异的贡献;越小越让靠前的名次主导。60 是发表默认,基本不用调。

第 4 步:混合检索 + 重排

from sentence_transformers import CrossEncoder reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2") def hybrid_search(query, bm25, encoder, dense_embeddings, corpus, top_k=5, pool_size=30, reranker=reranker): sparse_ranking = bm25.rank(query, top_k=pool_size) dense_ranking = dense_search(encoder, dense_embeddings, query, top_k=pool_size) fused = reciprocal_rank_fusion([sparse_ranking, dense_ranking])[:pool_size] pairs = [(query, corpus[doc_idx]) for _, doc_idx in fused] scores = reranker.predict(pairs) reranked = sorted(zip(scores, [doc_idx for _, doc_idx in fused]), reverse=True) return reranked[:top_k]

三段组合。BM25 找词法匹配;稠密找语义匹配;RRF 把两份排名合并,无需分数标定;交叉编码器把查询与文档一起编码后对前 30 重打分,捕捉双编码器漏掉的细粒度相关性。留前 5。

第 5 步:评估

指标 含义
Recall@k 在正确文档存在的查询里,它落在前 k 的比例有多大?
MRR(平均倒数排名) 第一个相关文档排名的倒数的平均。
nDCG@k 计入相关度的梯度,而不只是「相关/不相关」二值。

对 RAG 而言,检索器的 Recall@k 是最重要的数字。正确段落不在检索集里,阅读器就答不了。

💡 调试技巧:对失败的查询,diff 稀疏与稠密两份排名。一个找到了正确文档、另一个没找到,说明你要么遇到词汇错配(修法:补上缺失的那一半),要么遇到语义歧义(修法:更好的嵌入或加重排器)。

三、框架对比

2026 的栈:

规模
1 千~10 万文档 内存 BM25 + all-MiniLM-L6-v2 嵌入 + RRF。无需独立数据库。
10 万~1000 万文档 FAISS 或 pgvector 存稠密 + Elasticsearch/OpenSearch 存 BM25。并行跑。
1000 万以上 Qdrant / Weaviate / Vespa / Milvus,带混合支持。前 30 跑交叉编码器重排。
最高质量前沿 三路(BM25 + 稠密 + SPLADE)+ ColBERT 晚期交互重排

无论选什么,都要为评估留预算。先基准检索召回,再基准端到端 RAG 准确率。阅读器修不了检索器漏掉的。

2026 生产 RAG 的血泪经验

  • 80% 的 RAG 失败可追溯到摄入与分块,而非模型。 团队花几周换 LLM、调提示,而检索器每三条查询就静悄悄返回错上下文。先修分块。
  • 分块策略比分块大小更重要。 定长切分会切碎表格、代码、嵌套标题。句子感知切分是默认;语义或基于 LLM 的切分对技术文档与产品手册有回报。
  • 父文档模式(Parent-doc)。 检索小的「子块」求精确;当同一父节的多个子块命中时,换成父块以保留上下文。这在无需重训的情况下稳定提升答案质量。
  • k_rerank=3 通常最优。 每多加一块都增加 token 成本与生成延迟,却不提升答案质量。如果你的 k=8 还比 k=3 好,说明重排器没发挥好。
  • HyDE / 查询扩展。 从查询生成一个假设性答案,嵌它,再检索。弥合短问题与长文档之间的措辞鸿沟。免训练的精确率提升。
  • 上下文预算控制在 8K token 以内。 在这个上限上稳定命中,意味着重排器阈值太松。
  • 一切都要版本化。 提示、分块规则、嵌入模型、重排器。任何漂移都会静悄悄破坏答案质量。对忠实度、上下文精确、未答问题率上 CI 门,在用户看到之前挡住回归。
  • 三路检索(BM25 + 稠密 + SPLADE 之类的学习稀疏)在 2026 基准上优于两路,尤其对专有名词与语义混杂的查询。基础设施支持 SPLADE 索引时就上。

据 2026 行业测量,良好的检索设计能把幻觉降低 70~90%。多数 RAG 性能提升来自更好的检索,而非模型微调。

四、可复用产物

保存为 outputs/skill-retrieval-picker.md:

--- name: retrieval-picker description: Pick a retrieval stack for a given corpus and query pattern. version: 1.0.0 phase: 5 lesson: 14 tags: [nlp, retrieval, rag, search] --- Given requirements (corpus size, query pattern, latency budget, quality bar, infra constraints), output: 1. Stack. BM25 only, dense only, hybrid (BM25 + dense + RRF), hybrid + cross-encoder rerank, or three-way (BM25 + dense + learned-sparse). 2. Dense encoder. Name the specific model. Match to language(s), domain, and context length. 3. Reranker. Name the specific cross-encoder model if used. Flag that rerank adds 30-100ms latency on top-30. 4. Evaluation plan. Recall@10 is the primary retriever metric. MRR for multi-answer. Baseline first, incremental improvements measured against it. Refuse to recommend dense-only for corpora with named entities, error codes, or product SKUs unless the user has evidence dense handles exact matches. Refuse to skip reranking for high-stakes retrieval (legal, medical) where the final top-5 decides the user's answer.

五、练习

  1. 基础:在 500 文档语料上实现上面的 hybrid_search,测 20 条查询,对比纯 BM25、纯稠密、混合三者在 recall@5 上的差异。
  2. 进阶:加 MRR 计算。对每条已知正确文档的测试查询,找正确文档在 BM25、稠密、混合三份排名里的名次,报告各自的 MRR。
  3. 挑战:用 MultipleNegativesRankingLoss(Sentence Transformers)在你的领域上微调稠密编码器。用 500 对查询-文档构训练集,对比微调前后的 recall。

本节要点回顾

  1. BM25 精确但脆弱:跑倒排索引,毫秒级命中实体与代号,但词汇不重叠就彻底漏。
  2. 稠密检索撒大网:向量最近邻抓改写与语义相似,却漏差一字的精确匹配。
  3. RRF 是省心融合:用 1/(k+rank+1) 合并两份排名,忽略分数尺度,k=60 是论文默认。
  4. 交叉编码器重排慢但准:只对前 30 跑查询+文档联合打分,留前 5,摊薄成本。
  5. 两路 + 重排是甜点,三路(BM25 + 稠密 + SPLADE)在前沿更优但要基础设施。
  6. Recall@k 是 RAG 检索器首要数字:正确段落不在检索集,阅读器就答不了。
  7. 80% 的 RAG 失败在摄入与分块:先修分块,分块策略比分块大小更重要。
  8. k_rerank=3 通常最优:再多只增 token 成本与延迟,不提升答案质量。
  9. HyDE 免训练提精确率:生成假设答案再嵌它来检索,弥合短问题与长文档的措辞鸿沟。
  10. 一切版本化:提示、分块、嵌入、重排器,任何漂移都静悄悄破坏答案,上 CI 门防回归。

下一节,我们换一个角度——从「找文档」转向「理解一整批文档在讲什么」,进入「主题模型」,看 LDA 与现代神经主题模型如何从语料里抽出隐含话题。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U