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