"""FastAPI app exposing keyword and similarity search over the ACL Anthology corpus.""" import logging import os from fastapi import FastAPI, HTTPException from fastapi.responses import JSONResponse from app.config import get_settings from app.schemas import PaperResult, SimilarityResult, HealthResponse from common.db import init_db from common.vector_index import load_index from sync.embeddings_client import EmbeddingsClient from sync.hf_dataset_store import download_snapshot from app.search_service import SearchService logger = logging.getLogger(__name__) app = FastAPI(title="ACL Anthology Search API") app.state.startup_ok = False app.state.search_service = None app.state.last_synced_at = None @app.on_event("startup") def load_snapshot() -> None: settings = get_settings() try: state = download_snapshot(settings.hf_repo_id, settings.data_dir, settings.hf_token) conn = init_db(os.path.join(settings.data_dir, "papers.db")) index = load_index(os.path.join(settings.data_dir, "index.faiss")) embeddings_client = EmbeddingsClient(base_url=settings.embedding_base_url, api_key=settings.embedding_api_key) app.state.search_service = SearchService(conn=conn, index=index, embeddings_client=embeddings_client) app.state.last_synced_at = state.get("last_synced_at") app.state.startup_ok = True except Exception: logger.exception("Failed to load index/DB snapshot on startup") app.state.startup_ok = False @app.middleware("http") async def block_when_not_ready(request, call_next): if not app.state.startup_ok and request.url.path != "/health": return JSONResponse(status_code=503, content={"detail": "service not ready"}) return await call_next(request) @app.get("/health", response_model=HealthResponse) def health(): if not app.state.startup_ok: raise HTTPException(status_code=503, detail="index/db not loaded") return HealthResponse( status="ok", index_size=app.state.search_service.index.ntotal, last_synced_at=getattr(app.state, "last_synced_at", None), ) @app.get("/search/keyword", response_model=list[PaperResult]) def search_keyword(q: str, limit: int = 10): return app.state.search_service.keyword_search(q, limit=limit) @app.get("/search/similarity", response_model=list[SimilarityResult]) def search_similarity(q: str, k: int = 10): return app.state.search_service.similarity_search(q, k=k) @app.get("/paper/{paper_id}", response_model=PaperResult) def get_paper(paper_id: str): paper = app.state.search_service.get_paper(paper_id) if paper is None: raise HTTPException(status_code=404, detail="paper not found") return paper