3、全量构建与增量构建:全量索引构建流程、增量索引构建流程、两者如何协同工作。

好,咱们接着聊索引构建。上一节讲了倒排索引的结构,这一节咱们聊聊索引到底是怎么「造」出来的。

你想想看,搜索引擎要处理的数据量,动不动就是几亿、几十亿条。怎么把这些数据高效地转成索引?这里有两个核心手段:全量构建增量构建

说白了,全量构建就是「从头再来」,增量构建就是「小修小补」。两者各有各的用武之地,也各有各的坑。我做了这么多年搜索,这两块踩过的坑可真不少。

3.1 全量索引构建流程

全量构建,顾名思义,就是把所有数据重新捞一遍,从头生成一份全新的索引。什么时候用?系统第一次上线、数据出现严重不一致、或者索引结构发生了重大变更——这时候,别犹豫,直接跑全量。

全量构建的流程,我习惯把它拆成四个阶段:

  1. 数据抽取:从数据库、HDFS、或者消息队列里,把原始数据拉出来。
  2. 数据预处理:清洗、去重、字段映射、格式转换。这一步很关键,脏数据进来,索引就废了。
  3. 索引构建:对预处理后的数据做分词、倒排、排序,生成最终的索引文件。
  4. 索引发布:把新索引推送到线上集群,替换掉旧索引。

这里我贴一段简化版的构建代码,帮你理解核心逻辑:

// 伪代码:全量索引构建
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 增量索引构建流程

增量构建,就是只处理新增、修改、删除的数据。它的核心目标是:

增量构建的流程相对轻量:

  1. 捕获变更:监听数据库的binlog、或者消息队列的变更事件。
  2. 增量预处理:只处理变更的那几条数据。
  3. 增量索引构建:对变更数据单独生成一个小索引段(segment)。
  4. 合并到主索引:把小索引段合并到已有的索引中。

嗯,这里要注意一个关键点:增量索引不是直接修改原索引文件。而是生成一个独立的「小索引段」,然后通过合并机制整合进去。为什么?因为直接修改大索引文件,性能太差了,而且容易出并发问题。

我举个例子,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
段数量 > 阈值? --> 是 --> 触发合并 --> 减少段数量
      |
      否 --> 继续接收增量
      
定时触发 --> 全量构建 --> 生成新基座 --> 替换旧基座 --> 清空所有增量段

说白了,全量和增量不是二选一的关系,而是搭档。全量负责「稳」,增量负责「快」。两者配合好了,你的搜索引擎才能既快又准,还不出幺蛾子。

下一节,咱们聊聊索引的存储和压缩。嗯,那又是一个很有意思的话题。