2026年6月4日 周四晚上19:30,报名腾讯会议了解“业务抓夹如何成为前线部署工程师(FDE)”(限30人)
免费POC, 零成本试错
FDE知识库

FDE知识库

学习大模型的前沿技术与行业落地应用


我要投稿

万字深度|做了8年向量数据库后,我们决定为Milvus重构AI时代的存储引擎

发布日期:2026-06-02 18:35:42 浏览次数: 1566
作者:Zilliz

微信搜一搜,关注“Zilliz”

推荐语

我们为什么决定重构Milvus的存储引擎?因为AI时代的数据形态,已远超传统数据库的承载能力。

核心内容:
1. 传统存储模型在AI时代面临的三大典型问题
2. 全新Loon引擎如何通过混合格式等方案解决这些问题
3. 以多模态训练为例的AI数据生产场景剖析

杨芳贤
53AI创始人/腾讯云(TVP)最具价值专家
过去八九年,我们一直在做一件事:把向量数据库从一个很小众的系统方向,做成 AI 基础设施里的关键组件。
这中间我们听过很多建议,也得到了很多成长。
当然,也有很多人试图劝我们立刻原地倒闭,比如:传统数据库不是已经能存int、string、json了吗?现在不过多了一种 vector 字段。传统数据库加一个向量类型,再配一个ANN索引,你们就要完蛋了!
的确,很多传统数据库也是这么做的:加一列 vector,接一个索引,demo 能跑,也有很多企业,在语义检索初期,会选择这样的产品。
但跑到一定规模以后,就一定会发现传统数据库+补丁的方式,根本无法解决AI时代的新问题。向量不只是一种新的字段类型,更是一种新的数据形态。
比如,单从存储模型设计来看:
  • 它有表结构,有 row,有主键,所以像数据库。
  • 它规模巨大,要被 Spark、Ray、DuckDB、训练 pipeline 和评估系统反复读取和改写,所以像数据湖。
  • 它的原始对象往往是视频、图片、PDF、音频,继续留在 S3、GCS、OSS 里,所以也像对象存储。
但它还多了一层传统系统没有处理好的东西:embedding、sparse vector、caption、vector index、text index、delete log、stats、外部对象引用,以及这些对象之间不断变化的版本关系。
更不用提,当一份 AI 数据集持续演进、被多个系统共同读写、同时服务在线检索和离线分析时,旧的存储模型还能不能承担作为数据事实来源的责任。
因此,沿用传统数据库、数据湖、对象存储的任何一种传统管理方式,最后都会半路失灵。
接下来,本文将重点讲解传统存储模型在AI时代面临的三个典型问题:写放大、读路径冲突、多引擎协作
以及解决为了解决这些问题,我们是如何推出Loon存储引擎,用混合文件格式、row id 对齐和 Manifest 重新组织数据,解决海量向量数据的高效存储的。

01 

AI时代的生产场景是怎么使用数据的

我们最近和一个多模态训练数据团队聊过他们的工作流,大概是这样的:
video→ clips→ metadata→ aesthetic_score→ caption→ embedding→ search / dedup / training data filtering
一段长视频先上传到对象存储。pipeline 按“换镜头”“换场景”把它切成约 10 秒的片段,去掉过长、过短或者质量太低的片段。剩下的片段会跑美学打分,再让 caption 模型生成文本描述。最后给每个片段算一份 embedding,写进向量库,用来做去重、检索和训练数据筛选。
这个过程中,真正的麻烦其实体现在时间维度上。
  • 第一周:只跑了切帧和元数据,表里大致是 clip_id / video_id / start_offset / duration 这几列。
  • 第二周:加上美学打分,每个片段补一列 aesthetic_score。
  • 第三周:caption 模型跑通,每个片段补一列 caption,平均 200 字。
  • 第四周:第一版 embedding 上线,每个片段补一列 768 维 CLIP 向量。
  • 一个月后:换模型,重新算了一份 embedding_v2,1024 维。
  • 两个月后:要支持 hybrid search,补一列 sparse vector。
  • 三个月后:caption 做了一轮人工修订,要回填那一列。
