Ein digitales Zeitungsarchiv für eine private Sammlung: Zeitungsausgaben als PDF werden zu einem Archiv, in dem sich jeder Artikel finden lässt, per Stichwort, nach Bedeutung oder mit einer Frage in natürlicher Sprache, und die Quelle wird immer genannt.
Das Problem
Der Kunde besaß eine Sammlung von Zeitungsausgaben als PDF-Dateien. Um einen Artikel zu finden, musste man die Dateien einzeln öffnen, und viele Seiten waren Scans ohne Text. Zudem musste das Archiv privat bleiben: Nur vom Administrator eingeladene Personen sollten Zugang erhalten.
Was ich gebaut habe: ein digitales Zeitungsarchiv
Ich habe ein privates Webarchiv entwickelt, in dem jede Ausgabe mit Zeitungstitel, Datum und PDF gespeichert und jede Seite durchsuchbar wird.
- Textextraktion mit OCR-Fallback: Eine Seite behält ihre eigene Textebene, wenn sie eine hat. Seiten mit weniger als 40 Zeichen werden mit 220 DPI gerendert und mit Tesseract gelesen. So werden auch alte Scans durchsuchbar.
- Italienische Volltextsuche: Die Suche nutzt italienisches Stemming und ignoriert Akzente, sodass „citta“ auch „città“ findet. Exakte Wortfolgen stehen oben, mit hervorgehobenen Textausschnitten.
- Suche nach Bedeutung: Der Seitentext wird in überlappende Blöcke zerlegt und mit einem mehrsprachigen Modell in Vektoren mit 768 Dimensionen umgewandelt. So finden Leser Artikel auch dann, wenn sie den genauen Wortlaut nicht kennen.
- Assistent mit Quellenangaben: Ein Leser stellt eine Frage in natürlicher Sprache. Das System holt bis zu 8 Passagen, nach Bedeutung und nach Stichwort, und Claude antwortet ausschließlich auf Basis dieser Passagen, mit Angabe von Titel, Datum und Seite. Aktiviert wird er im Admin-Panel mit dem eigenen Anthropic-Schlüssel des Kunden.
- Massen-Upload aus einem Ordner: Der Administrator wählt einen Ordner mit PDFs, und das Datum wird aus jedem Dateinamen gelesen. Duplikate werden anhand von Titel und Datum oder per SHA-256-Fingerabdruck übersprungen, sodass sich ein Upload gefahrlos wiederholen lässt.
- Zugang nur auf Einladung: zwei Rollen, Administrator und Leser. Im Panel werden Nutzer, Titel, der KI-Schlüssel und Backups verwaltet.
Die Zahlen
- Speicherobergrenze von 8 GB für den Embedding-Dienst, mit täglichem Neustart. Vor der Begrenzung war er auf etwa 15 GB angewachsen und hatte die Warteschlange eine Woche lang blockiert.
- Nächtliches Backup um 03:30 Uhr: ein komprimierter Datenbank-Dump, von dem die letzten 14 aufbewahrt werden, plus ein inkrementelles Spiegelbild der PDFs.
- Hängende Jobs werden automatisch wiederhergestellt: Ein Job, der länger als 20 Minuten läuft, kommt zurück in die Warteschlange, bis zu 6 Versuche.
Womit es gebaut ist
Python 3.11 und FastAPI für die Web-App. PostgreSQL 16 mit pgvector, unaccent und pg_trgm für Daten, Textsuche und Vektoren. PyMuPDF und Tesseract für Text und OCR. Sentence-transformers mit dem Modell multilingual-e5-base für die Embeddings, betrieben als separater lokaler Dienst. Die Anthropic API für den Assistenten. Alles läuft als systemd-Dienste hinter einem Apache-Reverse-Proxy.
Warum so gebaut
Die Job-Warteschlange habe ich in PostgreSQL belassen, statt Redis hinzuzufügen. Worker übernehmen Jobs mit FOR UPDATE SKIP LOCKED, sodass mehrere Worker parallel laufen und nie dieselbe Ausgabe doppelt verarbeiten. Auf dem Server laufen auch andere Websites, daher war ein Dienst weniger, der betrieben und abgesichert werden muss, ein echter Gewinn.


