query-semantic-model
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseQuery Semantic Model
查询 Semantic Model
Answer business questions by querying the org's semantic model and rendering the result
directly in the chat — no notebook, no UI hop. The CLI already exposes everything you
need: lists the available cubes / measures / dimensions, and
runs a Cube.js query and returns the rows.
revos cubes metarevos cubes queryThis skill is the "last mile" after Bronze → Silver → Gold → Cubes have been built and
applied. It turns the semantic model into something you can actually look at.
通过查询组织的 semantic model 来回答业务问题,并直接将结果渲染到聊天中——无需
notebook,无需切换到 UI。CLI 已经提供所需的一切: 列出可用的
cubes / measures / dimensions,而 执行 Cube.js query 并返回数据行。
revos cubes metarevos cubes query这个 skill 是完成 Bronze → Silver → Gold → Cubes 构建与 apply 后的“最后一公里”。它让
semantic model 变成真正可供查看和分析的内容。
Step 1: Discover what's queryable
第 1 步:发现可查询的内容
Before guessing member names, list what the org's semantic model actually exposes:
bash
revos cubes meta --jsonThe response carries an array of cubes, each with , , ,
and a . Cube members are addressed as — for example
or .
measuresdimensionssegmentsname<CubeName>.<member>gold_order_items_enriched.countgold_order_items_enriched.order_datePick the cube and members that match the user's question. If the question can't be
answered from the available members, say so — don't invent member names.
在猜测 member 名称之前,先列出组织的 semantic model 实际暴露了哪些内容:
bash
revos cubes meta --json响应包含一个 cubes 数组,每个 cube 都有 、、 和
。Cube members 通过 寻址——例如
或 。
measuresdimensionssegmentsname<CubeName>.<member>gold_order_items_enriched.countgold_order_items_enriched.order_date选择与用户问题匹配的 cube 和 members。如果可用 members 无法回答该问题,就直接说明——
不要虚构 member 名称。
Map the join graph from the local cube files
从本地 cube 文件映射 join graph
revos cubes metacubes/*.ymlcubes/joins:yaml
undefinedrevos cubes metacubes/cubes/*.ymljoins:yaml
undefinedcubes/gold_order_items_enriched.yml
cubes/gold_order_items_enriched.yml
apiVersion: revos/v1
kind: Cube
metadata:
name: gold_order_items_enriched
spec:
name: gold_order_items_enriched
joins:
gold_users_with_order_stats:
relationship: many_to_one
sql: "${CUBE}.user_id = ${gold_users_with_order_stats}.user_id"
gold_product_performance:
relationship: many_to_one
sql: "${CUBE}.product_id = ${gold_product_performance}.product_id"
From this you can compute, for any pair of cubes, how many distinct join paths exist
between them. That's what lets you pick the right path in Step 2 instead of letting
Cube.js guess one for you.
**Caveat — system cubes are not in `cubes/`.** RevOS generates a handful of cubes
server-side (scoring, segments, model overlays, pre-aggregation overlays). Those
never appear as YAML in the project. The full member list is only visible through
`revos cubes meta`. So:
- Use the `cubes/` folder to **see the user-defined join graph**.
- Use `revos cubes meta` to **see every member that can actually be queried**,
including system cubes.
When the user's question touches a system cube (e.g. "show me users by segment",
"top scoring entities"), the join graph from `cubes/` will be incomplete. Don't
assume there's no path — list the meta and look for it there.
---apiVersion: revos/v1
kind: Cube
metadata:
name: gold_order_items_enriched
spec:
name: gold_order_items_enriched
joins:
gold_users_with_order_stats:
relationship: many_to_one
sql: "${CUBE}.user_id = ${gold_users_with_order_stats}.user_id"
gold_product_performance:
relationship: many_to_one
sql: "${CUBE}.product_id = ${gold_product_performance}.product_id"
基于这些信息,可以计算任意两个 cube 之间存在多少条不同的 join paths。这样便能在
第 2 步选择正确的 path,而不必让 Cube.js 自行猜测。
**注意——system cubes 不在 `cubes/` 中。** RevOS 会在 server-side 生成少量 cubes
(scoring、segments、model overlays、pre-aggregation overlays)。它们不会以 YAML 形式
出现在项目中。只有通过 `revos cubes meta` 才能看到完整的 member 列表。因此:
- 使用 `cubes/` 文件夹查看**用户定义的 join graph**。
- 使用 `revos cubes meta` 查看**所有实际可查询的 members**,包括 system cubes。
当用户的问题涉及 system cube(例如“按 segment 展示 users”“得分最高的 entities”)时,
`cubes/` 中的 join graph 并不完整。不要假定不存在 path——列出 meta 并在其中查找。
---Step 2: Build the query — and pin the join path when it matters
第 2 步:构建查询——必要时固定 join path
A Cube.js query is a JSON object. The shapes you'll use most often:
json
{ "measures": ["gold_order_items_enriched.count"] }json
{
"measures": ["gold_order_items_enriched.count"],
"dimensions": ["gold_order_items_enriched.traffic_source"],
"order": { "gold_order_items_enriched.count": "desc" },
"limit": 10
}Time series (the chart-friendliest shape):
json
{
"measures": ["gold_order_items_enriched.revenue"],
"timeDimensions": [
{
"dimension": "gold_order_items_enriched.order_date",
"granularity": "month",
"dateRange": "last 12 months"
}
]
}Rules of thumb:
- One measure + one dimension → bar chart by category.
- One measure + one with
timeDimensions→ bar chart over time.granularity - Multiple measures or multiple dimensions → render as an ASCII table only; don't try to draw a chart.
- Always cap with (default 20) so a stray query can't return a million rows.
limit
Cube.js query 是一个 JSON 对象。最常用的结构如下:
json
{ "measures": ["gold_order_items_enriched.count"] }json
{
"measures": ["gold_order_items_enriched.count"],
"dimensions": ["gold_order_items_enriched.traffic_source"],
"order": { "gold_order_items_enriched.count": "desc" },
"limit": 10
}时间序列(最适合绘制图表的结构):
json
{
"measures": ["gold_order_items_enriched.revenue"],
"timeDimensions": [
{
"dimension": "gold_order_items_enriched.order_date",
"granularity": "month",
"dateRange": "last 12 months"
}
]
}经验规则:
- 一个 measure + 一个 dimension → 按 category 绘制条形图。
- 一个 measure + 一个带 的
granularity→ 按时间绘制条形图。timeDimensions - 多个 measures 或多个 dimensions → 仅渲染为 ASCII 表格;不要尝试绘制图表。
- 始终使用 限制结果(默认 20),避免异常查询返回上百万行数据。
limit
Join paths — and how to pin them with a hint
Join paths——以及如何使用 hint 固定它们
Cube.js infers the join path from the cubes that appear in /
/ . When more than one path connects them, Cube.js picks
one for you — and the answer changes depending on which. This is the single
biggest source of "the number looks wrong" bugs in a semantic-model query.
measuresdimensionsfiltersYou spell the path out by prefixing the member with the cubes you want Cube.js to
walk through, dot-separated. The last segment is the actual member; everything
before it is the join hint. Examples:
text
undefinedCube.js 会根据出现在 / / 中的 cubes 推断 join path。
**当多个 path 将它们连接起来时,Cube.js 会替你选择一个——而不同 path 会产生不同
结果。**这是 semantic model query 中“数字看起来不对”问题的最大来源。
measuresdimensionsfilters固定 path 的方法是在 member 前添加希望 Cube.js 遍历的 cubes,并使用点号分隔。最后一段
是实际的 member,前面的所有内容都是 join hint。例如:
text
undefinedno hint — Cube.js picks a path
no hint — Cube.js picks a path
gold_users_with_order_stats.lifetime_revenue
gold_users_with_order_stats.lifetime_revenue
hint: reach lifetime_revenue via the fact spine, then the users mart
hint: reach lifetime_revenue via the fact spine, then the users mart
gold_order_items_enriched.gold_users_with_order_stats.lifetime_revenue
The hint syntax works the same on measures, dimensions, and filter members.
**When you need a hint (red flags):**
1. Two cubes in the query are connected by **more than one path** in `cubes/`.
Common shape: a fact spine joins to two dimension cubes, and one of the dimension
cubes _also_ joins to the other (e.g. orders → users, orders → products,
products → users via `last_purchaser_id`). Without a hint, Cube.js may pick the
non-obvious path and quietly double- or under-count.
2. The user's question implicitly names the path ("revenue **per order item** by
user country" vs "revenue **per user** by user country") — different grains,
different paths, different numbers.
3. Counts come back smaller than the smallest cube would allow, or larger than the
largest fact would allow. That's a sign Cube.js fanned out through the wrong
join.
4. The cube has a many-to-many relationship in the chain. Always pin it.
5. You added a measure and the number changed dramatically (more than ~10%) without
the dimension set changing — Cube.js probably re-routed the join.
**Worked example — same question, two answers.**
Cubes (from `cubes/`):
```text
gold_order_items_enriched ──many_to_one──▶ gold_users_with_order_stats
gold_order_items_enriched ──many_to_one──▶ gold_product_performance
gold_product_performance ──many_to_one──▶ gold_users_with_order_stats (last_purchaser_id)Question: "Total revenue by user country."
json
// Path A — order items → users directly (CORRECT for per-item revenue)
{
"measures": ["gold_order_items_enriched.revenue"],
"dimensions": [
"gold_order_items_enriched.gold_users_with_order_stats.country"
]
}json
// Path B — order items → product → last_purchaser (WRONG: revenue gets
// attributed to the last buyer's country, not the actual buyer's)
{
"measures": ["gold_order_items_enriched.revenue"],
"dimensions": [
"gold_order_items_enriched.gold_product_performance.gold_users_with_order_stats.country"
]
}The two queries return wildly different numbers. Always state which path you used
and why in the explanation underneath the chart so the user can spot a wrong
choice.
Worked example — filter via a specific path.
json
{
"measures": ["gold_order_items_enriched.revenue"],
"dimensions": ["gold_order_items_enriched.traffic_source"],
"filters": [
{
"member": "gold_order_items_enriched.gold_users_with_order_stats.country",
"operator": "equals",
"values": ["DE"]
}
]
}Rules for choosing a path:
- Default to the shortest path that goes through the fact spine the
question is naturally grained on (revenue per order → spine =
; users with X → spine =
gold_order_items_enriched).gold_users_with_order_stats - Never silently take the alternate path. If two paths are plausible, ask the user which grain they meant before running the query.
- If returns a member name that already contains dots (e.g.
revos cubes meta), that is the qualified form — paste it through verbatim, don't strip the prefix.cubeA.cubeB.foo
gold_order_items_enriched.gold_users_with_order_stats.lifetime_revenue
hint 语法同样适用于 measures、dimensions 和 filter members。
**需要使用 hint 的危险信号:**
1. 查询中的两个 cubes 在 `cubes/` 中通过**多条 path** 连接。常见结构是一个 fact spine
同时连接两个 dimension cubes,而其中一个 dimension cube 又会连接另一个(例如
orders → users、orders → products,以及 products → users via
`last_purchaser_id`)。没有 hint 时,Cube.js 可能选择不明显的 path,并悄悄造成
重复计数或漏计。
2. 用户的问题隐含指定了 path(“按 user country 统计每个 order item 的 revenue”与
“按 user country 统计每个 user 的 revenue”)——grain、path 和结果数字均不相同。
3. 返回的计数小于最小 cube 所允许的数量,或大于最大 fact 所允许的数量。这说明
Cube.js 沿错误的 join 发生了 fan-out。
4. chain 中存在 many-to-many relationship。此时必须固定 path。
5. 添加 measure 后,在 dimension 集合未改变的情况下,数字却发生了剧烈变化(超过约
10%)——Cube.js 很可能重新选择了 join path。
**示例——同一个问题,两种答案。**
Cubes(来自 `cubes/`):
```text
gold_order_items_enriched ──many_to_one──▶ gold_users_with_order_stats
gold_order_items_enriched ──many_to_one──▶ gold_product_performance
gold_product_performance ──many_to_one──▶ gold_users_with_order_stats (last_purchaser_id)问题:“按 user country 统计总 revenue。”
json
// Path A — order items → users directly (CORRECT for per-item revenue)
{
"measures": ["gold_order_items_enriched.revenue"],
"dimensions": [
"gold_order_items_enriched.gold_users_with_order_stats.country"
]
}json
// Path B — order items → product → last_purchaser (WRONG: revenue gets
// attributed to the last buyer's country, not the actual buyer's)
{
"measures": ["gold_order_items_enriched.revenue"],
"dimensions": [
"gold_order_items_enriched.gold_product_performance.gold_users_with_order_stats.country"
]
}这两个查询返回的数字差异巨大。始终在图表下方的说明中指出使用了哪条 path
以及为什么使用它,方便用户发现选择错误。
示例——通过特定 path 过滤。
json
{
"measures": ["gold_order_items_enriched.revenue"],
"dimensions": ["gold_order_items_enriched.traffic_source"],
"filters": [
{
"member": "gold_order_items_enriched.gold_users_with_order_stats.country",
"operator": "equals",
"values": ["DE"]
}
]
}选择 path 的规则:
- 默认选择最短的 path,并使其穿过问题自然 grain 所对应的 fact spine
(revenue per order → spine = ;users with X → spine =
gold_order_items_enriched)。gold_users_with_order_stats - 绝不静默选择另一条 path。如果两条 path 都合理,应在执行查询前询问用户所指的 grain。
- 如果 返回的 member 名称已经包含点号(例如
revos cubes meta),那么它就是 qualified form——应原样传入,不要去掉前缀。cubeA.cubeB.foo
Step 3: Run the query
第 3 步:执行查询
bash
revos cubes query --query '<INLINE_JSON>' --jsonFor anything longer than a one-liner, write the JSON to a temp file and pass it with
:
@bash
cat > /tmp/q.json <<'EOF'
{
"measures": ["gold_order_items_enriched.revenue"],
"timeDimensions": [
{
"dimension": "gold_order_items_enriched.order_date",
"granularity": "month",
"dateRange": "last 12 months"
}
]
}
EOF
revos cubes query --query @/tmp/q.json --jsonThe response is the raw Cube.js payload — is the array of rows, each row is a
flat object keyed by the same member names used in the query.
dataIf the API returns an error, surface the message verbatim. Common causes: a member
name typo (re-check ), a cube that hasn't been applied yet
( / ), or a against a column that isn't a time
dimension.
revos cubes metarevos statusrevos applydateRangebash
revos cubes query --query '<INLINE_JSON>' --json对于超过一行的内容,将 JSON 写入临时文件,并通过 传入:
@bash
cat > /tmp/q.json <<'EOF'
{
"measures": ["gold_order_items_enriched.revenue"],
"timeDimensions": [
{
"dimension": "gold_order_items_enriched.order_date",
"granularity": "month",
"dateRange": "last 12 months"
}
]
}
EOF
revos cubes query --query @/tmp/q.json --json响应是原始的 Cube.js payload—— 是数据行数组,每一行都是一个扁平对象,键名与
查询中使用的 member 名称相同。
data如果 API 返回错误,请原样呈现错误消息。常见原因包括:member 名称拼写错误(重新检查
)、cube 尚未 apply( / ),或者对不是 time
dimension 的列使用了 。
revos cubes metarevos statusrevos applydateRangeStep 4: Render the result in chat
第 4 步:在聊天中渲染结果
Print two blocks in the assistant's reply, both as fenced code so they render
monospaced:
在 assistant 的回复中输出两个代码块,均使用 fenced code,以确保以等宽字体呈现:
4a. ASCII table — always
4a. ASCII 表格——始终显示
Show every returned row. Right-align numeric columns, left-align everything else.
Format numbers with thousands separators; round to 2 decimals when not integral.
Truncate long string values to 32 chars with a trailing .
…traffic_source count
───────────────── ───────
Search 142,318
Organic 88,204
Email 41,907
Facebook 22,015
Display 9,471展示返回的每一行。数值列右对齐,其他内容左对齐。数字使用千位分隔符;非整数四舍五入
到 2 位小数。过长字符串值截断为 32 个字符,并添加末尾的 。
…traffic_source count
───────────────── ───────
Search 142,318
Organic 88,204
Email 41,907
Facebook 22,015
Display 9,4714b. ASCII bar chart — when the shape allows
4b. ASCII 条形图——在数据结构允许时显示
If the query returned exactly one measure plus exactly one dimension (categorical or
time), draw a horizontal bar chart underneath the table. Scale bars to a width of
40 characters based on the largest value in the result set; use the block
character for filled cells and a single trailing space.
█Search ████████████████████████████████████████ 142,318
Organic ████████████████████████▊ 88,204
Email ███████████▊ 41,907
Facebook ██████▏ 22,015
Display ██▋ 9,471Time-series example (oldest → newest, top-to-bottom):
2025-06 ███████████▊ € 41,200
2025-07 █████████████████▏ € 59,800
2025-08 ████████████████████▋ € 72,400
…Skip the chart (table only) when:
- The result set is empty.
- The query has 2+ measures, 2+ dimensions, or a single scalar value (no dimension).
- All measure values are zero or null.
For a single scalar answer (one row, one column), just say the number in prose
followed by the one-line table — no chart.
如果查询恰好返回一个 measure 和一个 dimension(categorical 或 time),则在表格下方
绘制水平条形图。根据结果集中的最大值,将条形缩放至 40 个字符宽;填充部分使用
block character,并在末尾保留一个空格。
█Search ████████████████████████████████████████ 142,318
Organic ████████████████████████▊ 88,204
Email ███████████▊ 41,907
Facebook ██████▏ 22,015
Display ██▋ 9,471时间序列示例(从最早到最新,自上而下):
2025-06 ███████████▊ € 41,200
2025-07 █████████████████▏ € 59,800
2025-08 ████████████████████▋ € 72,400
…以下情况下跳过图表,仅显示表格:
- 结果集为空。
- 查询包含 2 个或更多 measures、2 个或更多 dimensions,或者只返回一个标量值(没有 dimension)。
- 所有 measure 值均为零或 null。
对于单个标量结果(1 行、1 列),先用自然语言说出数字,再附上一行表格——不要显示图表。
Step 5: Explain the result briefly
第 5 步:简要解释结果
After the rendered output, add 1–3 short sentences in plain English: what was
measured, the highest / lowest bucket, and any obvious anomaly (e.g. a missing month,
a single category dominating the total). Keep it to facts that are visible in the
table — don't speculate about causes.
If the user is likely to want to drill in, suggest one concrete follow-up query
they can ask for next ("Want the same broken down by country?"). Don't list more than
one — keep the chat tight.
渲染输出后,用 1–3 个简短句子说明:测量了什么、最高和最低的 bucket,以及任何明显异常
(例如缺失某个月份,或某个 category 占据绝大部分总量)。只陈述表格中可见的事实——
不要推测原因。
如果用户可能希望进一步分析,建议 1 个具体的后续 query(“Want the same broken down by
country?”)。不要列出多个——保持聊天内容简洁。
Rules
规则
- Never hardcode cube or member names. Always confirm them via before composing the query.
revos cubes meta - Before composing a multi-cube query, scan the blocks in
joins:. If the cubes you need are connected by more than one path, pick one explicitly with a dotted join hint instead of letting Cube.js guess.cubes/*.yml - Remember that system cubes (scoring, segments, model overlays) live only in
, not in
revos cubes meta. Their joins aren't visible in the YAML — only in the meta output.cubes/ - Always render the table; render the chart only when the data shape supports it.
- Keep set (default 20). For "top N" questions, set
limitto N and addlimiton the measure.order - Quote query JSON safely on the shell — prefer a temp file with over a long inline string with embedded quotes.
@/tmp/q.json - Don't write the query result to disk or invoke a Python plotter — this skill is
in-chat only. For static PNG visualizations of the cube graph itself, use
.
visualize-semantic-model - If returns no cubes, stop and tell the user to run
revos cubes metafirst.revos apply
- 绝不硬编码 cube 或 member 名称。编写 query 前,始终通过 确认。
revos cubes meta - 编写 multi-cube query 前,检查 中的
cubes/*.ymlblocks。如果所需 cubes 通过多条 path 连接,使用带点号的 join hint 明确选择一条,不要让 Cube.js 猜测。joins: - 记住 system cubes(scoring、segments、model overlays)仅存在于 , 而不在
revos cubes meta中。它们的 joins 在 YAML 中不可见,只能在 meta 输出中查看。cubes/ - 始终渲染表格;仅当数据结构支持时渲染图表。
- 始终设置 (默认 20)。对于“top N”问题,将
limit设为 N,并在 measure 上添加limit。order - 在 shell 中安全引用 query JSON——优先使用带有 的临时文件,而不是包含 嵌入式引号的长 inline string。
@/tmp/q.json - 不要将 query 结果写入磁盘,也不要调用 Python plotter——此 skill 仅用于聊天内展示。
如需对 cube graph 本身生成静态 PNG 可视化,请使用 。
visualize-semantic-model - 如果 没有返回任何 cubes,请停止操作,并告知用户先运行
revos cubes meta。revos apply