Added search endpoints

This commit is contained in:
Cal Corum 2025-10-08 14:21:44 -05:00
parent b20d0cdf88
commit db0635b01d
2 changed files with 160 additions and 1 deletions

View File

@ -1,4 +1,4 @@
from fastapi import APIRouter, Depends, HTTPException, Response from fastapi import APIRouter, Depends, HTTPException, Response, Query
from typing import Optional from typing import Optional
import logging import logging
import pydantic import pydantic
@ -84,6 +84,79 @@ async def get_cardsets(
return return_val return return_val
@router.get('/search')
async def search_cardsets(
q: str = Query(..., description="Search query for cardset name"),
in_packs: Optional[bool] = None,
ranked_legal: Optional[bool] = None,
event_id: Optional[int] = None,
limit: int = Query(default=25, ge=1, le=100, description="Maximum number of results to return")):
"""
Real-time fuzzy search for cardsets by name.
Returns cardsets matching the query with exact matches prioritized over partial matches.
"""
# Start with all cardsets
all_cardsets = Cardset.select()
# Apply name filter (partial match)
all_cardsets = all_cardsets.where(fn.Lower(Cardset.name).contains(q.lower()))
# Apply optional filters
if in_packs is not None:
all_cardsets = all_cardsets.where(Cardset.in_packs == in_packs)
if ranked_legal is not None:
all_cardsets = all_cardsets.where(Cardset.ranked_legal == ranked_legal)
if event_id is not None:
try:
this_event = Event.get_by_id(event_id)
all_cardsets = all_cardsets.where(Cardset.event == this_event)
except Exception as e:
logging.error(f'Failed to find event {event_id}: {e}')
db.close()
raise HTTPException(status_code=404, detail=f'Event id {event_id} not found')
# Convert to list for sorting
cardsets_list = list(all_cardsets)
# Sort by relevance (exact matches first, then name starts, then partial)
query_lower = q.lower()
exact_matches = []
name_start_matches = []
partial_matches = []
for cardset in cardsets_list:
name_lower = cardset.name.lower()
if name_lower == query_lower:
exact_matches.append(cardset)
else:
# Check if query matches the start of any word in name
name_parts = name_lower.split()
starts_with_match = any(part.startswith(query_lower) for part in name_parts)
if starts_with_match:
name_start_matches.append(cardset)
elif query_lower in name_lower:
partial_matches.append(cardset)
# Combine and limit results (exact, then name starts, then partial)
results = exact_matches + name_start_matches + partial_matches
total_matches = len(results)
limited_results = results[:limit]
# Build response
return_val = {
'count': len(limited_results),
'total_matches': total_matches,
'cardsets': [model_to_dict(x) for x in limited_results]
}
db.close()
return return_val
@router.get('/{cardset_id}') @router.get('/{cardset_id}')
async def get_one_cardset(cardset_id, csv: Optional[bool] = False): async def get_one_cardset(cardset_id, csv: Optional[bool] = False):
try: try:

View File

@ -331,6 +331,92 @@ async def get_random_player(
return return_val return return_val
@router.get('/search')
async def search_players(
q: str = Query(..., description="Search query for player name"),
cardset_id: list = Query(default=None),
rarity_id: list = Query(default=None),
limit: int = Query(default=25, ge=1, le=100, description="Maximum number of results to return"),
unique_names: bool = Query(default=False, description="Return only unique player names (highest player_id)"),
short_output: bool = False):
"""
Real-time fuzzy search for players by name.
Returns players matching the query with exact matches prioritized over partial matches.
When unique_names=True, only returns one player per unique name (the one with highest player_id).
"""
# Start with all players
all_players = Player.select()
# Apply name filter (partial match)
all_players = all_players.where(fn.Lower(Player.p_name).contains(q.lower()))
# Apply optional filters
if cardset_id is not None:
all_players = all_players.where(Player.cardset_id << cardset_id)
if rarity_id is not None:
all_players = all_players.where(Player.rarity_id << rarity_id)
# Convert to list for sorting
players_list = list(all_players)
# Sort by relevance (exact matches first, then name starts, then partial)
query_lower = q.lower()
exact_matches = []
name_start_matches = []
partial_matches = []
for player in players_list:
name_lower = player.p_name.lower()
if name_lower == query_lower:
exact_matches.append(player)
else:
# Check if query matches the start of first or last name
name_parts = name_lower.split()
starts_with_match = any(part.startswith(query_lower) for part in name_parts)
if starts_with_match:
name_start_matches.append(player)
elif query_lower in name_lower:
partial_matches.append(player)
# Combine results (exact, then name starts, then partial)
results = exact_matches + name_start_matches + partial_matches
# Deduplicate by name if requested (keeping highest player_id)
if unique_names:
seen_names = {}
for player in results:
name_lower = player.p_name.lower()
if name_lower not in seen_names or player.player_id > seen_names[name_lower].player_id:
seen_names[name_lower] = player
results = list(seen_names.values())
total_matches = len(results)
limited_results = results[:limit]
# Build response
return_val = {
'count': len(limited_results),
'total_matches': total_matches,
'players': []
}
for x in limited_results:
this_record = model_to_dict(x, recurse=not short_output)
# this_dex = Paperdex.select().where(Paperdex.player == x)
# this_record['paperdex'] = {'count': this_dex.count(), 'paperdex': []}
# for y in this_dex:
# this_record['paperdex']['paperdex'].append(model_to_dict(y, recurse=False))
return_val['players'].append(this_record)
db.close()
return return_val
@router.get('/{player_id}') @router.get('/{player_id}')
async def get_one_player(player_id, csv: Optional[bool] = False): async def get_one_player(player_id, csv: Optional[bool] = False):
try: try: