Ga naar hoofdinhoud

Wij zijn de Hypermedia!

· 8 minuten leestijd
Koen Van Daele
Onroerend Erfgoed

Binnen de Vlaamse Overheid coördineert een centraal orgaan het beleid rond Informatie en ICT. Onder die koepel zijn er verschillende werkgroepen actief, waaronder de werkgroep OSLO-datastandaarden, die standaarden, semantische modellen en tools ontwikkelt voor Vlaamse overheidsdiensten en partners. Eén van hun projecten in 2018 was een Generieke Hypermedia API-standaard: richtlijnen en bouwstenen om REST-services meer hypermedia-gedreven, en dus interoperabeler, te maken. De eerste versie, goedgekeurd op 8 november 2018, bevat drie bouwstenen: taal, CRUD en paginering.

Paginering op webschaal​

Bij Onroerend Erfgoed zijn we onze informatiesystemen beginnen afstemmen op deze standaard, vooral op de bouwsteen paginering. Voorlopig kiezen we voor de HTTP Link-header-methode van paginering, zoals hier gespecificeerd en gedetailleerd in RFC 5988.

Om dit in actie te zien, kijk je naar de Link-HTTP-header — via een commandolijntool zoals cURL, een browserplug-in of een REST-client. Wij gebruiken cURL, aangezien die meestal al aanwezig of makkelijk te installeren is op Linux en Mac.

Stel dat we geïnteresseerd zijn in boeken en documenten over Knokke, een kustgemeente in Vlaanderen. De REST API-documentatie op https://bib.onroerenderfgoed.be/api_docs leert ons dat we https://bib.onroerenderfgoed.be/werken kunnen aanroepen met een parameter titel, dus https://bib.onroerenderfgoed.be/werken?titel=knokke geeft ons werken met Knokke in de titel, of in een hoofdstuktitel:

$ curl -G -i -H Accept:application/json https://bib.onroerenderfgoed.be/werken?titel=knokke
HTTP/1.1 200 OK
Date: Mon, 16 Dec 2019 19:51:07 GMT
Server: Apache
Content-Type: application/json
Content-Length: 3933
Access-Control-Expose-Headers: Content-Range, X-Content-Range
Accept-Ranges: items
Content-Range: items 0-9/167
Link: <https://bib.onroerenderfgoed.be/werken?titel=knokke&per_pagina=10&pagina=2>; rel="next", <https://bib.onroerenderfgoed.be/werken?titel=knokke&per_pagina=10&pagina=17>; rel="last", <https://bib.onroerenderfgoed.be/werken?titel=knokke&per_pagina=10&pagina=1>; rel="first"
X-Content-Type-Options: nosniff
Access-Control-Allow-Origin: *
X-Permitted-Cross-Domain-Policies: none
X-XSS-Protection: 1; mode=block

[{"publicatietype": {"url": "http://purl.org/ontology/bibo/Document", "naam": "Document", "id": 1}, "titel": "Het landelijke Knokke", "self": "https://bib.onroerenderfgoed.be/werken/552291", "uri": "https://id.erfgoed.net/infocat/werken/552291", "auteur": "D'hont A.", "id": 552291}, "... nog 9 resultaten ..."]

Deze curl-aanroep haalt boeken en documenten (werken) op uit onze bibliotheekcatalogus met knokke in de titel, en vraagt om ook de headers af te drukken. De header waar het ons hier om gaat is Link:

Link: <https://bib.onroerenderfgoed.be/werken?titel=knokke&per_pagina=10&pagina=2>; rel="next", <https://bib.onroerenderfgoed.be/werken?titel=knokke&per_pagina=10&pagina=17>; rel="last", <https://bib.onroerenderfgoed.be/werken?titel=knokke&per_pagina=10&pagina=1>; rel="first"

We krijgen resultaten terug, maar slechts een gedeeltelijke set. De Link-header vertelt ons dat er een next-pagina, een last-pagina en een first-pagina bestaan — en omdat we op pagina 1 zitten, geen previous. We kunnen deze links volgen om pagina per pagina verder te gaan, of meteen naar het einde springen. Belangrijk: een client mag niets veronderstellen buiten wat de links letterlijk zeggen. Je zou uit "17 pagina's van elk maximaal 10 records" kunnen afleiden dat er tussen de 161 en 170 resultaten zijn, maar de server is niet verplicht om die rekensom te laten kloppen, of om andere informatie in de link te steken. Dit toont meteen een echte beperking van paginering via de Link-header: er is geen manier om te weten hoeveel resultaten er in totaal zijn, of hoeveel pagina's er nog volgen. De client weet enkel of er een next-link is; is die er niet, dan zit je op de laatste pagina, en zijn next en last identiek, dan zit je op de voorlaatste. Het voordeel: de client hoeft niets te weten over de interne structuur van de URL — geen page versus pagina, geen limit/offset — waardoor dezelfde clientcode makkelijk hergebruikt kan worden voor heel verschillende webservices. Het enige wat de client moet weten: is er een next-link, dan is er meer op te halen.

Voor de tweede pagina:

