Nebulixlabs commited on
Commit
8eabc8c
Β·
verified Β·
1 Parent(s): 4977670

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +42 -40
app.py CHANGED
@@ -12,7 +12,7 @@ import gc
12
  import sys
13
  import array
14
  import time
15
- import numpy as np # NEW: For ultra-fast vector math
16
  from datetime import datetime
17
  from collections import Counter, defaultdict
18
  from contextlib import asynccontextmanager
@@ -20,9 +20,9 @@ from typing import List, Dict, Any, Tuple, Set
20
 
21
  from fastapi import FastAPI, Query, Form
22
  from fastapi.middleware.cors import CORSMiddleware
23
- from huggingface_hub import snapshot_download, HfApi # NEW: HfApi for uploading
24
  import torch
25
- from openai import OpenAI # NEW: For embeddings
26
 
27
  # Hugging Face imports
28
  from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline, BitsAndBytesConfig
@@ -59,7 +59,7 @@ WEIGHTS = {
59
  "W_BM25": 1.0, "W_TITLE": 4.0, "W_HEADINGS": 3.0, "W_DESCRIPTION": 2.0, "W_BODY": 1.0,
60
  "W_URL": 1.5, "W_DOMAIN": 1.5, "W_PHRASE": 2.5, "W_EXACT": 3.0, "W_FRESHNESS": 1.0,
61
  "W_QUALITY": 1.0, "W_POPULARITY": 0.5, "W_PROXIMITY": 1.5, "W_ROOT_DOMAIN": 8.0,
62
- "W_VECTOR": 5.0 # NEW: Weight for Semantic Search
63
  }
64
 
65
  MAX_RESULTS_PER_DOMAIN = 3
@@ -74,7 +74,6 @@ def tokenize(text: str, remove_stopwords: bool = True) -> List[str]:
74
  if remove_stopwords: tokens = [t for t in tokens if t not in STOP_WORDS]
75
  return tokens
76
 
77
- # --- NEW: OPENROUTER EMBEDDING FUNCTION ---
78
  def get_embeddings(texts: List[str]) -> List[List[float]]:
79
  """Batched API call to OpenRouter to get vectors."""
80
  if not OPENROUTER_API_KEY: return []
@@ -102,9 +101,8 @@ class HybridSearchIndex:
102
 
103
  self.inverted_index = defaultdict(lambda: array.array('I'))
104
 
105
- # NEW: Vector Search Data
106
  self.raw_embeddings = []
107
- self.doc_vectors = None # NumPy matrix for ultra-fast math
108
 
109
  self.suggestion_counter = Counter()
110
  self.suggestions = []
@@ -132,12 +130,11 @@ class HybridSearchIndex:
132
  "has_vector": False
133
  }
134
 
135
- # NEW: Store embeddings if they exist in DB
136
  if "embedding" in doc_json and doc_json["embedding"]:
137
  self.raw_embeddings.append(doc_json["embedding"])
138
  clean_doc["has_vector"] = True
139
  else:
140
- self.raw_embeddings.append([0.0]*1024) # Placeholder for non-embedded (Model dims depend on nemotron, assumed 1024)
141
 
142
  all_tokens = tokenize(clean_doc["_fast_title"]) + tokenize(clean_doc["_fast_desc"]) + tokenize(clean_doc["_fast_content"])[:250]
143
  clean_doc["_token_len"] = len(all_tokens)
@@ -154,18 +151,16 @@ class HybridSearchIndex:
154
  self.total_docs = len(self.docs)
155
  self.avg_doc_len = self.total_len / max(1, self.total_docs)
156
 
157
- # NEW: Compile vectors into Ultra-Fast NumPy Matrix
158
  print("🧠 Compiling Vector Matrix for Fast Semantic Search...")
159
  try:
160
  vectors = np.array(self.raw_embeddings, dtype=np.float32)
161
- # L2 Normalize for Cosine Similarity via Dot Product (Fastest method)
162
  norms = np.linalg.norm(vectors, axis=1, keepdims=True)
163
  self.doc_vectors = np.where(norms > 0, vectors / norms, 0)
164
  except Exception as e:
165
  print("⚠️ Vector compile failed:", e)
166
  self.doc_vectors = None
167
 
168
- self.raw_embeddings = [] # Free RAM
169
  gc.collect()
170
  print(f"πŸš€ Indexing Complete! {self.total_docs} records ready.")
171
 
@@ -173,7 +168,6 @@ class HybridSearchIndex:
173
  query_norm = normalize_text(query)
