想象你是一家大型图书馆的管理员。图书馆里有一本巨大的《全书索引》——这就是所谓的静态索引。它按字母顺序记录着每一本书的位置,查找速度极快。但问题来了:每天都有新书入库、旧书下架、图书被读者归还后改变了摆放位置。如果每次变化都重新印刷这本大厚书,成本高得惊人;但如果不去更新,读者按照索引去找书,就会扑空。 聪明的你想到一个办法:保持这本大索引不变,同时在旁边放一叠便利贴。新书来了,先在便利贴上记录;旧书下架了,在便利贴上标注“已作废”。读者查书时,先查大索引,再翻便利贴,两者结合就能找到正确的书。等到便利贴积攒得太多、翻起来太麻烦时,就重新印刷一次大索引,把便利贴的内容全部合并进去,然后清空便利贴,开始新一轮循环。 这就是倒排索引动态更新的核心思想——双索引架构。搜索引擎每天要处理海量的文档增删改,如果每次更新都重建整个索引,系统会瞬间瘫痪;如果完全不更新,用户搜索到的全是过期信息。动态索引更新与增量合并,就是为了在“实时性”和“性能”之间找到最佳平衡。这套机制,是所有现代搜索引擎(如 Elasticsearch、Lucene)能够支撑亿级数据实时检索的基石。

岗位关联

掌握动态索引更新与增量合并,是搜索引擎从业者的必修课。它决定了你能否在数据持续涌入时,依然让用户获得秒级搜索体验——这是从“能搜”到“搜得好、搜得快”的关键一跃。

目录
  1. 可视化演示
  2. 静态索引的困境
  3. 双索引架构协作
  4. 段合并策略解析
  5. 删除位图与回收
  6. 合并触发机制
  7. 工程稳定性保障
  8. 实时查询的完整流程
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

演示双索引架构、段合并策略、以及删除位图管理机制。

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

双索引架构 内存索引 磁盘索引 写入→内存→刷盘 实时更新

静态索引的困境

图书馆的《全书索引》是一本精装大厚书,每一条记录都清楚地写着书名、作者和书架位置。对于读者来说,这本大厚书是“确定性”的——只要索引上写着什么,就一定能找到对应的书,查询速度极快。这种索引有一个专业名称:静态索引——它一旦建好,就不会轻易改变,因为改变的成本实在太高了。

但图书馆是活的。每天都有新书入库,旧书被送进仓库,甚至偶尔有图书被修补后换到另一个书架。如果每次变化都重新印刷这本大厚书,会发生什么?假设印刷需要整整一天,那么在印刷期间,图书馆的查询服务完全停摆。更糟糕的是,即使印刷完成了,一觉醒来又来了十本新书,一切又过时了。全量重建索引,代价高昂且永无止境。

在实际的搜索引擎中,全量重建索引意味着需要重新读取磁盘上所有文档,重新分词、排序、构建倒排链表。对于一个拥有亿级文档的搜索引擎来说,这个过程可能需要几个小时。在这几个小时里,新文档无法被搜索到,旧文档的删除也无法生效,用户看到的永远是“昨天”的数据。

明白了全量重建之痛,你就会理解为什么搜索引擎工程师们不愿频繁“重印大厚书”。但实时更新又是刚需——用户刚发布了一篇博客,立刻就想在搜索引擎里搜到它。这个看似矛盾的需求,催生了下一节的解决方案:双索引架构。

Trebuild=∑d∈D(tread(d)+tparse(d)+tindex(d))T_{\text{rebuild}} = \sum_{d \in \mathcal{D}} \left( t_{\text{read}}(d) + t_{\text{parse}}(d) + t_{\text{index}}(d) \right)
公式解读:全量重建时间等于每个文档的读取、解析、索引三项开销之和。文档数量越大,重建时间越长——这就是“静态索引之痛”。
  1. 求和符号的含义将集合 \(\mathcal{D}\) 中所有文档的开销逐一累加
    ∑d∈D\sum_{d \in \mathcal{D}}
  2. 读取开销从磁盘把文档数据读入内存的时间
    tread(d)t_{\text{read}}(d)
  3. 解析开销对文档进行分词、去停用词等预处理
    tparse(d)t_{\text{parse}}(d)
  4. 索引开销将解析结果写入倒排索引结构
    tindex(d)t_{\text{index}}(d)

