Commit Graph
11735 Commits
Author SHA1 Message Date
stumpylogandClaude Sonnet 5 7372cdaf6b Perf: move modified out of the vec0 metadata column into document_meta
vec0 only inlines TEXT metadata up to 12 bytes (sqlite-vec.c); modified
is an ISO timestamp (e.g. "2026-07-28T12:34:56.789012+00:00", 32 chars),
always over that threshold, so every read of it recompiled and stepped a
fresh SQL statement per row -- uncached, on every full scan
(get_modified_times(), every compact(), every structural migration). It
was also never filtered on inside a KNN query, so it never needed to be
a vec0 column in the first place.

Add document_meta(document_id INTEGER PRIMARY KEY, modified TEXT), one
row per document rather than per chunk (all of a document's chunks share
the same modified value). get_modified_times() drops its per-chunk dedup
loop entirely as a result -- one row read per document, not per chunk.
No separate index needed here (unlike document_chunks): document_id
being the INTEGER PRIMARY KEY makes it the table's own rowid, and every
access pattern is already keyed by it directly.

Schema bump to v3 (m0002), backfilling document_meta from the old vec0
column for existing stores. Along the way:
- _Row/_Vec0Params (NamedTuple) replace bare positional tuples for node
  rows and vec0 bind parameters -- still plain tuples as far as
  sqlite3.executemany() is concerned, but self-documenting.
- Both new document_meta copy paths (compact()/migration backfill) are
  batched via fetchmany(), matching the existing discipline for the
  chunk-scoped copy -- small today (one row per document), but the same
  bounded-memory principle applies regardless of size.
- Discovered along the way: m0001 delegated to _rebuild_into(), which
  always reflects the *current* schema. That was fine while v2 was
  current, but silently wrong the moment v3 existed (m0001 would
  produce a v3-shaped table instead of its actual v2 shape) -- fixed
  upstream on perf/13314-vecstore-point-delete, since it's a defect in
  that branch's own code independent of this change.

Second item from VECTOR_STORE_PERF_BACKLOG.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 14:39:20 -07:00
stumpylogandClaude Sonnet 5 3838706194 Fix: freeze m0001's vec0 table shape instead of tracking current schema
_rebuild_into()/_create_vec_table() always reflect whatever the *current*
schema is -- correct for compact() (which only ever runs on an
already-current-schema store), but wrong for a migration that isn't the
latest one anymore. m0001 delegated to them, which worked fine as long as
v2 was current, but silently breaks the moment a v2 -> v3 migration is
added: m0001 would then produce a v3-shaped table (whatever columns
happen to be current) instead of its actual v2 shape, and the next
migration in the chain would find the columns it expects to migrate from
already gone.

Spell out m0001's own v2-shaped CREATE VIRTUAL TABLE and row copy instead,
so it keeps producing the same output forever, independent of later
schema changes -- the same reasoning already documented on the test
helper _copying_apply(), just not, until now, applied to the real
migration.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 13:34:25 -07:00
stumpylogandClaude Sonnet 5 15938945d7 Simplify: dedupe file-swap rebuild, migration-check locking, and test mocks
Prompted by an automated simplification review of the branch. Applies the
highest-value findings, all behavior-preserving (full suite still green):

- vector_store.py: extract _rebuild_file() (temp-file lifecycle: open,
  populate, swap-in-or-discard-on-failure) and _rebuild_into() (create
  table, copy meta, stream rows) out of compact() and the v1->v2
  migration, which had duplicated both almost verbatim. The migration
  module shrinks to a single _rebuild_into() call instead of reaching
  into four PaperlessSqliteVecVectorStore privates.
- vector_store.py: extract _stored_schema_version() out of
  has_pending_migration() and check_and_run_migrations(), which
  duplicated the same schema_version read and its "missing key means
  current" default.
- indexing.py: extract _with_exclusive_access(), collapsing three
  identical _exclude_readers()/Timeout/log-and-skip blocks (compaction
  in update_llm_index() and llm_index_compact(), the migration check in
  _check_and_run_migrations()) to one line each. Also trims
  _check_and_run_migrations()'s docstring, which had grown to restate
  content already documented on has_pending_migration() and
  check_and_run_migrations(), including a claim that was now stale
  (llm_index_migrate() is a second consumer of the re-embed signal).
