cinematch / app /main.py
Alluri Lakshman Narendra
Deploy CineMatch backend (FastAPI + hybrid retrieval)
22b7d63
Raw
History Blame Contribute Delete
3.3 kB
"""
main.py — the CineMatch FastAPI service.
Routes (see app/schemas.py for the full contract):
GET /api/health engine + model status
GET /api/stats catalog facets for the empty state
GET /api/suggest?q=&limit= title autocomplete
POST /api/search hybrid search (SearchRequest -> SearchResponse)
GET /api/movie/{id} single movie
GET /api/movie/{id}/similar?limit= "More like this"
"""
from __future__ import annotations
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from app.config import settings
from app.engine.search import SearchEngine
from app.schemas import (
HealthResponse,
Movie,
SearchRequest,
SearchResponse,
StatsResponse,
Suggestion,
)
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
log = logging.getLogger("cinematch")
state: dict[str, SearchEngine] = {}
@asynccontextmanager
async def lifespan(app: FastAPI):
log.info("loading engine ...")
engine = SearchEngine(settings)
state["engine"] = engine
log.info("corpus=%d slm=%s loading models (first run downloads them) ...",
engine.corpus.size, engine.slm.label)
try:
engine.warmup()
log.info("ready. reranker=%s", bool(engine.reranker and engine.reranker.available))
except Exception as e: # pragma: no cover - model download failure shouldn't kill server
log.warning("warmup issue (engine still serves): %s", e)
yield
state.clear()
app = FastAPI(title="CineMatch API", version="2.0.0", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
# Public, read-only, no-credentials API — allow any origin so the deployed
# frontend (Vercel, HF Space, localhost, …) can call it from the browser.
allow_origins=["*"],
allow_credentials=False,
allow_methods=["*"],
allow_headers=["*"],
)
def engine() -> SearchEngine:
eng = state.get("engine")
if eng is None:
raise HTTPException(503, "engine not ready")
return eng
@app.get("/")
def root():
return {"service": "CineMatch API", "docs": "/docs", "health": "/api/health"}
@app.get("/api/health", response_model=HealthResponse)
def health():
return engine().health()
@app.get("/api/stats", response_model=StatsResponse)
def stats():
return engine().stats()
@app.get("/api/suggest", response_model=list[Suggestion])
def suggest(q: str = Query(..., min_length=1), limit: int = Query(8, ge=1, le=20)):
return engine().suggest(q, limit)
@app.post("/api/search", response_model=SearchResponse)
def search(req: SearchRequest):
return engine().search(req)
@app.get("/api/movie/{movie_id}", response_model=Movie)
def get_movie(movie_id: str):
try:
return engine().get_movie(movie_id)
except KeyError:
raise HTTPException(404, f"movie '{movie_id}' not found")
@app.get("/api/movie/{movie_id}/similar", response_model=SearchResponse)
def similar(movie_id: str, limit: int = Query(12, ge=1, le=50)):
try:
return engine().similar(movie_id, limit)
except KeyError:
raise HTTPException(404, f"movie '{movie_id}' not found")