174
  search_tokens = tokenize(query_norm)
175
 
176
- # 1. NEW: Get Query Vector
177
  query_vector = None
178
  vector_scores = None
179
  if self.doc_vectors is not None and OPENROUTER_API_KEY:
@@ -183,15 +177,12 @@ class HybridSearchIndex:
183
  q_norm_val = np.linalg.norm(q_vec)
184
  if q_norm_val > 0:
185
  q_vec = q_vec / q_norm_val
186
- # ULTRA-FAST Matrix Multiplication for all docs at once
187
  vector_scores = np.dot(self.doc_vectors, q_vec)
188
 
189
- # 2. Get Lexical Candidates
190
  candidates_ids = set()
191
  for term in search_tokens:
192
  if term in self.inverted_index: candidates_ids.update(self.inverted_index[term])
193
 
194
- # If Vector Search worked, add Top 50 semantic matches to candidates
195
  if vector_scores is not None:
196
  top_vector_indices = np.argsort(vector_scores)[-50:][::-1]
197
  candidates_ids.update(top_vector_indices)
@@ -202,18 +193,15 @@ class HybridSearchIndex:
202
  for doc_id in candidates_ids:
203
  doc = self.docs[doc_id]
204
 
205
- # --- Lexical Scoring (BM25) Simplified for brevity ---
206
  lexical_score = 0.0
207
  doc_text = (doc["_fast_title"] + " " + doc["_fast_desc"]).lower()
208
  for t in search_tokens:
209
  if t in doc_text: lexical_score += 2.0
210
 
211
- # --- Vector Scoring ---
212
  v_score = 0.0
213
  if vector_scores is not None and doc["has_vector"]:
214
  v_score = float(vector_scores[doc_id])
215
 
216
- # HYBRID FORMULA
217
  final_score = lexical_score + (WEIGHTS["W_VECTOR"] * v_score)
218
 
219
  if final_score > 0.1:
@@ -238,7 +226,6 @@ class HybridSearchIndex:
238
 
239
  search_index = HybridSearchIndex()
240
 
241
- # --- NEW: AUTO-EMBEDDING & HUGGINGFACE SYNC ---
242
  def load_and_index_dataset():
243
  print("πŸ“¦ Loading dataset and checking for Vectors...")
244
  dataset_path = snapshot_download(
@@ -250,6 +237,8 @@ def load_and_index_dataset():
250
  new_data_folder = "./updated_data"
251
  os.makedirs(new_data_folder, exist_ok=True)
252
 
 
 
253
  for file in os.listdir(data_folder):
254
  if not file.endswith(".jsonl"): continue
255
  file_path = os.path.join(data_folder, file)
@@ -261,53 +250,66 @@ def load_and_index_dataset():
261
  if not line.strip(): continue
262
  doc = json.loads(line)
263
 
264
- # Check if embedding is missing
265
  if "embedding" not in doc and OPENROUTER_API_KEY:
266
  batch_docs.append(doc)
267
- # Process in small batches to avoid OpenRouter rate limits
268
  if len(batch_docs) >= 20:
269
- texts = [f"{d.get('metadata',{}).get('title','')} {d.get('content',{}).get('snippet','')}" for d in batch_docs]
 
 
 
 
 
 
270
  embeddings = get_embeddings(texts)
271
 
272
  for i, d in enumerate(batch_docs):
273
- if i < len(embeddings): d["embedding"] = embeddings[i]
 
274
  out_f.write(json.dumps(d) + "\n")
275
  search_index.add_document(d)
276
 
277
  batch_docs = []
278
  needs_upload = True
279
- time.sleep(1) # Rate limit safety
 
 
 
280
  else:
281
  out_f.write(line)
282
  search_index.add_document(doc)
283
 
284
- # Leftover batch
285
  if batch_docs:
286
- texts = [f"{d.get('metadata',{}).get('title','')} {d.get('content',{}).get('snippet','')}" for d in batch_docs]
 
 
 
 
 
287
  embeddings = get_embeddings(texts)
288
  for i, d in enumerate(batch_docs):
289
- if i < len(embeddings): d["embedding"] = embeddings[i]
290
  out_f.write(json.dumps(d) + "\n")
291
  search_index.add_document(d)
292
  needs_upload = True
293
 
294
  search_index.finalize_build()
295
 
296
- # --- UPLOAD BACK TO HUGGING FACE ---
297
  if needs_upload and HF_TOKEN:
298
  print("☁️ New Embeddings generated! Uploading updated dataset to Hugging Face...")
299
- api = HfApi(token=HF_TOKEN)
300
- for file in os.listdir(new_data_folder):
301
- api.upload_file(
302
- path_or_fileobj=os.path.join(new_data_folder, file),
303
- path_in_repo=f"data/{file}",
304
- repo_id=HF_DATASET_REPO,
305
- repo_type="dataset"
306
- )
307
- print("βœ… Dataset successfully updated on Hugging Face!")
308
-
 
 
309
 
310
- # --- AI GENERATION MODULE (Unchanged) ---
311
  def load_ai_models():
312
  global ai_generator
313
  print("πŸ€– Loading SmolLM2...")
 
12
  import sys
13
  import array
14
  import time
15
+ import numpy as np
16
  from datetime import datetime
17
  from collections import Counter, defaultdict
18
  from contextlib import asynccontextmanager
 
20
 
21
  from fastapi import FastAPI, Query, Form
22
  from fastapi.middleware.cors import CORSMiddleware
23
+ from huggingface_hub import snapshot_download, HfApi
24
  import torch
25
+ from openai import OpenAI
26
 
27
  # Hugging Face imports
28
  from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline, BitsAndBytesConfig
 
59
  "W_BM25": 1.0, "W_TITLE": 4.0, "W_HEADINGS": 3.0, "W_DESCRIPTION": 2.0, "W_BODY": 1.0,
60
  "W_URL": 1.5, "W_DOMAIN": 1.5, "W_PHRASE": 2.5, "W_EXACT": 3.0, "W_FRESHNESS": 1.0,
61
  "W_QUALITY": 1.0, "W_POPULARITY": 0.5, "W_PROXIMITY": 1.5, "W_ROOT_DOMAIN": 8.0,
62
+ "W_VECTOR": 5.0 # Weight for Semantic Search
63
  }
