mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-08-20 09:43:29 +00:00
fix(search): migrate off whoosh-compat's removed QueryEmitError/UnsupportedQueryError
whoosh-compat replaced both exception classes with a single QueryError carrying a structured Diagnostic (kind/cause/field_kind); the old message-text regex stripping is now dead weight since the library no longer embeds host-facing wording (DIVERGENCES refs, fast=True advice) in Diagnostic.message. Branch on diagnostic.kind instead. Also trims a test that was re-asserting whoosh-compat's own message contract (now covered by its own test_kind_matrix.py) down to the one rewrite paperless still owns: EXISTS_REQUIRES_FAST's user-facing message. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
committed by
stumpylog
co-authored by
Claude Sonnet 5
parent
5abd568209
commit
bb0d0fc6c0
@@ -11,8 +11,7 @@ from django.conf import settings
|
||||
from whoosh_compat.emitters.tantivy_ import emit as tantivy_emit
|
||||
from whoosh_compat.errors import Diagnostic
|
||||
from whoosh_compat.errors import DiagnosticKind
|
||||
from whoosh_compat.errors import QueryEmitError
|
||||
from whoosh_compat.errors import UnsupportedQueryError
|
||||
from whoosh_compat.errors import QueryError
|
||||
|
||||
from documents.search._errors import InvalidDateQuery
|
||||
from documents.search._errors import InvalidNumberQuery
|
||||
@@ -112,21 +111,19 @@ def _rewrite_bare_json_field_prefixes(raw_query: str) -> str:
|
||||
return raw_query
|
||||
|
||||
|
||||
# whoosh-compat's emit() error messages are written for the HOST: they
|
||||
# cite the library's own divergence ledger and give registry-configuration
|
||||
# advice. Neither belongs in a message shown to a searching user.
|
||||
_DIVERGENCE_REF_RE: Final = regex.compile(r"\s*\(DIVERGENCES\.md entry \d+\)")
|
||||
def _user_facing_emit_message(d: Diagnostic) -> str:
|
||||
"""A user-safe message for an emit-time QueryError's Diagnostic.
|
||||
|
||||
|
||||
def _user_facing_emit_message(exc: Exception) -> str:
|
||||
"""A user-safe message for a QueryEmitError/UnsupportedQueryError."""
|
||||
message = _DIVERGENCE_REF_RE.sub("", str(exc))
|
||||
if "fast=True" in message:
|
||||
# The exists-check message advises marking the field fast=True, a
|
||||
# host configuration action; the user just needs to know the
|
||||
# search form is unsupported here.
|
||||
whoosh-compat's own Diagnostic.message is developer/log output with no
|
||||
stability guarantee (branch on kind, never parse the message). Only
|
||||
EXISTS_REQUIRES_FAST needs a distinct user-facing rewrite: its message
|
||||
advises marking the field fast=True, a host configuration action the
|
||||
user can't act on; the user just needs to know the search form is
|
||||
unsupported here.
|
||||
"""
|
||||
if d.kind is DiagnosticKind.EXISTS_REQUIRES_FAST:
|
||||
return "existence searches (field:*) are not supported for this field"
|
||||
return message
|
||||
return d.message
|
||||
|
||||
|
||||
def _has_cjk(text: str) -> bool:
|
||||
@@ -321,8 +318,8 @@ def parse_user_query(
|
||||
and raise — the view returns HTTP 400 with every offending field
|
||||
listed, not just the first.
|
||||
3. emit() turns the AST into a tantivy.Query directly (no string
|
||||
round-trip). UnsupportedQueryError (a construct that parses but can't
|
||||
execute against tantivy, e.g. a text-field range) also maps to a 400.
|
||||
round-trip). QueryError (a construct that parses but can't execute
|
||||
against tantivy, e.g. a text-field range) also maps to a 400.
|
||||
4. Optional fuzzy blend (ADVANCED_FUZZY_SEARCH_THRESHOLD) builds a
|
||||
plain word string from the parsed AST's free-text tokens
|
||||
(whoosh_compat.free_text_tokens) and feeds THAT to
|
||||
@@ -348,10 +345,13 @@ def parse_user_query(
|
||||
|
||||
try:
|
||||
exact = tantivy_emit(result.ast, index=index, registry=registry)
|
||||
except (QueryEmitError, UnsupportedQueryError) as e:
|
||||
# emit()'s documented host contract: BOTH of these are user-input
|
||||
# errors, exactly like a parse diagnostic, and both map to a 400.
|
||||
raise SearchQueryError(_user_facing_emit_message(e)) from e
|
||||
except QueryError as e:
|
||||
# emit()'s documented host contract: every reachable-from-query-text
|
||||
# kind here (TEXT_RANGE, PATTERN_TOO_COMPLEX, EXISTS_REQUIRES_FAST)
|
||||
# is a user-input error, exactly like a parse diagnostic, and maps
|
||||
# to a 400. The AST_*/BACKEND_REJECTED backstop kinds cannot occur
|
||||
# here: whoosh-compat only ever hands us the AST it parsed itself.
|
||||
raise SearchQueryError(_user_facing_emit_message(e.diagnostic)) from e
|
||||
|
||||
cjk_query = (
|
||||
_build_cjk_query(index, raw_query, _CJK_ALL_FIELDS)
|
||||
|
||||
@@ -313,43 +313,46 @@ class TestSearchQueryErrors:
|
||||
|
||||
|
||||
class TestEmitErrorContract:
|
||||
"""A diagnostics list, or a QueryEmitError/UnsupportedQueryError from
|
||||
emit(), are both user-input errors and must surface as
|
||||
SearchQueryError (HTTP 400), with library-internal wording stripped
|
||||
from the message."""
|
||||
"""A diagnostics list, or a QueryError from emit(), are both user-input
|
||||
errors and must surface as SearchQueryError (HTTP 400)."""
|
||||
|
||||
def test_query_emit_error_maps_to_search_query_error(
|
||||
self,
|
||||
query_index: tantivy.Index,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
from whoosh_compat.errors import QueryEmitError
|
||||
from whoosh_compat.errors import Cause
|
||||
from whoosh_compat.errors import Diagnostic
|
||||
from whoosh_compat.errors import DiagnosticKind
|
||||
from whoosh_compat.errors import QueryError
|
||||
|
||||
import documents.search._query as query_mod
|
||||
|
||||
def raise_emit_error(*args: object, **kwargs: object) -> None:
|
||||
raise QueryEmitError("synthetic emit failure")
|
||||
raise QueryError(
|
||||
Diagnostic(
|
||||
kind=DiagnosticKind.TEXT_RANGE,
|
||||
cause=Cause.UNSUPPORTED,
|
||||
message="synthetic emit failure",
|
||||
),
|
||||
)
|
||||
|
||||
monkeypatch.setattr(query_mod, "tantivy_emit", raise_emit_error)
|
||||
with pytest.raises(SearchQueryError):
|
||||
parse_user_query(query_index, "invoice", UTC)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("query", "leaked_fragment"),
|
||||
[
|
||||
pytest.param("title:[a TO b]", "DIVERGENCES", id="text-range-doc-ref"),
|
||||
pytest.param("notes.note:wild*", "DIVERGENCES", id="json-wildcard-doc-ref"),
|
||||
pytest.param("notes.user:*", "fast=True", id="exists-host-advice"),
|
||||
],
|
||||
)
|
||||
def test_unsupported_messages_carry_no_internal_vocabulary(
|
||||
def test_exists_requires_fast_gets_the_user_facing_rewrite(
|
||||
self,
|
||||
query_index: tantivy.Index,
|
||||
query: str,
|
||||
leaked_fragment: str,
|
||||
) -> None:
|
||||
# The only emit-time diagnostic kind paperless rewrites itself
|
||||
# (_user_facing_emit_message): whoosh-compat's own message advises
|
||||
# a host-side fast=True config change, which the user can't act
|
||||
# on, so this checks OUR rewrite, not whoosh-compat's wording
|
||||
# (that's whoosh-compat's own tests/emitter/test_kind_matrix.py's
|
||||
# job now).
|
||||
with pytest.raises(SearchQueryError) as exc_info:
|
||||
parse_user_query(query_index, query, UTC)
|
||||
assert leaked_fragment not in str(exc_info.value)
|
||||
# The message must still say something useful, not be blanked.
|
||||
assert str(exc_info.value).strip()
|
||||
parse_user_query(query_index, "notes.user:*", UTC)
|
||||
assert str(exc_info.value) == (
|
||||
"existence searches (field:*) are not supported for this field"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user