跳转至

本体(实验性)

实验性功能

本页介绍的功能仍在成形中。参数、输出和错误负载都可能不经通知就改变, 本页也有意不从文档的其他位置链接过来。

Dosi 可以在核心 Ossie 模型之外——或者取而代之——加载一份 Ossie 本体。本体并不带来 vendor 扩展装不下的信息; 它带来的是同一份模型可以承载多份本体,而且它们可以互相冲突:运营部说"机场延误" 指起飞机场,地服部说同一句指到达机场。两者都对,各自是同一批表之上的一份本体。

下文都是 dosi 命令行。示例用仓库里的 fixture:fixtures/ossie-mini-flights (两个概念)、fixtures/aviation(手写模型加一份本体)和 fixtures/ossie-flights (上游参考本体)。

加载本体

以下参数对每条命令都生效:

参数 环境变量 含义
--ontology <path> DOSI_ONTOLOGY Ossie 本体文件
--mapping <name> 多个 ontology_mappings 时选用哪一个;不止一个时必填
--model <path> DOSI_MODEL 核心模型——也可以是本体文件,以顶层 ontology: 键识别

model-first 与 ontology-first

命令 真相源 另一层的角色
ontology-first --ontology o.yaml 本体内嵌的 semantic_model
model-first --model m.yaml --ontology o.yaml m.yaml 本体只加概念、角色与值域

两个都给时 --model 全胜,且不做一致性校验:内嵌那份只需要让本体的映射表达式能解析, 手写模型多带几个字段是超集,不是冲突。如果 --model 已经声明了一条与本体 link 列完全相同的 join,概念就沿用模型里这条关系,不会再加第二条。

加载后得到什么

本体会被降级(lower)成一份普通的核心 Ossie 模型,之后与手写模型走完全相同的 校验 → 编译 → 规划流程。旁边另存一份 ConceptMap,它只负责命名:把概念路径映射到 它所解析成的 dataset、字段与连接路径。

本体构造 变成什么
带对象映射的 identify_by dataset 的 primary_key,模型已声明时以模型为准
Entity → Value 关系 已有字段的一个名字;时间类值类型会标记为 is_time
Entity → Entity,ManyToOne / OneToOne 名为 <owner>_<link> 的关系,如 flight_departs_from
存在另一个实体表上的实体 不 join、不设主键、不能作查询根——见限制
带过滤映射的一元关系 布尔字段 <owner>_<name>,可用 Concept.name 访问
derived_by: AGG[<value> WHERE <correlation> GROUP BY <concept>] 指标 <owner>_<relationship>——见派生指标

没有被降级的构造,每次调用都会在 stderr 上打一行 note:

$ dosi describe --ontology fixtures/ossie-flights/ontology.yaml
note: City.state: State has no table of its own; reached without a join
note: Airport.city: City has no table of its own; reached without a join
note: Flight.registers_latitude_series: n-ary relationship recorded as unmapped
...

浏览:describe 与 paths

dosi describe [concept]

不带参数时返回概念目录:

$ dosi describe --ontology fixtures/ossie-mini-flights/ontology.yaml
ontology MiniFlights
Two airports' worth of ontology, kept small enough to read whole.
concept  identity  out  in  props  root
Airport  code      0    2   3      yes
Flight   id        2    0   2      yes

outin 是 link 数,props 是属性数,root 表示查询能否以该概念为根。

带概念名时,返回针对它写查询所需的一切:

$ dosi describe FlightAlert --model fixtures/aviation/model.yaml \
    --ontology fixtures/aviation/ontology.yaml
concept FlightAlert
identity: id  (root: flight_alerts)

properties
path                            type                    domain                   time
FlightAlert.id                  AlertId (String)
FlightAlert.type                AlertType (String)
FlightAlert.priority            AlertPriority (String)  one of [P0, P1, P2, P3]
FlightAlert.status              AlertStatus (String)    one of [OPEN, RESOLVED]
FlightAlert.created_at          DateTime                                         yes
FlightAlert.resolved_at         DateTime                                         yes
FlightAlert.resolution_comment  String

links out
name    role    to      multiplicity  join
flight  flight  Flight  ManyToOne     yes

metrics by group-by
group by        metrics
FlightAlert.id  alert_count, open_alert_count, flights_with_open_alert, maintenance_alerts