双索引架构协作

既然重印大厚书不可行,聪明的图书馆管理员决定:保留大厚书作为主索引,再加一叠便利贴作为辅助索引。大厚书继续稳定服务绝大多数查询,便利贴则负责记录所有新增、修改、删除的信息。读者每次查书时,先查大厚书,再翻便利贴,把两处的结果合并起来——这就是双索引架构(Dual Index Architecture)。

这套架构的关键在于分工明确。主索引(Main Index)是一份长期稳定、已经优化好的索引结构,它只接受定期的“批量合并”,平时绝不轻易变动。辅助索引(Auxiliary Index)则是一个轻量级的临时索引,存放自上次合并以来所有新增或修改的文档。当用户发起查询时,搜索引擎需要同时搜索主索引和辅助索引,然后将两部分结果按相关度合并、去重,再返回给用户。

双索引架构的另一个精妙之处在于内存缓冲。新文档不会直接写入磁盘上的索引文件,而是先进入内存中的一个缓冲区。当缓冲区积累到一定规模后,一次性批量写入辅助索引。这样做的好处是显而易见的:批量写入远比逐条写入高效,而且可以减少磁盘的随机 I/O 次数。这就像管理员先把便利贴攒成一叠,再一次性贴到公告板上,而不是每来一本书就跑去贴一次。

双索引架构的精髓在于“变化隔离”——让稳定的继续稳定,让变化的快速变化。主索引的查询效率不会因为频繁更新而下降,辅助索引的写入也足够轻量。但便利贴越积越多,查询时需要翻越的“纸张”也越来越多,性能不可避免地开始下降。这时候就需要一项专门的机制来“清理桌面”,这就是下一节要讲的段合并策略。

Indexquery(q)=Searchmain(q)∪Searchaux(q)\text{Index}_{\text{query}}(q) = \text{Search}_{\text{main}}(q) \cup \text{Search}_{\text{aux}}(q)
公式解读:一次完整的查询 = 主索引的搜索结果 + 辅助索引的搜索结果。两部分做并集操作,确保主索引中“旧数据”和辅助索引中“新数据”都不遗漏。
  1. 查询请求用户提交的关键词 \(q\)
    qq
  2. 主索引搜索在大厚书中查找关键词对应的文档集合
    Searchmain(q)\text{Search}_{\text{main}}(q)
  3. 辅助索引搜索在便利贴中查找关键词对应的文档集合
    Searchaux(q)\text{Search}_{\text{aux}}(q)
  4. 合并结果将两路结果取并集,确保完整性
    ∪\cup

段合并策略解析

便利贴用久了,会积成一摞。初期翻翻还挺快,但攒到几百张时,找一张特定的便签变得费时费力。解决方案是什么?把便利贴的内容合并进大厚书,然后将便利贴清空——这个过程在搜索引擎中称为段合并(Segment Merge)。

要理解段合并,首先得理解“段”的概念。在 Lucene 等搜索引擎中,索引并不是一个单一的大文件,而是由多个段(Segment)组成的集合。每个段本身就是一个完整的小型倒排索引,包含了一部分文档的索引信息。段是索引的最小独立单元:它可以独立地被搜索,也可以独立地被合并。

段合并策略遵循一个朴素的思路:小段合并成大段,大段合并成更大的段。新文档不断产生新段,这些段越来越小、越来越多。查询时,系统需要依次搜索每一个段并汇总结果——段越多,查询开销越大。合并就是将多个小段读入内存,重新排序、去重、构建成一个更紧凑的大段,然后删除原来的小段。合并完成后,索引的文件数大幅减少,查询变得更快。

段合并的核心是减少文件句柄占用。每个段对应磁盘上的一个文件,而操作系统对进程打开的文件句柄数量是有限制的。如果段的数量过多,文件句柄会耗尽,导致索引无法继续写入。合并不仅提升了查询性能,还是一种必要的基础资源管理手段。

