"""Sentence-transformer embedder for the archival tier. Lazy singleton wrapping ``SentenceTransformer``. Vectors are L2-normalized at encode time so downstream FAISS ``IndexFlatIP`` search returns cosine similarity directly (Step 16). """ from __future__ import annotations import numpy as np from sentence_transformers import SentenceTransformer from src.config import settings _MODEL: SentenceTransformer | None = None def _get_model() -> SentenceTransformer: """Load the model on first call; return the cached instance thereafter. Cache directory is set explicitly via ``cache_folder`` (not HF_HOME) so the setting scopes to just this model. On HF Spaces, the configured path lives under the persistent data dir, surviving rebuilds. """ global _MODEL if _MODEL is None: _MODEL = SentenceTransformer( settings.embeddings.model_name, cache_folder=settings.storage.embeddings_cache, ) return _MODEL def embed(text: str) -> np.ndarray: """Encode one string. Returns a normalized 1D float32 array of shape (dim,).""" model = _get_model() return model.encode(text, normalize_embeddings=True) def embed_batch(texts: list[str]) -> np.ndarray: """Encode a list of strings. Returns a normalized 2D float32 array of shape (N, dim).""" model = _get_model() return model.encode(texts, normalize_embeddings=True)