mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-08-19 17:23:21 +00:00
feat(search): add shared PUBLIC_FIELDS table
Create the shared field-definition table consumed by the schema builder (_schema.py) and the whoosh-compat field registry (_registry.py). This eliminates drift between what the index exposes and what queries can address. - Create PublicField frozen dataclass with field metadata - Define PUBLIC_FIELDS tuple with 16 searchable fields - Add comprehensive test suite covering field properties The whoosh-compat pyproject.toml dependency addition is added in a follow-up commit, with the correct [tantivy] extra, source comment, and a matching uv.lock update. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
0042b0c4f7
commit
876db6d744
@@ -0,0 +1,45 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from whoosh_compat import FieldKind
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PublicField:
|
||||
"""One query-syntax-addressable field, shared by the Tantivy schema
|
||||
builder (_schema.py) and the whoosh-compat FieldRegistry (_registry.py).
|
||||
|
||||
Internal-only schema fields with no query-syntax meaning of their own
|
||||
(sort shadow fields, bigram CJK fields, simple_title/simple_content,
|
||||
autocomplete_word, notes_text) are NOT represented here — they stay
|
||||
hardcoded in _schema.py's build_schema().
|
||||
"""
|
||||
|
||||
name: str
|
||||
kind: FieldKind
|
||||
aliases: tuple[str, ...] = ()
|
||||
comma_values: bool = False
|
||||
date_only: bool = False
|
||||
fast: bool = False
|
||||
subpaths: tuple[str, ...] = () # JSON kind only
|
||||
|
||||
|
||||
PUBLIC_FIELDS: tuple[PublicField, ...] = (
|
||||
PublicField("title", FieldKind.TEXT),
|
||||
PublicField("content", FieldKind.TEXT),
|
||||
PublicField("correspondent", FieldKind.TEXT),
|
||||
PublicField("document_type", FieldKind.TEXT, aliases=("type",)),
|
||||
PublicField("storage_path", FieldKind.TEXT, aliases=("path",)),
|
||||
PublicField("original_filename", FieldKind.TEXT),
|
||||
PublicField("tag", FieldKind.TEXT, comma_values=True),
|
||||
PublicField("checksum", FieldKind.KEYWORD),
|
||||
PublicField("asn", FieldKind.U64, fast=True),
|
||||
PublicField("page_count", FieldKind.U64, fast=True),
|
||||
PublicField("num_notes", FieldKind.U64, fast=True),
|
||||
PublicField("created", FieldKind.DATE, date_only=True, fast=True),
|
||||
PublicField("modified", FieldKind.DATETIME, fast=True),
|
||||
PublicField("added", FieldKind.DATETIME, fast=True),
|
||||
PublicField("notes", FieldKind.JSON, subpaths=("user", "note")),
|
||||
PublicField("custom_fields", FieldKind.JSON, subpaths=("name", "value")),
|
||||
)
|
||||
@@ -0,0 +1,49 @@
|
||||
from whoosh_compat import FieldKind
|
||||
|
||||
from documents.search._fields import PUBLIC_FIELDS
|
||||
|
||||
|
||||
class TestPublicFields:
|
||||
def test_every_field_has_a_whoosh_compat_kind(self) -> None:
|
||||
for field in PUBLIC_FIELDS:
|
||||
assert isinstance(field.kind, FieldKind)
|
||||
|
||||
def test_names_are_unique(self) -> None:
|
||||
names = [f.name for f in PUBLIC_FIELDS]
|
||||
assert len(names) == len(set(names))
|
||||
|
||||
def test_json_fields_have_subpaths(self) -> None:
|
||||
for field in PUBLIC_FIELDS:
|
||||
if field.kind is FieldKind.JSON:
|
||||
assert field.subpaths, f"{field.name} is JSON but has no subpaths"
|
||||
|
||||
def test_non_json_fields_have_no_subpaths(self) -> None:
|
||||
for field in PUBLIC_FIELDS:
|
||||
if field.kind is not FieldKind.JSON:
|
||||
assert field.subpaths == ()
|
||||
|
||||
def test_document_type_alias_is_type(self) -> None:
|
||||
field = next(f for f in PUBLIC_FIELDS if f.name == "document_type")
|
||||
assert field.aliases == ("type",)
|
||||
|
||||
def test_storage_path_alias_is_path(self) -> None:
|
||||
field = next(f for f in PUBLIC_FIELDS if f.name == "storage_path")
|
||||
assert field.aliases == ("path",)
|
||||
|
||||
def test_tag_allows_comma_values(self) -> None:
|
||||
field = next(f for f in PUBLIC_FIELDS if f.name == "tag")
|
||||
assert field.comma_values is True
|
||||
|
||||
def test_notes_subpaths(self) -> None:
|
||||
field = next(f for f in PUBLIC_FIELDS if f.name == "notes")
|
||||
assert field.subpaths == ("user", "note")
|
||||
|
||||
def test_custom_fields_subpaths(self) -> None:
|
||||
field = next(f for f in PUBLIC_FIELDS if f.name == "custom_fields")
|
||||
assert field.subpaths == ("name", "value")
|
||||
|
||||
def test_no_internal_id_fields_present(self) -> None:
|
||||
# tag_id/owner_id/viewer_id/etc. are permission-filter-only fields,
|
||||
# never user-query-addressable (see design spec, "Field surface").
|
||||
names = {f.name for f in PUBLIC_FIELDS}
|
||||
assert not any(name.endswith("_id") for name in names)
|
||||
Reference in New Issue
Block a user