3、全量构建与增量构建:全量索引构建流程、增量索引构建流程、两者如何协同工作。
好,咱们接着聊索引构建。上一节讲了倒排索引的结构,这一节咱们聊聊索引到底是怎么「造」出来的。
你想想看,搜索引擎要处理的数据量,动不动就是几亿、几十亿条。怎么把这些数据高效地转成索引?这里有两个核心手段:全量构建和增量构建。
说白了,全量构建就是「从头再来」,增量构建就是「小修小补」。两者各有各的用武之地,也各有各的坑。我做了这么多年搜索,这两块踩过的坑可真不少。
3.1 全量索引构建流程
全量构建,顾名思义,就是把所有数据重新捞一遍,从头生成一份全新的索引。什么时候用?系统第一次上线、数据出现严重不一致、或者索引结构发生了重大变更——这时候,别犹豫,直接跑全量。
全量构建的流程,我习惯把它拆成四个阶段:
- 数据抽取:从数据库、HDFS、或者消息队列里,把原始数据拉出来。
- 数据预处理:清洗、去重、字段映射、格式转换。这一步很关键,脏数据进来,索引就废了。
- 索引构建:对预处理后的数据做分词、倒排、排序,生成最终的索引文件。
- 索引发布:把新索引推送到线上集群,替换掉旧索引。
这里我贴一段简化版的构建代码,帮你理解核心逻辑:
// 伪代码:全量索引构建
void buildFullIndex() {
// 1. 从数据库拉取全量数据
List<Document> docs = fetchAllFromDB();
// 2. 数据预处理
for (Document doc : docs) {
doc.clean(); // 清洗
doc.validate(); // 校验
doc.transform(); // 格式转换
}
// 3. 构建倒排索引
InvertedIndex index = new InvertedIndex();
for (Document doc : docs) {
List<Token> tokens = analyzer.tokenize(doc.getContent());
for (Token token : tokens) {
index.add(token, doc.getId());
}
}
// 4. 排序并写入磁盘
index.sort();
index.writeToDisk("/data/index/full_index_v3");
// 5. 发布索引
publishIndex("/data/index/full_index_v3");
}
重点提醒:全量构建期间,线上服务不能停。所以一般会采用「双缓冲」策略——新索引在后台默默构建,构建完成后,原子性地切换。用户几乎无感知。
全量构建的优点很明显:数据一致性高,索引结构干净。但缺点也致命——太慢了。我经历过一个项目,全量构建一次要跑6个小时。这期间如果有新数据进来,根本来不及反映。怎么办?这就轮到增量构建上场了。
3.2 增量索引构建流程
增量构建,就是只处理新增、修改、删除的数据。它的核心目标是:快。
增量构建的流程相对轻量:
- 捕获变更:监听数据库的binlog、或者消息队列的变更事件。
- 增量预处理:只处理变更的那几条数据。
- 增量索引构建:对变更数据单独生成一个小索引段(segment)。
- 合并到主索引:把小索引段合并到已有的索引中。
嗯,这里要注意一个关键点:增量索引不是直接修改原索引文件。而是生成一个独立的「小索引段」,然后通过合并机制整合进去。为什么?因为直接修改大索引文件,性能太差了,而且容易出并发问题。
我举个例子,Lucene 就是这么干的。每个增量构建生成一个 segment,多个 segment 在后台定期合并。查询时,同时查多个 segment,再把结果合并返回。
// 伪代码:增量索引构建
void buildIncrementalIndex(List<Document> changedDocs) {
// 1. 对变更数据单独构建小索引段
Segment segment = new Segment();
for (Document doc : changedDocs) {
List<Token> tokens = analyzer.tokenize(doc.getContent());
for (Token token : tokens) {
segment.add(token, doc.getId());
}
}
segment.flush();
// 2. 将小段加入索引列表
indexManager.addSegment(segment);
// 3. 触发后台合并(条件满足时)
if (indexManager.shouldMerge()) {
indexManager.mergeSegments();
}
}
我的经验:增量构建的延迟,一般控制在秒级到分钟级。如果业务要求实时性极高(比如电商库存),可以考虑用内存索引 + 定时刷盘的方式。我曾经在一个广告系统里,把增量延迟压到了500毫秒以内。
3.3 全量与增量如何协同工作
好,现在问题来了:全量构建和增量构建,到底怎么配合?
我见过不少团队,上来就只用增量构建,觉得全量太慢。结果呢?跑着跑着,增量段越来越多,查询越来越慢,数据还出现了不一致。最后不得不半夜跑全量重建——这就是典型的「增量陷阱」。
正确的做法是:全量兜底,增量提效。
具体来说,协同策略是这样的:
| 策略 | 触发时机 | 作用 |
|---|---|---|
| 定期全量 | 每天/每周一次(低峰期) | 消除数据碎片,保证一致性 |
| 实时增量 | 数据变更时立即触发 | 保证数据的时效性 |
| 紧急全量 | 发现数据严重不一致时 | 快速恢复数据正确性 |
我习惯用「基座 + 补丁」的模型来理解:
- 全量索引 = 基座。它是一份完整、干净、结构最优的索引。
- 增量索引 = 补丁。它是在基座之上,不断叠加的小修改。
查询时,先查基座,再查所有补丁,最后合并结果。当补丁太多、查询变慢时,就触发一次全量重建,把基座升级到最新版本,然后清空所有补丁。
我曾经踩过的坑:有一次,增量索引的合并策略没配好,导致线上同时存在了200多个小段。查询时,每个请求都要扫描200多个段,CPU直接打满,接口超时率飙升。后来我加了一个强制合并策略——当段数量超过20个时,自动触发全量合并。问题才解决。
最后,给你一个实战中的协同流程图:
数据变更 --> 增量构建 --> 生成小段 --> 加入索引列表
|
v
查询时合并结果
|
v
段数量 > 阈值? --> 是 --> 触发合并 --> 减少段数量
|
否 --> 继续接收增量
定时触发 --> 全量构建 --> 生成新基座 --> 替换旧基座 --> 清空所有增量段
说白了,全量和增量不是二选一的关系,而是搭档。全量负责「稳」,增量负责「快」。两者配合好了,你的搜索引擎才能既快又准,还不出幺蛾子。
下一节,咱们聊聊索引的存储和压缩。嗯,那又是一个很有意思的话题。