File size: 4,676 Bytes
6b6e83f 64ce412 6b6e83f 64ce412 6b6e83f 64ce412 6b6e83f 64ce412 6b6e83f 64ce412 6b6e83f 1a782a6 6b6e83f | 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 104 105 106 107 108 109 | """
embedding_engine.py
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Offline embedding strategy using TF-IDF + SVD (LSA) for
environments where sentence-transformers cannot be installed
due to disk/network constraints.
When sentence-transformers IS available (recommended for
production), swap EmbeddingEngine with SentenceTransformerEngine
below. The rest of the pipeline is identical.
"""
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.decomposition import TruncatedSVD
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import Normalizer
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Baseline: TF-IDF + Latent Semantic Analysis (offline, fast)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class TFIDFEmbeddingEngine:
"""
Produces 256-dim L2-normalised embeddings via TF-IDF β SVD.
Works completely offline with no extra downloads.
Optimised for English text processing.
"""
def __init__(self, n_components: int = 256):
self.n_components = n_components
self.pipeline = Pipeline([
("tfidf", TfidfVectorizer(
analyzer = "word", # word level
stop_words = "english", # filter out english stop words
max_features = 20_000,
sublinear_tf = True,
strip_accents = "unicode",
lowercase = True,
)),
("svd", TruncatedSVD(n_components=n_components, random_state=42)),
("norm", Normalizer(norm="l2")),
])
self.is_fitted = False
def fit(self, texts: list[str]) -> "TFIDFEmbeddingEngine":
self.pipeline.fit(texts)
self.is_fitted = True
return self
def encode(self, texts: list[str]) -> np.ndarray:
"""Return (N, n_components) float32 array."""
if not self.is_fitted:
raise RuntimeError("Call .fit() before .encode()")
return self.pipeline.transform(texts).astype(np.float32)
def encode_single(self, text: str) -> np.ndarray:
return self.encode([text])[0]
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Upgrade path: sentence-transformers (recommended)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class SentenceTransformerEngine:
"""
Drop-in replacement using all-MiniLM-L6-v2.
State-of-the-art English semantic search embedding model.
Install: pip install sentence-transformers
The model is downloaded once and cached locally (~80 MB).
Usage:
engine = SentenceTransformerEngine()
vectors = engine.encode(texts)
"""
def __init__(self, model_name: str = "all-MiniLM-L6-v2"):
try:
from sentence_transformers import SentenceTransformer
self.model = SentenceTransformer(model_name)
self.is_fitted = True
except ImportError:
raise ImportError(
"pip install sentence-transformers # then retry"
)
def fit(self, texts): # no-op: pretrained
return self
def encode(self, texts: list[str]) -> np.ndarray:
return self.model.encode(texts, convert_to_numpy=True,
show_progress_bar=False, batch_size=32)
def encode_single(self, text: str) -> np.ndarray:
return self.encode([text])[0]
def get_embedding_engine(prefer_transformer: bool = True):
"""Factory: use sentence-transformers if available, else TF-IDF+SVD."""
if prefer_transformer:
try:
engine = SentenceTransformerEngine()
print("[EmbeddingEngine] Using sentence-transformers (English).")
return engine
except (ImportError, Exception) as e:
print(f"[EmbeddingEngine] sentence-transformers unavailable ({e})."
" Falling back to TF-IDF+SVD.")
print("[EmbeddingEngine] Using TF-IDF + SVD (offline baseline).")
return TFIDFEmbeddingEngine()
|