Zum Inhalt

Semantic Data & SPARQL

Der Semantic-Data-Dienst überführt hochgeladene Datensätze in Linked Open Data: Aus einer CSV-Datei und ihrem Frictionless-Schema entsteht eine RDF-Repräsentation, die in den Triple Store (Apache Jena Fuseki) geschrieben und dort per SPARQL abfragbar wird. Diese Seite beschreibt den Ablauf, die beteiligten Komponenten, ihre Konfiguration und typische Abfragen.

Die Einordnung in das Gesamtsystem zeigt die Architektur-Übersicht; als Teilschritt des CSV-Uploads ist die RDF-Erzeugung in Prozess: Datensatz hochladen beschrieben.

Ablauf

Die RDF-Erzeugung ist ein asynchroner Teilschritt des CSV-Uploads. Das Staging-Backend stößt sie an, wartet aber nicht auf ihr Ergebnis: Schlägt sie fehl, wird der Datensatz trotzdem veröffentlicht und die RDF-Distribution später ergänzt.

Semantic-Data-Workflow: vom CSV-Upload über Scheduler, Redis und Worker zum Triple Store und zur SPARQL-Abfrage

1. Auslöser im Staging-Backend

RDF entsteht überall dort, wo das Backend die Repräsentationen eines Datensatzes (neu) aufbaut – nicht nur beim Vervollständigen:

Vorgang Endpunkt RDF
Neuupload POST /upload/csv wird erzeugt
Fortführung (Datenserie) POST /upload/csv/append wird erzeugt (neuer Datensatz je Fortführung)
Überschreiben PUT /upload/csv/overwrite/{datasetId} wird ersetzt bzw. erzeugt
Vervollständigen PUT /upload/csv/complete/{datasetId} wird erzeugt, sofern noch keine RDF-Distribution existiert

Alle Wege laufen über denselben RDF-Client des Backends und dieselbe Kette aus Scheduler, Queue und Worker. Rein geografische Uploads (GeoJSON) lösen keine RDF-Erzeugung aus.

Das Backend übergibt dem Auftrag alle Angaben, die der Worker zur Verarbeitung braucht – insbesondere die Quell-URL der Tabular Data Resource, das Ziel im Triple Store sowie Ziel und Token für den Datenkatalog. Der Worker selbst hält dafür keine eigene Konfiguration vor.

2. Einreihen und Verarbeiten

Der Scheduler nimmt den Auftrag über POST /api/v2/jobs/ an und legt ihn in die Redis-Queue. Der Worker entnimmt ihn und arbeitet ihn ab: Er konvertiert die Tabellendaten anhand des Schemas nach RDF, leitet die Ziel-IRI des Named Graph ab, lädt die Triples per Graph Store Protocol (PUT …?graph=<IRI>) in den Triple Store und legt die serialisierte RDF-Datei zusätzlich als Distribution am Datensatz im Datenkatalog ab.

Komponenten

Baustein Repository Namespace Aufgabe
inference-scheduler (Inference-Framework) odi-triple-converter Nimmt RDF-Aufträge unter /api/v2/jobs/ entgegen und reiht sie in die Queue ein; Python/FastAPI
Redis odi-triple-converter Auftrags-Queue zwischen Scheduler und Worker
triple-converter-worker triple-converter-worker odi-triple-converter Konvertiert CSV nach RDF und lädt das Ergebnis in Triple Store und Datenkatalog; Python
Apache Jena Fuseki odi-triple-jena-fuseki-docker-image odi-triple-store Triple Store (Dataset ds, GeoSPARQL); stellt den SPARQL-Endpunkt bereit
SPARQL-Frontend odi-sparql-frontend odi-triple-store Web-Oberfläche für interaktive SPARQL-Abfragen

Konfiguration

Staging-Backend

Das Backend spricht Scheduler, Triple Store und Datenkatalog cluster-intern an (*.svc.cluster.local). Das ist Absicht: Der RDF-Client sendet keine Authentifizierungs-Header, die extern erreichbaren Ingresse sind dagegen mit Basic-Auth geschützt. Über die internen Adressen entfällt die Auth-Schicht.

Umgebungsvariable Wert (Produktion, cluster-intern)
RDF_SERVICE_URL http://inference-scheduler-service.odi-triple-converter.svc.cluster.local:7535/api/v2
TRIPLE_STORE_URL http://apache-jena-fuseki-service.odi-triple-store.svc.cluster.local:3030/ds/
PIVEAU_SERVICE_URL http://ckan-service-service.odi-staging.svc.cluster.local:3000

Fehlt eine dieser Variablen, greift im Code ein nicht auflösbarer *.example.com-Default, und der Vervollständigungs-Schritt bricht mit einem DNS-Fehler ab. Die Werte gehören also ins Deployment.

Scheduler und Worker

Scheduler und Worker brauchen nur die Anbindung an die Queue:

Umgebungsvariable Wert
INFERENCE_REDIS__HOST redis-service

Die auftragsspezifischen Ziele (Triple Store, Datenkatalog, Token) kommen aus dem Auftrag des Backends, nicht aus Umgebungsvariablen des Workers.

Graph-IRI

Jeder Datensatz landet in einem eigenen benannten Graphen. Die IRI leitet sich aus dem Schema ab, Basis ist https://ld.schleswig-holstein.de/:

Fall Graph-IRI
Einzeldatensatz ohne subject_uri_prefix https://ld.schleswig-holstein.de/{resource_name}
Datenserie ohne Prefix https://ld.schleswig-holstein.de/{serienname}/{version}
Schema mit gesetztem subject_uri_prefix genau dieser Prefix

Beim Ableiten aus dem Namen werden alle Zeichen außer A–Z, a–z und 0–9 durch - ersetzt. Beispiele: Resource-Name „epayment" ergibt …/epayment, „Ladesäulen SH" ergibt …/Lades-ulen-SH, die Datenserie „windkraftanlagen" in Version 1.0.0 ergibt …/windkraftanlagen/1.0.0.

RDF-Annotationen im Schema

Wie die Tabellendaten in Triples übersetzt werden, steuern Annotationen im Frictionless-Schema. Maßgeblich ist dabei der triple-converter: Er liest die folgenden Angaben und bildet daraus je Zeile ein Subjekt mit Prädikat-Objekt-Paaren und Typ-Angaben.

Aus jeder Zeile entsteht ein Subjekt (eine URI), an das pro Spalte ein Prädikat → Objekt gehängt wird, dazu ein oder mehrere rdf:type.

Annotation Ebene Wirkung
subjectUriPrefix Schema (lod) Basis-Namespace der Subjekt-URIs. Ohne Angabe wird er aus dem Ressourcennamen abgeleitet.
rowRdfType Schema (lod) Setzt rdf:type jeder Zeile (eine oder mehrere Vokabular-URIs), z. B. https://schema.org/Place.
propertyUrl Feld Prädikat der Spalte. Ohne Angabe wird als Prädikat der Feldname in CamelCase unter dem Property-Namespace verwendet.
resourceUriPrefix Feld Der Zellwert wird zu einer verlinkten URI (resourceUriPrefix + Wert) statt zu einem Literal.

Subjekt-URI: Der lokale Teil hinter dem subjectUriPrefix bildet sich aus den Primärschlüssel-Werten (mit - verbunden und bereinigt). Ist kein Primärschlüssel definiert, wird stattdessen eine UUID erzeugt.

Objekt-Datentyp: Ohne resourceUriPrefix wird der Wert zu einem typisierten Literal. Der XSD-Typ ergibt sich aus dem Feld-type:

Feld-type XSD-Typ
string xsd:string
number xsd:double
integer xsd:integer
boolean xsd:boolean
date · datetime · time xsd:date · xsd:dateTime · xsd:time
year · yearmonth xsd:gYear · xsd:gYearMonth
wkt geo:wktLiteral

Beispiel

Schema-Ausschnitt:

{
  "lod": {
    "subjectUriPrefix": "https://ld.schleswig-holstein.de/id/wea/",
    "rowRdfType": ["https://schema.org/Place"]
  },
  "primaryKey": ["ID"],
  "fields": [
    { "name": "ID",           "type": "string" },
    { "name": "NABENHOEHE",   "type": "number", "propertyUrl": "https://schema.org/height" },
    { "name": "GEMEINDESCHL", "type": "string", "resourceUriPrefix": "https://www.dcat-ap.de/def/politicalGeocoding/municipalityKey/" }
  ]
}

Für die Zeile ID=42, NABENHOEHE=140, GEMEINDESCHL=01001 entstehen die Triples:

<https://ld.schleswig-holstein.de/id/wea/42>
    a <https://schema.org/Place> ;
    <https://schema.org/height> "140"^^xsd:double ;
    <…/wea#gemeindeschl> <https://www.dcat-ap.de/def/politicalGeocoding/municipalityKey/01001> .

Damit lässt sich gezielt über die vergebenen Typen und Prädikate abfragen:

SELECT ?wea ?hoehe WHERE {
  GRAPH ?g {
    ?wea a <https://schema.org/Place> ;
         <https://schema.org/height> ?hoehe .
  }
}

SPARQL-Abfragen

Zugänge

Zweck Adresse
SPARQL-UI (interaktiv) https://sparql.odi.schleswig-holstein.de
Query-Endpunkt https://jena-fuseki.triple-store.odi.schleswig-holstein.de/ds/sparql
Fuseki-Admin-UI https://jena-fuseki.triple-store.odi.schleswig-holstein.de/
Cluster-intern http://apache-jena-fuseki-service.odi-triple-store.svc.cluster.local:3030/ds/sparql

Die SPARQL-UI ist fest auf den Query-Endpunkt verdrahtet – Abfrage eintippen und ausführen genügt.

Vorhandene Graphen auflisten

Weil jeder Datensatz in einem eigenen Graphen liegt, verschafft diese Abfrage einen Überblick samt Anzahl der Triples je Graph:

SELECT DISTINCT ?g (COUNT(*) AS ?triples)
WHERE { GRAPH ?g { ?s ?p ?o } }
GROUP BY ?g ORDER BY DESC(?triples)

Einen Datensatz abfragen

Die passende IRI aus der Liste einsetzen:

SELECT * WHERE {
  GRAPH <https://ld.schleswig-holstein.de/epayment> {
    ?s ?p ?o
  }
} LIMIT 100

Benannter Graph statt Default-Graph

Die Daten liegen in benannten Graphen. Eine Abfrage ohne GRAPH (SELECT * WHERE { ?s ?p ?o }) liefert sie nur, wenn im Dataset der Union Default Graph aktiv ist. Verlässlich ist immer die Form mit explizitem GRAPH ?g { … } oder FROM <graph>.