Apache GraphAr 格式规格与元数据详解
逐字段解读 Apache GraphAr 的三层元数据 YAML(GraphInfo/VertexInfo/EdgeInfo)与 Parquet 数据文件组织:chunk 切分、offset 索引、vertex_count 计数、prefix 命名规则及 CSR/CSC/COO 邻接表布局对比。
Apache GraphAr 格式规格与元数据详解
目标:基于真实产出的 GraphAr 数据集,逐字段解读三层元数据 YAML(GraphInfo/VertexInfo/EdgeInfo),并解析数据侧的 Parquet chunk 组织、offset 索引、计数文件与 prefix 命名规则。所有文件内容与回显均来自实测环境(Ubuntu 22.04,Python 3.10.12,graphar 0.13.0.dev1)中真实写出的数据。
一、环境信息
本章解读对象是两份真实 GraphAr 数据集,均在同一实测环境中产生与校验:
- 操作系统:Ubuntu 22.04.5 LTS(x86_64),Python 3.10.12
- Apache GraphAr:0.13.0.dev1(PyPI
graphar包,含 C++ 核心库与 CLI 工具) - 实测样本一:自建小图(3 个 person 顶点、4 条 knows 边),产出目录
/root/graphar-demo,经graphar check校验通过(回显INFO Graph is valid) - 实测样本二:官方测试数据 ldbc_sample(903 个顶点、6626 条边),目录
/opt/incubator-graphar/testing/ldbc_sample/parquet/ - Parquet 检查工具:独立虚拟环境安装 pyarrow 25.0.1,用于打开数据文件核对内部列
版本与校验状态的复验命令与实测回显:
graphar --version && graphar check -p /root/graphar-demo/demo_graph.graph.yml
GraphAr CLI Version: 0.13.0.dev1
INFO Graph is valid
💡 说明:
graphar check是解读一份未知 GraphAr 数据前最常用的第一步——先确认元数据自洽,再逐文件解析。
二、格式总览与实测样本
2.1 三层元数据 + 数据文件的模型
GraphAr 把一张图拆成「三层元数据 YAML + 一组数据文件」:
| 层 | 元数据文件 | 描述内容 | 数据文件 |
|---|---|---|---|
| 图 | <图名>.graph.yml(GraphInfo) | 图名、根前缀、版本、顶点/边子文件引用 | 无 |
| 顶点类型 | <类型名>.vertex.yaml(VertexInfo) | 类型名、chunk 大小、属性分组、文件类型 | vertex/<类型>/... 下的 Parquet/CSV 文件 |
| 边类型 | <源>_<边>_<目的>.edge.yaml(EdgeInfo) | 源/边/目的类型、三种 chunk 大小、邻接表布局、属性分组 | edge/<源>_<边>_<目的>/... 下的文件 |
三个关键设计:元数据与数据分离(YAML 纯描述,数据纯列存);顶点与边都按 chunk 切分(并行读写的最小单位);边按邻接表布局组织(决定遍历方向与性能)。
2.2 实测样本全景
自建小图的完整文件清单(实测):
find /root/graphar-demo -type f | sort
/root/graphar-demo/demo_graph.graph.yml
/root/graphar-demo/edge/person_knows_person/ordered_by_dest/adj_list/part0/chunk0
/root/graphar-demo/edge/person_knows_person/ordered_by_dest/creationDate/part0/chunk0
/root/graphar-demo/edge/person_knows_person/ordered_by_dest/edge_count0
/root/graphar-demo/edge/person_knows_person/ordered_by_dest/offset/chunk0
/root/graphar-demo/edge/person_knows_person/ordered_by_dest/vertex_count
/root/graphar-demo/person.vertex.yaml
/root/graphar-demo/person_knows_person.edge.yaml
/root/graphar-demo/vertex/person/firstName_lastName_gender/chunk0
/root/graphar-demo/vertex/person/id/chunk0
/root/graphar-demo/vertex/person/vertex_count
下面三章按「图 → 顶点 → 边」逐层解读这份样本的三层 YAML,第六章解析数据文件。
三、GraphInfo:图级元数据详解
3.1 实测 YAML 全文
自建图的图级元数据 demo_graph.graph.yml 实测内容:
name: demo_graph
prefix: /root/graphar-demo/
vertices:
- person.vertex.yaml
edges:
- person_knows_person.edge.yaml
version: gar/v1
3.2 字段逐项说明
| 字段 | 实测取值 | 含义与要点 |
|---|---|---|
| name | demo_graph | 图名,读取侧 GraphInfo.load() 后用 get_name() 获取 |
| prefix | /root/graphar-demo/ | 数据根前缀。Builder 写出与 Collection 读回都基于它做路径拼接,必须与实际目录一致且带尾斜杠(自建图实测踩坑点,见《Apache GraphAr 数据读写实战详解》第七章) |
| vertices | person.vertex.yaml 列表 | 顶点子元数据文件引用,相对 graph.yml 所在目录解析 |
| edges | person_knows_person.edge.yaml 列表 | 边子元数据文件引用,相对 graph.yml 所在目录解析 |
| version | gar/v1 | 格式版本号,当前规范统一为 gar/v1 |
3.3 引用与解析规则(实测确认)
三个实测确认的解析行为,直接影响手工组织数据目录:
- 子文件引用相对 graph.yml 所在目录解析:官方 ldbc_sample 的
ldbc_sample.graph.yml没有写 prefix 字段,加载后前缀自动取 graph.yml 所在目录(实测graphar show输出prefix: "/opt/incubator-graphar/testing/ldbc_sample/parquet/"); - save() 固定生成 .yaml 后缀引用:SDK 的
GraphInfo.save()写出的引用固定是<类型名>.vertex.yaml、<源>_<边>_<目的>.edge.yaml;官方测试数据用的是.yml后缀也能正常加载(加载端两种后缀都认),但自建图必须按 save() 的 .yaml 命名存放子文件,否则重新 load 报ValueError: IOError: Failed to open local file ...(实测排障记录见实战篇); - show 输出统一为 .yaml 形态:
graphar show打印的是加载后重新序列化的元数据,磁盘上的.yml在输出中统一显示为.yaml(ldbc_sample 实测:磁盘person.vertex.yml,show 输出person.vertex.yaml)。
四、VertexInfo:顶点类型元数据详解
4.1 实测 YAML 全文
自建图 person.vertex.yaml 实测内容:
type: person
chunk_size: 100
prefix: vertex/person/
property_groups:
- properties:
- name: id
data_type: int64
is_primary: true
file_type: parquet
- properties:
- name: firstName
data_type: string
is_primary: false
- name: lastName
data_type: string
is_primary: false
- name: gender
data_type: string
is_primary: false
file_type: parquet
version: gar/v1
4.2 字段逐项说明
| 字段 | 实测取值 | 含义与要点 |
|---|---|---|
| type | person | 顶点类型名,边元数据通过它引用顶点 |
| chunk_size | 100 | 每个顶点数据文件最多容纳的顶点行数,决定 chunk 切分 |
| prefix | vertex/person/ | 该类型数据在图前缀下的相对目录(实测同样要求带尾斜杠) |
| property_groups | 两组 | 属性分组,每组独立落成一个数据文件目录,实现按属性组裁剪读取 |
| properties[].name | id、firstName 等 | 属性名,数据文件中的列名 |
| properties[].data_type | int64、string | 属性数据类型(bool/int32/int64/float32/float64/string 等) |
| properties[].is_primary | true/false | 是否主键。实测主键属性 id 的 is_nullable 由加载器置为 false,其余属性可空 |
| file_type | parquet | 该属性组数据文件的物理格式;规范同时支持 csv(官方 modern_graph 实测使用 csv) |
| version | gar/v1 | 格式版本号 |
4.3 属性分组设计
实测样本把 4 个属性拆成两组:id(主键组)与 firstName、lastName、gender(姓名组)。分组的意义在磁盘上看得最清楚——vertex/person/ 下实测正好两个数据目录 id/ 与 firstName_lastName_gender/,各自一个 chunk0 文件。读取「只要 id 列」时只需打开 id 组的文件,这就是 GraphAr 属性裁剪能力的物理基础。
4.4 prefix 命名规则(三种实测形态对照)
属性组的数据目录名(组前缀)有三种来源,实测样本恰好各占其一:
| 形态 | 实测来源 | 写法 | 磁盘结果 |
|---|---|---|---|
| 显式声明 | modern_graph(官方) | 属性组内写 prefix: id/、prefix: name_age/ | 目录与声明一致 |
| 隐式推导 | 自建图 person.vertex.yaml(不写组 prefix) | 按组内属性名以下划线连接,末尾加 / | id/、firstName_lastName_gender/ |
| 隐式推导 | ldbc_sample 官方元数据(同样不写组 prefix) | 同上 | vertex/person/id/ 等目录与推导一致 |
用 graphar show -v person 观察自建图,加载器把推导出的组前缀、可空性一并展示(实测节选):
INFO chunk_size: 100
prefix: vertex/person/
property_groups:
- file_type: parquet
prefix: id/
properties:
- data_type: int64
is_nullable: false
is_primary: true
name: id
- file_type: parquet
prefix: firstName_lastName_gender/
⚠️ 注意:组前缀推导规则随属性名变化——改名或调组会改变磁盘目录名。对接已有系统时建议像 modern_graph 一样显式声明 prefix,避免隐性耦合。
五、EdgeInfo:边类型元数据详解
5.1 实测 YAML 全文
自建图 person_knows_person.edge.yaml 实测内容:
src_type: person
edge_type: knows
dst_type: person
chunk_size: 1024
src_chunk_size: 100
dst_chunk_size: 100
directed: false
prefix: edge/person_knows_person/
adj_lists:
- ordered: false
aligned_by: src
file_type: parquet
- ordered: true
aligned_by: src
file_type: parquet
- ordered: true
aligned_by: dst
file_type: parquet
property_groups:
- file_type: parquet
properties:
- name: creationDate
data_type: string
is_primary: false
version: gar/v1
5.2 字段逐项说明
| 字段 | 实测取值 | 含义与要点 |
|---|---|---|
| src_type / edge_type / dst_type | person / knows / person | 边的类型三元组,GraphInfo 用它引用边元数据 |
| chunk_size | 1024 | 每个边数据文件(chunk)最多容纳的边行数 |
| src_chunk_size / dst_chunk_size | 100 / 100 | 源/目的顶点侧的分块大小,决定边数据 part 的划分粒度 |
| directed | false | 有向性声明;实测无向边同样只存一份邻接数据 |
| prefix | edge/person_knows_person/ | 边数据相对目录(默认按 <src>_<edge>_<dst> 命名) |
| adj_lists | 三条 | 声明该边集支持的邻接表布局(可多种并存),每条含 ordered、aligned_by、file_type、prefix |
| adj_lists[].ordered / aligned_by | false/true、src/dst | 是否按对齐侧顶点排序;对齐源为 src、对齐目的为 dst |
| adj_lists[].prefix | 声明中未写 | 实测由 ordered 与 aligned_by 推导为 unordered_by_source/、ordered_by_source/、ordered_by_dest/(graphar show -es person -e knows -ed person 回显确认) |
| property_groups | creationDate 一组 | 边属性分组,语义同顶点 |
| version | gar/v1 | 格式版本号 |
5.3 邻接表布局:三种声明与实际落盘
EdgeInfo 可以同时声明多种布局,但数据文件只为实际写出的布局存在:自建图实测只写出 ordered_by_dest 一种(find 清单里 edge 目录下只有 ordered_by_dest 子树),而官方 ldbc_sample 三种布局的数据目录全部存在(实测 find edge -type d 可见 unordered_by_source、ordered_by_source、ordered_by_dest 三组并列,各含 adj_list、creationDate 与 offset 目录)。
💡 技术拓展:邻接表布局对应图计算领域的经典存储结构,选择依据如下——
GraphAr 布局 经典结构 有序性 数据文件特征 适用场景 unordered_by_source COO(坐标列表) 不排序 仅 adj_list 表,无 offset 文件 顺序扫描全部边、追加写 ordered_by_source CSR(压缩稀疏行) 按源顶点排序 adj_list + offset(每源段一个) 沿出边遍历,图计算主流 ordered_by_dest CSC(压缩稀疏列) 按目的顶点排序 adj_list + offset(每目的段一个) 沿入边遍历、入度分析 CSR/CSC 的取舍本质是遍历方向:CSR 是「行视图」(按源顶点取邻域),CSC 是「列视图」(按目的顶点取邻域)。本文实测自建图采用
ordered_by_dest(CSC)写出,第六章将用真实文件内容展示其 offset 组织与行排序;性能敏感的图计算场景一般首选ordered_by_source。
六、数据文件组织:chunk、offset 与计数文件
6.1 顶点数据:属性组目录、chunk 切分与 vertex_count
顶点数据按「类型前缀 + 属性组前缀」两级目录组织,按 chunk_size 切成 chunk0、chunk1 等文件。实测 ldbc_sample 的 person 顶点(903 个、chunk_size 100)正好 10 个 chunk:
ls /opt/incubator-graphar/testing/ldbc_sample/parquet/vertex/person/id/
chunk0
chunk1
chunk2
chunk3
chunk4
chunk5
chunk6
chunk7
chunk8
chunk9
每个顶点类型目录下有 8 字节定长计数文件 vertex_count,内容为小端 int64 的顶点总数,od 实测:
od -An -tu8 /opt/incubator-graphar/testing/ldbc_sample/parquet/vertex/person/vertex_count
903
数据文件虽无 .parquet 扩展名,实测文件头 4 字节为 Parquet 魔数:
head -c 4 /opt/incubator-graphar/testing/ldbc_sample/parquet/vertex/person/id/chunk0 | od -c
0000000 P A R 1
0000004
6.2 顶点 chunk 内部结构(pyarrow 实测)
用 pyarrow 打开自建图的顶点数据文件,观察每列内容(脚本保存为 /tmp/dump_parquet_doc.py 后运行):
import pyarrow.parquet as pq
base = "/root/graphar-demo"
files = [
"vertex/person/id/chunk0",
"vertex/person/firstName_lastName_gender/chunk0",
"edge/person_knows_person/ordered_by_dest/adj_list/part0/chunk0",
"edge/person_knows_person/ordered_by_dest/creationDate/part0/chunk0",
"edge/person_knows_person/ordered_by_dest/offset/chunk0",
]
for f in files:
table = pq.read_table(base + "/" + f)
print("== " + f)
print("columns:", table.column_names)
print("rows:", table.num_rows)
if f.endswith("offset/chunk0"):
offsets = table.column("_graphArOffset").to_pylist()
print("first five:", offsets[:5])
print("last five:", offsets[-5:])
print("distinct:", sorted(set(offsets)))
else:
print("data:", table.to_pydict())
顶点部分实测回显:
== vertex/person/id/chunk0
columns: ['_graphArVertexIndex', 'id']
rows: 3
data: {'_graphArVertexIndex': [0, 1, 2], 'id': [0, 1, 2]}
== vertex/person/firstName_lastName_gender/chunk0
columns: ['_graphArVertexIndex', 'firstName', 'lastName', 'gender']
rows: 3
data: {'_graphArVertexIndex': [0, 1, 2], 'firstName': ['John', 'Jane', 'Alice'], 'lastName': ['Smith', 'Doe', 'Wonderland'], 'gender': ['male', 'female', 'female']}
要点:每个顶点 chunk 内部第一列是实现写入的 _graphArVertexIndex(chunk 内行号),其余列与元数据声明的属性一一对应;两个属性组文件的行数与行号一一对应,共同还原完整顶点。
6.3 边数据:adj_list、属性、offset、edge_count 四类文件
ordered_by_dest 布局下,自建图边目录实测包含四类文件(见 2.2 清单)。pyarrow 实测回显:
== edge/person_knows_person/ordered_by_dest/adj_list/part0/chunk0
columns: ['_graphArSrcIndex', '_graphArDstIndex']
rows: 4
data: {'_graphArSrcIndex': [1, 0, 2, 0], '_graphArDstIndex': [0, 1, 1, 2]}
== edge/person_knows_person/ordered_by_dest/creationDate/part0/chunk0
columns: ['creationDate']
rows: 4
data: {'creationDate': ['2010-01-01', '2011-01-01', '2013-01-01', '2012-01-01']}
== edge/person_knows_person/ordered_by_dest/offset/chunk0
columns: ['_graphArOffset']
rows: 101
first five: [0, 1, 3, 4, 4]
last five: [4, 4, 4, 4, 4]
distinct: [0, 1, 3, 4]
逐项解读:
- adj_list 本体:每行一条边,列是源/目的顶点在各自 chunk 内的行号索引。实测 4 行的顺序为 (1,0)、(0,1)、(2,1)、(0,2)——先按目的顶点升序(0、1、1、2),同目的内再按源顶点升序,这正是 CSC 排序语义(与实战篇读回顺序完全一致);
- 边属性文件:目录按属性组前缀命名(creationDate),行数与 adj_list 严格对齐——第 i 行属性属于 adj_list 第 i 行的边。实测 4 个日期与写入时的边一一对应;
- offset 索引:CSC 式前缀和,
offset[i]等于目的顶点索引小于 i 的边数。实测目的顶点 0、1、2 分别有 1、2、1 条入边,前缀和为 0、1、3、4;文件共 101 行 = dst_chunk_size 100 + 1,超出实际顶点数的行以最后一个前缀和值 4 填充。每个目的顶点段(part)对应一个 offset chunk 文件——ldbc_sample 实测 ordered_by_source 布局的 offset 目录恰好 10 个文件,对应 10 个源顶点 chunk; - 计数文件:
edge_count0为该 part 的边数(实测自建图 4、ldbc ordered_by_source part0 为 667),vertex_count为对齐侧顶点数(实测 ordered_by_source 下为 903、ordered_by_dest 下同样为 903)。三个值均由 od 实测确认,文件同样为 8 字节小端 int64。
计数文件 od 实测(ldbc knows,ordered_by_source 布局):
od -An -tu8 /opt/incubator-graphar/testing/ldbc_sample/parquet/edge/person_knows_person/ordered_by_source/vertex_count /opt/incubator-graphar/testing/ldbc_sample/parquet/edge/person_knows_person/ordered_by_source/edge_count0
903 667
6.4 csv 文件类型的实测形态
file_type 并不限于 parquet:官方 modern_graph 的元数据实测声明 file_type: csv(并显式声明组前缀 id/、name_age/)。打开其数据文件即普通 CSV,首行为内部索引列与属性列(实测):
head -3 /opt/incubator-graphar/testing/modern_graph/vertex/person/id/chunk0
_graphArVertexIndex,id
0,2
1,6
💡 说明:csv 形态下
_graphArVertexIndex与属性列同样成对出现,且属性值(2、6)独立于行号——主键值与 chunk 内行号是两个概念,这是排查「id 不连续」类问题时最容易混淆的点。
6.5 prefix 命名规则汇总
综合三层元数据的实测行为,GraphAr 目录命名规则可归纳为:
| 对象 | 默认命名规则 | 实测示例 |
|---|---|---|
| 顶点类型目录 | 元数据 prefix 字段,惯例 vertex/<类型名>/ | vertex/person/ |
| 顶点属性组目录 | 显式 prefix,或组内属性名下划线连接 | id/、firstName_lastName_gender/ |
| 边类型目录 | 元数据 prefix 字段,惯例 edge/<src>_<edge>_<dst>/ | edge/person_knows_person/ |
| 边布局目录 | 按 ordered 与 aligned_by 推导 | ordered_by_dest/ |
| 边 part 目录 | part<源或目的顶点 chunk 序号> | part0 到 part9 |
| chunk 文件 | chunk<chunk 序号> | chunk0 到 chunk9 |
| 计数文件 | 顶点 vertex_count;边 edge_count<part 序号> | vertex_count、edge_count0 |
七、注意事项
- 三层 YAML 缺一不可:GraphInfo 引用子文件失败会直接 load 报错;自建图按 save() 规则命名(.yaml 后缀 + 类型名组合)最稳妥;
- prefix 是纯路径拼接:图级、类型级 prefix 与 Builder 目录参数都按字符串拼接,任何一处漏掉尾斜杠都会静默写偏路径且写出端不报错;
- 声明与数据可不对齐:EdgeInfo 可声明三种布局而数据只写其一(自建图实测即如此),
graphar check校验元数据自洽、不检查数据文件存在性,评估数据完整性要用 find 逐目录核对; - 行对齐关系:边属性文件与 adj_list 文件按行严格对齐,offset 前缀和与对齐侧顶点段一一对应,改写这两个文件的行序或行数会直接破坏读取语义;
- 内部列约定:
_graphArVertexIndex、_graphArSrcIndex、_graphArDstIndex、_graphArOffset是实现写入的内部索引列,外部工具读取数据时应保留其行序语义; - 计数文件是定长二进制:vertex_count/edge_count 为 8 字节小端 int64,不是文本、也不是 Parquet,直接 cat 会得到乱码,用 od 或按 int64 解析。
八、参考链接
- Apache GraphAr 格式规范:https://graphar.apache.org/docs/
- 官方测试数据仓库(ldbc_sample、modern_graph 样本):https://github.com/apache/incubator-graphar-testing
- Apache GraphAr 官网:https://graphar.apache.org/
- 同系列文档:《Apache GraphAr 数据读写实战详解》《Ubuntu 22.04 安装 Apache GraphAr 指南》