File size: 3,416 Bytes
017e961
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
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")

# CORS
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)  # {(backend,name), ...}
    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)

    # 1) preset_id가 있으면 우선 적용
    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)

    # 2) 허용목록 체크
    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
    )