64
 
65
  MAX_RESULTS_PER_DOMAIN = 3
 
74
  if remove_stopwords: tokens = [t for t in tokens if t not in STOP_WORDS]
75
  return tokens
76
 
 
77
  def get_embeddings(texts: List[str]) -> List[List[float]]:
78
  """Batched API call to OpenRouter to get vectors."""
79
  if not OPENROUTER_API_KEY: return []
 
101
 
102
  self.inverted_index = defaultdict(lambda: array.array('I'))
103
 
 
104
  self.raw_embeddings = []
105
+ self.doc_vectors = None
106
 
107
  self.suggestion_counter = Counter()
108
  self.suggestions = []
 
130
  "has_vector": False
131
  }
132
 
 
133
  if "embedding" in doc_json and doc_json["embedding"]:
134
  self.raw_embeddings.append(doc_json["embedding"])
135
  clean_doc["has_vector"] = True
136
  else:
137
+ self.raw_embeddings.append([0.0]*1024)
138
 
139
  all_tokens = tokenize(clean_doc["_fast_title"]) + tokenize(clean_doc["_fast_desc"]) + tokenize(clean_doc["_fast_content"])[:250]
140
  clean_doc["_token_len"] = len(all_tokens)
 
151
  self.total_docs = len(self.docs)
152
  self.avg_doc_len = self.total_len / max(1, self.total_docs)
153
 
 
154
  print("🧠 Compiling Vector Matrix for Fast Semantic Search...")
155
  try:
156
  vectors = np.array(self.raw_embeddings, dtype=np.float32)
 
157
  norms = np.linalg.norm(vectors, axis=1, keepdims=True)
158
  self.doc_vectors = np.where(norms > 0, vectors / norms, 0)
159
  except Exception as e:
160
  print("⚠️ Vector compile failed:", e)
161
  self.doc_vectors = None
162
 
163
+ self.raw_embeddings = []
164
  gc.collect()
165
  print(f"πŸš€ Indexing Complete! {self.total_docs} records ready.")
166
 
 
168
  query_norm = normalize_text(query)
169
  search_tokens = tokenize(query_norm)
170
 
 
171
  query_vector = None
172
  vector_scores = None
173
  if self.doc_vectors is not None and OPENROUTER_API_KEY:
 
177
  q_norm_val = np.linalg.norm(q_vec)
178
  if q_norm_val > 0:
179
  q_vec = q_vec / q_norm_val
 
180
  vector_scores = np.dot(self.doc_vectors, q_vec)
181
 
 
182
  candidates_ids = set()
183
  for term in search_tokens:
184
  if term in self.inverted_index: candidates_ids.update(self.inverted_index[term])
185
 
 
186
  if vector_scores is not None:
187
  top_vector_indices = np.argsort(vector_scores)[-50:][::-1]
188
  candidates_ids.update(top_vector_indices)
 
193
  for doc_id in candidates_ids:
194
  doc = self.docs[doc_id]
195
 
 
196
  lexical_score = 0.0
197
  doc_text = (doc["_fast_title"] + " " + doc["_fast_desc"]).lower()
198
  for t in search_tokens:
199
  if t in doc_text: lexical_score += 2.0
200
 
 
201
  v_score = 0.0
202
  if vector_scores is not None and doc["has_vector"]:
203
  v_score = float(vector_scores[doc_id])
204
 
 
205
  final_score = lexical_score + (WEIGHTS["W_VECTOR"] * v_score)
206
 
207
  if final_score > 0.1:
 
226
 
227
  search_index = HybridSearchIndex()
228
 
 
229
  def load_and_index_dataset():
230
  print("πŸ“¦ Loading dataset and checking for Vectors...")
231
  dataset_path = snapshot_download(
 
237
  new_data_folder = "./updated_data"
238
  os.makedirs(new_data_folder, exist_ok=True)
239
 
240
+ docs_embedded = 0
241
+
242
  for file in os.listdir(data_folder):
243
  if not file.endswith(".jsonl"): continue
244
  file_path = os.path.join(data_folder, file)
 
250
  if not line.strip(): continue
251
  doc = json.loads(line)
252
 
 
253
  if "embedding" not in doc and OPENROUTER_API_KEY:
254
  batch_docs.append(doc)
 
255
  if len(batch_docs) >= 20:
256
+ texts = []
257
+ for d in batch_docs:
258
+ # FIX 1: Handle empty text so we don't get 400 Bad Request error
259
+ raw_text = f"{d.get('metadata',{}).get('title','')} {d.get('content',{}).get('snippet','')}".strip()
260
+ if len(raw_text) < 2: raw_text = "empty document"
261
+ texts.append(raw_text)
262
+
263
  embeddings = get_embeddings(texts)
264
 
265
  for i, d in enumerate(batch_docs):
266
+ if embeddings and i < len(embeddings):
267
+ d["embedding"] = embeddings[i]
268
  out_f.write(json.dumps(d) + "\n")
269
  search_index.add_document(d)
270
 
271
  batch_docs = []
272
  needs_upload = True
273
+ docs_embedded += 20
274
+ print(f"πŸ”„ Embedded {docs_embedded} new docs... (Waiting 4s to prevent Rate Limits ⏳)")
275
+ # FIX 2: Wait 4 seconds (instead of 1s) to stay under Free Tier 20 limit!
276
+ time.sleep(4)
277
  else:
278
  out_f.write(line)
279
  search_index.add_document(doc)
280
 
 
281
  if batch_docs:
282
+ texts = []
283
+ for d in batch_docs:
284
+ raw_text = f"{d.get('metadata',{}).get('title','')} {d.get('content',{}).get('snippet','')}".strip()
285
+ if len(raw_text) < 2: raw_text = "empty document"
286
+ texts.append(raw_text)
287
+
288
  embeddings = get_embeddings(texts)
289
  for i, d in enumerate(batch_docs):
290
+ if embeddings and i < len(embeddings): d["embedding"] = embeddings[i]
291
  out_f.write(json.dumps(d) + "\n")
292
  search_index.add_document(d)
293
  needs_upload = True
294
 
295
  search_index.finalize_build()
296
 
 
297
  if needs_upload and HF_TOKEN:
298
  print("☁️ New Embeddings generated! Uploading updated dataset to Hugging Face...")
299
+ try:
300
+ api = HfApi(token=HF_TOKEN)
301
+ for file in os.listdir(new_data_folder):
302
+ api.upload_file(
303
+ path_or_fileobj=os.path.join(new_data_folder, file),
304
+ path_in_repo=f"data/{file}",
305
+ repo_id=HF_DATASET_REPO,
306
+ repo_type="dataset"
307
+ )
308
+ print("βœ… Dataset successfully updated on Hugging Face!")
309
+ except Exception as e:
310
+ print(f"⚠️ Dataset upload failed: {e}")
311
 
312
+ # --- AI GENERATION MODULE ---
313
  def load_ai_models():
314
  global ai_generator
315
  print("πŸ€– Loading SmolLM2...")