Come abbiamo indicizzato 1,14 milioni di decisioni giudiziarie in 3 settimane
A volte ci chiedono come una giovane azienda possa disporre di una banca dati più completa di quanto fornitori consolidati abbiano costruito in anni. La risposta non è spettacolare: abbiamo fatto la cosa ovvia che nessuno fa. Siamo andati direttamente alla fonte.
Questo articolo non è marketing. È un resoconto dell’esperienza: cosa abbiamo costruito, quali decisioni abbiamo preso e cosa è andato storto.
Giorno 1: la presa di coscienza
Il diritto svizzero è pubblico. Completamente. Gratuitamente.
Fedlex pubblica tutte le leggi federali. I 26 cantoni pubblicano le loro leggi. Il Tribunale federale pubblica le sue decisioni. Il Tribunale amministrativo federale pubblica le sue decisioni. 115 tribunali pubblicano online.
Tutto è lì. Sui server ufficiali dello Stato. In formati strutturati. Nessun paywall. Nessun contratto di licenza.
Eppure studi legali e aziende pagano migliaia di franchi all’anno per accedere a banche dati che fanno fondamentalmente la stessa cosa: raccogliere questi dati pubblici, elaborarli e renderli consultabili. L’elaborazione ha valore. Il monopolio sui dati stessi non esiste.
Ci siamo chiesti: cosa succede se prendiamo le stesse fonti pubbliche, ma con un’infrastruttura moderna? Non la tecnologia del 2005. Bensì: PostgreSQL con ricerca full-text in quattro lingue. Embedding vettoriali per la ricerca semantica. Un grafo delle citazioni con 1,42 milioni di archi. Tutto su un singolo server.
Settimana 1: scraping
Le fonti
| Fonte | Tipo | Volume | Formato |
|---|---|---|---|
| Fedlex | Leggi federali | 4 800+ | XML/HTML |
| 26 portali cantonali | Leggi cantonali | 23 000+ | HTML (26 formati diversi) |
| TF | Decisioni del Tribunale federale | ~70 000 | HTML |
| TAF | Tribunale amministrativo federale | 91 582 | HTML |
| Tribunali cantonali | Decisioni cantonali | ~980 000 | HTML (113 formati diversi) |
| FUSC | Foglio ufficiale | 2 500 000 | XML |
| FINMA | Regolamentazione | 27 tabelle | HTML/PDF |
La sfida dei 26 cantoni
Ogni cantone ha il proprio portale. Il proprio formato HTML. La propria struttura URL. Nessuna API. Nessuna interfaccia unificata.
Zurigo fornisce HTML pulito con struttura coerente. Berna usa un CMS degli anni 2000 con frame profondamente annidati. Appenzello Interno ha un sito web statico probabilmente mantenuto a mano.
Abbiamo scritto un parser separato per ogni cantone. 26 parser. Alcuni con 50 righe di codice, altri con 500. Il lavoro non era intellettualmente impegnativo. Richiedeva pazienza.
Lezione 1: il collo di bottiglia nell’acquisizione dei dati non è la tecnologia. È la disponibilità a lavorare attraverso 26 formati HTML diversi senza prendere scorciatoie.
Architettura dello scraper
Abbiamo costruito volutamente in modo semplice:
- Script Python. Nessun framework. Nessun Scrapy. Nessun Selenium.
requestsper HTTP.BeautifulSoupper il parsing HTML.psycopg2per PostgreSQL.- Ogni fonte il proprio script. Nessun tentativo di costruire un’astrazione universale.
- Aggiornamenti incrementali: ogni scraper ricorda dove si è fermato. Al lancio successivo recupera solo le nuove voci.
Lezione 2: i framework di scraping generici fanno risparmiare tempo quando le fonti sono simili. Quando ogni fonte è un caso speciale, uno script semplice per fonte è più veloce da scrivere e più facile da debuggare.
Frequenza
Tutto gira di notte. Un cron job alle 02:00 avvia la pipeline. Prima Fedlex, poi i cantoni in ordine alfabetico, poi i tribunali, poi il FUSC. Durata totale: 2-4 ore a seconda dei tempi di risposta dei server.
Non sovraccarichiamo i server. Parallelismo massimo: 2 richieste simultanee per fonte. Pause tra le richieste. Siamo ospiti su server pubblici e ci comportiamo di conseguenza.
Settimana 2: strutturazione e indicizzazione
Lo schema
Avere dati grezzi è inutile se non sono strutturati. Il nostro schema di database:
- laws: 27 795 leggi con metadati (numero RS, titolo, data di entrata in vigore, cantone, lingua, stato)
- law_units: 2,02 milioni di unità legislative (articoli, capoversi, cifre). Ogni unità fa riferimento alla propria legge. Ciascuna contiene il testo integrale in fino a quattro lingue.
- decisions: 1,14 milioni di decisioni con metadati (tribunale, data, numero di dossier, area giuridica)
- citations: 1,42 milioni di archi di citazione. Quale decisione cita quale articolo? Quale articolo è citato da quali decisioni?
Ricerca full-text
PostgreSQL ha la ricerca full-text integrata. Per quattro lingue servivano quattro diverse configurazioni di ricerca testuale:
germanper il DEfrenchper il FRitalianper l’ITenglishper l’EN
Ogni unità legislativa ha un indice tsvector per lingua. La ricerca usa ts_rank per l’ordinamento per rilevanza. Per la maggior parte delle query è sufficientemente veloce: sotto i 200 ms per la ricerca full-text su 2 milioni di voci.
Lezione 3: la ricerca full-text di PostgreSQL è sottovalutata. Per corpus testuali strutturati in lingue note, fornisce il 90% della qualità di Elasticsearch al 10% della complessità operativa.
Embedding
La ricerca full-text trova parole. La ricerca semantica trova concetti.
Esempio: una ricerca «protezione contro il licenziamento in caso di malattia» deve trovare anche decisioni che menzionano «periodo di protezione» e «art. 336c CO» senza contenere il termine «protezione contro il licenziamento».
Abbiamo convertito tutti i 2,02 milioni di unità legislative e 1,14 milioni di decisioni in vettori a 384 dimensioni con il modello all-MiniLM-L6-v2. Il modello è piccolo (80 MB), veloce e multilingue. Non il miglior modello di embedding sul mercato, ma del tutto sufficiente per un primo indice.
I vettori risiedono in PostgreSQL con l’estensione pgvector. Indice HNSW con m=16 ed ef_construction=64. Ricerca approssimata dei vicini più prossimi su 3 milioni di vettori in meno di 50 ms.
Lezione 4: iniziate con il modello più semplice che funziona. Un embedding mediocre in produzione batte un embedding perfetto ancora in valutazione.
Il grafo delle citazioni
Le decisioni citano leggi. Le leggi rinviano ad altre leggi. Le decisioni successive confermano, precisano o ribaltano decisioni precedenti.
Queste relazioni sono il vero valore. Non il testo in sé, che è pubblico. Ma le connessioni.
Estraiamo le citazioni automaticamente dai testi delle decisioni. Pattern RegEx per: «art. 336c CO», «DTF 148 III 25», «TAF A-1234/2025», riferimenti a decisioni cantonali. 1,42 milioni di archi. Archiviati in una tabella PostgreSQL. Interrogabili con CTE ricorsive.
Cosa questo consente:
- Quali decisioni citano un determinato articolo?
- Come si è evoluta la giurisprudenza su un articolo nel tempo?
- Esistono contraddizioni tra tribunali?
- Quali articoli sono citati più frequentemente (e sono quindi più controversi)?
Lezione 5: il grafo delle citazioni non è una funzionalità. È il prodotto. Tutto il resto è infrastruttura.
Settimana 3: controllo qualità
Cosa è andato storto
Problema 1: 34 000 nodi strutturali senza testo. Unità legislative come «Secondo titolo» o «Terza sezione» sono elementi di struttura, non contenuto. Inizialmente le abbiamo indicizzate, il che ha inquinato i risultati di ricerca. Soluzione: identificare questi nodi ed escluderli dalla pipeline di embedding. 34 000 voci ripulite.
Problema 2: ~12 000 decisioni senza testo. Alcuni tribunali cantonali pubblicano solo i considerandi principali, non il testo integrale. Gli scraper hanno creato queste voci con il campo testo vuoto. Soluzione: contrassegnate, non cancellate. I metadati (tribunale, data, citazioni) mantengono il loro valore.
Problema 3: caos di encoding con i testi francesi. Due portali cantonali forniscono Latin-1 anziché UTF-8. Gli accenti diventavano punti interrogativi. Soluzione: rilevamento esplicito dell’encoding con chardet prima del parsing.
Problema 4: duplicati nelle leggi cantonali. Alcuni cantoni pubblicano la stessa legge sotto URL diversi (versione consolidata e legge di modifica). La nostra deduplica si basa su numero RS + cantone + data di entrata in vigore. Ha intercettato il 98% dei duplicati. Il restante 2% è stato pulito manualmente.
Cosa abbiamo fatto bene
- Nessun dato inventato. Ogni voce nella nostra banca dati ha un URL sorgente che punta a un server ufficiale dello Stato. Nemmeno un singolo punto dati proviene da una fonte terza.
- Incrementale, non monolitico. Ogni scraper può essere riavviato individualmente. Se un portale cantonale è offline, il resto continua.
- Tutto in un’unica banca dati. PostgreSQL. Nessun cluster Elasticsearch, nessuna istanza Redis, nessun vector store separato. Un’unica banca dati che fa tutto. Meno infrastruttura, meno punti di guasto.
I numeri
| Metrica | Valore |
|---|---|
| Leggi | 27 795 |
| Unità legislative | 2 020 000 |
| Decisioni giudiziarie | 1 140 000 |
| Archi di citazione | 1 420 000 |
| Voci FUSC | 2 500 000 |
| Tabelle FINMA | 27 |
| Dimensione database | 41 GB |
| Tempo di sviluppo | 3 settimane |
| Costi infrastruttura | CHF 0 (server locale) |
| Costi di esercizio | ~CHF 15/mese (inferenza IA per gli alert) |
Cosa significa
Non possediamo dati esclusivi. Possediamo una pipeline che sblocca i dati pubblici meglio di qualsiasi fornitore esistente. Non è un segreto. Le fonti dati sono aperte a tutti.
Il vantaggio risiede nell’esecuzione: 26 cantoni correttamente analizzati, 115 tribunali coperti, 1,42 milioni di archi di citazione estratti, quattro lingue indicizzate, tutto consultabile in un’unica interrogazione. Chiunque può replicarlo, a patto di essere disposto a dedicare tre settimane a scrivere parser per 26 formati HTML diversi.
La maggior parte non lo è.
Questo articolo ha finalità informative e non costituisce una consulenza legale.