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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +73 -127
app.py CHANGED
@@ -19,31 +19,18 @@ from typing import List, Dict, Any, Tuple, Set
19
  from fastapi import FastAPI, Query
20
  from fastapi.middleware.cors import CORSMiddleware
21
  from huggingface_hub import snapshot_download
22
- import torch
23
- import torch.nn.functional as F
24
- import faiss
25
-
26
- # Hugging Face imports (Only for embeddings now)
27
- from transformers import AutoTokenizer, AutoModel
28
 
29
  # --- CONFIGURATION & MULTIPROCESSING ---
30
  TOTAL_CORES = multiprocessing.cpu_count()
31
- torch.set_num_threads(TOTAL_CORES)
32
- faiss.omp_set_num_threads(TOTAL_CORES)
33
- print(f"🖥️ Total Cores Assigned for High-Speed Search: {TOTAL_CORES}")
34
 
35
- HF_TOKEN = os.getenv("HF_TOKEN")
 
36
 
37
  # Ultra-fast Cache for handling multiple users seamlessly
38
  QUERY_CACHE = {}
39
  MAX_CACHE_SIZE = 50000
40
 
41
- # --- DENSE RETRIEVAL GLOBALS (FAISS + E5) ---
42
- embed_tokenizer = None
43
- embed_model = None
44
- faiss_index = None
45
- faiss_metadata = []
46
-
47
  # --- 1. DETERMINISTIC PREPROCESSING & CONSTANTS ---
48
  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"}