发现没有,这份数据永远不会真正“写完”。这就是向量数据和传统业务数据的关键差异。
传统业务表里,一行订单写完以后,状态可能变化,但它不会每隔几周长出一个几 KB 的新字段
向量数据不是这样,模型会换,特征会加,caption 会修,标签会补,索引会重建。同一批 row 会不断被重新解释。
而且规模向量数据的规模通常不是百万级,多模态训练数据动辄亿级片段。参照 LAION-5B 这种 58 亿图文对的规模,非结构化训练数据从一开始就不是小数据。
所以,向量存储的压力不在第一次 insert,而在后续持续演进。
以下是几个典型需求与变化。

02 

需求一:降低长字段带来的写放大

我们先来分析写入和回填。
过去十年,lakehouse 生态里最常见的存储选择是 Parquet 这一类列存格式。它适合分析型负载:字段稳定,数据多读少改,扫描时只读需要的列,压缩率高,生态成熟。
问题是,向量数据打破了少改多读这个前提。
先看数据形状。
结构化数据里,TPC-H 是一个典型参照,过去几十年分析型存储格式基本都是围着它做优化的。它最大的表 lineitem 有 16 列,包括 4 个整数键、4 个 decimal、3 个日期、若干短字符串,还有一个 44 字节以内的 comment。一行未压缩大约 150 字节,压缩后通常在 60 到 80 字节之间。TPC-H 1GB scale 的 lineitem 大约 600 万行。
这就是 Parquet 这类分析型存储格式假设的世界。默认 64 MB row group,在这种行宽下可以装下 40 万到 100 万行;按列压缩后,一整列的物理体积通常在几 MB 到几十 MB。
非结构化数据经过向量化以后,形状完全不同。
LAION 是非结构化数据(图文对)经过向量化之后的代表,AI 工作流里很多 dataset 都是这个形状。LAION-5B 的每条记录包含一个 URL、一段 caption(几十到一百多字节)、几个数值字段(width、height、相似度、NSFW 标签等),加上一份 CLIP embedding。社区最常用的 ViT-L/14 是 768 维:fp16 存 1.5 KB,fp32 存 3 KB。
对比一下:

TPC-H lineitem

LAION-5B 风格

单行未压缩字节

~150

~1.5–3 KB

64 MB row group 装下的行

40 万–100 万

2 万–4 万

单看一行的字节数,LAION 就比 TPC-H 大了 20 倍上下,这 20 倍里,向量列就贡献了 95% 以上。如果换成 OpenAI text-embedding-3-large 这种 3072 维 fp32 embedding,一行向量直接到 12 KB。多模态训练里常见的 1024 到 2048 维 embedding,单条也有 4 到 8 KB。
视频切帧场景里,每个片段除了 embedding,还会有 caption、aesthetic_score、原始视频引用、parser 版本、模型版本。结构化字段不一定少,向量列却贡献了绝大部分体积。
行的形状一变,列存原来的账就算不下去。
Parquet 这一类格式的核心假设有两条:列基本不可变,schema 演进低频。在 TPC-H 这种“行宽 150 字节、列数固定”的世界里,这两条假设几乎不付代价。加一列就是写一个新文件,每个 row group 64 MB,总共也就几 GB——分析任务里几乎属于是背景噪声。改一行靠 delete log + compaction,重写的也只是 64 MB 这个量级,每行才 150 字节,搬几十万行,成本都还在可控范围内。
到向量数据这里,代价直接放大。
第一,加一列向量本身就是 TB 级工程
假设有 1 亿个 clip。给每个 clip 补一列 1024 维 fp32 embedding,原始向量数据就是约 400 GB。
这还只是数据本身。真实系统里还要写对象存储,更新一份 stats,建立索引,更新元数据,校验覆盖范围。
如果每个月新增一到两列向量字段,比如 embedding_v2、sparse_vector、rerank feature,那么每次 schema 演进都意味着一次大型数据工程。前文中那个团队每个月加一到两列向量类型的字段,意味着每个月有 TB 级的写入和回填。
第二,改一行也不再便宜
列存系统里,删除或修改旧数据通常不是原地改,而是通过 delete log 加 compaction 完成。删除日志写进去,compaction 时把活着的行搬到新文件。
列存里删除或更新旧数据,通常会暴力的简单重写文件(因为一行不大,重新生成数据——例如制作一张宽表——的成本不高)。在 TPC-H 那种列上,一行也就几十到几百字节,搬一批 row 成本也不算高。
但向量数据不一样。一条 768 维 fp32 embedding 就有 3 KB;1024 到 2048 维 embedding 常见于多模态场景,单条就是 4 到 8 KB;如果是 3072 维 fp32,单条可以到 12 KB。
这意味着,一次 caption 人工修订,表面上只是回填几百字节的文本;但在旧的统一存储布局里,它可能会触发上百万 row id 的物理重写。整体数据量可达 GB 级别,相当于大规模重新写入数据。
如果文本、向量和其他派生特征共享同一批物理文件或 compaction 生命周期,那么修一列文本,就可能让同一批 row 上的 embedding、embedding_v2、sparse_vector 也被顺带重写。几百字节的更新,最后变成 GB 级 I/O。
这就是为什么说写放大问题,在面对向量数据时需要被格外重视。
第三,加列和改行 在向量数据里是高频动作
传统分析表的 schema 可能一年改几次。向量数据不是。
caption 模型会升级,embedding 模型会换,sparse vector 会后补,rerank feature 会增加,人工标注会修订,数据治理会补标签。
这些动作都不是简单追加新行,每一件都同时是改一列和针对一批 row 重写。
lakehouse 生态过去十年的优化方向是读得更快,不是改得更便宜。当主要写入模式从append-only 追加新行变成高频回填已有 row 的某几列,原本可控的写放大就会被向量列的长度放大成主导成本。
所以一个矛盾就出现了:结构化数据上列存几乎没有写放大问题;同一种列存放在向量数据上,写放大却成了主导成本。

