首页
/ Paperless-ngx v3 迁移指南:配置项、消费者与搜索索引的完整破坏性变更清单

Paperless-ngx v3 迁移指南:配置项、消费者与搜索索引的完整破坏性变更清单

2026-09-05 17:12:43作者:曹令琨Iris

本文基于 paperless-ngx 官方迁移文档 docs/migration-v3.md,系统梳理从 v2.20.15 升级到 v3 时必须面对的全部破坏性变更:强制的 PAPERLESS_SECRET_KEY、消费者监控设置重构、OCR/归档生成设置拆分、Whoosh 到 Tantivy 搜索后端替换、数据库配置整合、条码扫描器切换等,并逐项结合当前仓库源码给出可核验的实现证据,帮助你在升级前完成配置核对、在升级后定位行为差异。

迁移前提:只能从 v2.20.15 直接升级

升级 v3 的唯一合法起点是 v2.20.15。如果你的部署还停留在更早版本,必须先升级到 v2.20.15,再执行 v3 升级。

这条限制并非文档上的口头约定,源码中有硬性检查:启动时的系统检查会核对 documents 应用最后应用的迁移是否为 1075_workflowaction_order(即 v2.20.15 的迁移终点),不满足时直接返回错误码 paperless.E002,并提示先升级、先跑 manage.py migrate 再升 v3,见 src/paperless/checks.py#L245-L262

PAPERLESS_SECRET_KEY 成为必填项

PAPERLESS_SECRET_KEY 环境变量在 v3 中是强制要求。它用于加密签名(会话、签名令牌等),应当设置为一个足够长的随机值。

在设置加载阶段,若该变量未设置、为空字符串,或者仍保留 paperless.conf.example 中的占位默认值 change-me,程序会直接抛出 ImproperlyConfigured 异常拒绝启动,并在错误信息中给出生成命令,见 src/paperless/settings/init.py#L497-L503

SECRET_KEY = os.getenv("PAPERLESS_SECRET_KEY")
if not (SECRET_KEY or "").strip() or SECRET_KEY == "change-me":
    raise ImproperlyConfigured(
        "PAPERLESS_SECRET_KEY is not set or is the default 'change-me' value. ..."
    )

占位值 change-me 可以在 paperless.conf.example 中看到。

需要执行的操作

  • 如果你的安装一直依赖旧版本内置的默认密钥,有两个选择:
    • PAPERLESS_SECRET_KEY 设为那个旧值,以保留现有会话和令牌的有效性;
    • 设为新的随机值以提高安全性,代价是使所有现有会话和其他签名令牌失效。
  • 全新安装或选择轮换密钥时,可用以下命令生成:
python3 -c "import secrets; print(secrets.token_urlsafe(64))"

安装脚本 install-paperless-ngx.sh 在安装流程中也会生成该变量并写入配置,可直接参考其处理方式。

消费者监控设置重构:统一到 watchfiles

v3 的 document_consumer 命令改用 watchfiles 库来统一监听 consume 目录中的新文件。对使用者而言,这意味着多个与延时、重试相关的配置项被移除并合并为一个统一设置;同时消费忽略(ignore)过滤的实现从 fnmatch 改为正则表达式,并且目录级忽略与文件级忽略被拆分成了两个独立设置。

源码印证:消费者命令直接导入并调用 watchfiles 的 watchChangeDefaultFilter,见 src/documents/management/commands/document_consumer.py#L23-L25;四个新设置在该命令中作为参数读取,见 src/documents/management/commands/document_consumer.py#L428-L431;监听循环会按条件在轮询模式与原生文件系统事件模式之间切换(日志中分别输出 polling (interval: ...)native file system events),见 src/documents/management/commands/document_consumer.py#L555-L576

新旧设置对照

旧设置 新设置 说明
CONSUMER_POLLING CONSUMER_POLLING_INTERVAL 仅为清晰而改名
CONSUMER_INOTIFY_DELAY CONSUMER_STABILITY_DELAY 所有模式下统一
CONSUMER_POLLING_DELAY 已移除 改用 CONSUMER_STABILITY_DELAY
CONSUMER_POLLING_RETRY_COUNT 已移除 由稳定性追踪自动处理
CONSUMER_IGNORE_PATTERNS CONSUMER_IGNORE_PATTERNS 现在是正则,不再是 fnmatch;用户模式是追加到默认模式之上,而非替换
新增 CONSUMER_IGNORE_DIRS 额外忽略的目录;用户条目同样是追加而非替换默认值

