query-semantic-model

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Query 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:
revos cubes meta
lists the available cubes / measures / dimensions, and
revos cubes query
runs a Cube.js query and returns the rows.
This 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 已经提供所需的一切:
revos cubes meta
列出可用的 cubes / measures / dimensions,而
revos cubes query
执行 Cube.js 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 --json
The response carries an array of cubes, each with
measures
,
dimensions
,
segments
, and a
name
. Cube members are addressed as
<CubeName>.<member>
— for example
gold_order_items_enriched.count
or
gold_order_items_enriched.order_date
.
Pick 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 都有
measures
、
dimensions
、
segments
和
name
。Cube members 通过
<CubeName>.<member>
寻址——例如
gold_order_items_enriched.count
或
gold_order_items_enriched.order_date
。
选择与用户问题匹配的 cube 和 members。如果可用 members 无法回答该问题,就直接说明—— 不要虚构 member 名称。

Map the join graph from the local cube files

从本地 cube 文件映射 join graph

revos cubes meta
tells you which members exist; the local
cubes/
folder tells you how cubes are connected. Read every
*.yml
file in
cubes/
and pull out each cube's
joins:
block — that's how the user-defined part of the semantic model is wired up. Example:
yaml
undefined
revos cubes meta
告诉你有哪些 members;本地
cubes/
文件夹则告诉你 cubes 是如何连接的。读取
cubes/
中的每个
*.yml
文件,并提取每个 cube 的
joins:
block——用户定义的 semantic model 部分正是通过这种方式连接的。例如:
yaml
undefined

cubes/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
    timeDimensions
    with
    granularity
    → bar chart over time.
  • Multiple measures or multiple dimensions → render as an ASCII table only; don't try to draw a chart.
  • Always cap with
    limit
    (default 20) so a stray query can't return a million rows.
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 表格;不要尝试绘制图表。
  • 始终使用
    limit
    限制结果(默认 20),避免异常查询返回上百万行数据。

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
measures
/
dimensions
/
filters
. 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.
You 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
undefined
Cube.js 会根据出现在
measures
/
dimensions
/
filters
中的 cubes 推断 join path。 **当多个 path 将它们连接起来时,Cube.js 会替你选择一个——而不同 path 会产生不同 结果。**这是 semantic model query 中“数字看起来不对”问题的最大来源。
固定 path 的方法是在 member 前添加希望 Cube.js 遍历的 cubes,并使用点号分隔。最后一段 是实际的 member,前面的所有内容都是 join hint。例如:
text
undefined

no 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 =
    gold_order_items_enriched
    ; users with X → spine =
    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
    revos cubes meta
    returns a member name that already contains dots (e.g.
    cubeA.cubeB.foo
    ), that is the qualified form — paste it through verbatim, don't strip the prefix.

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 =
    gold_order_items_enriched
    ;users with X → spine =
    gold_users_with_order_stats
    )。
  • 绝不静默选择另一条 path。如果两条 path 都合理,应在执行查询前询问用户所指的 grain。
  • 如果
    revos cubes meta
    返回的 member 名称已经包含点号(例如
    cubeA.cubeB.foo
    ),那么它就是 qualified form——应原样传入,不要去掉前缀。

Step 3: Run the query

第 3 步:执行查询

bash
revos cubes query --query '<INLINE_JSON>' --json
For 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 --json
The response is the raw Cube.js payload —
data
is the array of rows, each row is a flat object keyed by the same member names used in the query.
If the API returns an error, surface the message verbatim. Common causes: a member name typo (re-check
revos cubes meta
), a cube that hasn't been applied yet (
revos status
/
revos apply
), or a
dateRange
against a column that isn't a time dimension.

bash
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——
data
是数据行数组,每一行都是一个扁平对象,键名与 查询中使用的 member 名称相同。
如果 API 返回错误,请原样呈现错误消息。常见原因包括:member 名称拼写错误(重新检查
revos cubes meta
)、cube 尚未 apply(
revos status
/
revos apply
),或者对不是 time dimension 的列使用了
dateRange
。

Step 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,471

4b. 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,471
Time-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
    revos cubes meta
    before composing the query.
  • Before composing a multi-cube query, scan the
    joins:
    blocks in
    cubes/*.yml
    . 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.
  • Remember that system cubes (scoring, segments, model overlays) live only in
    revos cubes meta
    , not in
    cubes/
    . Their joins aren't visible in the YAML — only in the meta output.
  • Always render the table; render the chart only when the data shape supports it.
  • Keep
    limit
    set (default 20). For "top N" questions, set
    limit
    to N and add
    order
    on the measure.
  • Quote query JSON safely on the shell — prefer a temp file with
    @/tmp/q.json
    over a long inline string with embedded quotes.
  • 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
    revos cubes meta
    returns no cubes, stop and tell the user to run
    revos apply
    first.
  • 绝不硬编码 cube 或 member 名称。编写 query 前,始终通过
    revos cubes meta
    确认。
  • 编写 multi-cube query 前,检查
    cubes/*.yml
    中的
    joins:
    blocks。如果所需 cubes 通过多条 path 连接,使用带点号的 join hint 明确选择一条,不要让 Cube.js 猜测。
  • 记住 system cubes(scoring、segments、model overlays)仅存在于
    revos cubes meta
    , 而不在
    cubes/
    中。它们的 joins 在 YAML 中不可见,只能在 meta 输出中查看。
  • 始终渲染表格;仅当数据结构支持时渲染图表。
  • 始终设置
    limit
    (默认 20)。对于“top N”问题,将
    limit
    设为 N,并在 measure 上添加
    order
    。
  • 在 shell 中安全引用 query JSON——优先使用带有
    @/tmp/q.json
    的临时文件,而不是包含 嵌入式引号的长 inline string。
  • 不要将 query 结果写入磁盘,也不要调用 Python plotter——此 skill 仅用于聊天内展示。 如需对 cube graph 本身生成静态 PNG 可视化,请使用
    visualize-semantic-model
    。
  • 如果
    revos cubes meta
    没有返回任何 cubes,请停止操作,并告知用户先运行
    revos apply
    。