Faraz618 commited on
Commit
ef998fa
·
verified ·
1 Parent(s): 80b07a7

Update src/embeddings.py

Browse files
Files changed (1) hide show
  1. src/embeddings.py +29 -43
src/embeddings.py CHANGED
@@ -1,12 +1,11 @@
1
  """
2
  embeddings.py — Local embedding generation and FAISS index management.
3
 
4
- all-MiniLM-L6-v2 runs entirely on CPU in ~200ms per batch.
5
- No API key, no cost, no rate limits.
6
 
7
- FAISS IndexFlatIP (inner product on normalized vectors = cosine similarity)
8
- is exact search — appropriate for < 100k chunks. For millions of chunks
9
- you'd switch to IndexIVFFlat with nprobe tuning.
10
  """
11
 
12
  import logging
@@ -17,74 +16,62 @@ from src.utils import get_env
17
 
18
  logger = logging.getLogger("enterprise-rag.embeddings")
19
 
20
- # Global model instance — loaded once, reused across queries
21
- # Loading takes ~2-3 seconds on first call; cached in memory after that
22
  _embedding_model = None
23
-
24
- EMBEDDING_DIM = 384 # fixed for all-MiniLM-L6-v2
25
 
26
 
27
  def get_embedding_model() -> SentenceTransformer:
28
  """
29
- Lazy-load the embedding model (singleton pattern).
30
- On Hugging Face Spaces, the model is downloaded on first use
31
- and cached in the Space's persistent storage.
32
  """
33
  global _embedding_model
34
  if _embedding_model is None:
35
- model_name = get_env("EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2")
 
 
 
36
  logger.info(f"Loading embedding model: {model_name}")
37
  _embedding_model = SentenceTransformer(model_name)
38
- logger.info("Embedding model loaded successfully")
39
  return _embedding_model
40
 
41
 
42
  def embed_texts(texts: list) -> np.ndarray:
43
  """
44
- Generate normalized embeddings for a list of texts.
45
 
46
- Normalization (L2) converts dot product to cosine similarity,
47
- which is what IndexFlatIP computes. This is the standard approach
48
- for semantic search with sentence-transformers.
49
 
50
- Returns: float32 array of shape (len(texts), 384)
51
  """
52
  if not texts:
53
- return np.array([], dtype=np.float32)
54
 
55
  model = get_embedding_model()
56
-
57
- # batch_size=32 balances memory and throughput on CPU
58
- # show_progress_bar=False keeps logs clean in production
59
  embeddings = model.encode(
60
  texts,
61
  batch_size=32,
62
- normalize_embeddings=True, # L2 normalize for cosine similarity
63
  show_progress_bar=False,
64
  convert_to_numpy=True,
65
  )
66
-
67
  return embeddings.astype(np.float32)
68
 
69
 
70
  def build_faiss_index(embeddings: np.ndarray) -> faiss.IndexFlatIP:
71
  """
72
- Build a FAISS flat inner-product index from embeddings.
73
-
74
- IndexFlatIP with L2-normalized vectors = exact cosine similarity search.
75
- No training required, no approximation, deterministic results.
76
-
77
- For > 500k vectors, consider IndexIVFFlat:
78
- quantizer = faiss.IndexFlatIP(EMBEDDING_DIM)
79
- index = faiss.IndexIVFFlat(quantizer, EMBEDDING_DIM, nlist=1024)
80
- index.train(embeddings)
81
  """
82
  if embeddings.shape[0] == 0:
83
- raise ValueError("Cannot build FAISS index from empty embeddings array")
84
 
85
  index = faiss.IndexFlatIP(EMBEDDING_DIM)
86
  index.add(embeddings)
87
- logger.info(f"FAISS index built: {index.ntotal} vectors, dim={EMBEDDING_DIM}")
88
  return index
89
 
90
 
