Initial commit: Tasteby - YouTube restaurant map service
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>
This commit is contained in:
66
backend/api/routes/search.py
Normal file
66
backend/api/routes/search.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user