BM25 靠精确词匹配召回文档。用户搜"怎样并行处理",文档里写的是"多线程编程"——BM25 一条也召不回来。dense retrieval 用向量弥补词汇鸿沟,但丢掉了精确匹配的优势:搜错误码 NullPointerException,dense 模型可能把它和所有异常类文档混在一起。

学习稀疏检索(learned sparse retrieval)试图兼得两头:保留倒排索引的精确匹配和可解释性,同时用模型扩展词汇覆盖范围。

词汇鸿沟的代价

回顾 BM25 的召回逻辑:query 中的 term 必须出现在文档中,文档才能进入候选集。这意味着:

  • 同义词缺失:用户搜"取消订单",文档写的是"订单退回"→ 无命中
  • 上下位词:搜"数据库",文档讨论的是"PostgreSQL"→ 无命中
  • 缩写/全称:搜"JVM",文档写的是"Java 虚拟机"→ 无命中

传统解法是同义词表。第 13 篇介绍的同义词扩展能缓解部分问题,但人工维护同义词表的成本随词汇量指数增长,且无法覆盖领域内的新术语和上下文相关的语义关系。

SPLADE:用 MLM 头生成 term 权重

SPLADE(Formal et al., 2021)的核心思路:把 BERT 的 Masked Language Model 头拿来给每个 term 打权重。

训练阶段

给定一段文本(query 或 document),SPLADE 做以下操作:

  1. 文本输入 BERT encoder,得到每个 token 位置的上下文向量
  2. 每个上下文向量过 MLM head,输出词表大小(如 30,522)的 logits
  3. 对所有 token 位置的 logits 取 max-pooling,得到一个词表大小的向量
  4. 对这个向量取 ReLU + log(1+x),生成稀疏的 term 权重
1
2
3
4
5
6
7
8
9
10
11
输入: "Java 并发编程指南"

MLM head 输出(max-pooling 后部分非零项):
"java"2.31
"并发"1.87
"编程"1.52
"多线程"0.93 ← 原文没有,模型扩展出来
"线程池"0.71 ← 原文没有,模型扩展出来
"concurrent"0.45 ← 跨语言扩展
"指南"0.38
...其余 term 权重为 0

关键观察:模型不只给原文 term 打权重,还"扩展"出了语义相关但原文不存在的 term。这正是 BM25 做不到的——模型在训练中学会了 term 之间的关联。

FLOPS 正则化

如果不加约束,模型可能给所有 term 都赋予非零权重——这就退化成了 dense retrieval,失去稀疏索引的效率优势。

SPLADE 通过 FLOPS regularization 控制稀疏度:

1
L_flops = Σ_j (mean_i(w_ij))^2

其中 w_ij 是第 i 个文档在第 j 个 term 上的权重。这个正则项惩罚在整个 batch 中普遍出现的 term(类似 IDF 的逆文档频率思想),迫使模型只保留区分性强的 term。

典型超参数下,每个文档的非零 term 数在 128-256 之间。

索引与检索

SPLADE 生成的稀疏向量可以直接存入倒排索引:

1
2
3
4
倒排表:
"java" → {doc1: 2.31, doc5: 1.89, doc12: 0.67, ...}
"多线程" → {doc1: 0.93, doc3: 2.15, doc8: 0.42, ...}
"线程池" → {doc1: 0.71, doc3: 1.53, ...}

检索时:

  1. query 也过 SPLADE 模型,生成稀疏 term 权重
  2. 用 query 的非零 term 查倒排表
  3. 对每个候选文档,相关性 = Σ(query_term_weight × doc_term_weight)

整个流程复用倒排索引的基础设施:skip list、WAND 剪枝、Top-K 堆——第 7-11 篇建立的所有结构都能直接用。

Lucene 中的实现路径

Lucene 的 FeatureField 可以存储 learned sparse 权重:

