想象你是图书馆总管理员。馆里有一百万册书,读者跑来问你:“找所有提到'北京'的书。”如果一本本翻,几个月都翻不完。聪明的你会提前做一套索引卡片柜:每张卡片正面写一个关键词,背面按顺序写下所有出现过这个词的书号。读者报一个词,你直接抽出对应卡片。这套“词→书号”的卡片系统,就是搜索引擎的倒排索引。 但卡片不是凭空出现的:你得先决定把句子拆成哪些词——分词;把每个词对应的书号串起来——倒排表设计;卡片太多柜子放不下——索引压缩;每天有新书进来不能重抄全部卡片——增量索引;偶尔换新馆彻底翻新——全量重建;读者要立刻搜到刚上架的书——实时策略;馆大书多人少——工程实践。整篇文章,就是讲你如何从零建起并不断优化这套卡片系统。

岗位关联

对AI从业者而言,索引构建决定了搜索与RAG系统的查询效率和成本。掌握它,你才能设计出支撑千万级文档、秒级更新的生产级检索系统。

目录
  1. 可视化演示
  2. 1. 分词预处理
  3. 2. 倒排表设计
  4. 3. 索引压缩
  5. 4. 增量索引
  6. 5. 全量重建
  7. 6. 实时策略
  8. 7. 工程实践
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

本节配有交互动效(进入视口后自动加载,约 3KB)。

倒排索引构建流程 ——文档→分词→词项→倒排表 词典:所有唯一词项集合 倒排表:词项→文档ID列表 score = TF-IDF = tf * log(N/df),BM25改进版

1. 分词预处理

图书管理员整理“北京”这张卡片前,必须先判断书里哪些词值得做卡片。中文书没有空格,“我爱北京天安门”这句话,管理员得凭经验切成“我”“爱”“北京”“天安门”。切得好,整套卡片系统才准确;切得烂,“天安门”被切成“天安”和“门”,读者按“天安门”查就永远找不到这本书。分词看似是细节,其实是索引构建的第一道地基。

这就是中文检索的第一道工序——分词。英文单词天然以空格分隔,中文却需要额外的工具:一套常用词词典加匹配规则。系统扫描文本时从左到右尝试最长匹配:“北京”比“北”长,优先切出“北京”。分词器还会顺势过滤掉“的”“了”这类没有检索价值的停用词,减轻后续倒排表的负担。分词粒度直接决定检索质量:切得太粗漏召回,切得太细噪声大。

分词完成后,需要统计词在文档中的分布情况,其中最基础的统计量是词频(Term Frequency)。它衡量一个词在单篇文档里的重要性:出现得越多,说明它对这篇文章越有代表性。公式如下:

图书管理员会把词频比例记在卡片角落:某本书里“北京”出现比例越高,这张卡片在检索“北京”时权重越大。分词做得好,每张卡片都准确指向真正相关的内容;分词偷懒,整套卡片系统从源头就失真了。

tft,d=nt,d∑knk,d\displaystyle \text{tf}_{t,d} = \frac{n_{t,d}}{\sum_{k} n_{k,d}}
词 t 在文档 d 中出现的次数,除以文档 d 中所有词出现次数之和,得到一个 0 到 1 之间的比例,衡量这个词对这篇文档的代表性。
  1. 识别分子词 t 在文档 d 中的出现次数,管理员数一数“北京”在这本书里出现了几次。
    nt,dn_{t,d}
  2. 分母求和把文档 d 中所有不同词的出现次数加总,得到全文总词数。
    ∑knk,d\sum_{k} n_{k,d}
  3. 整体相除分子除以分母,得到词 t 在文档 d 中的占比词频,范围在 0 到 1 之间。
    nt,d∑knk,d\frac{n_{t,d}}{\sum_{k} n_{k,d}}

2. 倒排表设计

