Uma hemeroteca digital para uma coleção particular: edições de jornal em PDF viram um acervo onde qualquer matéria pode ser encontrada por palavra, por significado ou fazendo uma pergunta, sempre com a fonte citada.
O problema
O cliente tinha uma coleção de edições de jornais em arquivos PDF. Encontrar um artigo significava abrir os arquivos um por um, e muitas páginas eram digitalizações sem texto. O acervo também precisava continuar privado: só podiam entrar as pessoas convidadas pelo administrador.
O que eu construí: uma hemeroteca digital
Construí um acervo web privado em que cada edição é armazenada como título do jornal, data e PDF, e cada página se torna pesquisável.
- Extração de texto com OCR como fallback: uma página mantém a própria camada de texto quando tem uma. Páginas com menos de 40 caracteres são renderizadas a 220 DPI e lidas com o Tesseract. Assim, até as digitalizações antigas ficam pesquisáveis.
- Busca full-text em italiano: a busca usa stemming italiano e ignora acentos, então “citta” encontra “città”. Frases exatas aparecem primeiro, com trechos destacados.
- Busca por significado: o texto das páginas é dividido em blocos sobrepostos e convertido em vetores de 768 dimensões com um modelo multilíngue. Os leitores encontram artigos mesmo sem saber as palavras exatas.
- Assistente com citações: o leitor faz uma pergunta em linguagem comum. O sistema recupera até 8 trechos, por significado e por palavra-chave, e o Claude responde apenas com base nesses trechos, citando título, data e página. Ele é ativado no painel de administração com a chave Anthropic do próprio cliente.
- Upload em lote a partir de uma pasta: o administrador seleciona uma pasta de PDFs e a data é lida do nome de cada arquivo. Duplicatas são ignoradas por título e data ou pela impressão digital SHA-256, então um upload pode ser repetido com segurança.
- Acesso só por convite: dois perfis, administrador e leitor. O painel gerencia usuários, títulos, a chave de IA e os backups.
Os números
- Limite de memória de 8 GB no serviço de embeddings, com reinício diário. Antes do limite, ele havia crescido até cerca de 15 GB e travado a fila por uma semana.
- Backup noturno às 03:30: um dump compactado do banco de dados, mantendo os 14 mais recentes, além de um espelho incremental dos PDFs.
- Jobs travados recuperados automaticamente: um job que fica rodando por mais de 20 minutos volta para a fila, até 6 tentativas.
Com o que foi feito
Python 3.11 e FastAPI para a aplicação web. PostgreSQL 16 com pgvector, unaccent e pg_trgm para dados, busca textual e vetores. PyMuPDF e Tesseract para texto e OCR. Sentence-transformers com o modelo multilingual-e5-base para os embeddings, rodando como um serviço local separado. A API da Anthropic para o assistente. Tudo roda como serviços systemd atrás de um proxy reverso Apache.
Por que foi feito assim
Mantive a fila de jobs dentro do PostgreSQL em vez de adicionar o Redis. Os workers pegam os jobs com FOR UPDATE SKIP LOCKED, então vários workers rodam em paralelo e nunca processam a mesma edição duas vezes. O servidor também hospeda outros sites, então ter um serviço a menos para rodar e proteger foi um ganho real.


