mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-08-20 01:33:22 +00:00
test(search): hoist deferred imports, add _index() helper in test_acceptance.py
User/DocumentType/StoragePath were imported inside individual test bodies despite the module already importing documents.models at top level - nothing here needed deferred import. Also add an _index() helper (Document.objects.create + backend.add_or_update in one call) for the many sites where nothing needs to happen between creating a document and indexing it; the two-step ceremony was outweighing the fixture data at every call site. Left as two explicit steps wherever a Note or CustomFieldInstance genuinely has to be attached before indexing.
This commit is contained in:
committed by
stumpylog
parent
e535e3b859
commit
f2f4d04ab6
@@ -14,11 +14,14 @@ from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
import time_machine
|
||||
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 DocumentType
|
||||
from documents.models import Note
|
||||
from documents.models import StoragePath
|
||||
from documents.search._query import parse_user_query
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -33,37 +36,48 @@ def _matched_ids(backend: TantivyBackend, query: str) -> set[int]:
|
||||
return set(backend.search_ids(query, user=None))
|
||||
|
||||
|
||||
def _index(backend: TantivyBackend, **kwargs: object) -> Document:
|
||||
"""Create a Document and index it in one step, for the common case
|
||||
where nothing needs to happen between the two (no related Note/
|
||||
CustomFieldInstance to attach first)."""
|
||||
doc = Document.objects.create(**kwargs)
|
||||
backend.add_or_update(doc)
|
||||
return doc
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def indexed_documents(backend: TantivyBackend) -> dict[str, int]:
|
||||
"""Index a small fixture set, return {label: doc_id} for corpus queries."""
|
||||
docs = {
|
||||
"invoice_2020": Document.objects.create(
|
||||
"invoice_2020": _index(
|
||||
backend,
|
||||
title="Invoice 2020",
|
||||
content="invoice total due",
|
||||
checksum="acc-invoice-2020",
|
||||
archive_serial_number=100,
|
||||
),
|
||||
"invoice_2021": Document.objects.create(
|
||||
"invoice_2021": _index(
|
||||
backend,
|
||||
title="Invoice 2021",
|
||||
content="invoice total due",
|
||||
checksum="acc-invoice-2021",
|
||||
archive_serial_number=101,
|
||||
),
|
||||
"invoice_2023": Document.objects.create(
|
||||
"invoice_2023": _index(
|
||||
backend,
|
||||
title="Invoice 2023",
|
||||
content="invoice total due",
|
||||
checksum="acc-invoice-2023",
|
||||
archive_serial_number=102,
|
||||
),
|
||||
"receipt_2022": Document.objects.create(
|
||||
"receipt_2022": _index(
|
||||
backend,
|
||||
title="Receipt 2022",
|
||||
content="receipt total due",
|
||||
checksum="acc-receipt-2022",
|
||||
archive_serial_number=103,
|
||||
),
|
||||
}
|
||||
for doc in docs.values():
|
||||
backend.add_or_update(doc)
|
||||
return {label: doc.pk for label, doc in docs.items()}
|
||||
|
||||
|
||||
@@ -99,18 +113,18 @@ class TestFieldBoosts:
|
||||
self,
|
||||
backend: TantivyBackend,
|
||||
) -> None:
|
||||
title_match = Document.objects.create(
|
||||
title_match = _index(
|
||||
backend,
|
||||
title="urgent",
|
||||
content="nothing else relevant",
|
||||
checksum="acc-boost-title",
|
||||
)
|
||||
content_match = Document.objects.create(
|
||||
_index(
|
||||
backend,
|
||||
title="nothing",
|
||||
content="urgent matter here",
|
||||
checksum="acc-boost-content",
|
||||
)
|
||||
backend.add_or_update(title_match)
|
||||
backend.add_or_update(content_match)
|
||||
query = parse_user_query(backend._index, "urgent", UTC)
|
||||
searcher = backend._index.searcher()
|
||||
results = searcher.search(query, limit=10)
|
||||
@@ -125,8 +139,6 @@ class TestJsonSubpaths:
|
||||
self,
|
||||
backend: TantivyBackend,
|
||||
) -> None:
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
alice = User.objects.create_user(username="alice")
|
||||
doc_with_note = Document.objects.create(
|
||||
title="Has note",
|
||||
@@ -134,13 +146,8 @@ class TestJsonSubpaths:
|
||||
checksum="acc-note-with",
|
||||
)
|
||||
Note.objects.create(document=doc_with_note, user=alice, note="reminder")
|
||||
doc_without = Document.objects.create(
|
||||
title="No note",
|
||||
content="x",
|
||||
checksum="acc-note-without",
|
||||
)
|
||||
backend.add_or_update(doc_with_note)
|
||||
backend.add_or_update(doc_without)
|
||||
_index(backend, title="No note", content="x", checksum="acc-note-without")
|
||||
matched = _matched_ids(backend, "notes.user:alice")
|
||||
assert matched == {doc_with_note.pk}
|
||||
|
||||
@@ -166,6 +173,7 @@ class TestJsonSubpaths:
|
||||
field=field,
|
||||
value_text="policy",
|
||||
)
|
||||
backend.add_or_update(matching)
|
||||
non_matching = Document.objects.create(
|
||||
title="Non-matching",
|
||||
content="x",
|
||||
@@ -176,7 +184,6 @@ class TestJsonSubpaths:
|
||||
field=other_field,
|
||||
value_text="policy",
|
||||
)
|
||||
backend.add_or_update(matching)
|
||||
backend.add_or_update(non_matching)
|
||||
matched = _matched_ids(
|
||||
backend,
|
||||
@@ -215,13 +222,13 @@ class TestFuzzyBlendSurvivesWhooshGrammar:
|
||||
) -> None:
|
||||
settings.ADVANCED_FUZZY_SEARCH_THRESHOLD = 0.5
|
||||
with time_machine.travel(FROZEN_NOW, tick=False):
|
||||
doc = Document.objects.create(
|
||||
doc = _index(
|
||||
backend,
|
||||
title="Receipt March",
|
||||
content="receipt total due",
|
||||
checksum="fuzzy-blend-1",
|
||||
archive_serial_number=900,
|
||||
)
|
||||
backend.add_or_update(doc)
|
||||
# Sanity: the exact spelling matches through the exact clause.
|
||||
assert doc.pk in _matched_ids(backend, "added:today receipt")
|
||||
# The regression: the misspelling (one transposition) only
|
||||
@@ -249,13 +256,13 @@ class TestFuzzyBlendSurvivesWhooshGrammar:
|
||||
# it even for a naive implementation.)
|
||||
settings.ADVANCED_FUZZY_SEARCH_THRESHOLD = 0.5
|
||||
with time_machine.travel(FROZEN_NOW, tick=False):
|
||||
receipt_only = Document.objects.create(
|
||||
_index(
|
||||
backend,
|
||||
title="Receipt Archive",
|
||||
content="receipt archived stack",
|
||||
checksum="fuzzy-blend-2",
|
||||
archive_serial_number=901,
|
||||
)
|
||||
backend.add_or_update(receipt_only)
|
||||
assert _matched_ids(backend, "added:today total NOT receipt") == set()
|
||||
|
||||
|
||||
@@ -269,22 +276,22 @@ class TestUnquotedDateKeywordPhrases:
|
||||
@pytest.fixture
|
||||
def period_documents(self, backend: TantivyBackend) -> dict[str, int]:
|
||||
with time_machine.travel(FROZEN_NOW, tick=False):
|
||||
in_may = Document.objects.create(
|
||||
in_may = _index(
|
||||
backend,
|
||||
title="May Doc",
|
||||
content="statement",
|
||||
checksum="kw-may",
|
||||
archive_serial_number=910,
|
||||
added=datetime(2026, 5, 20, 12, 0, tzinfo=UTC),
|
||||
)
|
||||
in_june = Document.objects.create(
|
||||
in_june = _index(
|
||||
backend,
|
||||
title="June Doc",
|
||||
content="statement",
|
||||
checksum="kw-june",
|
||||
archive_serial_number=911,
|
||||
added=datetime(2026, 6, 10, 12, 0, tzinfo=UTC),
|
||||
)
|
||||
for doc in (in_may, in_june):
|
||||
backend.add_or_update(doc)
|
||||
return {"in_may": in_may.pk, "in_june": in_june.pk}
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -337,13 +344,13 @@ class TestUnquotedDateKeywordPhrases:
|
||||
# text, not a date phrase: a title actually containing the words
|
||||
# matches, and the date-window documents do not.
|
||||
with time_machine.travel(FROZEN_NOW, tick=False):
|
||||
wordy = Document.objects.create(
|
||||
wordy = _index(
|
||||
backend,
|
||||
title="Notes from the previous month",
|
||||
content="meeting notes",
|
||||
checksum="kw-text",
|
||||
archive_serial_number=912,
|
||||
)
|
||||
backend.add_or_update(wordy)
|
||||
assert _matched_ids(backend, "title:previous month") == {wordy.pk}
|
||||
|
||||
|
||||
@@ -357,8 +364,6 @@ class TestBareJsonFieldPrefixes:
|
||||
self,
|
||||
backend: TantivyBackend,
|
||||
) -> None:
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
alice = User.objects.create_user(username="alice")
|
||||
with_note = Document.objects.create(
|
||||
title="Has note",
|
||||
@@ -366,15 +371,15 @@ class TestBareJsonFieldPrefixes:
|
||||
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.
|
||||
decoy = Document.objects.create(
|
||||
_index(
|
||||
backend,
|
||||
title="Notes about things",
|
||||
content="notes crocodile mention",
|
||||
checksum="bare-notes-decoy",
|
||||
)
|
||||
backend.add_or_update(with_note)
|
||||
backend.add_or_update(decoy)
|
||||
assert _matched_ids(backend, "notes:crocodile") == {with_note.pk}
|
||||
|
||||
def test_bare_custom_fields_prefix_searches_values(
|
||||
@@ -395,21 +400,19 @@ class TestBareJsonFieldPrefixes:
|
||||
field=field,
|
||||
value_text="crocodile",
|
||||
)
|
||||
decoy = Document.objects.create(
|
||||
backend.add_or_update(with_value)
|
||||
_index(
|
||||
backend,
|
||||
title="Custom things",
|
||||
content="custom fields crocodile",
|
||||
checksum="bare-cf-decoy",
|
||||
)
|
||||
backend.add_or_update(with_value)
|
||||
backend.add_or_update(decoy)
|
||||
assert _matched_ids(backend, "custom_fields:crocodile") == {with_value.pk}
|
||||
|
||||
def test_subpath_spellings_are_untouched(
|
||||
self,
|
||||
backend: TantivyBackend,
|
||||
) -> None:
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
bob = User.objects.create_user(username="bob")
|
||||
doc = Document.objects.create(
|
||||
title="Bob note",
|
||||
@@ -431,8 +434,6 @@ class TestFieldAliases:
|
||||
self,
|
||||
backend: TantivyBackend,
|
||||
) -> None:
|
||||
from documents.models import DocumentType
|
||||
|
||||
invoice_type = DocumentType.objects.create(name="invoice")
|
||||
# Discriminating shape: document_type is itself a default search
|
||||
# field, so if alias resolution ever broke and "type:invoice"
|
||||
@@ -441,19 +442,19 @@ class TestFieldAliases:
|
||||
# word in content, so a demoted search matches BOTH documents and
|
||||
# the exact-set assertions fail. (The title avoids stemming to
|
||||
# "type": english stems Typed -> type.)
|
||||
typed = Document.objects.create(
|
||||
typed = _index(
|
||||
backend,
|
||||
title="First",
|
||||
content="quarterly statement",
|
||||
checksum="alias-type-1",
|
||||
document_type=invoice_type,
|
||||
)
|
||||
decoy = Document.objects.create(
|
||||
_index(
|
||||
backend,
|
||||
title="Second",
|
||||
content="invoice mentioned in body",
|
||||
checksum="alias-type-2",
|
||||
)
|
||||
backend.add_or_update(typed)
|
||||
backend.add_or_update(decoy)
|
||||
assert _matched_ids(backend, "type:invoice") == {typed.pk}
|
||||
assert _matched_ids(backend, "document_type:invoice") == {typed.pk}
|
||||
|
||||
@@ -461,10 +462,9 @@ class TestFieldAliases:
|
||||
self,
|
||||
backend: TantivyBackend,
|
||||
) -> None:
|
||||
from documents.models import StoragePath
|
||||
|
||||
archive = StoragePath.objects.create(name="archive", path="archive/{title}")
|
||||
stored = Document.objects.create(
|
||||
stored = _index(
|
||||
backend,
|
||||
title="Stored",
|
||||
content="quarterly statement",
|
||||
checksum="alias-path-1",
|
||||
@@ -473,12 +473,11 @@ class TestFieldAliases:
|
||||
# storage_path is NOT a default search field today, so a demoted
|
||||
# "path:archive" already matches nothing; the content decoy keeps
|
||||
# this test discriminating even if it ever joins the defaults.
|
||||
loose = Document.objects.create(
|
||||
_index(
|
||||
backend,
|
||||
title="Loose",
|
||||
content="archive mentioned in body",
|
||||
checksum="alias-path-2",
|
||||
)
|
||||
backend.add_or_update(stored)
|
||||
backend.add_or_update(loose)
|
||||
assert _matched_ids(backend, "path:archive") == {stored.pk}
|
||||
assert _matched_ids(backend, "storage_path:archive") == {stored.pk}
|
||||
|
||||
Reference in New Issue
Block a user