分词完成后,管理员要为每个关键词制作一张“位置卡”:卡片正面写关键词,背面按书架顺序写下一串书号。“北京”这张卡片背面写着 [1, 3],说明第1排和第3排书柜都出现过它。所有卡片按词条排序,就构成一套完整的“词→书号”反向定位系统——这就是搜索引擎的倒排索引。

倒排表的核心是 Posting List,即每个词对应的一个文档 ID 列表。构建时,每处理一篇文档,就把文档 ID 追加到该词对应的列表里,最终按 ID 升序排序,方便后续合并与压缩。这个过程和正排索引正好相反:正排记录“每本书包含哪些词”,倒排记录“每个词出现在哪些书”。

描述一个词在全部文档中的重要程度,需要文档频率(Document Frequency):

管理员看着“的”“是”这种卡片串得老长却没什么用,心里明白:高频词不一定是关键词。倒排表设计不仅要把结构建对,还要为后续压缩和权重计算打基础——哪些词该好好存,哪些词可以轻量处理,都从这张“词→书号”卡片开始规划。

dft=∣{d∈D∣t∈d}∣\displaystyle df_t = \left| \left\{ d \in D \mid t \in d \right\} \right|
在所有文档 D 中,统计包含词 t 的文档一共有多少篇。它和词频不同:词频看单篇内出现次数,文档频率看横跨多少篇文档。
  1. 全集 D全部文档编号的集合,代表图书馆里所有书的编号。
    DD
  2. 筛选条件从 D 中挑出所有包含词 t 的文档编号。
    {d∈D∣t∈d}\{ d \in D \mid t \in d \}
  3. 取大小对筛选结果计数,得到词 t 的文档频率。
    ∣{d∈D∣t∈d}∣\left| \{ d \in D \mid t \in d \} \right|

3. 索引压缩

索引卡片越来越多,柜子快塞不下了。管理员发现一个规律:书号是按顺序写的,[1, 3, 8, 20] 这张卡片不需要完整存四个数,只要记第一个书号,再记后面的“间隔”——1、2、5、12。间隔比原编号小得多,写起来自然省地方。这就是倒排索引压缩的朴素起点。

利用 docID 的有序性做差值编码,是压缩的核心思路。原始文档 ID 可能上千万,而相邻文档 ID 的差值往往很小,小数字可以用更少的字节表示。变长字节编码(VByte)就是按需分配字节:小数值 1 个字节就能存下,大数值才用更多字节。差值编码的公式非常直观:

除了差值法,位图压缩用 0/1 位串标记每个文档是否包含该词,适合分布密集的列表;主流检索引擎的底层综合使用这些方法。卡片柜经过这轮“缩写改造”,空间占用大减。索引压缩不只是省磁盘——更小的数据读取更快,内存能放更多索引块,管理员终于不必频繁扩建柜子。

Δi=docIDi−docIDi−1(i≥2)\displaystyle \Delta_i = docID_i - docID_{i-1} \quad (i \geq 2)
从第二个文档 ID 开始,只保存与上一个文档 ID 的差;第一个编号原样保存,解码时从头累加即可还原。
  1. 首个基准列表第一个文档 ID 原样保存,作为累计起点。
    docID1docID_1
  2. 相邻求差第 i 个文档 ID 减去第 i−1 个,得到差值。
    docIDi−docIDi−1docID_i - docID_{i-1}
  3. 差值序列整体公式,解码时从第一个基准开始逐个累加还原。
    Δi=docIDi−docIDi−1(i≥2)\Delta_i = docID_i - docID_{i-1} \quad (i \geq 2)

4. 增量索引

图书馆每天都有新书进馆,如果每进一本就把全部卡片重抄一遍,管理员早就累倒了。聪明的做法:新书单独做一套小卡片,先夹在前台的小抽屉里,读者检索时同时翻大柜和小抽屉;攒到一定量,再把小卡片整理合并进大柜。这就是增量索引的雏形。

