Dublettenprüfung
This commit is contained in:
@@ -17,6 +17,10 @@ Spezifikation: [`phase_02_spec.md`](phase_02_spec.md).
|
||||
| `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
|
||||
|
||||
@@ -43,24 +47,46 @@ psql "$DATABASE_URL" -c 'select name, angewandt_am from schema_migrationen order
|
||||
Die Zählung trägt das Präfix `p2-`, weil die Ingest-Migrationen eine eigene
|
||||
haben und beide auf dieselbe Datenbank laufen.
|
||||
|
||||
**Migrationen brauchen einen anderen Zugang als der Betrieb.** Die Tabellen
|
||||
gehören `postgres`, nicht `wurzelwerk`; `alter table` scheitert sonst mit
|
||||
`must be owner of table codings`. Im Dev-Container:
|
||||
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:
|
||||
|
||||
```sh
|
||||
podman exec -i postgres psql -U postgres -d wurzelwerk -v ON_ERROR_STOP=1 -f - < migrations/p2-001-codings-mehrschichtig.sql
|
||||
podman exec -i postgres psql -U postgres -d wurzelwerk -v ON_ERROR_STOP=1 -f - < migrations/p2-003-eigentuemer.sql
|
||||
```
|
||||
|
||||
Dass die Anwendungsrolle ihr eigenes Schema nicht besitzt, ist eine offene
|
||||
Frage — entweder bleibt es so und Migrationen laufen als Administrator, oder
|
||||
die Tabellen wechseln per `alter table … owner to wurzelwerk` den Besitzer.
|
||||
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
|
||||
|
||||
`pgvector` fehlt im Image `postgres:latest`: `select * from pg_available_extensions
|
||||
where name = 'vector'` bleibt leer, `create extension vector` scheitert. Ohne
|
||||
sie kein `item_embeddings`. Das ist ein Image-Tausch auf `pgvector/pgvector:pg18`,
|
||||
keine Migration. Die vier übrigen Module laufen ohne.
|
||||
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
|
||||
|
||||
@@ -70,3 +96,82 @@ 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
|
||||
|
||||
```sh
|
||||
.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.
|
||||
|
||||
Reference in New Issue
Block a user