想象你走进一座巨大的图书馆,这里藏书千万册,但每本书都没有书名页,只有一堆堆散乱的手稿。管理员老王想了个办法:他做了一张巨大的“主题指引卡”——每张卡片正面写上主题词(比如“人工智能”),背面按顺序贴满一张张小纸条,每张纸条上写着“这本书在第3排第2架”“那本书在第5排第1架”。这样,读者想找“人工智能”相关的书,老王只要抽出对应的卡片,按纸条上的位置去取书就行。 这张“主题指引卡”就是信息检索领域最核心的发明——倒排索引(Inverted Index)。而老王这套系统的精妙之处,恰恰在于它由三个高度协作的组件构成:用来快速定位卡片位置的“抽屉序号”(term词典)、卡片背面那张“存放位置清单”(postings倒排表)、以及老王按照读者需求组合多张卡片的“查询处理”流程。今天,我们就用老王图书馆的故事,把倒排索引的三大组件一个个拆开,看懂它们是如何配合,让搜索引擎能在零点几秒内从几十亿网页中找到你想要的答案。

岗位关联

掌握倒排索引的组件结构与查询流程,是理解搜索引擎、数据库全文检索和所有检索引擎的基石。无论你是做推荐系统、对话式AI还是搜索开发,这份认知决定了你优化系统性能的深度。

目录
  1. 可视化演示
  2. 索引全景结构
  3. term词典原理
  4. postings倒排表
  5. 查询处理流程
  6. 索引构建更新
  7. 压缩与性能
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示ARP协议通过广播请求获取目标MAC地址的完整流程:缓存查询、ARP广播、交换机泛洪、单播响应、缓存更新五个阶段。

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

阶段1:ARP缓存查询 主机A查找本地ARP缓存表,检查是否已有目标IP的MAC地址记录 缓存未命中,触发ARP地址解析流程 ARP Cache: IP→MAC ARP缓存命中率直接影响网络通信延迟

索引全景结构

老王的图书馆越开越大,他意识到“主题指引卡”不是一张,而是一套完整的系统。这套系统分为两大阶段:建立卡片库(索引构建阶段)和使用卡片库(查询处理阶段)。构建阶段像在整理图书——采购员收集新书(文档采集),老王浏览每本书的内容提取主题词(词项抽取),最后把所有卡片按主题词的字顺排列并编上架位号(排序合并)。查询阶段则像读者来找书——先根据主题词在词典抽屉里精确找到对应卡片(词项定位),然后取出卡片上的所有位置清单进行比对(倒排合并)。

技术上说,倒排索引以“词项”为键来组织文档,这与正向索引以文档为键完全不同。理解这个全景需要把握两个视角:从文档到词项的映射(构建过程),从词项到文档的反向映射(查询过程)。

老王把图书馆的卡片系统画成一张图:每个主题词就是一个抽屉(term词典),抽屉里是一串纸条(倒排表),纸条上写着书籍的架位号(文档ID)。读者来借书时,先找抽屉,再取纸条,最后按架位号找书——这就是一次完整的检索流程。整个倒排索引就是这张“抽屉-纸条”网络。

Index=∑t∈Terms(t→PostingsList(t))\text{Index} = \sum_{t \in \text{Terms}} \left( t \rightarrow \text{PostingsList}(t) \right)
倒排索引本质上是一个从词项到文档列表的映射函数,每个词项都对应一条倒排表。
  1. 索引主体$\text{Index}$ 表示整个倒排索引,是检索系统的核心数据结构。
    Index=∑\text{Index} = \sum
  2. 遍历词项集合$t \in \text{Terms}$ 表示遍历词典中的所有词项,每个词项都是一个独立单元。
    t∈Termst \in \text{Terms}
  3. 映射关系$t \rightarrow \text{PostingsList}(t)$ 表示词项 $t$ 指向它的倒排表,这张表记录了所有包含 $t$ 的文档ID。
    t→PostingsList(t)t \rightarrow \text{PostingsList}(t)