系统为新增文档构建独立的“段”(Segment),段之间彼此独立、可并发查询。新文档的索引延迟从“全量重做”的几小时降到“单独构建”的几秒。但段不是越多越好——一次查询要扫描所有段再合并结果,段多了反而变慢,因此需要定期段合并。合并两个段的倒排列表时,长度计算不是简单相加,因为同一文档可能在两个旧段里都有记录,需要去重:

小卡片夹、大卡片柜、加上定期的“合卡仪式”,图书馆在不停业的前提下维持了检索的实时性和整洁度。增量索引的核心哲学就是:把“全量重来”拆成“小步快跑 + 定期大扫除”。

∣Pnew∣=∣Pa∣+∣Pb∣−∣Pa∩Pb∣\displaystyle |P_{new}| = |P_a| + |P_b| - |P_a \cap P_b|
合并后列表长度 = 两段长度之和减去两段的重复文档数。删除文档也不急着物理清除,先用删除标记逻辑跳过,合并时再真正剔除。
  1. 两段长度两个旧段的倒排列表长度。
    ∣Pa∣,∣Pb∣|P_a|, |P_b|
  2. 交集去重两个段中同时包含的文档 ID 取交集,计数为重复部分。
    Pa∩PbP_a \cap P_b
  3. 合并结果两段长度相加减去重复,得到新列表长度。
    ∣Pnew∣=∣Pa∣+∣Pb∣−∣Pa∩Pb∣|P_{new}| = |P_a| + |P_b| - |P_a \cap P_b|

5. 全量重建

有时候图书馆要搬家到新馆舍,旧卡片布局全乱了,管理员决定推倒重来:把所有书按新规则重新拆词、重新做卡。虽然费时费力,但重建出的卡片系统最干净、顺序最规整,没有任何历史包袱。这就是全量索引构建。

全量构建从全部文档出发:扫描所有内容 → 分词 → 收集词条 → 排序 → 生成倒排表。流程简单粗暴,结果一致性最好,适合首次建库、或索引结构升级后的整体重建。但文档量巨大时,内存根本放不下所有中间结果,这时就需要外部排序。

外部排序的做法是:把数据切分成多个有序的临时段,再通过多路归并合成最终索引。归并趟数决定磁盘 IO 次数,公式为:

全量重建是“慢但彻底”的工程手段。它不追求实时,而是追求最终一致性:所有卡片从同一套规则重新生成,不会有旧卡和新卡规则打架的问题。生产环境中,全量构建通常放在低峰期批量执行,作为增量索引的“校准基准”。

R=⌈log⁡MK⌉\displaystyle R = \lceil \log_M K \rceil
K 是初始临时段个数,M 是每次最多能同时归并的段数。M 越大,归并趟数越少,构建越快,但内存占用也越高。
  1. K 个初始段内存放不下时,切分得到的临时段数量。
    KK
  2. M 路归并每趟最多同时读取 M 个段参与合并。
    MM
  3. 趟数计算每趟把段数降为原来的 1/M,取对数得到所需趟数。
    R=⌈log⁡MK⌉R = \lceil \log_M K \rceil

6. 实时策略

读者刚把新书放上架,转身就来前台问“这本书能搜到吗?”如果答案要等今晚的全量重建,读者早就走了。管理员需要让新书“即刻可查”:前台放一张临时登记表,新书信息先写上去,查询时先看临时表,再定时把登记表归档到主卡片柜。这就是实时索引的朴素模型。

技术实现上,核心是内存索引缓冲。新文档先写入内存中的倒排结构,查询路径合并检查内存索引和磁盘段。内存索引积累到一定程度——文档条数达到上限,或占用内存超过阈值——就触发一次刷新,把内存中的倒排表落盘为新的小段。刷新时机的选择是实时性与写入吞吐的平衡:

这套机制让索引达到“近实时”:新文档从写入到可见,延迟通常控制在秒级到分钟级,远快于全量重建。临时登记表、定时归档、小段落盘,让图书馆在高频进书时依然保证“上架即能搜到”。