段合并是典型的“空间换时间”策略——用合并时的一次性开销,换取后续查询时的持续性能提升。但合并还有一个容易被忽视的重要任务:真正删除那些已被标记为废弃的文档。这时候就需要“删除位图”出场了。

Costmerge=∑s∈Ssmall(IO(s)+Sort(s)+Write(s))\text{Cost}_{\text{merge}} = \sum_{s \in S_{\text{small}}} \left( \text{IO}(s) + \text{Sort}(s) + \text{Write}(s) \right)
公式解读:合并开销等于每个参与合并的小段的读入、排序、写出开销之和。合并虽贵,但能换来更少的段数量和更快的查询速度。
  1. 待合并的段集合所有参与本次合并的小段
    SsmallS_{\text{small}}
  2. 读入开销将段从磁盘读入内存
    IO(s)\text{IO}(s)
  3. 排序开销对段内的词项进行归并排序
    Sort(s)\text{Sort}(s)
  4. 写出开销将合并后的结果写为新的大段
    Write(s)\text{Write}(s)

删除位图与回收

图书馆的便利贴上写着“这本书已经下架了”。但“这本书”其实还安静地躺在书架上,只是不再对读者开放。为什么管理员不直接把它扔掉?因为下架可能只是暂时的,说不定过几天又恢复上架了;更重要的是,直接从大厚书中划掉一条记录成本太高,而标记一下几乎不花任何成本。

这个思路对应到搜索引擎,就是标记删除(Mark-and-Sweep)策略。当用户删除一篇文档时,索引并不立即将该文档从倒排链表中移除,而是在一个删除位图(Deletion Bitmap)上将该文档对应的位置标记为“已删除”。查询时,系统检索到这条记录后会检查删除位图,如果发现被标记,就跳过它。

删除位图本质上是一个位数组,每一位对应一个文档 ID,1 表示已删除,0 表示未删除。它的优势在于极小的存储开销和 O(1) 的检查复杂度。一个拥有百万级文档的索引,删除位图只需几十万字节,查询时的检查也只是一个简单的位操作,几乎不产生任何性能损失。

那么,被标记删除的文档什么时候被物理清理?答案就是段合并时。合并的过程中,参与合并的段会被完整地读入内存,这时候检查删除位图,将被标记的文档从合并结果中直接剔除。这样一来,合并后的新段中根本不包含已删除的文档,磁盘空间也随之释放。删除位图 + 段合并,构成了“标记-清理-回收”的完整闭环。

删除位图解决了“如何知道哪些文档已删除”的问题,段合并解决了“何时真正回收空间”的问题。但还有一个工程问题悬而未决:什么时候触发合并? 这个问题看似简单,却在很大程度上决定了整个系统的稳定性。

Spacesaved=∑d∈BSize(d)\text{Space}_{\text{saved}} = \sum_{d \in \mathcal{B}} \text{Size}(d)
公式解读:回收的空间等于删除位图中所有被标记文档大小之和。合并时一次性回收,避免频繁的小规模空间释放。
  1. 删除位图中的集合所有被标记为已删除的文档
    B\mathcal{B}
  2. 单个文档占用的空间该文档在索引中所占的存储大小
    Size(d)\text{Size}(d)
  3. 加总所有待回收空间所有被删文档的空间累加
    ∑d∈B\sum_{d \in \mathcal{B}}

合并触发机制

便利贴积累到多少张才值得重新印刷大厚书?如果积累 10 张就重印,那和每来一本书就重印没有区别,成本太高;如果积累 1000 张才重印,查询性能早已严重下降,用户体验受损。寻找合适的触发时机,是动态索引系统的核心工程问题之一。

常见的触发机制有三种。第一种是阈值触发:当辅助索引的文档数量或占用空间超过预设阈值时,触发一次合并。例如,当辅助索引中的文档数达到主索引的 5% 时,就执行合并。这种方式的优点是实现简单、效果可控,但缺点是在流量高峰时,阈值可能很快被突破,导致合并频繁发生。

