4、元数据存储与索引:图数据库选型、关系型存储、全文索引与搜索

元数据存哪儿、怎么查得快,这问题我几乎在每个数据网格项目里都会被问到。说白了,选存储就是选 trade-off。你不可能用一个库搞定所有查询模式——图遍历、关系过滤、全文检索,它们的访问路径完全不同。

这一章,我把自己的选型思路和踩过的坑摊开来讲。先看整体框架,再逐个击破。

元数据存储与索引选型框架 元数据湖 / 事件流 图数据库 Neo4j / JanusGraph 关系型存储 PostgreSQL / MySQL 全文索引 Elasticsearch / MeiliSearch 血缘追踪 · 影响分析 实体关系遍历 资产清单 · 变更记录 ACID 事务 关键词搜索 · 模糊匹配 高亮 / 打分 Cypher / Gremlin SQL / REST Query DSL / REST 统一元数据查询网关(路由 + 聚合)

4.1 图数据库选型:为什么血缘分析离不开图

元数据里最难搞的是什么?我投血缘关系一票。一张表经过 ETL 变成另一张表,中间还经过了视图、存储过程、数据湖的 Parquet 文件——这种多跳关系,用 SQL 写 JOIN 能写到怀疑人生。

图数据库天生就是干这个的。节点是表、字段、任务,边是“依赖于”、“生成自”。查一条血缘链路,就是一次图遍历,毫秒级返回。

选型对比:Neo4j vs JanusGraph

维度Neo4jJanusGraph
存储后端自有 Native 存储Cassandra / HBase / Bigtable
查询语言Cypher(声明式,易学)Gremlin(函数式,灵活)
事务ACID,单机强一致最终一致性(依赖后端)
水平扩展企业版支持集群原生支持,适合超大规模
部署复杂度低,单机可跑高,需配套 Cassandra/ES
社区生态成熟,工具多中等,Apache 项目
我的建议:团队小于 20 人、元数据节点数在千万级以下,直接上 Neo4j。我见过一个团队用 JanusGraph 搭了三个月还没调通查询性能,换 Neo4j 一周就上线了。规模真的很大再考虑 JanusGraph。

实战:用 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 层。我在项目中用这个模式帮数据团队定位过好几次“这个字段到底从哪来的”的争论。

注意:图遍历深度不要设太大。我曾经设过 20 层,结果查询跑了 30 秒,直接把 Neo4j 的堆内存打满了。一般 5-8 层足够覆盖绝大多数血缘场景。

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" }
    }
  }
}
避坑:别把所有字段都设成 text 类型。tag、domain、owner 这些用 keyword,支持精确过滤和聚合。我见过一个团队把 owner 设成 text,结果搜“zhang”把“zhangwei”和“zhangxiao”全召回了,过滤完全失效。

搜索查询示例

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 三库协同:统一查询网关

三个存储引擎各司其职,但用户不能感知到背后有三个库。我设计了一个轻量级查询网关,做两件事:

  1. 路由:判断查询类型。关键词搜索走 ES,血缘查询走 Neo4j,资产清单走 PostgreSQL。
  2. 聚合:有些查询需要跨库。比如“搜到一张表,同时展示它的血缘图”。网关先调 ES 拿到表 ID,再调 Neo4j 查血缘,最后合并结果返回。

我的经验:网关不要做太重的逻辑。保持薄层,只做路由和简单合并。复杂聚合交给客户端或 BFF 层。否则网关会成为新的性能瓶颈。

4.5 本章小结

选存储就是选 trade-off。图库管关系,关系库管清单,ES 管搜索。三个库通过统一网关对外暴露,用户只看到一个元数据平台。

嗯,这里要注意:数据网格的元数据量会随着域增多而线性增长。一开始就规划好存储拆分,比后期重构要省心得多。我见过太多项目在 MVP 阶段把所有元数据塞进一个 MySQL 表,半年后查询超时、迁移痛苦——别走那条路。