Inverted-Index-倒排索引

倒排索引

倒排索引(Inverted Index)发明的目的,本质只有一个:

让“从词找文档”这件事,从不可用,变成可规模化、可工程化、可实时的事情。

介绍

比如下列的文本

Doc1: 我喜欢学习人工智能
Doc2: 我喜欢学习机器学习
Doc3: 人工智能改变世界
Doc4: 学习让世界更美好

传统的索引做法是:

Doc1 → [我, 喜欢, 学习, 人工智能]
Doc2 → [我, 喜欢, 学习, 机器学习]
Doc3 → [人工智能, 改变, 世界]
Doc4 → [学习, 让, 世界, 更, 美好]

这种结构的核心问题是:

它适合“从文档看内容”,不适合“从词找文档”。

你想查「人工智能」,必须遍历所有文档,复杂度为O(N)

正排索引-倒排索引

而倒排索引的做法为:

  1. 倒排索引做了一个方向反转
  2. 对文本进行 Tokenize / 分词
  3. token 作为 key
  4. 文档列表(posting list)作为 value
我         → [Doc1, Doc2]
喜欢       → [Doc1, Doc2]
学习       → [Doc1, Doc2, Doc4]
人工智能   → [Doc1, Doc3]
机器学习   → [Doc2]
改变       → [Doc3]
世界       → [Doc3, Doc4]
让         → [Doc4]
更         → [Doc4]
美好       → [Doc4]

这样我在查找一个词时,时间复杂度从 O(N) 降为 O(1) + posting list 扫描

引入的新问题

  1. 公共词太多(信息熵极低)

例如: 的、是、了、and、the、to、我、你、他

特点:

  • 几乎出现在所有文档
  • 对区分文档 毫无帮助
  • posting list 极长

不但无助于排序,还会拖慢查询

  1. 词表(vocab)爆炸

例如:

  • 人名
  • 错别字
  • 只出现 1 次的词
  • OCR 噪声
  • 非法 token

结果:

  • vocab 数量暴涨【极其稀疏】
  • posting list 极短
  • 空间浪费
  • cache miss 严重

工业界的选择

Lucene / Elasticsearch / OpenSearch 的共识:

不是“所有词都值得被索引”

算法介绍

信息检索(Information Retrieval, IR)

主流的打分算法

1.TF-IDF

TF-IDF(Term Frequency-Inverse Document Frequency,词频-逆文档频率)是一种常用的文本挖掘方法,用于衡量一个词语在文档集合中的重要性。

核心思想

TF-IDF 的核心思想是: 一个词是否“重要”,取决于它在“当前文档中是否常见”,以及它在“整个语料中是否罕见”。

计算公式

  1. TF(词频)

TF 为词频,表示某个词在所有文档中出现的稀有程度

公式为: $$ TF = \frac{某词在当前文档出现的次数}{当前文档的总词数} $$ OR $$ TF(t,d)=\frac{f(t,d)}{\sum_{t’\in d}f(t’,d)} $$

  1. IDF

IDF 衡量某个词在所有文档中出现的稀有程度。 公式为: $$ IDF = log(\frac{总文档数}{包含该词的文档数 + 1}) $$ OR $$ IDF(t)=log(\frac{N}{df(t)+1}) $$

  • N:文档总数

  • df(t):包含词 ttt 的文档数

  • +1:防止除零(平滑)

  1. TF-IDF

综合词频和逆文档频率,反映词语对当前文档的区分能力。 公式: $$ \text{TF-IDF} = TF \times IDF $$

最终效果

情况 结果
高频 + 全文档常见 权重极低
文档内高频 + 全局稀有 权重极高

2.BM25

BM25 全称Okapi BM25

工业默认算法

TF-IDF 在工程上有两个问题:

  1. 长文档天然占优势
  2. TF 线性增长不合理

BM25 是 TF-IDF 的工程改进版

Lucene / Elasticsearch / OpenSearch 默认使用 BM25

核心思想:

  • TF 增益递减(出现 100 次 ≠ 100 倍重要)
  • 文档长度归一化
  • 参数可调,适合不同业务

你可以理解为:

BM25 = 可控、稳定、可调参的 TF-IDF

BM25打分公式

对一个查询 $$q$$ 和文档 $$D$$,BM25 的总分是把查询里每个词的贡献相加: $$ \mathrm{score}(D, q)

\sum_{t \in q} \mathrm{IDF}(t) \cdot \frac{ f(t, D),(k_1 + 1) }{ f(t, D) + k_1 \cdot \left( 1 - b + b \cdot \frac{|D|}{\mathrm{avgdl}} \right) } $$ 各符号含义:

  • **t:**查询中的某个词(term)
  • **f(t,D):**词 $t$ 在文档 $D$ 里的出现次数(TF)
  • **|D|:**文档长度(通常是分词后的 token 数)
  • **avgdl:**全库文档平均长度
  • **k_1:**控制 TF “饱和”速度(常用 1.2~2.0)
  • **b:**控制长度归一化强度(0~1,常用 0.75)
  • **IDF(t):**逆文档频率(下面说)

BM25里的DIF

常见 BM25 IDF(Okapi 版本): $$ IDF(t)=log(\frac{N-n_t+0.5}{n_t+0.5}+1) $$

  • N:总文档数

  • n_t:包含词 ttt 的文档数(DF)

直觉:越稀有的词,IDF 越大;越常见的词,IDF 越小。

具体做法

1) 用倒排索引取候选文档

对查询分词:q=[t1,t2,…] q = [t_1, t_2, …]q=[t1,t2,…] 用倒排索引拿到每个 term 的 posting list,然后 union / intersection 得到候选文档集合。

2) 对每个候选文档计算 score

对每个 term:

  • 从 posting list 里拿到 f(t,D)f(t,D)f(t,D)(很多系统的 posting 里直接存 TF)
  • 取文档长度 ∣D∣|D|∣D∣
  • 用全局统计得到 N,nt,avgdlN, n_t, \text{avgdl}N,nt,avgdl
  • 代入公式算该 term 的贡献并累加

3) 按 score 排序输出 TopK

score 高的排前面。

3. 语义相关度

倒排索引有一个无法绕过的本质限制:

它只理解“词”,不理解“意思”

例如:

“我想买一部手机”
“有没有性价比高的安卓”

语义检索

语义检索的做法

  1. 使用模型将文本转为向量(Embedding)
  2. 在向量空间中做相似度搜索(ANN)

ANN(Approximate Nearest Neighbor)是一类用于高维向量空间的近似相似度搜索算法, 通过牺牲极少量精度,换取数量级的性能提升, 是语义检索与向量数据库能够工程化落地的关键技术【此处暂时不展开了】

常见方式:

  • 倒排索引 → 粗召回
  • 向量检索 → 语义召回
  • Rerank → 精排

这种语义的思想最早在1953年就有出现

2013年的 Word2Vec 才实现了工程上可用

实践

1. 本地文档模糊查询

使用的技术为:

  • 模糊匹配
  • 倒排索引

代码仓库在:

采用的三方库有:(这些库的性能超乎意料,速度出奇的快)

jieba —> 中文分词

rapidfuzz ----> 高性能的模糊字符串匹配

docs

2. OpenSearch

使用开源的 opensearch 来做简单演示

OpenSearch 是aws基于Elasticsearch 一个分支的版本

代码仓库在:

Docker启动服务

docker pull opensearchproject/opensearch:3

docker pull opensearchproject/opensearch-dashboards:3

opensearch-dashboards 可视化窗口

3. 语义检索

代码仓库在:

大概演示的效果,RAG有很多工程实践的难点,想做好其实并不容易

聊斋志异检索

github