"""Search endpoint — POST /api/v1/search (Phase 3.2, master plan sec 9.1).""" from __future__ import annotations import logging import time from typing import Annotated from fastapi import APIRouter, Depends from sqlalchemy import text from sqlalchemy.orm import Session from app.core.db import get_db from app.schemas.search import SearchParams from app.schemas.search_response import SearchResponse, SearchResultItem from app.services.cache import get_search_cache from app.services.search_query import build_count_query, build_search_query logger = logging.getLogger(__name__) router = APIRouter() @router.post("/search", response_model=SearchResponse) async def search( params: SearchParams, db: Annotated[Session, Depends(get_db)], ) -> SearchResponse: """Поиск listings по фильтрам (cross-source matview + Redis 5min cache).""" started = time.monotonic() cache = get_search_cache() cache_key = cache.search_key(params.model_dump(mode="json")) cached = await cache.get(cache_key) if cached is not None: logger.info( "search cache HIT key=%s page=%d size=%d", cache_key[:24], params.page, params.page_size, ) return SearchResponse.model_validate(cached) sql, args = build_search_query(params) rows = db.execute(text(sql), args).mappings().all() count_sql, count_args = build_count_query(params) total = int(db.execute(text(count_sql), count_args).scalar() or 0) items = [SearchResultItem.model_validate(dict(r)) for r in rows] elapsed_ms = (time.monotonic() - started) * 1000.0 response = SearchResponse( items=items, total=total, page=params.page, page_size=params.page_size, elapsed_ms=round(elapsed_ms, 1), cache_hit=False, ) await cache.set(cache_key, response.model_dump(mode="json"), ttl=cache.TTL_SEARCH) logger.info( "search MISS page=%d size=%d total=%d items=%d elapsed=%.1fms", params.page, params.page_size, total, len(items), elapsed_ms, ) return response