T=min⁡(Tdoc,Tmem)\displaystyle T = \min(T_{doc}, T_{mem})
T_doc 是允许积累的最大文档条数,T_mem 是允许占用的最大内存字节。哪个先到,就触发刷新,保证资源不超限。
  1. 文档数阈值内存索引允许积累的最大文档条数。
    TdocT_{doc}
  2. 内存阈值允许占用的最大内存字节数。
    TmemT_{mem}
  3. 取最小哪个先到就触发刷新,避免内存溢出或索引过大。
    T=min⁡(Tdoc,Tmem)T = \min(T_{doc}, T_{mem})

7. 工程实践

图书馆规模大了,光靠一个管理员做卡片显然不够。馆长把书库按区域分给多个管理员,每人负责自己片区的拆词和做卡,最后把各片区的卡片汇合排序,合并成总卡片柜。这就是索引构建的并发化思路。

全量重建时把文档集合切成多个分片,每个分片由一个 worker 独立构建局部倒排表,最后做一次归并。并发数越高构建越快,但内存和 CPU 消耗也越大,需要根据机器规格动态调整。构建耗时的估算公式很直观:

工程上还要引入资源控制:限制构建进程的内存上限,防止与在线查询抢资源;同时用监控指标盯住构建速度、段数量、索引大小,及时发现合并积压等问题。从一个人单干到团队协作,索引构建的工程化就是把“会做卡”升级为“稳定、可控、可观测地做卡”。

tbuild=NP×S\displaystyle t_{build} = \frac{N}{P \times S}
N 是待处理文档数,P 是并行 worker 数,S 是单个 worker 的每秒吞吐。总耗时等于文档数除以总吞吐。
  1. 文档总数待构建的全部文档数量。
    NN
  2. 并行度并发 worker 的数量,相当于管理员人数。
    PP
  3. 总吞吐与耗时每个 worker 每秒处理 S 篇,总耗时等于文档数除以总处理能力。
    tbuild=NP×St_{build} = \frac{N}{P \times S}

AI 应用场景

电商站内搜索的新品秒级可见

阶段:数据预处理与索引维护

解决的问题:新上架商品搜不到。传统方案直接扫描数据库按名称模糊查询,数据量大后速度慢且不支持多词组合。采用增量索引+实时策略:新品先写入内存倒排缓冲,秒级刷新为小段,搜索立即可见;定期段合并控制查询开销。

具体案例:某电商平台每天数千款新品上架,运营要求上架 1 分钟内可搜。工程师用内存索引缓冲加 2 分钟刷新周期实现近实时检索,凌晨低峰期执行段合并。

日志检索平台的空间优化

阶段:全量构建与编码优化

解决的问题:海量日志(每天数十亿条)全文检索的磁盘成本与构建耗时。全量重建配合差值编码和位图压缩,把索引体积降到原始日志的 10%-20%;低峰期定时全量重建保证数据一致性。

具体案例:SRE 团队搭建日志平台,每天新增 TB 级日志。采用增量索引实时入库,夜间对昨日日志做压缩合并,月索引体积减少约 60%。

推荐系统的标签召回

阶段:特征索引构建

解决的问题:物品标签到候选集的快速匹配。把标签当作“词”、物品 ID 当作“文档 ID”构建倒排索引,用并发构建加速千万级物品的索引生成,查询用户兴趣标签可毫秒级召回候选池。

具体案例:短视频推荐中每个视频打上“分类 + 话题”标签。工程师将标签映射到视频 ID 的倒排表,用户刷新时按兴趣标签并行查询多个列表再合并排序。

Python 演示

