59 lines
1.6 KiB
Python
59 lines
1.6 KiB
Python
# 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]: ...
|