UX improvements: mobile bottom sheet, cuisine taxonomy, search enhancements
- Add BottomSheet component for Google Maps-style restaurant detail on mobile (3-snap drag: 40%/55%/92%, velocity-based close, backdrop overlay) - Mobile map mode now full-screen with bottom sheet overlay for details - Collapsible filter panel on mobile with active filter badge count - Standardized cuisine taxonomy (46 categories: 한식|국밥, 일식|스시 etc.) with LLM remap endpoint and admin UI button - Enhanced search: keyword search now includes foods_mentioned + video title - Search results include channels array for frontend filtering - Channel filter moved to frontend filteredRestaurants (not API-level) - LLM extraction prompt updated for pipe-delimited region + cuisine taxonomy - Vector rebuild endpoint with rich JSON chunks per restaurant - Geolocation-based auto region selection on page load - Desktop filters split into two clean rows Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
107
backend/core/cache.py
Normal file
107
backend/core/cache.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""Redis cache layer — graceful fallback when Redis is unavailable."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import redis
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_client: redis.Redis | None = None
|
||||
_disabled = False
|
||||
|
||||
DEFAULT_TTL = 600 # 10 minutes
|
||||
|
||||
|
||||
def _get_client() -> redis.Redis | None:
|
||||
global _client, _disabled
|
||||
if _disabled:
|
||||
return None
|
||||
if _client is None:
|
||||
host = os.environ.get("REDIS_HOST", "192.168.0.147")
|
||||
port = int(os.environ.get("REDIS_PORT", "6379"))
|
||||
db = int(os.environ.get("REDIS_DB", "0"))
|
||||
try:
|
||||
_client = redis.Redis(
|
||||
host=host, port=port, db=db,
|
||||
socket_connect_timeout=2,
|
||||
socket_timeout=2,
|
||||
decode_responses=True,
|
||||
)
|
||||
_client.ping()
|
||||
logger.info("Redis connected: %s:%s/%s", host, port, db)
|
||||
except Exception as e:
|
||||
logger.warning("Redis unavailable (%s), caching disabled", e)
|
||||
_client = None
|
||||
_disabled = True
|
||||
return None
|
||||
return _client
|
||||
|
||||
|
||||
def make_key(*parts: Any) -> str:
|
||||
"""Build a cache key like 'tasteby:restaurants:cuisine=한식:limit=100'."""
|
||||
return "tasteby:" + ":".join(str(p) for p in parts if p is not None and p != "")
|
||||
|
||||
|
||||
def get(key: str) -> Any | None:
|
||||
"""Get cached value. Returns None on miss or error."""
|
||||
try:
|
||||
client = _get_client()
|
||||
if not client:
|
||||
return None
|
||||
val = client.get(key)
|
||||
if val is not None:
|
||||
return json.loads(val)
|
||||
except Exception as e:
|
||||
logger.debug("Cache get error: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
def set(key: str, value: Any, ttl: int = DEFAULT_TTL) -> None:
|
||||
"""Cache a value as JSON with TTL."""
|
||||
try:
|
||||
client = _get_client()
|
||||
if not client:
|
||||
return
|
||||
client.setex(key, ttl, json.dumps(value, ensure_ascii=False, default=str))
|
||||
except Exception as e:
|
||||
logger.debug("Cache set error: %s", e)
|
||||
|
||||
|
||||
def flush() -> None:
|
||||
"""Flush all tasteby cache keys."""
|
||||
try:
|
||||
client = _get_client()
|
||||
if not client:
|
||||
return
|
||||
cursor = 0
|
||||
while True:
|
||||
cursor, keys = client.scan(cursor, match="tasteby:*", count=200)
|
||||
if keys:
|
||||
client.delete(*keys)
|
||||
if cursor == 0:
|
||||
break
|
||||
logger.info("Cache flushed")
|
||||
except Exception as e:
|
||||
logger.debug("Cache flush error: %s", e)
|
||||
|
||||
|
||||
def invalidate_prefix(prefix: str) -> None:
|
||||
"""Delete all keys matching a prefix."""
|
||||
try:
|
||||
client = _get_client()
|
||||
if not client:
|
||||
return
|
||||
cursor = 0
|
||||
while True:
|
||||
cursor, keys = client.scan(cursor, match=f"{prefix}*", count=200)
|
||||
if keys:
|
||||
client.delete(*keys)
|
||||
if cursor == 0:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug("Cache invalidate error: %s", e)
|
||||
Reference in New Issue
Block a user