Цифровой архив газет для частной коллекции: выпуски газет в PDF превращаются в архив, где любую статью можно найти по слову, по смыслу или задав вопрос, и источник всегда указан.
Задача
У клиента была коллекция номеров газет в виде PDF-файлов. Чтобы найти статью, приходилось открывать файлы один за другим, а многие страницы были сканами без текста. Кроме того, архив должен был оставаться закрытым: войти могли только те, кого пригласил администратор.
Что я сделал: цифровой архив газет
Я создал закрытый веб-архив, где каждый номер хранится как название газеты, дата и PDF, а по каждой странице можно искать.
- Извлечение текста с OCR в качестве запасного варианта: если у страницы есть собственный текстовый слой, он сохраняется. Страницы, где меньше 40 символов, рендерятся в 220 DPI и распознаются Tesseract. Так поиск работает и по старым сканам.
- Полнотекстовый поиск на итальянском: поиск учитывает итальянскую морфологию и игнорирует диакритику, поэтому «citta» находит «città». Точные фразы выводятся первыми, с подсвеченными фрагментами.
- Поиск по смыслу: текст страниц делится на перекрывающиеся блоки и превращается в векторы размерностью 768 с помощью многоязычной модели. Читатели находят статьи, даже если не знают точных слов.
- Ассистент со ссылками на источники: читатель задаёт вопрос обычным языком. Система находит до 8 фрагментов — по смыслу и по ключевым словам, — и Claude отвечает только на их основе, указывая название, дату и страницу. Ассистент включается в админ-панели с собственным ключом Anthropic клиента.
- Массовая загрузка из папки: администратор выбирает папку с PDF, и дата считывается из имени каждого файла. Дубликаты отсеиваются по названию и дате или по отпечатку SHA-256, так что загрузку можно безопасно повторять.
- Доступ только по приглашению: две роли — администратор и читатель. В панели управляются пользователи, названия изданий, ключ ИИ и резервные копии.
Цифры
- Ограничение памяти 8 ГБ для сервиса эмбеддингов с ежедневным перезапуском. До введения лимита он разрастался примерно до 15 ГБ и на неделю заблокировал очередь.
- Ночное резервное копирование в 03:30: сжатый дамп базы данных с хранением последних 14 копий плюс инкрементное зеркало PDF.
- Автоматическое восстановление зависших задач: задача, которая выполняется дольше 20 минут, возвращается в очередь, до 6 попыток.
Технологии
Python 3.11 и FastAPI для веб-приложения. PostgreSQL 16 с pgvector, unaccent и pg_trgm для данных, текстового поиска и векторов. PyMuPDF и Tesseract для текста и OCR. Sentence-transformers с моделью multilingual-e5-base для эмбеддингов, запущенные как отдельный локальный сервис. API Anthropic для ассистента. Всё работает как сервисы systemd за обратным прокси Apache.
Почему именно так
Я оставил очередь задач внутри PostgreSQL вместо того, чтобы добавлять Redis. Воркеры забирают задачи через FOR UPDATE SKIP LOCKED, поэтому несколько воркеров работают параллельно и никогда не обрабатывают один номер дважды. На сервере размещены и другие сайты, так что на один сервис меньше для поддержки и защиты — это реальный выигрыш.