03 

需求二:一份数据既要能扫,也要能点查

写完以后,读路径也会分裂。
同一份向量数据,业务侧可以有两种完全不同的读法。
第一种是列式扫描。
比如:
WHERE aesthetic_score > 0.8 AND duration > 5
或者:
COUNTGROUP BY离线分析全量 embedding 评估BM25 统计bitmap 构建数据质量检查
这种读法的特点是:读很多行,每行只看少数几列。它喜欢连续 I/O、大 row group、列压缩、批量解码和向量化执行。
Parquet 很适合这条路径。
第二种是行级点查
ANN 检索返回一批 candidate row id 后,系统要回表拿读取:
captionembeddingrerank featurevideo_urimetadata
这种读法的特点是:读的行不多(几百到几千),但要按 row id 精确拿到几列。它需要低延迟随机访问,希望直接定位到某一行某一列,只读需要的 byte range,而不是拉一个完整 row group。
两种读法对底层格式的要求几乎相反。
  • 扫描希望 row group 大,多列在物理上挨在一起,做向量化执行省 cache miss。这样一次 I/O 可以拉很多连续数据,压缩率好,吞吐高。点查希望读取粒度小。最好不用依赖大 row group,而是能按 row id 找到 segment 或 byte range。
  • 扫描喜欢重压缩,因为数据量大,少传输很重要点查必须能在压缩块中间挑出一条记录,重压缩反而是负担。
  • 扫描可以靠并发掩盖对象存储延迟。点查在对象存储上很敏感,因为每一次随机读都可能变成一次远程 range GET。row group 越大、单条 row 的位置越模糊,读放大越严重。
标量要的是宽和压缩,向量要的是窄和点查。一个文件格式不可能同时满足这两种需求。
如果所有列都放进 Parquet,标量扫描会很舒服,但 ANN 召回后的向量回表会很麻烦。系统可能只想拿几百条向量,底层却要拉很多无关 row group。数据在本地 SSD 上时,还可以靠 cache 和 mmap 缓解。一旦数据上 S3,每一次 cache miss 都会变成远程读取,一次 ANN 查询触发几十 MB 的网络流量。一次召回要回表读 1000 条 row,可能要触发上千次 row group 拉取,每次几十 MB——总流量直接上 GB。
如果为了点查把 row group 切得很小,点查变好了,但扫描又变差。大量小片段会拖垮连续读和压缩效率。
更现实的是,hybrid search 会在同一次查询里同时触发两种读法。
先用标量字段过滤:
aesthetic_score > 0.8 AND duration > 5
再做 ANN 召回。
再按 row id 回表取 caption、vector 和 metadata。
也就是说,同一份数据在同一个查询里既像分析表,又像低延迟随机访问。同一份数据被同一个用户在 200 毫秒内两种读法各走一遍。
这不是调 row group size 能解决的问题。