49
  ADULT_KEYWORDS = {
@@ -65,16 +52,25 @@ EXPANSION_DICT = {
65
  "js": ["javascript"]
66
  }
67
 
68
- # Fine-tuned Weights for best Dense + Lexical Fusion
69
  WEIGHTS = {
70
- "W_BM25": 1.0, "W_TITLE": 4.0, "W_HEADINGS": 3.0, "W_DESCRIPTION": 2.0, "W_BODY": 1.0,
71
- "W_URL": 1.5, "W_DOMAIN": 1.5, "W_PHRASE": 2.5, "W_EXACT": 3.0, "W_FRESHNESS": 1.0,
72
- "W_QUALITY": 1.0, "W_POPULARITY": 0.5, "W_PROXIMITY": 1.5,
73
- "W_ROOT_DOMAIN": 8.0, "W_DENSE": 35.0 # Increased priority for Semantic Matching
 
 
 
 
 
 
 
 
 
74
  }
75
 
76
- MAX_RESULTS_PER_DOMAIN = 2 # Diversity constraint
77
- STRICT_RESULT_LIMIT = 5 # ✨ AS REQUESTED: Only return Top 5 URLs to boost speed
78
 
79
  def normalize_text(text: str) -> str:
80
  if not text: return ""
@@ -89,11 +85,6 @@ def tokenize(text: str, remove_stopwords: bool = True) -> List[str]:
89
  tokens = [t for t in tokens if t not in STOP_WORDS]
90
  return tokens
91
 
92
- # --- E5 POOLING FUNCTION ---
93
- def average_pool(last_hidden_states, attention_mask):
94
- last_hidden = last_hidden_states.masked_fill(~attention_mask[..., None].bool(), 0.0)
95
- return last_hidden.sum(dim=1) / attention_mask.sum(dim=1)[..., None]
96
-
97
  # --- 2. QUERY INTENT CLASSIFIER ---
98
  def detect_query_intent(query_tokens: List[str]) -> Dict[str, Any]:
99
  intents = []
@@ -110,7 +101,7 @@ def detect_query_intent(query_tokens: List[str]) -> Dict[str, Any]:
110
  if len(query_tokens) <= 2 and not intents: intents.append("NAVIGATIONAL")
111
  return {"intent": intents[0] if intents else "GENERAL", "signals": intents}
112
 
113
- # --- 3. CORE DETERMINISTIC + DENSE HYBRID INDEX ---
114
  class HybridSearchIndex:
115
  def __init__(self):
116
  self.docs = []
@@ -130,7 +121,6 @@ class HybridSearchIndex:
130
 
131
  def add_document(self, doc_json: dict):
132
  doc_id = len(self.docs)
133
-
134
  dataset_doc_id = doc_json.get("doc_id", "")
135
  if dataset_doc_id:
136
  self.dataset_id_to_internal[dataset_doc_id] = doc_id
@@ -151,6 +141,14 @@ class HybridSearchIndex:
151
  parsed_url = urllib.parse.urlparse(url)
152
  domain_str = sys.intern(parsed_url.netloc.lower().replace("www.", ""))
153
 
 
 
 
 
 
 
 
 
154
  clean_doc = {
155
  "url": url,
156
  "_fast_domain": domain_str,
@@ -159,7 +157,8 @@ class HybridSearchIndex:
159
  "_fast_desc": desc_str[:250],
160
  "_fast_snippet": snippet_str[:300],
161
  "_fast_content": main_text,
162
- "_fast_headings": (h1_str + " " + h2_str)[:200]
 
163
  }
164
 
165
  title_tokens = tokenize(clean_doc["_fast_title"])
@@ -196,7 +195,7 @@ class HybridSearchIndex:
196
  self.high_freq_vocab = [word for word, count in Counter(self.df).most_common(15000)]
197
  self.suggestion_counter.clear()
198
  gc.collect()
199
- print(f"🚀 Core Search Engine Indexing Complete! Total Docs: {self.total_docs}")
200
 
201
  def idf(self, term: str) -> float:
202
  df = self.df.get(term, 0)
@@ -301,9 +300,9 @@ class HybridSearchIndex:
301
 
302
  return scores
303
 
304
- def search(self, query: str, debug: bool = False) -> Tuple[List[dict], Dict[str, Any], str]:
305
  if self.total_docs == 0:
306
- return [], {"intent": "GENERAL", "signals": []}, None
307
 
308
  query_norm = normalize_text(query)
309
  raw_tokens = query_norm.split()
@@ -340,34 +339,9 @@ class HybridSearchIndex:
340
  intent = intent_info["intent"]
341
  user_wants_adult = any(aw in query_norm for aw in ADULT_KEYWORDS)
342
 
343
- # 1. Lexical Retrieval (Fast BM25 Base Candidates)
344
  candidates_ids = self._get_candidates(sorted_tokens)
345
 
346
- # 2. Dense Semantic Retrieval (FAISS - Top Priority)
347
- dense_scores_map = {}
348
- if faiss_index is not None and embed_model is not None and embed_tokenizer is not None:
349
- try:
350
- embed_inputs = embed_tokenizer(f"query: {query_norm}", return_tensors="pt", padding=True, truncation=True, max_length=512)
351
-
352
- # Use inference_mode for maximum PyTorch speed
353
- with torch.inference_mode():
354
- embed_outputs = embed_model(**embed_inputs)
355
- xq = average_pool(embed_outputs.last_hidden_state, embed_inputs["attention_mask"])
356
- xq = F.normalize(xq, p=2, dim=1).numpy()
357
-
358
- # Fetch Top 20 Contextual Matches via FAISS
359
- D, I = faiss_index.search(xq, k=20)
360
-
361
- for score, idx in zip(D[0], I[0]):
362
- if 0 <= idx < len(faiss_metadata):
363
- ds_id = faiss_metadata[idx].get("doc_id", "")
364
- if ds_id in self.dataset_id_to_internal:
365
- internal_id = self.dataset_id_to_internal[ds_id]
366
- dense_scores_map[internal_id] = float(score)
367
- candidates_ids.add(internal_id) # Merge into Hybrid Pool
368
- except Exception as e:
369
- print(f"⚠️ FAISS Search Warning: {e}")
370
-
371
  scored_results = []
372
  seen_domains = defaultdict(int)
373
  seen_urls = set()
@@ -386,12 +360,6 @@ class HybridSearchIndex:
386
  w_domain = WEIGHTS["W_DOMAIN"] + (4.0 if intent == "NAVIGATIONAL" else 0.0)
387
  w_title = WEIGHTS["W_TITLE"] + (2.0 if intent == "DEFINITION" else 0.0)
388
  w_phrase = WEIGHTS["W_PHRASE"] + (1.5 if intent in ("DEFINITION", "COMPARISON") else 0.0)
389
-
390
- dense_score = dense_scores_map.get(doc_id, 0.0)
391
- features["dense_score"] = dense_score
392
-
393
- # Massive Dense Boost (Threshold >= 0.75 gets heavily rewarded)
394
- dense_boost = max(0.0, dense_score - 0.75) * WEIGHTS["W_DENSE"]
395
 
396
  final_score = (
397
  WEIGHTS["W_BM25"] * norm_bm25 +
@@ -405,29 +373,9 @@ class HybridSearchIndex:
405
  WEIGHTS["W_PROXIMITY"] * features["proximity"] +
406
  WEIGHTS["W_ROOT_DOMAIN"] * features["root_domain"] +
407
  WEIGHTS["W_HEADINGS"] * features["headings"] +
408
- WEIGHTS["W_BODY"] * features["body"] +
409
- dense_boost
410
  )
411
 
412
- is_semantic_match = dense_score >= 0.81
413
-
414
- if len(sorted_tokens) > 0:
415
- top_term = sorted_tokens[0]
416
- doc_title = tokenize(doc.get("_fast_title", ""), False)
417
- doc_desc = tokenize(doc.get("_fast_desc", ""), False)
418
- doc_content = doc.get("_fast_content", "").lower()
419
- doc_headings = tokenize(doc.get("_fast_headings", ""), False)
420
-
421
- if top_term not in doc_title and top_term not in doc_desc and top_term not in doc.get("_fast_domain", "") and top_term not in doc_content and top_term not in doc_headings:
422
- if is_semantic_match: final_score *= 0.7
423
- else: final_score *= 0.1
424
-
425
- if len(sorted_tokens) > 1:
426
- second_term = sorted_tokens[1]
427
- if second_term not in doc_title and second_term not in doc_desc and second_term not in doc.get("_fast_domain", "") and second_term not in doc_content and second_term not in doc_headings:
428
- if is_semantic_match: final_score *= 0.85
429
- else: final_score *= 0.4
430
-
431
  if final_score > 0.1:
432
  doc_out = {
433
  "doc_id": str(doc_id),
@@ -437,61 +385,62 @@ class HybridSearchIndex:
437
  "url": doc.get("url", ""),
438
  "domain": doc.get("_fast_domain", ""),
439
  "match_score": round(final_score, 4),
440
- "_internal_content": doc.get("_fast_content", "")
 
441
  }
442
  if debug:
443
  doc_out["_debug_features"] = features
444
- doc_out["_debug_intent"] = intent_info
445
 
446
  scored_results.append((final_score, doc_out, base_url, doc["_fast_domain"]))
447
 
448
  scored_results.sort(key=lambda x: x[0], reverse=True)
449
 
 
 
 
 
 
 
 
 
450
  diversified_results = []
451
  for score, doc_info, b_url, domain in scored_results:
452
  if seen_domains[domain] >= MAX_RESULTS_PER_DOMAIN: continue
453
  seen_domains[domain] += 1
454
  seen_urls.add(b_url)
 
 
455
  diversified_results.append(doc_info)
456
- if len(diversified_results) >= STRICT_RESULT_LIMIT: break # 🔥 Only 5 URLs
457
 
458
  for idx, item in enumerate(diversified_results): item["rank"] = idx + 1
459
- return diversified_results, intent_info, corrected_query_str
460
 
461
  search_index = HybridSearchIndex()
462
 
463
- # --- DATA LOADER (FAISS & LEXICAL) ---
464
- def load_dense_retriever():
465
- global embed_tokenizer, embed_model, faiss_index, faiss_metadata
466
- print("🧠 Loading Lightning Fast E5-Small-v2 Embedder (CPU Optimized)...")
467
  try:
468
- embed_tokenizer = AutoTokenizer.from_pretrained("intfloat/e5-small-v2")
469
- # Load directly in eval mode for speed
470
- embed_model = AutoModel.from_pretrained("intfloat/e5-small-v2").eval()
471
- print("✅ E5 Embedder Ready!")
472
- except Exception as e:
473
- print(f"❌ E5 Loading Failed: {e}")
474
-
475
- print("📥 Downloading FAISS Index via HF_TOKEN...")
476
- try:
477
- dense_path = snapshot_download(repo_id="Nebulixlabs/embedded-data", repo_type="dataset", token=HF_TOKEN)
478
- index_file = os.path.join(dense_path, "dataset_embeddings.index")
479
- meta_file = os.path.join(dense_path, "metadata.json")
480
 
481
- if os.path.exists(index_file) and os.path.exists(meta_file):
482
- faiss_index = faiss.read_index(index_file)
483
- with open(meta_file, "r", encoding="utf-8") as f:
484
- faiss_metadata = json.load(f)
485
- print(f"✅ FAISS High-Speed Index Loaded! ({faiss_index.ntotal} vectors)")
486
- except Exception as e:
487
- print(f"❌ FAISS Loading Failed: {e}")
488
-
489
- def load_and_index_dataset():
490
- print("📦 Streaming 'web-search-dataset' ...")
491
- try:
492
- dataset_path = snapshot_download(repo_id="Nebulixlabs/web-search-dataset", repo_type="dataset", allow_patterns="data/*.jsonl", token=HF_TOKEN)
493
  except Exception as e:
494
  dataset_path = "."
 
495
 
496
  data_folder = os.path.join(dataset_path, "data")
497
  if not os.path.exists(data_folder): data_folder = "data"
@@ -515,11 +464,10 @@ def load_and_index_dataset():
515
  # --- FASTAPI APP ENDPOINTS ---
516
  @asynccontextmanager
517
  async def lifespan(app: FastAPI):
518
- asyncio.create_task(asyncio.to_thread(load_dense_retriever))
519
- asyncio.create_task(asyncio.to_thread(load_and_index_dataset))
520
  yield
521
 
522
- app = FastAPI(title="Nebulix Ultra-Fast Hybrid Engine", lifespan=lifespan)
523
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
524
 
525
  @app.get("/suggest")
@@ -533,16 +481,14 @@ async def pure_search(q: str = Query(...), content: bool = Query(False), debug:
533
  req_id = str(uuid.uuid4())
534
  cache_key = f"{normalize_text(q)}_{content}_{debug}"
535
 
536
- # ⚡ Cache Hit: Instant response for concurrent/repeated queries (0.001s)
537
  if cache_key in QUERY_CACHE:
538
  cached_response = QUERY_CACHE[cache_key]
539
  cached_response["request_id"] = req_id
540
  return cached_response
541
 
542
- # ⚡ asyncio.to_thread prevents Event Loop blocking so multiple users can search simultaneously
543
- results, intent_info, corrected_query = await asyncio.to_thread(search_index.search, q, debug)
544
 
545
- # ⚡ Strictly Top 5 limits for ultra-fast network transfer
546
  top_5 = results[:STRICT_RESULT_LIMIT]
547
 
548
  for r in top_5:
@@ -553,11 +499,11 @@ async def pure_search(q: str = Query(...), content: bool = Query(False), debug:
553
  "request_id": req_id,
554
  "query": q,
555
  "corrected_query": corrected_query,
 
556
  "results_count": len(top_5),
557
  "results": top_5
558
  }
559
 
560
- # Save to Cache
561
  if len(QUERY_CACHE) > MAX_CACHE_SIZE: QUERY_CACHE.clear()
562
  QUERY_CACHE[cache_key] = final_response
563
 
 
19
  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")
29
 
30
  # Ultra-fast Cache for handling multiple users seamlessly
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
  "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,
69
+ "W_ROOT_DOMAIN": 8.0
70
  }
71
 
72
+ MAX_RESULTS_PER_DOMAIN = 2
73
+ STRICT_RESULT_LIMIT = 5
74
 
75
  def normalize_text(text: str) -> str:
76
  if not text: return ""
 
85
  tokens = [t for t in tokens if t not in STOP_WORDS]
86
  return tokens
87
 
 
 
 
 
 
88
  # --- 2. QUERY INTENT CLASSIFIER ---
89
  def detect_query_intent(query_tokens: List[str]) -> Dict[str, Any]:
90
  intents = []
 
101
  if len(query_tokens) <= 2 and not intents: intents.append("NAVIGATIONAL")
102
  return {"intent": intents[0] if intents else "GENERAL", "signals": intents}
103
 
104
+ # --- 3. CORE OFFLINE HYBRID INDEX ---
105
  class HybridSearchIndex:
106
  def __init__(self):
107
  self.docs = []
 
121
 
122
  def add_document(self, doc_json: dict):
123
  doc_id = len(self.docs)
 
124
  dataset_doc_id = doc_json.get("doc_id", "")
125
  if dataset_doc_id:
126
  self.dataset_id_to_internal[dataset_doc_id] = doc_id
 
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):
148
+ img = open_graph.get("image")
149
+ if img and isinstance(img, str) and img.startswith("http"):
150
+ image_url = img
151
+
152
  clean_doc = {
153
  "url": url,
154
  "_fast_domain": domain_str,
 
157
  "_fast_desc": desc_str[:250],
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"])
 
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}")
199
 
