想象你走进一座藏书百万的图书馆。每天都有成百上千本新书被送进来,而读者希望“今天上架的新书,今天就能在目录里查到”。如果每次来一批书,就把整个图书馆重新编目一遍,馆员累垮不说,读者也没法正常用目录了。于是馆长定下规矩:新书先放在“待登记车”,每十五分钟统一登记一次;零散登记卡片定期装订成正式目录册;更换新版目录时先把新目录全部做好,再挂一块“请用新版目录”的牌子。 这座图书馆的运营智慧,正是搜索引擎索引构建与更新的精髓。本篇文章用这个比喻贯穿始终,带你理解倒排索引结构、全量重建与增量更新、近实时可见、段合并策略、版本无感切换、读写一致性和生产运维实践——这些听起来吓人的概念,其实都是图书馆里每天都在发生的小事。

岗位关联

掌握索引构建与更新,你就能设计“写入不丢、查询不抖、更新无感”的搜索系统。这是 AI 应用落地的核心工程能力,也是从熟练调用 Elasticsearch API 到能独当一面设计检索架构的分水岭。

目录
  1. 可视化演示
  2. 倒排索引结构
  3. 全量构建流程
  4. 增量更新机制
  5. 近实时可见
  6. 段合并策略
  7. 版本无感切换
  8. 读写一致性保障
  9. 生产运维实践
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

模拟搜索引擎Near Real-Time索引构建与段合并过程,展示版本切换的无感更新,观察高写入吞吐下的检索一致性。

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

模式1:Near Real-Time索引 文档写入缓冲 刷新生成段 visible = refresh(buf) 缓冲刷新后文档近乎实时可检索。

倒排索引结构

图书馆的目录柜里,每一格抽屉都贴着一个词条标签:“机器”、“学习”、“食谱”……拉开“机器学习”的抽屉,里面是一张张卡片,每张卡片写着一本书的编号和这个词在书里出现的次数。读者想找相关书籍,只需翻抽屉、抽卡片,而不是抱着一摞书从头翻到尾。这就是目录柜的秘密:用“词”找“书”。

搜索引擎的核心数据结构也叫“倒排索引”——它反着来:不是“书里有什么词”,而是“这个词在哪些书里”。建索引时,一篇文档先被分词器切成一连串词项,再为每个词项登记它所属的文档编号。查询“机器学习 排名”,就分别取出两个词项的文档列表,再求交集,几毫秒内返回结果。

倒排索引主要由词项字典和倒排列表两大部分构成。字典保存所有出现过的词项,支持快速查找;倒排列表则是该词项对应的文档号序列,通常还附带词频(该词在文中出现次数),用来计算相关性。整个结构可以写成:

PostingList(t)=[(d1,tf1),(d2,tf2),…,(dk,tfk)]\displaystyle \text{PostingList}(t) = [ (d_1, \text{tf}_1), (d_2, \text{tf}_2), \ldots, (d_k, \text{tf}_k) ]

1. 词项 t:被索引的词,是查询的基本单位。

t∈Dictionary\displaystyle t \in \text{Dictionary}

2. 文档号 d₁:包含该词的第一篇文档编号,用于定位原文。

di∈DocSet(t)\displaystyle d_i \in \text{DocSet}(t)

3. 词频 tfᵢ:该词在文档 dᵢ 中出现的次数,衡量词对文档的代表性。

tfi=count(t,di)\displaystyle \text{tf}_i = \text{count}(t, d_i)

4. 整体映射:字典中的每个词项都对应这样一个倒排列表。

Index={ t:PostingList(t) }\displaystyle \text{Index} = \{\, t : \text{PostingList}(t) \,\}

图书馆目录柜做得越整齐,读者找书越快;倒排索引压缩得越紧凑、字典查找越高效,搜索引擎检索也就越快。理解了这张“词到书单”的映射表,后面所有构建与更新的技巧,都是在围绕它做文章。

全量构建流程

新图书馆落成那天,馆长面临一个庞杂任务:把馆里所有书逐本登记、编目、贴标签,做出一整套完整的目录柜。这是一次“从零开始”的总编目——耗时耗力,但做完之后,整个馆的图书脉络一清二楚。

