Nebulixlabs commited on
Commit
c7d7cd9
Β·
verified Β·
1 Parent(s): 5db4905

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +70 -57
app.py CHANGED
@@ -7,7 +7,6 @@ import re
7
  import math
8
  import multiprocessing
9
  import urllib.parse
10
- import difflib
11
  import gc
12
  import sys
13
  import array
@@ -20,9 +19,13 @@ from fastapi import FastAPI, Query
20
  from fastapi.middleware.cors import CORSMiddleware
21
  from huggingface_hub import snapshot_download
22
 
 
 
 
23
  # --- CONFIGURATION & MULTIPROCESSING ---
24
  TOTAL_CORES = multiprocessing.cpu_count()
25
- print(f"πŸ–₯️ Total Cores Assigned for High-Speed Lexical Search: {TOTAL_CORES}")
 
26
 
27
  # Apka HF Token (Environment variable se ya direct yaha daal sakte hain)
28
  HF_TOKEN = os.getenv("HF_TOKEN", "YOUR_HF_TOKEN_HERE")
@@ -31,6 +34,10 @@ HF_TOKEN = os.getenv("HF_TOKEN", "YOUR_HF_TOKEN_HERE")
31
  QUERY_CACHE = {}
32
  MAX_CACHE_SIZE = 50000
33
 
 
 
 
 
34
  # --- 1. DETERMINISTIC PREPROCESSING & CONSTANTS ---
35
  STOP_WORDS = {"the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for", "of", "with", "is", "are", "by", "this", "that", "it", "what", "who", "where", "when", "why", "how"}
