跳转到内容

语义层

语义层是你向 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") + entities, metrics, ...

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

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

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

str(ref) 返回 .id,因此引用可以用在任何接受字符串 id 的地方。 相等性和哈希基于 (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, timescope={...})

目录的 catalog.get("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
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>
defaultboolTrueTrue 时,本文件中的 decorators 在未传 domain= 时解析到该领域。
ai_contextAiContextValueNone面向智能体的上下文,通过 ms.ai_context(...) 构造。
import marivo.semantic as ms
ms.domain(name="sales")

实体是一个物理源(一张表或一个文件)加上它的主键。它是维度、 度量和指标依附的锚点。

参数类型必填默认含义
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) —— 按天分区的快照;读取最新分区。
  • ms.validity(valid_from, valid_to, interval, open_end, timezone=None) —— SCD2 有效性区间。interval"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 | str所属实体。
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 | str所属实体。
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 粒度时间-维度 id

timezone 默认为数据源引擎时区;只有当列的挂钟含义不同时才设置它(例如 "UTC")。 形如 (5, "minute")sample_interval 标记一个被周期采样的时间轴,供半可加 折叠使用。

# 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 | str所属实体。
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 | str要聚合的度量。
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 | str]请求体读取的实体。
additivity可加性值"additive""non_additive"ms.semi_additive(...)
root_entityEntityRef | str单个实体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)
  • 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 | str源实体。
to_entityEntityRef | str目标实体。
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)],
)

声明就绪后,加载目录并检查它:

import marivo.semantic as ms
catalog = ms.load() # SemanticCatalog
catalog.list().show() # everything, grouped
catalog.list(kind="metric").show() # just metrics
revenue = catalog.get("sales.revenue") # one object
region = catalog.get("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(...)

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