time dimensions: FlightAlert.created_at, FlightAlert.flight.actual_arrival, FlightAlert.flight.actual_departure, FlightAlert.flight.scheduled_arrival, FlightAlert.flight.scheduled_departure, FlightAlert.resolved_at
区块 告诉你什么
properties 每个已映射属性的概念路径,带值类型、封闭值域与时间标记
links out 名称、角色、目标与 multiplicity。join: none 表示目标就存在本表上
links in (query from) 链接到这里的概念——查询应该从那一侧起根
metrics reachable 本体派生出的指标,带 group-by,以及赋予它含义的边(via
metrics by group-by 本概念能分组的模型自有指标,按 group-by 写法分行列出,写法从指标所在的概念出发。每条写法打印前都先经 planner 验证
time dimensions 本概念及一跳之外的时间属性,可用于时间范围
unmapped 已声明但未降级的关系:n 元关系,或没有映射的 link
not queryable 没有绑定到列的属性(多数是行级 derived_by

表格里的每条路径都能原样粘进 selectquery 这条契约是这个视图值得调用的原因。

--format json 返回相同内容:conceptidentityrootpropertiespathtype_namedomaindomain_summaryis_timemapped)、linksincomingquery_from)、metrics_reachablenamemetricgraingroup_byvia)、 groupable_metricsgroup_byfrommetrics)、time_dimensions_reachabledefault_projectionunmapped。目录则是 ontologydescriptionrequiresconceptsnameidentitylinks_outlinks_inpropertiesqueryablesummary)。

没有本体时,不带参数的 describe 改为列出 dataset 与指标,带概念名则报错:

$ dosi describe Airport --model fixtures/ossie-mini-flights/model.yaml
error: this model has no ontology, so "Airport" is not a concept; pass --ontology <path>, or use `list` for datasets and metrics

dosi paths --from <concept> --to <concept>

两个概念之间三跳以内的全部命名路径,带走过的角色,以及本体声明为等价的路径对:

$ dosi paths --from Flight --to Airport --model fixtures/aviation/model.yaml \
    --ontology fixtures/aviation/ontology.yaml
paths Flight → Airport
path                              roles                        hops  multiplicity  join
Flight.arrives_at                 destination                  1     ManyToOne     yes
Flight.departs_from               origin                       1     ManyToOne     yes
Flight.follows_route.destination  follows_route → destination  2     ManyToOne     yes
Flight.follows_route.origin       follows_route → origin       2     ManyToOne     yes
equivalent: Flight.departs_from ≡ Flight.follows_route.origin  (Flight.follows_route.origin == origin)
equivalent: Flight.arrives_at ≡ Flight.follows_route.destination  (Flight.follows_route.destination == destination)

到同一目标的两条路径,正是裸 dataset.field 引用解析不了的情况 (ambiguous_join_path);区分它们的是角色名。等价关系读自形如 Concept.link.role == role 的关系级 requires。只有一条路径时,JSON 会带 unique: true

查询里的概念路径

加载本体后,selectquery 在所有接受字段的地方都接受概念拼写:

位置 拼写
select --from 概念名:FlightAlert
字段、--group-by Concept.propertyConcept.link.propertyConcept.link.link.property
--where 同样的路径;字符串字面量永远不会被改写
--time-dimension 时间属性路径:Delay.flight.scheduled_departure
--metrics 派生指标的概念路径:Airport.average_departure_delay

每一个都解析成 planner 早就认识的 dataset、字段与关系路径,所以 SQL 与 dataset 拼写完全一致:

$ dosi select --model fixtures/aviation/model.yaml \
    --ontology fixtures/aviation/ontology.yaml \
    --from FlightAlert \
    --fields "FlightAlert.id,FlightAlert.priority,FlightAlert.flight.arrives_at.code" \
    --where "FlightAlert.status = 'OPEN' AND FlightAlert.priority = 'P1'"
SELECT flight_alerts.alert_id AS alert_id, flight_alerts.priority AS priority, airports.airport_code AS flight_alerts_flight__flights_arrives_at__airport_code FROM main.flight_alerts AS flight_alerts LEFT JOIN main.flights AS flights ON flight_alerts.flight_id = flights.flight_id LEFT JOIN main.airports AS airports ON flights.destination_airport_code = airports.airport_code WHERE flight_alerts.status = 'OPEN' AND flight_alerts.priority = 'P1' LIMIT 100

输出列沿用 planner 起的名字,--order 也接受这些列名,而不是概念路径。

派生指标

