Idag checkade jag in en ganska stor ändring i
kodbasen för lagen.nu. Det är lite vanskligt att göra stora ändringar
nu såhär en vecka innan lagkommenteringsprojektet drar igång
på allvar, men just den här ändringen bör kunna göra det enklare att
pussla ihop ny funktionalitet jämfört med hur det ser ut idag.
Vad ändringen handlar om är att använda RDF på en djupare nivå än
vad som gjorts hittils, och med verktyg som är bättre lämpade. Det jag
har nu börjar kännas nära en arkitektur för att bygga
webbtillämpningar på semantic web-koncept istället för den traditionella kombon RDBMS + SQL + dynamiskt språk
(eller den något mindre traditionella dynamiskt språk + MVC-ramverk + O/R-mappning).
För att man ska förstå poängen börjar jag med att beskriva hur det har
sett ut tidigare.
Innehållet på lagen.nu skapas i huvudsak i fyra
steg: Download, Parse, Relate och Generate. De två första stegen handlar
om att hämta innehåll (lagtexter och rättsfall) från andra ställen,
respektive att semantiskt strukturera upp detta (från de text- och
wordfiler som laddats ner). Dessa berörs inte alls av ändringen.
Relate-steget är ganska enkelt: Det går igenom de strukturerade
XHTML2/RDFa-filer som
skapats och plockar ut alla RDF-triples. Dvs från filer som innehåller
både data (lagtext, domslutstext) och metadata (SFS-nummer, titlar,
lagrumshänvisningar…) så plockas metadatat fram. Detta lagras sedan
separat på lämpligt sätt.
Generate-steget tar sedan en strukturerad XHTML2-fil, exempelvis en
lagtext och transformerar den till XHTML 1.0, färdigt att visas i en
webbläsare. Denna transformering lägger till en massa information,
exempelvis sidhuvud och sidfot, innehållsförteckning, och listor av
relevanta rättsfall under varje paragraf.
För att göra det sista måste transformeringssteget på något vis ha
tillgång till en lista över de relevanta rättsfallen. Tidigare har
detta skett genom att Relate-steget konstruerat en (1) gigantisk
RDF/XML-fil med information om samtliga 10 000+ rättsfall som finns i
systemet, och XSLT-transformationen sedan laddat in denna med ett document()
-anrop. Det
funkar, men är både långsamt och oflexibelt.
Att skapa kopplingar mellan dokument genom systemets samlade metadata
skulle kunna användas på rätt många olika sätt, men idag görs det bara
på två sätt — dels för listorna på relevanta rättsfall under
lagtextparagraferna, dels för att se till att bara de rättsfall som
faktiskt finns i systemet är länkade i den lista över föregående
rättsfall som finns bredvid varje rättsfall. Det beror dels på att
det är bökigt att implementera nya kopplingar, dels på att det just nu
inte finns så mycket mer information i systemet än just lagtext och
(svenska) rättsfall.
Men med lagkommenteringsprojektet börjar det senare
ändras. Kommentaren för varje enskild paragraf lagras som ett
RDF-påstående, på formen:
<http://rinfo.lagrummet.se/publ/sfs/1998:204#P13> dct:description "Huvudregeln för känsliga uppgifter är ..."
Och även andra informationskällor ska in. Jag har fyra moduler i
varierande stadier av halvfärdighet (för propositioner och
utredningsbetänkanden, Allmäna
reklamationsnämnens praxis, JO-beslut och EG-domstolens
domslut) och en på planeringensstadiet (EG-lagstiftning).
Med flera informationskällor växer möjligheten för användbara
kopplingar exponentiellt. Under varje paragraf skulle man kunna lista
namnen på den proposition som föranlett lagändringar i denna, eller
namn på EG-direktiv som föranlett propositionerna. För varje sökord
som rättsfall använder kan man skapa en sida som listar andra
rättsfall med samma sökord. Och sen plocka in information från rikstermbanken, svenska Wikipedia och förstås vår egen wiki.
Om man kan skapa en koppling mellan EG-direktiv och svensk lag (på
paragraf/artikelnivå) skulle man tillochmed skapa en koppling mellan
svenska lagparagrafer och förhandsavgöranden från EG-domstolen för
andra länder. Om ni bara kunde se in i huvudet på mig skulle ni
förstå hur vackert allt kommer att bli.
Men systemet med att spara ner allt i gigantiska XML-blobbar och gräva
igenom dessa med XSLT-funktionen document()
skalar inte.
Så, vad dagens ändring gör är att byta ut metadatalagringen – från
stora statiska filer till en RDF-databas. Jag vet att det strider
mot mina tidigare principer, men med strax över en miljon poster
(triples) har jag insett
att det faktiskt behövs.
Enligt den nya ordningen så stoppar Relate-steget in all metadata i
Sesame-databasen istället för att stoppa in det i en gigantisk fil. I
Generate-steget körs sedan ett antal SPARQL-frågor mot denna databas,
och sparar ner steget i en temporär XML-fil, som bara innehåller den
information som är relevant för just det dokument som ska skapas. Den
läses sen in med document()
precis som tidigare. Skillnaden är att
denna temporärfil är liten och därmed snabbprocessad, samt att
utvecklingen för att stoppa ner nya datamängder i den är liten – dels
en SPARQL-fråga, dels lite klisterkod för att mappa svaret till en
XML-ig trädstruktur. Vad det gäller det senare hoppas jag kunna
använda SPARQLTree
för att kunna generalisera det helt.
Vad det gäller det förra så är det dock inte hel-enkelt att skriva bra
SPARQL-frågor. Den enda hjälpen som är något att ha är själva specen,
och den innehåller liksom inte så mycket tips av ”for
dummies”-karaktär. Det leder till att mina frågor är ganska yxiga än
så länge. Förhoppningsvis inser jag hur de kan förenklas allt eftersom
jag lär mig krypa på det här området.
Som ett exempel på svårigheterna kan nämnas hur man ska formulera en
fråga som hämtar information alla rättsfall som hänvisar till en viss
lag. Informationen om ett visst rättsfall lagras enligt följande mall
(N3-notation):
@prefix dct: <http://purl.org/dc/terms/> . @prefix rinfo: <http://rinfo.lagrummet.se/taxo/2007/09/rinfo/pub#> . <http://rinfo.lagrummet.se/publ/rattsfall/rh/2003:65> a rinfo:Rattsfallsreferat ; dct:creator <http://lagen.nu/org/2008/hovratten-for-nedre-norrland> ; dct:description "En kvinna med en insulinberoende diabetes, åtalad för misshandel[...]"@sv ; dct:identifier "RH 2003:65"@sv ; dct:relation ""@sv ; dct:subject "Uppsåt"@sv ; rinfo:avgorandedatum "2003-11-21"@sv ; rinfo:lagrum <http://rinfo.lagrummet.se/publ/sfs/1962:700#K1P2S2> rinfo:malnummer "B24-03"@sv .
Det intressanta här är rinfo:lagrum
-triplen (i fetstil). Det är fråga
om ett rättsfall som hänvisar till brottsbalken, närmare bestämt 1
kap. 2 § 2 st. Brottsbalken har i det här systemet URI:n
<http://rinfo.lagrummet.se/publ/sfs/1962:700>
.
En inititial SPARQL-fråga för att hämta information om alla rättsfall som hänvisar
till brottsbalken kan se ut såhär:
PREFIX dct:<http://purl.org/dc/terms/> PREFIX rinfo:<http://rinfo.lagrummet.se/taxo/2007/09/rinfo/pub#> SELECT ?uri ?id ?desc WHERE { ?uri rinfo:lagrum <http://rinfo.lagrummet.se/publ/sfs/1962:700> . ?uri dct:identifier ?id . ?uri dct:description ?desc }
(Not: om du inte redan kan grundläggande SPARQL rekommenderar jag att
exempelvis läsa wikipediasidans
introduktion).
Problemet är att den frågan enbart kommer att hitta rättsfall som
hänvisar till brottsbalken i sin helhet, inte rättsfall som hänvisar
till någon enskild paragraf. Detta eftersom URI:er i RDF är
ogenomskinliga — det finns inget inherent samband mellan
<http://rinfo.lagrummet.se/publ/sfs/1962:700>
och
<http://rinfo.lagrummet.se/publ/sfs/1962:700#K1P2S2>
(som
är URI:n för 1 kap. 2 § 2 st. Brottsbalken).
Det vi vill göra är att ställa en fråga som matchar alla rättsfall som
hänvisar till en viss lag eller något som är en del av samma lag
(enskilda kapitel, paragrafer eller stycken). Ett enkelt, fult,
långsamt och fel sätt är att göra det med ett filter:
PREFIX dct:<http://purl.org/dc/terms/> PREFIX rinfo:<http://rinfo.lagrummet.se/taxo/2007/09/rinfo/pub#> SELECT ?uri ?id ?desc ?lagrum WHERE { ?uri dct:identifier ?id . ?uri dct:description ?desc . ?uri rinfo:lagrum ?lagrum . FILTER regex(str(?lagrum), "http://rinfo.lagrummet.se/publ/sfs/1962:700") }
Ganska enkelt, men i praktiken väldigt långsamt, gissningsvis för att
SPARQL-motorn är tvungen att konvertera varenda URI (en datatyp) till
en sträng (en helt annan datatyp), för att sen se om den möjligtvis
börjar på ett visst sätt. Det är också fel, eftersom frågan gör ett
antagande om att
<http://rinfo.lagrummet.se/publ/sfs/1962:700#K1P2S2>
är en resurs som är en del av resursen
<http://rinfo.lagrummet.se/publ/sfs/1962:700>
, utan
någon täckning för det antagandet.
Det korrekta sättet är att modellera just detta
”är-del-av”-förhållande. I RDF-databasen finns en mängd triples av
följande typ:
<http://rinfo.lagrummet.se/publ/sfs/1962:700#K1P2S2> a rinfo:Stycke; dct:isPartOf <http://rinfo.lagrummet.se/publ/sfs/1962:700#K1P2>. <http://rinfo.lagrummet.se/publ/sfs/1962:700#K1P2> a rinfo:Paragraf; dct:isPartOf <http://rinfo.lagrummet.se/publ/sfs/1962:700#K1>. <http://rinfo.lagrummet.se/publ/sfs/1962:700#K1> a rinfo:Kapitel; dct:isPartOf <http://rinfo.lagrummet.se/publ/sfs/1962:700>.
Med det kan vi formulera en fråga som ger oss information om alla
rättsfall för en viss lagtext. Vi vill ha alla rättsfall som antingen
har en rinfo:lagrum
-relation med lagtextens URI
eller en rinfo:lagrum
-relation med en URI som har
en dct:isPartOf
-relation med lagtextens URI eller
en rinfo:lagrum
-relation med en URI som har en
dct:isPartOf
-relation med en URI som har en
dct:isPartOf
-relation med lagtextens URI
eller…
Ok, det blir inte så vackert det här heller, men det blir rätt och
snabbt. Den SPARQL-fråga jag har just nu blir såhär:
PREFIX dct:<http://purl.org/dc/terms/> PREFIX rinfo:<http://rinfo.lagrummet.se/taxo/2007/09/rinfo/pub#> SELECT ?uri ?id ?desc ?lagrum WHERE { { ?uri rinfo:lagrum <http://rinfo.lagrummet.se/publ/sfs/1962:700> . ?uri dct:identifier ?id . ?uri dct:description ?desc } UNION { ?uri rinfo:lagrum ?lagrum . ?lagrum dct:isPartOf <http://rinfo.lagrummet.se/publ/sfs/1962:700> . ?uri dct:identifier ?id . ?uri dct:description ?desc } UNION { ?uri rinfo:lagrum ?lagrum . ?lagrum dct:isPartOf ?b . ?b dct:isPartOf <http://rinfo.lagrummet.se/publ/sfs/1962:700> . ?uri dct:identifier ?id . ?uri dct:description ?desc } UNION { ?uri rinfo:lagrum ?lagrum . ?lagrum dct:isPartOf ?b . ?b dct:isPartOf ?c . ?c dct:isPartOf <http://rinfo.lagrummet.se/publ/sfs/1962:700> . ?uri dct:identifier ?id . ?uri dct:description ?desc } UNION { ?uri rinfo:lagrum ?lagrum . ?lagrum dct:isPartOf ?b . ?b dct:isPartOf ?c . ?c dct:isPartOf ?d . ?d dct:isPartOf <http://rinfo.lagrummet.se/publ/sfs/1962:700> . ?uri dct:identifier ?id . ?uri dct:description ?desc } UNION { ?uri rinfo:lagrum ?lagrum . ?lagrum dct:isPartOf ?b . ?b dct:isPartOf ?c . ?c dct:isPartOf ?d . ?d dct:isPartOf ?e . ?e dct:isPartOf <http://rinfo.lagrummet.se/publ/sfs/1962:700> . ?uri dct:identifier ?id . ?uri dct:description ?desc } }
Förslag på hur man kan förenkla det hela mottages tacksamt…
Det framgår kanske att det här inte är en arkitektur för alla
problem. Men om man har data som kan modelleras med RDF (och när man
jobbat med RDF ett tag så tycker man att i stort sett allt gör det) så
tror jag att åtminstone kombinationen triplestore + SPARQL kan vara
ett intressant alternativ till RDBMS + SQL.