搜索引擎的世界里,这套流程叫全量重建:从所有原始文档出发,重新分词、构建倒排表、写回磁盘,最终生成一份完整的基线索引。它通常在系统首次上线、索引格式升级或数据严重损坏时执行。全量构建以离线批处理为主,把文档切成多个分片并行处理,最后归并成一个统一索引;所有步骤都不影响线上查询,因为它生成的是全新的文件。

全量构建的耗时由文档总量、并行度和单机吞吐共同决定。假设共有 N 篇文档、P 个并行 worker 同时处理、每个 worker 每秒钟能处理 S 篇文档,那么构建时长大致是:

Tbuild=NP×S\displaystyle T_{\text{build}} = \frac{N}{P \times S}

1. 文档总数 N:本次构建要处理的全部文档数量,是总工作量。

N=∣AllDocs∣\displaystyle N = |\text{AllDocs}|

2. 并行度 P:同时参与构建的 worker 数,增大 P 能摊薄时间。

P=#workers\displaystyle P = \#\text{workers}

3. 单机吞吐 S:单个 worker 每秒处理的文档数,受 CPU 与磁盘速度限制。

S=docssecond\displaystyle S = \frac{\text{docs}}{\text{second}}

4. 整体耗时 T_build:总工作量除以总吞吐,得到预估构建时长。

Tbuild=NP×S\displaystyle T_{\text{build}} = \frac{N}{P \times S}

全量重建好比搬家后把全屋重新归类摆放,彻底但劳心费力。正因如此,它只适合低频场景——通常是“开馆日”或者“换新馆”时才做一次,平时的日常更新还得靠更轻巧的办法。

增量更新机制

图书馆每天都会到几十本新书。如果为这几本新书又把全馆重编一次,那馆员什么也别干了。馆员的日常做法是:只给新书补几张目录卡片,插进对应的抽屉;如果某本书下架了,就在卡片上盖一个“停用”章,而不是立刻把柜台拆掉重做。

这套日常流程对应搜索引擎的增量更新:只处理新增、修改、删除的那一小部分文档,而不是重建整个索引。写入方先把变更推进一个更新队列,后台按批次消费:新增文档做分词并追加倒排记录;修改文档则先标记旧文档失效,再写入新版本;删除文档并不立刻从物理文件中移除,而是写入一个“墓碑”标记,延迟清理。

增量更新的代价与变更量成正比,而全量更新的代价与全部数据量成正比。假设库中有 D 篇文档,本次变更了 ΔD 篇,那么两种策略的成本对比是:

Cinc=O(ΔD) ≪ Cfull=O(D)\displaystyle C_{\text{inc}} = O(\Delta D) \ \ll \ C_{\text{full}} = O(D)

1. 变更量 ΔD:本次新增、修改、删除的文档数量,通常远小于总量。

ΔD≪D\displaystyle \Delta D \ll D

2. 增量成本 C_inc:只处理变更文档,成本与 ΔD 同阶。

Cinc=O(ΔD)\displaystyle C_{\text{inc}} = O(\Delta D)

3. 全量成本 C_full:必须重读全部文档,成本与 D 同阶。

Cfull=O(D)\displaystyle C_{\text{full}} = O(D)

4. 对比结论:当 D 很大而 ΔD 很小时,增量更新优势巨大。

Cinc 远小于 Cfull\displaystyle C_{\text{inc}} \text{ 远小于 } C_{\text{full}}

日常运营靠增量,节庆大扫除才做全量,两者配合才是完整的索引维护策略。但增量更新带来一个新问题:新卡片是插进抽屉了,读者到底什么时候能在目录里看到它?

近实时可见

读者希望“今天的新书,最好马上就能查到”。但每本新书一进门就立刻登记,馆员会被频繁的登记工作拖垮。于是馆长规定:新书先放“待登记车”,每十五分钟整点统一登记一次。读者在绝大多数情况下,等一小会儿就能在目录里查到新书——这就是“近实时”,不是严格意义的“立刻”,但已经足够好。