- test_ai_indexing.py: add a mock_store fixture for the
  write_store()-yields-a-MagicMock pattern that 8 tests were hand-rolling
  identically.
- migrations/__init__.py: consolidate the "how to add a migration"
  instructions to one place instead of three.
- docs/administration.md: fix two now-inaccurate claims -- the bare-metal
  migrate step doesn't run "automatically" (it's the manual step being
  documented), and the self-contradictory "if enabled... no-op if
  disabled" phrasing on the same step.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 12:32:59 -07:00
stumpylogandClaude Sonnet 5 470bcc1751 Refactor: address PR review comments
- Move schema migrations into their own paperless_ai/migrations package,
  one module per migration (mNNNN_description.py -- a leading digit isn't
  a valid Python identifier, unlike Django's own numbered migrations,
  which load via importlib.import_module() rather than a static import
  statement), establishing the pattern before more migrations accumulate
  in vector_store.py.
- Drop the redundant "if the LLM index is enabled..." clause from the
  Docker upgrade note in docs/administration.md -- it's already covered
  by the LLM index section below, and calling out just one of several
  auto-applied migrations there was incomplete.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 11:45:18 -07:00
stumpylogandClaude Sonnet 5 cd7038a7a4 Fix: don't gate document_ids-scoped LLM index updates on Document.modified
bulk_edit.py's tag/correspondent/document_type/storage_path/custom_field
helpers write via queryset.update() or direct M2M/through-model bulk
operations, none of which call Document.save() -- so Document.modified's
auto_now never fires. Comparing against get_modified_times() for a
document_ids-scoped update_llm_index() call was silently skipping reindex
of documents whose embedded metadata had just changed via a bulk edit.
The modified-time comparison is now only used for the unscoped,
full-library incremental scan, where it is still needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 11:13:05 -07:00
stumpylogandClaude Sonnet 5 df8a6cc715 Feature: run pending LLM index migrations automatically on startup
Add document_llmindex migrate, a cheap check-only path (no reindex) safe
to run unconditionally: has_pending_migration() short-circuits to a
metadata-only read once the store is current, so a healthy install pays
almost nothing. If a pending migration would require re-embedding, it
only logs a warning and leaves the index as-is -- re-embedding can be
slow and, for a metered embedding backend, cost money, so it stays a
deliberate manual action (document_llmindex rebuild), never automatic.

Wire it into the Docker image as a new init-llmindex-migrate oneshot
(modeled on init-search-index, gated on init-migrations), and document
the equivalent manual step for bare-metal upgrades.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 10:48:24 -07:00
stumpylogandClaude Sonnet 5 b21d50645d Fix: run pending structural migrations before delete()/upsert_document()
Only update_llm_index()'s nightly rebuild ran check_and_run_migrations(),
but llm_index_remove_document()/llm_index_add_or_update_document() call
delete()/upsert_document() directly and immediately (on every document
delete/edit). On upgrade, every pre-existing document is "pre-migration"
until the next scheduled rebuild -- up to 24h by default -- so a delete or
edit in that window would find zero document_chunks rows and silently
leave stale/orphaned vec0 rows behind.

