跳转至

倒排索引

Abstract

倒排索引(Inverted Index)是一种以 "term -> document list" 为核心映射的索引结构,用来快速回答“哪些文档包含这个词”。 它把查询从逐篇扫描文档,转化为对词项到文档列表的查表过程,是全文检索、布尔查询、短语匹配和相关性排序的基础。

inverted-index

倒排索引的目标是:给定一个查询词,快速找到所有包含该词的文档。

倒排索引 vs. 正排索引

与正排索引(Forward Index,即"文档 → 词列表")相反,倒排索引建立的是 词 → 文档列表 的映射。

  • 正排索引(Forward Index)


    文档 ID 包含的词
    Doc 1 apple, banana, cherry
    Doc 2 apple, date
    Doc 3 banana, cherry, date
  • 倒排索引(Inverted Index)


    出现的文档
    apple Doc 1, Doc 2
    banana Doc 1, Doc 3, Doc 5
    cherry Doc 1, Doc 3

查询 "apple" 时,只需直接查找索引表,O(1) 取得 [Doc 1, Doc 2],无需扫描全部文档。


核心组成

倒排索引由两部分构成:

词典

词典(Vocabulary / Dictionary)存储所有出现过的词(Term),通常使用哈希表或有序数组实现,支持快速精确查找或前缀查找。

每条词典记录包含:

  • Term:词本身;
  • Document Frequency(DF):包含该词的文档总数;
  • 指针:指向对应的倒排列表。

倒排列表(Posting List)

倒排列表存储某个词出现的位置信息,每条记录(Posting)通常包含:

  • Document ID:文档编号;
  • Term Frequency(TF):该词在文档中出现的次数;
  • Position List:词在文档中的位置序列(支持短语查询);
  • Offset:字符级偏移(支持高亮显示)。
"apple" → [(Doc1, TF=2, positions=[3,17]), (Doc2, TF=1, positions=[5])]

构建流程

构建倒排索引的主要步骤:

1. 文本预处理

原始文档经过以下处理后才进入索引:

  • 分词(Tokenization):将文本切分为词语。英文按空格/标点分,中文通常使用分词算法(如 jieba、HanLP);
  • 归一化(Normalization):统一大小写、全半角等;
  • 停用词过滤(Stop Words Removal):移除 "的"、"is"、"the" 等高频无意义词;
  • 词干提取 / 词形还原(Stemming / Lemmatization):将 "running" → "run",减少词形变体带来的碎片化。

2. 词频统计

对每个文档,统计每个词出现的次数和位置,生成 <Term, DocID, TF, Positions> 的记录。

3. 排序与合并

将所有记录按 Term 排序,相同 Term 的记录合并为一条倒排列表,最终写入磁盘。

4. 压缩存储

倒排列表通常对 DocID 使用差分编码(Delta Encoding) 后再压缩(如 VByte、FOR、PFOR),大幅降低存储占用。

原始 DocID 列表:[1, 5, 10, 11, 20]
差分编码后:    [1, 4,  5,  1,  9]

查询处理

单词查询

直接在词典中查找词,返回对应倒排列表中的 DocID 集合。

布尔查询

多个词之间用 AND / OR / NOT 组合:

  • AND(交集):对两个有序倒排列表做归并交集,时间复杂度 O(m+n);
  • OR(并集):归并两个列表取并集;
  • NOT:对倒排列表做差集。
查询:"apple AND banana"
apple  → [Doc1, Doc2]
banana → [Doc1, Doc3]
结果   → [Doc1]        ← 交集

短语查询

需要 Position 信息。例如查询 "apple banana",要求 banana 的 position = apple 的 position + 1。

相关性排序(TF-IDF & BM25)

全文搜索不只返回"是否匹配",还需要按相关性排序。最常用的两种模型:

TF-IDF:

\[ \text{TF-IDF}(t, d) = \text{TF}(t, d) \times \log\frac{N}{\text{DF}(t)} \]
  • \(\text{TF}(t,d)\):词 \(t\) 在文档 \(d\) 中的频率,越高说明越相关;
  • \(\text{IDF}(t)\):逆文档频率,包含该词的文档越少,区分度越强。

BM25(Lucene/ES 默认):

\[ \text{BM25}(t, d) = \text{IDF}(t) \cdot \frac{\text{TF}(t,d) \cdot (k_1 + 1)}{\text{TF}(t,d) + k_1 \cdot \left(1 - b + b \cdot \frac{|d|}{\text{avgdl}}\right)} \]

其中:

  • \(k_1\):词频饱和系数,控制 TF 增长的上限,通常取 1.2 ~ 2.0;
  • \(b\):文档长度归一化系数,\(b=0\) 不归一化,\(b=1\) 完全归一化,通常取 0.75;
  • \(|d|\):文档词数;\(\text{avgdl}\):语料库平均文档词数。

BM25 在 TF-IDF 基础上加入了文档长度归一化,避免长文档因词频绝对值高而获得不公平的优势,是现代搜索引擎的事实标准(Elasticsearch / Lucene 默认打分算法)。


索引更新

静态索引

一次性构建,不支持增量更新,适合离线批量索引场景。

动态更新策略

实时搜索引擎(如 Elasticsearch)通常采用以下策略:

  • 内存缓冲区(In-memory Buffer):新文档先写入内存中的小索引(Segment);
  • 定期刷新(Refresh):将内存 Segment 写入磁盘,使其可被搜索(默认 1s);
  • 段合并(Segment Merge):后台将多个小 Segment 合并为大 Segment,降低查询时需扫描的段数;
  • 删除标记(Delete Bitmap):删除/更新文档时先打标记,Merge 时再物理清除。

Lucene 的 Segment 模型

Lucene 中每个 Segment 本身就是一个完整的倒排索引,查询时并行搜索所有 Segment 再合并结果。这种 不可变(Immutable) 设计避免了并发写入冲突,大幅简化了索引的并发控制。


与向量索引的对比

维度 倒排索引 向量索引
检索方式 关键词精确 / 模糊匹配 语义相似度匹配
查询输入 文本关键词 向量(Embedding)
优势 精确、可解释、构建快 理解语义、跨语言、多模态
劣势 无法理解同义词、语义 召回不可控、计算成本高
典型系统 Elasticsearch、Lucene Faiss、Milvus、Weaviate
典型场景 日志搜索、电商搜索 RAG、以图搜图、推荐召回

混合检索(Hybrid Search)

现代搜索系统常将倒排索引与向量索引结合使用:倒排索引保证关键词精确召回,向量索引补充语义相关结果,最终通过 RRF(Reciprocal Rank Fusion) 等方法融合排序,兼顾精确性与语义理解能力。