Blanke noder for gyldighetsinformasjon i referanseobjekter - Utdanningsdirektoratet/Grep_SPARQL GitHub Wiki

Forklaring på et par begreper som er brukt i denne artikkelen:

  • Gyldighetsinformasjon: Egenskapene "gyldig-fra" og "gyldig-til" med sine dato-verdier (som i json ligger under egenskapen "gyldighet")
  • Referanseobjekter: json-objekt {...} som refererer til et annet objekt.
  • Blanke noder: "verdier" i sparql-retur som begynner med _:. Se nærmere forklaring nedenfor

I artikkelen Forflatning-av-objekter-og-oppløfting-av-attributter,-JSON VS JSON LD sier vi at vi i json-ld, og dermed også RDF/SPARQL, kun viser URIen til en referanse. Resten av informasjonen ligger uansett i objektet det refereres til. Men vi har ett unntak, og det er når gyldighetsinformasjonen til referanseobjektet er en egenskap for koblingen, og ikke for de tilkoblede objektene i seg selv. Vi snakker om gyldighet for koblingen.

Eksempel

Fagkoden ADI2001 er i seg selv

  • "gyldig-fra": "2007-08-01T00:00:00"
  • "gyldig-til": "2022-12-31T00:00:00"

Den har også et referanseobjekt til 'opplaeringsfag', som vi gjengir som et json-objektet nedenfor:

"opplaeringsfag": [
   {
      "kode": "ADI2Z01",
      "uri": "http://psi.udir.no/kl06/ADI2Z01",
      "url-data": "https://data.udir.no/kl06/v201906/opplaeringsfag/ADI2Z01",
      "tittel": "Nyanlegg",
      "gyldighet": {
         "gyldig-fra": null,
         "gyldig-til": "2021-07-31T00:00:00"
      },
      "id": "uuid:33ab6917-0db3-4e57-ac7e-a6eae3c2c064",
      "grep-type": "http://psi.udir.no/ontologi/kl06/opplaeringsfag",
      "status": "https://data.udir.no/kl06/v201906/status/status_publisert"
   }
],

Her kan vi se at koblingen mellom fagkoden ADI2001 og opplæringsfaget ADI2Z01 er gyldig til "2021-07-31T00:00:00". Men slår vi opp dette opplæringsfaget isolert, vil vi se at dette objektet ikke har noe gyldighetsinformasjon i seg selv. Det handler altså om når koblingen er gyldig, og ikke objektene selv.

Når vi skal gjengi dette json-objektet over i RDF-sammenheng, kommer vi derfor ikke forbi begrepet blanke noder. Og nå er vi nok inne på et at de kanskje mest forvirrende og kompliserte hjørnene av SPARQL, men se disse to videoene laget av Noureddin Sadawi som forklarer dette fenomenet innen RDF nærmere (ses best i rekkefølge):

  1. Understanding Blank Nodes
  2. Searching with Blank Nodes

I vårt tilfelle får vi flere ledd med blanke noder for å komme nederst i strukturen og hente verdien "2021-07-31T00:00:00" for "gyldig-til".

Utsnitt av json-objektet:

"opplaeringsfag": [
   {
      "uri": "http://psi.udir.no/kl06/ADI2Z01",
      "gyldighet": {
         "gyldig-til": "2021-07-31T00:00:00"
      }
   }
]

I RDF kan vi ikke peke på både uri og gyldighet samtidig, siden RDF har en flat struktur som baserer seg på tripler. All informasjon ligger (litt forenklet) i en tabell med tre kolonner som vi kan illustrere slik:

  • ADI2001, opplaeringsfag, ADI2Z01 ("ADI2001 har opplæringsfag, ADI2Z01") Men vi kan ikke ha seks ledd, slik:
  • ADI2001, opplaeringsfag, ADI2Z01, gyldighet, gyldig-til, "2021-07-31T00:00:00"

Dette har vi løst slik at informasjonen kan uttrykkes på denne måten (med kun tre ledd i hver linje):

  • ADI2001, opplaeringsfag, ADI2Z01
  • ADI2001, gyldighet-opplaeringsfag-ADI2Z01, blankNode01
  • blankNode01, gyldig-til, "2021-07-31T00:00:00"

Disse "triplene" utgjør nå en graf som vi f.eks. kan gjøre spørringer på.

