Aller au contenu

OSL — la couche de consommation

La OpenDome Semantic Layer (OSL) est une spécification ouverte (Apache-2.0) qui décrit, en YAML déclaratif, un modèle sémantique couvrant deux plans physiques : tables structurées dans Iceberg et collections vectorielles dans Lance. Une spec, un langage, une chaîne de gouvernance.

Cette page fixe le vocabulaire et le modèle mental. Pour le contrat normatif, voir la spécification ; pour la surface d’API, voir la référence.

Les couches sémantiques OSS d’aujourd’hui (dbt MetricFlow, Cube, WrenAI) décrivent des tables relationnelles. Aucune ne traite les collections vectorielles comme des citoyens de première classe. Cela impose deux modèles parallèles — le sémantique-SQL et « ce que le RAG sait du client » — qui divergent dès le lendemain de leur mise en production.

OSL couvre les deux plans :

  1. Structuré — entités, métriques, dimensions, contrats de données. Ici OSL est un superset de dbt MetricFlow : tout projet MetricFlow valide est un OSL valide.
  2. Non structuré — collections Lance avec leur chunk schema, embeddings, index, filtres de metadata et ACL row-level. L’extension OSL-only.
  3. Le pont — la JointEntity, un objet logique (Customer, Policy, Order) qui relie des colonnes Iceberg à des facettes Lance sous une seule clé primaire.

Déclarez une fois. La gouvernance, le lineage, les APIs et les agents consomment tous le même modèle.

Le modèle

STRUCTURÉ · ICEBERGNON STRUCTURÉ · LANCEJOINTENTITY
Une seule spec déclarative couvre les deux plans physiques ; la JointEntity les relie sous une seule clé.

OSL décrit un modèle sémantique sur deux substrats physiques avec un seul langage déclaratif. Chaque plan a ses primitives, sa convention de FQN, son moteur d’exécution et son chemin d’évolution. La JointEntity est la pièce qui les compose.

