跳转到内容

语义层

语义层用于向 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.revenuesales.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 doctorcatalog.previewms.verify_object 与分析会话都会按同一组 配置根解析数据源-backed 语义对象。

每个语义对象都由一个 语义引用 标识。它是类型化、不可变的句柄,同时携带限定名 和对象种类。引用在编写阶段和分析循环中使用同一套类型:ms.entity(...) 调用、 catalog.get(...).ref 查找,以及分析意图参数,都属于同一个 SemanticRef 族。

所有引用共享两个只读属性:

属性类型含义
.idstr限定语义 id(如 "sales.revenue")。
.kindSemanticKind对象种类——以下八个值之一。

str(ref) 返回 .id,因此引用在接受语义 id 的读取/查询 API 中仍然方便。编写参数必须使用引用对象,不能传裸字符串。 相等性和哈希基于 (type, id),所以同子类同 id 的两个引用可互换。

每个 SemanticKind 值对应一个具体的引用子类:

类型子类由谁返回可调用?
domainDomainRefms.domain(...)
datasourceDatasourceRefmd.ref(...)
entityEntityRefms.entity(...)
dimensionDimensionRef直接列用 ms.dimension_column(...),表达式用 @ms.dimension是——在指标请求体中
time_dimensionTimeDimensionRef直接列用 ms.time_dimension_column(...),表达式用 @ms.time_dimension是——在指标请求体中
measureMeasureRef直接列用 ms.measure_column(...),表达式用 @ms.measure是——在指标请求体中
metricMetricRefms.aggregate(...)ms.count(...)@ms.metricms.ratio(...)
relationshipRelationshipRefms.relationship(...)

可调用的字段引用(DimensionRefMeasureRefTimeDimensionRef)在指标请求体 内被调用时,会解析为 ibis 表达式。其他引用如果被误调用会抛出教学性错误:它们是身份令牌, 不是装饰器。

因为编写引用和目录引用属于同一类型族,编写引用可以直接传给分析意图, 不需要额外包装:

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 链,编写和分析之间也不会出现引用类型不匹配。

  • mv.make_ref(id, kind) —— 内部使用;已从公开接口中移除。
  • as_ref_id(value) —— 从 SemanticRefSemanticObject 或纯 str 中提取 .id 字符串。 对字符串保持宽容:原始 id 会直接通过。

ai_context:人与智能体之间的契约

Section titled “ai_context:人与智能体之间的契约”

每个语义对象和数据源都接受可选的 ai_context 参数,并通过 ms.ai_context(...) 构造。业务含义和约束放在这里;智能体使用对象前会读取这部分上下文。所有参数都是可选的, 但未知的关键字参数会在调用时被 Python 拒绝。

字段类型必填默认含义
business_definitionstrNone用一两句话说明对象的业务含义。
guardrailslist[str][]智能体必须遵守的规则:必要的过滤、排除项、范围限制。
synonymslist[str][]别名,方便智能体解析自然语言引用。
exampleslist[str][]该对象能回答的示例问题或表述。
instructionsstrNone关于如何(以及如何不)使用该对象的直接指引。
owner_notesstrNone来自人类负责人的备注:来源、注意事项、已知问题。
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 引用引用 数据源。

models/datasources/warehouse.py
import marivo.datasource as md
import 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.duckdbmd.mysqlmd.postgresmd.trinomd.clickhouse)都共享以下参数:

参数类型必填默认含义
namestr全局数据源名称(字母、数字、_-)。供 md.ref(name) 使用。
ai_contextAiContextValueNone面向智能体的上下文,通过 ms.ai_context(...) 构造。
extradictNone类型化辅助函数未建模的、罕见的 JSON 安全 ibis 关键字参数。

各后端特有的参数:

辅助函数必填可选
md.duckdbpath(默认 ":memory:")、read_only(默认 False
md.mysqlhostdatabaseport(3306)、autocommituser_envpassword_env
md.postgreshostdatabaseport(5432)、schemaautocommituser_envpassword_env
md.trinohostcatalogport(8080)、schemasourcetimezonehttp_schemeclient_tagssession_propertiesuser_envauth_env
md.clickhousehostport(9000 / 9440 secure)、databasesecuresettingsuser_envpassword_env

ClickHouse 数据源默认关闭 clickhouse-connect 自动生成的会话 id,这样分析 缓存后端时,重复查询或并发查询不会共享同一个 ClickHouse server 会话 lock。 如果数据源需要临时表等 ClickHouse 会话状态,可以显式传 extra={"autogenerate_session_id": True}settings={...} 用于 server/查询 settings;Ibis/clickhouse-connect client keyword arguments 放在 extra={...}

models/datasources/lake.py
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= 传入,用来覆盖同级文件中对象的活动领域。

参数类型必填默认含义
namestr领域命名空间,例如 "sales"。对象成为 <name>.<object>
ownerstr负责人姓名,负责该领域的语义正确性与质量,例如 "Mina Zhang"
defaultboolTrueTrue 时,本文件中的 decorators 在未传 domain= 时解析到该领域。
ai_contextAiContextValueNone面向智能体的上下文,通过 ms.ai_context(...) 构造。
import marivo.semantic as ms
ms.domain(name="sales", owner="Mina Zhang")

实体表示一个物理源(一张表或一个文件)及其主键。维度、度量和指标 都依附在实体上。

参数类型必填默认含义
namestr实体名称。成为 <domain>.<name>
datasourceDatasourceRefmd.ref("datasource.warehouse")
source来源构建器ms.table(...)ms.parquet(...)ms.csv(...)
primary_keylist[str]None构成主键的列名。
versioningms.snapshot | ms.validityNone快照或 SCD2 有效性版本控制(见下)。
domainDomainRef文件默认覆盖活动领域。
ai_contextAiContextValueNone面向智能体的上下文,通过 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."),
)
构建器必填可选用于
ms.table(name)namedatabase数据源中的一张表(Trino/MySQL 用 database="schema")。
ms.parquet(path)pathhive_partitioningcolumnsParquet 文件(通常经 DuckDB)。
ms.csv(path)pathheaderdelimitercolumnsCSV 文件(通常经 DuckDB)。

对于行会随时间变化的实体,需要声明如何读取当前状态:

  • ms.snapshot(partition_field, grain="day", timezone=None, format=None) —— 按天分区的快照;partition_field 必须是 DimensionRefTimeDimensionRef
  • ms.validity(valid_from, valid_to, interval, open_end, timezone=None) —— SCD2 有效性区间;valid_fromvalid_to 必须是 DimensionRefTimeDimensionRefinterval"closed_open"[from, to))或 "closed_closed"open_end 列出表示“仍然有效”的哨兵值(例如 SQL NULL(None,),或 ("9999-12-31",))。

维度是用于分组或过滤的分类属性。直接物理列使用 ms.dimension_column(...);需要表达式 (例如 table.region.upper())时,使用 @ms.dimension 装饰器,其请求体返回一个针对 实体表的 ibis 表达式。

参数类型必填默认含义
namestr维度名称。成为 <domain>.<entity>.<name>
entityEntityRef所属实体。
columnstr实体表上的物理列名。
domainDomainRef文件默认覆盖活动领域。
ai_contextAiContextValueNone面向智能体的上下文,通过 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 的时间轴。

参数类型必填默认含义
namestr维度名称。
entityEntityRef所属实体。
columnstr实体表上的物理列名。
granularity粒度字面量yearquartermonthweekdayhourminutesecond —— 查询有意义的最细粒度。
parse解析变体None源列如何变成时间值(见下)。原生时间列可省略——分析时自动推断解析变体。
is_defaultboolFalse当实体有多个时间轴时,标记默认时间轴。省略 time_dimension=observe 会使用它。
domainDomainRef文件默认覆盖活动领域。
ai_contextAiContextValueNone面向智能体的上下文,通过 ms.ai_context(...) 构造。

