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…
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.
Tags: lagen.nu, metadata, RDF, semantic web, sparql, webutveckling
[...] En arkitektur för webutveckling med semantic web-koncept [...]