搜索引擎用同样的思路实现 Near Real-Time(NRT) 索引:新文档先写入内存缓冲区,由后台每隔一个刷新周期把缓冲区内容生成一个“内存段”,并把它加入到可查询的段列表中。Elasticsearch 默认 refresh 间隔约 1 秒,因此写入后通常 1 秒内即可被搜到。为了避免内存数据在崩溃时丢失,写入的同时还会记录事务日志(translog),重启后先重放日志再提供服务。

文档从写入到可被搜索,等待时间近似等于写入耗时加上下一个刷新周期:

tvisible≈twrite+Rrefresh\displaystyle t_{\text{visible}} \approx t_{\text{write}} + R_{\text{refresh}}

1. 写入耗时 t_write:文档被接收并写入缓冲区的耗时,通常极短。

twrite≈0\displaystyle t_{\text{write}} \approx 0

2. 刷新周期 R_refresh:两次刷新之间的间隔时间,如 1 秒。

Rrefresh=1s\displaystyle R_{\text{refresh}} = 1\text{s}

3. 可见延迟 t_visible:从写入到可被查询的总等待时间。

tvisible≈Rrefresh\displaystyle t_{\text{visible}} \approx R_{\text{refresh}}

4. 平衡点:刷新越频繁越实时,但产生的段文件也越多,见下一节。

Rrefresh↓段数量↑\displaystyle R_{\text{refresh}} \downarrow \quad \text{段数量} \uparrow

“整点登记”让新书在读者可接受的等待窗口内上架,近实时就是工程意义上的“够快的实时”。不过这套登记制度还有个隐患:每十五分钟就多一小叠卡片,抽屉越来越乱,怎么办?

段合并策略

每次整点登记,馆员都会往抽屉塞进几张新卡片。时间一长,同一个词条的抽屉里塞满了来自不同批次的小纸片,读者翻找越来越吃力。馆长的解决办法是:定期把小纸片抄写整理,装订成一本干净的正式目录册,把小纸片烧掉,抽屉瞬间清爽,查书也更快。

搜索引擎同样面临“段碎片化”问题。每次 refresh 产生一个不可变的小段,查询时要依次读取所有段再合并结果;段数量一多,文件句柄、磁盘读取和内存开销都会上涨。于是后台的段合并机制登场:采用分层合并策略(tiered merge),把小段按大小分层,在后台把小段读出来合并成较大的段,再原子地替换旧段。

合并过程并不修改旧段,而是生成新段文件,因此查询始终不受阻断。合并成本与所有被合并段的大小之和成正比:

Cmerge=O ⁣(∑i=1m∣Si∣)\displaystyle C_{\text{merge}} = O\!\left(\sum_{i=1}^{m} |S_i|\right)

1. 被合并段数 m:本次合并涉及的小段个数。

S1,S2,…,Sm\displaystyle S_1, S_2, \ldots, S_m

2. 单段大小 |Sᵢ|:每个段的文档数与索引体积。

∣Si∣=size(Si)\displaystyle |S_i| = \text{size}(S_i)

3. 总合并成本:需要读取所有段内容再写出新段。

Cmerge∝∑i=1m∣Si∣\displaystyle C_{\text{merge}} \propto \sum_{i=1}^{m} |S_i|

4. 好处:合并后段数减少,查询只需读更少的段。

#Segments↓查询性能↑\displaystyle \#\text{Segments} \downarrow \quad \text{查询性能} \uparrow

装订目录册通常选在馆内人少的时段进行,还要控制抄写速度避免影响读者借书。段合并也有同样的讲究:限流、避开高峰,在后台悄悄进行,绝不打扰前台查询。

版本无感切换

图书馆决定启用一套全新版目录:用更细的主题词重新分类,让读者更好查。馆长没有把旧目录撕掉让读者干等,而是让馆员们花几天时间把新目录全部做好,然后在一个周一的早晨,挂出“请使用新版目录”的牌子。读者照样查目录找书,完全没察觉到更换过程。