04 

需求三:让外部系统围绕同一份数据工作

前两个问题发生在数据库内部:怎么写,怎么读。第三个问题发生在系统边界上:以视频切帧场景为例,它从头到尾几乎没有一个环节,只发生在向量数据库内部。
向量数据库只是整条链路中间的一段。它负责在线检索,但数据生产、数据修订、数据分析和数据治理都发生在外部。其中,原始视频在 S3 上;切帧在 PySpark 或者 Ray 里跑;美学打分用一个独立的 GPU 服务;caption 由一个 LLM inference pipeline 算;embedding 在另一个 GPU 池里算;sparse vector 在 SPLADE 服务上算。此外,检索完成后,检索结果要喂给训练数据筛选 pipeline;同一份数据要进入离线评估,跟人工标注比对;rerank model 要拿这批 vector 重新训练;caption 要做人工审核然后回填;数据治理要审计敏感内容。这些工作也都不在向量数据库里。
数据库本身只占整条链路的中间一段——服务在线检索。它前面是 ML pipeline,后面是分析、评估、治理、回填。如果向量数据库的物理格式只有自己能读写,那么每次外部系统参与,都要导出、转换、复制、再导入。
这就导致同一份 collection 可能同时存在于:数据库里、在 Spark 的临时目录里、在评估 pipeline 的本地拷贝里。那么,谁是 source of truth?哪一份对应的是上个月那个 caption 模型版本?人工修订之后,哪一批 row 已经被回填,哪一批还没?
数据小的时候,我们只需要付出一些额外的时间成本,靠 Excel 和命名规范解决;数据规模大到亿级片段、TB 级 embedding以后,它会变成一致性问题。这时候,能解决它的,只有存储格式:它要回答的,是当前这份数据集是什么版本、有哪些列、覆盖哪些 row range、哪些 index 还有效、哪些原始对象还活着。
这也是 lakehouse 生态过去十年解决的问题。从 Hive 表到 Iceberg、Delta Lake、Hudi,核心不是怎么把文件压得更小,而是多个引擎如何围绕同一份数据安全工作。
向量数据库现在也走到了同一个路口,只是它要管理的对象更多。除了列和分区,还包括了 vector index、text index、sparse feature、删除日志,以及指向原始视频的 Blob 引用。
所以问题不在于Spark 能不能读 Milvus 的文件。真正的问题是:Spark 在外部补完一列 sparse vector 以后,Milvus 怎么知道这列属于哪个版本,覆盖哪些 row,用了哪个模型,什么时候可以被线上查询使用。

05 

如何解决三大需求?需要重构表的格式

回头看这三个向量数据的特性以及对应的挑战。
  • 向量字段长又频繁演进——列存的写放大被放大。
  • 同一份数据上的两种读法天生相反——单一文件格式没法两边讨好。
  • 数据在数据库之外被参与改写——私有格式成了系统级的债务。
听起来是三个独立的工程问题。可以一个一个打补丁,比如写放大上一些 batching,访问模式上做一个 cache 层,外部协作做一个导出工具。这条路过去几年向量数据库行业基本都走过,但都走不远。
因为真正的问题是:一份向量数据集里住着有长有短、有冷有热、有适合扫描有适合点查的几种完全不同的数据,以视频切帧为例:
clip_id / video_id / duration是短标量字段,适合过滤和分析。caption是文本字段,可能要做 BM25、审核、修订和回填。embedding是长向量字段,ANN 召回后需要低延迟点查。embedding_v2是新模型产生的新向量列,可能后补。sparse_vector服务 hybrid search,访问模式又不同。raw video应该继续留在对象存储里,数据库只记录引用。vector index / text index / stats / delete log是围绕数据产生的派生结构和版本语义。
它们只是共享同一个 row id,但需要的处理方式天差地别。如果把它们硬当成一张普通表,写放大会失控;如果把它们硬塞进同一种列存格式,点查会付出高读放大;如果把它们当成一堆对象文件,多引擎协作会变成版本噩梦。
所以重新设计向量存储这件事,我们需要先承认一份向量数据集天生是异构的:
  • 混合文件格式承认不同列的访问模式不同。标量列适合扫描,向量列适合点查,原始大对象适合留在对象存储。
  • 行对齐承认这些列虽然物理上分开,但逻辑上仍然共享同一批 row。
  • Manifest 承认这份数据不是一次性写完的,而是会被多个系统、多个版本、多个任务持续修改。
