Der Abruf warf bisher fast alles weg, was im XML stand. Neu ausgewertet werden Kategorien (<category>, dc:subject, Atom-term), content:encoded zusaetzlich zur kurzen description, Medien-URLs aus enclosure und Media RSS, Verweise aus dem Teaser-HTML und die Position im Feed beim ersten Sehen. Die Kategorien sind fuer Stufe 2 der wertvollste Posten: vom Verlag selbst vergebene Themenlabel, brauchbar als Vorfilter und als Prompt-Kontext. Gemessen an zwoelf Quellen tragen 189 von 814 Items Kategorien, 315 einen laengeren Volltext, 430 Medien. Ausserdem landen die rohen Antwortbytes gezippt in feed_abrufe, dedupliziert ueber ihren sha256. Der Parser wird sich weiter aendern; nur mit den Rohbytes laesst sich eine Verbesserung rueckwirkend auf die Historie anwenden - dieselbe Ueberlegung wie bei der Trennung von raw_items und codings. Kompression rund 22 %, etwa 20 kB je Abruf. Items ohne Titel werden weiter uebersprungen, jetzt aber gezaehlt. Bestandszeilen bekommen die neuen Felder beim naechsten Lauf nachgetragen, ohne als Revision zu zaehlen; der Inhalts-Hash bleibt dafuer auf Titel/Teaser/URL beschraenkt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZAdZb3EifGb9nvQ5Zp2TE
212 lines
8.4 KiB
Markdown
212 lines
8.4 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, aufsteigend:
|
|
# psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f migrations/001-revisionen.sql
|
|
# psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f migrations/002-rohablage-und-felder.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, und die rohen Feeds unter `<dir>/YYYYMMDD/feeds/`. 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;
|
|
```
|
|
|
|
### Was die Verlage selbst labeln
|
|
|
|
```sql
|
|
-- Themenverteilung je Quelle, ohne einen Kodierer bemueht zu haben
|
|
select quelle, k as kategorie, count(*)
|
|
from raw_items, unnest(kategorien) k
|
|
group by quelle, k order by count(*) desc limit 20;
|
|
```
|
|
|
|
### Speicherbedarf im Blick behalten
|
|
|
|
```sql
|
|
select pg_size_pretty(pg_total_relation_size('feed_abrufe')) as rohablage,
|
|
pg_size_pretty(pg_total_relation_size('raw_items')) as items;
|
|
|
|
-- Rohablage aelter als ein Jahr wegwerfen, falls es eng wird. raw_items
|
|
-- bleibt davon unberuehrt; nur die Moeglichkeit, neu zu parsen, entfaellt.
|
|
-- delete from feed_abrufe where abgerufen_am < now() - interval '1 year';
|
|
```
|
|
|
|
### 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`.
|
|
- **Rohe Feed-Antworten** landen gezippt in `feed_abrufe`, dedupliziert ueber
|
|
den sha256 der Rohbytes. Der Parser wird sich aendern; nur mit den Rohbytes
|
|
laesst sich eine spaetere Verbesserung rueckwirkend auf die Historie
|
|
anwenden. Kompression rund 22 %, etwa 20 kB je abgelegtem Abruf.
|
|
- **Der Parser nimmt mit, was im XML steht**: Kategorien (`<category>`,
|
|
`dc:subject`, Atom-`term`), `content:encoded` zusaetzlich zur kurzen
|
|
`description`, Medien-URLs aus `enclosure` und Media RSS, Verweise aus dem
|
|
Teaser-HTML, und die Position im Feed beim ersten Sehen. Die Kategorien sind
|
|
fuer Stufe 2 der wertvollste Posten: vom Verlag selbst vergebene Themenlabel,
|
|
brauchbar als Vorfilter und als Prompt-Kontext.
|
|
- **Items ohne Titel** werden weiterhin uebersprungen, aber gezaehlt
|
|
(`poll_laeufe.items_uebersprungen`) statt stillschweigend verworfen.
|
|
- **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.
|
|
|
|
## Migration bestehender Installationen
|
|
|
|
`001` und `002` sind idempotent und koennen im laufenden Betrieb eingespielt
|
|
werden; danach den Poller neu starten.
|
|
|
|
Nach `002` sind die neuen Spalten auf Bestandszeilen zunaechst leer. Der
|
|
naechste Lauf traegt sie fuer alle Items nach, die die Quelle noch ausliefert
|
|
(in der Ausgabe als `nachgetr`), ohne sie als Revision zu zaehlen. Items, die
|
|
inzwischen aus dem Feed gefallen sind, behalten leere Zusatzfelder - die Daten
|
|
sind nicht mehr abrufbar.
|
|
|
|
Der Inhalts-Hash bleibt bewusst auf Titel, Teaser und URL beschraenkt. Naehme
|
|
er die neuen Felder auf, gaelte beim ersten Lauf nach dem Update jedes
|
|
Bestandsitem als revidiert.
|
|
|
|
## 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.
|