Skip to content

4.3.3 增量更新索引通常怎么做? ​

先给结论:增量更新的核心,不是“只更新改过的那一部分”这么一句话,而是让系统能够稳定识别变化、精确定位受影响对象,并把新增、修改、删除分别同步到索引。

所以一个真正可用的增量更新机制,通常至少要回答三件事:

  1. 哪些数据变了
  2. 变化会影响哪些索引对象
  3. 这些对象应该新增、覆盖、删除,还是失效

一个更稳的增量更新链路长什么样 ​

你可以先把增量更新理解成下面这条链路:

  1. 检测源数据变化
  2. 找到变化对应的文档或记录
  3. 重新清洗受影响内容
  4. 重新切块并生成新的索引对象
  5. 对比新旧对象
  6. 对索引执行 upsert、delete 或失效操作
  7. 记录同步结果,便于回查和重试

注意,这里真正的关键不是“upsert”这个动作本身,而是中间那几步:

  • 如何定位变化
  • 如何稳定映射到对象
  • 如何避免误删和重复

先要有“变化检测”能力 ​

如果系统连哪些数据发生了变化都不知道,就谈不上增量更新。

常见的变化检测信号包括:

  • updated_at
  • 递增版本号
  • 数据库变更日志
  • 消息队列事件
  • 内容哈希 content_hash

如果只是依赖“重新抓一遍再比较全文”,也不是不能做,但效率和可靠性通常会差很多。

更稳的方式通常是让数据源本身提供可追踪的变更信号。

然后要把变化稳定映射到索引对象 ​

这是增量更新最容易被低估的一步。

因为索引里的对象往往不是“原始文档本身”,而是:

  • 文档切成的 chunk
  • 文档和元数据的组合对象
  • 结构化记录转出来的检索单元

所以你不能只知道“文档 A 改了”,还要知道:

  • 文档 A 会影响哪些 chunk
  • 哪些 chunk 应该被替换
  • 哪些旧 chunk 应该被删除

这也是为什么稳定 ID 很重要。

一个常见做法是同时维护:

  • doc_id:标识原始文档
  • chunk_id:标识具体索引对象
  • content_hash:判断对象内容是否真的变化

这样后面才有能力做精确同步,而不是一改就整库重建。

一个最小示意 ​

python
def sync_document(doc):
    cleaned = clean_document(doc)
    new_chunks = chunk_document(cleaned)

    existing_chunks = load_indexed_chunks(doc_id=doc["doc_id"])

    new_by_id = {chunk["chunk_id"]: chunk for chunk in new_chunks}
    old_by_id = {chunk["chunk_id"]: chunk for chunk in existing_chunks}

    to_upsert = []
    to_delete = []

    for chunk_id, chunk in new_by_id.items():
        if chunk_id not in old_by_id:
            to_upsert.append(chunk)
        elif chunk["content_hash"] != old_by_id[chunk_id]["content_hash"]:
            to_upsert.append(chunk)

    for chunk_id in old_by_id:
        if chunk_id not in new_by_id:
            to_delete.append(chunk_id)

    upsert_chunks(to_upsert)
    delete_chunks(to_delete)

这段代码不是生产方案,但它说明了增量更新最核心的几件事:

  • 先找变更文档
  • 再生成新的对象集合
  • 然后对比新旧对象
  • 最后分别执行 upsert 和 delete

增量更新最常见的实现路线 ​

路线一:按文档级事件驱动 ​

适合:

  • 文档中心
  • 知识库页面
  • 文件上传类场景

做法通常是:

  • 文档新增、修改、删除时触发事件
  • 事件进入同步任务队列
  • 后台按 doc_id 处理一整份文档的增量重建

这种方式实现相对直观,也容易和内容管理系统对接。

路线二:按批次扫描变更时间 ​

适合:

  • 数据库表
  • 定时同步任务
  • 没有事件流的数据源

做法通常是:

  • 定时扫描 updated_at > last_sync_time
  • 把命中的记录送入同步流程
  • 每次更新同步游标

这条路线实现成本较低,但要特别注意漏扫、重复扫和时钟偏差问题。

路线三:按变更日志或 CDC 处理 ​

适合:

  • 数据量大
  • 更新频繁
  • 对一致性要求高

做法通常是:

  • 从数据库 binlog、CDC 或事件总线读取变更
  • 按操作类型拆分新增、修改、删除
  • 逐条同步到索引维护流程

这种方式更适合生产系统,但工程门槛也更高。

怎么判断增量更新有没有做稳 ​

至少可以从这几个问题检查:

  1. 修改一篇文档后,旧 chunk 会不会残留
  2. 删除一篇文档后,检索里还能不能搜到它
  3. 同一文档重复同步多次,结果会不会越来越多
  4. 同步失败后,有没有重试和补偿机制
  5. 能不能追踪某个回答命中的对象来自哪次同步

如果这些问题答不上来,通常说明增量更新还没有真正站稳。

一个常见误区 ​

很多人会把增量更新理解成:

  • 只要支持 upsert 就算支持增量更新

这远远不够。

因为增量更新真正难的不是写入,而是:

  • 识别变化
  • 定位影响范围
  • 删除旧对象
  • 避免重复和残留

如果这些没做好,系统表面上是“支持增量更新”,实际上只是在不断把新数据堆进去。

一句话总结 ​

增量更新索引通常不是一个单点动作,而是一条完整链路:先识别变化,再映射受影响对象,对比新旧结果,最后执行 upsert、delete 和补偿。真正难的不是写进去,而是持续保持索引和源数据一致。