基于这一判断,给现有引擎打补丁这条路已经走不通,我们需要的是一套全新的存储引擎。

06  

我们推出了Loon ,一举解决三大困境

这个全新的存储模型。落到设计上,其实就是三个动作:把不同列放进不同物理格式(混合文件格式),让分散的列在逻辑上拼回一张表(行对齐),用一份声明出来的元数据描述整个数据集(Manifest)
能力上,它要允许一张逻辑 collection 在物理上拆成多个列组;允许每个列组选择合适的文件格式;允许这些列组通过 row id 对齐;允许索引、stats、delete log、外部对象引用都进入同一个版本视图;允许外部计算系统围绕同一份数据安全写入和读取。
为此,我们推出了Loon,新一代存储引擎。
它延续了 Zilliz 的鸟类命名传统:Loon 是生活在湖泊上的潜水鸟。这个名字直接对应技术目标:向量数据库不该每次查询、回填、建索引都搬起整片数据湖;它应该先看清当前数据集由哪些版本、列、索引和对象引用组成,再潜下去读取真正需要的那一小部分。
它替换的是 Milvus 原有的 segment binlog 存储层。上层组件——Proxy 路由、QueryCoord/DataCoord 调度、IndexNode 索引构建——接口没变;改变的是 DataNode(写路径)、QueryNode 与 segcore(读路径)、Compaction,以及外部 Spark / Ray connector,都改为通过同一份 stoRAGe abstraction 落到 Loon。
架构上,Loon 是分层的。从上到下依次是:
Manifest 是数据集的版本视图;ColumnGroup 把逻辑表映射到符合访问模式的物理布局;格式层支持四种物理格式按列组挑选;下面是统一的 Filesystem 抽象。

07 

优化一:混合文件格式

既然不同列的访问模式不同,就不应该再用一种文件格式管理所有列。
一份向量数据集应该按访问模式拆成多个 ColumnGroup。
第一类是标量列、过滤字段、业务键、统计字段。这些字段经常用于过滤、扫描、聚合、分析,喜欢宽 row group、重压缩、向量化执行。Parquet 的压缩、列裁剪和生态互通正好适合它们。
第二类是向量列、稀疏特征、rerank feature。访问模式是 ANN 召回后的行级点查,要的是低延迟随机访问、按 byte range 拉取、按需解码。这一组适合走段式(segment-based)的格式。Vortex 是当前正在成熟的一个方向,每段都有 offset 和 length,对象存储上可以直接 range GET。
第三类是原始大对象(原始视频、PDF、图片)。这些对象本身不应该进入向量数据库的数据文件。它们应该继续留在对象存储里。数据库里记录的是 URI、checksum、mime type、parser version 和对应关系。
对上层应用来说,它仍然是一张 clips 表。
对存储层来说,不同列住在不同物理格式里。
原则只有一个:用对的格式装对的列。
具体实践中,Loon 目前支持这几种文件格式:
  • Parquet——标量列、过滤字段、生态互通。
  • Vortex——向量、稀疏特征。当前用社区开源的。
  • Lance(只读)——兼容已有 Lance 数据集,直接 mount 成一个 ColumnGroup。