迁移注意两点:一是 CONSUMER_IGNORE_PATTERNS 的语法从通配符变成正则,原有的 *.tmp 之类写法需要改写为如 .*\.tmp$;二是忽略模式现在是叠加在内置默认值之上的,删除旧默认行为不能靠“清空变量”。

重复文档处理策略变化

v3 默认不再拒绝重复文档:重复文件会被允许入库,但 UI 中提供了识别重复的方式。如需恢复 v2 的拒绝行为,在环境中设置:

PAPERLESS_CONSUMER_DELETE_DUPLICATES=true

加密支持被移除

文档与缩略图的加密(encrypted documents/thumbnails)在 v3 中不再受支持。该能力早在 paperless-ng 0.9.3 就已标记弃用(可查阅 docs/changelog.md 中的对应版本记录)。

升级前必须执行:使用 decrypt_documents 命令解密所有加密文档。该命令属于 v2 时代的消费端管理命令,请在升级前于 v2 环境运行。

条码扫描器:pyzbar 移除,zxing-cpp 成为唯一后端

v3 移除了对 pyzbar 的支持。迁移文档给出的理由:底层 libzbar 已约 16 年未更新、基本处于无人维护状态,pyzbar 的 Python 封装最后发布于 2022 年 3 月;实践中 pyzbar 在倾斜、低对比度或部分遮挡的条码上检测可靠性不佳。zxing-cpp 则持续维护、检测可靠性更高,并且为 x86_64 与 arm64 提供预编译 wheel,不再需要自行编译。

旧设置 新设置 说明
CONSUMER_BARCODE_SCANNER 已移除 zxing-cpp 现在是唯一后端

条码检测的实现位于 src/documents/barcodes.py

需要执行的操作

  • 如果你本来就用 CONSUMER_BARCODE_SCANNER=ZXING,直接删掉该设置即可;
  • 如果是 PYZBAR 或使用默认值,除删除设置外没有功能性变化;zxing-cpp 支持相同的条码格式,且检测可靠性有所提升;
  • libzbar0 / libzbar-dev 系统包不再需要,可以从自定义 Docker 镜像或主机安装的依赖列表中移除。

数据库引擎配置:PAPERLESS_DBENGINE 变为显式必填

此前引擎可从 PAPERLESS_DBHOST 是否存在来推断,PAPERLESS_DBENGINE 只在需要于 PostgreSQL 与 MariaDB 之间选择时才必须设置。v3 中,使用 PostgreSQL 或 MariaDB 都必须显式设置 PAPERLESS_DBENGINE;SQLite 用户无需任何改动,但也可以显式声明引擎。

需要执行的操作——PostgreSQL 与 MariaDB 用户要为环境加上 PAPERLESS_DBENGINE

# v2 (PostgreSQL inferred from PAPERLESS_DBHOST)
PAPERLESS_DBHOST: postgres

# v3 (engine must be explicit)
PAPERLESS_DBENGINE: postgresql
PAPERLESS_DBHOST: postgres

PAPERLESS_DBENGINE 的合法取值见 docs/configuration.md#PAPERLESS_DBENGINE。从源码结构看,解析入口 src/paperless/settings/custom.py#L204-L230parse_db_settings() 先用 get_choice_from_env("PAPERLESS_DBENGINE", {"sqlite", "postgresql", "mariadb"}) 读取显式取值,未显式设置时仍保留一段向后兼容的推断逻辑(存在 PAPERLESS_DBHOST 推断为 postgresql,否则为 sqlite)——代码注释说明这是为了照顾从未设置过引擎变量的 MariaDB 与 SQLite 老用户,但按迁移文档的要求,升级时应显式声明引擎以消除歧义。

数据库高级选项整合:PAPERLESS_DB_OPTIONS

