Backend (FastAPI + Oracle ADB), Frontend (Next.js), daemon worker. Features: channel/video/restaurant management, semantic search, Google OAuth, user reviews. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
67 lines
2.0 KiB
Python
67 lines
2.0 KiB
Python
"""Search API routes — keyword + semantic search."""
|
|
|
|
from fastapi import APIRouter, Query
|
|
|
|
from core import restaurant, vector
|
|
from core.db import conn
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("")
|
|
def search_restaurants(
|
|
q: str = Query(..., min_length=1),
|
|
mode: str = Query("keyword", pattern="^(keyword|semantic|hybrid)$"),
|
|
limit: int = Query(20, le=100),
|
|
):
|
|
"""Search restaurants by keyword, semantic similarity, or hybrid."""
|
|
if mode == "semantic":
|
|
return _semantic_search(q, limit)
|
|
elif mode == "hybrid":
|
|
kw = _keyword_search(q, limit)
|
|
sem = _semantic_search(q, limit)
|
|
# merge: keyword results first, then semantic results not already in keyword
|
|
seen = {r["id"] for r in kw}
|
|
merged = list(kw)
|
|
for r in sem:
|
|
if r["id"] not in seen:
|
|
merged.append(r)
|
|
seen.add(r["id"])
|
|
return merged[:limit]
|
|
else:
|
|
return _keyword_search(q, limit)
|
|
|
|
|
|
def _keyword_search(q: str, limit: int) -> list[dict]:
|
|
sql = """
|
|
SELECT id, name, address, region, latitude, longitude,
|
|
cuisine_type, price_range
|
|
FROM restaurants
|
|
WHERE latitude IS NOT NULL
|
|
AND (UPPER(name) LIKE UPPER(:q)
|
|
OR UPPER(address) LIKE UPPER(:q)
|
|
OR UPPER(region) LIKE UPPER(:q)
|
|
OR UPPER(cuisine_type) LIKE UPPER(:q))
|
|
FETCH FIRST :lim ROWS ONLY
|
|
"""
|
|
pattern = f"%{q}%"
|
|
with conn() as c:
|
|
cur = c.cursor()
|
|
cur.execute(sql, {"q": pattern, "lim": limit})
|
|
cols = [d[0].lower() for d in cur.description]
|
|
return [dict(zip(cols, row)) for row in cur.fetchall()]
|
|
|
|
|
|
def _semantic_search(q: str, limit: int) -> list[dict]:
|
|
similar = vector.search_similar(q, top_k=limit)
|
|
if not similar:
|
|
return []
|
|
|
|
rest_ids = list({s["restaurant_id"] for s in similar})
|
|
results = []
|
|
for rid in rest_ids[:limit]:
|
|
r = restaurant.get_by_id(rid)
|
|
if r and r.get("latitude"):
|
|
results.append(r)
|
|
return results
|