视频切帧场景对应过来:clip_id / video_id / start_offset / duration / aesthetic_score 进 Parquet 列组;caption 也进 Parquet 列组;embedding、embedding_v2、sparse_vector 各自进段式列组;原始视频文件还在 S3,每个 clip_id 对应一个引用。加 embedding_v2 只是新增一个段式列组、登记 row id 范围、commit Manifest——既不动 caption 列组里的字段,也不动 embedding 列组里已有的 400 GB 向量。
Parquet 这一边,Loon 没有用社区默认配置:row group 收紧到 1 MB(默认 64 MB 在向量列上等于每次回表都至少拉 64 MB),向量列上关掉 dict 等对随机数据没收益的编码。这些调整加起来,比起社区默认行为已经是相当大的优化。
Vortex优化才是真正为向量存储做的重点工作。我们基于社区开源 Vortex 实现了一个行存与列存平衡的 layout:同一 row group 内各列的 segment 在文件中物理相邻,scan 时可以连续读;新增 sub-segment 读取,让 take 只拉 segment 内部的一段字节而不是整段。
向量列上的三组关键数字对比如下:

动作

Vortex

Parquet

Vortex 优势

Take(索引回表,K=1000 随机行)

5.8 ms

144 ms

25×

Scan(向量列全表扫描)

21 ms

142 ms

6.76×

文件大小(约 21 MB 原始数据)

6.62 MB

7.16 MB

小 7%

三组差距背后是不同的存储机制:Take 的优势来自新的 layout 只需要访问命中的 row group 内部数据,而 Parquet 则要解析命中行所在的整个 row group;Scan 的优势来自 vortex 压缩算法和工程实现的优化。
[1] 测试环境:8 vCPU Ubuntu 22.04 KVM。单文件 40,000 行,schema 为{id: int64, name: utf8, value: float64, vector: list[128]},row group 1 MB。基于本地文件系统测试。S3 上的性能由网络 IO 主导,Vortex 段式布局的读放大更小,相对 Parquet 的优势会被进一步放大。

08 

优化二:行对齐

混合文件格式解决了怎么拆。拆开以后,还要回答一个问题:怎么把它们重新看成一张表?
答案是 row id。
每个列组在元数据里登记自己覆盖的 row id 范围——start_index / end_index。同一个 row id 空间里,多个列组分别管一部分列。
这样,row id = 12345 的字段可以分散在多个文件、多个格式、多个列组里,但逻辑上通过 row id仍然是一行。
这带来几个能力升级。
第一,加列不动原始列
新增 embedding_v2 时,只需要写一个新的向量列组,并登记它覆盖哪些 row id。老的 caption、metadata、embedding_v1 不需要重写。
第二,删除可以先进入 delete log
当前版本里哪些 row 不可见,可以先由 delete log 表达。真正的物理清理推迟到 compaction阶段。更重要的是,compaction 可以只触及相关列组,而不是每次都搬动整张表。
第三,hybrid search 可以稳定回表
ANN 召回 row id 后,系统可以从不同列组里分别取 caption、metadata、vector、video URI,再拼成结果。
row id 在这里不是业务主键,更像是存储层的坐标系。没有这个坐标系,混合格式只是一堆散文件。有了 row id,对象存储上的多个文件、多种格式、多个列组才能重新组成一张逻辑 collection。
要解决以上三个行对齐需求,落到Loon里,我们主要做了两件事。
ColumnGroupFile 的结构——每个物理文件登记 path / start_index / end_index 三个数字(外加一段可选 metadata)。start_index / end_index 是行对齐的全部秘密:不同列组、不同文件、不同格式,通过同一套 row id 范围对齐。
Packed Reader——上层读到的是统一的 Arrow RecordBatch stream,下面可能跨好几个列组。Packed Reader 干三件事:屏蔽下层格式差异(不管下面是 Parquet 还是别的列格式);按 row id 范围把多个列组拼齐;用最小堆调度多文件 I/O 控制内存。它还有另一个入口——按 row id 直接 take,定位到对应的 ColumnGroupFile,发起 range read 拿到具体数据。
视频切帧场景下,ANN 召回回表读 caption + embedding + 视频引用:Packed Reader 拆成两次 range GET(一次标量列组、一次向量列组),不动其他列组任何字节。

09 

优化三:Manifest

