Wie wir 1,14 Millionen Gerichtsentscheide in 3 Wochen indexiert haben

Ein Blick hinter die Kulissen: Wie Mont Virtua die grösste quellenverifizierte Schweizer Rechtsdatenbank aufgebaut hat. Technische Entscheide, Fehler und Lektionen.

Wie wir 1,14 Millionen Gerichtsentscheide in 3 Wochen indexiert haben

Man fragt uns manchmal, wie eine junge Firma eine Datenbank hat, die umfangreicher ist als das, was etablierte Anbieter in Jahren aufgebaut haben. Die Antwort ist unspektakulär: Wir haben die offensichtliche Sache getan, die niemand tut. Wir sind direkt zur Quelle gegangen.

Dieser Artikel ist kein Marketing. Es ist ein Erfahrungsbericht: Was wir gebaut haben, welche Entscheide wir getroffen haben und was schiefgegangen ist.

Tag 1: Die Erkenntnis

Schweizer Recht ist öffentlich. Vollständig. Kostenlos.

Fedlex publiziert alle Bundesgesetze. Die 26 Kantone publizieren ihre Gesetze. Das Bundesgericht publiziert seine Entscheide. Das Bundesverwaltungsgericht publiziert seine Entscheide. 115 Gerichte publizieren online.

Alles ist da. Auf offiziellen Staatsservern. In strukturierten Formaten. Kein Paywall. Kein Lizenzvertrag.

Trotzdem bezahlen Kanzleien und Firmen Tausende Franken pro Jahr für den Zugang zu Datenbanken, die im Kern dasselbe tun: diese öffentlichen Daten sammeln, aufbereiten und durchsuchbar machen. Die Aufbereitung hat Wert. Das Monopol auf die Daten selbst existiert nicht.

Wir haben uns gefragt: Was passiert, wenn wir dieselben öffentlichen Quellen nehmen, aber mit moderner Infrastruktur? Nicht die Technologie von 2005. Sondern: PostgreSQL mit Full-Text-Search in vier Sprachen. Vektoreinbettungen für semantische Suche. Ein Zitationsgraph mit 1,42 Millionen Kanten. Alles auf einem einzigen Server.

Woche 1: Scraping

Die Quellen

Quelle Typ Menge Format
Fedlex Bundesgesetze 4.800+ XML/HTML
26 kantonale Portale Kantonsgesetze 23.000+ HTML (26 verschiedene Formate)
BGer Bundesgerichtsentscheide ~70.000 HTML
BVGer Bundesverwaltungsgericht 91.582 HTML
Kantonale Gerichte Kantonsentscheide ~980.000 HTML (113 verschiedene Formate)
SHAB Amtsblatt 2.500.000 XML
FINMA Regulierung 27 Tabellen HTML/PDF

Die 26-Kantone-Herausforderung

Jeder Kanton hat sein eigenes Portal. Eigenes HTML-Format. Eigene URL-Struktur. Keine API. Keine einheitliche Schnittstelle.

Zürich liefert sauberes HTML mit konsistenter Struktur. Bern nutzt ein CMS aus den 2000er-Jahren mit tief verschachtelten Frames. Appenzell Innerrhoden hat eine statische Website, die vermutlich manuell gepflegt wird.

Wir haben für jeden Kanton einen eigenen Parser geschrieben. 26 Parser. Manche mit 50 Zeilen Code, manche mit 500. Die Arbeit war nicht intellektuell anspruchsvoll. Sie war geduldig.

Lektion 1: Der Engpass bei der Datenerfassung ist nicht Technologie. Es ist die Bereitschaft, sich durch 26 verschiedene HTML-Formate zu arbeiten, ohne Abkürzungen zu nehmen.

Scraper-Architektur

Wir haben bewusst einfach gebaut:

  • Python-Skripte. Kein Framework. Kein Scrapy. Kein Selenium.
  • requests für HTTP. BeautifulSoup für HTML-Parsing. psycopg2 für PostgreSQL.
  • Jede Quelle ein eigenes Skript. Kein Versuch, eine universelle Abstraktion zu bauen.
  • Inkrementelle Updates: Jeder Scraper merkt sich, wo er aufgehört hat. Beim nächsten Lauf holt er nur neue Einträge.