36
  ADULT_KEYWORDS = {
@@ -52,17 +59,16 @@ EXPANSION_DICT = {
52
  "js": ["javascript"]
53
  }
54
 
55
- # πŸ”₯ Tuned Weights for Best Ranking WITHOUT AI Dense Models
56
  WEIGHTS = {
57
- "W_BM25": 15.0, # Increased priority
58
  "W_TITLE": 10.0,
59
  "W_HEADINGS": 5.0,
60
  "W_DESCRIPTION": 3.0,
61
  "W_BODY": 1.5,
62
  "W_URL": 2.5,
63
  "W_DOMAIN": 3.5,
64
- "W_PHRASE": 8.0, # Exact phrase matching heavily rewarded
65
- "W_EXACT": 10.0, # Exact queries boosted
66
  "W_FRESHNESS": 2.0,
67
  "W_QUALITY": 3.0,
68
  "W_PROXIMITY": 6.0,
@@ -107,8 +113,6 @@ class HybridSearchIndex:
107
  self.docs = []
108
  self.total_docs = 0
109
  self.total_len = 0
110
- self.vocab = set()
111
- self.high_freq_vocab = []
112
  self.df = defaultdict(int)
113
  self.avg_doc_len = 0.0
114
 
@@ -141,7 +145,6 @@ class HybridSearchIndex:
141
  parsed_url = urllib.parse.urlparse(url)
142
  domain_str = sys.intern(parsed_url.netloc.lower().replace("www.", ""))
143
 
144
- # πŸ”₯ Extracting Image URL
145
  image_url = None
146
  open_graph = metadata.get("open_graph") or {}
147
  if isinstance(open_graph, dict):
@@ -158,7 +161,7 @@ class HybridSearchIndex:
158
  "_fast_snippet": snippet_str[:300],
159
  "_fast_content": main_text,
160
  "_fast_headings": (h1_str + " " + h2_str)[:200],
161
- "_fast_image": image_url # Hidden field for images output
162
  }
163
 
164
  title_tokens = tokenize(clean_doc["_fast_title"])
@@ -177,11 +180,9 @@ class HybridSearchIndex:
177
 
178
  self.docs.append(clean_doc)
179
 
180
- distinct_tokens = set(all_tokens)
181
- for term in distinct_tokens:
182
  self.inverted_index[term].append(doc_id)
183
  self.df[term] += 1
184
- self.vocab.add(term)
185
 
186
  if not clean_doc["_is_adult"]:
187
  clean_title = [w for w in title_tokens if w not in ADULT_KEYWORDS and len(w) > 2]
@@ -192,7 +193,6 @@ class HybridSearchIndex:
192
  self.total_docs = len(self.docs)
193
  self.avg_doc_len = self.total_len / max(1, self.total_docs)
194
  self.suggestions = self.suggestion_counter.most_common(20000)
195
- self.high_freq_vocab = [word for word, count in Counter(self.df).most_common(15000)]
196
  self.suggestion_counter.clear()
197
  gc.collect()
198
  print(f"πŸš€ Offline Search Engine Indexing Complete! Total Docs: {self.total_docs}")
@@ -205,13 +205,8 @@ class HybridSearchIndex:
205
  def _get_candidates(self, query_tokens: List[str]) -> Set[int]:
206
  candidates = set()
207
  for term in query_tokens:
208
- if term in self.inverted_index: candidates.update(self.inverted_index[term])
209
-
210
- if len(candidates) < 10 and self.high_freq_vocab:
211
- for term in query_tokens:
212
- if term not in self.inverted_index:
213
- matches = difflib.get_close_matches(term, self.high_freq_vocab, n=2, cutoff=0.8)
214
- for m in matches: candidates.update(self.inverted_index[m])
215
  return candidates
216
 
217
  def calculate_features(self, doc: dict, query_tokens_sorted: List[str], query_str: str) -> Dict[str, float]:
@@ -304,42 +299,51 @@ class HybridSearchIndex:
304
  if self.total_docs == 0:
305
  return [], [], {"intent": "GENERAL", "signals": []}, None
306
 
307
- query_norm = normalize_text(query)
308
- raw_tokens = query_norm.split()
309
-
310
- corrected_tokens = []
311
- is_corrected = False
312
 
313
- if self.high_freq_vocab:
314
- for t in raw_tokens:
315
- if t in self.vocab or t in EXPANSION_DICT or t in ADULT_KEYWORDS:
316
- corrected_tokens.append(t)
317
- else:
318
- matches = difflib.get_close_matches(t, self.high_freq_vocab, n=1, cutoff=0.85)
319
- if matches:
320
- corrected_tokens.append(matches[0])
321
- is_corrected = True
322
- else:
323
- corrected_tokens.append(t)
324
- else:
325
- corrected_tokens = raw_tokens
326
-
327
- corrected_query_str = " ".join(corrected_tokens) if is_corrected else None
328
- search_tokens = [t for t in corrected_tokens if t not in STOP_WORDS]
 
 
 
 
 
 
 
 
 
329
 
330
  expanded_tokens = list(search_tokens)
331
  for t in search_tokens:
332
  if t in EXPANSION_DICT: expanded_tokens.extend(EXPANSION_DICT[t])
333
 
334
  sorted_tokens = list(dict.fromkeys(expanded_tokens))
335
- if not sorted_tokens: sorted_tokens = corrected_tokens
336
 
337
  sorted_tokens.sort(key=lambda t: self.idf(t), reverse=True)
338
  intent_info = detect_query_intent(sorted_tokens)
339
  intent = intent_info["intent"]
340
  user_wants_adult = any(aw in query_norm for aw in ADULT_KEYWORDS)
341
 
342
- # 1. Pure Fast Lexical Retrieval
343
  candidates_ids = self._get_candidates(sorted_tokens)
344
 
345
  scored_results = []
@@ -386,7 +390,7 @@ class HybridSearchIndex:
386
  "domain": doc.get("_fast_domain", ""),
387
  "match_score": round(final_score, 4),
388
  "_internal_content": doc.get("_fast_content", ""),
389
- "_fast_image": doc.get("_fast_image", None) # Safe keeping for later
390
  }
391
  if debug:
392
  doc_out["_debug_features"] = features
@@ -395,13 +399,13 @@ class HybridSearchIndex:
395
 
396
  scored_results.sort(key=lambda x: x[0], reverse=True)
397
 
398
- # πŸ”₯ Extract Top Images globally first (before applying strict domain filters)
399
  global_images = []
400
- for score, doc_info, b_url, domain in scored_results[:20]: # Check top 20 best results for images
401
  img_url = doc_info.get("_fast_image")
402
  if img_url and img_url not in global_images:
403
  global_images.append(img_url)
404
- if len(global_images) >= 10: break # Collect Max 10 relevant images
405
 
406
  diversified_results = []
407
  for score, doc_info, b_url, domain in scored_results:
