Anteckningar från utforskandet av Kungliga bibliotekets öppna data, maj 2026. Sammanfattning skriven av Claude Opus 4.7
Vi behöver hämta hela ämnesordslistor från Libris — SAO, barnämnesord, SAOGF, barngf och deras geografiska/genre-syskon — för att kunna matcha katalogposter mot dem. KB exponerar dessa via tre olika API:er, var och en med sin karaktär. Den här texten är våra anteckningar från valet vi gjorde, och fungerar förhoppningsvis även när tjänsterna har utvecklats vidare: queryerna och länkarna nedan går att klistra in i en terminal idag och du kan jämföra ditt resultat med våra siffror.
Sammanhanget
id.kb.se är KB:s tjänst för persistenta identifierare. Varje ämnesord har en URI som följer mönstret https://id.kb.se/term/<schema>/<urlencode(label)>. Slår man upp URI:n med Accept: application/ld+json får man tillbaka termen som JSON-LD:
curl -H "Accept: application/ld+json" https://id.kb.se/term/barn/Sommaren
{
"@id": "https://id.kb.se/term/barn/Sommaren",
"@type": "Topic",
"broader": [{"@id": "https://id.kb.se/term/barn/%C3%85rstiderna"}],
"inScheme": {"@id": "https://id.kb.se/term/barn"},
"prefLabel": "Sommaren"
}
Det är representationen. Frågan vi behövde svara på var hur man listar alla termer i ett schema. KB exponerar tre vägar för det.
Spår 1 — SPARQL
KB:s tripelstore (Virtuoso) på libris.kb.se/sparql är den klassiska semantiska vägen.
PREFIX : <https://id.kb.se/vocab/>
SELECT ?topic ?type ?label
WHERE {
[] :mainEntity ?topic .
?topic a ?type ;
:inScheme <https://id.kb.se/term/barn> ;
:prefLabel ?label .
}
ORDER BY ?label
För många termer fungerar det utmärkt — Hundar, Katter osv. faller tillbaka med sina kanoniska URI:er. Två observationer vi gjorde under arbetet:
Topic-noder utan utgående triplar
För en delmängd av termerna är topicens triplar (a :Topic, :prefLabel, :broader) lagrade under en blank node istället för under termens id.kb.se-URI. Den klassiska reproduktionen:
SELECT ?p ?o WHERE { <https://id.kb.se/term/barn/Sommaren> ?p ?o }
Den 27 maj 2026 gav frågan 0 rader — termen har inga utgående triplar från sin URI. Söker man baklänges får man däremot träffar:
SELECT ?s ?p WHERE { ?s ?p <https://id.kb.se/term/barn/Sommaren> }
Här dyker en mängd katalogposter upp som pekar på URI:n via :subject. URI:n existerar alltså i grafen, men bara som objekt — inte som subjekt för Topic-noden själv. Sökningen på ?topic :prefLabel "Sommaren" returnerar en blank node (nodeID://b<…>) som bär all metadata. Det är en konsekvens av hur tripelstoren exporteras, inte av hur termen är modellerad i Libris XL — JSON-LD-vyn på id.kb.se serverar termen korrekt med sin URI.
MaxSortedTopRows = 10 000
Virtuoso har en serverkonfiguration som avvisar ORDER BY-frågor där OFFSET + LIMIT överstiger 10 000. För större vokabulärer som SAO (~34 000 termer) träffar man väggen vid den elfte sidan med page size 1000:
HTTP 500 — Virtuoso 22023 Error SR353:
Sorted TOP clause specifies more then 11000 rows to sort.
Only 10000 are allowed. Either decrease the offset and/or row count
or use a scrollable cursor
Det är en hård cap som inte går runt med vanlig SPARQL — workarounds är att ta bort ORDER BY (då blir paginationen instabil av samma skäl som vi möter hos find nedan), filtrera ner med FILTER(STRSTARTS(?label, "A")) etc., eller använda Virtuosos icke-standard scrollable cursors.
Båda begränsningarna kan ändras på server-sidan av KB. I dagens läge gör de SPARQL mindre lämpligt för listning av stora vokabulärer, men kraftfullt för det det är gjort för: grafens uttrycksfulla relationer — transitiva broader/narrower, OPTIONAL-länkar, FILTER över strukturerade attribut.
Spår 2 — find
libris.kb.se/find är KB:s sök- och listningsändpunkt över Libris XL, ett JSON-LD-API ovanpå ett ElasticSearch-index. Filter uttrycks som platta nyckel-värde-par med dot-notation in i datan:
curl -H "Accept: application/ld+json" \
"https://libris.kb.se/find?inScheme.@id=https://id.kb.se/term/barn&_limit=200&_offset=0&_sort=@id"
Svaret är ett JSON-LD-objekt med totalItems, items[] och navigations-länkar:
{
"totalItems": 2723,
"items": [
{
"@id": "https://id.kb.se/term/barn/Akvarier",
"@type": "Topic",
"prefLabel": "Akvarier",
"inScheme": {"@id": "https://id.kb.se/term/barn"}
},
...
]
}
För barn fick vi 2723 termer (jämfört med SPARQL:s 2651 — find ser termer som SPARQL har som blank-noder). För SAO fick vi 34 793 — något som SPARQL inte kommer åt alls i ett svep.
Sort-parametern är avgörande
En liten men viktig detalj: utan explicit _sort är find:s ordning över sidor inte deterministisk. Items kan hoppa positioner mellan anrop, och paginering tappar då termer tyst utan att felmeddela. Den dag vi körde jämförelsen utan _sort hade vi inkonsistenta gap — termer som Dynamit, Grundämnen, YouTube saknades den ena minuten och fanns nästa.
Med _sort=@id (eller annan stabil nyckel) blir paginationen helt deterministisk:
for offset in 0 200 400 ...; do
curl ".../find?inScheme.@id=...&_limit=200&_offset=$offset&_sort=@id"
done
Detta är inte en bug — det är normal sökmotor-semantik. Utan tiebreaker är ordningen vad ElasticSearch returnerar i den ögonblicket, vilket kan ändras när indexet uppdateras under foten. Designen är konsekvent; man behöver bara veta om det.
Spår 3 — EMM (Entity Modification Monitor[Entity Metadata Management])
libris.kb.se/api/emm/full är KB:s bulk-dump-API. Tanken är att man hämtar hela datasetet en gång och sedan håller det uppdaterat via en ändringsström.
curl "https://libris.kb.se/api/emm/full?selection=type:Topic"
EMM paginerar djupt utan problem — inga 500:or vid OFFSET 10000, inga blank-noder. För vår jämförelse 2026-05-27, där vi reproducerade find:s inScheme-vy genom att union:a EMM-typerna Topic + GenreForm + Geographic + Temporal + ComplexSubject:
| Kategori | find | EMM (union) | SPARQL |
|---|---|---|---|
| barn | 2723 | 2723 | 2651 |
| barngf | 119 | 119 | 116 |
| sao_geo | 563 | 563 | 551 |
| sao | 34 793 | 34 761 | (timeout) |
| saogf | 1595 | 1623 | 2013 |
| sao_geo_complex | 1096 | 1096 | 1088 |
Find och EMM är väsentligen lika fullständiga. Skillnaderna går åt båda håll: find leder på sao med 32 termer (typen TopicSubdivision som vi separerar i en egen kategori), EMM leder på saogf med 28 termer. EMM har inga blank-noder och inga dubblettade URI:er.
Styrka: rena delmängder via selection
EMM:s selection-parameter låter dig ange typ-partitioner: type:Topic, type:GenreForm, type:Geographic, osv. Varje partition är en ren dump utan blank-noder eller schema-leakage utöver typen. Det betyder att man kan bygga upp en lokal kopia av exakt den partition man bryr sig om.
Ett par praktiska detaljer värda att känna till: bulk-dumparna för en typ inkluderar resurser från flera scheman (type:Topic ger både SAO- och queerlit-termer; type:GenreForm har gmgpc/swe och swepub bredvid SAOGF/barngf). Schemat måste filtreras client-side på inScheme.@id. Den uppenbara genvägen — selection=inScheme:<URI> — finns syntaktiskt men timeout:ade vid våra anrop.
Brist: ingen filterbar push-ström
EMM:s tekniska charm ligger i andra halvan av designen: efter att man hämtat full-dumpen ska man kunna ansluta till en ändringsström och få notifikationer när enskilda resurser ändras. Det fungerar — men strömmen är inte filterbar med samma selection-syntax. Du får ändringar på alla resurstyper, eller inget. Det betyder att om du vill ha en live-vy av "barntermer" får du själv konsumera hela strömmen och filtrera bort sådant som inte är intressant. Det är inte ett världsproblem för en client som ändå behöver göra sin egen scheme-filtrering, men det är skillnaden mellan en pure pull-and-forget-arkitektur och en notifikationsdriven.
Vad vi landade i
Vi använder EMM som primärkälla för termsorterna och find som freshness-signal — ett snabbt anrop till find returnerar totalItems, och om den siffran avviker från EMM-dumpens cachade summa vet vi att det är dags att hämta om dumpen. Allt cachas på disk; en livscykel-process startar uppvärmd från cache och pinar find för att se om något har förändrats.
SPARQL behåller vi för det den är bäst på: grafens egna relationer. För frågor som "alla ComplexSubject:s vars termComponentList innehåller en Geographic" är SPARQL fortfarande renast — även om vi i praktiken kunde uttrycka samma sak i find som termComponentList.@type=Geographic. För djupare traverseringar (rdf:rest*/rdf:first, transitive broader) finns ingen rimlig ersättning.
Allt detta är observationer från tjänsternas tillstånd i slutet av maj 2026. Blank-node-mönstret i SPARQL, MaxSortedTopRows-capen, find:s sorteringsdetalj och EMM:s pull-design är operativa egenskaper som kan komma att se annorlunda ut efter en kommande Libris XL-uppdatering. Queryerna ovan står sig dock — de går att klistra in och köra mot KB idag och själv se vad bilden är då du läser texten.
Länkar
- libris.kb.se/sparql — SPARQL-endpoint (Virtuoso)
- libris.kb.se/find — find-API (JSON-LD över ElasticSearch)
- libris.kb.se/api/emm/full — EMM full-dump
- id.kb.se — KB:s identifierartjänst, källan för termernas URI:er
- id.kb.se/term/barn — exempel: barnämnesordsschemat
- id.kb.se/term/sao — exempel: SAO-schemat