""" FTS search over photos.search_vector with optional tag/date filters. """ import logging from typing import Optional from sqlalchemy import select, text from sqlalchemy.ext.asyncio import AsyncSession from app.models import Photo logger = logging.getLogger(__name__) async def hybrid_search( db: AsyncSession, q: Optional[str] = None, tag_ids: Optional[list[str]] = None, date_from: Optional[str] = None, date_to: Optional[str] = None, limit: int = 50, offset: int = 0, ) -> list[dict]: """Full-text search using photos.search_vector. No embeddings, no OCR.""" if q: try: fts_stmt = text(""" SELECT id, ts_rank(search_vector, plainto_tsquery('english', :q)) AS rank FROM photos WHERE search_vector @@ plainto_tsquery('english', :q) AND is_trashed = false AND is_hidden = false ORDER BY rank DESC LIMIT 500 """) rows = (await db.execute(fts_stmt, {"q": q})).fetchall() except Exception as e: logger.warning("FTS search failed: %s", e) rows = [] scored = [(pid, float(rank)) for pid, rank in rows] if tag_ids: from app.models.tags import photo_tags photo_ids = [pid for pid, _ in scored] if not photo_ids: return [] stmt = select(photo_tags.c.photo_id).where( photo_tags.c.photo_id.in_(photo_ids), photo_tags.c.tag_id.in_(tag_ids), ).distinct() valid = {row[0] for row in (await db.execute(stmt)).fetchall()} scored = [(pid, s) for pid, s in scored if pid in valid] page = scored[offset : offset + limit] return [{"photo_id": pid, "score": s} for pid, s in page] # No text query — recent photos with tag/date filters. if tag_ids: from app.models.tags import photo_tags sub = select(photo_tags.c.photo_id).where( photo_tags.c.tag_id.in_(tag_ids) ).distinct().subquery() stmt = select(Photo.id).join(sub, Photo.id == sub.c.photo_id) else: stmt = select(Photo.id) stmt = stmt.where( Photo.is_discarded.is_(False), Photo.is_hidden.is_(False), ) if date_from: stmt = stmt.where(Photo.taken_at >= date_from) if date_to: stmt = stmt.where(Photo.taken_at <= date_to) stmt = stmt.order_by(Photo.added_at.desc()).offset(offset).limit(limit) rows = (await db.execute(stmt)).fetchall() return [{"photo_id": row[0], "score": 0.0} for row in rows]