""" Search API router — unified hybrid search endpoint. """ from typing import Optional from fastapi import APIRouter, Depends from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from app.database import get_db from app.models import Photo from app.services.search import hybrid_search from app.models.user import User from app.dependencies import get_current_user router = APIRouter() class SearchRequest(BaseModel): q: Optional[str] = None filters: Optional[dict] = None limit: int = 50 offset: int = 0 @router.post("") async def search_photos(body: SearchRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)): """FTS search over photo metadata with optional tag and date filters.""" filters = body.filters or {} results = await hybrid_search( db=db, q=body.q, tag_ids=filters.get("tag_ids"), date_from=filters.get("date_from"), date_to=filters.get("date_to"), limit=body.limit, offset=body.offset, ) if not results: return {"results": [], "total": 0} # Hydrate with photo data photo_ids = [r["photo_id"] for r in results] stmt = select(Photo).where(Photo.id.in_(photo_ids), Photo.user_id == current_user.id) rows = (await db.execute(stmt)).scalars().all() photo_map = {p.id: p for p in rows} hydrated = [] for r in results: photo = photo_map.get(r["photo_id"]) if not photo: continue hydrated.append({ "id": photo.id, "filename": photo.filename, "filepath": photo.filepath, "media_type": photo.media_type, "width": photo.width, "height": photo.height, "taken_at": photo.taken_at.isoformat() if photo.taken_at else None, "rating": photo.rating, "color_label": photo.color_label, "thumb_small": photo.thumb_small, "thumb_medium": photo.thumb_medium, "score": r["score"], }) return {"results": hydrated, "total": len(hydrated)}