Files
swp-02-aufbereitung/README.md
T
2026-09-07 14:30:02 +02:00

8.4 KiB
Raw Blame History

Wurzelwerk — Phase 2: Aufbereitung

Reichert raw_items deterministisch an: Dubletten, Akteure, Orte, Tonalität, Embeddings. Kein LLM, kein Sampling, keine Netzabfrage zur Laufzeit — das Ergebnis hängt allein an der Eingabe und der coder_version.

Setzt das Ingest-Schema aus swp-01-ingest voraus und schreibt ausschließlich nach codings. raw_items bleibt unberührt.

Spezifikation: phase_02_spec.md.

Dateien

Datei Zweck
phase_02_spec.md Spezifikation der fünf Module und ihrer Abnahmekriterien
migrations/ Schemaänderungen, aufsteigend anzuwenden
db.py Auflösung von DATABASE_URL, Verbindung
check_db.py Rauchtest: Anmeldung, Bestand, fehlende Voraussetzungen
normalisierung.py Textnormalisierung und Trigramme, gemeinsam für alle Module
version.py coder_version aus den Komponenten
dubletten.py Modul 1: exakte Übernahmen
test_dubletten.py Testfälle dazu, ohne Datenbank

Einrichtung

python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
cp .env.example .env && chmod 600 .env && $EDITOR .env
.venv/bin/python check_db.py

Migrationen

Aufsteigend anwenden, auf Dev und Prod dieselben Dateien in derselben Reihenfolge. Welche schon liefen, steht in schema_migrationen:

psql "$ADMIN_DATABASE_URL" -v ON_ERROR_STOP=1 -f migrations/p2-000-migrationsbuch.sql
psql "$ADMIN_DATABASE_URL" -v ON_ERROR_STOP=1 -f migrations/p2-001-codings-mehrschichtig.sql

# Stand abfragen
psql "$DATABASE_URL" -c 'select name, angewandt_am from schema_migrationen order by name'

Die Zählung trägt das Präfix p2-, weil die Ingest-Migrationen eine eigene haben und beide auf dieselbe Datenbank laufen.

Zwei Migrationen brauchen einen Superuser, weil sie Rechte vergeben, die sie selbst noch nicht haben: p2-002 (create extension vector — pgvector ist nicht als trusted gekennzeichnet) und p2-003 (Besitzerwechsel). Im Dev-Container über den lokalen Socket:

podman exec -i postgres psql -U postgres -d wurzelwerk -v ON_ERROR_STOP=1 -f - < migrations/p2-003-eigentuemer.sql

Alle übrigen laufen über DATABASE_URL. Seit p2-003 gehören die Tabellen der Rolle wurzelwerk statt postgres — vorher scheiterte jedes alter table mit must be owner of table codings, und die Datenbank war damit der einzige Ausreißer auf der Instanz: gitea und hedgedoc besitzen ihre Tabellen vollständig. Die Erweiterungen selbst (pg_trgm, vector) bleiben beim Superuser, wo sie hingehören.

Voraussetzungen an die Datenbank

Das Embedding-Modul braucht pgvector. Im offiziellen Image postgres:latest ist die Erweiterung nicht enthalten; der Postgres-Container läuft deshalb auf docker.io/pgvector/pgvector:pg18-trixie — derselbe Build wie das offizielle Image (18.6-1.pgdg13+2, Debian 13, glibc 2.41), nur mit pgvector zusätzlich.

Der Tag ist mit Bedacht gewählt. Auf derselben Instanz liegen weitere Dienste; ein Wechsel der Hauptversion oder der Distribution wäre ein Ausfall, ein Wechsel der glibc ein Collation-Problem in den Textindizes aller Datenbanken (datlocprovider = 'c'). pg18-trixie schließt beides aus und zieht nur Minor-Updates nach. Die versionierten Tags (0.8.4-pg18-trixie) taugen dafür nicht: sie hängen der Postgres-Minor-Version hinterher und hätten 18.6 auf 18.4 zurückgedreht.

Das Anlegen der Erweiterung in wurzelwerk ist davon getrennt und geschieht in p2-002, nicht beim Containerstart.

Ein Vektor belegt 4100 Byte. Bei den derzeit rund 1400 Items pro Tag sind das etwa 2 GB im Jahr, mit HNSW-Index eher das Doppelte — tragbar. Die in der Spezifikation angenommenen 200–400 Items je Slot wären rund 38.000 am Tag und damit etwa 55 GB im Jahr, je coder_version. Ein Modellwechsel verdoppelt den Bedarf, bis der alte Bestand gelöscht ist. Sollte es eng werden, halbiert halfvec(1024) den Platz.

Grenzen des Datensatzes

Volltext liefert nur ein Teil der Quellen — Ingest liest RSS, und viele Häuser geben dort keinen content:encoded heraus (Stand 7.9.2026: faz 311/489, heise 174/196, aber derstandard, welt, dlf, taz, sz jeweils 0). Das ist keine Lücke, die sich schließen lässt, sondern eine Eigenschaft der Quellen. Module müssen auf Titel und Teaser allein tragfähig bleiben und dürfen die Vergleichsbasis nicht davon abhängig machen, ob ein Item zufällig Volltext hat.

