Vocabulary: what the expansion line means¶
A term slot holds a word from a closed list, and your question is spelled in words the store may not use: "DACH" where rows say Germany, "≥ VP" where rows say SVP, "Vice President" where the ladder says VP. Before a relation query or a search runs, every clause on a term slot is expanded through the workspace's vocabulary into the set of terms it names — and the expansion is printed, one line per clause, so you can see what was actually asked of the store.
Each axis of a relation has a vocabulary of three tables — synonym, hierarchy, order.
Places and languages come from Limma's built-in core; a relation drafted
from a connected export builds its own term lists from the values its records hold
(a collection nobody wrote a pack for),
and a relation set up for a workspace's specialised documents comes with its own. You
never send it; you read what it did. Examples run against
roster(client), a workspace with the expert-network pack
applied.
Reading a line¶
from docs.examples.roster import roster
ws = roster(client).workspace_id
for clause in (["seniority", "gte", "VP"], ["geography", "eq", "DACH"],
["seniority", "eq", "Vice President"], ["geography", "eq", "D-A-CH"],
["function", "eq", "Sales"], ["seniority", "ne", "VP"]):
r = client.post("/v1/public/facts", json={
"workspace_id": ws, "question": "roles", "mode": "enumerate",
"relation_query": {"relation": "held_role", "where": [clause]}})
rel = r.json()["relations"]
print(clause, "->", rel["count"], "row(s)")
for line in rel["expansion"]:
print(" ", line)
print(" ran:", rel["where_run"])
['seniority', 'gte', 'VP'] -> 4 row(s)
seniority: ≥ VP → {VP, SVP, C-level} via order
ran: [['seniority', 'in', ['c-level', 'svp', 'vp']]]
['geography', 'eq', 'DACH'] -> 5 row(s)
geography: DACH → {DACH, Germany, Austria, Switzerland} via hierarchy
ran: [['geography', 'in', ['austria', 'dach', 'germany', 'switzerland']]]
['seniority', 'eq', 'Vice President'] -> 2 row(s)
seniority: 'Vice President' = VP via synonym
ran: [['seniority', 'in', ['vp']]]
['geography', 'eq', 'D-A-CH'] -> 5 row(s)
geography: 'D-A-CH' = DACH via synonym; DACH → {DACH, Germany, Austria, Switzerland} via hierarchy
ran: [['geography', 'in', ['austria', 'dach', 'germany', 'switzerland']]]
['function', 'eq', 'Sales'] -> 4 row(s)
function: Sales is a term of axis function
ran: [['function', 'in', ['sales']]]
['seniority', 'ne', 'VP'] -> 4 row(s)
seniority: VP is a term of axis seniority
ran: [['seniority', 'not_in', ['vp']]]
Each line is <slot>: <what you wrote> → {<terms>} via <route>:
| route | what happened |
|---|---|
via order |
a comparison on an ordered axis: every ranked term at or above (or below) the pivot |
via hierarchy |
a group term: itself and every term under it (a parent is a member of its own expansion — a row stored at "DACH" is in DACH) |
via synonym |
a surface the pack maps to a term ('Vice President' = VP); a synonym resolves before the hierarchy, so D-A-CH → DACH → its members |
is a term of axis <axis> |
the word is itself a term; nothing to expand |
via ambiguous |
the pack uses the surface for two terms; the union of both is run, and the line names the candidates |
where_run is the clause that ran: an in / not_in over normalised (case-folded)
terms. ne becomes not_in over the expansion. A stored surface of a term is matched as
that term — a row whose seniority column says "Vice President" is a VP row — and the
expansion prints a line for it (the last example on this page).
When the vocabulary cannot vouch for the set¶
Four outcomes are not a clean expansion, and each is named rather than guessed around:
from docs.examples.roster import roster
ws = roster(client).workspace_id
for relation, clause in (("held_role", ["geography", "eq", "Munich"]),
("held_role", ["title", "eq", "Director, Sales"]),
("held_role", ["title", "gte", "Director, Sales"]),
("held_role", ["seniority", "gte", "Boss"])):
r = client.post("/v1/public/facts", json={
"workspace_id": ws, "question": "roles", "mode": "enumerate",
"relation_query": {"relation": relation, "where": [clause]}})
body = r.json()
rel = body["relations"]
print(clause, "-> bound:", rel["bound"], "| count:", rel["count"],
"| licensed:", body["coverage"]["completeness_licensed"])
print(" unbound:", rel["unbound_slots"], "| unknown surface:",
rel["unknown_surface_slots"], "| unplaced:", rel["unplaced_slots"])
print(" narrowed_by:", body["coverage"].get("narrowed_by"),
"| unbound_because:", rel["unbound_because"])
for line in rel["expansion"]:
print(" ", line)
['geography', 'eq', 'Munich'] -> bound: True | count: 0 | licensed: False
unbound: [] | unknown surface: ['geography'] | unplaced: []
narrowed_by: ['vocabulary_unknown_surface:geography'] | unbound_because: None
geography: 'Munich' is not a term of axis geography
geography: 'Munich' names no term of axis geography; matched as written — the vocabulary cannot say which rows such a surface names, so the count is not a completeness claim
['title', 'eq', 'Director, Sales'] -> bound: True | count: 1 | licensed: False
unbound: ['title'] | unknown surface: [] | unplaced: []
narrowed_by: ['vocabulary_unbound:title'] | unbound_because: None
title: axis 'title' has no vocabulary in pack 'expert_network' — UNBOUND
['title', 'gte', 'Director, Sales'] -> bound: False | count: None | licensed: False
unbound: ['title'] | unknown surface: [] | unplaced: []
narrowed_by: None | unbound_because: vocabulary_unbound:title
title: axis 'title' has no vocabulary in pack 'expert_network' — UNBOUND
['seniority', 'gte', 'Boss'] -> bound: False | count: None | licensed: False
unbound: ['seniority'] | unknown surface: [] | unplaced: []
narrowed_by: None | unbound_because: vocabulary_unbound:seniority
seniority: 'Boss' is not a term of axis seniority
seniority: 'gte Boss' names no term of the order on axis seniority; the set is undefined, not empty — UNBOUND
- An unknown surface on a populated axis ("Munich" where the geography axis lists
countries and regions): the value runs as written beside the known terms, and the
licence is withheld —
unknown_surface_slotsnames the slot,narrowed_by: vocabulary_unknown_surface:<slot>. A vocabulary cannot say which rows a word it does not know names, so a zero here is never a licensed none. - An axis with no vocabulary (
titlein this pack — titles are open-ended): an equality runs as written, an exact-spelling match, and the licence is withheld —unbound_slots,narrowed_by: vocabulary_unbound:<slot>. - A comparison with no order to compare on (
gteontitle), or a pivot the order does not know (gte Boss): the set above an unknown point is undefined, not empty, so nothing runs —bound: false,unbound_because: vocabulary_unbound:<slot>. - A stored value no term places (a row whose geography column says "Bavaria", or a
row stored at a coarser term than you asked for): the rows cannot be said to belong or
not, so the licence is withheld —
unplaced_slots,narrowed_by: vocabulary_unplaced:<slot>. On an axis backed by the workspace catalogue (the finance pack'smeasure), a fold counts the same way on either side. A row stored as "adjusted ebitda" asked asebitda, and a row stored asrevenueasked as "net sales", are both delivered, the fold is printed inexpansion, and the licence is withheld. Only the same spelling (case, spacing, and_for a space) keeps it.
The shared roster's export uses the pack's own terms, so unplaced_slots is empty above.
This example re-syncs a fresh copy of the export in which one row says "Vice President"
(a synonym of VP) and "Bavaria" (no term of the axis):
from docs.examples.roster import new_roster, update_export, connect, ROLES_MAPPING
r = new_roster(client)
update_export(r.export, "UPDATE roles SET seniority = ?, geography = ? WHERE row_id = ?",
("Vice President", "Bavaria", 1))
connect(client, r.workspace_id, r.export, "roles", ROLES_MAPPING, replaces=r.roles_source)
for clause in (["seniority", "gte", "VP"], ["geography", "eq", "DACH"]):
body = client.post("/v1/public/facts", json={
"workspace_id": r.workspace_id, "question": "roles", "mode": "enumerate",
"relation_query": {"relation": "held_role", "where": [clause]}}).json()
rel = body["relations"]
print(clause, "->", rel["count"], "| unplaced:", rel["unplaced_slots"],
"| licensed:", body["coverage"]["completeness_licensed"])
for line in rel["expansion"]:
print(" ", line)
print(" ran:", rel["where_run"])
['seniority', 'gte', 'VP'] -> 4 | unplaced: [] | licensed: True
seniority: ≥ VP → {VP, SVP, C-level} via order
seniority: stored {vice president} read as the terms they name on axis seniority
ran: [['seniority', 'in', ['c-level', 'svp', 'vice president', 'vp']]]
['geography', 'eq', 'DACH'] -> 4 | unplaced: ['geography'] | licensed: False
geography: DACH → {DACH, Germany, Austria, Switzerland} via hierarchy
geography: 1 stored value(s) axis geography cannot place inside or outside this set — no single term, or a coarser one ({bavaria}); whether their rows belong cannot be said, so the count is not a completeness claim
ran: [['geography', 'in', ['austria', 'dach', 'germany', 'switzerland']]]
The stored synonym is matched as its term and the line says so; the row stored as "Bavaria" can be placed neither inside the DACH set nor outside it, so the count is exact about the rows it holds and withholds these are all of them.
A ne / not_in over an ambiguous surface is also unbound (the complement of a union
would omit rows under either reading); the expert-network pack declares no ambiguous
surface.
The same lines on the search verb¶
limma_search expands each constraint through the same vocabulary and prints the same
lines under expansion; an unbound constraint is listed in gaps.unbound and does not
run (search). The compile step that turns a brief into
constraints is shown each axis's terms (and for an ordered axis, the ladder), and is told
that synonyms and groups resolve server-side — so a compiled DACH expands here, not in
the model.
What an expansion licenses¶
A clean expansion (a term, a synonym, a hierarchy, an order) licenses reading the count as
a count of what you meant, as the pack defines it: "DACH" is Germany, Austria and
Switzerland because the pack says so. It does not license a membership the pack does not
state — a city is not in a country here unless the pack lists it — and anything the
vocabulary could not place is named in coverage.narrowed_by, never silently widened or
dropped.