1
2
3
4
5
6
7
// 索引阶段:存储 SPLADE 权重
Document doc = new Document();
Map<String, Float> sparseWeights = spladeModel.encode(text);
for (Map.Entry<String, Float> entry : sparseWeights.entrySet()) {
doc.add(new FeatureField("sparse", entry.getKey(), entry.getValue()));
}
writer.addDocument(doc);
1
2
3
4
5
6
7
8
// 检索阶段:构建 SPLADE 查询
Map<String, Float> queryWeights = spladeModel.encode(queryText);
BooleanQuery.Builder builder = new BooleanQuery.Builder();
for (Map.Entry<String, Float> entry : queryWeights.entrySet()) {
Query tq = FeatureField.newLinearQuery("sparse", entry.getKey(), entry.getValue());
builder.add(tq, BooleanClause.Occur.SHOULD);
}
Query sparseQuery = builder.build();

FeatureField 在 Lucene 内部用跳表结构存储,支持高效的 Top-K 查询。

三路融合实验

在主线的 BM25 + dense + RRF 基础上,加入 learned sparse 变成三路:

1
2
3
4
5
6
7
8
9
10
query
├── BM25 检索 → 候选集 A
├── dense embedding → 候选集 B
└── learned sparse → 候选集 C

三路 RRF 融合

reranker 重排

最终结果

RRF 公式不变,只是从两路变三路:

1
RRF_score(d) = 1/(k + rank_bm25(d)) + 1/(k + rank_dense(d)) + 1/(k + rank_sparse(d))

对比实验设计

组合 nDCG@10 变化预期 额外成本
BM25 only 基线
BM25 + dense +15-25% embedding 推理
BM25 + sparse +10-15% SPLADE 推理 + 索引
BM25 + dense + sparse +20-30% 两套模型推理 + 两套索引

三路融合通常能在两路基础上再提 3-8%——收益递减,但对长尾查询(专业术语、罕见搭配)的提升最明显。

中文 learned sparse 的现实

中文 learned sparse 模型的公开选择有限。几个可用方向:

BGE-M3(BAAI, 2024)同时输出 dense、sparse 和 ColBERT 三种表示,是目前最直接的中文 learned sparse 来源。sparse 输出的 term 权重可以直接存入 FeatureField。

自训练路线:基于中文 BERT 加 MLM head,在领域数据上微调 SPLADE。需要对比查询对(正负样本)和足够的训练数据。对于本系列的 100 篇小语料,自训练不现实。

务实选择:如果语料规模小且领域集中,BM25 + dense 的两路融合已经覆盖大部分收益。learned sparse 的边际收益在大规模、宽领域的场景中更显著。

可解释性

learned sparse 相对 dense retrieval 的一个独特优势是可解释性。当用户问"为什么这篇文档排在第一"时:

1
2
3
4
5
6
7
8
9
10
Query: "Java 线程安全的集合"
Document: "ConcurrentHashMap 的分段锁实现"

BM25 命中 term: [无](词汇不匹配)
Dense 相似度: 0.82(黑盒)
SPLADE 命中 term: "java"1.2 × 2.3 = 2.76
"线程"0.9 × 1.8 = 1.62
"concurrent"0.7 × 2.1 = 1.47
"安全"0.4 × 0.6 = 0.24
总分: 6.09

每个 term 的贡献清晰可见——这在 debug 搜索质量时很有价值。

练习

  1. 用 FeatureField 为种子语料的每篇文档存储手工 term 权重(模拟 SPLADE 输出),验证检索是否正确
  2. 对比 BM25 和手工 sparse 权重在同义词查询上的召回差异
  3. 设计一个实验:固定 query 集,分别测 BM25、dense、模拟 sparse 的 nDCG@10,计算三路 RRF 融合的提升

延伸阅读

  • Formal et al., “SPLADE: Sparse Lexical and Expansion Model for First Stage Ranking”, SIGIR 2021
  • Formal et al., “From Distillation to Hard Negative Sampling: Making Sparse Neural IR Models More Effective”, SIGIR 2022
  • Xiao et al., “BGE-M3: Multi-lingual, Multi-functionality, Multi-granularity Text Embeddings Through Self-knowledge Distillation”, 2024
  • Lucene FeatureField Javadoc