@@ -409,29 +413,36 @@ class HybridSearchIndex:
409
  seen_domains[domain] += 1
410
  seen_urls.add(b_url)
411
 
412
- doc_info.pop("_fast_image", None) # Remove it from URL output
413
  diversified_results.append(doc_info)
414
- if len(diversified_results) >= STRICT_RESULT_LIMIT: break # πŸ”₯ Top 5 URLs limit
415
 
416
  for idx, item in enumerate(diversified_results): item["rank"] = idx + 1
417
  return diversified_results, global_images, intent_info, corrected_query_str
418
 
419
  search_index = HybridSearchIndex()
420
 
421
- # --- DATA LOADER (OFFLINE + SYNC) ---
 
 
 
 
 
 
 
 
 
 
 
422
  def sync_and_load_datasets():
423
  print("πŸ“₯ Syncing Required Datasets securely...")
424
  try:
425
- # Sync Main Lexical dataset to memory
426
  dataset_path = snapshot_download(
427
  repo_id="Nebulixlabs/web-search-dataset",
428
  repo_type="dataset",
429
  allow_patterns="data/*.jsonl",
430
  token=HF_TOKEN
431
  )
432
-
433
- # Simply downloading the Private Vector repo as per your request for offline storage
434
- # (Vectors won't be queried live since you strictly requested NO AI MODEL for search engine)
435
  snapshot_download(
436
  repo_id="Nebulixlabs/web-search-faiss-index",
437
  repo_type="dataset",
@@ -440,7 +451,7 @@ def sync_and_load_datasets():
440
  print("βœ… Private Vector & Public Datasets Synced Offline!")
441
  except Exception as e:
442
  dataset_path = "."
443
- print(f"⚠️ Offline Fallback mode (No internet or token issue): {e}")
444
 
445
  data_folder = os.path.join(dataset_path, "data")
446
  if not os.path.exists(data_folder): data_folder = "data"
@@ -464,6 +475,8 @@ def sync_and_load_datasets():
464
  # --- FASTAPI APP ENDPOINTS ---
465
  @asynccontextmanager
466
  async def lifespan(app: FastAPI):
 
 
467
  asyncio.create_task(asyncio.to_thread(sync_and_load_datasets))
468
  yield
469
 
@@ -499,7 +512,7 @@ async def pure_search(q: str = Query(...), content: bool = Query(False), debug:
499
  "request_id": req_id,
500
  "query": q,
501
  "corrected_query": corrected_query,
502
- "images": global_images, # πŸ–ΌοΈ Images output globally list ki tarah yaha set hai!
503
  "results_count": len(top_5),
504
  "results": top_5
505
  }
 
7
  import math
8
  import multiprocessing
9
  import urllib.parse
 
10
  import gc
11
  import sys
12
  import array
 
19
  from fastapi.middleware.cors import CORSMiddleware
20
  from huggingface_hub import snapshot_download
21
 
22
+ import torch
23
+ from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
24
+
25
  # --- CONFIGURATION & MULTIPROCESSING ---
26
  TOTAL_CORES = multiprocessing.cpu_count()
27
+ torch.set_num_threads(TOTAL_CORES)
28
+ print(f"πŸ–₯️ Total Cores Assigned for High-Speed Search: {TOTAL_CORES}")
29
 
30
  # Apka HF Token (Environment variable se ya direct yaha daal sakte hain)
31
  HF_TOKEN = os.getenv("HF_TOKEN", "YOUR_HF_TOKEN_HERE")
 
34
  QUERY_CACHE = {}
35
  MAX_CACHE_SIZE = 50000
36
 
37
+ # --- LIGHTWEIGHT AI SPELL CHECKER (60M Params) ---
38
+ spell_tokenizer = None
39
+ spell_model = None
40
+
41
  # --- 1. DETERMINISTIC PREPROCESSING & CONSTANTS ---
42
  STOP_WORDS = {"the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for", "of", "with", "is", "are", "by", "this", "that", "it", "what", "who", "where", "when", "why", "how"}
43
  ADULT_KEYWORDS = {
 
59
  "js": ["javascript"]
60
  }
61
 
 
62
  WEIGHTS = {
63
+ "W_BM25": 15.0,
64
  "W_TITLE": 10.0,
65
  "W_HEADINGS": 5.0,
66
  "W_DESCRIPTION": 3.0,
67
  "W_BODY": 1.5,
68
  "W_URL": 2.5,
69
  "W_DOMAIN": 3.5,
70
+ "W_PHRASE": 8.0,
71
+ "W_EXACT": 10.0,
72
  "W_FRESHNESS": 2.0,
73
  "W_QUALITY": 3.0,
74
  "W_PROXIMITY": 6.0,
 
113
  self.docs = []
114
  self.total_docs = 0
115
  self.total_len = 0
 
 
116
  self.df = defaultdict(int)
117
  self.avg_doc_len = 0.0
118
 
 
145
  parsed_url = urllib.parse.urlparse(url)
146
  domain_str = sys.intern(parsed_url.netloc.lower().replace("www.", ""))
147
 
 
148
  image_url = None
149
  open_graph = metadata.get("open_graph") or {}
150
  if isinstance(open_graph, dict):
 
161
  "_fast_snippet": snippet_str[:300],
162
  "_fast_content": main_text,
163
  "_fast_headings": (h1_str + " " + h2_str)[:200],
164
+ "_fast_image": image_url
165
  }
166
 
167
  title_tokens = tokenize(clean_doc["_fast_title"])
 
180
 
181
  self.docs.append(clean_doc)
182
 
183
+ for term in set(all_tokens):
 
184
  self.inverted_index[term].append(doc_id)
185
  self.df[term] += 1
 
186
 
187
  if not clean_doc["_is_adult"]:
188
  clean_title = [w for w in title_tokens if w not in ADULT_KEYWORDS and len(w) > 2]
 
193
  self.total_docs = len(self.docs)
194
  self.avg_doc_len = self.total_len / max(1, self.total_docs)
195
  self.suggestions = self.suggestion_counter.most_common(20000)
 
196
  self.suggestion_counter.clear()
197
  gc.collect()
198
  print(f"πŸš€ Offline Search Engine Indexing Complete! Total Docs: {self.total_docs}")
 
205
  def _get_candidates(self, query_tokens: List[str]) -> Set[int]:
206
  candidates = set()
207
  for term in query_tokens:
208
+ if term in self.inverted_index:
209
+ candidates.update(self.inverted_index[term])
 
 
 
 
 
210
  return candidates
211
 
212
  def calculate_features(self, doc: dict, query_tokens_sorted: List[str], query_str: str) -> Dict[str, float]:
 
299
  if self.total_docs == 0:
300
  return [], [], {"intent": "GENERAL", "signals": []}, None
301
 
302
+ # ==========================================
303
+ # πŸ€– 1. TINY AI SPELL CORRECTION (Smart Logic)
304
+ # ==========================================
305
+ corrected_query_str = None
306
+ search_query = query
307
 
308
+ if spell_model is not None and spell_tokenizer is not None:
309
+ try:
310
+ # "correct grammar: " is required by this specific lightweight model
311
+ input_text = "correct grammar: " + query
312
+ input_ids = spell_tokenizer(input_text, return_tensors="pt").input_ids
313
+
314
+ # CPU-Optimized Fast Inference
315
+ with torch.inference_mode():
316
+ outputs = spell_model.generate(input_ids, max_length=20, num_beams=2, early_stopping=True)
317
+
318
+ ai_corrected = spell_tokenizer.decode(outputs[0], skip_special_tokens=True).strip()
319
+
320
+ # Validation: Punctuation hata kar match karte hain (ki meaning change na ho)
321
+ q_clean = normalize_text(query)
322
+ ai_clean = normalize_text(ai_corrected)
323
+
324
+ # Agar user ne galat type kiya hai tabhi change hoga, warna "ai for coding" same hi rahega
325
+ if q_clean != ai_clean and len(ai_clean) > 0:
326
+ corrected_query_str = ai_corrected
327
+ search_query = ai_corrected
328
+ except Exception as e:
329
+ pass # Fail safe mechanism
330
+
331
+ query_norm = normalize_text(search_query)
332
+ search_tokens = [t for t in query_norm.split() if t not in STOP_WORDS]
333
 
334
  expanded_tokens = list(search_tokens)
335
  for t in search_tokens:
336
  if t in EXPANSION_DICT: expanded_tokens.extend(EXPANSION_DICT[t])
337
 
338
  sorted_tokens = list(dict.fromkeys(expanded_tokens))
339
+ if not sorted_tokens: sorted_tokens = query_norm.split()
340
 
341
  sorted_tokens.sort(key=lambda t: self.idf(t), reverse=True)
342
  intent_info = detect_query_intent(sorted_tokens)
343
  intent = intent_info["intent"]
344
  user_wants_adult = any(aw in query_norm for aw in ADULT_KEYWORDS)
345
 
346
+ # 2. Pure Fast Lexical Retrieval
347
  candidates_ids = self._get_candidates(sorted_tokens)
348
 
349
  scored_results = []
 
390
  "domain": doc.get("_fast_domain", ""),
391
  "match_score": round(final_score, 4),
392
  "_internal_content": doc.get("_fast_content", ""),
393
+ "_fast_image": doc.get("_fast_image", None)
394
  }