混合文件格式解决物理布局,row id 解决逻辑对齐,Manifest 解决的则是事实来源问题。
当一份数据横跨多个文件、多种格式、多个版本、多个写入者时,目录结构已经不能再说明真相。
对象存储目录里可能有:旧版本文件、新版本文件、失败任务留下的文件、仍被旧 snapshot 引用的文件、还没清理的 delete log、临时输出文件,文件存在,不代表它属于当前版本。
所以需要 Manifest。
Manifest 描述的是数据集在某个版本上长什么样:
  • 有哪些列组、各自覆盖什么 row id 范围、用什么格式、文件落在哪。
  • 有哪些 delta log 生效,是按主键删(PRIMARY_KEY)、按位置删(POSITIONAL),还是按值删(EQUALITY)。
  • 有哪些统计可用——bloom filter、min/max。
  • 有哪些索引覆盖到哪些列。
  • 引用了哪些外部 Blob,对应哪个 source object 版本。
Manifest 的形式是一个版本号 + 一份二进制结构,每次更新就是写一个新版本的 Manifest 文件。读者读到哪一版 Manifest,就看到哪一版数据集——读者之间互不打扰。
这层优化的核心关键不在于结构本身,而在于谁能写
如果只有数据库内部能写 Manifest,那它仍然是一份内部元数据,只是换了个名字。但如果 Manifest 的写入路径是开放的——Spark 跑完一列 sparse vector backfill 之后能生成新的列组、新的 stats、新的 Manifest,再通过乐观并发提交进同一个版本视图——那它就成了多引擎协作的接口。在线查询继续读旧版本不受影响;提交成功之后,新版本立刻成为事实来源。
这套思路和 Iceberg、Delta Lake 类似。但向量数据库的 Manifest 管得更多。不只是表文件和分区,还包括 vector index、text index、sparse feature、delete log、stats、Blob 引用和 row id 范围。
一定程度上,没有 Manifest,多格式、多列组、多引擎写入都会退化成文件约定和人工对账。而人工对账在 TB 级向量数据面前没有未来。
具体来说,每个 Loon 数据集在对象存储上的目录是这样组织的:_metadata/ 放版本化的 Manifest 文件、_data/ 放列组数据文件、_delta/ 放删除日志、_stats/ 放统计文件、_index/ 放索引文件。这几个目录里的内容是否属于当前版本,全部由当前 Manifest 决定——读者不去 list 目录推测。
Manifest body 用 Apache Avro 编码,按顺序排四块:
  • ColumnGroups——列、格式、文件、覆盖的 row id 范围。
  • DeltaLogs——删除日志。三种语义对应三种典型来源:PRIMARY_KEY(客户端 delete)、POSITIONAL(compaction 内部位置删)、EQUALITY(Spark 等外部引擎按 predicate 删)。
  • Stats——bloom filter、BM25、min/max 这一类。查询规划器在打开数据文件之前先用 Stats 做 pruning。
  • Indexes——每个 index 记录覆盖列、类型(HNSW、IVF、inverted、bitmap 等)、参数、覆盖的 row id 范围。
每提交一次写一份新版本,旧版本继续在那里。这条性质让乐观并发提交能用最小语义实现:写者基于版本 N 生成新内容,提交时写入 manifest-{N+1}.avro——通过对象存储的 conditional write / generation match 能力保证"该版本已存在就失败"。冲突重试的代价远低于走强一致 CAS。

10 

Loon 现在走到了哪一步,未来会走向何方?

