docs-vector-search — performance record

Measured performance of the deployed service. Append a dated row to History after each notable run (new corpus size, model change, box resize, RAGAS run).

Environment (baseline)

Boxc360-api-uat-docs · s-general-1x2 (1 vCPU / 2 GB) + 2 GB swap
Storepgvector on the vDB — rag.doc_chunks, vector(384), HNSW cosine
Corpus928 chunks from docs/** (heading-aware + ~400-token windows)
Embedparaphrase-multilingual-MiniLM-L12-v2 (384-dim, VN+EN), fastembed/ONNX
RerankBAAI/bge-reranker-base, fastembed cross-encoder
GenerateQwen2.5-0.5B-Instruct Q4_K_M, llama-cpp-python
Local generation tuningDOCS_LOCAL_LLM_THREADS=2, DOCS_LOCAL_LLM_BATCH_SIZE=512, memory-mapped GGUF

Startup (cold boot)

The server warms embed + rerank in the lifespan; Qwen loads lazily on the first /ask.

PhaseBefore model pre-bakeAfter model pre-bake (baked in image)
Qwen GGUF fetch (deploy step, curl)~6 min first deploy, then host-cachedsame (host-cached in /opt/c360/docs-models)
Reranker download at boot (unauth HF)~10 min (dominant cost)0 — baked into the image
Embed download at boot(cached after enrich)0 — baked
Time to healthy :8001~15 min (fresh box)22 s measured — cold restart, load from baked cache, no download

The reranker download from unauthenticated Hugging Face was the killer (~10 min on the box). Fix: pre-bake the default embed + rerank models into the image at build time (DockerfileFASTEMBED_CACHE=/app/model-cache/fastembed). Alternative: set HF_TOKEN.

Latency (warm)

Measured 2026-09-06 (UAT, first live smoke), curl from the box (localhost:8001):

EndpointLatencyNotes
GET /health<100 mschunk count + model names
POST /search~3 sembed query + pgvector top-N + bge rerank (CPU)
POST /ask (first call)~15–42 sincludes lazy Qwen load; grounded answer + 5 sources
POST /ask (warm)~17–42 shistorical UAT baseline; dominated by Qwen 0.5B generation. Current tuning uses DOCS_LOCAL_LLM_MAX_OUTPUT_TOKENS, DOCS_LOCAL_LLM_THREADS, and DOCS_LOCAL_LLM_BATCH_SIZE.
Boot to healthy (models cached)~1 smeasured on restart with fastembed cache present — the state the pre-bake guarantees on any fresh box

Resource use (under /ask)

Mem used~1.53 GiB / 1.92 GiB resident under /ask (Qwen + embed + rerank all loaded); swap ~0; restarts=0, no OOM
VerdictFits 2 GB but tight (~0.2 GB headroom) with everything resident. Drop DOCS_RERANK_ENABLED=false to shed ~300 MB if it OOMs under concurrency.

Quality eval — fully local (no hosted judge)

python local_eval.py (see README) — judge-free, nothing leaves the box. The system is local end-to-end, so it’s evaluated locally too. (ragas_eval.py with an OpenAI/gateway judge stays available as an opt-in for LLM-judged faithfulness/relevancy.)

Datehit@5MRRkeyword coveragegrounding proxyrefusal rateNotes
2026-09-061.00 (10/10)0.830.900.820.50 ⚠️baseline: retrieval strong EN+VN; grounding good; out-of-scope refusal unreliable — the 0.5B answered “Who won the 2022 World Cup?” instead of declining.
2026-09-061.00 (10/10)0.830.800.901.00after the firmer refusal prompt (agent.py ANSWER_SYSTEM): both out-of-scope questions declined; grounding up 0.82→0.90 (outside knowledge forbidden); retrieval unchanged. Coverage dip 0.90→0.80 is a must_contain exact-match artifact, not a regression.
2026-09-101.00 (10/10)0.810.900.821.00 (OOS) ✅RAGAS + probe audit (UAT). The dataset scores perfect but is all keyword-rich queries, so it can’t see the reported bug. Bare/colloquial queries falsely refuse (persona là gì vậy? → “I don’t know” while a correct source card shows): root-caused to RETRIEVE_TOP_N=20 starving the reranker (the definition chunk sits at vector-rank 21–50). Fix RETRIEVE_TOP_N 20→50; live re-test 9/9, OOS refusal intact. See docs/docs-rag-uat-investigation-2026-09-10.md.

History

DateEventResult
2026-09-06First successful live UAT deploy + smoke928 chunks; /ask "What is CIR?" → correct grounded answer, 15.3 s cold; retrieval EN + VN OK
2026-09-06Root-caused ~15 min first-boot~10 min reranker download (unauth HF) → pre-bake models in image
2026-09-06Redeploy on merged main + latency runboot 1 s (cache warm); /ask warm 17–42 s (0.5B on 1 vCPU); /search ~3 s; mem 1.53/1.92 GiB, no OOM. Note: a concurrent CD deploy (docs-search is in the default set) collided with the manual redeploy — settled healthy.
2026-09-06Pre-bake verified on the pre-baked image (sha-ec0479f, FASTEMBED_CACHE=/app/model-cache/fastembed)cold restart boot-to-healthy = 22 s (977 chunks), down from ~15 min — the reranker download is baked away.
2026-09-06CD docs-search failed (exit 255)single SSH session idle-dropped ~6 min into enrich from the CI runner → added SSH keepalive (ServerAliveInterval).
2026-09-10Local CPU tuning validationwarm /ask improved from 7.98 s to 7.34 s (~8%) with DOCS_LOCAL_LLM_THREADS=2, DOCS_LOCAL_LLM_BATCH_SIZE=512, and use_mmap=True; health remained healthy with no OOM/restarts.
2026-09-10Root-caused “shows a source but answers I don’t know” (bare VN query persona là gì vậy?)The reranker was starved: RETRIEVE_TOP_N=20 cut the pool before the definition chunk (vector-rank 21–50) could be reranked. Fix RETRIEVE_TOP_N 20→50 (generator top_k unchanged); live UAT re-test 9/9, out-of-scope refusal preserved. Full write-up in docs/docs-rag-uat-investigation-2026-09-10.md.