395
  if debug:
396
  doc_out["_debug_features"] = features
 
399
 
400
  scored_results.sort(key=lambda x: x[0], reverse=True)
401
 
402
+ # πŸ”₯ Extract Top Images globally
403
  global_images = []
404
+ for score, doc_info, b_url, domain in scored_results[:20]:
405
  img_url = doc_info.get("_fast_image")
406
  if img_url and img_url not in global_images:
407
  global_images.append(img_url)
408
+ if len(global_images) >= 10: break
409
 
410
  diversified_results = []
411
  for score, doc_info, b_url, domain in scored_results:
 
413
  seen_domains[domain] += 1
414
  seen_urls.add(b_url)
415
 
416
+ doc_info.pop("_fast_image", None)
417
  diversified_results.append(doc_info)
418
+ if len(diversified_results) >= STRICT_RESULT_LIMIT: break
419
 
420
  for idx, item in enumerate(diversified_results): item["rank"] = idx + 1
421
  return diversified_results, global_images, intent_info, corrected_query_str
422
 
423
  search_index = HybridSearchIndex()
424
 
425
+ # --- DATA LOADER (OFFLINE + SYNC + AI CHECKER) ---
426
+ def load_tiny_ai():
427
+ global spell_tokenizer, spell_model
428
+ print("🧠 Loading Ultra-Lightweight T5 Spell Checker (~60M params) on CPU...")
429
+ try:
430
+ model_name = "AventIQ-AI/t5-small-grammar-correction"
431
+ spell_tokenizer = AutoTokenizer.from_pretrained(model_name)
432
+ spell_model = AutoModelForSeq2SeqLM.from_pretrained(model_name).eval() # Direct Eval mode for Speed
433
+ print("βœ… Tiny AI Spell Checker Loaded Successfully!")
434
+ except Exception as e:
435
+ print(f"⚠️ Could not load AI Spell Checker: {e}")
436
+
437
  def sync_and_load_datasets():
