Skip to content

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:

  1. The AI Ask layer — descriptions and synonyms give the LLM real business context; aggregation kinds, grains, and required filters keep its query suggestions sane.
  2. PII protection — the saiku.semantic.pii=true marker 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 keyValid onType / valuesPurpose
saiku.semantic.descriptionDimension · Measure · Levelfree textBusiness description; surfaced in /ai/schema so the LLM has context
saiku.semantic.synonymsDimension · Measure · LevelCSV listAlternative names so the AI can resolve “country” → “Store Country”
saiku.semantic.unitMeasurefree text (e.g. USD, kWh, count)Unit string; flows into the AI cell {value, formatted, unit} shape
saiku.semantic.currencyMeasureISO 4217 code (e.g. USD, EUR)Currency hint for monetary measures
saiku.semantic.aggregation_kindMeasureenum: sum · count · distinct-count · non-additiveAggregation semantics. Drives sort/total semantics in the AI layer.
saiku.semantic.cardinalityLevelenum: low · medium · highMember-count hint. Drives picker UX and AI cost hints.
saiku.semantic.grainLevelenum: year · quarter · month · week · day · hour · minuteTime grain for date-filter modal and time-series chart inference
saiku.semantic.required_filtersLevelCSV of (hierarchy, level) pairsLevels the AI must include in filters whenever this level is touched
saiku.semantic.piiLevel · Measureboolean (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>

Levels 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>

PII 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:

  • caption
  • description
  • synonyms
  • 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_POLICYWhat the AI sees
schema-onlySchema + sample members only. No cellset values reach the provider.
aggregatedAggregated cell totals reach the provider; row-level cells stripped.
row-levelFull 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 a WARN with the offending value + the allowed list and the field defaults to unset.
  • Boolean fields (pii) accept true / false case-insensitively after trim. Anything else is false. Schema authors get the conservative default if they mistype.
  • Free-text fields (description, unit, currency) are trimmed; empty strings become null.
  • synonyms and required_filters parse as CSV; empty entries are dropped.
  • Exporting to Apache Ossie — how the saiku.semantic.* annotations survive round-trip into portable Ossie YAML (description + synonyms lift into ai_context; everything else rides in custom_extensions as 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 which saiku.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).