信息检索与搜索 本节摘要: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、子词)。
阅读完本节,你应当能够:
用户输入「有人为了钱撒谎会怎样」,期待找到真正覆盖该情形的法条「印度刑法典第 420 条」。关键词检索完全错过(词汇不重叠);语义检索若嵌入没在法律文本上训过也会漏。真实搜索必须同时处理这两类问题。
信息检索是每个 RAG 系统、每个搜索框、每个文档站模糊查找底下的流水线。2026 在生产中有效的架构不是单一方法,而是一条由互补方法串成的链,每一环接住上一环的失败:
四层,按需取用:
💡 三路检索(BM25 + 稠密 + SPLADE 之类的学习稀疏)在 2026 基准上优于两路,但需要学习稀疏索引的基础设施。对多数团队,两路 + 交叉编码器重排是甜点。
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 的贡献):
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.5 或 e5-large-v2。
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 是发表默认,基本不用调。
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。
| 指标 | 含义 |
|---|---|
| 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 准确率。阅读器修不了检索器漏掉的。
k_rerank=3 通常最优。 每多加一块都增加 token 成本与生成延迟,却不提升答案质量。如果你的 k=8 还比 k=3 好,说明重排器没发挥好。据 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.
hybrid_search,测 20 条查询,对比纯 BM25、纯稠密、混合三者在 recall@5 上的差异。MultipleNegativesRankingLoss(Sentence Transformers)在你的领域上微调稠密编码器。用 500 对查询-文档构训练集,对比微调前后的 recall。1/(k+rank+1) 合并两份排名,忽略分数尺度,k=60 是论文默认。k_rerank=3 通常最优:再多只增 token 成本与延迟,不提升答案质量。下一节,我们换一个角度——从「找文档」转向「理解一整批文档在讲什么」,进入「主题模型」,看 LDA 与现代神经主题模型如何从语料里抽出隐含话题。