# Semantik

Fachliche Begriffe einmal definieren und deine Kontrakte damit verknüpfen.

Source: https://learn.datacontract.com/de/semantics/

Jeder deiner Kontrakte hat eine eigene Kopie der Beschreibungen von `order_id`, `order_total` und `sku`.
Und eine Beschreibung sagt, was ein Feld *enthält*, nicht welcher fachliche Begriff es *ist*.
Hier definierst du jeden Begriff **einmal**, in einer Ontologie-Datei mit stabilen **IRIs**, lädst sie in einem Rutsch hoch und verknüpfst die Kontrakte aus [der letzten Übung](https://learn.datacontract.com/de/publish/) damit.

**You will learn:**
- Eine fachliche Ontologie als eine Datei schreiben: Entitäten, Properties, Beziehungen
- Begriffe mit IRIs identifizieren, die auf jeder Plattform-Instanz funktionieren
- Felder im Kontrakt per `authoritativeDefinitions` mit Begriffen verknüpfen
- „Welche Datenprodukte enthalten X?“ per Rückwärtssuche beantworten

> **Voraussetzung**
>
> Baut auf [Auf Entropy Data veröffentlichen](https://learn.datacontract.com/de/publish/) auf: Deine Kontrakte sind veröffentlicht und `entropy-data connection test` funktioniert. In der Community Edition aus dem Repository ist Semantics aktiviert.

> **Kontrakt vs. Ontologie**
>
> Ein Datenkontrakt beschreibt **einen Datensatz**. Eine Ontologie beschreibt die **Fachsprache**, die alle Datensätze teilen: *Eine Bestellung hat eine Bestellsumme, eine Bestellung enthält Artikel.*
> Wenn Felder auf Begriffe zeigen, steht die Bedeutung an einer Stelle, und du kannst alle Datensätze nach Bedeutung statt nach Spaltennamen durchsuchen.

> **Warum IRIs?**
>
> Eine IRI (Internationalized Resource Identifier) ist ein weltweit eindeutiger, stabiler Name für einen Begriff, z. B. `https://learn.datacontract.com/ontology/ecommerce#sku`.
> Sie hängt weder vom Host der Plattform noch vom Namen deiner Organisation ab. Derselbe Kontrakt verlinkt damit richtig in der Cloud, in einer lokalen Community Edition und in jedem Werkzeug, das die Ontologie kennt.
> Die IRI muss keine erreichbare Webseite sein. Die Plattform löst sie zu dem Begriff auf, der sie trägt.

## Die Ontologie schreiben

### Ontologie-Datei schreiben

Lege `semantics.yaml` im Wurzelverzeichnis des Repositorys an. Sie enthält den kompletten Namespace `ecommerce`:

- `prefixes` deklariert `ecom:` als Kurzform der IRI-Basis, sodass jeder Begriff `iri: ecom:Order` schreiben kann.
- Begriffe vom Typ `EntityType` sind die Geschäftsobjekte (Order, Article). Begriffe vom Typ `ValueType` sind ihre Properties (Order ID, Order Total, SKU).
- Beziehungen vom Typ `hasProperty` hängen Properties an Entitäten. Beziehungen vom Typ `relatedTo` verbinden Entitäten: Eine Bestellung enthält Artikel.

```yaml title=semantics.yaml
version: 0.2.0.dev0
name: ecommerce
description: Business concepts of the e-commerce platform.
custom_properties:
  display_name: E-Commerce
prefixes:
  ecom: https://learn.datacontract.com/ontology/ecommerce#
ontology:
  - concept: Order
    id: order
    type: EntityType
    description: A customer order in the e-commerce platform.
    iri: ecom:Order
    relationships:
      - id: order_has_order_id
        name: order_id
        type: hasProperty
        roles:
          - concept: Order ID
      - id: order_has_order_total
        name: order_total
        type: hasProperty
        roles:
          - concept: Order Total
      - id: order_contains_article
        name: contains
        type: relatedTo
        description: An order contains one or more articles.
        roles:
          - concept: Article
        verbalizes:
          - "{Order} contains {Article}"
  - concept: Article
    id: article
    type: EntityType
    description: A product that can be bought in the e-commerce platform.
    iri: ecom:Article
    relationships:
      - id: article_has_sku
        name: sku
        type: hasProperty
        roles:
          - concept: SKU
  - concept: Order ID
    id: order_id
    type: ValueType
    description: Unique identifier of an order (UUID).
    iri: ecom:orderId
  - concept: Order Total
    id: order_total
    type: ValueType
    description: Total amount of an order in cents, never negative.
    iri: ecom:orderTotal
  - concept: SKU
    id: sku
    type: ValueType
    description: Stock keeping unit, the unique identifier of an article.
    iri: ecom:sku
```

### In einem Rutsch hochladen

Ein einziges `PUT` ersetzt den ganzen Namespace durch die Datei: Es legt den Namespace, alle Begriffe und alle Beziehungen in der richtigen Reihenfolge an.
Die Entropy Data CLI hat dafür noch keinen Befehl, deshalb rufst du die API direkt auf. Sie nutzt denselben API Key und Host wie die CLI:

macOS / Linux:

```bash
curl -sS -f -X PUT "$ENTROPY_DATA_HOST/api/semantics/experimental/namespaces/ecommerce/ontology.yaml" \
  -H "x-api-key: $ENTROPY_DATA_API_KEY" \
  -H "Content-Type: application/yaml" \
  --data-binary @semantics.yaml -w "HTTP %{http_code}\n"
```

Windows (PowerShell):

```powershell
Invoke-RestMethod -Method Put `
  -Uri "$env:ENTROPY_DATA_HOST/api/semantics/experimental/namespaces/ecommerce/ontology.yaml" `
  -Headers @{ "x-api-key" = $env:ENTROPY_DATA_API_KEY } `
  -ContentType "application/yaml" `
  -InFile semantics.yaml
```

Output:

```text
HTTP 200
```

Unter Windows gibt ein erfolgreicher Aufruf nichts aus.

> **Community Edition**
>
> Das Setup-Skript hat `ENTROPY_DATA_API_KEY` und `ENTROPY_DATA_HOST` in die `.env` geschrieben, aber `curl` und `Invoke-RestMethod` lesen diese Datei nicht. Lade die beiden Werte zuerst in deine Shell:
>
> macOS / Linux:
>
> ```bash
> set -a; source .env; set +a
> ```
>
> Windows (PowerShell):
>
> ```powershell
> Get-Content .env | Where-Object { $_ -match '^ENTROPY_DATA_(HOST|API_KEY)=' } | ForEach-Object { $k, $v = $_ -split '=', 2; Set-Item "env:$k" $v }
> ```

Prüfe, was angekommen ist:

macOS / Linux:

```bash
entropy-data semantics concepts list ecommerce
```

Windows (PowerShell):

```powershell
entropy-data semantics concepts list ecommerce
```

Output:

```text
               semantic-concepts (page 0)
┏━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━┳━━━━━━━━┓
┃ ID          ┃ Name        ┃ Kind     ┃ Group ┃ Status ┃
┡━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━╇━━━━━━━━┩
│ article     │ Article     │ entity   │       │        │
│ order       │ Order       │ entity   │       │        │
│ order_id    │ Order ID    │ property │       │        │
│ order_total │ Order Total │ property │       │        │
│ sku         │ SKU         │ property │       │        │
└─────────────┴─────────────┴──────────┴───────┴────────┘
```

Später etwas ändern? Bearbeite `semantics.yaml` und lade sie erneut hoch. Begriffe, die du aus der Datei entfernst, verschwinden auch aus dem Namespace.

## Deine Datenkontrakte verknüpfen

### Felder per IRI verknüpfen

Verknüpfe Schemas und Felder über `authoritativeDefinitions` vom Typ `semantics` mit den Begriffen und nutze die volle IRI als `url`:

```yaml
schema:
  - name: orders
    authoritativeDefinitions:
      - type: semantics
        url: https://learn.datacontract.com/ontology/ecommerce#Order
    properties:
      - name: order_id
        authoritativeDefinitions:
          - type: semantics
            url: https://learn.datacontract.com/ontology/ecommerce#orderId
```

Mach das für jedes Feld, zu dem es einen Begriff gibt:

| Feld | IRI |
|---|---|
| `order_total` | `https://learn.datacontract.com/ontology/ecommerce#orderTotal` |
| `sku` | `https://learn.datacontract.com/ontology/ecommerce#sku` |

Verknüpfe sie in `orders_v1`, `orders_v2` und `sku_sales_per_year`. **Entferne dann die kopierten Beschreibungen** aus diesen Feldern. Die Definition steht jetzt im Begriff.

Prüfe, ob die Kontrakte gültig bleiben:

macOS / Linux:

```bash
datacontract lint orders_v1.odcs.yaml
datacontract lint orders_v2.odcs.yaml
datacontract lint sku_sales_per_year.odcs.yaml
```

Windows (PowerShell):

```powershell
datacontract lint orders_v1.odcs.yaml
datacontract lint orders_v2.odcs.yaml
datacontract lint sku_sales_per_year.odcs.yaml
```

Output:

```text
╭────────┬────────────────────────────────────────────┬───────┬─────────╮
│ Result │ Check                                      │ Field │ Details │
├────────┼────────────────────────────────────────────┼───────┼─────────┤
│ passed │ Data contract is valid against ODCS v3.2.0 │       │         │
╰────────┴────────────────────────────────────────────┴───────┴─────────╯
🟢 data contract is valid. Run 1 checks. Took 0.17 seconds.
…
🟢 data contract is valid. Run 1 checks. Took 0.17 seconds.
…
🟢 data contract is valid. Run 1 checks. Took 0.14 seconds.
```

> **Auch die CLI löst IRIs auf**
>
> `datacontract test` schlägt jede IRI über `ENTROPY_DATA_HOST` nach (`/api/semantics?iri=...`) und übernimmt die Beschreibung des Begriffs, wo das Feld keine hat. Dafür braucht es `ENTROPY_DATA_API_KEY`.

### Kontrakte erneut veröffentlichen

macOS / Linux:

```bash
entropy-data datacontracts put orders_v1 --file orders_v1.odcs.yaml
entropy-data datacontracts put orders_v2 --file orders_v2.odcs.yaml
entropy-data datacontracts put sku_sales_per_year --file sku_sales_per_year.odcs.yaml
```

Windows (PowerShell):

```powershell
entropy-data datacontracts put orders_v1 --file orders_v1.odcs.yaml
entropy-data datacontracts put orders_v2 --file orders_v2.odcs.yaml
entropy-data datacontracts put sku_sales_per_year --file sku_sales_per_year.odcs.yaml
```

Output:

```text
Data contract 'orders_v1' saved.
Open https://app.entropy-data.com/tutorial-simon/datacontracts/orders_v1
Data contract 'orders_v2' saved.
Open https://app.entropy-data.com/tutorial-simon/datacontracts/orders_v2
Data contract 'sku_sales_per_year' saved.
Open https://app.entropy-data.com/tutorial-simon/datacontracts/sku_sales_per_year
```

> **Deine CI-Pipeline braucht jetzt die Plattform**
>
> `datacontract test` und `datacontract ci` lösen die IRIs über die Plattform auf. Ohne `ENTROPY_DATA_API_KEY` oder wenn der Host nicht erreichbar ist, scheitert der Lauf mit „Could not resolve business definition“.
> In deinem Workflow aus [CI/CD mit GitHub Actions](https://learn.datacontract.com/de/ci-cd/) hast du zwei Möglichkeiten:
>
> - **Cloud:** Gib dem Step das Secret `ENTROPY_DATA_API_KEY` und `ENTROPY_DATA_HOST` mit (siehe Bonus der letzten Übung).
> - **Community Edition** (von GitHub aus nicht erreichbar): Ergänze `--no-inline-references` am Befehl `datacontract ci`.

**Solution: Kontrakte mit semantischen Verknüpfungen**

```yaml title=orders_v1.with-semantics.odcs.yaml
# Copy of ../exercise1/orders_v1.odcs.yaml plus semantics links (authoritativeDefinitions)
# and without the text descriptions that the semantic concepts now carry - keep in sync!
version: 1.0.0
kind: DataContract
apiVersion: v3.2.0
id: orders_v1
name: Orders
status: active
domain: ecommerce
description:
  purpose: Order data for analytics and reporting
  limitations: Contains PII (customer email addresses)
tags: ['orders', 'ecommerce']
context:
  instructions: >-
    Orders of the e-commerce platform and their line items.
    order_total is in cents. Timestamps are in UTC.
    Use this data for order volume and revenue analysis.
  verifiedStatements:
  - question: How many orders were placed in 2023?
    answer: SELECT COUNT(*) FROM orders_v1.orders WHERE EXTRACT(YEAR FROM order_timestamp) = 2023;
  - question: What was the revenue per year?
    answer: SELECT EXTRACT(YEAR FROM order_timestamp)::int AS year, SUM(order_total) / 100.0 AS revenue FROM orders_v1.orders GROUP BY 1 ORDER BY 1;
  constraints:
  - constraint: Never output customer_email_address or customer_id. Aggregate the data instead.
    tags: ['pii']
servers:
- server: postgres
  type: postgres
  host: localhost
  port: 5433
  database: workshop
  schema: orders_v1
schema:
- name: orders
  synonyms:
  - synonym: purchases
  - synonym: Bestellungen
    locale: de
  context:
    instructions: One row per order. Join line_items on order_id to get the purchased SKUs.
  physicalType: table
  logicalType: object
  physicalName: orders
  authoritativeDefinitions:
  - type: semantics
    url: https://learn.datacontract.com/ontology/ecommerce#Order
  properties:
  - name: order_id
    physicalType: text
    logicalType: string
    required: true
    primaryKey: true
    authoritativeDefinitions:
    - type: semantics
      url: https://learn.datacontract.com/ontology/ecommerce#orderId
    quality:
    - type: sql
      description: Ensure order_id is unique
      query: SELECT COUNT(*) FROM (SELECT order_id FROM orders_v1.orders GROUP BY order_id HAVING COUNT(*) > 1) duplicates;
      mustBe: 0
  - name: order_timestamp
    physicalType: timestamptz
    logicalType: date
    quality:
    - type: text
      description: Must not be in the future
    - type: sql
      description: Ensure order_timestamp is not in the future
      query: SELECT COUNT(*) FROM orders_v1.orders WHERE order_timestamp > CURRENT_TIMESTAMP;
      mustBe: 0
  - name: order_total
    physicalType: bigint
    logicalType: integer
    authoritativeDefinitions:
    - type: semantics
      url: https://learn.datacontract.com/ontology/ecommerce#orderTotal
    quality:
    - type: sql
      description: Ensure order_total is non-negative
      query: SELECT COUNT(*) FROM orders_v1.orders WHERE order_total < 0;
      mustBe: 0
  - name: customer_id
    physicalType: text
    logicalType: string
    quality:
    - type: text
      description: Must be a non-empty alphanumeric identifier
    - type: sql
      description: Ensure customer_id is not empty
      query: SELECT COUNT(*) FROM orders_v1.orders WHERE customer_id IS NULL OR customer_id = '';
      mustBe: 0
  - name: customer_email_address
    physicalType: text
    logicalType: string
    required: true
    classification: confidential
    examples: ['test394@example.org', 'test4757@example.org']
    quality:
    - type: text
      description: Must be a valid email address
    - type: sql
      description: Ensure email addresses contain @
      query: SELECT COUNT(*) FROM orders_v1.orders WHERE customer_email_address NOT LIKE '%@%';
      mustBe: 0
  quality:
  - type: text
    description: Orders table must not be empty
  - type: sql
    description: Ensure orders table has data
    query: SELECT COUNT(*) FROM orders_v1.orders;
    mustBeGreaterThan: 0
  - type: sql
    description: Ensure every order has at least one line item
    query: SELECT COUNT(*) FROM orders_v1.orders o LEFT JOIN orders_v1.line_items li ON o.order_id = li.order_id WHERE li.order_id IS NULL;
    mustBe: 0
- name: line_items
  synonyms:
  - synonym: order lines
  - synonym: Bestellpositionen
    locale: de
  context:
    instructions: One row per purchased SKU in an order.
  physicalType: table
  logicalType: object
  physicalName: line_items
  properties:
  - name: lines_item_id
    physicalType: text
    logicalType: string
    required: true
    primaryKey: true
    quality:
    - type: text
      description: Must be a valid UUID
    - type: sql
      description: Ensure lines_item_id is unique
      query: SELECT COUNT(*) FROM (SELECT lines_item_id FROM orders_v1.line_items GROUP BY lines_item_id HAVING COUNT(*) > 1) duplicates;
      mustBe: 0
  - name: order_id
    physicalType: text
    logicalType: string
    required: true
    authoritativeDefinitions:
    - type: semantics
      url: https://learn.datacontract.com/ontology/ecommerce#orderId
    relationships:
    - type: foreignKey
      to: orders.order_id
  - name: sku
    synonyms:
    - synonym: article number
    - synonym: Artikelnummer
      locale: de
    physicalType: text
    logicalType: string
    authoritativeDefinitions:
    - type: semantics
      url: https://learn.datacontract.com/ontology/ecommerce#sku
    quality:
    - type: sql
      description: Ensure sku is not empty
      query: SELECT COUNT(*) FROM orders_v1.line_items WHERE sku IS NULL OR sku = '';
      mustBe: 0
  quality:
  - type: text
    description: Line items table must not be empty
  - type: sql
    description: Ensure line_items table has data
    query: SELECT COUNT(*) FROM orders_v1.line_items;
    mustBeGreaterThan: 0
  - type: sql
    description: Ensure every line_items.order_id exists in orders
    query: SELECT COUNT(*) FROM orders_v1.line_items li LEFT JOIN orders_v1.orders o ON li.order_id = o.order_id WHERE o.order_id IS NULL;
    mustBe: 0
team:
  name: order_data_team
  members:
  - username: owner@example.com
    role: Owner
support:
- channel: "#order-data-help"
  url: https://example.slack.com/archives/order-data-help
  tool: slack
slaProperties:
- property: retention
  value: 10
  unit: y
  element: orders.order_timestamp
  description: Orders are deleted after 10 years
- property: frequency
  value: 1
  unit: d
  description: Data updated daily
```

```yaml title=sku_sales_per_year.with-semantics.odcs.yaml
# Copy of ../exercise4/sku_sales_per_year.odcs.yaml plus semantics links (authoritativeDefinitions)
# and with the active status from the end of exercise 5 - keep in sync!
version: 1.0.0
kind: DataContract
apiVersion: v3.2.0
id: sku_sales_per_year
name: SKU Sales per Year
status: active
domain: ecommerce
description:
  purpose: Shows how often each SKU is bought, grouped by year. Built for the purchasing team to support supplier negotiations.
  limitations: Aggregated data only, no PII. Based on the orders data product.
tags: ['sku', 'sales', 'analytics']
context:
  instructions: >-
    One row per SKU and year. Sum order_count or total_quantity across years for totals.
  verifiedStatements:
  - question: Which three SKUs sold the most units in 2024?
    answer: SELECT sku, total_quantity FROM analytics.sku_sales_per_year WHERE year = 2024 ORDER BY total_quantity DESC LIMIT 3;
servers:
- server: postgres
  type: postgres
  host: localhost
  port: 5433
  database: workshop
  schema: analytics
schema:
- name: sku_sales_per_year
  physicalType: view
  logicalType: object
  physicalName: sku_sales_per_year
  properties:
  - name: sku
    semanticType: dimension
    physicalType: text
    logicalType: string
    required: true
    authoritativeDefinitions:
    - type: semantics
      url: https://learn.datacontract.com/ontology/ecommerce#sku
    quality:
    - type: sql
      description: Ensure sku and year combination is unique
      query: SELECT COUNT(*) FROM (SELECT sku, year FROM analytics.sku_sales_per_year GROUP BY sku, year HAVING COUNT(*) > 1) duplicates;
      mustBe: 0
  - name: year
    semanticType: dimension
    physicalType: integer
    logicalType: integer
    required: true
    quality:
    - type: sql
      description: Ensure year is plausible
      query: SELECT COUNT(*) FROM analytics.sku_sales_per_year WHERE year < 2020 OR year > 2100;
      mustBe: 0
  - name: order_count
    semanticType: measure
    transformLogic: COUNT(*)
    physicalType: bigint
    logicalType: integer
    quality:
    - type: text
      description: Number of orders containing the SKU in that year
    - type: sql
      description: Ensure order_count is positive
      query: SELECT COUNT(*) FROM analytics.sku_sales_per_year WHERE order_count <= 0;
      mustBe: 0
  - name: total_quantity
    semanticType: measure
    transformLogic: SUM(line_items.quantity)
    physicalType: bigint
    logicalType: integer
    quality:
    - type: text
      description: Total units bought, never less than the number of orders
    - type: sql
      description: Ensure total_quantity is at least order_count
      query: SELECT COUNT(*) FROM analytics.sku_sales_per_year WHERE total_quantity < order_count;
      mustBe: 0
  quality:
  - type: sql
    description: Ensure the view has data
    query: SELECT COUNT(*) FROM analytics.sku_sales_per_year;
    mustBeGreaterThan: 0
team:
  name: purchasing_analytics_team
  members:
  - username: purchasing@example.com
    role: Owner
support:
- channel: "#purchasing-analytics"
  url: https://example.slack.com/archives/purchasing-analytics
  tool: slack
```

## Die Ontologie erkunden

### Ontologie erkunden

Geh in der Weboberfläche auf **Semantics → E-Commerce** und öffne **Article → SKU**:

- Das Diagramm zeigt die Ontologie: Order mit ihren Properties, contains, Article mit SKU.
- **Metadata** zeigt die IRI des Begriffs, als `ecom:sku` und ausgeschrieben.
- **Data Products** ist die Rückwärtssuche: alle Datenprodukte, deren Kontrakte auf den Begriff verweisen. *„Welche Datenprodukte enthalten SKUs?“* ist jetzt ein Klick.

![Der Begriff SKU: Ontologie-Diagramm, IRI und die Datenprodukte, die ihn nutzen](https://learn.datacontract.com/screenshots/semantics-concept-sku.webp)

Auch die Liste der Datenprodukte zeigt die verknüpften Begriffe:

![Datenprodukte mit ihren verknüpften Begriffen in der Spalte Semantics](https://learn.datacontract.com/screenshots/semantics-data-products.webp)

**Quick check:** Warum verknüpfst du Felder per IRI statt über die URL des Begriffs auf der Plattform?

- IRIs sind kürzer zu tippen
- Die IRI ist ein stabiler, globaler Bezeichner, der Link funktioniert auf jeder Instanz und in anderen Werkzeugen (correct)
- Die Plattform kann per URL verknüpfte Begriffe nicht anzeigen
- Mit IRIs prüft datacontract test auch die Semantik

Genau. Eine URL enthält Host und Organisation und bricht, wenn sich eins davon ändert. Die IRI benennt den Begriff selbst, und die Plattform löst sie dort auf, wo die Ontologie liegt.