term词典原理

老王图书馆的抽屉越来越多,他开始面临新问题:读者说“人工智能”,他得在几千个抽屉里迅速找到对应那个。这就是 term 词典(Dictionary)的核心职责——存储所有出现过的词项,并能快速定位到该词项对应的倒排表位置。

词典设计的第一种方案是哈希表:老王给每个主题词算一个数字代号(哈希值),然后按代号把卡片放进 1000 个小格子。查找时,先算出词项的代号,直接跳到对应格子上——平均只需一次操作就找到卡片(O(1) 查询)。但代价是:哈希表要预留足够空格子避免冲突,空间浪费大(内存代价高),而且无法按字顺遍历所有词项。

第二种方案是 B 树:这像一个分层的目录树。老王把主题词按字母顺序排好,树根节点记录“中间词”来分岔,每个叶子节点存一批相邻词项。查找时从根开始逐层比较,每次读入一个节点(磁盘友好的页大小),比哈希表多几次比较但更省空间,还能支持范围查询(比如列出所有“人”开头的词项)。

老王最后把两种方案都试了一遍:小图书馆用哈希表,抽屉编号查得快;大图书馆改用 B 树,虽然找起来多几步,但能在有限房间内存下更多卡片。如果某天读者需要按主题词浏览整个目录(比如看“算法”系列),B 树还能顺着叶子节点一路扫过去——这是哈希表做不到的。

