Files
tasteby/backend/core/cache.py
joungmin 2bddb0f764 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>
2026-03-09 10:54:28 +09:00

108 lines
2.9 KiB
Python

"""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)