derived_by 规则的相关路径才是赋予指标含义的东西。两个这样的指标可以编译出逐字节 相同的 SQL,含义却不同:

$ dosi query --ontology fixtures/ossie-mini-flights/ontology.yaml \
    --metrics Airport.average_departure_delay --group-by Airport.code --execute --db mini.duckdb
code  airport_average_departure_delay
ATL   16
ORD   30
DCA   20
SFO   32
4 rows

$ dosi query --ontology fixtures/ossie-mini-flights/ontology.yaml \
    --metrics Airport.average_inbound_departure_delay --group-by Airport.code --execute --db mini.duckdb
code  airport_average_inbound_departure_delay
ATL   30
DCA   16
SFO   35
ORD   10
4 rows

两者都是 AVG(FLIGHT.dep_delay)。用概念路径指名时,每个指标沿自己的边分组—— 第一个经起飞机场,第二个经到达机场。由此带来两个有意为之的结果:

  • 用扁平名要模型指标(airport_average_departure_delay)时没有指定路径, 按 Airport.code 分组会得到 ambiguous_join_path:单靠模型分不出两者。
  • 同时要两个指标时,两条路径互相矛盾,歧义会被报告,而不是悄悄选定一边。

规则要么整条降级,要么完全不降级。WHERE 不是相关条件(如 WHERE Flight.departure_delay > 10),或相关路径解析不到任何 link 时,规则保留为文本 并打一行 note:,不会生成指标。

group-by 起根在错误一侧时

指标沿一条从它到达不了的概念出发的路径分组时,planner 会拒绝。加载了本体时, 拒绝信息会给出修正写法——同一条路径,改从指标所在的概念出发:

$ dosi query --model fixtures/aviation/model.yaml --ontology fixtures/aviation/ontology.yaml \
    --metrics delayed_flights --group-by Flight.arrives_at.code
error: InvalidRelationshipPath: a relationship path pins this query to dataset "flights", which has no many→one path to "delays" — where measure "delays_flight_id_count_distinct" of metric "delayed_flights" lives
  candidates: Delay.flight.arrives_at.code
  hint: root the group-by on the concept the metric lives on: --group-by Delay.flight.arrives_at.code

每个候选写法给出前都先经过规划验证;最多三个,短的在前。--format json 下它们是 candidatessuggested_retry

值域

ValueTyperequires 若枚举了一个封闭集合,过滤字面量就能在查询编译之前被判定:

$ dosi select --model fixtures/aviation/model.yaml --ontology fixtures/aviation/ontology.yaml \
    --from FlightAlert --fields FlightAlert.id --where "FlightAlert.status = 'unresolved'"
error: value_not_in_domain: "unresolved" is not a value of FlightAlert.status; its domain is a closed set
  allowed: OPEN, RESOLVED
{
  "code": "value_not_in_domain",
  "violations": [
    {
      "reference": "FlightAlert.status",
      "literal": "Open",
      "allowed": ["OPEN", "RESOLVED"]
    }
  ]
}

这是探测数仓做不到的事:probe 能查出一列当前有哪些取值,但只有声明才断言这个集合是封闭的。

具体检查哪些:

  • 写成 X == 'A' OR X == 'B'、且每个值都是字面量的枚举。与其他东西比较的规则 (Status == Other.status)不产生值域。
  • 形如 <path> = '<literal>'<path> IN ('a', 'b') 的谓词,字面量里的 '' 读作一个引号。字符串字面量内部的文本永远不会被当成引用。
  • 区间(1 <= X, X <= 10)会在 describe 里显示,但不检查;数值字面量也不检查。 跨字段、跨关系与模型级约束原样保留,不由任何东西执行。

血缘

dosi lineage 画出指标血缘图——物理表 → dataset → 原子指标 → 派生指标——加载了本体时, 还在上面画出概念。图本身的说明见血缘;本节只讲本体加了什么。

$ dosi lineage view --ontology fixtures/ossie-mini-flights/ontology.yaml
$ dosi lineage view --ontology fixtures/ossie-flights/ontology.yaml --no-open --out flights.html
flights.html
$ dosi lineage dump --ontology fixtures/ossie-mini-flights/ontology.yaml > graph.json

view 直接从模型生成一个 HTML 页面——不读 dump 的输出——页面是自包含的:图数据内嵌在 页面里,不请求任何外部资源,通过 file:// 就能打开。它会用默认浏览器打开页面;带 --no-open 或没有浏览器时,改为打印文件路径。加载时的 note: 仍然输出到 stderr。

