gendesign/tradein-mvp/backend/app/api/v1/search.py
bot-backend cdf493f345
All checks were successful
Deploy / changes (push) Successful in 9s
Deploy Trade-In / changes (push) Successful in 13s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy / build-backend (push) Successful in 2m23s
Deploy Trade-In / test (push) Successful in 3m56s
Deploy / build-worker (push) Successful in 4m16s
Deploy Trade-In / build-backend (push) Successful in 1m19s
Deploy / deploy (push) Successful in 1m49s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 12s
Deploy Trade-In / deploy (push) Successful in 2m25s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
chore(format): нормализация под ruff 0.15.20 — 161 файл, только формат (#2864) (#3022)
2026-08-21 12:01:52 +00:00

84 lines
2.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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()
def _map_row(row: object) -> dict[str, object]:
"""Маппит строку matview в поля SearchResultItem.
SQL отдаёт колонку `cadastral_number`, а в схеме поле зовётся
`kadastr_num` — без явного маппинга значение молча теряется (#1514).
"""
data = dict(row)
if "cadastral_number" in data:
data["kadastr_num"] = data.pop("cadastral_number")
return data
@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,
)
resp = SearchResponse.model_validate(cached)
resp.cache_hit = True
return resp
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(_map_row(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