| from fastapi import FastAPI, Header, HTTPException
|
| from fastapi.middleware.cors import CORSMiddleware
|
| from loguru import logger
|
| import time
|
| from typing import Optional, List
|
|
|
| from .config import settings
|
| from .models import SearchRequest, SearchResponse, Hit, ModelSpec
|
| from .embeddings import embed_query
|
| from .qdrant_wrapper import query_points
|
| from .embeddings_registry import PRESETS
|
|
|
| app = FastAPI(title="Vector Search WebAPI", version="0.1.0")
|
|
|
|
|
| origins = [o.strip() for o in settings.CORS_ALLOW_ORIGINS.split(",") if o.strip()]
|
| app.add_middleware(
|
| CORSMiddleware,
|
| allow_origins=origins or ["*"],
|
| allow_credentials=True,
|
| allow_methods=["*"],
|
| allow_headers=["*"],
|
| )
|
|
|
| def _require_key(x_api_key: Optional[str]):
|
| if settings.API_KEY and x_api_key != settings.API_KEY:
|
| raise HTTPException(status_code=401, detail="Invalid API key")
|
|
|
| @app.get("/health")
|
| def health():
|
| return {"ok": True, "qdrant_url": settings.QDRANT_URL}
|
|
|
| @app.get("/models")
|
| def models():
|
| allow = set(settings.allow_models)
|
| items = []
|
|
|
| for pid, spec in PRESETS.items():
|
| pair = (spec["backend"], spec["name"])
|
| if pair in allow:
|
| items.append({"preset_id": pid, **spec})
|
| return {"models": items}
|
|
|
| @app.post("/search", response_model=SearchResponse)
|
| def search(req: SearchRequest, x_api_key: Optional[str] = Header(default=None, alias="X-API-Key")):
|
| _require_key(x_api_key)
|
|
|
|
|
| model_spec: ModelSpec = req.model
|
| if req.preset_id:
|
| if req.preset_id not in PRESETS:
|
| raise HTTPException(status_code=400, detail="Unknown preset_id")
|
| p = PRESETS[req.preset_id]
|
| model_spec = ModelSpec(**p)
|
|
|
|
|
| if (model_spec.backend, model_spec.name) not in settings.allow_models:
|
| raise HTTPException(status_code=400, detail="Model not allowed")
|
|
|
| t0 = time.time()
|
| try:
|
| vec = embed_query(req.text, model_spec)
|
| except Exception as e:
|
| logger.exception("Embedding failed")
|
| raise HTTPException(status_code=500, detail=f"Embedding error: {e}")
|
|
|
| try:
|
| points = query_points(
|
| cfg=req.qdrant,
|
| vector=vec,
|
| limit=req.top_k,
|
| with_payload=req.with_payload
|
| )
|
| except Exception as e:
|
| logger.exception("Qdrant query failed")
|
| raise HTTPException(status_code=404, detail=f"Qdrant error: {e}")
|
|
|
| hits: List[Hit] = []
|
| for p in points:
|
| score = float(p.score) if getattr(p, "score", None) is not None else 0.0
|
| if score < req.threshold:
|
| continue
|
| hits.append(Hit(id=p.id, score=score, payload=getattr(p, "payload", None)))
|
|
|
| took_ms = int((time.time() - t0) * 1000)
|
| logger.info({
|
| "event": "search",
|
| "took_ms": took_ms,
|
| "backend": model_spec.backend,
|
| "model": model_spec.name,
|
| "collection": req.qdrant.collection,
|
| "top_k": req.top_k,
|
| "threshold": req.threshold,
|
| "result_count": len(hits)
|
| })
|
| return SearchResponse(
|
| took_ms=took_ms,
|
| model=model_spec,
|
| collection=req.qdrant.collection,
|
| total_candidates=len(points),
|
| hits=hits
|
| )
|
|
|