Spaces:
Running
Running
Syed Arfan
commited on
Commit
·
66da1e3
1
Parent(s):
09107be
Add Docker Compose setup with PostgreSQL, Redis, and Nginx; implement caching and database models
Browse files- docker-compose.yml +57 -0
- nginx/nginx.conf +19 -0
- requirements.txt +8 -1
- src/cache.py +151 -0
- src/database.py +91 -0
- src/main.py +81 -5
docker-compose.yml
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
services:
|
| 2 |
+
postgres:
|
| 3 |
+
image: postgres:15-alpine
|
| 4 |
+
container_name: sentiment-api-postgres
|
| 5 |
+
environment:
|
| 6 |
+
POSTGRES_DB: sentiment
|
| 7 |
+
POSTGRES_USER: user
|
| 8 |
+
POSTGRES_PASSWORD: pass
|
| 9 |
+
ports:
|
| 10 |
+
- "5432:5432"
|
| 11 |
+
volumes:
|
| 12 |
+
- postgres_data:/var/lib/postgresql/data
|
| 13 |
+
healthcheck:
|
| 14 |
+
test: ["CMD-SHELL", "pg_isready -U user -d sentiment"]
|
| 15 |
+
interval: 10s
|
| 16 |
+
timeout: 5s
|
| 17 |
+
retries: 5
|
| 18 |
+
|
| 19 |
+
redis:
|
| 20 |
+
image: redis:7-alpine
|
| 21 |
+
container_name: sentiment-api-redis
|
| 22 |
+
ports:
|
| 23 |
+
- "6379:6379" # Redis default port
|
| 24 |
+
healthcheck:
|
| 25 |
+
test: ["CMD", "redis-cli", "ping"]
|
| 26 |
+
interval: 10s
|
| 27 |
+
timeout: 3s
|
| 28 |
+
retries: 5
|
| 29 |
+
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
|
| 30 |
+
|
| 31 |
+
nginx:
|
| 32 |
+
image: nginx:alpine
|
| 33 |
+
container_name: sentiment-api-nginx
|
| 34 |
+
ports:
|
| 35 |
+
- "80:80"
|
| 36 |
+
volumes:
|
| 37 |
+
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
| 38 |
+
depends_on:
|
| 39 |
+
- api
|
| 40 |
+
|
| 41 |
+
api:
|
| 42 |
+
build: .
|
| 43 |
+
container_name: sentiment-api
|
| 44 |
+
ports:
|
| 45 |
+
- "8000:8000"
|
| 46 |
+
environment:
|
| 47 |
+
DATABASE_URL: postgresql://user:pass@postgres:5432/sentiment
|
| 48 |
+
REDIS_URL: redis://redis:6379 # ← ADD THIS LINE
|
| 49 |
+
depends_on:
|
| 50 |
+
postgres:
|
| 51 |
+
condition: service_healthy
|
| 52 |
+
redis: # ← ADD THIS
|
| 53 |
+
condition: service_healthy
|
| 54 |
+
command: uvicorn src.main:app --host 0.0.0.0 --port 8000
|
| 55 |
+
|
| 56 |
+
volumes:
|
| 57 |
+
postgres_data:
|
nginx/nginx.conf
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
events {
|
| 2 |
+
worker_connections 1024;
|
| 3 |
+
}
|
| 4 |
+
|
| 5 |
+
http {
|
| 6 |
+
upstream api {
|
| 7 |
+
server sentiment-api:8000;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
server {
|
| 11 |
+
listen 80;
|
| 12 |
+
|
| 13 |
+
location / {
|
| 14 |
+
proxy_pass http://api;
|
| 15 |
+
proxy_set_header Host $host;
|
| 16 |
+
proxy_set_header X-Real-IP $remote_addr;
|
| 17 |
+
}
|
| 18 |
+
}
|
| 19 |
+
}
|
requirements.txt
CHANGED
|
@@ -8,4 +8,11 @@ httpx==0.28.1
|
|
| 8 |
|
| 9 |
# Testing
|
| 10 |
pytest==8.3.4
|
| 11 |
-
pytest-cov==6.0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
# Testing
|
| 10 |
pytest==8.3.4
|
| 11 |
+
pytest-cov==6.0.0
|
| 12 |
+
|
| 13 |
+
# Database
|
| 14 |
+
psycopg2-binary==2.9.9
|
| 15 |
+
sqlalchemy==2.0.23
|
| 16 |
+
|
| 17 |
+
# Cache
|
| 18 |
+
redis==5.2.1
|
src/cache.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Redis cache layer for sentiment analysis API
|
| 3 |
+
|
| 4 |
+
Caches ML inference results to avoid redundant model calls
|
| 5 |
+
Uses Redis for sub-millisecond lookup times
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import redis
|
| 9 |
+
import hashlib
|
| 10 |
+
import json
|
| 11 |
+
import os
|
| 12 |
+
from typing import Optional, Dict, Any
|
| 13 |
+
|
| 14 |
+
# Get Redis URL from environment variable
|
| 15 |
+
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379")
|
| 16 |
+
|
| 17 |
+
# Create Redis client
|
| 18 |
+
# decode_responses=True converts bytes to strings automatically
|
| 19 |
+
redis_client = redis.from_url(REDIS_URL, decode_responses=True)
|
| 20 |
+
|
| 21 |
+
# Cache TTL (Time To Live) - how long to keep cached results
|
| 22 |
+
CACHE_TTL_SECONDS = 3600 # 1 hour
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def generate_cache_key(text: str) -> str:
|
| 26 |
+
"""
|
| 27 |
+
Generate a unique cache key for the input text
|
| 28 |
+
|
| 29 |
+
Uses SHA-256 hash to create consistent keys
|
| 30 |
+
Same text always generates same key
|
| 31 |
+
|
| 32 |
+
Args:
|
| 33 |
+
text: Input text to analyze
|
| 34 |
+
|
| 35 |
+
Returns:
|
| 36 |
+
Cache key string (e.g., "sentiment:abc123...")
|
| 37 |
+
"""
|
| 38 |
+
# Create hash of the text (consistent for same input)
|
| 39 |
+
text_hash = hashlib.sha256(text.encode()).hexdigest()[:16]
|
| 40 |
+
return f"sentiment:{text_hash}"
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def get_cached_result(text: str) -> Optional[Dict[str, Any]]:
|
| 44 |
+
"""
|
| 45 |
+
Retrieve cached sentiment analysis result
|
| 46 |
+
|
| 47 |
+
Args:
|
| 48 |
+
text: Input text to look up
|
| 49 |
+
|
| 50 |
+
Returns:
|
| 51 |
+
Cached result dict if found, None if cache miss
|
| 52 |
+
"""
|
| 53 |
+
try:
|
| 54 |
+
cache_key = generate_cache_key(text)
|
| 55 |
+
cached_data = redis_client.get(cache_key)
|
| 56 |
+
|
| 57 |
+
if cached_data:
|
| 58 |
+
# Parse JSON string back to dict
|
| 59 |
+
return json.loads(cached_data)
|
| 60 |
+
|
| 61 |
+
return None
|
| 62 |
+
except Exception as e:
|
| 63 |
+
# If Redis fails, log but don't crash
|
| 64 |
+
print(f"Cache retrieval error: {e}")
|
| 65 |
+
return None
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def cache_result(text: str, result: Dict[str, Any]) -> bool:
|
| 69 |
+
"""
|
| 70 |
+
Store sentiment analysis result in cache
|
| 71 |
+
|
| 72 |
+
Args:
|
| 73 |
+
text: Input text that was analyzed
|
| 74 |
+
result: Analysis result to cache
|
| 75 |
+
|
| 76 |
+
Returns:
|
| 77 |
+
True if cached successfully, False otherwise
|
| 78 |
+
"""
|
| 79 |
+
try:
|
| 80 |
+
cache_key = generate_cache_key(text)
|
| 81 |
+
|
| 82 |
+
# Convert dict to JSON string
|
| 83 |
+
result_json = json.dumps(result)
|
| 84 |
+
|
| 85 |
+
# Store in Redis with TTL
|
| 86 |
+
redis_client.setex(
|
| 87 |
+
cache_key,
|
| 88 |
+
CACHE_TTL_SECONDS,
|
| 89 |
+
result_json
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
return True
|
| 93 |
+
except Exception as e:
|
| 94 |
+
print(f"Cache storage error: {e}")
|
| 95 |
+
return False
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def get_cache_stats() -> Dict[str, Any]:
|
| 99 |
+
"""
|
| 100 |
+
Get Redis cache statistics
|
| 101 |
+
|
| 102 |
+
Returns:
|
| 103 |
+
Dict with cache info (memory usage, keys, hits, etc.)
|
| 104 |
+
"""
|
| 105 |
+
try:
|
| 106 |
+
info = redis_client.info("stats")
|
| 107 |
+
memory = redis_client.info("memory")
|
| 108 |
+
|
| 109 |
+
# Count sentiment-related keys
|
| 110 |
+
sentiment_keys = len(redis_client.keys("sentiment:*"))
|
| 111 |
+
|
| 112 |
+
return {
|
| 113 |
+
"status": "connected",
|
| 114 |
+
"total_keys": redis_client.dbsize(),
|
| 115 |
+
"sentiment_keys": sentiment_keys,
|
| 116 |
+
"memory_used_mb": round(memory["used_memory"] / 1024 / 1024, 2),
|
| 117 |
+
"hits": info.get("keyspace_hits", 0),
|
| 118 |
+
"misses": info.get("keyspace_misses", 0),
|
| 119 |
+
"hit_rate": round(
|
| 120 |
+
info.get("keyspace_hits", 0) /
|
| 121 |
+
max(info.get("keyspace_hits", 0) + info.get("keyspace_misses", 0), 1) * 100,
|
| 122 |
+
2
|
| 123 |
+
)
|
| 124 |
+
}
|
| 125 |
+
except Exception as e:
|
| 126 |
+
return {
|
| 127 |
+
"status": "error",
|
| 128 |
+
"error": str(e)
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def clear_cache() -> bool:
|
| 133 |
+
"""
|
| 134 |
+
Clear all sentiment cache entries
|
| 135 |
+
|
| 136 |
+
WARNING: This removes all cached results
|
| 137 |
+
|
| 138 |
+
Returns:
|
| 139 |
+
True if cleared successfully
|
| 140 |
+
"""
|
| 141 |
+
try:
|
| 142 |
+
# Get all sentiment keys
|
| 143 |
+
keys = redis_client.keys("sentiment:*")
|
| 144 |
+
|
| 145 |
+
if keys:
|
| 146 |
+
redis_client.delete(*keys)
|
| 147 |
+
|
| 148 |
+
return True
|
| 149 |
+
except Exception as e:
|
| 150 |
+
print(f"Cache clear error: {e}")
|
| 151 |
+
return False
|
src/database.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Database connection and models for sentiment API
|
| 3 |
+
|
| 4 |
+
Uses SQLAlchemy ORM for clean database interactions
|
| 5 |
+
PostgreSQL for persistent storage (runs in Docker container - FREE!)
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime
|
| 9 |
+
from sqlalchemy.ext.declarative import declarative_base
|
| 10 |
+
from sqlalchemy.orm import sessionmaker
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
import os
|
| 13 |
+
|
| 14 |
+
# Get database URL from environment variable
|
| 15 |
+
# Set by docker-compose.yml
|
| 16 |
+
DATABASE_URL = os.getenv(
|
| 17 |
+
"DATABASE_URL",
|
| 18 |
+
"postgresql://user:pass@localhost:5432/sentiment" # Fallback for local dev
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
# Create database engine
|
| 22 |
+
# This connects to PostgreSQL container
|
| 23 |
+
engine = create_engine(DATABASE_URL)
|
| 24 |
+
|
| 25 |
+
# Create session factory
|
| 26 |
+
# Sessions are like "conversations" with the database
|
| 27 |
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 28 |
+
|
| 29 |
+
# Base class for database models
|
| 30 |
+
Base = declarative_base()
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# Database Model (Table Definition)
|
| 34 |
+
class SentimentAnalysis(Base):
|
| 35 |
+
"""
|
| 36 |
+
Stores sentiment analysis results
|
| 37 |
+
|
| 38 |
+
Table: sentiment_analyses
|
| 39 |
+
"""
|
| 40 |
+
__tablename__ = "sentiment_analyses"
|
| 41 |
+
|
| 42 |
+
# Primary key (auto-incrementing ID)
|
| 43 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 44 |
+
|
| 45 |
+
# The text that was analyzed
|
| 46 |
+
text = Column(String(512), nullable=False)
|
| 47 |
+
|
| 48 |
+
# Sentiment result (POSITIVE or NEGATIVE)
|
| 49 |
+
sentiment = Column(String(50), nullable=False)
|
| 50 |
+
|
| 51 |
+
# Confidence score (0.0 to 1.0)
|
| 52 |
+
confidence = Column(Float, nullable=False)
|
| 53 |
+
|
| 54 |
+
# Processing time in milliseconds
|
| 55 |
+
processing_time_ms = Column(Integer, nullable=False)
|
| 56 |
+
|
| 57 |
+
# When this analysis was created
|
| 58 |
+
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
# Create all tables in the database
|
| 62 |
+
# This runs when the app starts
|
| 63 |
+
def init_db():
|
| 64 |
+
"""
|
| 65 |
+
Initialize database tables
|
| 66 |
+
|
| 67 |
+
Creates sentiment_analyses table if it doesn't exist
|
| 68 |
+
Safe to call multiple times (won't recreate existing tables)
|
| 69 |
+
"""
|
| 70 |
+
Base.metadata.create_all(bind=engine)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
# Dependency for FastAPI routes
|
| 74 |
+
# Provides a database session to each request
|
| 75 |
+
def get_db():
|
| 76 |
+
"""
|
| 77 |
+
FastAPI dependency that provides a database session
|
| 78 |
+
|
| 79 |
+
Usage in routes:
|
| 80 |
+
@app.post("/analyze")
|
| 81 |
+
def analyze(db: Session = Depends(get_db)):
|
| 82 |
+
# Use db here
|
| 83 |
+
pass
|
| 84 |
+
|
| 85 |
+
Automatically closes session after request
|
| 86 |
+
"""
|
| 87 |
+
db = SessionLocal()
|
| 88 |
+
try:
|
| 89 |
+
yield db
|
| 90 |
+
finally:
|
| 91 |
+
db.close()
|
src/main.py
CHANGED
|
@@ -1,7 +1,11 @@
|
|
| 1 |
from fastapi import FastAPI, HTTPException
|
| 2 |
from pydantic import BaseModel, Field
|
| 3 |
from transformers import pipeline
|
|
|
|
|
|
|
|
|
|
| 4 |
import time
|
|
|
|
| 5 |
|
| 6 |
app = FastAPI(
|
| 7 |
title="Sentiment Analysis API",
|
|
@@ -9,6 +13,14 @@ app = FastAPI(
|
|
| 9 |
version="1.0.0"
|
| 10 |
)
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
# Load model once at startup
|
| 13 |
print("Loading sentiment analysis model...")
|
| 14 |
sentiment_analyzer = pipeline(
|
|
@@ -26,6 +38,7 @@ class SentimentResponse(BaseModel):
|
|
| 26 |
sentiment: str
|
| 27 |
confidence: float
|
| 28 |
processing_time_ms: int
|
|
|
|
| 29 |
|
| 30 |
@app.get("/")
|
| 31 |
def root():
|
|
@@ -37,29 +50,92 @@ def root():
|
|
| 37 |
}
|
| 38 |
|
| 39 |
@app.post("/analyze", response_model=SentimentResponse)
|
| 40 |
-
def analyze_sentiment(
|
|
|
|
|
|
|
|
|
|
| 41 |
"""
|
| 42 |
-
Analyze sentiment of input text.
|
|
|
|
| 43 |
Returns sentiment (POSITIVE/NEGATIVE) with confidence score.
|
|
|
|
| 44 |
"""
|
| 45 |
start_time = time.time()
|
| 46 |
|
| 47 |
try:
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
result = sentiment_analyzer(request.text)[0]
|
| 50 |
|
| 51 |
processing_time = int((time.time() - start_time) * 1000)
|
| 52 |
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
text=request.text,
|
| 55 |
sentiment=result['label'],
|
| 56 |
confidence=round(result['score'], 4),
|
| 57 |
processing_time_ms=processing_time
|
| 58 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
except Exception as e:
|
| 60 |
raise HTTPException(status_code=500, detail=str(e))
|
| 61 |
|
| 62 |
@app.get("/health")
|
| 63 |
def health():
|
| 64 |
"""Kubernetes-style health check"""
|
| 65 |
-
return {"status": "ok"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from fastapi import FastAPI, HTTPException
|
| 2 |
from pydantic import BaseModel, Field
|
| 3 |
from transformers import pipeline
|
| 4 |
+
from sqlalchemy.orm import Session
|
| 5 |
+
from fastapi import Depends
|
| 6 |
+
from .database import init_db, get_db, SentimentAnalysis
|
| 7 |
import time
|
| 8 |
+
from . import cache
|
| 9 |
|
| 10 |
app = FastAPI(
|
| 11 |
title="Sentiment Analysis API",
|
|
|
|
| 13 |
version="1.0.0"
|
| 14 |
)
|
| 15 |
|
| 16 |
+
# Initialize database on startup
|
| 17 |
+
@app.on_event("startup")
|
| 18 |
+
def startup_event():
|
| 19 |
+
"""Create database tables if they don't exist"""
|
| 20 |
+
print("Initializing database...")
|
| 21 |
+
init_db()
|
| 22 |
+
print("Database ready!")
|
| 23 |
+
|
| 24 |
# Load model once at startup
|
| 25 |
print("Loading sentiment analysis model...")
|
| 26 |
sentiment_analyzer = pipeline(
|
|
|
|
| 38 |
sentiment: str
|
| 39 |
confidence: float
|
| 40 |
processing_time_ms: int
|
| 41 |
+
cached: bool = False # ← ADD THIS LINE
|
| 42 |
|
| 43 |
@app.get("/")
|
| 44 |
def root():
|
|
|
|
| 50 |
}
|
| 51 |
|
| 52 |
@app.post("/analyze", response_model=SentimentResponse)
|
| 53 |
+
def analyze_sentiment(
|
| 54 |
+
request: TextRequest,
|
| 55 |
+
db: Session = Depends(get_db)
|
| 56 |
+
):
|
| 57 |
"""
|
| 58 |
+
Analyze sentiment of input text with caching.
|
| 59 |
+
|
| 60 |
Returns sentiment (POSITIVE/NEGATIVE) with confidence score.
|
| 61 |
+
Stores result in PostgreSQL database and Redis cache.
|
| 62 |
"""
|
| 63 |
start_time = time.time()
|
| 64 |
|
| 65 |
try:
|
| 66 |
+
cached_result = cache.get_cached_result(request.text)
|
| 67 |
+
|
| 68 |
+
if cached_result:
|
| 69 |
+
# Cache HIT - return cached result
|
| 70 |
+
print(f"Cache HIT for: {request.text[:50]}")
|
| 71 |
+
|
| 72 |
+
# Add cache indicator
|
| 73 |
+
cached_result["cached"] = True
|
| 74 |
+
cached_result["processing_time_ms"] = int((time.time() - start_time) * 1000)
|
| 75 |
+
|
| 76 |
+
return SentimentResponse(**cached_result)
|
| 77 |
+
|
| 78 |
+
# Cache MISS - run ML model
|
| 79 |
+
print(f"Cache MISS for: {request.text[:50]}")
|
| 80 |
+
|
| 81 |
result = sentiment_analyzer(request.text)[0]
|
| 82 |
|
| 83 |
processing_time = int((time.time() - start_time) * 1000)
|
| 84 |
|
| 85 |
+
# Create response
|
| 86 |
+
response_data = {
|
| 87 |
+
"text": request.text,
|
| 88 |
+
"sentiment": result['label'],
|
| 89 |
+
"confidence": round(result['score'], 4),
|
| 90 |
+
"processing_time_ms": processing_time,
|
| 91 |
+
"cached": False # NEW: indicate this wasn't cached
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
# Store in database
|
| 95 |
+
db_analysis = SentimentAnalysis(
|
| 96 |
text=request.text,
|
| 97 |
sentiment=result['label'],
|
| 98 |
confidence=round(result['score'], 4),
|
| 99 |
processing_time_ms=processing_time
|
| 100 |
)
|
| 101 |
+
db.add(db_analysis)
|
| 102 |
+
db.commit()
|
| 103 |
+
db.refresh(db_analysis)
|
| 104 |
+
|
| 105 |
+
# ===== NEW: Store in cache =====
|
| 106 |
+
cache.cache_result(request.text, response_data)
|
| 107 |
+
# ===============================
|
| 108 |
+
|
| 109 |
+
return SentimentResponse(**response_data)
|
| 110 |
+
|
| 111 |
except Exception as e:
|
| 112 |
raise HTTPException(status_code=500, detail=str(e))
|
| 113 |
|
| 114 |
@app.get("/health")
|
| 115 |
def health():
|
| 116 |
"""Kubernetes-style health check"""
|
| 117 |
+
return {"status": "ok"}
|
| 118 |
+
|
| 119 |
+
@app.get("/cache/stats")
|
| 120 |
+
def get_cache_statistics():
|
| 121 |
+
"""
|
| 122 |
+
Get Redis cache statistics
|
| 123 |
+
|
| 124 |
+
Shows cache hit rate, memory usage, and key counts
|
| 125 |
+
"""
|
| 126 |
+
return cache.get_cache_stats()
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
@app.delete("/cache/clear")
|
| 130 |
+
def clear_cache_endpoint():
|
| 131 |
+
"""
|
| 132 |
+
Clear all cached sentiment results
|
| 133 |
+
|
| 134 |
+
Use this to force fresh analysis for all requests
|
| 135 |
+
"""
|
| 136 |
+
success = cache.clear_cache()
|
| 137 |
+
|
| 138 |
+
if success:
|
| 139 |
+
return {"message": "Cache cleared successfully"}
|
| 140 |
+
else:
|
| 141 |
+
return {"message": "Failed to clear cache"}
|