@@ -97,20 +84,19 @@ def search_index(
97
  Retrieve top-k most similar chunks.
98
 
99
  Returns:
100
- scores: cosine similarity scores (float32 array, shape [top_k])
101
- indices: chunk indices in original list (int64 array, shape [top_k])
102
 
103
- Score interpretation:
104
- 1.0 = identical (query matches chunk exactly)
105
  0.8+ = highly relevant
106
  0.5-0.8 = moderately relevant
107
- < 0.5 = likely irrelevant — consider raising this threshold
108
  """
109
  if index.ntotal == 0:
110
- return np.array([]), np.array([])
111
 
112
  k = min(top_k, index.ntotal)
113
  query_2d = query_embedding.reshape(1, -1).astype(np.float32)
114
  scores, indices = index.search(query_2d, k)
115
-
116
  return scores[0], indices[0]
 
1
  """
2
  embeddings.py — Local embedding generation and FAISS index management.
3
 
4
+ all-MiniLM-L6-v2 runs on CPU with no API key or cost.
5
+ 384-dimensional embeddings, ~80MB model, loads in ~3 seconds on first call.
6
 
7
+ FAISS IndexFlatIP with L2-normalized vectors = exact cosine similarity search.
8
+ Correct choice for < 500k chunks (enterprise PDF use case).
 
9
  """
10
 
11
  import logging
 
16
 
17
  logger = logging.getLogger("enterprise-rag.embeddings")
18
 
 
 
19
  _embedding_model = None
20
+ EMBEDDING_DIM = 384
 
21
 
22
 
23
  def get_embedding_model() -> SentenceTransformer:
24
  """
25
+ Lazy-load the embedding model once and reuse across all calls.
26
+ Downloading happens on first use; cached in HF Spaces persistent storage.
 
27
  """
28
  global _embedding_model
29
  if _embedding_model is None:
30
+ model_name = get_env(
31
+ "EMBEDDING_MODEL",
32
+ "sentence-transformers/all-MiniLM-L6-v2"
33
+ )
34
  logger.info(f"Loading embedding model: {model_name}")
35
  _embedding_model = SentenceTransformer(model_name)
36
+ logger.info("Embedding model ready")
37
  return _embedding_model
38
 
39
 
40
  def embed_texts(texts: list) -> np.ndarray:
41
  """
42
+ Generate L2-normalized embeddings for a list of texts.
43
 
44
+ Normalization converts dot product to cosine similarity,
45
+ which is what IndexFlatIP computes. Standard for semantic search.
 
46
 
47
+ Returns float32 array of shape (len(texts), 384).
48
  """
49
  if not texts:
50
+ return np.zeros((0, EMBEDDING_DIM), dtype=np.float32)
51
 
52
  model = get_embedding_model()
 
 
 
53
  embeddings = model.encode(
54
  texts,
55
  batch_size=32,
56
+ normalize_embeddings=True,
57
  show_progress_bar=False,
58
  convert_to_numpy=True,
59
  )
 
60
  return embeddings.astype(np.float32)
61
 
62
 
63
  def build_faiss_index(embeddings: np.ndarray) -> faiss.IndexFlatIP:
64
  """
65
+ Build a FAISS flat inner-product index.
66
+ With L2-normalized vectors this equals exact cosine similarity search.
67
+ No training required, deterministic results.
 
 
 
 
 
 
68
  """
69
  if embeddings.shape[0] == 0:
70
+ raise ValueError("Cannot build FAISS index from empty embeddings.")
71
 
72
  index = faiss.IndexFlatIP(EMBEDDING_DIM)
73
  index.add(embeddings)
74
+ logger.info(f"FAISS index built: {index.ntotal} vectors")
75
  return index
76
 
77
 
 
84
  Retrieve top-k most similar chunks.
85
 
86
  Returns:
87
+ scores cosine similarity scores, float32 array shape [top_k]
88
+ indices chunk positions in original list, int64 array shape [top_k]
89
 
90
+ Score guide:
91
+ 1.0 = identical
92
  0.8+ = highly relevant
93
  0.5-0.8 = moderately relevant
94
+ < 0.5 = likely irrelevant
95
  """
96
  if index.ntotal == 0:
97
+ return np.array([], dtype=np.float32), np.array([], dtype=np.int64)
98
 
99
  k = min(top_k, index.ntotal)
100
  query_2d = query_embedding.reshape(1, -1).astype(np.float32)
101
  scores, indices = index.search(query_2d, k)
 
102
  return scores[0], indices[0]