Der 2-Stunden-Takt verlor ganze Artikel. Ein RSS-Feed ist ein Fenster, kein Archiv: ist das Fenster kuerzer als der Poll-Abstand, fallen Eintraege zwischen zwei Abrufen heraus und sind nicht nachholbar. Gemessen am 2026-09-04 haelt zeit.de 0,6 h vor, spiegel und welt rund 5 h; von der Zeit sah man etwa jeden dritten Artikel. Der bedingte GET macht haeufiges Pollen billig, ein unveraenderter Abruf ist ein 304 ohne Body. Zweitens schrieb der Upsert Aenderungen nicht fort. Redaktionen aendern Ueberschriften nach der Veroeffentlichung; bei gleicher GUID lief das in ein `on conflict do nothing` und blieb unsichtbar. raw_items haelt jetzt den aktuellen Stand und zaehlt Revisionen, raw_item_versionen haelt jede Fassung einschliesslich der ersten. Erkannt wird das ueber einen Inhalts-Hash aus Titel, Teaser und URL. Die Migration zieht Bestandsdaten nach. Die SQL-Hashformel ist zeichengleich zu poller.inhalt_hash(), geprueft an 814 migrierten Items: null Scheinrevisionen beim ersten Lauf danach. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZAdZb3EifGb9nvQ5Zp2TE
164 lines
6.1 KiB
Markdown
164 lines
6.1 KiB
Markdown
# Wurzelwerk — Ingest
|
|
|
|
Stufe 0 und 1 der Pipeline: Quellenregister und Roh-Erfassung. Keine
|
|
Kodierung — was hier landet, soll sich beliebig oft neu kodieren lassen.
|
|
|
|
## Dateien
|
|
|
|
| Datei | Zweck |
|
|
|---|---|
|
|
| `sources.toml` | Quellenregister, einzige Wahrheit ueber die Quellen |
|
|
| `schema.sql` | Postgres-Schema, vollstaendiger Stand fuer Neuinstallationen |
|
|
| `migrations/` | Aenderungen fuer bestehende Installationen, aufsteigend anwenden |
|
|
| `poller.py` | Poller, zwei Senken: Postgres und gzip-JSONL |
|
|
| `Containerfile` | Image fuer den Produktivbetrieb |
|
|
| `wurzelwerk-ingest.container`, `wurzelwerk.network` | Quadlet-Units, rootless |
|
|
| `ingest.env.example` | Vorlage fuer die Zugangsdaten |
|
|
|
|
## Deploy mit Postgres + Podman/Quadlet (rootless)
|
|
|
|
```sh
|
|
# 1. Schema einspielen (Neuinstallation)
|
|
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f schema.sql
|
|
|
|
# Bestehende Installation stattdessen migrieren:
|
|
# psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f migrations/001-revisionen.sql
|
|
|
|
# 2. Image bauen
|
|
podman build -t localhost/wurzelwerk-ingest:latest .
|
|
|
|
# 3. Konfiguration ablegen
|
|
mkdir -p ~/.config/wurzelwerk
|
|
cp ingest.env.example ~/.config/wurzelwerk/ingest.env
|
|
cp sources.toml ~/.config/wurzelwerk/sources.toml
|
|
chmod 600 ~/.config/wurzelwerk/ingest.env
|
|
$EDITOR ~/.config/wurzelwerk/ingest.env
|
|
|
|
# 4. Quadlet installieren
|
|
mkdir -p ~/.config/containers/systemd
|
|
cp wurzelwerk-ingest.container wurzelwerk.network ~/.config/containers/systemd/
|
|
systemctl --user daemon-reload
|
|
systemctl --user start wurzelwerk-ingest
|
|
journalctl --user -u wurzelwerk-ingest -f
|
|
|
|
# Damit der Dienst ohne offene Sitzung weiterlaeuft:
|
|
loginctl enable-linger "$USER"
|
|
```
|
|
|
|
`sources.toml` ist eingehaengt, nicht ins Image gebacken: eine neue Quelle ist
|
|
ein `systemctl --user restart wurzelwerk-ingest`, kein Neubau.
|
|
|
|
## Schatten-Ingest auf einem Zweitgeraet
|
|
|
|
Ohne Postgres, ohne Container:
|
|
|
|
```sh
|
|
python3 poller.py --sink jsonl:/pfad/zu/deinem/ablageordner
|
|
```
|
|
|
|
Legt `<dir>/YYYYMMDD/YYYYMMDDHHMMSS.jsonl.gz` an, dedupliziert ueber eine
|
|
SQLite daneben. Als `systemd --user`-Unit mit `Restart=always` einrichten.
|
|
|
|
Der Sinn ist nicht Redundanz um ihrer selbst willen: RSS-Feeds haben kein
|
|
Archiv. Steht der Produktiv-Poller einen Tag still, ist dieser Tag
|
|
unwiederbringlich weg. Kodierungen lassen sich wiederholen, Rohdaten nie.
|
|
|
|
## Betrieb
|
|
|
|
```sh
|
|
# Lizenzstatus im Register gegen die aktuelle robots.txt pruefen
|
|
python3 poller.py --check-robots # Exit != 0 bei Abweichung
|
|
|
|
# ein einzelner Durchlauf
|
|
python3 poller.py --sink "$DATABASE_URL" --once
|
|
```
|
|
|
|
`--check-robots` gehoert in einen woechentlichen Cron. Ein Verlag, der seine
|
|
robots.txt aendert, aendert damit seinen Nutzungsvorbehalt — und das ist der
|
|
Punkt, an dem eine Quelle stillgelegt werden muss.
|
|
|
|
Aendert sich etwas, wird **`sources.toml` angepasst, nie der Code.** Eine
|
|
gesperrte Quelle bleibt im Register stehen (`aktiv = false`), damit
|
|
nachvollziehbar bleibt, warum sie fehlt. Der Poller verweigert den Start,
|
|
wenn eine als `gesperrt` gefuehrte Quelle aktiv geschaltet ist.
|
|
|
|
### Ueberschriften-Revisionen
|
|
|
|
```sql
|
|
-- welche Meldungen wurden nachtraeglich umbenannt
|
|
select quelle, fassungen, erste_fassung, letzte_fassung, zuletzt
|
|
from titel_revisionen order by zuletzt desc limit 20;
|
|
|
|
-- welche Redaktion revidiert am haeufigsten
|
|
select quelle, count(*) filter (where revisionen > 0) as revidiert,
|
|
count(*) as gesamt,
|
|
round(100.0 * count(*) filter (where revisionen > 0) / count(*), 1) as quote
|
|
from raw_items group by quelle order by quote desc;
|
|
```
|
|
|
|
### Waechter
|
|
|
|
```sql
|
|
-- aktive Quellen, die seit ueber sechs Stunden nichts Neues liefern
|
|
select * from quellen_stillstand;
|
|
|
|
-- Fehler der letzten Stunde
|
|
select quelle, http_status, fehler, begonnen_am from poll_laeufe
|
|
where fehler is not null and begonnen_am > now() - interval '1 hour';
|
|
```
|
|
|
|
### Arbeitsvorrat fuer Yantra
|
|
|
|
```sql
|
|
select * from unkodiert('qwen3-14b-q4/prompt-1') limit 500;
|
|
```
|
|
|
|
Liefert alle Items, die diese Kodierer-Version noch nicht gesehen hat. Eine
|
|
neue Prompt-Version ist damit automatisch ein vollstaendiger Recode-Auftrag
|
|
ueber die gesamte Historie, ohne dass ein Feed erneut abgerufen wird.
|
|
|
|
## Verhalten
|
|
|
|
- **15-Minuten-Takt**, an den Slotgrenzen ausgerichtet (+30 s Versatz).
|
|
Der Takt ist nicht frei waehlbar: ein RSS-Feed ist ein Fenster, kein Archiv.
|
|
Ist das Fenster kuerzer als der Poll-Abstand, fallen Artikel zwischen zwei
|
|
Abrufen heraus und sind unwiederbringlich weg. Gemessen am 2026-09-04:
|
|
zeit.de haelt 0,6 h vor, dlf gar nicht messbar, spiegel und welt rund 5 h --
|
|
bei 2 h Takt sah man von der Zeit etwa jeden dritten Artikel. Der bedingte
|
|
GET macht haeufiges Pollen billig: ein unveraenderter Abruf ist ein 304 ohne
|
|
Body.
|
|
- **Revisionen** werden mitgeschrieben. Redaktionen aendern Ueberschriften nach
|
|
der Veroeffentlichung; frueher lief das in ein `on conflict do nothing` und
|
|
blieb unsichtbar. `raw_items` haelt den aktuellen Stand, `raw_item_versionen`
|
|
jede Fassung. Der Datensatz existiert sonst nirgends -- GDELT sieht jede URL
|
|
genau einmal.
|
|
- **Bedingter GET** ueber ETag/Last-Modified, im Register gespeichert. Im
|
|
Test antworteten 4 von 12 Quellen im zweiten Durchlauf mit 304.
|
|
- **Dedup** ueber `unique (quelle, item_key)`; `item_key` ist guid, ersatzweise
|
|
Link, ersatzweise ein Titel-Hash.
|
|
- **Fehlertoleranz**: eine kaputte Quelle beendet den Durchlauf nicht, der
|
|
Fehler landet in `poll_laeufe`.
|
|
- **Kein Volltext-Abruf.** Nur der Feed selbst wird geholt. Titel und Teaser
|
|
reichen als Kodierungsinput, und damit stellt sich die TDM-Frage nach
|
|
§ 44b UrhG gar nicht erst.
|
|
|
|
## Gemessen (2026-09-04)
|
|
|
|
Erster Lauf ueber 12 aktive Quellen: 820 Items gesehen, 810 neu, gesamter
|
|
Durchlauf unter 3 s. Zweiter Lauf unmittelbar danach: 1 neues Item.
|
|
|
|
Das Trigramm-Clustering findet quellenuebergreifende Dubletten sofort:
|
|
|
|
```
|
|
zeit | handelsblatt | 1.00 | Bekennerschreiben: Erneut Sabotage am Stromnetz
|
|
spiegel | derstandard | 0.92 | Niederlage fuer Donald Trump - Gericht in Missouri
|
|
```
|
|
|
|
## Offen
|
|
|
|
- Telegram-Fetcher (Telethon/MTProto). Register ist vorbereitet, Eintraege
|
|
stehen auf `aktiv = false`. Die Bot-API kann fremde Kanaele nicht lesen.
|
|
- Ersatz-URL fuer den BR24-Feed (aktuell 404).
|
|
- Quellenpool von 12 auf ~40 erweitern; Regionalzeitungen ueber SearXNG
|
|
suchen.
|