Add has_pending_migration(), a cheap metadata-only read that needs no
exclusive access, so normal writes only pay for check_and_run_migrations()'s
exclusive locking when a migration is actually pending -- otherwise they'd
be gated by the same lock compaction uses, which
test_normal_write_is_not_gated_by_the_compaction_lock exists to forbid.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 10:30:04 -07:00
stumpylog 24b86770df Perf: store document_chunks.document_id as INTEGER
document_chunks is a plain SQLite table, not a vec0 virtual table, so
(unlike vec0's own TEXT metadata column) it gets normal type-affinity
coercion between the TEXT document ids used elsewhere in this module and
an INTEGER column -- verified empirically. INTEGER keys make the
per-document delete lookup this table exists for cheaper: smaller index
entries and integer rather than text B-tree comparisons.
2026-07-28 09:39:02 -07:00
stumpylog 6a2972f313 Test: add missing document_chunks coverage and GIVEN/WHEN/THEN docstrings
Cover compact()'s handling of a churned document's final chunk generation,
delete() on a never-indexed document, upsert_document() clearing to an empty
node list, and the v1->v2 migration's embed_model preservation and
idempotency, per repo convention.
2026-07-28 09:36:58 -07:00
Trenton HolmesandClaude Sonnet 5 fa9741b555 Simplify: dedupe row-copy/migration-test helpers, type fixture params
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 08:56:16 -07:00
Trenton HolmesandClaude Sonnet 5 63c9430eec Perf: point-delete vector store chunks by id instead of a document_id scan
vec0 only gets an efficient lookup on a metadata column inside a KNN
query; a plain document_id-filtered DELETE is a full table scan
regardless of index size. Add a document_chunks side table (plain
SQLite, real index) to look up chunk ids for a document, then delete
each by its id primary key instead. Includes a v1 -> v2 migration to
backfill document_chunks for indexes created before this existed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 08:56:16 -07:00
shamoonandGitHub de9f520143 Chore/fix: refactor frontend task service (#13365) 2026-07-28 00:34:30 -07:00
shamoonandGitHub a68c487d60 Fix: fix Enter selection in search autocomplete (#13361) 2026-07-27 23:33:13 -07:00
github-actions[bot]andGitHub 217421bceb Documentation: Add v3.0.4 changelog (#13356) 2026-07-27 21:21:10 -07:00
shamoon 3bc03bbaec Bump version to 3.0.4 v3.0.4 2026-07-27 20:14:39 -07:00
shamoon aaee24ac0b Merge branch 'dev' 2026-07-27 20:14:04 -07:00
674dab29df New Crowdin translations by GitHub Action (#13308)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-07-27 20:12:46 -07:00
GitHub Actions b2b2f7b488 Auto translate strings 2026-07-28 00:27:34 +00:00
shamoonandGitHub 0b588c2d31 Fix: prevent pdfjs highlight scrolling from affecting the entire page (#13355) 2026-07-27 17:25:52 -07:00
Trenton HandGitHub dcff067dc1 Fix: don't skip OCR/archive for tagged PDFs with no actual text (#13351) 2026-07-27 16:52:29 -07:00
shamoonandGitHub 7eee8538ec Performance: more performant filtering diacritic normalization (#13347) 2026-07-27 22:13:16 +00:00
98b66fdf24 Perf: prefetch notes and custom fields for LLM index text building (#13350)
build_llm_index_text queried Note and CustomFieldInstance (plus its
field FK) per document, uncovered by the earlier correspondent/type/
storage_path/tags prefetch fix. Add notes and custom_fields__field to
the rebuild and scoped document querysets, and read from doc.notes.all()
instead of a fresh Note.objects.filter() so the prefetch is actually used.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 14:50:37 -07:00
GitHub Actions 693a111c5a Auto translate strings 2026-07-27 19:35:15 +00:00
Trenton HandGitHub b19edd0b74 Fix: dedupe permission-visible documents when combined with multi-tag ALL filtering (#13331) (#13345)
The permission filter OR'd three querysets together on top of a
queryset that could already carry two independent tags__id__all
joins, letting a document that matched more than one branch (e.g.
unowned + group-permissioned) come back twice. Replaced it with a
single id__in filter against the existing permitted_document_ids
helper, which is join-free and can't hit this.
2026-07-27 19:33:26 +00:00
Trenton HandGitHub 0e98a7f1ce Performance: Scope llm index updates to actually modified documents (#13322)
* Perf: scope bulk_update_documents' LLM index refresh to the edited document ids instead of the whole library

* Perf: select_related/prefetch_related for the scoped LLM index batch

* Perf: select_related/prefetch_related for the full LLM index rebuild path

* Fix: address Copilot review findings on LLM index scoping
2026-07-27 10:23:43 -07:00
GitHub Actions 14517062c4 Auto translate strings 2026-07-27 16:53:33 +00:00
shamoonandGitHub 0c0faf7f80 Fixhancement: pass LLM output language to chat if specified (#13340) 2026-07-27 16:51:31 +00:00
SandroandGitHub eda79603fe Documentation: fix PAPERLESS_AI_LLM_OUTPUT_LANGUAGE heading level (#13341) 2026-07-27 09:37:09 -07:00
shamoonandGitHub 8f017942d9 Chore: resolve npm provenance issues with chokidar and semver (#13323) 2026-07-26 16:25:49 -07:00
GitHub Actions f4e7a2e9e4 Auto translate strings 2026-07-26 22:38:16 +00:00
shamoonandGitHub 317e534aa0 Fix: ensure preview reload on live changes (#13321) 2026-07-26 22:36:03 +00:00
Trenton HandGitHub 3ba6d0325a Fix: clamp out-of-range MailRule.order and ApplicationConfiguration DPI/page fields before smallint migration (#13316) 2026-07-26 22:11:27 +00:00
Trenton HandGitHub 318520a0f1 Fix: add docstring to DocumentClassifierSchema for cleaner LLM tool description (#13315) 2026-07-26 22:00:06 +00:00
Trenton HandGitHub ea1f3653a9 Fix: guard build_document_node against stale FK on deleted correspondent/doc type (#13318) 2026-07-26 21:45:59 +00:00
shamoonandGitHub 423d4b9f2d Fix: prevent search filter loss when closing document with Escape key (#13317) 2026-07-26 14:17:26 -07:00
shamoonandGitHub 8bc4e5ee94 Fix: fix frontend permissions display for stats/system perms (#13305) 2026-07-26 02:56:06 -07:00
github-actions[bot]GitHubgithub-actions <41898282+github-actions[bot]@users.noreply.github.com>shamoon
65fe6aacc7 Documentation: Add v3.0.3 changelog (#13300)
* Changelog v3.0.3 - GHA

* Update changelog.md

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2026-07-25 18:57:03 -07:00
shamoon b5a23cff2f Bump version to 3.0.3 v3.0.3 2026-07-25 16:32:39 -07:00
aa90f79c21 New Crowdin translations by GitHub Action (#13297)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-07-25 16:31:40 -07:00
e704bf88dc New Crowdin translations by GitHub Action (#13239)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-07-25 16:30:17 -07:00
Trenton HandGitHub 1646756c6c Docs: warn bare-metal users about stale files when upgrading (#13296) 2026-07-25 14:06:31 -07:00
shamoon 48d6ff13f2 Tweak: use css grid for navbar 2026-07-25 09:13:34 -07:00
GitHub Actions bfe2d92619 Auto translate strings 2026-07-25 07:13:46 +00:00
shamoonandGitHub 4f0845b094 Fixhancement: PAPERLESS_ALLAUTH_TRUSTED_PROXY_COUNT (#13281) 2026-07-25 00:12:12 -07:00
shamoonandGitHub dee568e2a6 Tweak: adjust top navbar wrapping when AI chat button visible (#13280) 2026-07-24 23:50:10 -07:00
shamoon f60a2af841 Merge branch 'main' into dev 2026-07-24 19:08:23 -07:00
Trenton HandGitHub 33cd48a17f Fix: preserve document fields during Gotenberg conversion (#13271)
Gotenberg's LibreOffice route enables updateIndexes by default, which refreshes dynamic fields (e.g. auto-dates) to the current date. Disable it so field values are preserved as authored.
2026-07-24 13:59:51 -07:00
Trenton HandGitHub d76dbf5366 Documentation: Add the NumPy CPU baseline increase to the migration guide (#13269) 2026-07-24 09:18:07 -07:00
Trenton HandGitHub b66182cd7b Fix: Makes the email date aware as soon as possible during parsing (#13266) 2026-07-24 15:39:25 +00:00
Trenton HandGitHub 3fe6562c57 Fix: Handle a plain string as Celery sometimes provides for the traceback (#13267) 2026-07-24 15:27:57 +00:00