mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-09-11 20:28:01 +00:00
Fture search error ideas
This commit is contained in:
@@ -0,0 +1,99 @@
|
|||||||
|
# Spec stub: friendlier advanced-search error shapes (follow-up)
|
||||||
|
|
||||||
|
**Status:** stub / not yet planned. Follow-up to the `InvalidDateQuery` work on
|
||||||
|
branch `fix/search-query-translation` (PR #13010).
|
||||||
|
**Builds on:** the `SearchQueryError(ValueError)` base added in
|
||||||
|
`documents/search/_translate.py` and the single `except SearchQueryError` handler
|
||||||
|
in `UnifiedSearchViewSet.list` (`documents/views.py`). Any new subclass surfaces
|
||||||
|
through that one handler automatically, so this work is purely additive.
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Every failure on the advanced-search path (other than the now-handled invalid
|
||||||
|
date) lands in the view's generic `except Exception` and returns
|
||||||
|
`HttpResponseBadRequest("Error listing search results, check logs for more
|
||||||
|
detail.")`. `index.parse_query(...)` runs _outside_ the `translate_query`
|
||||||
|
try/except in `parse_user_query`, so anything Tantivy rejects bypasses
|
||||||
|
`SearchQueryError` entirely and gets the unhelpful generic 400. Some Tantivy
|
||||||
|
errors also leak Rust internals if surfaced verbatim.
|
||||||
|
|
||||||
|
## Ground truth: what Tantivy raises (empirically probed 2026-06-15)
|
||||||
|
|
||||||
|
`index.parse_query` raises `ValueError` with three distinguishable message
|
||||||
|
families:
|
||||||
|
|
||||||
|
| Family | Example inputs | Tantivy message |
|
||||||
|
| ---------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
|
||||||
|
| Unknown field | `foobar:hello`, `owner:5`, `has_tags:true`, `is_shared:true` | `Field does not exist: 'foobar'` |
|
||||||
|
| Syntax error | `(invoice OR bill`, `created:[2020 TO 2021`, `invoice AND`, `OR invoice`, `title:"abc`, `:value`, `AND OR` | `Syntax Error: <echoes the whole query, no location>` |
|
||||||
|
| Wrong value type | `asn:notanumber`, `page_count:[foo TO bar]` | `Expected a valid integer: 'ParseIntError { kind: InvalidDigit }'` |
|
||||||
|
|
||||||
|
Parses OK (NOT errors, but possibly silently wrong — out of scope for error
|
||||||
|
shapes, noted for awareness): `page_count:>5`, `asn:<10` (the `>`/`<`
|
||||||
|
comparisons), `title:[a TO b]` (range on a text field), `title:~~~` (bad fuzzy),
|
||||||
|
`` (empty query).
|
||||||
|
|
||||||
|
## Proposed error shapes (all `SearchQueryError` subclasses)
|
||||||
|
|
||||||
|
### 1. `UnknownFieldError` (highest value)
|
||||||
|
|
||||||
|
Distinguish two sub-cases:
|
||||||
|
|
||||||
|
- **Typo** -> `Unknown search field 'corespondent'.` plus a "Did you mean
|
||||||
|
'correspondent'?" suggestion via `difflib.get_close_matches` against the known
|
||||||
|
schema field set.
|
||||||
|
- **Whoosh-only field with no Tantivy equivalent** (finite known set: `owner`,
|
||||||
|
`has_tags`, `has_correspondent`/other `has_*`, `is_shared`, custom-field
|
||||||
|
id/count) -> targeted message, e.g. "`has_tags` isn't available in full-text
|
||||||
|
search; use the filter sidebar," rather than "field does not exist."
|
||||||
|
|
||||||
|
The list of valid fields already exists implicitly: schema fields in
|
||||||
|
`documents/search/_schema.py` and `KNOWN_FIELDS` in `_translate.py`. Reconcile to
|
||||||
|
a single source of truth for validation + suggestions.
|
||||||
|
|
||||||
|
### 2. `InvalidFieldValueError` (sibling of `InvalidDateQuery`)
|
||||||
|
|
||||||
|
For numeric fields (`asn`, `page_count`, `num_notes`, `*_id`): "Field 'asn'
|
||||||
|
expects a number, got 'notanumber'." Carries `field` + `value` like
|
||||||
|
`InvalidDateQuery`. Also removes the `ParseIntError { ... }` Rust leak.
|
||||||
|
|
||||||
|
### 3. `MalformedQueryError` (structural syntax)
|
||||||
|
|
||||||
|
A cheap balance-check pass gives specific hints for the common cases (unbalanced
|
||||||
|
`"`, `[`, `(`, dangling `AND`/`OR`) before falling back to a clean generic
|
||||||
|
"check for unbalanced quotes, brackets, or parentheses." Tantivy's message has no
|
||||||
|
location, so do not echo it verbatim.
|
||||||
|
|
||||||
|
## Detection strategy: hybrid
|
||||||
|
|
||||||
|
- **Proactive validation in the scanner** (`_translate.py`): it already tokenizes
|
||||||
|
`field:value` / `field:[range]`, so extend it to validate field existence (+
|
||||||
|
suggestions) and numeric value types up front. Messages are rich,
|
||||||
|
context-aware, and independent of Tantivy's English error strings. Lives next
|
||||||
|
to `InvalidDateQuery`. Covers only the field-scoped tokens it recognizes.
|
||||||
|
- **Catch-and-sanitize wrapper** around `index.parse_query` in
|
||||||
|
`parse_user_query`: map residual `Syntax Error:` / type-mismatch messages into
|
||||||
|
`MalformedQueryError` / `InvalidFieldValueError` so nothing leaks internals or
|
||||||
|
the generic message. Backstop for pure structural errors the scanner does not
|
||||||
|
model.
|
||||||
|
|
||||||
|
Both paths raise `SearchQueryError`; the existing view handler routes them all.
|
||||||
|
|
||||||
|
## Risks / notes
|
||||||
|
|
||||||
|
- The catch-and-sanitize wrapper depends on Tantivy's error _message string
|
||||||
|
prefixes_ (`Field does not exist:`, `Syntax Error:`, `Expected a valid
|
||||||
|
integer:`), which are brittle across `tantivy-py` upgrades. Add a test that
|
||||||
|
pins those prefixes so an upgrade that changes them fails loudly rather than
|
||||||
|
silently regressing to the generic 400.
|
||||||
|
- Decide separately whether `>`/`<` comparisons should be supported, rejected
|
||||||
|
with a message, or left as-is. They currently parse without error and are
|
||||||
|
likely silently wrong relative to Whoosh semantics.
|
||||||
|
- Keep messages safe to surface: they may echo user input but must not include
|
||||||
|
internal paths, stack details, or Rust error structs.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
Frontend rendering of the structured `{"query": [...]}` 400 (the inline search
|
||||||
|
error UI) — only relevant if the messages should render differently from the
|
||||||
|
current generic banner.
|
||||||
Reference in New Issue
Block a user