mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-09-13 05:07:59 +00:00
207 lines
7.3 KiB
Python
207 lines
7.3 KiB
Python
"""Bare notes:/custom_fields: prefix resolution.
|
|
|
|
"notes:foo"/"custom_fields:foo" were valid fielded searches before the
|
|
whoosh-compat migration. The registry only exposes them as JSON subpaths, so
|
|
each JSON FieldSpec declares a default subpath (SubpathSpec(default=True)):
|
|
notes: resolves to notes.note:, custom_fields: resolves to
|
|
custom_fields.value:. This replaced an earlier regex-based rewrite
|
|
(_rewrite_bare_json_field_prefixes) that ran on the raw query string before
|
|
parsing and was blind to quoting, so a phrase like
|
|
content:"payment notes: none" was silently corrupted into a notes-field
|
|
search and matched nothing. Resolving the default subpath inside the parser
|
|
instead means quoting is already understood by the time it happens.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
import pytest
|
|
from django.contrib.auth.models import User
|
|
|
|
from documents.models import CustomField
|
|
from documents.models import CustomFieldInstance
|
|
from documents.models import Document
|
|
from documents.models import Note
|
|
|
|
if TYPE_CHECKING:
|
|
from documents.search._backend import TantivyBackend
|
|
|
|
pytestmark = [pytest.mark.search, pytest.mark.django_db]
|
|
|
|
|
|
def _matched_ids(backend: TantivyBackend, query: str) -> set[int]:
|
|
return set(backend.search_ids(query, user=None))
|
|
|
|
|
|
def _index(backend: TantivyBackend, **kwargs: object) -> Document:
|
|
doc = Document.objects.create(**kwargs)
|
|
backend.add_or_update(doc)
|
|
return doc
|
|
|
|
|
|
class TestBareJsonFieldPrefixes:
|
|
def test_bare_notes_prefix_searches_note_text(
|
|
self,
|
|
backend: TantivyBackend,
|
|
) -> None:
|
|
"""
|
|
GIVEN:
|
|
- A document with a note whose text contains a word, and a
|
|
decoy document whose content (not notes) contains the same
|
|
word
|
|
WHEN:
|
|
- A bare "notes:" prefix query is run (notes declares "note"
|
|
as its default subpath)
|
|
THEN:
|
|
- Only the document whose note matches is returned; the
|
|
decoy's content match does not resurface through a demoted
|
|
text search
|
|
"""
|
|
alice = User.objects.create_user(username="alice")
|
|
with_note = Document.objects.create(
|
|
title="Has note",
|
|
content="x",
|
|
checksum="bare-notes-with",
|
|
)
|
|
Note.objects.create(document=with_note, user=alice, note="crocodile")
|
|
backend.add_or_update(with_note)
|
|
# This document's CONTENT contains the words a demoted text search
|
|
# would match; it must NOT match once the prefix addresses notes.
|
|
_index(
|
|
backend,
|
|
title="Notes about things",
|
|
content="notes crocodile mention",
|
|
checksum="bare-notes-decoy",
|
|
)
|
|
assert _matched_ids(backend, "notes:crocodile") == {with_note.pk}
|
|
|
|
def test_bare_custom_fields_prefix_searches_values(
|
|
self,
|
|
backend: TantivyBackend,
|
|
) -> None:
|
|
"""
|
|
GIVEN:
|
|
- A document with a custom field instance whose value
|
|
contains a word, and a decoy document whose content (not a
|
|
custom field value) contains the same word
|
|
WHEN:
|
|
- A bare "custom_fields:" prefix query is run (custom_fields
|
|
declares "value" as its default subpath)
|
|
THEN:
|
|
- Only the document whose custom field value matches is
|
|
returned
|
|
"""
|
|
field = CustomField.objects.create(
|
|
name="Policy Number",
|
|
data_type=CustomField.FieldDataType.STRING,
|
|
)
|
|
with_value = Document.objects.create(
|
|
title="Has field",
|
|
content="x",
|
|
checksum="bare-cf-with",
|
|
)
|
|
CustomFieldInstance.objects.create(
|
|
document=with_value,
|
|
field=field,
|
|
value_text="crocodile",
|
|
)
|
|
backend.add_or_update(with_value)
|
|
_index(
|
|
backend,
|
|
title="Custom things",
|
|
content="custom fields crocodile",
|
|
checksum="bare-cf-decoy",
|
|
)
|
|
assert _matched_ids(backend, "custom_fields:crocodile") == {with_value.pk}
|
|
|
|
def test_subpath_spellings_are_untouched(
|
|
self,
|
|
backend: TantivyBackend,
|
|
) -> None:
|
|
"""
|
|
GIVEN:
|
|
- A document with a note carrying both an author and note text
|
|
WHEN:
|
|
- The explicit subpath spellings "notes.user:" and
|
|
"notes.note:" are queried
|
|
THEN:
|
|
- Both resolve to their intended subpath and match the
|
|
document; the default-subpath resolution for the bare
|
|
prefix does not interfere with explicit subpath addressing
|
|
"""
|
|
bob = User.objects.create_user(username="bob")
|
|
doc = Document.objects.create(
|
|
title="Bob note",
|
|
content="x",
|
|
checksum="bare-subpath",
|
|
)
|
|
Note.objects.create(document=doc, user=bob, note="remark")
|
|
backend.add_or_update(doc)
|
|
assert _matched_ids(backend, "notes.user:bob") == {doc.pk}
|
|
assert _matched_ids(backend, "notes.note:remark") == {doc.pk}
|
|
|
|
|
|
class TestQuotedPhraseContainingNotesColonIsNotCorrupted:
|
|
"""The regex rewrite this migration removes was blind to quoting: it
|
|
matched "notes:" anywhere in the raw query string, including inside an
|
|
already-quoted phrase on an unrelated field, silently turning
|
|
content:"payment notes: none" into a notes-field search that matched
|
|
nothing. Resolving the default subpath during parsing (which is
|
|
quote-aware) fixes this."""
|
|
|
|
def test_quoted_phrase_with_notes_colon_matches_by_content(
|
|
self,
|
|
backend: TantivyBackend,
|
|
) -> None:
|
|
"""
|
|
GIVEN:
|
|
- A document whose content literally contains the text
|
|
"payment notes: none" inside a quoted phrase
|
|
WHEN:
|
|
- A query quoting that exact phrase against the content
|
|
field is run
|
|
THEN:
|
|
- It matches by content, rather than the "notes:" substring
|
|
inside the quotes being corrupted into a notes-field search
|
|
that matches nothing (the bug the deleted regex rewrite
|
|
caused, since it was blind to quoting)
|
|
"""
|
|
target = _index(
|
|
backend,
|
|
title="Statement",
|
|
content="payment notes: none",
|
|
checksum="quoted-phrase-notes-colon",
|
|
)
|
|
assert _matched_ids(
|
|
backend,
|
|
'content:"payment notes: none"',
|
|
) == {target.pk}
|
|
|
|
def test_quoted_phrase_matches_the_same_document_unquoted(
|
|
self,
|
|
backend: TantivyBackend,
|
|
) -> None:
|
|
"""
|
|
GIVEN:
|
|
- A document whose content contains the same words as the
|
|
previous test's phrase, but without the colon
|
|
WHEN:
|
|
- A query quoting that phrase against the content field is
|
|
run
|
|
THEN:
|
|
- It matches by content, proving the earlier fix is about
|
|
quote-awareness specifically, not about the words
|
|
themselves being unsearchable
|
|
"""
|
|
target = _index(
|
|
backend,
|
|
title="Statement",
|
|
content="payment notes none",
|
|
checksum="quoted-phrase-no-colon",
|
|
)
|
|
assert _matched_ids(
|
|
backend,
|
|
'content:"payment notes none"',
|
|
) == {target.pk}
|