$ curl -G -i -H Accept:application/json https://bib.onroerenderfgoed.be/werken?titel=knokke\&per_pagina=10\&pagina=2

Al jullie data is van ons!​

Om dit te demonstreren schreven we een klein Python-script dat verschillende van onze informatiesystemen bevraagt en de paginering via de Link-header automatisch afhandelt. In Python is de bibliotheek requests de standaard voor REST-aanroepen, en handig genoeg heeft die al ingebouwde ondersteuning voor de Link-header, waardoor het triviaal wordt om over alle resultaten te itereren. Voor dit voorbeeld halen we alle afbeeldingen (Zoeken in de beeldbank), thema's (Thema's, personen en gebeurtenissen), erfgoed- en aanduidingsobjecten (Erfgoedobjecten en aanduidingsobjecten) op voor de gemeente Knokke-Heist. Het is een kort scriptje dat een berg URI's aan je voeten legt, door de verschillende API's te doorlopen en telkens de next-link te volgen.

Als je het script uitvoert, krijg je een lange lijst URI's te zien — functioneel, maar niet meteen bevredigend: een URI is fijn voor een machine, maar zegt een mens op zich weinig.

Geen context, geen informatie​

Een andere interessante techniek in de Linked Data-wereld is JSON-LD (JSON for Linking Data), dat gewone JSON omzet naar linked data door ze via een context te koppelen aan RDF — je definieert daarin welke JSON-sleutels met welke RDF-eigenschappen overeenkomen. Hier is een eenvoudige context die je zou kunnen gebruiken op ons beeldbank-endpoint:

{
"dct": "http://purl.org/dc/terms/",
"title": "dct:title",
"description": "dct:description",
"uri": "@id"
}

Dit definieert een naamruimte-afkorting dct voor de Dublin Core Terms-naamruimte, zodat dct:title staat voor het volledige http://purl.org/dc/terms/title, en koppelt uri aan @id, de manier waarop JSON-LD de unieke identificator van een record aanduidt. Pas je deze context toe op een afbeelding uit onze beeldbank, dan krijg je:

{
"@id": "https://id.erfgoed.net/afbeeldingen/392715",
"http://purl.org/dc/terms/title": "Villa Cools met kinesistenpraktijk",
"http://purl.org/dc/terms/description": ""
}

Dit vertelt ons dat er een resource is met identificator https://id.erfgoed.net/afbeeldingen/392715, te beschrijven met een Dublin Core title. In JSON-LD is het gebruikelijk om ook een @type toe te kennen, een URI die verwijst naar een klasse in een RDF-vocabulaire, zodat records uit verschillende bronnen onderscheidbaar blijven eens samengevoegd:

{
"@id": "https://id.erfgoed.net/afbeeldingen/392715",
"@type": "http://purl.org/dc/dcmitype/Image",
"http://purl.org/dc/terms/title": "Villa Cools met kinesistenpraktijk",
"http://purl.org/dc/terms/description": ""
}

Nog eens, met gevoel​

Laten we JSON-LD gebruiken om ons script te verbeteren. We willen een homogene dataset met alles wat we weten over Knokke-Heist, waarbij elk record dezelfde structuur deelt — dus bouwden we twee JSON-LD-contexten, één voor de beeldbank en één gedeeld door de drie nauw verwante inventarisdatasets. Omdat geen van deze datasets standaard hun eigen resourcetype meegeven, voegen we dat zelf toe. Ons inventarissysteem heeft ook een veld locatie_samenvatting dat de geografische locatie van een object samenvat — de onderliggende samenvattingslogica kan behoorlijk complex zijn (sommige dingen liggen in meer dan één straat, gemeente of zelfs provincie), maar is erg handig voor een mens die snel door resultaten bladert. De beeldbank heeft zo'n veld niet, dus berekenen we er zelf een eenvoudig equivalent voor (de locatie van afbeeldingen is veel eenvoudiger dan die van erfgoedobjecten).

Het script haalt alles op zoals voorheen, voegt het recordtype en een locatiesamenvatting voor afbeeldingen toe, en expandeert alles naar JSON-LD met de Python-bibliotheek pyLD. Dat levert geldige, maar onhandige JSON-LD op — elke sleutel is nu een URI. Dus comprimeren we opnieuw, met een derde context gebaseerd op Dublin Core Terms, en drukken we het resultaat af.

Om dit script te draaien, installeer je eerst requests en pyLD. Voor grotere gemeenten zoals Gent of Antwerpen kan het wel even duren. Als extraatje schreven we ook een variant die de resulterende dataset naar een csv wegschrijft in plaats van af te drukken — pas gerust één van beide scripts aan om je eigen dataset voor een andere gemeente op te bouwen.

Interesseren Hypermedia API's en Linked Data je? Op https://data.vlaanderen.be/ vind je veel meer achtergrond en verwijzingen naar de relevante standaarden en documenten, en er is een hele community van Linked Data-mensen die je vragen graag beantwoordt — een aantal van hen vind je op Twitter. Zoals steeds, neem gerust contact op met verdere vragen.