4、元数据存储与索引:图数据库选型、关系型存储、全文索引与搜索
元数据存哪儿、怎么查得快,这问题我几乎在每个数据网格项目里都会被问到。说白了,选存储就是选 trade-off。你不可能用一个库搞定所有查询模式——图遍历、关系过滤、全文检索,它们的访问路径完全不同。
这一章,我把自己的选型思路和踩过的坑摊开来讲。先看整体框架,再逐个击破。
4.1 图数据库选型:为什么血缘分析离不开图
元数据里最难搞的是什么?我投血缘关系一票。一张表经过 ETL 变成另一张表,中间还经过了视图、存储过程、数据湖的 Parquet 文件——这种多跳关系,用 SQL 写 JOIN 能写到怀疑人生。
图数据库天生就是干这个的。节点是表、字段、任务,边是“依赖于”、“生成自”。查一条血缘链路,就是一次图遍历,毫秒级返回。
选型对比:Neo4j vs JanusGraph
| 维度 | Neo4j | JanusGraph |
|---|---|---|
| 存储后端 | 自有 Native 存储 | Cassandra / HBase / Bigtable |
| 查询语言 | Cypher(声明式,易学) | Gremlin(函数式,灵活) |
| 事务 | ACID,单机强一致 | 最终一致性(依赖后端) |
| 水平扩展 | 企业版支持集群 | 原生支持,适合超大规模 |
| 部署复杂度 | 低,单机可跑 | 高,需配套 Cassandra/ES |
| 社区生态 | 成熟,工具多 | 中等,Apache 项目 |
实战:用 Cypher 查字段级血缘
假设我们有一个数据资产图。字段 order_amount 从源表 raw_orders 经过计算流入 dw_order_fact。查它的完整上游:
MATCH (f:Field {name: 'order_amount'})<-[:DERIVED_FROM*1..5]-(upstream:Field)
RETURN upstream.name, upstream.table_name
ORDER BY length(upstream) DESC
这条查询会沿着 DERIVED_FROM 边向上追溯最多 5 层。我在项目中用这个模式帮数据团队定位过好几次“这个字段到底从哪来的”的争论。
4.2 关系型存储:元数据资产清单的基石
图数据库擅长关系,但做不了精细的过滤和聚合。比如“查所有创建时间在昨天、拥有者是 data-team 的表”——这种查询在关系型数据库里就是一条简单 SQL。
我习惯把元数据分成两类:
- 结构元数据:表名、字段名、类型、注释、分区信息。这些用关系表存,清晰可控。
- 关系元数据:血缘、依赖、影响链路。这些交给图库。
关系型存储我首选 PostgreSQL。为什么?
- 原生 JSONB 支持,可以存元数据的扩展属性(比如标签、自定义属性)。
- 物化视图,适合预计算“每个数据域有多少张表”这类统计。
- 逻辑复制,方便把元数据变更实时同步到 Elasticsearch。
表结构示例
CREATE TABLE metadata_assets (
id UUID PRIMARY KEY,
name VARCHAR(255) NOT NULL,
asset_type VARCHAR(50) NOT NULL, -- 'table', 'view', 'topic', 'pipeline'
domain_id UUID REFERENCES data_domains(id),
owner VARCHAR(100),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
properties JSONB DEFAULT '{}'
);
CREATE INDEX idx_asset_type_domain ON metadata_assets(asset_type, domain_id);
CREATE INDEX idx_asset_owner ON metadata_assets(owner);
CREATE INDEX idx_asset_properties ON metadata_assets USING GIN(properties);
关键点:用 asset_type 做分区键,按类型分表存储也可以,但我觉得单表 + 索引更灵活。数据网格里资产类型会不断新增,分表方案改起来太痛苦。
4.3 全文索引与搜索:让用户自己找到元数据
图库和关系库都解决不了一个问题:用户不知道资产叫什么名字,只知道大概描述。比如“我想找那个跟订单金额有关的表,好像是叫 order 什么的”。
这时候就需要全文搜索引擎了。我选 Elasticsearch,生态成熟,Kibana 还能直接做元数据探索。
索引设计要点
- 多字段搜索:名称、描述、字段注释、标签,全部索引。
- 权重分配:名称匹配得分最高,描述次之,注释最低。
- 同义词扩展:比如“订单”和“order”应该能互相召回。
PUT /metadata_assets
{
"settings": {
"analysis": {
"analyzer": {
"metadata_analyzer": {
"type": "standard",
"stopwords": "_english_"
}
}
}
},
"mappings": {
"properties": {
"name": { "type": "text", "boost": 3.0 },
"description": { "type": "text", "boost": 2.0 },
"field_comments": { "type": "text", "boost": 1.0 },
"tags": { "type": "keyword" },
"domain": { "type": "keyword" },
"owner": { "type": "keyword" },
"updated_at": { "type": "date" }
}
}
}
搜索查询示例
GET /metadata_assets/_search
{
"query": {
"bool": {
"must": {
"multi_match": {
"query": "订单金额",
"fields": ["name^3", "description^2", "field_comments"]
}
},
"filter": [
{ "term": { "domain": "finance" } }
]
}
},
"highlight": {
"fields": {
"name": {}, "description": {}, "field_comments": {}
}
}
}
返回结果里会带上高亮片段,前端直接渲染,用户体验好很多。
4.4 三库协同:统一查询网关
三个存储引擎各司其职,但用户不能感知到背后有三个库。我设计了一个轻量级查询网关,做两件事:
- 路由:判断查询类型。关键词搜索走 ES,血缘查询走 Neo4j,资产清单走 PostgreSQL。
- 聚合:有些查询需要跨库。比如“搜到一张表,同时展示它的血缘图”。网关先调 ES 拿到表 ID,再调 Neo4j 查血缘,最后合并结果返回。
我的经验:网关不要做太重的逻辑。保持薄层,只做路由和简单合并。复杂聚合交给客户端或 BFF 层。否则网关会成为新的性能瓶颈。
4.5 本章小结
选存储就是选 trade-off。图库管关系,关系库管清单,ES 管搜索。三个库通过统一网关对外暴露,用户只看到一个元数据平台。
嗯,这里要注意:数据网格的元数据量会随着域增多而线性增长。一开始就规划好存储拆分,比后期重构要省心得多。我见过太多项目在 MVP 阶段把所有元数据塞进一个 MySQL 表,半年后查询超时、迁移痛苦——别走那条路。