Un’emeroteca digitale privata, cercabile come un motore di ricerca: raccolte di giornali in PDF diventano un archivio in cui si trova qualunque articolo per parola, per significato o facendo una domanda, con le fonti sempre citate.
Il problema
Il cliente aveva una raccolta di numeri di giornale in PDF. Per trovare un articolo bisognava aprire i file uno per uno, e molte pagine erano scansioni senza testo. L’archivio doveva anche restare privato: potevano entrare solo le persone invitate dall’amministratore.
Cosa ho costruito: un’emeroteca digitale privata
Ho realizzato un archivio web privato in cui ogni numero è registrato con testata, data e PDF, e ogni pagina diventa ricercabile.
- Estrazione del testo con OCR di riserva: una pagina conserva il proprio livello di testo quando ce l’ha. Le pagine con meno di 40 caratteri vengono renderizzate a 220 DPI e lette con Tesseract. Così diventano ricercabili anche le vecchie scansioni.
- Ricerca full-text in italiano: la ricerca usa lo stemming italiano e ignora gli accenti, quindi «citta» trova «città». Le frasi esatte compaiono per prime, con estratti evidenziati.
- Ricerca per significato: il testo delle pagine viene diviso in blocchi sovrapposti e trasformato in vettori a 768 dimensioni con un modello multilingue. I lettori trovano gli articoli anche quando non conoscono le parole esatte.
- Assistente con citazioni: il lettore fa una domanda in linguaggio naturale. Il sistema recupera fino a 8 passaggi, per significato e per parola chiave, e Claude risponde solo sulla base di quei passaggi, citando testata, data e pagina. Si attiva dal pannello di amministrazione con la chiave Anthropic del cliente.
- Caricamento in blocco da una cartella: l’amministratore seleziona una cartella di PDF e la data viene letta dal nome di ogni file. I duplicati vengono scartati per testata e data o per impronta SHA-256, quindi un caricamento si può ripetere senza rischi.
- Accesso solo su invito: due ruoli, amministratore e lettore. Dal pannello si gestiscono utenti, testate, chiave AI e backup.
I numeri
- Limite di memoria di 8 GB sul servizio di embedding, con un riavvio giornaliero. Prima del limite era arrivato a circa 15 GB e aveva bloccato la coda per una settimana.
- Backup notturno alle 03:30: un dump compresso del database, di cui si conservano gli ultimi 14, più una copia incrementale dei PDF.
- Processi bloccati recuperati in automatico: un processo rimasto in esecuzione per più di 20 minuti torna in coda, fino a 6 tentativi.
Con cosa è fatto
Python 3.11 e FastAPI per l’applicazione web. PostgreSQL 16 con pgvector, unaccent e pg_trgm per dati, ricerca testuale e vettori. PyMuPDF e Tesseract per testo e OCR. Sentence-transformers con il modello multilingual-e5-base per gli embedding, eseguito come servizio locale separato. L’API di Anthropic per l’assistente. Tutto gira come servizi systemd dietro un reverse proxy Apache.
Perché è fatto così
Ho tenuto la coda dei processi dentro PostgreSQL invece di aggiungere Redis. I worker prendono in carico i processi con FOR UPDATE SKIP LOCKED, così più worker lavorano in parallelo senza mai elaborare due volte lo stesso numero. Il server ospita anche altri siti, quindi avere un servizio in meno da gestire e proteggere è stato un vantaggio concreto.