Lektion 2: Generische Scraping-Frameworks sparen Zeit, wenn die Quellen ähnlich sind. Wenn jede Quelle ein Sonderfall ist, ist ein simples Skript pro Quelle schneller geschrieben und einfacher zu debuggen.

Frequenz

Alles läuft nachts. Ein Cronjob um 02:00 Uhr startet die Pipeline. Fedlex zuerst, dann Kantone alphabetisch, dann Gerichte, dann SHAB. Gesamtlaufzeit: 2-4 Stunden, je nach Server-Antwortzeiten.

Wir belasten keine Server. Maximale Parallelität: 2 gleichzeitige Requests pro Quelle. Pausen zwischen Requests. Wir sind Gäste auf öffentlichen Servern und benehmen uns entsprechend.

Woche 2: Strukturierung und Indexierung

Das Schema

Rohdaten zu haben ist wertlos, wenn sie nicht strukturiert sind. Unsere Datenbankstruktur:

  • laws: 27.795 Gesetze mit Metadaten (SR-Nummer, Titel, Inkrafttretungsdatum, Kanton, Sprache, Status)
  • law_units: 2,02 Millionen Gesetzeseinheiten (Artikel, Absätze, Ziffern). Jede Einheit referenziert ihr Gesetz. Jede hat den Volltext in bis zu vier Sprachen.
  • decisions: 1,14 Millionen Gerichtsentscheide mit Metadaten (Gericht, Datum, Dossiernummer, Rechtsgebiet)
  • citations: 1,42 Millionen Zitationskanten. Welcher Entscheid zitiert welchen Artikel? Welcher Artikel wird von welchen Entscheiden zitiert?

PostgreSQL hat eingebaute Volltextsuche. Für vier Sprachen brauchten wir vier verschiedene Text-Search-Konfigurationen:

  • german für DE
  • french für FR
  • italian für IT
  • english für EN

Jede Gesetzeseinheit hat einen tsvector-Index pro Sprache. Die Suche nutzt ts_rank für Relevanz-Sortierung. Für die meisten Abfragen ist das ausreichend schnell: unter 200ms für Volltextsuche über 2 Millionen Einträge.

Lektion 3: PostgreSQL Full-Text-Search ist unterschätzt. Für strukturierte Textkorpora mit bekannten Sprachen liefert sie 90% der Qualität von Elasticsearch bei 10% der Betriebskomplexität.

Embeddings

Volltextsuche findet Wörter. Semantische Suche findet Konzepte.

Beispiel: Eine Suche nach «Kündigungsschutz während Krankheit» soll auch Entscheide finden, die «Sperrfrist» und «Art. 336c OR» erwähnen, ohne das Wort «Kündigungsschutz» zu enthalten.

Wir haben alle 2,02 Millionen Gesetzeseinheiten und 1,14 Millionen Entscheide mit dem Modell all-MiniLM-L6-v2 in 384-dimensionale Vektoren umgewandelt. Das Modell ist klein (80 MB), schnell und mehrsprachig. Nicht das beste Embedding-Modell auf dem Markt, aber für einen ersten Index völlig ausreichend.

Die Vektoren liegen in PostgreSQL mit der pgvector-Erweiterung. HNSW-Index mit m=16 und ef_construction=64. Approximate Nearest Neighbor Search über 3 Millionen Vektoren in unter 50ms.

Lektion 4: Beginnen Sie mit dem einfachsten Modell, das funktioniert. Ein schlechtes Embedding, das live ist, schlägt ein perfektes Embedding, das noch in der Evaluation steckt.

Der Zitationsgraph

Gerichtsentscheide zitieren Gesetze. Gesetze verweisen auf andere Gesetze. Spätere Entscheide bestätigen, präzisieren oder verwerfen frühere Entscheide.

Diese Beziehungen sind der eigentliche Wert. Nicht der Text selbst, der ist öffentlich. Sondern die Verbindungen.

Wir extrahieren Zitationen automatisch aus dem Entscheidungstext. RegEx-Muster für: «Art. 336c OR», «BGE 148 III 25», «BVGer A-1234/2025», kantonale Entscheidungsreferenzen. 1,42 Millionen Kanten. Stored in einer PostgreSQL-Tabelle. Abfragbar mit rekursiven CTEs.