搜索引擎发布新索引版本时也追求这种“无感”。索引由多个不可变的段文件构成,系统用一个提交点记录当前生效的全部段列表。构建新版本时,新段文件在后台全部准备就绪,然后只需要把提交点从旧段列表原子地替换为新段列表——查询请求从此读到新段,旧段则在确认不再被引用后删除。

整个切换过程可以表达为提交点的原子交换:

CommitPoint={S1,S2,…,Sm}→atomic swap{S1′,S2′,…,Sn′}\displaystyle \text{CommitPoint} = \{ S_1, S_2, \ldots, S_m \} \xrightarrow{\text{atomic swap}} \{ S'_1, S'_2, \ldots, S'_n \}

1. 旧提交点:当前查询正在使用的段集合。

{S1,…,Sm}\displaystyle \{ S_1, \ldots, S_m \}

2. 新提交点:后台构建完成的新段集合。

{S1′,…,Sn′}\displaystyle \{ S'_1, \ldots, S'_n \}

3. 原子替换:一次内存指针切换,让所有新请求立刻使用新列表。

swap(old,new)\displaystyle \text{swap}(\text{old}, \text{new})

4. 无感效果:切换瞬间完成,无需重启,查询不中断。

downtime=0\displaystyle \text{downtime} = 0

先做好新目录册再挂牌,读者全程无感知;先构建好新段再换提交点,线上查询也全程无感知。这就是“版本无感切换”的工程之美。

读写一致性保障

读者按目录查书,最怕遇到“目录写着有,书架却是空的”。图书馆要做到账实一致:目录里能查到的书,书架上一定有。哪怕正在整点登记或装订目录,读者也不能看到“半本登记了一半”的书——要么查不到,要么查到完整信息。

搜索引擎通过读写隔离保证这种一致性。写入路径走内存缓冲加事务日志,查询路径只读“已提交”的段文件。新文档在刷新之前对查询完全不可见,刷新时一次性把缓冲内容转为段,之后查询就能看到完整记录——不存在中间状态。如果节点崩溃,事务日志会重放,保证已确认写入的数据不丢失;副本之间则采用最终一致,主节点先落日志,从节点异步追赶,快速收敛。

一次查询可见的所有数据,来自内存缓冲中已刷新的部分和所有磁盘段:

VisibleSet(q)=⋃S∈Memory∪SegmentsSearch(S,q)\displaystyle \text{VisibleSet}(q) = \bigcup_{S \in \text{Memory} \cup \text{Segments}} \text{Search}(S, q)

1. 可见来源集合:内存缓冲与磁盘段共同构成查询的读取范围。

Memory∪Segments\displaystyle \text{Memory} \cup \text{Segments}

2. 单段查询:每个段独立执行倒排检索,返回局部结果。

Search(S,q)\displaystyle \text{Search}(S, q)

3. 结果并集:把各段的命中结果合并且去重,返回完整答案。

⋃SSearch(S,q)\displaystyle \bigcup_{S} \text{Search}(S, q)

图书馆允许“新书暂未上架”,但绝不纵容“目录有书、架上无书”。搜索引擎同样在用“最终一致 + 快速收敛”换取高写入吞吐——这是工程上最优雅的取舍。

生产运维实践

一座大型图书馆能长治久安,靠的不是馆员加班,而是制度:高峰期限制馆内人流、每晚闭馆后盘点整理、新书按批次上架、数月一次的全面检修。馆长的日常不是搬书,而是定规则、盯指标、做预案。

搜索引擎的生产运维同样讲究制度。最要紧的是容量规划:根据数据总量和单节点容量决定分片数;然后监控告警:盯住段数量、刷新耗时、合并队列长度和 GC 指标;再用压测验证高写入吞吐下的表现,最后用滚动发布把新版本逐批推上线。

估算所需分片数可以套用一个朴素公式:数据总量除以单节点可容纳的数据量,向上取整:

Shards=⌈DataSizeNodeCapacity⌉\displaystyle \text{Shards} = \left\lceil \frac{\text{DataSize}}{\text{NodeCapacity}} \right\rceil

1. 数据总量 DataSize:包含原始数据、索引副本和未来增长余量。

DataSize=raw×(1+growth)\displaystyle \text{DataSize} = \text{raw} \times (1 + \text{growth})

2. 单节点容量 NodeCapacity:单个节点可稳定承载的数据量。

NodeCapacity=per-node limit\displaystyle \text{NodeCapacity} = \text{per-node limit}

3. 向上取整 ceil:分片数必须取整,并留出余量。

⌈⋅⌉\displaystyle \left\lceil \cdot \right\rceil

4. 最终分片数:过少导致单分片过大,过多导致管理开销上升。

Shards=max⁡(formula,minRequired)\displaystyle \text{Shards} = \max(\text{formula}, \text{minRequired})

馆长的制度让图书馆几十年平稳运转;运维工程师的容量规划、监控和压测,让搜索引擎在千万级流量下依然“写入不丢、查询不抖”。所有功夫,都下在读者看不见的日常里。

AI 应用场景

电商商品搜索的实时上架

阶段:数据接入与索引构建

解决的问题:商品上架、改价、库存变更需要几乎实时反映到搜索结果。若用全量重建,每次上架百万级商品要跑数小时;用增量更新加近实时刷新,用户搜索后 1 秒内就能看到新商品,下架商品也能快速失效。

具体案例:某电商平台每天上架数十万件商品,工程师将商品变更写入消息队列,搜索服务以批量方式消费并写入索引缓冲,refresh 间隔设为 1 秒;段合并任务调度到凌晨低峰执行,并限制合并 IO 带宽。大促前再执行一次全量重建,保证索引干净高效。

日志平台的持续写入与稳健检索

阶段:数据管道与运维监控

解决的问题:日志平台每天接收 TB 级日志,必须持续写入且随时可查。索引更新策略直接决定磁盘占用和查询延迟。

具体案例:云日志平台按天创建索引,每个索引内部用段合并控制段数量;写入高峰时自动调大刷新间隔、降低合并并发,保证写入吞吐优先;通过监控段数量和合并延迟,在磁盘水位过高前触发索引生命周期管理(如冷热分层)。

企业知识库的版本升级

阶段:知识库索引发布

解决的问题:企业内部文档知识库在导入全新数据、升级分词器或字段映射时,需要换一套全新的索引。如果直接覆盖线上索引,会造成查询中断或新旧数据交错。

具体案例:某企业知识库首次上线采用全量构建生成基线索引,日常文档更新走增量通道;当升级中文分词词典时,团队先在后台灰度构建新索引,构建完成后通过提交点原子切换,用户全程无感知,甚至没有重启服务。

Python 演示

python55 行
import time  class MiniIndex:    def __init__(self):        self.buffer = {}        self.segments = []        self.merge_count = 0     def add(self, doc_id, terms):        for t in set(terms):            self.buffer.setdefault(t, []).append(doc_id)     def refresh(self):        if not self.buffer:            return False        self.segments.append(self.buffer)        self.buffer = {}        return True     def merge(self):        if len(self.segments) < 2:            return False        merged = {}        for seg in self.segments:            for t, docs in seg.items():                merged.setdefault(t, []).extend(docs)        self.segments = [merged]        self.merge_count += 1        return True     def search(self, term):        hits = []        for seg in self.segments:            hits.extend(seg.get(term, []))        hits.extend(self.buffer.get(term, []))        return sorted(set(hits))  def main():    idx = MiniIndex()    for i in range(50):        idx.add(i, ["python", "index"])    idx.refresh()    print("refresh 后命中:", len(idx.search("python")))    for i in range(50, 100):        idx.add(i, ["python", "merge"])    idx.refresh()    print("两个小段搜索:", len(idx.search("python")))    idx.merge()    print("合并后段数:", len(idx.segments), "命中:", len(idx.search("python")))  if __name__ == "__main__":    main()
目的:用 60 行代码模拟搜索引擎索引的完整生命周期——写入缓冲、刷新成段、段合并、搜索时跨段取并集。
  1. self.buffer = {}内存缓冲,新文档先积累在这里,对查询不可见,模拟“待登记车”。
  2. self.buffer.setdefault(t, []).append(doc_id)按词项追加文档号,构建内存中的倒排表。
  3. idx.refresh()模拟近实时刷新:缓冲变为一个新段,从此对搜索可见。
  4. self.segments.append(self.buffer)把缓冲整个作为不可变段加入段列表,之后不再修改它。
  5. for seg in self.segments:查询需要遍历所有段取并集,这正是段太多会拖慢查询的原因。
  6. merged.setdefault(t, []).extend(docs)合并时把多个段的倒排记录汇总进一个大段。
  7. self.merge_count += 1记录合并次数,便于在真实系统中监控合并频率。

常见误区

误区我每次都把整个索引重建一遍,逻辑简单也不容易出错,为什么要搞增量更新和段合并那套复杂机制?

正解数据量小的时候全量确实省心,但当文档达到百万、千万级,全量重建要耗时数小时,期间索引无法提供最新数据,CPU、磁盘 IO 都被占满。真实系统里,全量重建只作为低频“大扫除”,日常新数据必须走增量写入,否则用户搜索到的永远是昨天的旧数据。

为何常见:全量实现直观、调 bug 容易,小规模场景的代价看不出来,容易把“能用”当成“可扩展”。

误区把 refresh_interval 设为 0,每次写入都立刻刷成段,搜索就能实时看到最新数据,用户体验拉满。

正解每次刷新都会产生一个不可变小段文件。写入越频繁刷新,产生的小段就越多,查询时访问的段数、打开的文件句柄数量暴涨,检索延迟反而升高,写入吞吐也大幅下降。正确的做法是让缓冲攒一批文档再统一刷新,用可控的秒级延迟换取高吞吐。

为何常见:直觉上“越实时越好”,忽略了频繁刷新造成的小段碎片成本。

误区合并段就是要读旧段、写新段,肯定得加锁,不然写入的数据会丢。所以合并期间索引应该不可写。

正解段是不可变的,合并只是后台读取旧段内容、生成新的大段文件,写入路径照常往缓冲里加新文档,查询也继续读旧段。唯一的风险是磁盘 IO 竞争,所以工程上用合并限流来控制对写入和查询的影响,而不是暂停服务。

为何常见:把“文件归并”类比成关系型数据库的锁表迁移,忽略了分段存储天然支持读写隔离。

误区我调用删除接口把一批旧文档删掉了,磁盘上的索引空间应该立刻变小。

正解由于段不可变,删除只是在倒排记录上打一个“墓碑”标记,被删文档仍然占用磁盘空间。只有等到段合并发生时,墓碑记录才会被真正跳过,空间才会释放。这就是为什么大批量删除后要执行强制合并来回收磁盘。

为何常见:把逻辑删除和物理删除混为一谈,不了解不可变段的结构约束。

误区更新分词器或字段映射,意味着要换一套新索引,那只能停服重启,让所有节点加载新配置。

正解索引切换通过提交点原子替换完成:新段列表准备好后,内存中把提交指针换掉,新查询立刻用新段,整个过程无需重启。只有涉及上层代码或配置热加载失败的场景才需要重启,而且可以滚动重启做到零总停机。

为何常见:把“配置发布”和“文件替换”混为一谈,不知道提交点机制的存在。

面试题

Q1. 全量重建索引最适合以下哪个场景?

单选题 · easy

  • A.系统上线时初次构建索引← 正确答案
  • B.实时订单数据的持续更新
  • C.聊天消息的实时送达检索
  • D.用户点击流的秒级统计

答案:A。系统首次上线没有可用索引,必须从零全量构建。订单、聊天、点击流都属于高频增量场景,若用全量重建会消耗巨大资源且无法提供实时数据。

Q2. 近实时索引通过什么控制新文档的可见延迟?

单选题 · easy

  • A.磁盘读写的队列长度
  • B.内存缓冲区的容量上限
  • C.索引的刷新周期频率← 正确答案
  • D.查询请求的并发数量

答案:C。refresh 周期决定内存缓冲中的文档多久生成内存段并进入可查询列表。周期越短,可见越快,但段碎片越多。这就像图书馆“整点登记”的间隔时间。

Q3. 关于段(Segment)的描述,正确的是?

单选题 · easy

  • A.段可以被原地修改
  • B.段是不可变的子索引← 正确答案
  • C.段只存储删除标记
  • D.段只能存在于内存

答案:B。段是倒排索引的不可变子集,写入后不再修改。正因不可变,查询可以无锁并发读取;也正因不可变,删除只能先打标记,等合并时再物理清理。

Q4. 频繁刷新索引会带来什么副作用?

单选题 · mid

  • A.减少小段数量和句柄开销
  • B.生成大量小段拖慢查询← 正确答案
  • C.提升写入吞吐和检索速度
  • D.使事务日志彻底失去作用

答案:B。每次 refresh 产生一个新段,刷新越频繁段越多,查询要访问更多段文件,句柄和内存开销上涨,写入吞吐也被拖累。所以 refresh 间隔要平衡实时性与性能。

Q5. 为什么删除文档后索引空间不会立即释放?

单选题 · mid

  • A.删除操作会被系统拒绝
  • B.删除以标记延迟到段合并← 正确答案
  • C.空间被倒排列表永久占用
  • D.系统等待备份完成后清理

答案:B。段不可变,删除只是写入“墓碑”标记,文档仍然占用磁盘。只有段合并重写文件时才会跳过被删除文档,真正释放空间。

Q6. 索引版本无感切换的核心机制是什么?

单选题 · mid

  • A.直接覆盖正在服务的段文件
  • B.原子替换提交点段列表引用← 正确答案
  • C.重启全部节点并重新加载
  • D.复制新目录覆盖旧目录

答案:B。提交点保存当前生效的段列表,切换时新段已全部就绪,只需要把提交点指针原子地换掉,新查询即刻读取新段,无需重启。就像图书馆先做好新目录册再挂牌。

Q7. 段合并过程中查询为何不受阻断?

单选题 · hard

  • A.查询只读旧段直到新段就绪← 正确答案
  • B.合并在内存中完成不占磁盘
  • C.查询结果全部缓存无需读盘
  • D.查询被调度到其他集群执行

答案:A。合并是后台创建新段文件,旧段在合并完成前继续服务查询;新段就绪后通过提交点替换,查询才切到新段。整个过程查询一直读到完整数据。

Q8. LSM 树近实时写入路径的正确顺序是?

单选题 · extreme

  • A.先写磁盘段再进内存缓冲
  • B.先写内存缓冲再刷成段← 正确答案
  • C.直接修改旧段并追加文档
  • D.先合并大段再写入新段

答案:B。近实时写入先进入内存缓冲区,并同时写事务日志;到达刷新周期后,缓冲内容被整体刷成一个不可变段,再进入段合并流程。顺序是“缓冲 → 段 → 合并”。

Q9. 哪些情况更适合触发全量重建而不是增量更新?

多选题 · hard

  • A.分词器版本完全不兼容← 正确答案
  • B.字段映射需要重新解析← 正确答案
  • C.单条商品价格发生变化
  • D.索引版本升级要求重写← 正确答案

答案:ABD。分词器变化、字段映射调整、索引版本升级都会影响整库文档的解析方式,必须全量重写;而单条商品价格变化只需增量更新该文档,不需要重建整个索引。

Q10. 关于近实时索引一致性的正确说法有哪些?

多选题 · mid

  • A.写后立即对所有副本立即可见
  • B.通过刷新周期控制可见延迟← 正确答案
  • C.事务日志保证崩溃后不丢数据← 正确答案
  • D.查询合并内存段与磁盘段结果← 正确答案

答案:BCD。近实时是最终一致的,写入后需等待下次刷新才对查询可见,因此 A 错误。刷新周期决定延迟;事务日志在崩溃后重放保证不丢数据;查询时会合并内存段与磁盘段的结果,这些说法都正确。