跳到主要内容

单向量版本标签:每个 Embedding 迁移背后的缺失列

· 阅读需 11 分钟
Tian Pan
Software Engineer

一个新的嵌入模型发布了。基准测试数据提升了 4 %。一位 Staff 工程师提交了一个工单:“将 embedding 升级到 v3。”两周后,索引已完成重新嵌入,别名已切换,团队通过特性标志(feature flag)发布了变更。六周后,支持工单堆积如山。搜索结果“感觉不对劲”。复盘会召开了。没人能解释为什么出现了退化,因为没有系统崩溃,每个仪表盘显示的都是绿色。

问题不在于模型的更换。问题在于向量存储根本不知道哪些向量来自哪个模型。数据库里没有这一列。没有用于追踪哪些记录已回填的迁移表。没有 alembic_version 行,没有 schema_migrations 表,也没有先前状态的 pg_dump。团队将 embedding 升级视为一次简单的配置切换,而向量存储在模式(schema)层面缺乏能阻止他们犯错的概念。

Embedding 迁移需要数据库迁移二十年来一直依赖的相同产物:写入每个向量、在每次查询时检索、并作为切换和回滚准入准则的单条记录版本标签。这是大多数团队最容易忘记添加的一列,而后期补救的成本远高于前期添加。

Postgres 教会了我们,但向量存储却忘记了

关系型数据库很早就拥有了迁移工具。对于大多数变更,ALTER TABLE 是在线完成的。像 pg_dumpgh-ostpt-online-schema-change、Liquibase、Flyway 和 Alembic 这样的工具为工程师提供了版本化、可重复且具备回滚意识的模式变更。每次迁移都带有版本号。每一行都知道自己符合哪种模式。工具链假设你至少会搞砸一次,并需要优雅地撤回。

向量存储在发布时并未提供这些功能。没有 ALTER VECTOR TABLE 来原地重新嵌入行。没有迁移框架来追踪已处理的行。除了“从头开始重建整个索引”之外,没有“切换嵌入模型”的回滚语义。Pinecone、Weaviate、Qdrant、Milvus 和 pgvector 都提供了存储带元数据向量的原语,但它们都把迁移规范留给应用团队作为“家庭作业”。

大多数团队直到踩坑才意识到这个差距。他们把向量存储当作一个“只管写入”的 KV 存储。他们对语料库进行一次嵌入,写入向量,然后进行查询。当更好的模型出现时,团队发现向量存储实际支持的唯一操作是“删除并重写”。没有在线模式变更。没有滚动切换。没有回填机制。于是团队在时间压力下仓促地构建了一个糟糕的方案并发布。

这种规范并不神秘。这与一位资深后端工程师在修改拥有 5000 万行数据的表列类型时所采用的规范是一样的。它之所以没被采用,是因为没人将这项工作标记为模式迁移,因为迁移所需的模式列缺失了。

每个向量的版本标签是你遗忘的那一列

出发点平淡而具体:你写入的每个向量都需要在其元数据中包含一个版本标签。不是在索引上,也不是在命名空间上,而是在行上。该标签标识产生它的嵌入模型——模型名称、模型版本以及任何发生变化的预处理参数(分块大小、归一化、提示词模板)。一个合理的格式是像 [email protected]/chunk-512/norm-l2 这样的短字符串。Pinecone、Weaviate、Qdrant 和 pgvector 都支持记录上的任意元数据;添加该字段的成本几乎为零,而以后由于缺少它所付出的成本大约是一个工程师月。

一旦每个向量都有了标签,几件以前不可能的事情就变得可行了:

  • 读取时过滤。查询可以限定在特定的 embedding 版本。在切换期间,生产流量查询新标签,而一小部分验证流量同时查询两者并进行对比。
  • 混合状态安全性。如果回填完成了一半,检索层知道哪些向量使用的是新“语言”,哪些仍在使用旧“语言”。它不会静默地混合它们并产生不连贯的近邻结果。
  • 单条记录回填追踪。迁移作业可以通过 SELECT WHERE version = 'v1' 来查找剩余内容。不需要维护额外的同步账本。向量存储本身就是账本。
  • 无需重新嵌入的回滚。如果新模型发生退化,将读取操作切回旧标签只需修改配置。旧向量依然存在,没有被删除。回滚时间受限于特性标志的延迟,而不是重新嵌入的成本。
加载中…
References:Let's stay in touch and Follow me for more thoughts and updates