Skip to content

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

  1. 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_slots names 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.
  2. An axis with no vocabulary (title in 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>.
  3. A comparison with no order to compare on (gte on title), 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>.
  4. 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's measure), a fold counts the same way on either side. A row stored as "adjusted ebitda" asked as ebitda, and a row stored as revenue asked as "net sales", are both delivered, the fold is printed in expansion, 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.