feat: Added sort query parameter (relevance/newest/oldest/alpha/creator…
- "backend/routers/search.py" - "backend/routers/topics.py" - "backend/routers/techniques.py" - "backend/search_service.py" GSD-Task: S02/T01
This commit is contained in:
parent
c1cdba14f2
commit
250d7315af
4 changed files with 80 additions and 6 deletions
|
|
@ -34,6 +34,7 @@ def _get_search_service() -> SearchService:
|
|||
async def search(
|
||||
q: Annotated[str, Query(max_length=500)] = "",
|
||||
scope: Annotated[str, Query()] = "all",
|
||||
sort: Annotated[str, Query()] = "relevance",
|
||||
limit: Annotated[int, Query(ge=1, le=100)] = 20,
|
||||
db: AsyncSession = Depends(get_session),
|
||||
) -> SearchResponse:
|
||||
|
|
@ -44,7 +45,7 @@ async def search(
|
|||
- **limit**: Max results (1–100, default 20).
|
||||
"""
|
||||
svc = _get_search_service()
|
||||
result = await svc.search(query=q, scope=scope, limit=limit, db=db)
|
||||
result = await svc.search(query=q, scope=scope, sort=sort, limit=limit, db=db)
|
||||
return SearchResponse(
|
||||
items=[SearchResultItem(**item) for item in result["items"]],
|
||||
partial_matches=[SearchResultItem(**item) for item in result.get("partial_matches", [])],
|
||||
|
|
|
|||
|
|
@ -157,7 +157,17 @@ async def list_techniques(
|
|||
stmt = stmt.options(selectinload(TechniquePage.creator))
|
||||
if sort == "random":
|
||||
stmt = stmt.order_by(func.random())
|
||||
elif sort == "oldest":
|
||||
stmt = stmt.order_by(TechniquePage.created_at.asc())
|
||||
elif sort == "alpha":
|
||||
stmt = stmt.order_by(TechniquePage.title.asc())
|
||||
elif sort == "creator":
|
||||
# Need a join for creator name ordering; avoid duplicate join if creator_slug filter already joined
|
||||
if not creator_slug:
|
||||
stmt = stmt.join(Creator, TechniquePage.creator_id == Creator.id, isouter=True)
|
||||
stmt = stmt.order_by(Creator.name.asc(), TechniquePage.title.asc())
|
||||
else:
|
||||
# Default: "recent" — newest first
|
||||
stmt = stmt.order_by(TechniquePage.created_at.desc())
|
||||
stmt = stmt.offset(offset).limit(limit)
|
||||
result = await db.execute(stmt)
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ async def list_topics(
|
|||
async def get_subtopic_techniques(
|
||||
category_slug: str,
|
||||
subtopic_slug: str,
|
||||
sort: Annotated[str, Query()] = "alpha",
|
||||
offset: Annotated[int, Query(ge=0)] = 0,
|
||||
limit: Annotated[int, Query(ge=1, le=100)] = 50,
|
||||
db: AsyncSession = Depends(get_session),
|
||||
|
|
@ -132,10 +133,21 @@ async def get_subtopic_techniques(
|
|||
|
||||
stmt = (
|
||||
stmt.options(selectinload(TechniquePage.creator))
|
||||
.order_by(TechniquePage.title)
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
)
|
||||
|
||||
# Apply sort ordering
|
||||
if sort == "newest":
|
||||
stmt = stmt.order_by(TechniquePage.created_at.desc())
|
||||
elif sort == "oldest":
|
||||
stmt = stmt.order_by(TechniquePage.created_at.asc())
|
||||
elif sort == "creator":
|
||||
stmt = stmt.join(Creator, TechniquePage.creator_id == Creator.id, isouter=True)
|
||||
stmt = stmt.order_by(Creator.name.asc(), TechniquePage.title.asc())
|
||||
else:
|
||||
# Default: "alpha" — alphabetical by title
|
||||
stmt = stmt.order_by(TechniquePage.title.asc())
|
||||
|
||||
stmt = stmt.offset(offset).limit(limit)
|
||||
result = await db.execute(stmt)
|
||||
pages = result.scalars().all()
|
||||
|
||||
|
|
@ -158,6 +170,7 @@ async def get_subtopic_techniques(
|
|||
@router.get("/{category_slug}", response_model=PaginatedResponse)
|
||||
async def get_topic_techniques(
|
||||
category_slug: str,
|
||||
sort: Annotated[str, Query()] = "alpha",
|
||||
offset: Annotated[int, Query(ge=0)] = 0,
|
||||
limit: Annotated[int, Query(ge=1, le=100)] = 50,
|
||||
db: AsyncSession = Depends(get_session),
|
||||
|
|
@ -179,7 +192,21 @@ async def get_topic_techniques(
|
|||
count_result = await db.execute(count_stmt)
|
||||
total = count_result.scalar() or 0
|
||||
|
||||
stmt = stmt.options(selectinload(TechniquePage.creator)).order_by(TechniquePage.title).offset(offset).limit(limit)
|
||||
stmt = stmt.options(selectinload(TechniquePage.creator))
|
||||
|
||||
# Apply sort ordering
|
||||
if sort == "newest":
|
||||
stmt = stmt.order_by(TechniquePage.created_at.desc())
|
||||
elif sort == "oldest":
|
||||
stmt = stmt.order_by(TechniquePage.created_at.asc())
|
||||
elif sort == "creator":
|
||||
stmt = stmt.join(Creator, TechniquePage.creator_id == Creator.id, isouter=True)
|
||||
stmt = stmt.order_by(Creator.name.asc(), TechniquePage.title.asc())
|
||||
else:
|
||||
# Default: "alpha" — alphabetical by title
|
||||
stmt = stmt.order_by(TechniquePage.title.asc())
|
||||
|
||||
stmt = stmt.offset(offset).limit(limit)
|
||||
result = await db.execute(stmt)
|
||||
pages = result.scalars().all()
|
||||
|
||||
|
|
|
|||
|
|
@ -180,6 +180,7 @@ class SearchService:
|
|||
scope: str,
|
||||
limit: int,
|
||||
db: AsyncSession,
|
||||
sort: str = "relevance",
|
||||
) -> dict[str, list[dict[str, Any]]]:
|
||||
"""Multi-token AND keyword search across technique pages, key moments, and creators.
|
||||
|
||||
|
|
@ -238,6 +239,7 @@ class SearchService:
|
|||
"creator_id": str(tp.creator_id),
|
||||
"creator_name": cr.name,
|
||||
"creator_slug": cr.slug,
|
||||
"created_at": tp.created_at.isoformat() if tp.created_at else "",
|
||||
"score": 0.0,
|
||||
})
|
||||
|
||||
|
|
@ -263,6 +265,7 @@ class SearchService:
|
|||
"creator_id": str(cr.id),
|
||||
"creator_name": cr.name,
|
||||
"creator_slug": cr.slug,
|
||||
"created_at": km.created_at.isoformat() if hasattr(km, "created_at") and km.created_at else "",
|
||||
"score": 0.0,
|
||||
})
|
||||
|
||||
|
|
@ -283,6 +286,7 @@ class SearchService:
|
|||
"topic_category": "",
|
||||
"topic_tags": cr.genres or [],
|
||||
"creator_id": str(cr.id),
|
||||
"created_at": cr.created_at.isoformat() if hasattr(cr, "created_at") and cr.created_at else "",
|
||||
"score": 0.0,
|
||||
})
|
||||
|
||||
|
|
@ -365,6 +369,7 @@ class SearchService:
|
|||
scope: str,
|
||||
limit: int,
|
||||
db: AsyncSession,
|
||||
sort: str = "relevance",
|
||||
) -> dict[str, Any]:
|
||||
"""Run semantic search with keyword fallback.
|
||||
|
||||
|
|
@ -404,13 +409,16 @@ class SearchService:
|
|||
|
||||
# Fallback to keyword search if semantic failed or returned nothing
|
||||
if not items:
|
||||
kw_result = await self.keyword_search(query, scope, limit, db)
|
||||
kw_result = await self.keyword_search(query, scope, limit, db, sort=sort)
|
||||
items = kw_result["items"]
|
||||
partial_matches = kw_result.get("partial_matches", [])
|
||||
fallback_used = True
|
||||
else:
|
||||
partial_matches = []
|
||||
|
||||
# Apply sort to enriched results (semantic or keyword)
|
||||
items = self._apply_sort(items, sort)
|
||||
|
||||
elapsed_ms = (time.monotonic() - start) * 1000
|
||||
|
||||
logger.info(
|
||||
|
|
@ -431,6 +439,33 @@ class SearchService:
|
|||
"fallback_used": fallback_used,
|
||||
}
|
||||
|
||||
# ── Sort helpers ────────────────────────────────────────────────────
|
||||
|
||||
@staticmethod
|
||||
def _apply_sort(items: list[dict[str, Any]], sort: str) -> list[dict[str, Any]]:
|
||||
"""Sort enriched result dicts by the requested criterion.
|
||||
|
||||
For 'relevance' (default), preserve existing order (score-based from
|
||||
Qdrant or DB order from keyword search).
|
||||
"""
|
||||
if sort == "relevance" or not items:
|
||||
return items
|
||||
|
||||
if sort == "newest":
|
||||
# Sort by created_at descending; items without it go last
|
||||
return sorted(items, key=lambda r: r.get("created_at", ""), reverse=True)
|
||||
elif sort == "oldest":
|
||||
# Sort by created_at ascending; items without it go last
|
||||
return sorted(items, key=lambda r: r.get("created_at") or "9999", reverse=False)
|
||||
elif sort == "alpha":
|
||||
return sorted(items, key=lambda r: (r.get("title") or "").lower())
|
||||
elif sort == "creator":
|
||||
return sorted(
|
||||
items,
|
||||
key=lambda r: ((r.get("creator_name") or "").lower(), (r.get("title") or "").lower()),
|
||||
)
|
||||
return items
|
||||
|
||||
# ── Result enrichment ────────────────────────────────────────────────
|
||||
|
||||
async def _enrich_results(
|
||||
|
|
@ -490,6 +525,7 @@ class SearchService:
|
|||
"creator_id": cid,
|
||||
"creator_name": creator_info["name"],
|
||||
"creator_slug": creator_info["slug"],
|
||||
"created_at": payload.get("created_at", ""),
|
||||
"score": r.get("score", 0.0),
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue