Embedding 模型看起来只是检索链路里的一个组件,但一旦进入生产环境,它实际上和向量维度、距离函数、文本预处理、切分方式、索引参数以及历史数据绑定在一起。
因此,模型从 embedding-v1 升级到 embedding-v2,不能简单理解为修改一行模型配置。即便两个模型都输出 1024 维向量,也不代表它们处于同一个向量空间。旧文档向量和新查询向量混用后,距离分数往往已经失去原来的语义意义。
生产迁移要解决四个问题:
- 存量数据如何重新向量化,同时不阻塞在线检索;
- 迁移期间新增和修改的数据如何保持一致;
- 什么时候可以安全把查询切到新模型和新索引;
- 新模型线上效果不达预期时如何快速回滚。
核心原则:向量索引必须和 Embedding 版本一起发布
生产系统里,真正需要版本化的不是一个 model_name,而是一份完整的 Embedding Contract。建议至少记录以下字段:
embedding_contract:
model: embedding-v2
model_revision: 2026-08-01
dimension: 1024
distance: cosine
preprocessing_version: normalize-v3
chunking_version: chunk-512-v4
index_version: knowledge-prod-v2
这里最容易被忽略的是 preprocessing_version 和 chunking_version。如果模型没变,但分词清洗、HTML 去噪或 chunk 边界发生变化,历史向量和新向量同样可能出现不可控漂移。
因此,模型、预处理、切分和索引应该作为一个发布单元治理。
方案一:蓝绿向量索引,兼容性最好
蓝绿方案维护两个物理索引:
knowledge_v1:当前线上索引;knowledge_v2:使用新 Embedding 模型构建的新索引;knowledge_prod:应用始终访问的逻辑 Alias。
迁移过程中,查询继续访问 knowledge_v1,后台构建 knowledge_v2。完成验证后,再把 Alias 原子切换到新索引。
第一步:先创建新索引,不要原地覆盖
新模型可能改变维度或距离函数,所以新索引应显式配置自己的 schema。
client.create_collection(
collection_name="knowledge_v2",
vectors_config=VectorParams(
size=1024,
distance=Distance.COSINE,
),
)
这类约束说明,把”模型升级”设计成新索引发布通常比原地修改更稳妥。
第二步:进入 Dual Write,而不是先全量重建再切换
如果后台重嵌入需要数小时甚至数天,迁移期间线上数据仍会变化。只做一次全量扫描,最终得到的新索引一定会落后于源数据。
正确顺序应是:
- 先开启双写;
- 新增/更新数据同时写入 v1 和 v2;
- 再后台处理历史存量;
- 最后检查增量是否已经追平。
伪代码可以保持简单:
def upsert_document(doc):
old_vec = embed_v1(doc.text)
new_vec = embed_v2(doc.text)
write("knowledge_v1", doc.id, old_vec, doc.metadata)
write("knowledge_v2", doc.id, new_vec, doc.metadata)
真实生产环境不应把”两次写成功”完全寄托在一次 HTTP 请求里。更稳妥的方式是通过消息队列、Outbox 或 CDC 流来驱动两个版本的索引消费者,并为失败数据提供 retry / DLQ。
第三步:后台 Backfill 时必须防止”旧快照覆盖新数据”
这是向量迁移中最隐蔽的竞态条件。
假设文档 A 在 10:00 被后台扫描,10:01 用户更新了文档,双写链路已经把最新版写入 v2;如果 10:02 后台任务又把 10:00 的旧内容重新向量化后覆盖到 v2,新索引反而被回滚成旧数据。
工程上可以使用:
source_version;updated_at;- 单调递增
sequence; - CDC offset;
- 内容 hash。
原则只有一个:Backfill 不能覆盖比自己更新的数据。
Deletes 和 Partial Update 比 Upsert 更难
很多迁移方案只处理 upsert,却忽略 delete。例如某条记录已经在新索引删除,但后台扫描旧索引时又把它重新创建出来。
因此,大规模生产系统更适合以业务主库或变更日志为 Source of Truth,而不是把旧向量库本身当作唯一迁移源。一个更稳妥的数据路径是:
Primary Data Source
|
+--> CDC / Event Log --> Embedding v1 Consumer --> Index v1
|
+--> CDC / Event Log --> Embedding v2 Consumer --> Index v2
|
+--> Backfill Job ----------------------------> Index v2
这样 delete、update、restore 都可以用统一事件语义处理。
方案二:Named Vectors,适合数据库原生支持多向量的场景
如果 Collection 已使用 Named Vectors,并且版本满足要求,可以在同一条数据里增加一个新向量字段,而不是复制整个 Collection。
point_id: 10001
payload: {...}
vectors:
embedding_v1: [...]
embedding_v2: [...]
迁移过程变成:
- 增加
embedding_v2vector schema; - 新写入同时生成 v1、v2;
- 后台为历史 point 补齐 v2;
- 查询从
using=embedding_v1切换为using=embedding_v2; - 观察稳定后再删除旧 vector。
这个方案的优势是 payload 和 point ID 无需复制,回滚也比较直接。但它依赖数据库能力和原 Collection 的结构,不能视为所有向量数据库的通用做法。
Alias 是切流开关,而不是迁移过程本身
很多团队看到 Alias 支持原子切换,就误以为有了 Alias 就完成了零停机迁移。
实际上,Alias 只解决最后一跳的流量切换。真正复杂的是切换之前的数据准备和切换之后的验证。推荐的发布状态机可以定义为:
BUILDING -> DUAL_WRITE -> BACKFILLING -> CAUGHT_UP -> SHADOW_VERIFY
-> CUTOVER -> OBSERVING -> STABLE
任何阶段失败,都应该能回到旧索引,而不是继续强制推进。
切流前不要只比较数据条数
count(v1) == count(v2) 并不能证明迁移成功。至少应做四层检查。
1. 数据完整性
检查 point/document 总量、缺失 ID、重复 ID、source version、最近一段时间的增量 lag,以及 embedding error / DLQ 数量。
2. 向量契约
检查新索引中的 dimension、distance metric、model revision、preprocessing version、chunking version。这些信息最好写入索引 metadata,而不是只存在部署文档里。
3. 离线检索质量
不要拿新旧模型的 cosine score 绝对值直接比较,因为不同向量空间的 score 分布可能不同。更有意义的是使用固定 query 集比较:
- Recall@K;
- MRR / NDCG;
- Top-K overlap;
- 关键业务 query 的人工验收结果。
4. Shadow Query
正式切流前,可以把真实请求复制一份到 v2,只记录结果,不影响用户响应。重点观察:
- Top-K 差异;
- 空结果率;
- p95/p99 检索延迟;
- embedding 调用失败率;
- 特定租户、语言、文档类型是否出现系统性退化。
增量更新:模型迁移结束后,索引仍然会继续变旧
模型升级只是一次大规模变更。日常运行时,原始文档不断修改,如果向量没有同步更新,检索结果同样会逐渐失真。
小规模数据可以周期性做完整 diff;当数据规模继续增长后,扫描整个 corpus 的成本会上升,更适合改为 CDC、事件日志或可枚举的增量版本机制。
换句话说,Embedding Freshness 应该是一条长期数据管道,而不是上线前跑一次的脚本。
适用场景
这套迁移方法尤其适合以下情况:
- 更换 Embedding 模型供应商;
- 模型版本升级导致向量空间变化;
- 维度从 768 调整到 1024/1536;
- dense encoder 替换或升级;
- chunking / preprocessing 规则大改;
- 需要同时升级向量索引参数;
- 对搜索服务停机时间非常敏感。
如果只是少量离线数据、允许维护窗口,而且可以一次性完整重建,则不必引入复杂的双写链路。
常见误区
| 误区 | 事实 |
|---|---|
| 维度一样就可以混用 | 维度只是张量形状,不代表两个模型共享同一语义空间 |
| 先全量重建,最后补一次增量就够了 | 迁移期间持续有更新、删除和部分修改,“最后补一次”很容易漏事件或产生顺序问题 |
| Alias 切换成功就可以马上删除旧索引 | Alias 只解决切流,线上质量还需要观察,旧索引应保留 rollback window |
| 只看离线 Recall,不看线上分布 | 线上真实请求分布可能与离线数据集不同,Shadow traffic 和分群指标很重要 |
| Embedding 版本只记录模型名称 | 应同时记录 model revision、dimension、distance、preprocessing、chunking 和 index version |
上线检查
切换生产 Alias 前,建议至少确认:
- 新旧索引双写已经稳定运行;
- Backfill 已完成,且增量 lag 已收敛;
- delete / partial update 语义已经验证;
- 无持续增长的 embedding error 或 DLQ;
- 新索引的维度、距离函数和模型版本正确;
- 固定检索集通过质量门槛;
- Shadow Query 没有发现明显分群退化;
- Alias / routing 切换操作已经演练;
- 回滚路径和旧索引保留时间已明确;
- 切流后监控包含质量、错误率和延迟,而不只是资源利用率。
参考资料
- Qdrant — Migrate to a New Embedding Model with Zero Downtime:https://qdrant.tech/documentation/tutorials-operations/embedding-model-migration/
- Qdrant — Incremental Embedding Updates:https://qdrant.tech/documentation/tutorials-operations/incremental-embedding-updates/
- Weaviate — Switching vectorizers:https://docs.weaviate.io/weaviate/tutorials/vectorizer-migration
- Milvus — Manage Aliases:https://milvus.io/docs/manage-aliases.md
- Pinecone — Configure an index, API 2026-04:https://docs.pinecone.io/reference/api/2026-04/control-plane/configure_index