parse= 值声明列的物理编码。省略时,分析会根据列的 ibis dtype 自动推断解析变体(原生 datedatetimetimestamp 列无需显式指定解析)。对于字符串或整数列,需提供 ms.strptime(format)ms.hour_prefix(prefix)。该变体必须与 granularity 兼容(例如 hour 粒度需要带时间的格式)。

构建器源列是…关键参数
(省略 parse原生时间列
ms.datetime()原生 datetimetimezone(IANA)、sample_interval
ms.timestamp()原生 timestamptimezone(IANA)、sample_interval
ms.strptime(format)需要解析的字符串/整数timezonesample_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 buckets
event_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。度量 携带可加性和可选的单位。

参数类型必填默认含义
namestr度量名称。成为 <domain>.<entity>.<name>
entityEntityRef所属实体。
columnstr实体表上的物理列名。
additivity可加性值"additive""non_additive"ms.semi_additive(...)
unitstrNoneUCUM 单位令牌:"USD""CNY""%""ms""{order}"
domainDomainRef文件默认覆盖活动领域。
ai_contextAiContextValueNone面向智能体的上下文,通过 ms.ai_context(...) 构造。
amount = ms.measure_column(
name="amount",
entity=orders,
column="amount",
additivity="additive",
unit="CNY",
)

指标是智能体分析时依赖的可信数值,可以直接用于分析。Marivo 提供几种编写形态, 按数值的计算方式选择。

来自度量的简单指标 —— ms.aggregate

Section titled “来自度量的简单指标 —— ms.aggregate”

聚合一个度量。无需请求体;可加性从度量继承。

参数类型必填默认含义
namestr指标名称。
measureMeasureRef要聚合的度量。
agg聚合方式"sum""mean""min""max" 等。
fold折叠None半可加度量的时间-折叠覆盖。
unitstr继承覆盖从度量推导的单位。
domain / ai_context同其他对象。
revenue = ms.aggregate(name="revenue", measure=amount, agg="sum")

需要统计实体行数时使用 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 由你显式声明。

参数类型必填默认含义
namestr函数名指标名称。
entitieslist[EntityRef]请求体读取的实体。
additivity可加性值"additive""non_additive"ms.semi_additive(...)
root_entityEntityRef单个实体entities 多于一个时必填。
fanout_policy"block" | "aggregate_then_join""block"如何处理跨实体连接的 fan-out。
unitstrNoneUCUM 单位令牌。
provenanceSqlProvenanceNone用于一致性校验的 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

它们也都接受 unitdomainai_context

net_revenue = ms.linear(name="net_revenue", add=[gross_revenue], subtract=[refunds])
aov = ms.ratio(name="aov", numerator=total_amount, denominator=orders_count)

当业务问题是”截至桶 t 累计了多少”时使用 ms.cumulative(...)。 基础必须是使用 sumcountcount_distinct 的 tier-1 ms.aggregate(...)ms.count(...) 指标。 累计锚点是全部历史:observe 窗口裁剪展示行,但不会重置累计值。

参数类型必填默认含义
namestr语义指标名称。
baseMetricRef使用 sumcountcount_distinct 的 tier-1 简单聚合指标。
overTimeDimensionRef | NoneNone累计时间轴。除非基础根实体只有一个时间维度,否则需显式传入。
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(...) 组合累计 分子和分母可得到累计比率。

  • 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"),
)

关系声明两个实体如何连接,使指标和维度能够跨越这两个实体。 键使用维度引用,而不是原始列名。

参数类型必填默认含义
namestr关系名称。
from_entityEntityRef源实体。
to_entityEntityRef目标实体。
keyslist[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)],
)

对数据源-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(...)

声明完成后,加载目录并检查它:

import marivo.semantic as ms
catalog = ms.load() # SemanticCatalog
catalog.list("domain").show() # top-level domains
catalog.list("metric").show() # just metrics across all domains
revenue = catalog.get("metric.sales.revenue") # one object
region = 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") —— 用指标的 provenance SQL 运行并比对结果。 需要 provenance=ms.from_sql(...)

就绪检查如何判断“是否就绪”,见 就绪检查。 分析如何记录结论,见 证据链。 然后继续阅读 分析流程