65 lines
2.0 KiB
Python
65 lines
2.0 KiB
Python
"""Search endpoint (SPEC §9.1 Search, §8). Results grouped by thread (FR-28)."""
|
|||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import uuid
|
||
|
|
from typing import Annotated
|
||
|
|
|
||
|
|
from fastapi import APIRouter, Depends, Query
|
||
|
|
from sqlalchemy import select
|
||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
|
|
||
|
|
from app.api.schemas import SearchOut, SearchThreadHit
|
||
|
|
from app.api.threads import _summary
|
||
|
|
from app.db import get_session
|
||
|
|
from app.identity.deps import CurrentUser
|
||
|
|
from app.models import Thread
|
||
|
|
from app.search.factory import get_search_backend
|
||
|
|
from app.search.queryparser import to_search_query
|
||
|
|
|
||
|
|
router = APIRouter(prefix="/search", tags=["search"])
|
||
|
|
|
||
|
|
DbSession = Annotated[AsyncSession, Depends(get_session)]
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("", response_model=SearchOut)
|
||
|
|
async def search(
|
||
|
|
user: CurrentUser,
|
||
|
|
db: DbSession,
|
||
|
|
q: str = Query(""),
|
||
|
|
cursor: str | None = None,
|
||
|
|
limit: int = Query(50, ge=1, le=200),
|
||
|
|
) -> SearchOut:
|
||
|
|
if not q.strip():
|
||
|
|
return SearchOut(threads=[], cursor=None)
|
||
|
|
|
||
|
|
backend = get_search_backend()
|
||
|
|
query = to_search_query(q, user.id)
|
||
|
|
hits, next_cursor = await backend.query(query, limit=limit, cursor=cursor)
|
||
|
|
|
||
|
|
# Group hits by thread, preserving order and keeping the first highlight.
|
||
|
|
order: list[uuid.UUID] = []
|
||
|
|
highlights: dict[uuid.UUID, dict[str, str]] = {}
|
||
|
|
for hit in hits:
|
||
|
|
if hit.thread_id not in highlights:
|
||
|
|
order.append(hit.thread_id)
|
||
|
|
highlights[hit.thread_id] = hit.highlights
|
||
|
|
elif hit.highlights and not highlights[hit.thread_id]:
|
||
|
|
highlights[hit.thread_id] = hit.highlights
|
||
|
|
|
||
|
|
if not order:
|
||
|
|
return SearchOut(threads=[], cursor=next_cursor)
|
||
|
|
|
||
|
|
threads = {
|
||
|
|
t.id: t
|
||
|
|
for t in await db.scalars(
|
||
|
|
select(Thread).where(Thread.id.in_(order), Thread.user_id == user.id)
|
||
|
|
)
|
||
|
|
}
|
||
|
|
results = [
|
||
|
|
SearchThreadHit(thread=_summary(threads[tid]), highlights=highlights.get(tid, {}))
|
||
|
|
for tid in order
|
||
|
|
if tid in threads
|
||
|
|
]
|
||
|
|
return SearchOut(threads=results, cursor=next_cursor)
|