200
  def idf(self, term: str) -> float:
201
  df = self.df.get(term, 0)
 
300
 
301
  return scores
302
 
303
+ def search(self, query: str, debug: bool = False) -> Tuple[List[dict], List[str], Dict[str, Any], str]:
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()
 
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 = []
346
  seen_domains = defaultdict(int)
347
  seen_urls = set()
 
360
  w_domain = WEIGHTS["W_DOMAIN"] + (4.0 if intent == "NAVIGATIONAL" else 0.0)
361
  w_title = WEIGHTS["W_TITLE"] + (2.0 if intent == "DEFINITION" else 0.0)
362
  w_phrase = WEIGHTS["W_PHRASE"] + (1.5 if intent in ("DEFINITION", "COMPARISON") else 0.0)
 
 
 
 
 
 
363
 
364
  final_score = (
365
  WEIGHTS["W_BM25"] * norm_bm25 +
 
373
  WEIGHTS["W_PROXIMITY"] * features["proximity"] +
374
  WEIGHTS["W_ROOT_DOMAIN"] * features["root_domain"] +
375
  WEIGHTS["W_HEADINGS"] * features["headings"] +
376
+ WEIGHTS["W_BODY"] * features["body"]
 
377
  )
378
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  if final_score > 0.1:
380
  doc_out = {
381
  "doc_id": str(doc_id),
 
385
  "url": doc.get("url", ""),
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
 
393
 
394
  scored_results.append((final_score, doc_out, base_url, doc["_fast_domain"]))
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:
408
  if seen_domains[domain] >= MAX_RESULTS_PER_DOMAIN: continue
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",
438
+ token=HF_TOKEN
439
+ )
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
  # --- 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
 
470
+ app = FastAPI(title="Nebulix Ultra-Fast Lexical Engine", lifespan=lifespan)
471
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
472
 
473
  @app.get("/suggest")
 
481
  req_id = str(uuid.uuid4())
482
  cache_key = f"{normalize_text(q)}_{content}_{debug}"
483
 
484
+ # ⚡ Cache Hit: Instant response
485
  if cache_key in QUERY_CACHE:
486
  cached_response = QUERY_CACHE[cache_key]
487
  cached_response["request_id"] = req_id
488
  return cached_response
489
 
490
+ results, global_images, intent_info, corrected_query = await asyncio.to_thread(search_index.search, q, debug)
 
491
 
 
492
  top_5 = results[:STRICT_RESULT_LIMIT]
493
 
494
  for r in top_5:
 
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
  }
506
 
 
507
  if len(QUERY_CACHE) > MAX_CACHE_SIZE: QUERY_CACHE.clear()
508
  QUERY_CACHE[cache_key] = final_response
509