Was das ermöglicht:

  • Welche Entscheide zitieren einen bestimmten Artikel?
  • Wie hat sich die Rechtsprechung zu einem Artikel über die Zeit entwickelt?
  • Gibt es Widersprüche zwischen Gerichten?
  • Welche Artikel werden am häufigsten zitiert (und sind damit am umstrittensten)?

Lektion 5: Der Zitationsgraph ist kein Feature. Er ist das Produkt. Alles andere ist Infrastruktur.

Woche 3: Qualitätssicherung

Was schiefgegangen ist

Problem 1: 34.000 strukturelle Knoten ohne Text. Gesetzeseinheiten wie «Zweiter Titel» oder «Dritter Abschnitt» sind Gliederungselemente, kein Inhalt. Wir haben sie zuerst mitindexiert, was die Suchergebnisse verschmutzt hat. Lösung: Diese Knoten identifizieren und von der Embedding-Pipeline ausschliessen. 34.000 Einträge bereinigt.

Problem 2: ~12.000 Entscheide ohne Text. Manche kantonalen Gerichte publizieren nur die Leitsätze, nicht den Volltext. Die Scraper haben diese Einträge angelegt, aber mit leerem Textfeld. Lösung: Markiert, nicht gelöscht. Die Metadaten (Gericht, Datum, Zitationen) haben trotzdem Wert.

Problem 3: Encoding-Chaos bei französischen Texten. Zwei kantonale Portale liefern Latin-1 statt UTF-8. Akzente wurden zu Fragezeichen. Lösung: Explizite Encoding-Erkennung mit chardet vor dem Parsing.

Problem 4: Duplikate bei Kantonsgesetzen. Einige Kantone publizieren dasselbe Gesetz unter verschiedenen URLs (konsolidierte Fassung und Änderungsgesetz). Unsere Deduplizierung basiert auf SR-Nummer + Kanton + Inkrafttretungsdatum. Das hat 98% der Duplikate gefangen. Die restlichen 2% wurden manuell bereinigt.

Was wir richtig gemacht haben

  • Keine Daten erfunden. Jeder Eintrag in unserer Datenbank hat eine Quell-URL, die auf einen offiziellen Staatsserver zeigt. Kein einziger Datenpunkt stammt aus einer Drittquelle.
  • Inkrementell statt monolithisch. Jeder Scraper kann einzeln neu gestartet werden. Wenn ein kantonales Portal offline ist, läuft der Rest weiter.
  • Alles in einer Datenbank. PostgreSQL. Kein Elasticsearch-Cluster, keine Redis-Instanz, kein separater Vektorstore. Eine Datenbank, die alles kann. Weniger Infrastruktur, weniger Ausfallpunkte.

Die Zahlen

Metrik Wert
Gesetze 27.795
Gesetzeseinheiten 2.020.000
Gerichtsentscheide 1.140.000
Zitationskanten 1.420.000
SHAB-Einträge 2.500.000
FINMA-Tabellen 27
Datenbankgrösse 41 GB
Entwicklungszeit 3 Wochen
Infrastrukturkosten CHF 0 (lokaler Server)
Laufende Kosten ~CHF 15/Monat (AI-Inferenz für Alerts)

Was das bedeutet

Wir besitzen keine exklusiven Daten. Wir besitzen eine Pipeline, die öffentliche Daten besser erschliesst als jeder bestehende Anbieter. Das ist kein Geheimnis. Die Datenquellen stehen jedem offen.

Der Vorteil liegt in der Ausführung: 26 Kantone korrekt geparst, 115 Gerichte abgedeckt, 1,42 Millionen Zitationskanten extrahiert, vier Sprachen indexiert, alles in einer Abfrage durchsuchbar. Das kann jeder nachbauen, der bereit ist, drei Wochen lang Parser für 26 verschiedene HTML-Formate zu schreiben.

Die meisten sind es nicht.


Dieser Artikel dient der Information und stellt keine Rechtsberatung dar.

Zurück zum Blog

Verwandte Artikel