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.
requestsfür HTTP.BeautifulSoupfür HTML-Parsing.psycopg2fü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?
Full-Text-Search
PostgreSQL hat eingebaute Volltextsuche. Für vier Sprachen brauchten wir vier verschiedene Text-Search-Konfigurationen:
germanfür DEfrenchfür FRitalianfür ITenglishfü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.