438
  print("πŸ“₯ Syncing Required Datasets securely...")
439
  try:
 
440
  dataset_path = snapshot_download(
441
  repo_id="Nebulixlabs/web-search-dataset",
442
  repo_type="dataset",
443
  allow_patterns="data/*.jsonl",
444
  token=HF_TOKEN
445
  )
 
 
 
446
  snapshot_download(
447
  repo_id="Nebulixlabs/web-search-faiss-index",
448
  repo_type="dataset",
 
451
  print("βœ… Private Vector & Public Datasets Synced Offline!")
452
  except Exception as e:
453
  dataset_path = "."
454
+ print(f"⚠️ Offline Fallback mode: {e}")
455
 
456
  data_folder = os.path.join(dataset_path, "data")
457
  if not os.path.exists(data_folder): data_folder = "data"
 
475
  # --- FASTAPI APP ENDPOINTS ---
476
  @asynccontextmanager
477
  async def lifespan(app: FastAPI):
478
+ # Dono kaam ek sath chalenge bina time waste kiye
479
+ asyncio.create_task(asyncio.to_thread(load_tiny_ai))
480
  asyncio.create_task(asyncio.to_thread(sync_and_load_datasets))
481
  yield
482
 
 
512
  "request_id": req_id,
513
  "query": q,
514
  "corrected_query": corrected_query,
515
+ "images": global_images,
516
  "results_count": len(top_5),
517
  "results": top_5
518
  }