Saiku semantic annotations
Mondrian’s generic <Annotation> block is a free-form key/value bag attached to any schema element (Cube, Dimension, Hierarchy, Level, Measure, …). Saiku reserves the saiku.semantic.* namespace inside that bag for a small typed vocabulary that drives:
- The AI Ask layer — descriptions and synonyms give the LLM real business context; aggregation kinds, grains, and required filters keep its query suggestions sane.
- PII protection — the
saiku.semantic.pii=truemarker flips a level or measure into “structurally present but opaque” mode in agent-facing surfaces and blocks drillthrough, share/embed exposure, and download paths from leaking the underlying values.
All keys are optional. A schema without any saiku.semantic.* annotations runs identically to one without the annotation block; the namespace is purely additive.
At a glance
| Annotation key | Valid on | Type / values | Purpose |
|---|---|---|---|
saiku.semantic.description | Dimension · Measure · Level | free text | Business description; surfaced in /ai/schema so the LLM has context |
saiku.semantic.synonyms | Dimension · Measure · Level | CSV list | Alternative names so the AI can resolve “country” → “Store Country” |
saiku.semantic.unit | Measure | free text (e.g. USD, kWh, count) | Unit string; flows into the AI cell {value, formatted, unit} shape |
saiku.semantic.currency | Measure | ISO 4217 code (e.g. USD, EUR) | Currency hint for monetary measures |
saiku.semantic.aggregation_kind | Measure | enum: sum · count · distinct-count · non-additive | Aggregation semantics. Drives sort/total semantics in the AI layer. |
saiku.semantic.cardinality | Level | enum: low · medium · high | Member-count hint. Drives picker UX and AI cost hints. |
saiku.semantic.grain | Level | enum: year · quarter · month · week · day · hour · minute | Time grain for date-filter modal and time-series chart inference |
saiku.semantic.required_filters | Level | CSV of (hierarchy, level) pairs | Levels the AI must include in filters whenever this level is touched |
saiku.semantic.pii | Level · Measure | boolean (true / false) | PII marker — see PII protection below |
Unknown values for the enum-typed fields are logged at WARN with the allowed list and otherwise ignored — a typo doesn’t break the schema.
Where to put them
Annotations attach to any schema element via a child <Annotations> block (XML) or an annotations: key (YAML).
<Measure name="Quantity" column="quantity_units" aggregator="sum" formatString="#,##0"> <Annotations> <Annotation name="saiku.semantic.description">Total units prescribed (across all dispensings).</Annotation> <Annotation name="saiku.semantic.synonyms">units, quantity, scripts</Annotation> <Annotation name="saiku.semantic.unit">count</Annotation> <Annotation name="saiku.semantic.aggregation_kind">sum</Annotation> </Annotations></Measure>measures: - name: Quantity column: quantity_units aggregator: sum formatString: "#,##0" annotations: saiku.semantic.description: "Total units prescribed (across all dispensings)." saiku.semantic.synonyms: "units, quantity, scripts" saiku.semantic.unit: count saiku.semantic.aggregation_kind: sumLevels accept the same <Annotations> shape:
<Level name="Year" column="cal_year" type="Numeric" uniqueMembers="true" levelType="TimeYears"> <Annotations> <Annotation name="saiku.semantic.description">Calendar year.</Annotation> <Annotation name="saiku.semantic.synonyms">annual, yearly, fiscal year, y</Annotation> <Annotation name="saiku.semantic.cardinality">low</Annotation> <Annotation name="saiku.semantic.grain">year</Annotation> </Annotations></Level>- name: Year column: cal_year type: Numeric uniqueMembers: true levelType: TimeYears annotations: saiku.semantic.description: "Calendar year." saiku.semantic.synonyms: "annual, yearly, fiscal year, y" saiku.semantic.cardinality: low saiku.semantic.grain: yearPII protection
Marking a level or measure with saiku.semantic.pii=true opts it into Saiku’s PII enforcement stack. The annotation is the declaration — Saiku layers four enforcement points on top of it.
1. AI schema projection — captions and samples stripped
The /ai/schema/{connection/catalog/schema/cube} endpoint feeds the LLM the cube’s structure. For any PII-flagged element it strips:
captiondescriptionsynonyms- sample members (the example values agent guidance usually shows)
The level or measure still appears structurally (the agent knows it exists and can reference it by name) but the values themselves stay opaque. The agent can plan queries that include the PII column in the projection without ever seeing actual rows or sample members.
2. Drillthrough returns= blocked
Drillthrough requests carry a returns= parameter listing the columns to export. If any token in returns= resolves to a PII-flagged level or measure, the resolver throws AiPiiException and the endpoint returns 400 with the offending column named in the body. The agent can retry with a narrower projection that omits the PII column.
HTTP 400{ "error": "Column 'Prescriber' is annotated PII (saiku.semantic.pii=true) and cannot be returned via drillthrough."}3. Embed/share tokens auto-redact
When you mint an embed or share token bound to a query that touches a PII-flagged level, the inspector escalates the token’s redaction policy to FORCE_ON regardless of the operator’s default. Recipients of the token see redacted cells even if the embed wasn’t explicitly configured for redaction. Eliminates the “I forgot to turn on redaction” footgun on shared links.
4. AI data policy gating
PII annotations are cube-level facts. They pair with the SAIKU_AI_POLICY environment variable, which is the deployment-level dial:
SAIKU_AI_POLICY | What the AI sees |
|---|---|
schema-only | Schema + sample members only. No cellset values reach the provider. |
aggregated | Aggregated cell totals reach the provider; row-level cells stripped. |
row-level | Full cells reach the provider. PII annotations still redact specifics. |
The annotation says “this is sensitive”; the policy says “this is how much sensitive data we let through”. Together they decide what the AI sees per turn. The default is schema-only — deployments must opt in to looser tiers.
Pre-flight scanner
PiiScanner runs over a cube’s schema at startup and logs WARN lines for any column whose name pattern looks PII-shaped (matches against *name*, *ssn*, *email*, *npi*, *dob*, etc.) but isn’t annotated. The warning includes a paste-ready XML snippet so the schema author can opt the column in without hunting through the spec:
WARN [PiiScanner] Likely PII column 'prescribername' on level [Prescriber].[Prescriber] is not annotated. Add: <Annotations> <Annotation name="saiku.semantic.pii">true</Annotation> </Annotations>The scanner never mutates the schema — it only surfaces candidates. Schema authors decide what’s actually sensitive.
Worked example — Pharma demo cube
The Pharma demo schema ships with PII annotations on prescriber name + NPI, and leaves specialty + decile un-annotated (those are operationally safe to share). Excerpt from saiku-home/data/Pharma.xml:
<Dimension name="Prescriber" foreignKey="prescriberkey"> <Hierarchy hasAll="true" primaryKey="prescriberkey"> <Table name="dim_prescriber" schema="public"/> <Level name="Specialty" column="specialty" uniqueMembers="false"/> <Level name="Decile" column="decile" type="Numeric" uniqueMembers="false"/> <Level name="Prescriber" column="prescriberkey" nameColumn="prescribername" type="Numeric" uniqueMembers="true"> <Annotations> <Annotation name="saiku.semantic.pii">true</Annotation> </Annotations> </Level> </Hierarchy> <Hierarchy name="NPI" hasAll="true" primaryKey="prescriberkey"> <Table name="dim_prescriber" schema="public"/> <Level name="NPI" column="prescribernpi" type="Numeric" uniqueMembers="true"> <Annotations> <Annotation name="saiku.semantic.pii">true</Annotation> </Annotations> </Level> </Hierarchy></Dimension>With this in place, an AI agent asking “break down Quantity by Specialty” succeeds normally, but “break down Quantity by Prescriber and download the rows” either refuses (drillthrough block) or returns aggregated buckets with prescriber names suppressed (depending on the AI policy tier).
Worked example — FoodMart cube
FoodMart ships with the full descriptive annotation set (no PII columns — synthetic data). Lifted from saiku-home/data/FoodMart4.xml:
<Level name="Store Country" column="store_country" uniqueMembers="true"> <Annotations> <Annotation name="saiku.semantic.description">Store's country.</Annotation> <Annotation name="saiku.semantic.synonyms">nation, country code</Annotation> <Annotation name="saiku.semantic.cardinality">low</Annotation> </Annotations></Level><Level name="Year" column="the_year" type="Numeric" uniqueMembers="true" levelType="TimeYears"> <Annotations> <Annotation name="saiku.semantic.description">Calendar year.</Annotation> <Annotation name="saiku.semantic.synonyms">annual, yearly, fiscal year, y</Annotation> <Annotation name="saiku.semantic.cardinality">low</Annotation> <Annotation name="saiku.semantic.grain">year</Annotation> </Annotations></Level>The same pattern scales across every dim + level. Each line is independently optional, so you can roll the annotations out incrementally.
Validation and unknown values
- Enum-typed fields (
aggregation_kind,cardinality,grain) validate against their allowed list. Unknown values log aWARNwith the offending value + the allowed list and the field defaults to unset. - Boolean fields (
pii) accepttrue/falsecase-insensitively after trim. Anything else isfalse. Schema authors get the conservative default if they mistype. - Free-text fields (
description,unit,currency) are trimmed; empty strings becomenull. synonymsandrequired_filtersparse as CSV; empty entries are dropped.
Related
- Exporting to Apache Ossie — how the
saiku.semantic.*annotations survive round-trip into portable Ossie YAML (description + synonyms lift intoai_context; everything else rides incustom_extensionsas SAIKU vendor data). - Well-known Ossie extensions — the Ossie-side equivalent (
saiku.display,saiku.roles,saiku.pii) for YAML models. Same intent, different file format. - Extensions: functions and formatters — the generic
<Annotation>block on whichsaiku.semantic.*builds (and its locale-metadata convention). - YAML schemas — full YAML reference, including
annotations:on every element. - Access control and roles — schema-level access enforcement (complements PII annotations: ACLs gate who can query a cube; PII annotations gate what the result exposes once they can).