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:
@@ -10,9 +10,9 @@ Design:
|
||||
- NU apelat din resolve_prestatii/load_mapping
|
||||
|
||||
API public (nivel modul):
|
||||
index_corpus(items) -> None
|
||||
suggest_nearest(text, top_k) -> [{cod, is_nul, similaritate}]
|
||||
is_available() -> bool
|
||||
index_corpus(items, signature, vectors) -> None
|
||||
suggest_nearest(text, top_k) -> [{cod, is_nul, similaritate}]
|
||||
is_available() -> bool
|
||||
|
||||
Clase (pentru teste / injectare backend):
|
||||
EmbeddingEngine(backend) -- motor testabil cu backend injectabil
|
||||
@@ -22,6 +22,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
import threading
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -107,8 +108,19 @@ class EmbeddingEngine:
|
||||
doar cand semnatura nomenclatorului s-a schimbat (evita re-embed inutil)."""
|
||||
return self._corpus_sig
|
||||
|
||||
def index_corpus(self, items: list[dict], signature: str | None = None) -> None:
|
||||
"""Vectorizeaza corpus [{denumire, cod}] si il pastreaza in memorie.
|
||||
def index_corpus(
|
||||
self,
|
||||
items: list[dict],
|
||||
signature: str | None = None,
|
||||
vectors: list | None = None,
|
||||
) -> None:
|
||||
"""Indexeaza corpus [{denumire, cod}] si il pastreaza in memorie.
|
||||
|
||||
`vectors`: vectori precalculati, aliniati POZITIONAL cu `items` (ex. din
|
||||
embedding_cache). `None` (default) = comportamentul existent, embed complet
|
||||
prin backend. Daca `vectors` e furnizat dar lungimea nu corespunde cu `items`
|
||||
sau contine `None`, se ignora (log.warning) si se cade pe embed complet (A8) --
|
||||
o dezaliniere silentioasa ar produce coduri sugerate GRESITE.
|
||||
|
||||
Ignora silentios daca backend-ul lipseste, corpus-ul e gol sau apare
|
||||
orice exceptie la vectorizare (degradare gratioasa).
|
||||
@@ -120,16 +132,35 @@ class EmbeddingEngine:
|
||||
if not items or not self.is_available():
|
||||
return
|
||||
|
||||
if vectors is not None and (len(vectors) != len(items) or any(v is None for v in vectors)):
|
||||
log.warning(
|
||||
"embeddings: index_corpus vectors (%d) nealiniat cu items (%d) sau contine None -- fallback embed complet",
|
||||
len(vectors), len(items),
|
||||
)
|
||||
vectors = None
|
||||
|
||||
try:
|
||||
texts = [str(item["denumire"]) for item in items]
|
||||
vecs = self._backend.embed(texts)
|
||||
self._corpus_vecs = vecs
|
||||
if vectors is not None:
|
||||
self._corpus_vecs = list(vectors)
|
||||
else:
|
||||
texts = [str(item["denumire"]) for item in items]
|
||||
self._corpus_vecs = self._backend.embed(texts)
|
||||
self._corpus_items = list(items)
|
||||
self._corpus_sig = signature
|
||||
except Exception as exc:
|
||||
log.warning("embeddings: index_corpus esuat: %s", exc)
|
||||
# corpus ramane gol -- suggest_nearest va returna []
|
||||
|
||||
def embed(self, texts: list[str]) -> list[list[float]]:
|
||||
"""Vectorizeaza texte brute prin backend (folosit la miss-uri de cache).
|
||||
|
||||
Arunca daca backend-ul lipseste sau embed() esueaza -- apelantul
|
||||
(sync_corpus_vectors) propaga eroarea, fara degradare gratioasa aici.
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise RuntimeError("embeddings: backend indisponibil")
|
||||
return self._backend.embed(texts)
|
||||
|
||||
def suggest_nearest(
|
||||
self,
|
||||
denumire: str,
|
||||
@@ -168,6 +199,7 @@ class EmbeddingEngine:
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
_engine: EmbeddingEngine | None = None
|
||||
_engine_lock = threading.Lock()
|
||||
|
||||
|
||||
def _load_engine() -> EmbeddingEngine:
|
||||
@@ -194,13 +226,24 @@ def _load_engine() -> EmbeddingEngine:
|
||||
|
||||
|
||||
def _get_engine() -> EmbeddingEngine:
|
||||
"""Returneaza engine-ul global (lazy-init)."""
|
||||
"""Returneaza engine-ul global (lazy-init, thread-safe).
|
||||
|
||||
Lock-ul previne incarcarea dubla a modelului cand warmup-ul de la startup
|
||||
si un request concurent ajung aici simultan.
|
||||
"""
|
||||
global _engine
|
||||
if _engine is None:
|
||||
_engine = _load_engine()
|
||||
with _engine_lock:
|
||||
if _engine is None:
|
||||
_engine = _load_engine()
|
||||
return _engine
|
||||
|
||||
|
||||
def is_loaded() -> bool:
|
||||
"""True daca engine-ul global a fost deja construit. NU forteaza incarcarea."""
|
||||
return _engine is not None
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# API public la nivel de modul (wiring L14-S6) #
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -233,12 +276,22 @@ def corpus_signature() -> str | None:
|
||||
return _engine.corpus_signature()
|
||||
|
||||
|
||||
def index_corpus(items: list[dict], signature: str | None = None) -> None:
|
||||
def embed_texts(texts: list[str]) -> list[list[float]]:
|
||||
"""Vectorizeaza texte brute prin motorul global (folosit la miss-uri de cache).
|
||||
|
||||
Arunca daca engine-ul e indisponibil -- apelantul (embedding_cache.sync_corpus_vectors)
|
||||
propaga eroarea catre ensure_embeddings_corpus (degradare gratioasa acolo).
|
||||
"""
|
||||
return _get_engine().embed(texts)
|
||||
|
||||
|
||||
def index_corpus(items: list[dict], signature: str | None = None, vectors: list | None = None) -> None:
|
||||
"""Vectorizeaza corpus [{denumire, cod}] in motorul global.
|
||||
|
||||
`vectors`: vezi EmbeddingEngine.index_corpus (precalculati, aliniati cu `items`).
|
||||
Silentios pe eroare (degradare gratioasa).
|
||||
"""
|
||||
_get_engine().index_corpus(items, signature=signature)
|
||||
_get_engine().index_corpus(items, signature=signature, vectors=vectors)
|
||||
|
||||
|
||||
def suggest_nearest(denumire: str, top_k: int = 3) -> list[dict]:
|
||||
|
||||
Reference in New Issue
Block a user