feat(embeddings): cache persistent de vectori in SQLite + warmup in fundal

Vectorii corpusului k-NN persista in tabela embedding_cache (PK model+text_hash,
blob float32 LE); la warmup se vectorizeaza doar textele lipsa din cache, deci
restartul cu corpus neschimbat nu mai plateste ~1-2 min de embed (embed=0).

- app/embedding_cache.py: serializare array('f'), load/save/purge chunk 500 cu
  BEGIN/COMMIT explicit (conexiuni autocommit), validare dimensiune la scriere
  si citire, orchestrare sync_corpus_vectors cu embed_fn injectat
- index_corpus(vectors=): vectori precalculati cu validare aliniere; mismatch
  -> fallback embed complet
- ensure_embeddings_corpus: warmup in thread la startup (block=True), calea de
  request ne-blocanta (acquire non-blocking pe lock; warmup in curs -> return
  imediat); purjare orfane + modele vechi doar dupa indexare reusita
- log warmup: cache=N embed=M in Xs
- 31 teste noi (cold/warm/incremental, model schimbat, concurenta, ranking
  exact, echivalenta float32); suita completa 1596 passed

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-07-06 21:57:49 +00:00
parent a46e364594
commit 705ad030fe
11 changed files with 1437 additions and 35 deletions

View File

@@ -7,6 +7,7 @@ un worker mort nu trebuie sa lase containerul "sanatos".
from __future__ import annotations
import secrets
import threading
from contextlib import asynccontextmanager
from datetime import datetime, timezone
from pathlib import Path
@@ -39,6 +40,25 @@ from .web.csrf import CsrfError
from .web.session import AdminRequired, LoginRequired
def _warmup_embeddings() -> None:
"""Incarca modelul de embeddings si indexeaza corpusul SILVER, in fundal.
Ruleaza intr-un thread daemon la startup: incarcarea modelului (~230MB) plus
vectorizarea corpusului dureaza zeci de secunde si NU are voie sa blocheze
primul request pe /mapari. Pana termina, sugestiile embeddings lipsesc
(degradare gratioasa); GOLD/SILVER/fuzzy functioneaza normal.
"""
from .mapping import ensure_embeddings_corpus
try:
conn = get_connection()
try:
ensure_embeddings_corpus(conn, block=True)
finally:
conn.close()
except Exception:
pass # best-effort: esecul warmup-ului nu opreste API-ul
@asynccontextmanager
async def lifespan(app: FastAPI):
install_log_redaction()
@@ -49,6 +69,8 @@ async def lifespan(app: FastAPI):
# cheia API si secretul de sesiune, in loc de o instanta descoperita post-deploy.
validate_prod_invariants(get_settings())
init_db()
if get_settings().embeddings_enabled:
threading.Thread(target=_warmup_embeddings, name="emb-warmup", daemon=True).start()
yield