Files
webmail-client/backend/app/search/base.py
T

59 lines
1.6 KiB
Python
Raw Normal View History

2026-07-17 23:20:34 +02:00
# INTERFACE VERSION: 1
"""SearchBackend — the frozen search contract (SPEC §5.2).
Both backends (``pg.py`` for M0-M1, ``meili.py`` for M2) index the same
:class:`SearchDoc` and honour invariant I-7 (every query carries ``user_id``).
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Protocol
from uuid import UUID
@dataclass(frozen=True)
class SearchQuery:
text: str | None # free-text remainder after operator extraction
filters: dict[str, object] # parsed operators, keys per §8.2
user_id: UUID # ALWAYS set (invariant I-7)
@dataclass(frozen=True)
class SearchHit:
message_id: UUID
thread_id: UUID
score: float
highlights: dict[str, str] # field -> HTML-escaped snippet with <mark>
@dataclass(frozen=True)
class SearchDoc:
"""The document indexed by every backend (SPEC §5.2)."""
message_id: UUID
thread_id: UUID
user_id: UUID
account_id: UUID
mailbox_special_use: str | None
from_: list[dict[str, str]] # name+email
to: list[dict[str, str]]
cc: list[dict[str, str]]
subject: str
body_text: str
attachment_names: list[str] = field(default_factory=list)
has_attachments: bool = False
is_unread: bool = False
is_starred: bool = False
date: int = 0 # unix seconds
size_bytes: int = 0
class SearchBackend(Protocol):
async def index(self, doc: SearchDoc) -> None: ...
async def delete(self, message_id: UUID) -> None: ...
async def delete_user(self, user_id: UUID) -> None: ...
async def query(
self, q: SearchQuery, *, limit: int, cursor: str | None
) -> tuple[list[SearchHit], str | None]: ...