Løsningen vår er med andre ord at vi har "diktet opp" (egentlig: "fått generert") en egen egenskap for gyldighetsinformasjonen til ADI2001 sin kobling til det spesifikke opplæringsfaget ADI2Z01, nemlig "gyldighet-opplaeringsfag-ADI2Z01", som i sin tur holder på både "gyldig-fra" og gyldig-fra, som ligger i en blank node (i eksempelet, "blankNode01). Generelt kan vi si at denne egenskapen er satt sammen slik: "gyldighet-"+[type element som er tilkoblet *]+"-"+[koden til tilkoblet element].

* = enten type eller forflatet property/egenskap for koblingen

Dette gjør at vi slipper unna med bare ett ledd med blanke noder. Det å håndtere flere lag med blanke noder er svært krevende, og vi har vurdert at selv om vi da får litt "støy" med mange nye unike properties, så oppveier fordelene med det, flere ganger.

Hvordan ser dette ut i praksis i sparql?

I sparql-spørringen nedenfor, viser vi fram koblingen til opplæringsfag, men også den sammensatte egenskapen vi nevnte ovenfor. Dermed kan vi spørre videre på denne blanke noden, og får i retur det som ligger gjemt der:

PREFIX d: <http://psi.udir.no/kl06/>
PREFIX u: <http://psi.udir.no/ontologi/kl06/>
select * where { 
    d:ADI2001 u:opplaeringsfag ?opplaeringsfag ;
              u:gyldighet-opplaeringsfag-ADI2Z01 ?bnode .
    ?bnode ?p ?o .
}

Denne spørringen gir følgende resultat:

opplaeringsfag bnode p o
1 d:ADI2Z01 _:genid-10f4a3c6b9d84524849533717f532132-b1 u:gyldig-til "2021-07-31T00:00:00"

Legg merke til at verdien for ?bnode i dette resultatet ikke nødvendigvis er det samme som du vil få når du kjører samme spørring. Det er fordi verdien (referansen) til den blanke noden blir generert av sparql-motoren (er altså ikke noe vi selv vedlikeholder). Derfor kan vi ikke referere direkte til disse i spørringer. De oppstår fra gang til gang, og viser bare til verdier som ligger bak. Men – og det er selve meningen med blanke noder – vi får tak i informasjonen bak de blanke nodene hvis vi lokker på dem via variabler, som i dette eksempelet "?bnode" som holder på informasjonen vi er ute etter: "u:gyldig-til" som her gir "2021-07-31T00:00:00").

Vanligvis vet vi jo at egenskapene vi bruker i predikat-delen av spørringer er felles for mange forekomster. Men med måten vi har gjort det på, ser vi får en unik egenskap som kun gjelder for én, og bare én kobling. u:gyldighet-opplaeringsfag-ADI2Z01 gjelder kun for koblingen mellom fagkoden ADI2001 og ADI2Z01. Dette kan være litt problematisk når vi ikke kun ser på én fagkode, men en liste av fagkoder. Da blir predikatet forskjellig fra fagkode til fagkode.

Spørringen nedenfor bøter på dette problemet, og legg merke til at vi nå ikke spør etter én fagkode, men har fagkode som variabel i starten av spørringen (?fagkode):

PREFIX d: <http://psi.udir.no/kl06/>
PREFIX u: <http://psi.udir.no/ontologi/kl06/>
select ?ofKode ?p ?o where { 
    ?fagkode a u:fagkode ;
             u:opplaeringsfag/u:kode ?ofKode;
             ?gyldigetskobling ?bnode .
    FILTER isBlank(?bnode)
    FILTER (regex(str(?gyldigetskobling), ?ofKode))
    ?bnode ?p ?o .
} LIMIT 100

Her ser vi at vi har en variabel ?gyldighetskobling som vi i regex-filteret binder til kun de vi får i retur som har i seg opplæringsfag-koden. Regex-filteret bruker koden til opplæringsfaget som vi to linjer opp har bundet til variabelen ?ofKode. Verdiene du får under ?ofKde er altså det vi bruker i regexfilteret: Vi vil kun ha u:gyldighet-opplaeringsfag-[ett-eller-annet], der [ett-eller-annet] er opplæringsfag-koden.

Vi har også med et filter: FILTER isBlank(?bnode) som gjør at vi kan tvinge sparql til kun å evaluere trippelen nederst (der både subjekt, predikat og objekt er variabler) til at ?bnode faktisk er blanke noder (da hopper vi over alle navngitte predikater, og sparer litt tid).

Jeg har nederst satt "LIMIT 100", for dette gir like mange treff (i skrivende stund 9 745) som det er koblinger mellom fagkoder og opplæringsfag ganger antall gyldig-fra og/eller -til. Dette er altså en ganske tung spørring, så ikke kjør denne spørringen i produksjonsmiljøet uten at du enten bruker LIMIT (som i eksempelet over), paginerer (kombo av LIMIT og OFFSET) eller kun tar utgangspunkt i et mindre utvalg av fagkoder (f.eks ikke-utgåtte fagkoder, knyttet til en læreplan e.l.). Her kan spørringen fort bruke minutter, og ikke fraksjoner av et sekund som vi liker å holde oss til. Vi har selv ikke testet dette med Curl. Det er mulig man sparer noen sekunder da. Moralen er: Bruk med forsiktighet!




Hvis noen med skills har ideer på hvordan den nederste spørringen kan effektiviseres, ta kontakt med oss :-)

⚠️ **GitHub.com Fallback** ⚠️