Le problème
Le client possédait une collection de numéros de journaux en PDF. Pour trouver un article, il fallait ouvrir les fichiers un par un, et beaucoup de pages étaient des numérisations sans texte. L’archive devait aussi rester privée : seules les personnes invitées par l’administrateur pouvaient y accéder.
Ce que j’ai construit : une archive de presse numérique
J’ai développé une archive web privée où chaque numéro est enregistré avec son titre de presse, sa date et son PDF, et où chaque page devient consultable par recherche.
- Extraction du texte avec OCR en secours : une page garde sa propre couche de texte quand elle en a une. Les pages de moins de 40 caractères sont rendues à 220 DPI et lues avec Tesseract. Les anciennes numérisations deviennent elles aussi consultables.
- Recherche plein texte en italien : la recherche utilise la racinisation italienne et ignore les accents, si bien que « citta » trouve « città ». Les phrases exactes arrivent en tête, avec des extraits surlignés.
- Recherche par le sens : le texte des pages est découpé en blocs qui se chevauchent, puis converti en vecteurs de 768 dimensions avec un modèle multilingue. Les lecteurs trouvent les articles même sans connaître les mots exacts.
- Assistant avec citations : le lecteur pose une question en langage courant. Le système récupère jusqu’à 8 passages, par le sens et par mot-clé, et Claude répond uniquement à partir de ces passages, en citant le titre, la date et la page. Il s’active depuis le panneau d’administration avec la clé Anthropic du client.
- Import en masse depuis un dossier : l’administrateur sélectionne un dossier de PDF et la date est lue dans le nom de chaque fichier. Les doublons sont écartés par titre et date ou par empreinte SHA-256, si bien qu’un import peut être relancé sans risque.
- Accès sur invitation uniquement : deux rôles, administrateur et lecteur. Le panneau gère les utilisateurs, les titres, la clé IA et les sauvegardes.
Les chiffres
- Plafond mémoire de 8 Go sur le service d’embeddings, avec un redémarrage quotidien. Avant ce plafond, il était monté à environ 15 Go et avait bloqué la file pendant une semaine.
- Sauvegarde nocturne à 03:30 : un dump compressé de la base, dont on conserve les 14 derniers, plus une copie miroir incrémentale des PDF.
- Tâches bloquées récupérées automatiquement : une tâche en cours depuis plus de 20 minutes retourne dans la file, dans la limite de 6 tentatives.
Comment c’est construit
Python 3.11 et FastAPI pour l’application web. PostgreSQL 16 avec pgvector, unaccent et pg_trgm pour les données, la recherche textuelle et les vecteurs. PyMuPDF et Tesseract pour le texte et l’OCR. Sentence-transformers avec le modèle multilingual-e5-base pour les embeddings, exécuté comme service local séparé. L’API Anthropic pour l’assistant. Le tout tourne en services systemd derrière un reverse proxy Apache.
Pourquoi ce choix
J’ai gardé la file de tâches dans PostgreSQL au lieu d’ajouter Redis. Les workers réservent les tâches avec FOR UPDATE SKIP LOCKED, si bien que plusieurs workers tournent en parallèle sans jamais traiter deux fois le même numéro. Le serveur héberge aussi d’autres sites : un service de moins à faire tourner et à sécuriser représentait un vrai gain.


