Poll-Takt auf 15 Minuten und Revisionshistorie fuer Items

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
This commit is contained in:
2026-09-05 18:17:07 +02:00
co-authored by Claude Opus 5
parent 8a1ab8a146
commit 4983662514
4 changed files with 264 additions and 44 deletions
+33 -4
View File
@@ -8,7 +8,8 @@ Kodierung — was hier landet, soll sich beliebig oft neu kodieren lassen.
| Datei | Zweck |
|---|---|
| `sources.toml` | Quellenregister, einzige Wahrheit ueber die Quellen |
| `schema.sql` | Postgres-Schema (`quellen`, `raw_items`, `codings`, `poll_laeufe`) |
| `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 |
@@ -17,9 +18,12 @@ Kodierung — was hier landet, soll sich beliebig oft neu kodieren lassen.
## Deploy mit Postgres + Podman/Quadlet (rootless)
```sh
# 1. Schema einspielen
# 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 .
@@ -78,6 +82,20 @@ 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
@@ -101,8 +119,19 @@ ueber die gesamte Historie, ohne dass ein Feed erneut abgerufen wird.
## Verhalten
- **2-Stunden-Takt**, an den Slotgrenzen ausgerichtet (+30 s Versatz), um
unnoetigen Traffic bei den Quellen zu vermeiden.
- **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