python53 行
# -*- coding: utf-8 -*-"""迷你倒排索引构建器: 词典分词 + 倒排表 + 增量合并"""import collections DIC = ["人工智能", "搜索引擎", "天安门", "北京", "索引", "我们", "学习", "构建"] def tokenize(text, dic):    i = 0    toks = []    while i < len(text):        best = text[i]        for w in dic:            if text.startswith(w, i) and len(w) > len(best):                best = w        toks.append(best)        i += len(best)    return toks def build_index(docs):    postings = collections.defaultdict(list)    for doc_id, text in enumerate(docs):        for tok in set(tokenize(text, DIC)):            postings[tok].append(doc_id)    for tok in postings:        postings[tok].sort()    return dict(postings) def merge_segment(base, extra):    merged = {}    for tok in set(list(base) + list(extra)):        merged[tok] = sorted(set(base.get(tok, []) + extra.get(tok, [])))    return merged def query(postings, word):    return postings.get(word, []) def main():    docs = [        "我们学习人工智能索引构建",        "搜索引擎使用倒排索引",        "北京天安门位于北京",    ]    idx = build_index(docs)    print("=== 构建结果 ===")    for w, pl in sorted(idx.items()):        print("词: %s -> 文档%s" % (w, pl))    print("查询'北京' ->", query(idx, "北京"))    extra = build_index(["天安门在北京"])    merged = merge_segment(idx, extra)    print("增量合并后'天安门' ->", query(merged, "天安门")) if __name__ == "__main__":    main()
目的:演示从词典分词、倒排表构建到增量段合并的完整索引生命周期。
  1. def tokenize(text, dic):定义分词函数,按词典做正向最长匹配
  2. text.startswith(w, i)从位置 i 开始尝试匹配词典中的词,命中则更新最长候选
  3. postings[tok].append(doc_id)把文档 ID 追加到该词的倒排列表,核心构建动作
  4. for tok in set(tokenize(text, DIC)):用 set 去重,避免同一篇文档重复写入
  5. merged[tok] = sorted(set(...))增量合并两个段并对文档 ID 去重排序
  6. postings.get(word, [])查询不存在的词时返回空列表,保证调用安全

常见误区

误区数据库里的索引(B+树)和搜索引擎的倒排索引都叫“索引”,应该是一样的东西。

正解两者数据结构与目标完全不同。数据库 B+树是“键→记录位置”的有序树,用于快速定位单条记录;倒排索引是“词→包含该词的文档集合”,用于检索匹配一组关键词的文档。想象图书管理员:B+树是“按书号查书在哪个书架”,倒排表是“按关键词找出所有相关书号”。用错会导致系统设计南辕北辙。

为何常见:两者都叫“索引”且都能加速查找,初学者很容易混淆概念边界。

误区分词切得越细召回越高,干脆切到每个单字,一定能覆盖所有查询。

正解单字粒度召回高但精度差。“天安门”切成“天”“安”“门”后,搜“天安门”反而匹配不到连续短语,噪声也变大。合理做法是用词典最大匹配保证长词完整,同时保留少量有区分度的单字。分词粒度要和检索需求匹配,不是越细越好。

为何常见:只看“召回率”忽略“精确率”,觉得切碎了总能命中更多结果。

误区增量索引建好后就不用管了,新段一直加下去,反正构建很快。

正解段越多查询越慢——一次查询要读所有段的倒排表再合并结果。必须定期段合并,把小段合并成大段,顺带物理清除删除标记。就像前台小卡片攒太多,找起来反而慢,定期并入大柜才高效。

为何常见:增量构建快,让人误以为“快”等于“永远最优”,忽视了段膨胀的代价。

误区压缩省空间但费时间,构建时不该用压缩。

正解差值编码和 VByte 这类方法反而常带来构建加速:压缩后数据体积小,磁盘读取和内存搬运更快,解码的 CPU 开销通常远小于 IO 节省。小数字还能提高缓存命中率。压缩省空间和提性能可以兼得。

为何常见:直觉上“压缩 = 解压 = 慢”,忽略了 IO 瓶颈往往比 CPU 解码更突出。

面试题

Q1. 倒排索引最核心的结构是?

单选题 · easy

  • A.词到文档列表的映射← 正确答案
  • B.文档到词的完整映射
  • C.文档编号到大小的映射
  • D.词语到字频统计的映射

