文章目录
- 索引版本管理实战:重建索引不中断服务的蓝绿切换
- 一、为什么索引"不能不中断地重建"
- 二、版本化索引的结构设计
- 2.1 三层结构:元数据层指向活动版本
- 2.2 版本 contents:不只是向量
- 三、构建新版本:一次完整的演练
- 四、切换的四种做法
- 4.1 原子切换(主推)
- 4.2 双写过渡(重大变更)
- 4.3 双读对比(保守策略验证)
- 五、快照与增量的时间缝
- 六、回滚:索引也要有 Plan B
- 七、嵌入模型升级:版本管理最典型的触发器
- 八、落地检查清单
- 九、小结
索引版本管理实战:重建索引不中断服务的蓝绿切换
RAG 系统最怕的运维动作是什么?重建索引。换 embedding 模型要重建(不同模型的向量空间不兼容)、修切分策略要重建、升级图谱要重建——而重建意味着"全库文档重新 embedding",一次几小时。这期间服务不能停,用户不能感知,新旧索引还得无缝过渡。这篇文章讲索引版本管理的完整方案:版本化索引怎么设计、蓝绿切换的四种做法、切换期的读写一致性、以及回滚策略。所有讨论都基于一个原则:索引变更对用户透明。
一、为什么索引"不能不中断地重建"
先算账,理解问题的规模。一个 50 万文档的知识库:
- 重新 embedding:50 万 × 3 秒/百文档 = 单 GPU 约 6~8 小时;调 API 按并发算也要小时级;
- 重建期间如果原地覆盖(先删后建),索引长时间不完整——用户查询命中旧索引的切片,答案质量随机漂移;
- 如果新旧索引并存但检索层随机路由,同一问题两次查询结果不一样——用户感知是"系统不稳定"。
核心需求:重建期间服务持续可用、质量不下降、可随时回滚。这三点只有版本化索引 + 受控切换能满足。
二、版本化索引的结构设计
2.1 三层结构:元数据层指向活动版本
┌─────────────────────────────────────────┐ │ 检索服务(对上层无感知) │ │ 查询 → 读"当前活动版本" → 路由到具体索引 │ └──────────────┬──────────────────────────┘ │ ┌──────────────▼──────────────────────────┐ │ 索引注册表(元数据层,小、强一致) │ │ kb_main: │ │ active_version: v3 │ │ v1: {status: retired, ...} │ │ v2: {status: retired, ...} │ │ v3: {status: active, created: ...} │ │ v4: {status: building, 20%...} │ └──────────────┬──────────────────────────┘ │ ┌──────────────▼──────────────────────────┐ │ 物理索引(各自独立,互不干扰) │ │ kb_main_v1/ kb_main_v2/ kb_main_v3/ │ │ kb_main_v4/ ← 正在构建,不对外服务 │ └─────────────────────────────────────────┘设计要点:注册表是唯一变更点,且变更要原子(etcd/Redis/配置中心保证),物理索引永远不动旧的只加新的。向量库天然支持多 collection(Qdrant)/多 index(Milvus)——一个版本一个 collection,物理隔离。
2.2 版本 contents:不只是向量
一个完整的"索引版本"包含:
| 组件 | 说明 |
|---|---|
| 向量 collection | embedding 模型 M 生成的全部向量 |
| 块存储(chunk store) | 原文块(换切分策略时内容也会变) |
| 词汇索引(BM25 倒排) | 混合检索的稀疏侧 |
| 图谱(如有) | 实体关系数据 |
| 版本元数据 | embedding 模型及版本、切分策略 hash、文档快照范围、构建时间 |
关键是版本元数据里记"用什么配置构建的"——排查"为什么召回变差"时,先看活动版本的配置 hash 变了没。没有这个,线上索引就是黑盒。
三、构建新版本:一次完整的演练
新版本构建流水线(对线上零影响):
1. 冻结输入快照:记录"此版本基于截至 T 时刻的文档集合 D" (之后进来的增量文档走 v_new 的增量流,见第五节) 2. 全量构建: ├─ 解析产物复用(上次解析的 Markdown/JSON 还在,不用重解析!) ├─ 新切分策略 → 新 chunks ├─ 新 embedding 模型 → 新 collection ├─ BM25 倒排重建 └─ 图谱重建(如涉及) 3. 离线校验(切流量前的门槛): ├─ 评估集回归:100~200 条黄金查询,新旧版本对比命中率 ├─ 抽样人工评估 20 条答案质量 └─ 性能基线:P99 延迟、QPS 承载 4. 灰度:5% 流量到新版本,监控错误率/延迟/用户反馈 5. 全量切换,观察期 6. 旧版本标记 retired,保留 N 天观察后物理删除第 3 步是灵魂:全量构建完不直接切,先过评估集——新旧版本的命中率/答案质量数字对比,比"感觉应该更好"可靠一万倍。质量回退的索引,就该在切换前拦住。
四、切换的四种做法
| 做法 | 机制 | 一致性 | 适用 |
|---|---|---|---|
| 原子切换 | 注册表 active_version 一步改写 | 切换瞬间前后查询分别走新旧 | 默认首选 |
| 双写过渡 | 期间新旧索引同时写,查询按比例路由 | 最终一致 | 大规模重建 + 高风险变更 |
| 双读对比 | 查询同时打新旧,返回旧的但记录新的质量 | 新版本影子验证 | 不放心时的预演 |
| 灰度放量 | 5% → 20% → 100% 逐步 | 渐进 | 大团队保守策略 |
4.1 原子切换(主推)
# 伪代码:注册表原子切换defswitch_version(kb,from_v,to_v):withregistry.transaction():# 原子 CASassertregistry.active[kb]==from_v registry.active[kb]=to_v registry.status[kb][from_v]="retired"registry.status[kb][to_v]="active"# 切换后:新查询走 to_v,in-flight 的旧查询走完 from_v切换瞬间的 in-flight 查询用旧版本——这是可以接受的(几十毫秒内完成的老查询),服务不中断、无脏读窗口。这是 90% 场景的答案。
4.2 双写过渡(重大变更)
换 embedding 模型这种"向量空间完全变了"的变更,怕的不是服务中断,是切换后质量不达预期。双写方案:
- 构建期:新旧版本都接受增量写入(新文档双写进 v_old 和 v_new);
- 过渡期:查询按 5% 抽样打 v_new,记录质量指标;
- 确认 v_new 指标达标且稳定 N 天后,原子切 100%;
- 停止 v_old 写入,观察后删除。
代价是构建期存储翻倍、写入双倍——只对全库重建这种大动作,日常增量不值得。
4.3 双读对比(保守策略验证)
查询同时发 v_old 和 v_new,用户答案永远来自 v_old,但 v_new 的检索结果和质量指标进对比报表。跑一周,人工看"新版本到底好在哪、差在哪",再决定切不切。适合"不敢切又想知道新版本行不行"的场景——本质上是用流量换信心。
五、快照与增量的时间缝
版本化最细的活是处理构建期间的增量文档:“v_new 基于 T 时刻的快照构建,T 之后进来的新文档怎么办?”
三种处理:
- 忽略(不可接受):T 后的新文档在 v_new 里不存在,切过去就"丢文档";
- 构建完补增量(推荐):快照集合 D 构建完成后,再把 T 后的变更(新文档/更新/删除)按同样策略补进 v_new——这就是 v_new 的增量流,逻辑和日常增量索引一致,直到 v_new 的增量流追上 v_old 的实时流;
- 切换窗口静态化:构建到"增量追平"才允许切换——注册表里加一道门槛:delta_lag = 0 才能 active。
核心状态机:
v_new: building → (全量完成,增量追赶中) → delta_caught_up → (灰度) → active v_old: active → (灰度期) → retired → deleted增量追平时的并发写入要处理:增量流同时写 v_old(现役服务)和 v_new(构建中),写路径短暂双倍。构建期长短决定了这个代价——构建超过一天的知识库,双写成本要评估(存储便宜,写入吞吐和 embedding 调用费不便宜)。
六、回滚:索引也要有 Plan B
新版本上线后发现问题(评估集没覆盖的 regression、用户投诉变多),回滚就是"注册表改回 v_old"——原子回滚,分钟级。前提:
- v_old 在 retired 后保留 7~14 天再物理删除(回滚窗口);
- v_old 的增量流在保留期内继续跑(不然回滚回去丢了一周文档)——这需要双写期覆盖整个观察期;
- 回滚流程演练过:注册表操作、写入路径切换、监控确认,一次 5 分钟的事故演练。
没有回滚预案的索引切换是不负责任的——评估集永远覆盖不全线上问题,新版本上线第一周的投诉率上升不罕见。
七、嵌入模型升级:版本管理最典型的触发器
最后说最常见的重建原因——换 embedding 模型(text-embedding-004 升到 3-large、bge-m3 换新版本)。注意:
- 不同模型的向量空间完全不兼容,不能混在一个 collection 里检索;
- 新旧模型可以并存一段时间:查询侧如果暂时没法全库重建,也不能用新模型只 embedding 新文档——❌新旧向量混检 = 检索质量崩坏。要么全库重切,要么不动;
- 灰度期可以用双读对比评估新模型(查询 → 新模型 embedding → 只在新 collection 检索 → 对比指标),这是模型选型的标准姿势——先对比后切换,别直接All in。
八、落地检查清单
- 索引注册表(活跃版本、各版本状态、版本元数据含 embedding 模型+切分 hash+快照范围);
- 一个版本一个物理 collection,旧版本只加不动;
- 构建流水线:快照冻结 → 复用解析产物 → 全量构建 → 增量追平;
- 切换前门槛:评估集回归通过(命中率不低于旧版)+ 灰度指标;
- 原子切换为主,重大变更(换模型)用双写过渡 + 灰度放量;
- 增量流双写覆盖整个构建+观察期,delta_lag=0 才允许 active;
- 旧版本保留 7~14 天(含增量流)再物理删除,回滚流程演练过;
- 版本元数据可查询,"当前线上索引用什么配置"随时能回答。
九、小结
索引版本管理的四句话:
- 物理索引只加不改,注册表唯一变更——一个版本一个 collection,原子切换,这是所有方案的地基;
- 构建不等同切换:评估集回归 + 灰度指标 + delta_lag=0,三道门槛全过才 active;
- 重大变更(换 embedding 模型)走双写过渡 + 灰度放量,日常变切片策略原子切换就够;
- 回滚预案和切换方案一样重要:旧版本保留 14 天、增量流继续跑、回滚流程演练过——评估集覆盖不全的 regression,只能靠回滚兜底。
下一篇讲多租户知识库:一套 RAG 系统服务多个租户时,数据隔离、检索过滤、成本分摊怎么做。