"""Search provider abstraction (section 16). API clients and the rest of the service layer talk to `SearchProvider`, not to PostgreSQL directly, so the underlying engine can be swapped later (e.g. for an `OpenSearchProvider`) without touching callers. """ from __future__ import annotations import uuid from abc import ABC, abstractmethod from dataclasses import dataclass, field from sqlalchemy.ext.asyncio import AsyncSession @dataclass class SearchMatch: entity_id: uuid.UUID score: float @dataclass class SearchResultIDs: movies: list[SearchMatch] = field(default_factory=list) series: list[SearchMatch] = field(default_factory=list) people: list[SearchMatch] = field(default_factory=list) collections: list[SearchMatch] = field(default_factory=list) companies: list[SearchMatch] = field(default_factory=list) class SearchProvider(ABC): """Returns ranked Aurora entity IDs matching a query. Never returns fully-hydrated entities — that's the SearchService's job, so the provider stays a thin, swappable ranking layer.""" @abstractmethod async def search( self, session: AsyncSession, query: str, *, limit_per_type: int = 10 ) -> SearchResultIDs: ...