Apache Parquet FILE Logical Type 如何为 AI 数据湖统一多模态对象

生成式 AI 正在改变数据湖里“数据”的含义,以前我们面对的数据都是数值、字符串、日期或嵌套结构;如今多模态训练、文档理解、语音分析等工作负载,要求数据湖同时管理图片、音频、视频、PDF 以及模型文件。这些对象通常体积较大,存储在对象存储中,但又需要和标签、业务属性、数据版本、训练快照一起被查询和治理。

过去,工程团队通常只有两种选择:

  • 把内容作为 BINARY 直接写进 Parquet。
  • 在 Parquet 中保存一个指向对象存储的字符串路径。

第一种方式容易让大对象进入 Parquet 的 Page 和 Row Group,会影响扫描、内存占用和文件布局。AI 时代,数据库需要具备什么样的能力?这篇文章有具体提到怎么个影响法,这里不过多阐述。

第二种方式虽然轻量,却没有标准语义——Reader 不知道字符串是不是文件、是否只引用其中一段、如何判断内容类型、如何校验内容。总的来说就是没有个标准,各个业务团队各自定义各自的语义。

Apache Parquet 的新 FILE Logical Type,试图解决的正是这个问题。

它的价值不是简单地“把文件塞进 Parquet”,而是让 Parquet 生态第一次拥有一种统一、可互操作的非结构化字节对象语义:内容可以直接内嵌,也可以放在当前 Parquet 文件的其他位置,还可以位于外部对象存储;对上层系统而言,它们都是同一种 FILE

这一标准目前已经在 PR 585 里面被正式合入了。不过规范刚落地,目前只有 parquet-java(PR 3608)和 arrow-rs(PR 10109)的 PoC 实现,各查询引擎的支持还需要时间。

FILE 不是一种新的物理编码

Parquet 很聪明,没有增加新的 encoding 格式,而是选择直接复用了 STRUCT 能力,然后在这个 STRUCT group 之上,标注了 FILE 逻辑类型。这样大大提升了兼容性,且降低了现有引擎的适配难度。

当前规范中的完整定义如下:

optional group my_file (FILE) {
  optional binary uri          (STRING);
  optional int64  offset;
  optional int64  size;
  optional binary content_type (STRING);
  optional binary checksum     (STRING);
  optional binary inline;
}

从物理视角看,它仍然只是六个普通叶子列:

my_file
├── uri
├── offset
├── size
├── content_type
├── checksum
└── inline

FILE annotation 告诉 Reader:这些字段不是任意业务 Struct,而是共同描述一段可解析的字节。

所有字段都是 optional,Writer 不必定义自己永远不会使用的字段。例如,一个只保存外部对象引用的 Schema 可以只有 uricontent_typechecksum;一个只内嵌小对象的 Schema,可以只有 inlinecontent_type

但“字段都是 optional”不等于“任何组合都合法”。FILE 值必须最终解析到一段确定的字节,否则它就是非法引用。官方 FILE 规范 对字段组合和 Reader 行为做了明确约束。

FILE 三种存储方式

FILE 的核心设计,是把非结构化对象的三种存储方式统一成同一个逻辑类型:

                    FILE
                      │
         ┌────────────┼────────────┐
         ▼            ▼            ▼
       inline      self-reference  external
    字节位于叶子列   字节位于当前文件  字节位于外部 URI

1. Inline:内容直接进入 Parquet 列

inline       = <PNG bytes>
content_type = "image/png"

inline 是一个普通 BYTE_ARRAY 叶子列,因此会和其他列一样进入 Parquet Page,并使用该 column chunk 配置的编码和压缩方式。

它适合小图片、短文本、小型附件等对象。优点是数据完全自包含,Reader 不需要额外访问对象存储;代价是大对象可能干扰 Row Group 大小、解码内存和投影扫描。

2. External:指向外部对象

uri          = "s3://datasets/images/a.png"
content_type = "image/png"
checksum     = "SHA-256:0123..."

checksum 的格式为 <algorithm>:<digest>,规范支持 ETAG、MD5、CRC32、CRC32C 和 SHA-256 五种算法。其中 ETAG 对对象存储场景特别友好——可以直接复用 S3 返回的 ETag 作为完整性标识,不需要重新计算哈希。

也可以配合 offsetsize,表示对象中的一个切片:

uri    = "s3://datasets/packed/blobs-001"
offset = 1048576
size   = 65536

对应的字节范围是:

[1048576, 1048576 + 65536)

这种表示适合把许多小对象打包进少量大对象,以减少对象存储 LIST、HEAD、GET 请求和小文件数量。

3. Self-reference:引用当前 Parquet 文件的一段区域

如果 uri 缺失,而 offsetsize 存在,则引用目标是当前 Parquet 文件自身:

uri    = null
offset = 8388608
size   = 131072

其含义是:

current_parquet_file[8388608 : 8388608 + 131072]

这些字节不必进入正常的 Parquet 列页;Writer 可以把 blob 放在文件中的其他位置,再通过绝对字节偏移引用它。