Modul 1: Dubletten

.venv/bin/python dubletten.py --backfill              # Gesamtbestand
.venv/bin/python dubletten.py --slot 2026-09-07T08:00 # ein Poll-Slot
.venv/bin/python dubletten.py --backfill --trocken --stichprobe 200
.venv/bin/python dubletten.py --backfill --basis titel+teaser   # die andere Basis
.venv/bin/python -m unittest test_dubletten

Normalisierung, SimHash über Wort-Trigramme, Kandidaten über gemeinsame SimHash-Bänder, Bestätigung über Jaccard, Union-Find. Jedes verarbeitete Item bekommt eine Zeile, auch ein Einzelstück — sonst ließe sich „keine Dublette" nicht von „nicht verarbeitet" unterscheiden. Unveränderte Zeilen werden nicht angefasst, kodiert_am bleibt also stehen, wenn sich nichts geändert hat.

Gesamtbestand (3407 Items) 0,2 s, ein Slot 0,4 s. Das Zeitbudget von 3 Minuten je Slot ist für dieses Modul kein Thema.

Ergebnis auf dem Bestand vom 4.–7.9.2026: 135 Gruppen mit 290 Mitgliedern, davon 63 Gruppen mit 143 Items über mehrere Häuser hinweg — das ist der eigentliche Zweck. Größte Gruppe: fünf Häuser mit derselben Zeile über den Start der Isar-Aerospace-Rakete.

Vier Abweichungen von der Spezifikation

Vergleichsbasis ist der Titel, nicht Titel + Teaser. Gemessen wurde beides. Über Titel + Teaser findet das Verfahren auf diesem Bestand keine einzige hausübergreifende Übernahme; über den Titel allein 63 Gruppen. Grund: die Häuser übernehmen die Agenturüberschrift wörtlich und schreiben den Teaser selbst, der Teaser verdünnt also genau das Signal, das trägt. Die Zahlen stehen in NOTES.md. --basis titel+teaser schaltet auf die alte Basis zurück; sie ergibt eine eigene coder_version und überschreibt nichts.

Das Ressortkürzel vor dem Titel wird abgetrennt. Raumfahrt: Deutsche Rakete … und Deutsche Rakete … sind dieselbe Meldung, Notfälle: und Flugverkehr: stehen vor derselben dpa-Zeile. Die Regel ist eng gefasst: höchstens vier Wörter vor dem Doppelpunkt, mindestens fünf danach — sonst verlöre Habeck: Wir haben uns geirrt seine Aussage.

Die Gruppen-UUID kommt vom Leitartikel, nicht aus der Mitgliedermenge. Sonst änderte ein später eintreffendes Mitglied die Kennung einer Gruppe, die Phase 3 bereits kodiert hat. Der Leitartikel ist das älteste Mitglied; ein neues Mitglied ist zwangsläufig jünger und verschiebt das Minimum nicht.

Dasselbe Haus an verschiedenen Tagen wird nicht gepaart. Wiederkehrende Formate — „tagesschau in 100 Sekunden", „Wetter", „Die Nachrichten" — sind zeichengleich, aber jeweils ein eigener Vorgang. Ohne diese Regel wuchsen sie transitiv zu einer Gruppe aus 19 Sendungen über drei Tage zusammen.

Warum ein Slot-Lauf seinen Rand wachsen lässt

Die Items eines Slots brauchen ihre Partner im Datumsfenster, diese Partner ihrerseits ihr eigenes Fenster. Ein fester Rand genügt trotzdem nicht: Übernahmen bilden Ketten — dieselbe Schlagzeile erscheint am Montag bei einem, am Dienstag beim zweiten, am Mittwoch beim dritten Haus. Wird eine solche Kette am geladenen Rand abgeschnitten, fällt die Gruppe anders aus als im Backfill.

Der Lauf lädt deshalb, gruppiert, und lädt erneut, solange eine Gruppe bis an den Rand reicht. Geschrieben wird nur der innere Bereich, in dem jedes Item seine vollständige Nachbarschaft gesehen hat.

Nachgewiesen: nach einem Backfill ändern beliebig viele Slot-Läufe nichts mehr (neu 0, geändert 0), und ein anschließender Backfill ebenso wenig. Live-Lauf und Neuaufbau liefern dasselbe Ergebnis — Invariante 2 und 4.

Was bleibt

Auf dem gemessenen Tag findet das Verfahren 42 von 45 hausübergreifenden Paaren. Die drei fehlenden gehen am SimHash-Bandvorfilter verloren, nicht an den Schwellen — bei sechs bis acht Trigrammen je Titel ist SimHash grob. Ein Trigramm-Index als zweiter Kandidatenweg würde sie einfangen; das ist nicht gebaut, weil die Spezifikation Präzision über Recall stellt.

Die Stichprobe (--stichprobe 200) schreibt die Paare zum Prüfen von Hand heraus, die zweifelhaftesten zuerst. Sie enthält Titel im Klartext und ist deshalb nicht eingecheckt.