页面有两个视图。Lineage 把泳道从左到右排开,在 dataset 与指标之间插入一条概念泳道; 没有自己的表的实体画成虚线框,每条 grain 边上标出它的路径。Concepts 把实体画成 ER 图,属性折叠在卡片里,平行的关系分开画。在任一视图里选中节点,都会显示它的详情和边; 点击一条边会跳到另一端,必要时自动切换视图。

本体加了什么

平面图里的东西一样都不动:四条泳道、七种边、每个节点与边的 id 全部不变;没有本体的模型 根本没有 ontology 键。本体加上的是:

新增 位置 内容
实体节点 ontology.nodes 每个实体概念一个,id 为 concept:<Name>
mapping ontology.edges dataset → 概念
relationship ontology.edges 概念 → 概念
concept 对象 nodes 里的指标节点 一条 derived_by 规则对该指标的描述:pathmeasuregroup_byviarealized_by

两种边都是血缘——概念的数据在哪里。concept 对象则是语义,所以它和 dimensions 一样挂在 节点上,而不是单独成边。

有一个字段在每张图上都新增,无论有没有本体:dataset 节点 detail.fields 里的每一项都带 label——模型里该字段的 label,没有就等于字段名。

值类型不是节点;它们作为 properties[].concept 出现在所属实体上。叠加层的边既指向平面图 的节点(dataset:metric:),也指向实体节点(concept:),所以渲染时要把 nodesontology.nodes 放在一起查端点。

实体节点

{
  "id": "concept:Airport", "kind": "entity", "name": "Airport", "label": "Airport",
  "mapping": "table", "dataset": "AIRPORT", "key_columns": ["code"], "identity": ["code"],
  "properties": [
    { "name": "code", "concept": "AirportCode", "base": "String",
      "field": "AIRPORT.code", "is_time": false, "derived": false }
  ]
}
字段 含义
mapping 本体映射落在了什么上。table:有自己的 dataset,一行一个对象。denormalized:没有自己的表——列在另一个实体的表上,那些行属于那个实体,它只是投影(AIRPORT 上的 City;加州两个机场会让 State 的 CA 出现两次)。unmapped:声明的东西一个都解析不到列
datasetkey_columns 标识它的 dataset 与键——仅当 mappingtable 时出现
identity identify_by 里列出的关系
properties[] nameconcept,值类型;base,它追溯到的内置类型;field,形如 dataset.field,未映射时缺省;is_timederived,是否由 derived_by 计算;domain,有封闭值集时给出;description
unmapped 已声明但未降级的关系——即 describe 打印的那张列表

本体给了 description 时才出现。没有内容的键直接缺省,而不是 null。

类型 起点 → 终点 负载 Id
mapping dataset:Dconcept:C viaidentity(实体自己的 dataset)或 property(其属性被反规范化到的 dataset——每个这样的 dataset 一条边) mapping:dataset:D->concept:C
relationship concept:Aconcept:B namerolemultiplicitydescriptionrealized_by,它走过的 join: 边 id;no_join,目标就在同一张表上时为 true(此时 realized_by 为空);equivalences,该关系的 requires 原文 relationship:A.<name>

指标节点上的 concept 对象

只有本体派生出的指标才有。模型里手写的指标(model-first)保留它从 dataset 来的 aggregates 边,不增加任何东西。

字段 含义
path 概念拼写,如 Airport.average_departure_delay——可直接粘进 query --metrics
measure 规则聚合的值,写成概念路径:Flight.departure_delay
group_by 规则按哪个概念分组
via 从被度量的概念走到粒度概念所经过的 relationship: id
realized_by 同一条路径的平面图 join: id

派生指标里那两个指标:

{ "id": "metric:airport_average_departure_delay", "concept": {
    "path": "Airport.average_departure_delay", "measure": "Flight.departure_delay",
    "group_by": "Airport", "via": ["relationship:Flight.departs_from"],
    "realized_by": ["join:flight_departs_from"] } }
{ "id": "metric:airport_average_inbound_departure_delay", "concept": {
    "path": "Airport.average_inbound_departure_delay", "measure": "Flight.departure_delay",
    "group_by": "Airport", "via": ["relationship:Flight.arrives_at"],
    "realized_by": ["join:flight_arrives_at"] } }

