mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-08-28 13:37:32 +00:00
fix(search): log a search misconfiguration once per field, not per request
EXISTS_REQUIRES_FAST is MISCONFIGURED and reachable from ordinary query text (notes.user:*), so the operator alert added with the Cause routing fired on every such request. An alert that repeats on every user query is one operators learn to filter out, which defeats routing MISCONFIGURED to an operator at all. The condition is a static configuration fact: it stays true until an operator changes the schema and reindexes, so the first log carries the same information as the ten-thousandth. Deduped on (kind, field) in a per-process set; a restart re-logs, re-surfacing the condition after a config change. The 400 is not deduped: every request still gets its response and its message. The key is bounded by the registry, not by query text. emit() only reports MISCONFIGURED for a field it resolved, and FieldRegistry.resolve returns None for any name or JSON subpath the registry does not declare. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
63de40c54e
commit
12667d9754
@@ -133,6 +133,35 @@ def _user_facing_emit_message(d: Diagnostic) -> str:
|
||||
return "The search query could not be executed."
|
||||
|
||||
|
||||
# MISCONFIGURED reports a static configuration fact (the registry declares a
|
||||
# field the schema does not carry, or one that is not fast), so it stays true
|
||||
# until an operator changes the schema and reindexes. The operator-facing log
|
||||
# therefore fires once per (kind, field) per process: an alert that repeats on
|
||||
# every user query is one operators learn to filter out. A restart re-logs,
|
||||
# which re-surfaces the condition after a config change.
|
||||
#
|
||||
# Bounded by the registry, not by query text: emit() only reports MISCONFIGURED
|
||||
# for a field it resolved, and FieldRegistry.resolve returns None for any name
|
||||
# or JSON subpath the registry does not declare (those become AST_UNKNOWN_FIELD,
|
||||
# an INTERNAL cause that never reaches here), so a user cannot grow this set.
|
||||
_logged_misconfigurations: set[tuple[DiagnosticKind, str]] = set()
|
||||
|
||||
|
||||
def _log_misconfiguration_once(d: Diagnostic) -> None:
|
||||
"""Log a registry/schema mismatch the first time this process sees it for
|
||||
a given field. Never gates the 400: every request still gets its answer."""
|
||||
key = (d.kind, str(d.field))
|
||||
if key in _logged_misconfigurations:
|
||||
return
|
||||
_logged_misconfigurations.add(key)
|
||||
logger.error(
|
||||
"Search index misconfiguration for field %s (%s): %s",
|
||||
d.field,
|
||||
d.kind.name,
|
||||
d.message,
|
||||
)
|
||||
|
||||
|
||||
def _map_emit_error(e: QueryError) -> SearchQueryError:
|
||||
"""Route an emit-time QueryError by its Diagnostic's Cause.
|
||||
|
||||
@@ -142,19 +171,15 @@ def _map_emit_error(e: QueryError) -> SearchQueryError:
|
||||
re-raised to surface the same way views.py already lets QueryParserError
|
||||
surface. MISCONFIGURED is deliberately both: the registry and the index
|
||||
schema disagree, which only an operator can fix, so it is logged as an
|
||||
error, but a request is still waiting and the query cannot run either
|
||||
way, so it also returns a 400.
|
||||
error (once per field per process, see _log_misconfiguration_once), but a
|
||||
request is still waiting and the query cannot run either way, so it also
|
||||
returns a 400.
|
||||
"""
|
||||
d = e.diagnostic
|
||||
if d.cause is Cause.INTERNAL:
|
||||
raise e
|
||||
if d.cause is Cause.MISCONFIGURED:
|
||||
logger.error(
|
||||
"Search index misconfiguration for field %s (%s): %s",
|
||||
d.field,
|
||||
d.kind.name,
|
||||
d.message,
|
||||
)
|
||||
_log_misconfiguration_once(d)
|
||||
return SearchQueryError(_user_facing_emit_message(d))
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user