Sparse-Vector-Retrieval

需要比布尔更有语义,又要比稠密向量更可解释(哪几个词项拉高了分)。根因是单向量维度不可审计。本方法用 SPLADE 一类稀疏语义。长尾同义仍弱于 ColBERT/稠密,适合术语密集库。

本文属于 RAG 工程框架中的「2 索引与召回」环节,聚焦「Sparse Vector Retrieval」方法。

定位

维度 内容
角色 可解释的稀疏语义检索
输入 → 输出 query → 稀疏向量 → 倒排点积 Top-k
默认组合 SPLADE + Pyserini;或 ES learned sparse
何时不用 需要 token 级对齐(ColBERT);或只要硬过滤(Boolean)

核心机制

$$
\mathrm{score}(q,d)=\sum_{t} w_q(t),w_d(t)
$$

多数 $w(t)=0$。非零维就是「亮了的词项」(可含模型扩展的同义项)。

实现路径与心智:用 SPLADE 一类编码器把 query/doc 写成词表上的稀疏权重,建倒排,检索就是稀疏点积。底层心智:仍是「看哪些词项亮了」,比 BM25 多一层模型扩词,比 dense 可审计。调试时先看非零维,而不是看一个 768 维黑盒。

优缺点

  • 优点:可解释;部署比 ColBERT 轻。
  • 缺点:长尾改写弱于 dense/ColBERT。

契约与走通样例

输入

1
{"query": "GAPDH knockdown 48 h"}

中间量

$w_q$ 非零:GAPDH:1.8, knockdown:1.1, siRNA:0.9, 48h:0.7(模型扩出 siRNA)。与方法段点积 $4.2$,与无关 actin 段 $1.1$。

输出

1
[{"doc_id": "P-GAPDH-01-C1", "score": 4.2, "top_terms": ["GAPDH", "siRNA", "48h"]}]

社区实现

SPLADE + Pyserini。风险:词表与分析器不一致时扩展项对不上倒排。

工程落地

最小可运行示例

复制为 .py 后直接运行(仅标准库)。生产稀疏编码换成 SPLADE / Pyserini,倒排换成 ES learned sparse。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
"""词表上的稀疏权重做点积;可打印点亮了哪些 term。"""
from __future__ import annotations

from collections import Counter


class SpladeLikeEncoder:
"""教学用:查询词 TF 作权重,并给同义 term 一小点扩展。

生产:from transformers import AutoModelForMaskedLM # SPLADE
或 pyserini 的 SpladeQueryEncoder。
"""

synonyms = {"knockdown": "silencing"}

def encode(self, text: str) -> dict[str, float]:
weights = Counter(text.lower().split())
for t, w in list(weights.items()):
if t in self.synonyms:
weights[self.synonyms[t]] += 0.3 * w
return dict(weights)


class SparseInvertedIndex:
"""term → (doc_id, weight) 倒排。生产对应 Lucene/ES 稀疏字段。"""

def __init__(self, docs: dict[str, str], encoder: SpladeLikeEncoder) -> None:
self.postings: dict[str, list[tuple[str, float]]] = {}
for doc_id, text in docs.items():
for term, w in encoder.encode(text).items():
self.postings.setdefault(term, []).append((doc_id, w))

def dot_topk(self, q_vec: dict[str, float], k: int) -> list[tuple[str, float]]:
scores: dict[str, float] = {}
for term, qw in q_vec.items():
for doc_id, dw in self.postings.get(term, []):
scores[doc_id] = scores.get(doc_id, 0.0) + qw * dw
return sorted(scores.items(), key=lambda x: -x[1])[:k]


if __name__ == "__main__":
enc = SpladeLikeEncoder()
index = SparseInvertedIndex(
{
"P-kd": "GAPDH knockdown siRNA 20 nM",
"P-other": "actin western blot",
},
enc,
)
q = "GAPDH silencing 20 nM"
print(enc.encode(q))
print(index.dot_topk(enc.encode(q), k=1))

参数

参数 起点 影响
查询非零维上限 数十 过大变慢且噪声词亮起
top_k 20 交给融合或精排

失效—信号—螺丝

  • 扩词漂移:点亮无关领域词。信号:top_terms 与问句无关。螺丝:提高稀疏阈值。
  • 分析器不一致:文档侧切词与模型词表不同。螺丝:统一 tokenizer。
  • 当 dense 用:丢掉可解释性还更慢。螺丝:保留 term 调试日志。

规模(100 篇生物学 PDF)

GPU 12–16 GB;稀疏索引 约 3–7 GB;编码+建库 0.8–2.5 GPU·h

-------------本文结束感谢您的阅读-------------