Self-reference 的重要性质是可移动性。因为引用中没有写入当前文件的绝对路径,所以整个 Parquet 文件被重命名或搬迁后,内部引用仍然成立。个人觉得这个不错,这样相当于大对象和 Parquet 文件是一个原子单元,有效避免对象存储里面的文件被删了,Parquet 里面还引用着(dangling reference)。

需要注意的是,规范明确规定含 self-reference 的文件不能使用 Parquet modular encryption——这些字节游离在正常的列加密体系之外,无法被列加密覆盖。加密场景下需要避开这种存储方式。

Reader 的解析规则

最终规范可以近似写成下面的读取流程:

if inline is set:
    return inline

if uri is set:
    if offset is set:
        require size
        return read(uri, offset, size)

    if size is set:
        return read(uri, 0, size)

    return read_whole_object(uri)

if offset is set:
    require size
    return read(current_parquet_file, offset, size)

invalid FILE value

完整组合如下:

inlineurioffsetsize解析结果
使用 inline
整个外部对象
外部对象 [0, size)
外部对象 [offset, offset+size)
当前 Parquet 文件中的字节范围
非法
非法
非法,如果想表达这一行为 null,应该是在外面的 struct 这一层,设置为 null(def_level=0)
兼容性表格

这里有两个容易误解的地方。

1、只要 inline 存在,Reader 就必须优先使用它。即使同一值还带有 urioffsetsize,这些定位字段也只能作为来源信息。Writer 可以选择不同时写入两者(规范允许将其视为互斥),但 Reader 不能忽略 inline 去读取外部对象。

2、只要设置 offset,就必须同时设置 size。设计稿一度允许“从 offset 读到 EOF”,但最终规范明确将 uri + offset 且无 size 定义为非法。这避免不同 Reader 对边界作出不同推断,也使所有范围引用都有确定长度。

FILE 给 AI 数据湖带来了什么

1. 可查询的多模态文件清单

一张训练数据表可以同时包含业务属性与标准 FILE 列:

id | tenant | label | asset(FILE)

查询引擎可以只读取 asset.content_typeasset.sizeasset.uri 等叶子列,完成:

  • 按图片、音频或 PDF 过滤;
  • 统计训练集不同模态的数量与大小;
  • 找出 checksum 缺失或引用异常的数据;
  • 构建训练 manifest;
  • 在真正需要时才获取大对象内容。

这保留了 Parquet 列式投影的优势:检查和筛选文件元数据时,不必读取 inline 或访问外部对象。

2. 小对象打包

对象存储中的海量小文件会带来请求开销、元数据压力和较差的吞吐。FILE 可以让多个逻辑对象共享一个大容器:

uri    = "s3://bucket/packs/part-0001.blob"
offset = 8192
size   = 32768

每行仍代表独立逻辑文件,并拥有自己的 content_type 和 checksum。查询先在 Parquet 中筛选,再对容器执行有针对性的范围读取。

self-reference 更进一步:容器字节和索引可以处于同一个 Parquet 文件中,形成可整体搬迁的单文件数据包。

3. 稳定的训练输入清单

FILE 允许训练系统把 URI、范围、内容类型和完整性标识与数据集快照一起记录。结合 Iceberg、Delta 或其他表格式的 snapshot,训练任务能够准确回答“某个模型使用了哪些文件引用”。

4. 跨引擎互操作基础

FILE 统一了标准,避免每个引擎都去发明自己的 STRUCT<path, mime, etag>

说到这里,我感觉 Paimon 惨了。Paimon 的 Blob 是在表格式层自己造的私有方案——把大对象写进单独的 blob 文件,由 Paimon 自己的元数据来管理引用——完全不如 Parquet 直接在文件格式层定义标准来得优雅,目前感觉是白实现了,还和 Parquet 标准打架。

结语

FILE Logical Type 表面上只增加了一个空的 Thrift annotation 和六个约定字段,背后解决的却是 AI 数据基础设施中的一个根本问题:

当数据不再只是数字和字符串,而是散落在列页、Parquet 文件内部和对象存储中的图片、音频、视频与文档时,开放格式应该如何一致地表达“这个值是一份文件”?

Parquet 的答案不是设计一种更复杂的 blob 编码,而是建立一层最小但完整的语义契约:

  • inlineself-referenceexternal 是同一个类型的不同存储形态;
  • uri / offset / size 决定字节位于哪里;
  • content_type 决定字节代表什么;
  • checksum 提供完整性标识;
  • Reader 和 Writer 对合法组合拥有共同理解。

这让 Parquet 保持了物理格式上的克制,同时向多模态数据迈出了关键一步。

总的来说,在 AI 时代,Parquet 这位“老人”显然并没有固步自封,FlatBuffers(用其重写 footer 元数据以加速解析的提案)、ALP、Fixed Size List 以及 FILE,无不彰显着 Parquet 想在 AI 时代,继续占领开放文件格式的山头。如果后面 Iceberg 把索引框架标准制定完,那么 Iceberg + Parquet 组合很有可能会成为 AI 数据湖的标准答案,这定会蚕食 Lance 和 Paimon 的一部分市场。

当然,格式垄断肯定是利好数据库从业者了,开发省心省力,做优化也能更有针对性。

3 条评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注