from fastapi import APIRouter, Depends, Query from sqlalchemy import func, or_, select, text from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.models import Bill, Member from app.schemas.schemas import BillSchema, MemberSchema router = APIRouter() @router.get("") async def search( q: str = Query(..., min_length=2, max_length=500), db: AsyncSession = Depends(get_db), ): # Bill ID direct match id_results = await db.execute( select(Bill).where(Bill.bill_id.ilike(f"%{q}%")).limit(20) ) id_bills = id_results.scalars().all() # Full-text search on title/content via tsvector fts_results = await db.execute( select(Bill) .where(text("search_vector @@ plainto_tsquery('english', :q)")) .order_by(text("ts_rank(search_vector, plainto_tsquery('english', :q)) DESC")) .limit(20) .params(q=q) ) fts_bills = fts_results.scalars().all() # Merge, dedup, preserve order (ID matches first) seen = set() bills = [] for b in id_bills + fts_bills: if b.bill_id not in seen: seen.add(b.bill_id) bills.append(b) # Fuzzy member search — matches "Last, First" and "First Last" first_last = func.concat( func.split_part(Member.name, ", ", 2), " ", func.split_part(Member.name, ", ", 1), ) member_results = await db.execute( select(Member) .where(or_( Member.name.ilike(f"%{q}%"), first_last.ilike(f"%{q}%"), )) .order_by(Member.last_name) .limit(10) ) members = member_results.scalars().all() return { "bills": [BillSchema.model_validate(b) for b in bills], "members": [MemberSchema.model_validate(m) for m in members], }