Database connection
This commit is contained in:
@@ -0,0 +1,414 @@
|
||||
# Wurzelwerkt — Phase 2: Aufbereitung
|
||||
|
||||
**Status:** Entwurf
|
||||
**Gilt für:** Stinkwurzpresse / Projekt Wurzelwerkt
|
||||
|
||||
---
|
||||
|
||||
## 0. Zweck und Abgrenzung
|
||||
|
||||
Phase 2 reichert `raw_items` deterministisch an. Sie erzeugt keine Ereignisse und
|
||||
keine Aussagen — das ist Phase 3. Sie erzeugt die Vorbedingungen dafür:
|
||||
aufgelöste Akteure, Orte, Tonalität, Embeddings, und die Erkennung exakter
|
||||
Übernahmen.
|
||||
|
||||
### Invarianten
|
||||
|
||||
1. **Kein LLM.** Alles regelbasiert, lexikonbasiert oder durch ein festes
|
||||
Embedding-Modell. Kein generativer Schritt.
|
||||
2. **Reproduzierbar.** Gleiche Eingabe + gleiche `coder_version` ⇒ bitgleiche
|
||||
Ausgabe. Kein Sampling, keine Zeitabhängigkeit, keine Netzabfrage zur Laufzeit.
|
||||
3. **Additiv.** `raw_items` wird nicht verändert. Ergebnisse gehen ausschließlich
|
||||
nach `codings`.
|
||||
4. **Verwerfbar.** Der gesamte Phase-2-Bestand muss löschbar und aus
|
||||
`raw_items` neu baubar sein.
|
||||
5. **Live.** Läuft auf Bebop, im Anschluss an jeden Poll-Lauf, innerhalb des
|
||||
15-Minuten-Slots.
|
||||
|
||||
---
|
||||
|
||||
## 1. Datenmodell
|
||||
|
||||
Phase 2 schreibt in die vorhandene `codings`-Tabelle. Keine neuen Tabellen außer
|
||||
den Lexika.
|
||||
|
||||
### 1.1 Ebenen
|
||||
|
||||
Je `raw_item` entsteht **eine** Coding-Zeile pro Ebene:
|
||||
|
||||
| `ebene` | Inhalt der `nutzlast` |
|
||||
|--------------|----------------------------------------------|
|
||||
| `dublette` | Zuordnung zu einer Übernahme-Gruppe |
|
||||
| `akteure` | Liste aufgelöster Akteure mit Offsets |
|
||||
| `geo` | Liste aufgelöster Orte mit Offsets |
|
||||
| `tonalitaet` | Tonwerte auf Titel und Teaser |
|
||||
| `embedding` | Vektor-Referenz und Modellkennung |
|
||||
|
||||
### 1.2 Erforderliche Schemaänderungen
|
||||
|
||||
```sql
|
||||
-- Re-Runs müssen ersetzen, nicht duplizieren
|
||||
ALTER TABLE codings
|
||||
ADD CONSTRAINT codings_uniq UNIQUE (raw_item_id, coder_version, ebene);
|
||||
|
||||
CREATE INDEX codings_ebene_idx ON codings (ebene, kodiert_am);
|
||||
CREATE INDEX codings_nutzlast_idx ON codings USING gin (nutzlast);
|
||||
```
|
||||
|
||||
### 1.3 `coder_version`
|
||||
|
||||
Format: `p2-<ISO-Datum>-<hash8>`
|
||||
|
||||
Der Hash bildet: Regelwerk-Version, Gazetteer-Version, Akteurslexikon-Version,
|
||||
Sentiment-Lexikon-Version, Embedding-Modell + Quantisierung, Normalisierungsregeln.
|
||||
Ändert sich eines davon, ändert sich die Version. Alte Codings bleiben liegen.
|
||||
|
||||
Eine Tabelle `coder_versionen (version, komponenten jsonb, erstellt_am)` hält
|
||||
auf, was hinter einem Hash steht. Ohne sie ist die Versionierung wertlos.
|
||||
|
||||
---
|
||||
|
||||
## 2. Eingabegrenzen
|
||||
|
||||
Phase 2 arbeitet auf `titel`, `teaser`, `kategorien` und — nur für Tonalität,
|
||||
Akteure, Geo und Embedding — auf `volltext`, sofern vorhanden.
|
||||
|
||||
**Kein Phase-2-Ergebnis darf Volltext oder Teaser im Klartext enthalten.**
|
||||
Offsets und Codes ja, Textausschnitte nein. Begründung: `codings` ist
|
||||
Kandidat für den OpenData-Dump; Snippets darin würden die Veröffentlichungsgrenze
|
||||
unterlaufen.
|
||||
|
||||
Ausnahme: die extrahierte Oberflächenform eines Akteurs oder Orts
|
||||
(z. B. `"Robert Habeck"`) ist ein Eigenname, kein Werkausschnitt, und darf
|
||||
gespeichert werden.
|
||||
|
||||
---
|
||||
|
||||
## 3. Modul: Exakte Dubletten (`ebene = 'dublette'`)
|
||||
|
||||
### Zweck
|
||||
|
||||
Agenturmeldungen (dpa, AFP, Reuters) erscheinen bei mehreren Quellen nahezu
|
||||
identisch. Diese Fälle sollen *vor* Phase 3 zusammengefasst werden, damit das
|
||||
LLM sie nicht mehrfach kodiert.
|
||||
|
||||
Dies ist **kein** thematisches Clustering. Nur near-exact.
|
||||
|
||||
### Verfahren
|
||||
|
||||
1. Normalisierung des Vergleichstexts (Titel + Teaser, oder Volltext falls
|
||||
vorhanden): Unicode NFKC, Kleinschreibung, Entities aufgelöst, Whitespace
|
||||
kollabiert, Anführungszeichen vereinheitlicht, redaktionelle Präfixe
|
||||
(`"Eilmeldung:"`, `"+++ ... +++"`, Ortsmarken wie `"Berlin (dpa) -"`) entfernt.
|
||||
2. SimHash (64 bit) über Wort-Trigramme des normalisierten Texts.
|
||||
3. Kandidaten: gleiches Datum ±1 Tag, Hamming-Distanz ≤ 3.
|
||||
4. Bestätigung: Jaccard über Trigramm-Mengen ≥ 0.85.
|
||||
5. Union-Find über die bestätigten Paare ⇒ `uebernahme_gruppe` (UUID,
|
||||
deterministisch aus der sortierten Menge der `raw_item_id` abgeleitet).
|
||||
|
||||
### Nutzlast
|
||||
|
||||
```json
|
||||
{
|
||||
"gruppe": "…",
|
||||
"gruppengroesse": 4,
|
||||
"leitartikel": 8814,
|
||||
"simhash": "a3f1…",
|
||||
"jaccard_min": 0.91,
|
||||
"agentur_vermutet": "dpa"
|
||||
}
|
||||
```
|
||||
|
||||
`leitartikel` ist die `raw_item_id` mit dem frühesten `zuerst_gesehen`; bei
|
||||
Gleichstand die kleinste ID. Rein deterministisch. Phase 3 kodiert nur den
|
||||
Leitartikel; die übrigen Mitglieder erben die Kodierung, erscheinen aber
|
||||
weiterhin einzeln als Erwähnung mit eigener Quelle.
|
||||
|
||||
`agentur_vermutet` aus Mustererkennung im Text (`"(dpa)"`, `"Reuters"` etc.),
|
||||
nullable. Keine Vermutung ohne Textbeleg.
|
||||
|
||||
### Erwartung
|
||||
|
||||
Trefferquote gegen eine handgeprüfte Stichprobe von 200 Paaren: Präzision ≥ 0.98
|
||||
ist Bedingung. Recall darf niedrig sein — verpasste Dubletten kosten nur
|
||||
Rechenzeit in Phase 3, falsche verschmelzen unterschiedliche Vorgänge.
|
||||
|
||||
---
|
||||
|
||||
## 4. Modul: Akteursauflösung (`ebene = 'akteure'`)
|
||||
|
||||
### Zweck
|
||||
|
||||
Erkennung und Auflösung von Personen, Organisationen, Parteien, Staaten und
|
||||
Ämtern auf stabile interne IDs.
|
||||
|
||||
### Lexikon
|
||||
|
||||
Neue Tabelle:
|
||||
|
||||
```sql
|
||||
CREATE TABLE akteure (
|
||||
id text PRIMARY KEY, -- z.B. 'per:habeck_robert'
|
||||
typ text NOT NULL, -- person|organisation|partei|staat|amt
|
||||
name text NOT NULL, -- kanonische Form
|
||||
aliase text[] NOT NULL DEFAULT '{}',
|
||||
land text, -- ISO-3166-1 alpha-3
|
||||
rolle text, -- z.B. 'GOV','LEG','OPP','JUD','MIL','MED','BUS','NGO'
|
||||
gueltig_von date,
|
||||
gueltig_bis date,
|
||||
wikidata_qid text,
|
||||
quelle text NOT NULL, -- woher der Eintrag stammt
|
||||
notiz text
|
||||
);
|
||||
CREATE INDEX akteure_aliase_idx ON akteure USING gin (aliase);
|
||||
```
|
||||
|
||||
**Zeitliche Gültigkeit ist nicht optional.** „Der Bundeskanzler" bezeichnet
|
||||
je nach `pubdate` verschiedene Personen. Auflösung von Ämtern erfolgt
|
||||
immer gegen das Datum des Items.
|
||||
|
||||
Erstbefüllung: Wikidata-Extrakt (Bundestagsabgeordnete, Kabinettsmitglieder,
|
||||
Ministerpräsidenten, DAX-Vorstände, Staaten, internationale Organisationen)
|
||||
plus manuelle Ergänzung. Als versionierter Dump im Repo, nicht als Live-Abfrage.
|
||||
|
||||
### Verfahren
|
||||
|
||||
1. NER über den Text (spaCy `de_core_news_lg` oder Flair). Modellkennung geht
|
||||
in die `coder_version`.
|
||||
2. Kandidatensuche im Lexikon über Alias-Match (normalisiert) und Fuzzy-Match
|
||||
(Levenshtein-Grenze abhängig von der Länge).
|
||||
3. Disambiguierung über Kontext: gleichzeitig genannte Akteure, Ressort der
|
||||
Quelle, Land. Regelbasiert und dokumentiert, keine Heuristik ohne Testfall.
|
||||
4. Ämter (`"der Kanzler"`, `"die Innenministerin"`) über Amtstabelle gegen
|
||||
`pubdate` auflösen.
|
||||
5. Nicht auflösbare Erwähnungen bleiben als unaufgelöste Oberflächenform
|
||||
erhalten — sie sind das Rohmaterial für die Lexikonpflege.
|
||||
|
||||
### Nutzlast
|
||||
|
||||
```json
|
||||
{
|
||||
"erkannt": [
|
||||
{"id": "per:habeck_robert", "form": "Robert Habeck",
|
||||
"typ": "person", "rolle": "GOV", "feld": "titel",
|
||||
"start": 0, "ende": 13, "konfidenz": 0.97, "methode": "alias"},
|
||||
{"id": null, "form": "Verband der Regionalversorger",
|
||||
"typ": "organisation", "feld": "teaser",
|
||||
"start": 44, "ende": 73, "konfidenz": 0.4, "methode": "ner_unaufgeloest"}
|
||||
],
|
||||
"ner_modell": "de_core_news_lg-3.7.0",
|
||||
"lexikon_version": "2026-09-01"
|
||||
}
|
||||
```
|
||||
|
||||
### Betrieb
|
||||
|
||||
Eine Sicht `akteure_unaufgeloest` aggregiert unaufgelöste Formen nach Häufigkeit.
|
||||
Wöchentliche Durchsicht; häufige Formen wandern ins Lexikon. Das ist der einzige
|
||||
laufende Handarbeitsposten der Phase.
|
||||
|
||||
---
|
||||
|
||||
## 5. Modul: Geokodierung (`ebene = 'geo'`)
|
||||
|
||||
### Zweck
|
||||
|
||||
Auflösung von Ortsnennungen auf Koordinaten und Verwaltungsebenen.
|
||||
|
||||
### Gazetteer
|
||||
|
||||
GeoNames-Dump für DE, AT, CH plus `countryInfo` und `admin1CodesASCII`, lokal
|
||||
und versioniert. Zusätzlich eine Ausschlussliste für notorische Fehlauslöser:
|
||||
Straßennamen mit Ortsbezug, Zeitungsnamen (`"Frankfurter Allgemeine"`),
|
||||
Produktnamen, Personennamen die Ortsnamen sind.
|
||||
|
||||
```sql
|
||||
CREATE TABLE geo_orte (
|
||||
geonames_id bigint PRIMARY KEY,
|
||||
name text NOT NULL,
|
||||
aliase text[] NOT NULL DEFAULT '{}',
|
||||
land text NOT NULL,
|
||||
adm1 text,
|
||||
adm2 text,
|
||||
lat double precision NOT NULL,
|
||||
lon double precision NOT NULL,
|
||||
einwohner bigint,
|
||||
feature_class char(1),
|
||||
feature_code text
|
||||
);
|
||||
```
|
||||
|
||||
### Verfahren
|
||||
|
||||
1. Ortskandidaten aus dem NER-Durchlauf (Typ `LOC`/`GPE`) übernehmen — kein
|
||||
zweiter NER-Lauf.
|
||||
2. Ausschlussliste anwenden.
|
||||
3. Gazetteer-Match. Bei Mehrdeutigkeit Rangfolge: (a) im Text genanntes
|
||||
übergeordnetes Gebiet, (b) Ressort/Regionalbezug der Quelle, (c) Einwohnerzahl.
|
||||
Die Regel ist fest und dokumentiert, nicht gelernt.
|
||||
4. Ableitung eines `hauptort` je Item: der Ort mit der höchsten Gewichtung aus
|
||||
Position (Titel > Teaser > Volltext) und Nennungshäufigkeit. Bei Gleichstand
|
||||
der spezifischste (höchste Verwaltungsebene).
|
||||
|
||||
### Nutzlast
|
||||
|
||||
```json
|
||||
{
|
||||
"orte": [
|
||||
{"geonames_id": 2879139, "form": "Leipzig", "land": "DEU",
|
||||
"adm1": "13", "lat": 51.33962, "lon": 12.37129,
|
||||
"feld": "titel", "start": 12, "ende": 19, "konfidenz": 0.93}
|
||||
],
|
||||
"hauptort": 2879139,
|
||||
"gazetteer_version": "2026-08-15"
|
||||
}
|
||||
```
|
||||
|
||||
Items ohne Ortsbezug bekommen `"hauptort": null` und eine leere Liste — die
|
||||
Coding-Zeile entsteht trotzdem, damit „nicht geokodiert" von „kein Ort gefunden"
|
||||
unterscheidbar bleibt.
|
||||
|
||||
---
|
||||
|
||||
## 6. Modul: Tonalität (`ebene = 'tonalitaet'`)
|
||||
|
||||
### Zweck
|
||||
|
||||
Ein Tonwert je Item als Aggregatsignal.
|
||||
|
||||
### Warnung zur Interpretation
|
||||
|
||||
Der Wert ist **nicht** GDELTs `AvgTone` und darf nicht als solcher ausgegeben
|
||||
werden. Er misst lexikalische Polarität eines deutschen Sentimentlexikons auf
|
||||
einem kurzen Text. Bei Nachrichtensprache ist die Aussagekraft je Einzelitem
|
||||
gering; erst im Aggregat über Quelle und Zeit wird sie brauchbar. Das gehört
|
||||
in die Felddokumentation und auf die Website, nicht in eine Fußnote.
|
||||
|
||||
### Verfahren
|
||||
|
||||
SentiWS + GerVADER, beide lokal, beide versioniert. Negation und Intensivierer
|
||||
über Abhängigkeitsparse. Getrennte Werte für Titel und Teaser, weil
|
||||
Titelzuspitzung ein eigenständig interessantes Signal ist.
|
||||
|
||||
### Nutzlast
|
||||
|
||||
```json
|
||||
{
|
||||
"titel": {"ton": -2.41, "pos": 0.8, "neg": 3.2, "polar_anteil": 0.18},
|
||||
"teaser": {"ton": -0.87, "pos": 1.9, "neg": 2.8, "polar_anteil": 0.11},
|
||||
"skala": "sentiws-2.0/gervader-1.1",
|
||||
"wortzahl": {"titel": 9, "teaser": 41}
|
||||
}
|
||||
```
|
||||
|
||||
Wertebereich −10 bis +10, dokumentiert. `polar_anteil` (Anteil polarer Wörter)
|
||||
dient als Zuverlässigkeitsindikator — niedrige Werte bedeuten, dass der Ton
|
||||
auf wenigen Treffern beruht.
|
||||
|
||||
---
|
||||
|
||||
## 7. Modul: Embeddings (`ebene = 'embedding'`)
|
||||
|
||||
### Zweck
|
||||
|
||||
Vorfilter für die Kandidatengenerierung in Phase 3b. Kein Selbstzweck, kein
|
||||
Clustering in Phase 2.
|
||||
|
||||
### Verfahren
|
||||
|
||||
Modell: ein mehrsprachiges Satz-Embedding mit guter Deutschabdeckung
|
||||
(`intfloat/multilingual-e5-large` oder `jina-embeddings-v3`), lokal auf Bebop,
|
||||
CPU-Inferenz, feste Version. Eingabe: Titel + Teaser, auf feste Tokenzahl
|
||||
gekürzt. Deterministisch — kein Dropout, feste Präzision.
|
||||
|
||||
Speicherung: `pgvector`.
|
||||
|
||||
```sql
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
|
||||
CREATE TABLE item_embeddings (
|
||||
raw_item_id bigint NOT NULL,
|
||||
coder_version text NOT NULL,
|
||||
vektor vector(1024) NOT NULL,
|
||||
PRIMARY KEY (raw_item_id, coder_version)
|
||||
);
|
||||
|
||||
CREATE INDEX item_embeddings_hnsw
|
||||
ON item_embeddings USING hnsw (vektor vector_cosine_ops);
|
||||
```
|
||||
|
||||
Der Vektor liegt bewusst **nicht** in `codings.nutzlast` — JSONB-Vektoren sind
|
||||
weder indizierbar noch platzsparend. Die Coding-Zeile hält nur die Kennung:
|
||||
|
||||
```json
|
||||
{"modell": "multilingual-e5-large", "dim": 1024, "gespeichert": true}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Ausführung
|
||||
|
||||
### Auslöser
|
||||
|
||||
Nach jedem Poll-Lauf für die in diesem Slot neuen oder geänderten Items.
|
||||
Zusätzlich ein Backfill-Modus für den Gesamtbestand bei
|
||||
`coder_version`-Wechsel.
|
||||
|
||||
### Reihenfolge
|
||||
|
||||
```
|
||||
dublette ──► akteure ──► geo ──► tonalitaet ──► embedding
|
||||
│ ▲
|
||||
└────────┘ (geo nutzt NER-Ausgabe von akteure)
|
||||
```
|
||||
|
||||
Module sind bis auf diese eine Abhängigkeit unabhängig und einzeln neu baubar.
|
||||
|
||||
### Idempotenz
|
||||
|
||||
`INSERT … ON CONFLICT (raw_item_id, coder_version, ebene) DO UPDATE`.
|
||||
Ein Modul zweimal laufen zu lassen ändert nichts.
|
||||
|
||||
### Fehlerverhalten
|
||||
|
||||
Ein fehlgeschlagenes Modul blockiert die anderen nicht. Fehler landen in einer
|
||||
`p2_laeufe`-Tabelle analog zu `poll_laeufe`: Slot, Modul, Anzahl verarbeitet,
|
||||
Anzahl fehlgeschlagen, Dauer, Fehlermeldung. Keine stillen Ausfälle.
|
||||
|
||||
### Budget
|
||||
|
||||
Ziel: vollständige Verarbeitung eines 15-Minuten-Slots (~200–400 Items bei
|
||||
40 Feeds) in unter 3 Minuten Wall-Clock auf Bebop. Wird das gerissen, ist das
|
||||
Embedding-Modul der erste Kandidat zur Auslagerung in einen Nachlauf.
|
||||
|
||||
---
|
||||
|
||||
## 9. Öffentliche Darstellung
|
||||
|
||||
Die Phase-2-Seite zeigt, was die Anreicherung tut, ohne Volltext zu berühren:
|
||||
|
||||
- Karte der Hauptorte des Tages (aus `geo`)
|
||||
- Häufigste Akteure der Woche, mit Trend
|
||||
- Übernahmegruppen: welche Meldung von wie vielen Häusern gleichlautend
|
||||
ausgespielt wurde, mit Zeitversatz. Das ist der publizistisch interessanteste
|
||||
Teil.
|
||||
- Tonalität je Quelle im Zeitverlauf — mit sichtbarem Hinweis auf die
|
||||
Interpretationsgrenzen aus Abschnitt 6.
|
||||
|
||||
Kein Modul ist ohne die Grenzen seiner Aussagekraft darzustellen.
|
||||
|
||||
---
|
||||
|
||||
## 10. Abnahmekriterien
|
||||
|
||||
| Kriterium | Schwelle |
|
||||
|---|---|
|
||||
| Dubletten: Präzision auf 200er-Stichprobe | ≥ 0.98 |
|
||||
| Akteure: aufgelöste Nennungen an allen Nennungen | ≥ 0.75 |
|
||||
| Akteure: Präzision auf 200er-Stichprobe | ≥ 0.95 |
|
||||
| Geo: Hauptort korrekt auf 200er-Stichprobe | ≥ 0.85 |
|
||||
| Laufzeit je Slot | < 3 min |
|
||||
| Bitgleicher Neuaufbau bei gleicher `coder_version` | verpflichtend |
|
||||
| Kein Text > 100 Zeichen in `codings.nutzlast` | verpflichtend, automatisch geprüft |
|
||||
|
||||
Die letzte Zeile ist als Constraint oder Test zu erzwingen, nicht als Vorsatz.
|
||||
Reference in New Issue
Block a user