这是一个为私人收藏打造的数字报纸档案:PDF 格式的报纸变成一个可以检索的档案库,任何文章都可以通过关键词、语义或直接提问找到,并且始终注明出处。
问题
客户有一批以 PDF 文件保存的报纸。想找一篇文章,就得一个个打开文件,而且很多页面是没有文字的扫描件。此外,档案库必须保持私密:只有管理员邀请的人才能进入。
我做了什么:一个数字报纸档案
我开发了一个私有的 Web 档案库,每一期报纸都按报名、日期和 PDF 存储,每一页都可以检索。
- 文字提取,OCR 兜底:页面如果自带文字层,就直接使用。文字少于 40 个字符的页面会以 220 DPI 渲染,再用 Tesseract 识别。旧扫描件也因此可以检索。
- 意大利语全文检索:检索采用意大利语词干提取并忽略重音,因此搜索「citta」也能找到「città」。完全匹配的短语排在最前,并附带高亮摘要。
- 语义检索:页面文字被切分成相互重叠的文本块,再用多语言模型转换为 768 维向量。即使读者不知道确切的用词,也能找到文章。
- 附带引用的助手:读者用日常语言提问。系统按语义和关键词检索最多 8 个段落,Claude 只依据这些段落作答,并注明报名、日期和页码。该功能在管理面板中启用,使用客户自己的 Anthropic 密钥。
- 按文件夹批量上传:管理员选择一个 PDF 文件夹,日期从每个文件名中读取。重复文件会按报名和日期或按 SHA-256 指纹跳过,因此可以放心地重复上传。
- 仅限邀请访问:两种角色,管理员和读者。面板用于管理用户、报名、AI 密钥和备份。
数据
- 嵌入服务设有 8 GB 内存上限,并每天重启。设置上限之前,它曾膨胀到约 15 GB,导致队列阻塞了一周。
- 每晚 03:30 备份:压缩的数据库转储,保留最近 14 份,另加 PDF 文件的增量镜像。
- 卡住的任务自动恢复:运行超过 20 分钟的任务会重新放回队列,最多尝试 6 次。
技术栈
Web 应用使用 Python 3.11 和 FastAPI。PostgreSQL 16 配合 pgvector、unaccent 和 pg_trgm,负责数据、全文检索和向量。PyMuPDF 和 Tesseract 负责文字提取和 OCR。嵌入使用 sentence-transformers 和 multilingual-e5-base 模型,作为独立的本地服务运行。助手使用 Anthropic API。所有组件都以 systemd 服务运行,前面是 Apache 反向代理。
为什么这样做
我把任务队列放在 PostgreSQL 内部,而没有另加 Redis。工作进程通过 FOR UPDATE SKIP LOCKED 领取任务,因此多个工作进程可以并行运行,而且绝不会重复处理同一期报纸。这台服务器还托管着其他网站,少运行、少维护一个服务,是实实在在的好处。