第二种是定期合并:每隔固定时间间隔(如每分钟、每小时)执行一次合并。这种方式的优点是系统行为可预测,运维人员可以预估合并开销;缺点是如果流量波动大,定期合并可能“不赶趟”——两个合并周期之间,辅助索引可能已经积累了大量文档。

第三种是自适应调控:系统根据当前查询负载、辅助索引增长速度、合并线程占用率等指标,动态调整合并频率。例如,在查询低谷期加大合并力度,在高峰期推迟甚至暂停合并。这是多数生产级搜索引擎采用的方案,但实现复杂度也最高。

触发机制就像图书馆管理员定下的规则:便利贴满 100 张就重印,或者每周固定重印一次。有了规则还不够,还需要考虑一个更实际的问题——合并时系统还要不要正常服务?

Mergetrigger={Trueif NauxNmain>θTrueif Δt≥TintervalFalseotherwise\text{Merge}_{\text{trigger}} = \begin{cases} \text{True} & \text{if } \dfrac{N_{\text{aux}}}{N_{\text{main}}} > \theta \\ \text{True} & \text{if } \Delta t \geq T_{\text{interval}} \\ \text{False} & \text{otherwise} \end{cases}
公式解读:当辅助索引与主索引的文档数量比超过阈值 \(\theta\),或者距上次合并超过时间间隔 \(T_{\text{interval}}\),触发合并。
  1. 条件判断的第一种情况辅助索引规模占比超过阈值
    NauxNmain>θ\frac{N_{\text{aux}}}{N_{\text{main}}} > \theta
  2. 阈值的作用\(\theta\) 越小,合并越频繁;越大,合并越少
    θ\theta
  3. 条件判断的第二种情况距离上次合并超过预设间隔
    Δt≥Tinterval\Delta t \geq T_{\text{interval}}
  4. 条件判断的默认情况两者都未达到,不触发合并
    False\text{False}

工程稳定性保障

想象一下:管理员正在把几百张便利贴的内容逐一抄写进大厚书,这时有读者来问询。如果管理员只顾着抄写,读者可能要等很久;如果管理员先服务读者,抄写工作又会变慢。合并操作和查询操作同时竞争 CPU、磁盘 I/O 和内存资源,如何平衡?

答案是合并限流(Merge Throttling)。搜索引擎在合并时不会“全力冲刺”,而是将合并线程的资源占用控制在预设范围内。例如,将合并线程的最大 I/O 带宽限制为系统总带宽的 20%,或者将合并线程数限制为 2 个。这样,即使在合并期间,查询请求的响应时间也不会出现剧烈波动。Elasticsearch 就提供了 indices.store.throttle.type 和 indices.store.throttle.max_bytes_per_sec 等配置项来控制合并限流。

另一个重要手段是冷热数据分级存储。热数据(最近写入、频繁访问的文档)所在的段常驻内存或高性能 SSD,冷数据(旧文档、访问频率低的文档)则存放在普通磁盘。合并时优先合并热段,冷段的合并频率降低。这种分级策略既保证了热点数据的查询性能,又降低了合并的整体开销。

故障恢复也不容忽视。合并进行到一半,系统崩溃了怎么办?如果直接丢弃未完成的新段,老段已经被删除,数据岂不丢失?生产系统通常采用“先写新段,再删旧段”的策略:合并先构建完整的新段并落盘,确认无误后再删除被合并的旧段。这样,即使合并中途崩溃,旧段依然完好,系统可以回滚到合并前的状态。

合并限流、冷热分级、故障恢复,这些措施共同保障了系统的稳定性。别忘了,在这一切机制的背后,还有一个始终贯穿的思想——增量更新是常态,全量重建是例外。图书馆管理员不会每天重印大厚书,但会每隔一段时间做一次彻底的整理。这份从容,正是双索引架构带来的力量。

Queryp99≈Querybaseline+IOmergeBWthrottle⋅α\text{Query}_{\text{p99}} \approx \text{Query}_{\text{baseline}} + \frac{\text{IO}_{\text{merge}}}{\text{BW}_{\text{throttle}}} \cdot \alpha
公式解读:查询延迟的 p99 近似等于基线延迟加上合并 I/O 与限流带宽之比乘以系数 \(\alpha\)。限流越严格,合并对查询的影响越小。
  1. 查询延迟的基线水平没有合并发生时,搜索引擎的常规查询延迟
    Querybaseline\text{Query}_{\text{baseline}}
  2. 合并产生的额外 I/O 开销合并操作消耗的磁盘带宽
    IOmerge\text{IO}_{\text{merge}}
  3. 限流带宽上限允许合并操作使用的最大带宽
    BWthrottle\text{BW}_{\text{throttle}}
  4. 影响系数不同存储层级和查询模式下的调节因子
    α\alpha

实时查询的完整流程

现在,让我们跟随一篇新发布的博客,走完它在搜索引擎中的完整旅程。这篇博客被用户点击发布后,它的文本内容首先要经过分词、去停用词等预处理,提取出关键词——这个过程叫文档解析。解析完成后,文档并不会立即出现在搜索结果中,而是先进入内存缓冲区。

缓冲区的文档积累到一定数量后,会被批量写入辅助索引。辅助索引收到这批文档,为每个关键词建立倒排链表,将其追加到辅助索引已有的倒排结构上。此刻,这篇博客已经可以被搜索到了——用户搜索博客中的某个关键词时,查询会同时命中主索引和辅助索引。

当辅助索引的规模超过了预设阈值,合并进程被唤醒,将辅助索引的所有文档与主索引中相关的段进行归并。归并的结果是一个新的大段,它既包含了老的文档,也包含了新发布的博客。老段被删除,新段上线服务。此时,这篇博客正式“晋升”为老数据,由主索引全权负责。

回顾整个流程,你会发现“实时性”和“稳定性”之间是一场精妙的舞蹈。缓冲是为了批量写入的效率,辅助索引是为了提供近实时的搜索能力,合并是为了保持长久的查询性能,而限流和故障恢复则是保证这场舞蹈不会在压力下崩溃。这套机制环环相扣,构成了搜索引擎动态索引的完整蓝图。

Reqprocess=Buffer→Aux→Tierhot→Merge→Main\text{Req}_{\text{process}} = \text{Buffer} \to \text{Aux} \to \text{Tier}_{\text{hot}} \to \text{Merge} \to \text{Main}
公式解读:一篇文档的索引生命周期:先入缓冲区,再进辅助索引,随热度升级为热数据,最终通过合并进入主索引。
  1. 文档进入缓冲区新文档先落在内存缓冲区等待批量处理
    Buffer\text{Buffer}
  2. 写入辅助索引缓冲区满后一次性写入辅助索引,并立即可搜索
    →Aux\to \text{Aux}
  3. 升级为热数据高访问频率的文档段进入热存储层级
    →Tierhot\to \text{Tier}_{\text{hot}}
  4. 合并入主索引触发合并机制,将辅助段归并进主索引
    →Merge→Main\to \text{Merge} \to \text{Main}

AI 应用场景

电商商品检索的实时更新

阶段:数据索引与实时检索

解决的问题:电商平台每秒都有新商品上架、老商品下架、价格修改。若使用静态索引,商家上新后消费者几分钟甚至几小时内都搜不到新商品;若每次更新都重建索引,高峰期系统会拥塞崩溃。动态索引的高效更新写入和渐进合并,让新商品在秒级时间内对消费者可见,同时合并限流保证搜索服务的稳定。

具体案例:某头部电商平台的大促活动中,使用了“内存缓冲 + 辅助索引 + 分段合并”的方案。上新商品先写入内存缓冲区,达到 1 万条后批量刷入辅助索引,消费者立即可检索。每 5 分钟触发一次段合并,将辅助索引归并进主索引,既保证了实时性,又控制住了合并对查询延迟的影响。

企业知识库的文档管理

阶段:文档增量索引

解决的问题:企业内部知识库每天有大量新文档上传、旧文档更新。传统方案需要全量重建索引,耗时数小时且严重影响使用体验。动态索引方案通过删除位图标记过期内容,通过段合并回收存储空间,实现了文档的持续可搜索性。

具体案例:字节跳动的内部知识库使用基于 Lucene 的动态索引方案。文档更新时,新版本文档被索引到辅助索引,旧版本文档在删除位图上被标记。定时合并时,旧版本被物理清除,索引空间得到释放。

新闻资讯平台的实时搜索

阶段:实时数据接入与检索

解决的问题:新闻资讯平台要求新闻发布后立即可以被搜索到。动态索引的辅助索引保证了新文档的即时可见性,而主索引的稳定结构保证了高并发查询下的低延迟。

具体案例:今日头条的资讯搜索系统,新稿件经 10 秒 NRT(近实时)流程即可被搜索到。系统每天处理的段合并超过 5000 次,通过自适应合并调控策略将在高峰期合并线程数调整为 0,确保突发流量下搜索服务不受影响。

Python 演示

python75 行
import mathfrom collections import defaultdict class DynamicIndex:    def __init__(self, merge_threshold=5):        self.main_index = defaultdict(list)  # 主索引        self.aux_index = defaultdict(list)   # 辅助索引        self.deleted = set()                # 删除位图(模拟)        self.doc_count = 0        self.merge_threshold = merge_threshold        self.gc_count = 0     def index_doc(self, doc_id, tokens):        """索引一篇文档,先写入辅助索引"""        for term in set(tokens):            self.aux_index[term].append(doc_id)        self.doc_count += 1        # 检查是否触发合并        if self.doc_count % self.merge_threshold == 0:            self.merge()     def delete_doc(self, doc_id):        """标记删除文档"""        self.deleted.add(doc_id)     def search(self, query_term):        """查询:合并主索引与辅助索引的结果"""        results = set(self.main_index.get(query_term, []))        results.update(self.aux_index.get(query_term, []))        # 过滤已删除的文档        return [doc_id for doc_id in results if doc_id not in self.deleted]     def merge(self):        """将辅助索引合并进主索引"""        for term, doc_ids in self.aux_index.items():            old_docs = self.main_index[term]            merged = list(set(old_docs + doc_ids))            self.main_index[term] = merged        # 合并完成后清空辅助索引        self.aux_index.clear()        self.gc_count += 1        # 模拟物理回收已删除文档        self.main_index = {            term: [doc_id for doc_id in doc_ids if doc_id not in self.deleted]            for term, doc_ids in self.main_index.items()        }        self.deleted.clear()     def stats(self):        main_docs = len({doc_id for doc_ids in self.main_index.values() for doc_id in doc_ids})        aux_docs = len({doc_id for doc_ids in self.aux_index.values() for doc_id in doc_ids})        return {            "main_terms": len(self.main_index),            "aux_terms": len(self.aux_index),            "main_docs": main_docs,            "aux_docs": aux_docs,            "merge_count": self.gc_count,        } if __name__ == "__main__":    index = DynamicIndex(merge_threshold=3)    # 模拟新增文档    index.index_doc(1, ["人工智能", "机器学习"])    index.index_doc(2, ["大数据", "人工智能"])    index.index_doc(3, ["机器学习", "深度学习"])    print("初次索引完成:", index.stats())    # 模拟查询    print("搜索'人工智能':", index.search("人工智能"))    # 删除一篇文档    index.delete_doc(2)    print("删除文档2后搜索'人工智能':", index.search("人工智能"))    # 继续添加文档触发合并    index.index_doc(4, ["人工智能", "自然语言处理"])    index.index_doc(5, ["搜索引擎", "机器学习"])    print("触发合并后:", index.stats())
目的:演示动态索引的核心机制——新文档先入辅助索引、查询合并两路结果、达到阈值触发合并。
  1. self.aux_index[term].append(doc_id)新文档的每个词项都写入辅助索引,这就是“先记在便利贴上”
  2. if self.doc_count % self.merge_threshold == 0: self.merge()每索引 3 篇文档就触发一次合并,模拟阈值触发机制
  3. results.update(self.aux_index.get(query_term, []))查询时把主索引和辅助索引的结果合并,确保新文档也能被搜到
  4. if doc_id not in self.deleted过滤掉删除位图上标记的文档,实现“标记删除”
  5. self.aux_index.clear()合并完成后清空辅助索引,开始新一轮的积累

常见误区

误区既然合并能减少段数量、提升查询性能,那合并得越频繁越好,最好每来一个文档就合并一次。

正解合并本身是一项开销很大的操作——它需要读取大量段文件、在内存中排序、再写回磁盘。如果合并过于频繁,系统的大量资源会消耗在合并上,查询性能反而会大幅下降。就像便利贴刚贴上一张就重新印书,那还不如一开始就用大厚书记录每一条变化。合理的策略是“攒一批、合并一次”,用合并频率去换系统的整体吞吐。

为何常见:初学者往往只看到了合并带来的收益(段变少、查询变快),却忽略了合并本身的成本。这就像只看到锻炼身体的好处,却没有意识到过度训练也会造成损伤。

误区调用删除接口后,索引的磁盘占用应该马上减少,和文档被彻底移除一样。

正解动态索引中的删除是“标记删除”——先在删除位图上将文档 ID 标记为 1,查询时跳过该文档。物理空间真正释放要等到下次段合并时,被标记的文档才会从合并结果中被剔除。这意味着删除后磁盘空间不会立即释放,这是设计上的取舍:为了删除操作的实时性,接受空间回收的延迟。

为何常见:大家对“删除”的直觉来自文件系统,删除文件后磁盘空间立即可用。而索引系统为了追求删除操作的 O(1) 复杂度,牺牲了空间回收的即时性。

误区辅助索引能提供实时搜索能力,所以让辅助索引无限增长,主索引进不进行合并都行。

正解辅助索引越大,查询时需要扫描的额外数据就越多,查询性能下降越明显。查询本来只需要搜索一个主索引,现在还必须同时搜索辅助索引并合并结果,辅助索引的规模直接影响到每次查询的额外开销。工程上,辅助索引的合理阈值通常是主索引的 5%-10%,超过这个比例就应该触发合并。

为何常见:只关注了“能搜到新文档”的好处,忽略了“所有查询都要多搜一份数据”的代价。这是只看到局部优化、忽略全局成本的典型思维误区。

误区合并是在后台执行的,和查询互不影响,用户根本感知不到。

正解合并和查询在底层资源上有激烈竞争——两者都要用 CPU、都要读磁盘、都要占内存。如果不做任何限流,一次大规模合并可能让查询延迟飙升数倍,甚至拖垮整个系统。生产环境必须对合并进行限流(如限制合并线程数、限制 I/O 带宽),让合并“温柔地”进行,才不至于影响在线服务的稳定性。

为何常见:初学者将“后台运行”误解为“不影响运行”,而忽略了资源竞争的本质。就像后台下载电影时,你依然会感觉到网页加载变慢了。

面试题

Q1. 双索引架构中,辅助索引的主要作用是什么?

单选题 · easy

  • A.备份主索引的数据
  • B.接收增量文档并提供近实时搜索← 正确答案
  • C.替代主索引执行全部查询
  • D.压缩主索引的存储空间

答案:B。辅助索引的设计目的是接收新写入的文档,让这些文档能立即被搜索到,实现近实时搜索。它不是备份(A错)、不替代主索引(C错)、不压缩存储(D错)。就像便利贴是记录新增图书的临时位置,而不是替代大厚书。

Q2. 删除位图的核心思想是什么?

单选题 · easy

  • A.直接删除文档对应的索引项
  • B.重建整个索引以移除文档
  • C.标记文档为已删除,查询时跳过← 正确答案
  • D.压缩文档内容以减少占用

答案:C。删除位图采取“标记删除”策略——为每个文档 ID 在位上标记 1 表示已删除,查询时跳过这些文档。不需要立即物理删除(A错),不需要重建索引(B错),压缩内容也与删除无关(D错)。就像在便利贴上标注“下架”,但书还留在书架上。

Q3. 段合并操作中,被合并的小段将经历什么?

单选题 · easy

  • A.永久保留在原位置
  • B.被合并成一个大段后删除← 正确答案
  • C.被移动到冷存储中
  • D.被压缩成更小的段

答案:B。段合并将多个小段读入内存、去重排序后写出成一个新的大段,再删除原来的小段。小段不会永久保留(A错)、不会转到冷存储(C错)、更不会被压成更小的段(D错)。这就像把多张便利贴的内容抄进大厚书,然后撕掉便利贴。

Q4. 辅助索引触发合并的常见条件是什么?

单选题 · mid

  • A.主索引空闲时
  • B.辅助索引规模超过阈值或超过时间间隔← 正确答案
  • C.用户发起查询请求时
  • D.系统发生故障后

答案:B。合并通常在两种条件下触发:辅助索引的文档数与主索引的比例超过预设阈值,或距上次合并的时间超过设定间隔。靠主索引是否空闲(A错)、靠用户查询触发(C错)、靠故障触发(D错)都不是工程实践中的主流方案。

Q5. 为什么合并前要先写新段、再删旧段?

单选题 · mid

  • A.因为新段占用空间更小
  • B.为了避免合并中途崩溃导致数据丢失← 正确答案
  • C.因为旧段已经不再需要了
  • D.因为系统只支持顺序写

答案:B。“先写新、再删旧”的策略是为了故障恢复——如果合并进行到一半系统崩溃,新段可能未写完,但旧段依然完好,系统可以回滚到合并前状态。若先删旧段,崩溃会导致数据永久丢失。这就像先抄好新书再撕旧笔记,而不是先把笔记撕了再抄。

Q6. 合并限流的主要目的是什么?

单选题 · mid

  • A.减少合并操作的总开销
  • B.控制合并对查询服务的影响← 正确答案
  • C.防止合并过程中出现数据丢失
  • D.提高合并操作的执行速度

答案:B。合并限流限制合并操作占用的 I/O 带宽和 CPU 资源,核心目的是避免合并与查询竞争资源,保证查询延迟稳定。限流不是减少合并总开销(A错),不是防止数据丢失(C错),更不能提高合并速度(D错)。

Q7. 关于双索引架构,以下哪些说法是正确的?

多选题 · mid

  • A.主索引负责服务大部分查询请求← 正确答案
  • B.辅助索引接收文档增量的写入← 正确答案
  • C.查询时需要将两个索引的结果合并← 正确答案
  • D.两个索引之间不需要任何协调机制

答案:ABC。双索引架构中,主索引承担主要查询服务(A对),辅助索引负责接收增量文档(B对),查询时合并两路结果(C对)。两索引之间需要协调——合并、删除位图同步等都是协调机制(D错)。就像大厚书和便利贴需要配合使用,缺一不可。

Q8. 以下哪些方式可以控制合并对系统稳定性的影响?

多选题 · mid

  • A.限制合并线程的数量← 正确答案
  • B.限制合并使用的 I/O 带宽← 正确答案
  • C.在查询高峰期暂停合并← 正确答案
  • D.让合并与查询同时全力运行

答案:ABC。限制合并线程数(A对)、限制 I/O 带宽(B对)、在高峰期暂停合并(C对)都是常用的稳定性控制手段。让合并与查询同时全力运行(D错)必然导致资源竞争和性能抖动,是不可取的。

Q9. 删除位图中的文档 ID 何时被物理清除?

单选题 · hard

  • A.删除操作执行后立即
  • B.删除位图被清空时
  • C.文档所在的段参与合并时← 正确答案
  • D.主索引重新构建时

答案:C。被删除位图标记的文档,在包含它的段参与合并时才会被物理清除——合并过程中读取段内容,遇到被标记的文档直接跳过,从而在合并结果中剔除。删除操作本身只是位图标记(A错),位图清空不代表文档被清除(B错),主索引重建不是常规操作(D错)。

Q10. 关于段合并的开销,以下哪些说法符合实际?

多选题 · hard

  • A.合并需要将段文件读入内存← 正确答案
  • B.合并过程中需要对词项进行排序← 正确答案
  • C.合并后的新段需写回磁盘← 正确答案
  • D.合并的开销与段大小无关

答案:ABC。段合并的流程是:读入小段(A对)、内存中归并排序(B对)、写出新的大段(C对)。合并开销与参与合并的段大小直接相关——段越大,读入、排序、写出的时间都越长(D错)。这就像整理便利贴,张数越多,粘贴工作越耗时。