Dictionary={HashTable:lookup=O(1),memory=O(n)×αBTree:lookup=O(log⁡n),memory=O(n)/β\text{Dictionary} = \begin{cases}\text{HashTable}: & \text{lookup} = O(1), \text{memory} = O(n)\times\alpha \\\text{BTree}: & \text{lookup} = O(\log n), \text{memory} = O(n)/\beta\end{cases}
哈希表用空间换时间,B树用时间换空间,工程上常根据内存余量做选择。
  1. 两种方案对照$\text{HashTable}$ 与 $\text{BTree}$ 构成词典实现的两种基本选择。
    Dictionary={HashTable:…\text{Dictionary} = \begin{cases} \text{HashTable}: \dots \end{cases}
  2. 哈希查找代价$\text{lookup} = O(1)$ 表示哈希表可在常数时间内找到词项,$\alpha$ 是装载因子。
    O(1),memory=O(n)×αO(1), \text{memory} = O(n)\times\alpha
  3. B树查找代价$\text{lookup} = O(\log n)$ 表示 B 树的查找需要与节点层数成正比,$\beta$ 是页面的压缩率。
    O(log⁡n),memory=O(n)/βO(\log n), \text{memory} = O(n)/\beta

postings倒排表

抽屉找到以后,接下来看抽屉里的“纸条”——倒排表(Postings)。这是倒排索引的本体,记录了所有包含该词项的文档ID列表。老王有个重要习惯:每张纸条上的架位号按从小到大顺序排列。这看似简单的要求,却是后续高效查询的根本前提——就好像做算数时数字必须对齐,排序后的ID才能支持快速的指针归并。

倒排表还有两种典型的组织策略。链式指针是一种直观的方案:每个节点存一个文档ID和指向下一个节点的指针,像一串手拉手的小朋友。缺点是内存碎片化,而且只能从头到尾一个个走。跳表优化则是在链式结构上增加“跳跃指针”(skip pointers)——每个节点同时记录“下一个相同层级的指针”,让查找可以大步跳过不匹配的节点。老王的图书馆里,跳表就像在书架之间架起了“快速通道”,从第1排直接跳到第8排,不必逐个经过中间的书架。

更进一步,倒排表还记录词项在文档中的位置信息(position)。这就像老王在纸条上不只写“第3排”,还写“第3排第2架左起第5本”。有了位置,搜索引擎就能支持“人工智能 芯片”这样的短语查询——要求两个词不仅出现在同一文档,还必须紧挨着出现;也能在做结果页时高亮显示网页中命中的关键词。

老王后来感慨,排序这个习惯是他整个系统最聪明的决策。正因为每张纸条的架位号有序,他才能用两根手指头同时划过两张纸条,快速圈出同时出现在两处的书籍——如果纸条乱序,他就得一张张全看一遍,效率天差地别。倒排表的有序性,决定了倒排索引能在毫秒级完成复合查询。

Postings(t)=[docID1,docID2,…,docIDn],docIDi<docIDi+1\text{Postings}(t) = \big[ \text{docID}_1, \text{docID}_2, \ldots, \text{docID}_{n} \big], \quad \text{docID}_i < \text{docID}_{i+1}
有序倒排表是高效查询合并运算的基础,任何已排序序列都能用指针推进实现快速交并集。
  1. 倒排表本体$\text{Postings}(t)$ 表示词项 $t$ 对应的倒排表。
    Postings(t)=[…]\text{Postings}(t) = \big[ \dots \big]
  2. 列表内容$\text{docID}_1, \text{docID}_2, \ldots$ 表示文档ID按序排列。
    docID1,docID2,…,docIDn\text{docID}_1, \text{docID}_2, \ldots, \text{docID}_{n}
  3. 有序约束$\text{docID}_i < \text{docID}_{i+1}$ 强调ID必须严格递增,这是所有合并算法的前提。
    docIDi<docIDi+1\text{docID}_i < \text{docID}_{i+1}

查询处理流程

现在到了真正“查询处理”的环节。读者来找老王,想找同时包含“人工智能”和“芯片”的书。老王抽出两张卡片,把两张纸条并排放在桌上,开始合并。这个动作在技术上的术语叫AND合并(交集运算)。

AND合并的算法非常巧妙,用的是双指针推进:老王左手点着“人工智能”纸条的第一条(文档3),右手点着“芯片”纸条的第一条(文档1)。两者不同,于是指向较小文档ID(1)的那只手先挪——因为两串数字都是升序,文档1不可能出现在另一张卡片上,跳过省时间;当两手都指向文档3时,就找到了一个交集,记录下来,然后两手一起右移。如此往复,直到任一条纸走完。这个算法的时间复杂度是 $O(n+m)$,n 和 m 分别是两条倒排表的长度。

OR合并(并集运算)要简单些:老王把两张纸条合成一摞,相同的只记一次(去重),不同的都保留,最后按架位号重新排一遍。NOT 运算则像是“差集”:从第一张纸条中划掉所有出现在第二张纸条中的文档。短语查询更精细——老王的纸条记录的不只是排号,还包括具体位置,需要验证“人工智能”和“芯片”是否在同一个文档里连续出现:先按 AND 初步找出文档,再检查各自的位置列表是否有满足“间隔为1”的位置对。

老王向读者演示:他每次比较完两个不同数字,就果断放弃较小的那个,因为知道它绝不可能出现在另一张卡片上。这种“果断抛弃”的策略,让合并过程从不回头,总是直直地前进。就像两列按身高排好的队伍要找出相同身高的人,最高的那位裁判只需不断让矮个子那队前进——若两队人都已经排好序,答案很快就能浮出水面。

AND(P1,P2)={d∣d∈P1∧d∈P2},复杂度 O(n+m)\text{AND}(P_1, P_2) = \big\{ d \mid d \in P_1 \wedge d \in P_2 \big\},\quad \text{复杂度 } O(n+m)
有序倒排表的双指针扫描是查询处理的核心思想,几乎所有布尔运算都建立在这个机制上。
  1. AND 合并结果$\text{AND}(P_1, P_2)$ 表示求两个倒排表的交集。
    AND(P1,P2)={d∣d∈P1∧d∈P2}\text{AND}(P_1, P_2) = \big\{ d \mid d \in P_1 \wedge d \in P_2 \big\}
  2. 交集条件$d \in P_1 \wedge d \in P_2$ 表示文档 $d$ 必须同时出现在两条倒排表中。
    d∈P1∧d∈P2d \in P_1 \wedge d \in P_2
  3. 线性复杂度$O(n+m)$ 表示合并只需线性扫描两表各一遍。
    O(n+m)O(n+m)

索引构建更新

图书馆的藏书每天都在增加,老王的卡片系统需要持续维护。索引构建有两条路:静态构建与动态增量。

静态构建适用于文档集合基本不再变动时,比如建立历史档案。老王把所有书重新扫描一遍,抽取每个人的主题词,全部排序后写入磁盘。这是一次性的大工程,但完成后查询性能最优。动态增量则适用于持续更新的场景,比如新闻网站每秒钟都有新文章。

动态增量的一种经典架构是内存缓冲 + 分段合并:老王把今天新到图书的主题词先记在一个小本子上(内存临时索引),本子记满后,把整本内容追加写到一个新磁盘文件(新分段)。新分段一旦生成就不可变,以后所有新文档都写到更新的分段里。查询时分段越来越多,老王先查小本子和各个新分段,再合并结果。当分段数量积累到一定阈值,就在后台定期把几个小分段合成一个大分段——这个过程叫合并策略(Merge Policy)。

合并策略有两种取向。原地合并:直接读取旧的大分段和新的小分段,在原来文件基础上重写一个大文件,旧文件被覆盖。新段+LSM型(Log-Structured Merge):不修改旧分段,而是新写一个更大的分段,让查询去访问多个分段。LSM型更简单安全,但如果合并不及时,分段会越来越多,拖慢查询,因此需要一个像“水位线”一样的阈值机制控制合并时机。

老王的图书馆管理手册上写着几条经验:全量重新构建太慢,只适合深夜做;增量写入虽然快,但会产生碎片;合并分段要控制频率,太勤则磁盘IO压力大,太懒则查询会变慢。他最后采取“周末大扫除”策略——平时只把新卡片收进临时抽屉,到了周末深夜没人借书时才把所有临时抽屉合并进大目录。这正是 Lucene 等真实检索引擎中定期合并分段的工程缩影。

NewSegment=Sort(InMemoryBuf),当∣InMemoryBuf∣≥θ\text{NewSegment} = \text{Sort}\big( \text{InMemoryBuf} \big),\quad \text{当} \lvert \text{InMemoryBuf} \rvert \geq \theta
动态索引通过限制每次合并的规模来实现平滑的写入性能,牺牲少量读性能换取写吞吐量。
  1. 新分段生成$\text{NewSegment}$ 表示最终落盘的新索引分段。
    NewSegment=Sort(InMemoryBuf)\text{NewSegment} = \text{Sort}( \text{InMemoryBuf} )
  2. 排序操作$\text{Sort}$ 表示在内存中先按词项+文档ID排好序,保证分段内部有序。
    Sort(InMemoryBuf)\text{Sort}( \text{InMemoryBuf} )
  3. 触发条件$\lvert \text{InMemoryBuf} \rvert \geq \theta$ 表示内存缓冲达到阈值后触发落盘。
    ∣InMemoryBuf∣≥θ\lvert \text{InMemoryBuf} \rvert \geq \theta

压缩与性能

当藏书量从一万册涨到一亿册,老王发现最占空间的既不是书架,也不是卡片本身,而是卡片上那一串串文档ID。他想了个省纸的办法——差值编码:不写完整的架位号,只记录与上一条的差距。比如“3, 7, 12“写成”3, 4, 5”——原来的数字可能高达几百万,但差值只在几十几百的范围内。小数字可以用更少的字节存储,这就是差值编码省空间的原理。

压缩不光省钱,有时还能加速。可变字节编码(VarByte)是一种典型方案:每个整数用1到4个字节存储,每个字节的最高位设为“继续标记”——如果最高位是1,说明后面还有字节;如果为0,说明这是该数的最后一个字节。老王的小本子上,"12345"这个架位号实际占2个字节而不是4个,意味着磁盘上要读的数据更少,CPU 等待IO的时间随之减少,查询自然变快。

老王算了笔账:有了差值编码和可变字节编码,原来需要10页纸的卡片目录现在只用3页。更重要的是,读者查询时他需要翻开的页数也少了,平均每次取卡片的时间从5秒降到2秒。当然,编码不是免费的:每次读取都要解码,CPU 多承担一点计算。但现代 CPU 处理简单解码的速度远快于磁盘IO等待,这笔交易几乎总是划算的——老王的经验正对应着倒排索引压缩在工程中的实际收益。

VarByte(n)={b1,0≤n<27b1b2,27≤n<214b1b2b3,214≤n<221\text{VarByte}(n) = \begin{cases} b_1, & 0 \le n \lt 2^7 \\ b_1 b_2, & 2^7 \le n \lt 2^{14} \\ b_1 b_2 b_3, & 2^{14} \le n \lt 2^{21} \end{cases}
压缩减少磁盘占用与IO负担,是倒排索引工程优化的核心抓手,但会轻微增加 CPU 解码开销。
  1. 编码分档$\text{VarByte}(n)$ 表示对整数 $n$ 的编码结果。
    VarByte(n)={b1,…\text{VarByte}(n) = \begin{cases} b_1, & \dots \end{cases}
  2. 边界条件$0 \le n \lt 2^7$ 表示小于128的数只需1字节,$2^7 \le n \lt 2^{14}$ 表示小于16384的数用2字节。
    0≤n<27,27≤n<2140 \le n \lt 2^7, \quad 2^7 \le n \lt 2^{14}
  3. 连续字节$b_1 b_2 b_3$ 表示每个字节最高位作“是否延续”的标记,最后字节的该位置0。
    b1b2b3b_1 b_2 b_3

AI 应用场景

搜索引擎查询优化

阶段:检索增强生成(RAG)中的检索器构建

解决的问题:在RAG系统中,需要从海量知识库中快速定位与用户问题相关的文档片段。传统向量检索虽然能捕捉语义,但冷启动成本高且难以处理精确关键词匹配(如产品型号、法律条款)。倒排索引提供一种确定性、可解释的检索通道,与向量检索形成互补。

具体案例:某企业智能客服系统接入RAG后,用户询问“2024版服务协议中关于数据隐私的条款”。系统先用SSD固态盘上的倒排索引精确匹配“2024版”“服务协议”“数据隐私”等词项,锁定候选文档集合(通常几百篇),再对候选集做向量相似度排序,最终回答准确率从67%提升到92%。倒排索引负责“初筛”,向量检索负责“精排”,这种混合检索架构正在成为RAG系统的主流配置。

电商搜索的类目过滤

阶段:查询理解与结果粗排

解决的问题:电商平台用户常输入“iPhone 15 蓝色 256G”,期望系统能同时满足品牌+型号+颜色+内存四个维度。仅依赖向量检索容易混淆“蓝色”和“蓝色手机壳”等干扰项。倒排索引可以对商品属性建立多字段索引,用布尔查询快速过滤无关商品。

具体案例:某中等规模跨境电商(SKU数约500万)使用 Elasticsearch 的倒排索引,为品牌、颜色、内存、价格区间等字段各建一个索引字段。查询“iPhone 15 蓝色 256G”时,先分别对每个字段的倒排表做 AND 合并,4次线性扫描就能从500万商品中筛出127个完全匹配的商品,耗时不到20ms。相比之前用关系型数据库逐字段匹配耗时350ms,性能提升近18倍。

Python 演示

python73 行
import mathfrom collections import defaultdict class InvertedIndex:    def __init__(self):        # 词典: 词项 -> {文档ID: 词频}        self.dictionary = defaultdict(lambda: defaultdict(int))        # 文档ID -> 位置信息        self.doc_len = defaultdict(int)        # 文档总数        self.doc_count = 0     def add_document(self, doc_id, text):        # 简化分词: 按空格切分并转小写        tokens = text.lower().split()        self.doc_len[doc_id] = len(tokens)        for pos, token in enumerate(tokens):            self.dictionary[token][doc_id] += 1        self.doc_count += 1     def postings_list(self, term):        # 返回有序文档ID列表        if term not in self.dictionary:            return []        return sorted(self.dictionary[term].keys())     def tf_idf(self, term, doc_id):        # 计算term在doc中的TF-IDF权重        tf = self.dictionary[term][doc_id]        df = len(self.dictionary[term])        idf = math.log((self.doc_count + 1) / (df + 1)) + 1        return (1 + math.log(tf)) * idf     def and_query(self, terms):        # AND合并: 双指针扫描两个有序倒排表        result = self.postings_list(terms[0])        for term in terms[1:]:            other = self.postings_list(term)            merged = []            i = j = 0            while i < len(result) and j < len(other):                if result[i] == other[j]:                    merged.append(result[i])                    i += 1                    j += 1                elif result[i] < other[j]:                    i += 1                else:                    j += 1            result = merged            if not result:                break        return result     def rank_results(self, docs, terms):        # 按TF-IDF加权得分排序        scores = []        for doc_id in docs:            score = sum(self.tf_idf(t, doc_id) for t in terms if t in self.dictionary)            scores.append((doc_id, round(score, 4)))        scores.sort(key=lambda x: x[1], reverse=True)        return scores if __name__ == "__main__":    idx = InvertedIndex()    idx.add_document(1, "cat sits on the mat")    idx.add_document(2, "dog runs in the park")    idx.add_document(3, "cat and dog are friends")    idx.add_document(4, "bird flies over the tree")    print("词典结构:", {k: dict(v) for k, v in idx.dictionary.items()})    print("cat的倒排表:", idx.postings_list("cat"))    print("AND查询(cat, dog):", idx.and_query(["cat", "dog"]))    print("TF-IDF排序:", idx.rank_results([1, 3], ["cat", "dog"]))
目的:演示倒排索引的核心数据结构与查询处理——构建词典与倒排表,执行双指针AND合并,并用TF-IDF加权排序。
  1. self.dictionary = defaultdict(lambda: defaultdict(int))词典采用两层嵌套字典,外层词项、内层文档ID到词频的映射。
  2. self.dictionary[token][doc_id] += 1统计词频,正对应倒排表记录词频的设计。
  3. sorted(self.dictionary[term].keys())返回有序文档ID列表,有序性是后续合并的前提。
  4. idf = math.log((self.doc_count + 1) / (df + 1)) + 1用对数+拉普拉斯平滑计算IDF权重。
  5. while i < len(result) and j < len(other):双指针扫描两个有序倒排表。
  6. elif result[i] < other[j]: i += 1较小文档ID果断前进,这是AND合并的加速关键。
  7. score = sum(self.tf_idf(t, doc_id) ...)多词查询将各词TF-IDF加权求和,体现查询处理与权重计算的结合。

常见误区

误区我以为倒排索引就是一张很大的链表,词项按字母序挂成一串,查找时每次都得从第一个词开始一个个比较,直到找到目标词项。

正解倒排索引在逻辑上分成三个独立组件,其中 term 词典负责快速定位词项,通常采用哈希表或 B 树存储,根本不需要遍历全部词项。查找“猫”这个词时,用哈希函数直接算出它在存储中的位置,平均只需一次访问;用 B 树也只需沿着树根到叶子走 O(log n) 次节点比较,而不是 O(n) 次。倒排表本身则按文档ID有序存储,而不是按词项顺序串成链表。将词典与倒排表分开,正是为了让“找到词”和“取列表”两个操作都能独立优化。

为何常见:教材上画倒排索引时常用“词项A → 文档列表1 → 词项B → 文档列表2”的连环示意图,让人误以为所有词项和列表连成了一条大链表,忘记了词典是随机访问式的跳转结构。

误区索引压缩主要是为了省钱、省存储空间,如果硬盘足够大,完全可以不做压缩,速度反而更快。

正解压缩的核心收益往往是减少磁盘IO——从磁盘读入的数据量变小,内存能缓存更多倒排表,查询延迟显著下降。差值编码把大数变成小数,可变字节编码又用更少字节存小数,磁盘读入量常能减少 50%—80%。CPU 解码的额外开销通常远小于 IO 的节省,所以压缩后查询反而更快。这就好比快递员要把 100 个包裹从仓库送到车上,如果把每个包裹的体积压缩一半,他能少跑很多趟,卸货时多花两秒拆包装完全值得。

为何常见:直觉上“解压需要算力”让人觉得压缩只会拖慢速度,忽略了现代 CPU 的解码速度远快于磁盘/网络IO延迟这一关键事实。

误区倒排表里的文档ID只要不重复就行,按什么顺序存都无所谓,反正合并时都能扫一遍。

正解有序性是高效合并算法的绝对前提。如果倒排表中文档ID乱序,双指针算法将完全失效——你无法通过“当前数字较小就跳过”来规避无意义的比较,最坏情况需要把两条列表做笛卡尔积级别的两两比较。文档ID在分配时通常按文档创建时间递增赋号,这样自然保证有序性。倒排表的设计原则高度依赖这个顺序:跳表指针、差值编码、区间查询全部建立在有序序列之上。

为何常见:初学者容易从哈希表的无序特性联想过来,认为倒排表也是一个用于等值匹配的集合,忽略了倒排表的核心应用场景是区间归并而非单点查找。

面试题

Q1. 倒排索引中 term 词典的主要作用是什么?

单选题 · easy

  • A.存储文档的完整内容
  • B.快速定位词项对应的倒排表← 正确答案
  • C.对搜索结果进行排序
  • D.对文档进行分词处理

答案:B。词典是“抽屉标签”的角色,它负责将词项映射到倒排表的位置。A错——文档内容存在正排文件而非词典中;C错——排序是查询处理阶段的多文档排序组件;D错——分词是文档预处理阶段的事。B才是词典在检索链路中的核心定位。

Q2. 关于哈希表与 B 树实现词典的区别,说法正确的是?

单选题 · easy

  • A.哈希表支持范围查询
  • B.B 树查找时间复杂度为 O(1)
  • C.哈希表查找平均 O(1) 但内存开销更大← 正确答案
  • D.B 树不可能存储在海量磁盘上

答案:C。哈希表用空间换时间,查找快但不支持前缀/范围遍历且浪费空间;B 树按页组织节点适合磁盘,支持区间扫描。A、B、D 均与事实相反,故选 C。

Q3. 倒排表中文档ID按升序排列的主要价值是什么?

单选题 · mid

  • A.节省存储空间
  • B.支持双指针线性合并算法← 正确答案
  • C.方便做文档去重
  • D.有助于词项哈希计算

答案:B。有序序列使双指针归并算法成为可能——当两条列表有序时,可以在 O(n+m) 时间内求出交集并集,不需要两两比较。A说“节省空间”不够准确,压缩才省空间,有序是压缩的前提但本身不直接缩体积。C、D 与有序性无直接关系。

Q4. AND 合并中当两个指针指向的 docID 不同且 p1 < p2 时,正确做法是?

单选题 · mid

  • A.p2 指针右移一位
  • B.记录 p1 后移动 p1
  • C.p1 指针右移一位← 正确答案
  • D.两个指针同时右移

答案:C。p1 较小,因为列表有序,p1 不可能出现在另一个列表后续元素中,直接舍弃并前进 p1。这就是“果断抛弃”策略,它在保证正确性的同时最大化效率。A会漏算 p1,B 和 D 都会产生错误交集。

Q5. 差值编码(Delta Encoding)具体存储的是?

单选题 · mid

  • A.文档ID本身
  • B.相邻文档ID之间的差值← 正确答案
  • C.文档ID的哈希值
  • D.词项在文档中出现的次数

答案:B。差值编码用相邻ID的差替代原始ID。例如 [3,7,12] 存成 [3,4,5],用更小整数大幅降低存储字节数。C 是哈希表的做法,D 是词频(tf)的统计,两者都不是差值编码的本质。

Q6. 关于可变字节编码(VarByte),以下说法正确的有?

多选题 · mid

  • A.每个整数占用固定4字节
  • B.字节最高位用于标记是否有后续字节← 正确答案
  • C.146 在编码时需要2个字节
  • D.编码后读取时必须解码才能得到原值← 正确答案

答案:BD。VarByte 每个字节的最高位为延续标记(B对),146 大于 2^7 小于 2^14 所以需要2字节(C对)。A 说固定4字节恰恰相反——VarByte 的精髓就是可变长;D 虽属实但表述不够准确,更关键是“编码后读取时必须解码”确实是使用前提,但题目要求“关于编码说法正确的”,B和C 是直接特性,D 属于使用流程。综合最优答案为 B、C。

Q7. 动态增量索引中,LSM型分段合并策略的核心特点是什么?

单选题 · hard

  • A.修改原有旧分段文件来写入新文档
  • B.新分段文件一旦生成就不可修改← 正确答案
  • C.只能进行全量重建,无法增量
  • D.合并时直接覆盖原文件路径

答案:B。LSM(Log-Structured Merge)型策略会不断创建新分段,旧分段只读不写,通过后台定期合并降低分段数量。A、D 描述的是原地合并策略而非 LSM;C 与增量更新的前提矛盾。LSM 的不可变设计简化了并发控制并提升了写入吞吐。

Q8. 以下属于倒排索引中查询处理常见操作的有?

多选题 · hard

  • A.AND 双指针交集合并← 正确答案
  • B.OR 并集归并去重← 正确答案
  • C.NOT 差集求反运算← 正确答案
  • D.文档全编码压缩

答案:ABC。A、B、C 都是倒排表上的经典布尔运算,分别对应交集、并集、差集。D 属于索引压缩优化,是构建阶段的处理,不属于查询处理操作。题目问“查询处理常见操作”,故只选 A、B、C。

Q9. 在索引构建中,内存缓冲达到阈值后的正确处理是?

单选题 · hard

  • A.丢弃最旧的文档数据
  • B.在缓冲区上原地排序然后直接查
  • C.将缓冲区排序后写入新的不可变分段← 正确答案
  • D.将所有旧分段合并后覆盖写入

答案:C。动态索引的标准流程是内存缓冲先积累,达到阈值后排序输出为一个新分段,该分段不可变,后续再靠后台合并策略整理。A 丢数据显然错误;B 不落盘无法持久化;D 是合并策略的具体动作但发生在更晚的时机,不是缓冲区触发后的第一步。

Q10. 关于倒排索引压缩与查询性能的关系,正确的工程认识有?

多选题 · extreme

  • A.压缩后查询不一定变慢,甚至可能更快← 正确答案
  • B.压缩的主要收益之一是减少磁盘IO← 正确答案
  • C.压缩编码不增加任何CPU开销
  • D.压缩后内存能容纳更多倒排表缓存← 正确答案

答案:ABD。压缩减少磁盘读入量,IO变快,且更多倒排表可缓存在内存(D对),所以查询未必变慢(A对)。B 说明了压缩的关键收益机制。C 错误——解码一定消耗 CPU,正确的工程观点是 CPU 开销小于 IO 节省,才让“压缩提速”成立。