Commit Graph
2 Commits
Author SHA1 Message Date
stumpylogandClaude Opus 5 885bc2fdf3 fix(search): cap query length at the shared search-param helper (F3)
whoosh-compat's fieldname tagger is O(n^2) in plain word characters,
reachable only through SearchMode.QUERY's whoosh grammar. Measured
against the real field registry: ~1s at 10k chars, ~3.7s at 20k, ~14.4s
at 40k. The POST selection-filter path (bulk edit, bulk download) has
no server-imposed length bound the way the GET path incidentally does
via header limits, making an unbounded query a single-request CPU
exhaustion vector.

Cap both entry points at their shared choke point,
_get_tantivy_query_and_mode, with a new QueryTooLongError that reuses
the existing SearchQueryError -> 400 routing both callers already
have. 4096 chars bounds the worst case to roughly 0.16s by quadratic
extrapolation, far beyond any plausible hand-typed query. TEXT and
TITLE modes route through simple_search_tokens instead and measure
linear even at 20k chars, so the same cap is hygiene for them rather
than a fix. Hardcoded rather than a PAPERLESS_* setting: this is a
security boundary, and a raisable ceiling could reintroduce the exact
hazard it exists to close.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 11:30:30 -07:00
Trenton Holmes ddf8287072 refactor(search): split error classes and build_permission_filter out of _query.py
_query.py mixed three unrelated responsibilities: the SearchQueryError
family (paperless's public error-surface API, re-exported by __init__.py),
the actual query rewrite/parse/emit/blend pipeline, and
build_permission_filter, which has nothing to do with query parsing and
is consumed only by _backend.py.

- New _errors.py: SearchQueryError, InvalidDateQuery, InvalidNumberQuery,
  MultipleSearchQueryErrors, search_query_error_messages. _query.py now
  imports these instead of defining them.
- build_permission_filter moves to _backend.py, next to its one caller
  (TantivyBackend._build_permission_filter).
- __init__.py re-exports the error classes from _errors.py instead of
  _query.py; the package's public API (documents.search import ...) is
  unchanged for every caller going through it (views.py etc.).

_query.py now reads top-to-bottom as rewrite -> parse -> emit -> blend,
matching what parse_user_query's own docstring already claimed the file
was.
2026-08-19 13:36:53 -07:00