原先分散的 SSL、超时与连接池环境变量在 v3 中全部移除,整合为单一 PAPERLESS_DB_OPTIONS 字符串。这样做的目的是把不断增长的引擎专属变量集中到一处,并且任何底层数据库驱动支持的选项都能直接设置,无需为每个选项各开一个环境变量。

被移除的变量及其在 PAPERLESS_DB_OPTIONS 中的替代写法:

被移除的变量 PAPERLESS_DB_OPTIONS 中的替代
PAPERLESS_DBSSLMODE sslmode=<value>(PostgreSQL)或 ssl_mode=<value>(MariaDB)
PAPERLESS_DBSSLROOTCERT sslrootcert=<path>(PostgreSQL)或 ssl.ca=<path>(MariaDB)
PAPERLESS_DBSSLCERT sslcert=<path>(PostgreSQL)或 ssl.cert=<path>(MariaDB)
PAPERLESS_DBSSLKEY sslkey=<path>(PostgreSQL)或 ssl.key=<path>(MariaDB)
PAPERLESS_DB_POOLSIZE pool.max_size=<value>(仅 PostgreSQL)
PAPERLESS_DB_TIMEOUT timeout=<value>(SQLite)或 connect_timeout=<value>(PostgreSQL/MariaDB)

这些弃用变量暂时仍可工作,但将在未来版本移除;只要环境中还设置了其中任意一个,启动时都会记录一条弃用警告。源码中两个事实可以核对:解析侧 src/paperless/settings/custom.py#L325-L338 以逗号分隔解析 PAPERLESS_DB_OPTIONS 并叠加到各引擎的默认选项之上(timeoutconnect_timeoutpool.min_sizepool.max_size 会被转换为整数);检查侧 src/paperless/checks.py#L265-L302check_deprecated_db_settings() 对上述六个变量逐一告警,警告文案中还明确写有“will be removed in v3.2”。

需要执行的操作——设置了任一弃用变量的用户应迁移到 PAPERLESS_DB_OPTIONS,多个选项用逗号拼接在同一个值里:

PAPERLESS_DB_OPTIONS="sslmode=require,sslrootcert=/certs/ca.pem,pool.max_size=10"

OCR 与归档文件生成设置重构