在平面图里,两个指标各只有一条入边 dataset:FLIGHT → metric,别无其他;区分它们的正是 via。查看器把它画成一条从粒度概念指向指标的箭头,边上标出路径。

--redact-sql 不会改动 ontology 块——它不含 SQL。在 Python 里,Engine(ontology_path=…) 加载本体,lineage() 返回同一张图。

命名:dosi 的,不是规范的

叠加层里的类型名与字段名是 dosi 血缘图自己的词汇。Apache Ossie 本体规范定义的是概念、关系、 角色、derived_byrequires 和本体映射;里面没有 groundsmeasuresgraingrounding。它们各自对应规范里的什么:

图里的名字 规范里的来源
实体节点;concept.path type: EntityType 的概念。Concept.relationship 是规范标识一个关系的方式
relationship 实体 → 实体的二元关系。不叫 link:规范里的 link 指关系的一个实例——一个元组——而不是关系本身
equivalences 该关系的 requires 原文
mapping 边;节点上的 mapping 字段 把实体落到 dataset 上的概念映射——对象映射与 identify_by。三个取值是 dosi 对"这些映射落在了什么上"的判断,规范本身不区分
concept.measureconcept.group_byconcept.via 一条聚合 derived_by 规则 AGG[<value> WHERE <correlation> GROUP BY <concept>] 拆成三部分:被聚合的值、GROUP BY 的概念、WHERE 里的关联路径。measuregrain 是语义层的通用词,不是规范的词
realized_byno_join 本体里没有对应——是降级产生的 join

还要注意:规范里根本没有"指标"这个概念。它声明的是一条派生关系Airport.average_departure_delay 把 Airport 关联到一个 Delay)。dosi 把它降级成 core OSI 的指标,图上才有一个指标节点来承载 concept 对象。

聚合写法本身也不是规范正文定义的语法:规范的派生表达式示例都是逻辑规则 (Person.parent_of(descendant)EXISTS ( Person.earns ))。dosi 降级的 AGG[…] 规则来自上游示例 examples/flights.yaml,也就是 fixtures/ossie-flights 这份本体。

错误

加载本体时的拒绝以非零状态退出,带一个或多个错误码,每个都指明出问题的构造, 有修正办法时一并给出:

错误码 含义
ontology_parse 文档的顶层结构不是本体
ontology_unknown_reference 引用的概念、关系、角色或映射不存在
ontology_federation_unsupported 有多个 ontology_mappings,却没有用 --mapping 选定一个
ontology_mapping_unresolved 映射表达式引用了模型里没有的 dataset 或字段
ontology_expression_unsupported 映射表达式不是单纯的限定列名(可带 WHERE ( … ) 过滤)
ontology_mapping_depth_mismatch link 映射节点所在深度与其关系的元数不符
ontology_multiplicity_required 必须变成 join 的关系没有声明 multiplicity
ontology_concept_spans_datasets 一个概念的映射落在不止一个 dataset 上
ontology_identity_unresolved identify_by 指向了无法作为标识的东西
ontology_name_collision 概念名与 dataset 或关系重名
ontology_type_unresolved extends 成环,或值类型追溯不到内置类型
ontology_recursion_unsupported derived_by 规则引用了它所定义的关系
$ dosi describe --ontology fixtures/ossie-mini-flights/ontology.yaml --mapping nope
error: ontology could not be lowered:
  ontology_unknown_reference: no ontology mapping named "nope" (ontology_mappings)
    candidates: mini_flights_mapping

查询时,本体额外带来 value_not_in_domain,并为 invalid_relationship_path 填上 candidates。其余都是普通的查询错误。

限制

  • 寄居在别人表上的概念不能作查询根。 存在另一个概念表上的概念——如 AIRPORT 上的 StateCityMarket——可以经它的宿主访问(Airport.city.name), 但 select --from State 会被拒绝:在能去重之前,它的每一行会随机场重复出现。
  • n 元关系行级 derived_byMINUTES(a - b))不降级,分别出现在 unmappednot queryable 下。
  • 区间值域不强制检查(见值域)。
  • --order 接受输出列名,不接受概念路径。
  • paths 最多三跳。
  • 检索不是 dosi 的命令。 检索需要索引与存储;dosi 只编译不存储,describe 的输出正是上层编排建索引的原料。
  • REST API 不加载本体。Python 绑定可以加载(Engine(ontology_path=…)),但只有 lineage() 用到它:那里的 compileselect 不接受概念路径。