Tables Iceberg, queries SQL, agrégations, joins. Ici OSL passe par MetricFlow sans modification :

  • semantic_models/*.yml décrivent des entités physiques avec leurs dimensions et measures.
  • metrics/*.yml définissent des métriques certifiées (simple, ratio, cumulative, derived).
  • saved_queries/*.yml empaquettent des requêtes réutilisables.
  • time_spine.yml définit le grain temporel du projet.

Le moteur de référence (tenant-semantic-api) compile ces YAML en SQL sur Trino, exécuté contre Iceberg dans le bucket du tenant.

Collections Lance avec chunks indexés. Ici OSL définit de nouvelles primitives :

  • facets/*.yml (kind: UnstructuredFacet) décrivent une collection Lance : son dataset path, chunk schema, modèle d’embedding, index (vector + FTS + scalaire), champs de metadata, ACL row-level et limites de retrieval.

Le moteur exécute un retrieval hybride (vector ⊕ BM25 ⊕ filtre de metadata) sur LanceDB et renvoie des passages avec citation, en respectant l’ACL et les limites déclarées.

  • joints/*.yml (kind: JointEntity) déclare un objet logique dont la clé primaire provient d’un semantic_model et qui expose des attributs composés — certains du plan structuré (colonnes, métriques), d’autres du non structuré (retrieval scoped par la clé).

Le pont est honnête : chaque join_on mappe une clé d’entité sur un champ de metadata autoritatif et indexé de la facette. Un champ avec provenance: derived ne peut jamais être une clé de join — l’extraction dérivée a lieu à l’ingest, jamais au query time.

Un agent qui invoque entity_get(Customer, customer_id=X) reçoit, en une seule réponse, les attributs canoniques de l’entité, les métriques pré-agrégées qui s’appliquent et les chunks Lance filtrés par customer_id=X. Un appel, une décision de politique, une trace d’audit.

Bien que la spec ait des primitives séparées (SemanticModel, JointEntity), qui consomme OSL ne voit qu’un seul mot : Entity. Ce qui distingue une Entity d’une autre n’est pas son kind mais sa composition :

Composition Ce que c’est D’où ça vient
structured Entity purement tabulaire (Iceberg seul) un SemanticModel qui n’est ni ancre de joint ni bridge
structured+content Colonnes structurées reliées à des facettes Lance un JointEntity avec des jambes non structurées
content Un corpus Lance autonome, sans ancre structurée une UnstructuredFacet qu’aucun joint n’absorbe

Les relations entre Entities viennent de deux endroits : entités MetricFlow partagées (primary / foreign / unique) pour les voisins structuré ↔ structuré, et N-N via bridge — une table de jonction (SemanticModel avec ≥2 entités foreign) qui se réduit à une arête N-N directe au lieu d’être un nœud.

Ce vocabulaire est celui que sert le domain-map (GET /osl/domain-map) : un unique node kind entity étiqueté par composition, plus des nœuds metric et candidate.

Famille Kind Objectif MetricFlow
Modélisation structurée SemanticModel, Metric, SavedQuery, TimeSpine Décrire les tables Iceberg + métriques ✅ identique / étendu
Modélisation non structurée UnstructuredFacet Décrire une collection Lance ❌ OSL-only
Composition JointEntity Relier les deux plans sous une entité logique ❌ OSL-only
Catalogue et contrats Lexicon, DataContract, PolicyBinding Synonymie, SLAs, hints pour le PDP ❌ OSL-only

Toute primitive porte apiVersion: osl.opendome.eu/v1, un kind et un name unique. L’identité d’un objet est (kind, name) — sauf SemanticModel, dont l’identité est (catalog, schema, table).

OSL déclare ; le moteur exécute. Entre les deux, il y a une étape de compilation.

1 · Author

Écrivez le YAML à la main ou depuis le modeleur. Convention : project.yml + models/ + metrics/ + facets/ + joints/ + lexicon/ + contracts/.

2 · Validate

osl validate exécute JSON Schema par fichier plus la cross-reference (chaque FQN référencé doit exister). Erreurs en E0xx (schema) ou E1xx (resolve).

3 · Compile

osl compile émet un unique target/manifest.json — la représentation canonique et reproductible que le moteur sert et que la policy engine lit.

4 · Publish

osl publish envoie le manifest au moteur du tenant. Atomique (revert avec osl publish --revert), émet un événement de lineage osl.publish.

À partir de là, le moteur sert les requêtes : REST pour le BI/les apps, tools MCP pour les agents.

OSL est strict sur les noms parce que les politiques en dépendent. Quatre espaces de FQN :

Espace Forme Exemple
Structuré <catalog>.<schema>.<table>[:<column>] iceberg.curated.customer:email
Non structuré lance://<collection>[/*][:<metadata_field>] lance://meeting_transcripts:attendees
Tools <tool_kind>:<resource> metrics:revenue, joints:Customer
Joint-exposed joint://<Entity>.<attribute> joint://Customer.recent_meeting_topics

Les FQN sont case-sensitive et ne portent pas de version — la version vit dans le manifest compilé et signé en SHA. La policy engine consomme ces FQNs dans sa tuple 4-D (data, action, actor, context). OSL n’invente pas une sémantique d’accès parallèle ; il émet les faits que le PDP indexe.

  • Pas un moteur SQL. Trino l’est ; OSL lui donne du sens.
  • Pas un orchestrateur. Dagster exécute les Jobs d’ingest/embedding.
  • Pas une base vectorielle. LanceDB l’est ; OSL décrit les collections.
  • Pas un IAM. OPA / le cell-model l’est ; OSL émet les faits que le PDP consomme.
  • Pas un outil de BI. Metabase, Superset, Streamlit consomment OSL.
  • Pas un framework d’agents. LangGraph, les serveurs MCP consomment OSL.

OSL est la spec partagée entre toutes ces pièces, pas les pièces.