语义层
语义层用于向 Marivo 说明数据的含义。你用 Python 声明数据源、实体、
维度和指标;智能体通过 语义引用(形如 sales.revenue 的限定名)
引用这些对象,而不是直接使用原始表名和列名。
每个对象都遵循三条规则:
- Python 声明是契约。 被装饰的函数和构建器调用,是名称、定义和形状的 唯一来源。
- Ibis 表达式是执行语言。 装饰器请求体返回 ibis 表达式,不返回原始 SQL 字符串。
- SQL 文本只是元数据。 需要 SQL(用于一致性校验)时,将它放在
provenance=ms.from_sql(sql=..., dialect=...)中,不作为可执行请求体。
日常编写会用到两个命名空间:
import marivo.datasource as md # connections (md.duckdb, md.ref, ...)import marivo.semantic as ms # meaning (ms.entity, ms.metric, ...)每个对象都归属于一个 领域,并通过限定引用引用:
- 领域级对象:
<domain>.<object>—— 例如sales.revenue、sales.orders。 - 实体级对象(维度和度量):
<domain>.<entity>.<field>—— 例如sales.orders.region。
一个项目由一组声明文件组成。数据源在 models/datasources/ 下声明一次;
语义放在 models/semantic/<domain>/ 下,每个领域有一个 _domain.py:
your-project/ marivo.toml models/ datasources/ warehouse.py # md.duckdb(name="warehouse", ...) semantic/ sales/ _domain.py # ms.domain(name="sales", owner="Mina Zhang") + entities, metrics, ...在多仓库语义层场景中,每个业务域仓库仍保持相同的已编写 models/ 布局,
中央分析项目只需要引用这些根:
[semantic]layer_paths = ["../finance-domain/models"]Marivo 会先加载中央项目的 models/,再按配置顺序加载外部 models/ 根。
所有数据源、领域与语义对象会进入同一个目录,因此名称必须全局唯一。
marivo doctor、catalog.preview、ms.verify_object 与分析会话都会按同一组
配置根解析数据源-backed 语义对象。
每个语义对象都由一个 语义引用 标识。它是类型化、不可变的句柄,同时携带限定名
和对象种类。引用在编写阶段和分析循环中使用同一套类型:ms.entity(...) 调用、
catalog.get(...).ref 查找,以及分析意图参数,都属于同一个 SemanticRef 族。
SemanticRef 基类
Section titled “SemanticRef 基类”所有引用共享两个只读属性:
| 属性 | 类型 | 含义 |
|---|---|---|
.id | str | 限定语义 id(如 "sales.revenue")。 |
.kind | SemanticKind | 对象种类——以下八个值之一。 |
str(ref) 返回 .id,因此引用在接受语义 id 的读取/查询 API 中仍然方便。编写参数必须使用引用对象,不能传裸字符串。
相等性和哈希基于 (type, id),所以同子类同 id 的两个引用可互换。
按类型区分的子类
Section titled “按类型区分的子类”每个 SemanticKind 值对应一个具体的引用子类:
| 类型 | 子类 | 由谁返回 | 可调用? |
|---|---|---|---|
domain | DomainRef | ms.domain(...) | 否 |
datasource | DatasourceRef | md.ref(...) | 否 |
entity | EntityRef | ms.entity(...) | 否 |
dimension | DimensionRef | 直接列用 ms.dimension_column(...),表达式用 @ms.dimension | 是——在指标请求体中 |
time_dimension | TimeDimensionRef | 直接列用 ms.time_dimension_column(...),表达式用 @ms.time_dimension | 是——在指标请求体中 |
measure | MeasureRef | 直接列用 ms.measure_column(...),表达式用 @ms.measure | 是——在指标请求体中 |
metric | MetricRef | ms.aggregate(...)、ms.count(...)、@ms.metric、ms.ratio(...) 等 | 否 |
relationship | RelationshipRef | ms.relationship(...) | 否 |
可调用的字段引用(DimensionRef、MeasureRef、TimeDimensionRef)在指标请求体
内被调用时,会解析为 ibis 表达式。其他引用如果被误调用会抛出教学性错误:它们是身份令牌,
不是装饰器。
一个族,一个访问器
Section titled “一个族,一个访问器”因为编写引用和目录引用属于同一类型族,编写引用可以直接传给分析意图, 不需要额外包装:
revenue = ms.aggregate(name="revenue", measure=amount, agg="sum")# revenue 是一个 MetricRef——直接传给 observe:frame = session.observe(revenue, time_scope={...})目录的 catalog.get("metric.sales.revenue").ref 返回同一个 MetricRef 子类。不存在
.ref.ref 链,编写和分析之间也不会出现引用类型不匹配。
工厂与归一化器
Section titled “工厂与归一化器”mv.make_ref(id, kind)—— 内部使用;已从公开接口中移除。as_ref_id(value)—— 从SemanticRef、SemanticObject或纯str中提取.id字符串。 对字符串保持宽容:原始 id 会直接通过。
ai_context:人与智能体之间的契约
Section titled “ai_context:人与智能体之间的契约”每个语义对象和数据源都接受可选的 ai_context 参数,并通过 ms.ai_context(...)
构造。业务含义和约束放在这里;智能体使用对象前会读取这部分上下文。所有参数都是可选的,
但未知的关键字参数会在调用时被 Python 拒绝。
| 字段 | 类型 | 必填 | 默认 | 含义 |
|---|---|---|---|---|
business_definition | str | 否 | None | 用一两句话说明对象的业务含义。 |
guardrails | list[str] | 否 | [] | 智能体必须遵守的规则:必要的过滤、排除项、范围限制。 |
synonyms | list[str] | 否 | [] | 别名,方便智能体解析自然语言引用。 |
examples | list[str] | 否 | [] | 该对象能回答的示例问题或表述。 |
instructions | str | 否 | None | 关于如何(以及如何不)使用该对象的直接指引。 |
owner_notes | str | 否 | None | 来自人类负责人的备注:来源、注意事项、已知问题。 |
ai_context=ms.ai_context( business_definition="Gross order amount before refunds.", guardrails=["Validate refund exclusions before using as net revenue."], synonyms=["sales", "gmv"], examples=["What was revenue by region last week?"],)数据源在 models/datasources/*.py 中声明,每种后端对应一个类型化辅助函数。
辅助函数会注册连接。语义文件通过 md.ref("datasource.warehouse") 以类型-qualified 引用引用
数据源。
import marivo.datasource as mdimport marivo.semantic as ms
md.duckdb( name="warehouse", path="warehouse.duckdb", ai_context=ms.ai_context( business_definition="Local DuckDB warehouse for sales analysis.", guardrails=["Use only for development or approved local analysis."], ),)每个辅助函数(md.duckdb、md.mysql、md.postgres、md.trino、md.clickhouse)都共享以下参数:
| 参数 | 类型 | 必填 | 默认 | 含义 |
|---|---|---|---|---|
name | str | 是 | — | 全局数据源名称(字母、数字、_、-)。供 md.ref(name) 使用。 |
ai_context | AiContextValue | 否 | None | 面向智能体的上下文,通过 ms.ai_context(...) 构造。 |
extra | dict | 否 | None | 类型化辅助函数未建模的、罕见的 JSON 安全 ibis 关键字参数。 |
各后端特有的参数:
| 辅助函数 | 必填 | 可选 |
|---|---|---|
md.duckdb | — | path(默认 ":memory:")、read_only(默认 False) |
md.mysql | host、database | port(3306)、autocommit、user_env、password_env |
md.postgres | host、database | port(5432)、schema、autocommit、user_env、password_env |
md.trino | host、catalog | port(8080)、schema、source、timezone、http_scheme、client_tags、session_properties、user_env、auth_env |
md.clickhouse | host | port(9000 / 9440 secure)、database、secure、settings、user_env、password_env |
ClickHouse 数据源默认关闭 clickhouse-connect 自动生成的会话 id,这样分析
缓存后端时,重复查询或并发查询不会共享同一个 ClickHouse server 会话 lock。
如果数据源需要临时表等 ClickHouse 会话状态,可以显式传
extra={"autogenerate_session_id": True}。settings={...} 用于 server/查询
settings;Ibis/clickhouse-connect client keyword arguments 放在 extra={...}。
import marivo.datasource as md
md.trino( name="lake", host="trino.example.internal", catalog="hive", user_env="TRINO_USER", auth_env="TRINO_AUTH",)ms.domain(...) 打开一个命名空间。每个 _domain.py 调用一次。它返回一个
DomainRef,也可以作为 domain= 传入,用来覆盖同级文件中对象的活动领域。
| 参数 | 类型 | 必填 | 默认 | 含义 |
|---|---|---|---|---|
name | str | 是 | — | 领域命名空间,例如 "sales"。对象成为 <name>.<object>。 |
owner | str | 是 | — | 负责人姓名,负责该领域的语义正确性与质量,例如 "Mina Zhang"。 |
default | bool | 否 | True | 为 True 时,本文件中的 decorators 在未传 domain= 时解析到该领域。 |
ai_context | AiContextValue | 否 | None | 面向智能体的上下文,通过 ms.ai_context(...) 构造。 |
import marivo.semantic as ms
ms.domain(name="sales", owner="Mina Zhang")实体表示一个物理源(一张表或一个文件)及其主键。维度、度量和指标 都依附在实体上。
| 参数 | 类型 | 必填 | 默认 | 含义 |
|---|---|---|---|---|
name | str | 是 | — | 实体名称。成为 <domain>.<name>。 |
datasource | DatasourceRef | 是 | — | md.ref("datasource.warehouse")。 |
source | 来源构建器 | 是 | — | ms.table(...)、ms.parquet(...) 或 ms.csv(...)。 |
primary_key | list[str] | 否 | None | 构成主键的列名。 |
versioning | ms.snapshot | ms.validity | 否 | None | 快照或 SCD2 有效性版本控制(见下)。 |
domain | DomainRef | 否 | 文件默认 | 覆盖活动领域。 |
ai_context | AiContextValue | 否 | None | 面向智能体的上下文,通过 ms.ai_context(...) 构造。 |
warehouse = md.ref("datasource.warehouse")
orders = ms.entity( name="orders", datasource=warehouse, source=ms.table("orders"), primary_key=["order_id"], ai_context=ms.ai_context(business_definition="One row per order."),)来源 builders
Section titled “来源 builders”| 构建器 | 必填 | 可选 | 用于 |
|---|---|---|---|
ms.table(name) | name | database | 数据源中的一张表(Trino/MySQL 用 database="schema")。 |
ms.parquet(path) | path | hive_partitioning、columns | Parquet 文件(通常经 DuckDB)。 |
ms.csv(path) | path | header、delimiter、columns | CSV 文件(通常经 DuckDB)。 |
版本化(可选)
Section titled “版本化(可选)”对于行会随时间变化的实体,需要声明如何读取当前状态:
ms.snapshot(partition_field, grain="day", timezone=None, format=None)—— 按天分区的快照;partition_field必须是DimensionRef或TimeDimensionRef。ms.validity(valid_from, valid_to, interval, open_end, timezone=None)—— SCD2 有效性区间;valid_from和valid_to必须是DimensionRef或TimeDimensionRef。interval为"closed_open"([from, to))或"closed_closed";open_end列出表示“仍然有效”的哨兵值(例如 SQLNULL用(None,),或("9999-12-31",))。
维度是用于分组或过滤的分类属性。直接物理列使用 ms.dimension_column(...);需要表达式
(例如 table.region.upper())时,使用 @ms.dimension 装饰器,其请求体返回一个针对
实体表的 ibis 表达式。
| 参数 | 类型 | 必填 | 默认 | 含义 |
|---|---|---|---|---|
name | str | 是 | — | 维度名称。成为 <domain>.<entity>.<name>。 |
entity | EntityRef | 是 | — | 所属实体。 |
column | str | 是 | — | 实体表上的物理列名。 |
domain | DomainRef | 否 | 文件默认 | 覆盖活动领域。 |
ai_context | AiContextValue | 否 | None | 面向智能体的上下文,通过 ms.ai_context(...) 构造。 |
region = ms.dimension_column( name="region", entity=orders, column="region", ai_context=ms.ai_context(business_definition="Sales reporting region."),)时间维度是携带粒度和解析元数据的特殊维度。只有时间维度才能作为
session.observe 的时间轴。
| 参数 | 类型 | 必填 | 默认 | 含义 |
|---|---|---|---|---|
name | str | 是 | — | 维度名称。 |
entity | EntityRef | 是 | — | 所属实体。 |
column | str | 是 | — | 实体表上的物理列名。 |
granularity | 粒度字面量 | 是 | — | year、quarter、month、week、day、hour、minute 或 second —— 查询有意义的最细粒度。 |
parse | 解析变体 | 否 | None | 源列如何变成时间值(见下)。原生时间列可省略——分析时自动推断解析变体。 |
is_default | bool | 否 | False | 当实体有多个时间轴时,标记默认时间轴。省略 time_dimension= 时 observe 会使用它。 |
domain | DomainRef | 否 | 文件默认 | 覆盖活动领域。 |
ai_context | AiContextValue | 否 | None | 面向智能体的上下文,通过 ms.ai_context(...) 构造。 |
parse= 值声明列的物理编码。省略时,分析会根据列的 ibis dtype 自动推断解析变体(原生
date、datetime、timestamp 列无需显式指定解析)。对于字符串或整数列,需提供
ms.strptime(format) 或 ms.hour_prefix(prefix)。该变体必须与 granularity 兼容(例如
hour 粒度需要带时间的格式)。
| 构建器 | 源列是… | 关键参数 |
|---|---|---|
(省略 parse) | 原生时间列 | — |
ms.datetime() | 原生 datetime | timezone(IANA)、sample_interval |
ms.timestamp() | 原生 timestamp | timezone(IANA)、sample_interval |
ms.strptime(format) | 需要解析的字符串/整数 | timezone、sample_interval |
ms.hour_prefix(prefix) | 仅含小时的分区 | sample_interval —— prefix 是提供日期的 day 粒度 TimeDimensionRef |
timezone 默认为数据源引擎时区;只有当列的挂钟含义不同时才设置它(例如 "UTC")。
形如 (5, "minute") 的 sample_interval 标记一个被周期采样的时间轴,供半可加
折叠使用。
对于时间-series 和面板 frame,bucket_start 始终是报告时区下的桶
标签,并以时区-naive 时间戳或日期输出。例如 report_timezone="Asia/Shanghai"
时,上海业务日的第一个小时桶显示为 2026-06-20 00:00:00,而不是等价的 UTC instant。
# Day partition stored as the string "20260131"log_date = ms.time_dimension_column( name="log_date", entity=orders, column="dt", granularity="day", parse=ms.strptime("%Y%m%d"), is_default=True,)
# Native UTC timestamp, usable for sub-day bucketsevent_ts = ms.time_dimension_column( name="event_ts", entity=orders, column="event_ts", granularity="minute", parse=ms.timestamp(timezone="UTC"),)度量是你打算聚合的行级数量事实(例如金额或数量)。直接物理列使用
ms.measure_column(...);需要基于一个或多个列的表达式时使用 @ms.measure。度量
携带可加性和可选的单位。
| 参数 | 类型 | 必填 | 默认 | 含义 |
|---|---|---|---|---|
name | str | 是 | — | 度量名称。成为 <domain>.<entity>.<name>。 |
entity | EntityRef | 是 | — | 所属实体。 |
column | str | 是 | — | 实体表上的物理列名。 |
additivity | 可加性值 | 是 | — | "additive"、"non_additive" 或 ms.semi_additive(...)。 |
unit | str | 否 | None | UCUM 单位令牌:"USD"、"CNY"、"%"、"ms"、"{order}"。 |
domain | DomainRef | 否 | 文件默认 | 覆盖活动领域。 |
ai_context | AiContextValue | 否 | None | 面向智能体的上下文,通过 ms.ai_context(...) 构造。 |
amount = ms.measure_column( name="amount", entity=orders, column="amount", additivity="additive", unit="CNY",)指标是智能体分析时依赖的可信数值,可以直接用于分析。Marivo 提供几种编写形态, 按数值的计算方式选择。
来自度量的简单指标 —— ms.aggregate
Section titled “来自度量的简单指标 —— ms.aggregate”聚合一个度量。无需请求体;可加性从度量继承。
| 参数 | 类型 | 必填 | 默认 | 含义 |
|---|---|---|---|---|
name | str | 是 | — | 指标名称。 |
measure | MeasureRef | 是 | — | 要聚合的度量。 |
agg | 聚合方式 | 是 | — | "sum"、"mean"、"min"、"max" 等。 |
fold | 折叠 | 否 | None | 半可加度量的时间-折叠覆盖。 |
unit | str | 否 | 继承 | 覆盖从度量推导的单位。 |
domain / ai_context | — | 否 | — | 同其他对象。 |
revenue = ms.aggregate(name="revenue", measure=amount, agg="sum")Count 指标 —— ms.count
Section titled “Count 指标 —— ms.count”需要统计实体行数时使用 ms.count,不要为了计行数额外声明冗余度量。这个辅助函数
只接受引用,并会从实体引用推断指标所属领域。
order_count = ms.count(name="order_count", entity=orders, ai_context=ms.ai_context(business_definition="订单总数。"))来自 ibis 请求体的 tier-2 自定义指标 —— @ms.metric
Section titled “来自 ibis 请求体的 tier-2 自定义指标 —— @ms.metric”当指标无法用 ms.aggregate(...)、ms.count(...) 或派生指标构建器表达时,
才使用这个 escape hatch。请求体返回一个 ibis 聚合;additivity 由你显式声明。
| 参数 | 类型 | 必填 | 默认 | 含义 |
|---|---|---|---|---|
name | str | 否 | 函数名 | 指标名称。 |
entities | list[EntityRef] | 是 | — | 请求体读取的实体。 |
additivity | 可加性值 | 是 | — | "additive"、"non_additive" 或 ms.semi_additive(...)。 |
root_entity | EntityRef | 否 | 单个实体 | 当 entities 多于一个时必填。 |
fanout_policy | "block" | "aggregate_then_join" | 否 | "block" | 如何处理跨实体连接的 fan-out。 |
unit | str | 否 | None | UCUM 单位令牌。 |
provenance | SqlProvenance | 否 | None | 用于一致性校验的 ms.from_sql(sql=..., dialect=...)。 |
domain / ai_context | — | 否 | — | 同其他对象。 |
@ms.metric( entities=[orders], additivity="additive", name="revenue", provenance=ms.from_sql( sql="SELECT SUM(amount) AS revenue FROM orders", dialect="duckdb", ), ai_context=ms.ai_context(business_definition="Gross order amount before refunds."),)def revenue(table): return table.amount.sum()派生指标 —— ms.ratio / ms.weighted_average / ms.linear
Section titled “派生指标 —— ms.ratio / ms.weighted_average / ms.linear”派生指标由其他指标组合而成,不需要请求体;计算完全来自组成成分。
| 构建器 | 必填 | 计算 |
|---|---|---|
ms.ratio(name, numerator, denominator) | 两个引用 | numerator / denominator(如客单价、各类比率) |
ms.weighted_average(name, value, weight) | 两个引用 | 加权平均;之后 attribute 拆分混合与 rate |
ms.linear(name, add, subtract) | add(共 ≥2 项) | add 之和减去 subtract(如 net = gross - refunds) |
它们也都接受 unit、domain 和 ai_context。
net_revenue = ms.linear(name="net_revenue", add=[gross_revenue], subtract=[refunds])aov = ms.ratio(name="aov", numerator=total_amount, denominator=orders_count)累计指标 —— ms.cumulative
Section titled “累计指标 —— ms.cumulative”当业务问题是”截至桶 t 累计了多少”时使用 ms.cumulative(...)。
基础必须是使用 sum、count 或 count_distinct 的 tier-1 ms.aggregate(...) 或 ms.count(...) 指标。
累计锚点是全部历史:observe 窗口裁剪展示行,但不会重置累计值。
| 参数 | 类型 | 必填 | 默认 | 含义 |
|---|---|---|---|---|
name | str | 是 | — | 语义指标名称。 |
base | MetricRef | 是 | — | 使用 sum、count 或 count_distinct 的 tier-1 简单聚合指标。 |
over | TimeDimensionRef | None | 否 | None | 累计时间轴。除非基础根实体只有一个时间维度,否则需显式传入。 |
unit / domain / ai_context | — | 否 | — | 同其他指标。 |
user_id = ms.measure_column(name="user_id", entity=events, column="user_id", additivity="non_additive")active_users = ms.aggregate(name="active_users", measure=user_id, agg="count_distinct")cumulative_active_users = ms.cumulative( name="cumulative_active_users", base=active_users, over=event_time,)对于 count_distinct 基础指标,累计值使用首次出现语义:每个去重实体在最早出现的桶
中计入,因此累计值单调非递减。累计指标可作为比率组成成分——用 ms.ratio(...) 组合累计
分子和分母可得到累计比率。
可加性与来源信息辅助器
Section titled “可加性与来源信息辅助器”ms.semi_additive(over, fold)—— 用于在大多数轴上可加、但需要在某个时间轴上折叠的 快照/状态事实。over必须是@ms.time_dimension(...)返回的状态时间 维度引用;fold为"last"、"first"、"mean"、"max"或("percentile", 0.95)。ms.from_sql(sql, dialect)—— 把 SQL 作为仅来源信息 附加,用于启用ms.parity_check(...)。它永远不会作为指标请求体执行。
snapshot_date = ms.time_dimension_column( name="snapshot_date", entity=inventory_daily, column="snapshot_date", granularity="day",)
on_hand_units = ms.measure_column( name="on_hand_units", entity=inventory_daily, column="on_hand_units", additivity=ms.semi_additive(over=snapshot_date, fold="last"),)关系声明两个实体如何连接,使指标和维度能够跨越这两个实体。 键使用维度引用,而不是原始列名。
| 参数 | 类型 | 必填 | 默认 | 含义 |
|---|---|---|---|---|
name | str | 是 | — | 关系名称。 |
from_entity | EntityRef | 是 | — | 源实体。 |
to_entity | EntityRef | 是 | — | 目标实体。 |
keys | list[JoinKey] | 是 | — | 一个或多个 ms.join_on(from_key, to_key) 配对。 |
domain / ai_context | — | 否 | — | 同其他对象。 |
ms.relationship( name="orders_to_customers", from_entity=orders, to_entity=customers, keys=[ms.join_on(order_customer_id, customer_id)],)先发现,再准备
Section titled “先发现,再准备”对数据源-backed 语义对象,先 inspect 表和分区元信息,再用显式分区范围收集有界
发现证据。查阅 md.help("authoring") 了解分阶段数据源工作流
(register/test/inspect 再 discover -> settle -> author -> 验证),查阅
ms.help("authoring") 了解语义依赖阶梯,查阅 md.help("ai_context") 了解共享的
ms.ai_context(...) 契约:
warehouse = md.ref("datasource.warehouse")orders = md.table("orders")
md.inspect_table(warehouse, orders).show()md.inspect_partitions(warehouse, orders).show()
scope = md.partition({"dt": "20260629"})
entity_evidence = md.discover_entity(warehouse, orders, scope=scope)dimension_evidence = md.discover_dimensions( warehouse, orders, scope=scope,)measure_evidence = md.discover_measures( warehouse, orders, columns=("amount",), scope=scope,)
entity_evidence.show()dimension_evidence.show()measure_evidence.show()发现返回有界证据、signals 和问题;它不会替你决定业务含义、单位、可加性,
或自动创建语义对象。智能体通过 .show() / .render() 阅读发现证据;
具体结果 class 和内部证据字段不是公开编写输入。ms.help(...)
告诉智能体哪些参数必须确定(静态编写契约),发现提供运行时证据。
一次只 author 一个对象,然后运行 ms.verify_object(...)。
加载与就绪检查
Section titled “加载与就绪检查”声明完成后,加载目录并检查它:
import marivo.semantic as ms
catalog = ms.load() # SemanticCatalogcatalog.list("domain").show() # top-level domainscatalog.list("metric").show() # just metrics across all domainsrevenue = catalog.get("metric.sales.revenue") # one objectregion = catalog.get("dimension.sales.orders.region") # also an object在任何分析之前,运行就绪检查。这是把未完整声明的对象挡在分析之外的结构性门禁:
report = ms.readiness()if report.status == "blocked": report.show() # blockers, with the next step for each另外两个检查支持编写:
ms.richness()—— 建议性的覆盖度/深度报告;永不阻塞。ms.parity_check("sales.revenue")—— 用指标的provenanceSQL 运行并比对结果。 需要provenance=ms.from_sql(...)。