控制 OCR 行为与归档文件(archive file)生成的两组设置经过重新设计。旧设置把这两个本应独立的关注点耦合在一起,v3 中整体移除——旧值不会被静默沿用,只要环境里还残留被移除的变量,启动时就会记录警告(对应检查实现见 src/paperless/checks.py#L305-L319 中对 PAPERLESS_OCR_SKIP_ARCHIVE_FILE 的告警)。

被移除的设置

被移除的设置 替代
PAPERLESS_OCR_MODE=skip PAPERLESS_OCR_MODE=auto(新默认值)
PAPERLESS_OCR_MODE=skip_noarchive PAPERLESS_OCR_MODE=auto + PAPERLESS_ARCHIVE_FILE_GENERATION=never
PAPERLESS_OCR_SKIP_ARCHIVE_FILE=never PAPERLESS_ARCHIVE_FILE_GENERATION=always
PAPERLESS_OCR_SKIP_ARCHIVE_FILE=with_text PAPERLESS_ARCHIVE_FILE_GENERATION=auto(新默认值)
PAPERLESS_OCR_SKIP_ARCHIVE_FILE=always PAPERLESS_ARCHIVE_FILE_GENERATION=never

改了什么、为什么改

此前 OCR_MODE 混淆了两件独立的事:是否运行 OCR是否产出归档文件skip 的语义是“已有文本就跳过 OCR,但总是产出归档”;skip_noarchive 则是“已有文本就跳过 OCR,同时也不产出归档”。这种耦合导致例如“彻底禁用 OCR 但仍生成归档”这样的组合无法表达。

新设置相互独立:

数据库中的配置自动迁移

如果 OCR 设置是通过后台管理界面(ApplicationConfiguration)改的,数据库里的值会在升级过程中自动迁移mode 的旧值(skip / skip_noarchive)被映射到新等价值,显式设置的 skip_archive_file 值则转换到新的 archive_file_generation 字段。迁移逻辑可以逐行核对,src/paperless/migrations/0008_replace_skip_archive_file.py#L6-L42 中:

_MODE_MAP = {"skip": "auto", "redo": "redo", "force": "force", "skip_noarchive": "auto"}
_ARCHIVE_MAP = {"never": "always", "with_text": "auto", "always": "never"}
# 特例:skip_noarchive 且未显式设置过 skip_archive_file 时 -> "never"

注意文档中的提醒:依赖旧默认值的用户必须把 archive_file_generation 显式设为 always,才能保持 v2“总是生成归档”的行为。升级完成后请到后台管理界面的 OCR 设置中复核迁移结果是否符合预期。

需要执行的操作

从环境中删除任何 PAPERLESS_OCR_SKIP_ARCHIVE_FILE 变量;如果用过 OCR_MODE=skipOCR_MODE=skip_noarchive,按下表更新:

# v2: 有文本就跳过 OCR,总是归档
PAPERLESS_OCR_MODE=skip
# v3: 等价写法
PAPERLESS_OCR_MODE=auto
PAPERLESS_ARCHIVE_FILE_GENERATION=always

# v2: 有文本就跳过 OCR,同时跳过归档
PAPERLESS_OCR_MODE=skip_noarchive
# v3: 等价写法
PAPERLESS_OCR_MODE=auto
PAPERLESS_ARCHIVE_FILE_GENERATION=never

# v2: 总是跳过归档
PAPERLESS_OCR_SKIP_ARCHIVE_FILE=always
# v3: 等价写法
PAPERLESS_ARCHIVE_FILE_GENERATION=never

# v2: 仅对数字原生(born-digital)文档跳过归档
PAPERLESS_OCR_SKIP_ARCHIVE_FILE=with_text
# v3: 等价写法(auto 就是新默认值)
PAPERLESS_ARCHIVE_FILE_GENERATION=auto

远程 OCR 解析器

如果你使用远程 OCR 解析器(Azure AI,实现位于 src/paperless/parsers/remote.py),ARCHIVE_FILE_GENERATION 的语义与本地引擎一致:当不请求归档(never,或 auto 且文件为数字原生 PDF)时,远程引擎会被整体跳过,改用本地提取的文本,从而避免一次不必要的 API 调用和一层重复的文本层。

全文搜索后端替换:Whoosh → Tantivy

全文搜索后端已被替换为 Tantivy,src/documents/search/ 目录下的 _backend.py_schema.py_query.py_tokenizer.py_translate.py_dates.py 即新搜索模块的实现。由于 Tantivy 索引格式与 Whoosh 不兼容,升级后首次启动会自动从零重建搜索索引,重建本身不需要任何手动操作。

备注与自定义字段搜索语法

旧的 Whoosh 索引把 notecustom_field 暴露为平坦文本字段,会被无字段限定的搜索命中(例如只输入 invoice 就会匹配到备注内容)。Tantivy 下它们变成结构化 JSON 字段,需通过点号路径访问:

旧语法 新语法
note:query notes.note:query
custom_field:query custom_fields.value:query

保存视图会被自动迁移。 数据迁移在升级时运行,会把保存视图过滤规则中显式使用 note:custom_field: 字段前缀的全文搜索查询重写为新语法。实现细节见 src/documents/migrations/0017_migrate_fulltext_query_field_prefixes.py#L5-L22:用带负向后顾的正则 (?<![.\w])note: / (?<![.\w])custom_field: 做替换,刻意避免误伤 denote: 这类词或已迁移过的 notes.note:

无字段限定的查询不会被迁移。 如果某个保存视图就是一个裸搜索词(如 invoice),而它恰好命中过备注或自定义字段值,那么迁移后将不再返回这些命中。需要把查询显式写成带前缀的形式,例如:

invoice OR notes.note:invoice OR custom_fields.value:invoice

此外,自定义字段名本身也可以用 custom_fields.name:fieldname 来搜索。

OpenID Connect 令牌端点认证方法

升级到 v3 后,部分既有的 OpenID Connect 部署可能需要显式指定令牌端点认证方法(token endpoint authentication method):

如果 OIDC 登录在回调阶段报 invalid_client 错误,需要在 PAPERLESS_SOCIALACCOUNT_PROVIDERS 的 provider settings 中加上 token_auth_method,例如:

{
  "openid_connect": {
    "APPS": [
      {
        "settings": {
          "server_url": "https://login.example.com",
          "token_auth_method": "client_secret_basic"
        }
      }
    ]
  }
}

相关认证适配逻辑位于 src/paperless/adapter.py,排查该问题时可从此文件入手。

升级时任务历史被清空

本次发布对任务跟踪系统做了重新设计(对应迁移文件 src/documents/migrations/0019_task_system_redesign.py)。升级过程中,数据库里所有既有的任务历史记录都会被删除——此前已完成、失败或已确认的任务在升级后不会再出现在任务列表中。

无需任何用户操作,知悉即可。

消费前后脚本不再接收位置参数

前置(pre-consumption)与后置(post-consumption)处理脚本不再接收位置参数,所有信息只通过环境变量传递(环境变量方式自更早版本起就已可用)。仓库中的示例脚本 scripts/post-consumption-example.sh 展示了后置脚本的标准用法。

前置消费脚本

此前原始文件路径通过 $1 传入,现在只能通过 DOCUMENT_SOURCE_PATH 获取。

升级前:

#!/usr/bin/env bash
# $1 was the original file path
process_document "$1"

升级后:

#!/usr/bin/env bash
process_document "${DOCUMENT_SOURCE_PATH}"

后置消费脚本

此前文档元数据通过 $1$8 八个位置参数传入,与新环境变量的对应关系:

参数 等价的环境变量
$1 DOCUMENT_ID
$2 DOCUMENT_FILE_NAME
$3 DOCUMENT_SOURCE_PATH
$4 DOCUMENT_THUMBNAIL_PATH
$5 DOCUMENT_DOWNLOAD_URL
$6 DOCUMENT_THUMBNAIL_URL
$7 DOCUMENT_CORRESPONDENT
$8 DOCUMENT_TAGS

升级前:

#!/usr/bin/env bash
DOCUMENT_ID=$1
CORRESPONDENT=$7
TAGS=$8

升级后:

#!/usr/bin/env bash
# Use environment variables directly
echo "Document ${DOCUMENT_ID} from ${DOCUMENT_CORRESPONDENT} tagged: ${DOCUMENT_TAGS}"

需要执行的操作

把任何读取 $1$2 等位置参数的前置/后置脚本改写为使用对应的环境变量。文档强调:环境变量自 v1.8.0 起就是推荐方式。

反向代理与登录限流

allauth 调整了登录限流时确定客户端 IP 的方式。运行在反向代理后面的用户可能需要设置 PAPERLESS_TRUSTED_PROXIESPAPERLESS_ALLAUTH_TRUSTED_PROXY_COUNTPAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER 或它们的组合,以避免登录时出现 403 Forbidden。注意:proxy count 指的是 X-Forwarded-For 中的代理跳数,可能与实际配置的代理 IP 地址数量不一致。

这三个设置直接注入 allauth,见 src/paperless/settings/init.py#L483-L487

NumPy 最低 CPU 基线(x86-64-v2 / SSE4.2)

自 NumPy 2.4.0 起,官方 manylinux x86_64 wheel 以 x86-64-v2 为最低 CPU 基线编译,要求 CPU 支持 SSE3、SSSE3、SSE4.1、SSE4.2、POPCNT 与 CMPXCHG16B。这是上游的刻意变更,理由是这些指令自 2008 年(Intel Nehalem 架构)或 2011 年(AMD Bulldozer 架构)起已存在于超过 99.7% 的 CPU 中。

NumPy 是文档分类器(经 scikit-learn)的依赖,因此任何不支持 SSE4.2 的 CPU 在分类器被加载或训练时都会以 SIGILL(非法指令)崩溃——无论 AI 功能是否启用。这与 NumPy 可选的 SIMD 分发(如 AVX2、AVX512)不同:后者会在运行时安全探测和选择,而 x86-64-v2 是烧死在 wheel 里的硬性下限,没有运行时回退。

受影响硬件

大约是 2008 年(Intel)或 2011 年(AMD)之前、不支持 SSE4.2 的 CPU。相比近十年的典型桌面/服务器硬件,低功率与嵌入式硬件(早期 Atom、Celeron 或 Bulldozer 之前的 AMD 芯片)更容易命中。可用以下命令检测 SSE4.2 支持:

grep -o -m1 sse4_2 /proc/cpuinfo

如果没有任何输出,你的 CPU 就在受影响范围内。

症状

Celery worker(以及可能的 web server)反复崩溃并重启,伴随 SIGILL 错误,通常在 dmesg/journalctl 中表现为 _multiarray_umath...so 内部的 trap invalid opcode。由于分类器按周期性计划训练(默认每小时一次),受影响实例会在该定时任务运行时出现间歇性、难以复现的文档消费失败,且 worker 会在任务中途被带走。

需要执行的操作(仅限受影响硬件)

没有办法让分类器本身在这种 CPU 上工作——那需要一个 cpu-baseline=none 的非官方 NumPy 构建,官方无法提供。可行的路径是让分类器永远不加载、不训练,从而完全避免导入 NumPy:

PAPERLESS_TRAIN_TASK_CRON=disable

这会禁用周期性分类器训练任务(见 PAPERLESS_TRAIN_TASK_CRON)。代价是基于分类器的自动匹配(已训练规则给出的建议发件人、文档类型、标签与存储路径)将不可用;但基于规则的匹配不受影响,文档消费本身也不再冒崩溃 worker 的风险。

数据库迁移细节:MailRule.maximum_age 钳制

v3 将若干整型字段改为更小的类型以减小数据库体积。副作用是:如果你的 MailRule 记录中 maximum_age 超过 32767,迁移会将其钳制为 32767,以避免迁移报错。该逻辑可直接核对,见 src/paperless_mail/migrations/0002_optimize_integer_field_sizes.py#L7-L10

def clamp_mailrule_maximum_age(apps, schema_editor):
    # PositiveIntegerField -> PositiveSmallIntegerField 的钳制
    MailRule.objects.filter(maximum_age__gt=32767).update(maximum_age=32767)

无需用户操作,迁移会自动完成钳制。

升级前核对清单

汇总上述各节,升级 v3 前建议按此清单逐项核对(“需要操作”项必须在升级前完成):

变更项 是否需要操作 操作摘要
版本前提 需要操作 确认当前为 v2.20.15 且已跑完 manage.py migrate
PAPERLESS_SECRET_KEY 需要操作 显式设置(沿用旧值保会话,或换新值轮换)
消费者设置 需要操作 对照新旧设置表改名/改写正则/拆分目录忽略
重复文档 可选 如需恢复拒绝行为,设 PAPERLESS_CONSUMER_DELETE_DUPLICATES=true
加密文档 需要操作 升级前用 decrypt_documents 解密全部加密文档
条码扫描 需要操作 删除 CONSUMER_BARCODE_SCANNER,清理 libzbar 依赖
PAPERLESS_DBENGINE 需要操作 PostgreSQL/MariaDB 用户显式声明引擎
PAPERLESS_DB_OPTIONS 需要操作 把六个弃用 DB 变量整合进单一选项串
OCR/归档设置 需要操作 删除 PAPERLESS_OCR_SKIP_ARCHIVE_FILE,按对照表改写 OCR_MODE
搜索索引 无需操作 首次启动自动重建;无字段限定的旧查询需手工补前缀
保存视图 无需操作 note:/custom_field: 前缀由迁移自动重写
OIDC 视情况 回调报 invalid_client 时加 token_auth_method
任务历史 无需操作 升级时清空,知悉即可
消费前后脚本 需要操作 位置参数全部改写为环境变量
反向代理限流 视情况 登录 403 时配置 TRUSTED_PROXIES 相关变量
老 CPU(无 SSE4.2) 需要操作 PAPERLESS_TRAIN_TASK_CRON=disable
MailRule.maximum_age 无需操作 迁移自动钳制到 32767

按此清单走完一遍,再执行 v3 升级,即可把绝大多数破坏性变更的影响控制在可预期范围内。

登录后查看全文
热门项目推荐
相关项目推荐