Loon 目前已经进入 Milvus 主干。已经完成的部分,主要集中在三层:
第一层是 Manifest。
Manifest 已经完成,包含 ColumnGroups、DeltaLogs、Stats、Indexes 四个 section。每次写入、回填、删除、索引更新,都会形成新的版本视图。写者基于旧版本生成新内容,再通过乐观并发提交新的 Manifest。读者只需要读到某一个 Manifest 版本,就能看到一份稳定的数据集。
第二层是 ColumnGroup 和格式层。
文件格式方面,Parquet 、Vortex 已经支持,我们会逐渐用 Vortex 替换 Parquet ,最终成为默认的格式;Lance 以只读方式接入,用来兼容已有 Lance 数据集。
第三层是 Index on Lake。
标量 stats、过滤索引、文本倒排索引已经可以按 row range 进入 Manifest 规划。向量索引的 lake-native 路径还在继续推进。HNSW / IVF 这类索引在对象存储上的访问模式不同,尤其是 HNSW 对随机访问和缓存更敏感,不能简单照搬本地 SSD 上的布局。
发布节奏会跟随 Milvus release。具体启用版本和能力边界,以对应 release note 为准。
放眼未来,我们还将在四个方面,做重点优化升级:
多引擎写入 SDK——Spark / Ray 端给出 first-class 的 Manifest 写入路径,让外部 backfill 不需要走 C/C++ 接口。
Iceberg / Delta Lake 互通——把向量列组对接到 lakehouse 的现有 catalog 体系,让 Trino / DuckDB / Athena 这一族引擎能围绕同一份数据工作。
索引支持——图索引在对象存储上的延迟特性和 IVF 不同,需要单独的 layout + 缓存策略。这是当前最直接的瓶颈:HNSW 在线召回还吃本地缓存,对象存储优势暂时主要兑现在 batch 路径上。
LOB类型支持——原始视频、PDF、音频等大对象的引用、版本化、删除连带语义。

尾声 

如何使用Loon

对应用层来说,Loon 不应该成为一个新的概念负担。
因此,在我们的设计中,collection、insert、search、hybrid search 这些 API 表面不会变。应用不需要知道 Manifest、ColumnGroup、row id range这些内部概念。
变化发生在存储层。
新建 collection 时,在启用 Loon 的 Milvus 版本里,Loon 会作为默认存储引擎,对用户透明。
存量 Milvus 2.x collection 可以通过迁移工具接入。迁移过程走后台 compaction,把旧存储布局逐步转换为 Loon 的列组布局,业务层读写接口保持不变。
Zilliz Cloud 里的 Vector Lakebase 也是基于这套存储模型构建。同一份数据集可以被线上 ANN 查询消费,也可以被 Spark / Ray 等外部引擎围绕同一份 Manifest 协作。它会随着 Milvus 版本在新集群上逐步默认启用。
换句话说,Loon 对用户暴露的不是一组新 API,而是一组原来很难做、现在可以自然发生的动作。
看几个最常见的动作,会更直观。
以前给一批老数据补一列 embedding_v2,要导出旧 collection,跑模型,生成新向量,再把数据重新导入或者通过 SDK 批量 update。中间要处理版本、失败重试、索引重建,还要尽量不影响线上查询。在 Loon 里,这件事变成:通过 schema evolution 添加字段;物理上写一个新列组,老列零搬动
以前对老 row 做 backfill,往往要走 client SDK 批量 update,写放大严重;在 Loon 里,Spark / Ray 这样的外部引擎可以直接生成新的列组和 stats,再通过 Manifest 提交。数据库不再是所有数据改写的唯一入口,而是和外部计算系统共享同一份版本视图。
以前做离线评估或数据分析,往往要先 dump 到 Parquet。线上 collection 是一份,分析目录里又是一份。只要中间有人工修订、模型换版、delete log 没同步,就会出现到底哪份数据是准的的问题。在 Loon 里,分析引擎可以直接读 Manifest 和相关列组。只读需要的 projection,只扫需要的 row range。
以前删除一批 row,也会遇到类似问题。delete log 可以先表达逻辑删除,但 compaction 时常常会牵动整批数据。只要 caption、embedding、sparse vector 被绑在同一个生命周期里,删改短字段也可能拖着长向量一起重写。在 Loon 里,delete log 仍然存在,但 compaction 可以只触及相关列组。没有被影响的列组不需要重写。这样,删除、回填、修订这些日常动作不会每次都演变成一次大规模数据搬迁。
更重要的是,它把很多过去需要 ops、ETL、停机窗口和人工对账的基础设施级动作,变成了围绕 Manifest 的普通 commit。而创新的加速,也往往是从这些看似不起眼地方开始的。

53AI,企业落地大模型首选服务商

产品:场景落地咨询+大模型应用平台+行业解决方案

承诺:免费POC验证,效果达标后再合作。零风险落地应用大模型,已交付160+中大型企业

联系我们

售前咨询
186 6662 7370
预约演示
185 8882 0121

微信扫码

添加专属顾问

回到顶部

加载中...

扫码咨询