答案:A。倒排索引的本质就是“词 → 包含该词的文档列表”,像卡片正面写词、背面写书号。B 是正排索引,C、D 与倒排无关。

Q2. 中文检索前必须做的关键处理是?

单选题 · easy

  • A.全文加密与压缩存储
  • B.分词得到有效词条← 正确答案
  • C.按行号重建正排表
  • D.对文档做镜像备份

答案:B。中文句子没有天然空格,必须先通过词典匹配等方法切分成词条,否则倒排表无法正确构建。压缩、镜像都是后续优化,不是前置关键步骤。

Q3. 增量索引的主要意义是?

单选题 · easy

  • A.减少新文档的索引延迟← 正确答案
  • B.消除全部索引压缩
  • C.替代全量重建与合并
  • D.使倒排表永远不排序

答案:A。增量索引只处理新文档,单独建段,把延迟从全量重做的数小时降到几秒。但增量不能替代段合并,也不能替代压缩,C 和 B 都是误解。

Q4. docID 列表 [1, 3, 8, 20] 做差值编码后是?

单选题 · mid

  • A.[1, 2, 5, 12]← 正确答案
  • B.[0, 2, 5, 12]
  • C.[1, 3, 8, 20]
  • D.[2, 5, 12, 20]

答案:A。第一个数 1 原样保留,后面依次为 3−1=2、8−3=5、20−8=12,所以结果是 [1, 2, 5, 12]。差值后数字变小,便于变长字节压缩。

Q5. 段合并的主要目的是?

单选题 · mid

  • A.减少段数提升查询效率← 正确答案
  • B.让新文档立即可见
  • C.压缩单个文档体积
  • D.增加词典匹配精度

答案:A。段越多,查询要扫描的倒排表就越多。段合并把小段合成大段,减少段数,同时物理清除删除标记,提升整体查询效率。

Q6. 实时索引常用的策略有哪些?

多选题 · mid

  • A.内存倒排缓冲区← 正确答案
  • B.周期刷新落盘分段← 正确答案
  • C.查询时全量扫文档
  • D.阈值触发段刷新← 正确答案

答案:ABD。内存缓冲 + 周期/阈值触发落盘是近实时索引的标准做法。查询时全量扫描文档是顺序检索,不是实时索引策略,C 错误。

Q7. 全量重建时内存放不下全部数据,正确的做法是?

单选题 · mid

  • A.丢弃部分低频词条
  • B.外部排序多路归并← 正确答案
  • C.只索引文档标题
  • D.改用单线程逐条构建

答案:B。内存不足时先切分成有序临时段,再用多路归并合成最终索引,保证所有词条都被处理。丢弃词条或只索引标题都会丢失信息。

Q8. 以下哪些属于索引压缩方法?

多选题 · hard

  • A.差值编码← 正确答案
  • B.变长字节编码← 正确答案
  • C.位图压缩← 正确答案
  • D.全文冗余备份

答案:ABC。差值编码利用 docID 有序性缩小数字,变长字节按需分配存储空间,位图用 0/1 位串表示文档是否含词。全文备份是冗余不是压缩。

Q9. 近实时索引期望达到的效果是?

单选题 · hard

  • A.新文档秒级可被搜到← 正确答案
  • B.每秒重建全部索引
  • C.索引完全不做压缩
  • D.查询强制全表扫描

答案:A。近实时意味着新文档写入后,经过短暂的内存缓冲和刷新,秒级到分钟级内可被检索到,而不是等待全量重建。每秒重建全部索引既不现实也无必要。

Q10. 关于段中删除文档的处理,正确的说法是?

多选题 · extreme

  • A.先逻辑标记跳过← 正确答案
  • B.合并时物理剔除← 正确答案
  • C.删除后立即重建全量
  • D.删除只需改正排表

答案:AB。删除文档先用标记逻辑跳过,避免频繁重写磁盘;等到段合并时再物理剔除,兼顾效率与一致性。每次删除都重建全量代价过高,D 忽略了倒排表也需要同步。