Nebulixlabs commited on
Commit
61f9576
·
verified ·
1 Parent(s): 3e0afff

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +440 -239
app.py CHANGED
@@ -4,42 +4,49 @@ import asyncio
4
  import os
5
  import uvicorn
6
  import re
 
 
7
  import multiprocessing
8
  import urllib.parse
9
- from collections import Counter
 
 
 
10
  from contextlib import asynccontextmanager
 
 
11
  from fastapi import FastAPI, Query, Form
12
  from fastapi.responses import JSONResponse
13
  from fastapi.middleware.cors import CORSMiddleware
14
  from huggingface_hub import hf_hub_download
15
- from rank_bm25 import BM25Okapi
16
- import torch
17
  import aiohttp
18
  from bs4 import BeautifulSoup
 
19
 
20
  # Hugging Face imports for Q4 Quantization
21
  from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline, BitsAndBytesConfig
22
 
23
- # --- 1. SETUP & CPU ALLOCATION ---
24
  TOTAL_CORES = multiprocessing.cpu_count()
25
  AI_CORES = max(1, int(TOTAL_CORES * 0.7))
26
  torch.set_num_threads(AI_CORES)
27
  print(f"🖥️ Total Cores: {TOTAL_CORES} | Dedicated AI Cores: {AI_CORES}")
28
 
29
- raw_data = []
30
- suggestion_tuples = []
31
- bm25_index = None
32
  MAX_CONCURRENT_AI = max(1, AI_CORES // 2)
33
  ai_semaphore = asyncio.Semaphore(MAX_CONCURRENT_AI)
34
  ai_generator = None
35
- URL_STATS = {}
 
 
36
  AI_RESPONSE_CACHE = {}
37
- MAX_CACHE_SIZE = 5000
38
 
39
- TOKEN_RE = re.compile(r'[a-z0-9]+')
40
- STOP_WORDS = {"the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for", "of", "with", "is", "are", "by", "this", "that", "it"}
41
- HF_TOKEN = os.getenv("HF_TOKEN")
42
 
 
 
43
  ADULT_KEYWORDS = {
44
  "porn", "sex", "nude", "nsfw", "xxx", "adult", "erotic", "onlyfans",
45
  "mms", "chudai", "bhabhi", "devar", "savita bhabhi", "xvideos", "pornhub",
@@ -49,29 +56,332 @@ ADULT_KEYWORDS = {
49
  "sex tape", "milf", "stepmom", "incest", "playboy", "scandal", "leaked"
50
  }
51
 
52
- def tokenize(text):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  if not text: return []
54
- return [t for t in TOKEN_RE.findall(str(text).lower()) if t not in STOP_WORDS]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
  def load_and_index_dataset():
57
- global bm25_index, raw_data, suggestion_tuples
58
- print("📥 Loading Dataset & Building Fast Index...")
59
  dataset_path = "dataset.jsonl"
60
-
61
  if HF_TOKEN:
62
- try:
63
- dataset_path = hf_hub_download(
64
- repo_id="Nebulixlabs/Ultimate-Search-Engine-Dataset",
65
- filename="dataset.jsonl",
66
- repo_type="dataset",
67
- token=HF_TOKEN
68
- )
69
- except Exception:
70
- pass
71
 
72
- tokenized_corpus = []
73
- suggestion_counter = Counter()
74
-
75
  if os.path.exists(dataset_path):
76
  with open(dataset_path, "r", encoding="utf-8") as f:
77
  for i, line in enumerate(f):
@@ -82,40 +392,18 @@ def load_and_index_dataset():
82
  url = doc.get("url", "")
83
  title_str = doc.get("title", "")
84
  desc_str = doc.get("description", "")
85
-
86
  parsed_url = urllib.parse.urlparse(url)
87
- domain = parsed_url.netloc.lower().replace("www.", "")
88
- clean_domain_name = domain.split('.')[0] if '.' in domain else domain
89
-
90
- doc_text = f"{title_str} {desc_str} {url}".lower()
91
- is_adult = doc.get("tag", "general").lower() in ["adult", "nsfw"] or any(aw in doc_text for aw in ADULT_KEYWORDS)
92
-
93
- doc["_fast_domain"] = domain
94
  doc["_fast_path"] = parsed_url.path.strip("/")
95
- doc["_fast_title"] = title_str.lower()
96
- doc["_fast_desc"] = desc_str.lower()
97
- doc["_is_adult"] = is_adult
98
  raw_data.append(doc)
99
-
100
- if not is_adult:
101
- if len(clean_domain_name) > 2 and clean_domain_name not in ADULT_KEYWORDS:
102
- suggestion_counter[clean_domain_name] += 5
103
-
104
- clean_t_tokens = [w for w in tokenize(title_str) if w not in ADULT_KEYWORDS]
105
- for w in clean_t_tokens:
106
- if len(w) > 2: suggestion_counter[w] += 1
107
- for j in range(len(clean_t_tokens) - 1):
108
- suggestion_counter[f"{clean_t_tokens[j]} {clean_t_tokens[j+1]}"] += 2
109
-
110
- tokenized_corpus.append((tokenize(title_str) * 5) + (tokenize(desc_str) * 3) + tokenize(url))
111
  except: pass
112
-
113
- if tokenized_corpus:
114
- suggestion_tuples = suggestion_counter.most_common(40000)
115
- bm25_index = BM25Okapi(tokenized_corpus)
116
- print(f"🚀 Indexing Complete! {len(raw_data)} records ready.")
117
 
118
- # --- QWEN 2.5 (0.5B) + Q4 OPTIMIZATION ---
119
  def load_ai_models():
120
  global ai_generator
121
  print("🤖 Loading Qwen/Qwen2.5-0.5B-Instruct...")
@@ -123,224 +411,137 @@ def load_ai_models():
123
  model_id = "Qwen/Qwen2.5-0.5B-Instruct"
124
  tokenizer = AutoTokenizer.from_pretrained(model_id)
125
 
126
- # Smart Hardware Check for 4-bit (Q4) Quantization
127
  if torch.cuda.is_available():
128
- print("⚡ GPU detected! Loading in 4-bit (Q4) using BitsAndBytes for Extreme Speed...")
129
- quant_config = BitsAndBytesConfig(
130
- load_in_4bit=True,
131
- bnb_4bit_compute_dtype=torch.float16,
132
- bnb_4bit_quant_type="nf4",
133
- )
134
- model = AutoModelForCausalLM.from_pretrained(
135
- model_id,
136
- quantization_config=quant_config,
137
- device_map="auto"
138
- )
139
  else:
140
- print("🖥️ CPU detected! Loading optimized model (PyTorch execution)...")
141
- # For CPU, 4-bit BitsAndBytes isn't natively optimal, so we load in standard float32
142
- # Qwen 0.5B is already tiny (~1GB) so it runs perfectly fast on CPU too.
143
- model = AutoModelForCausalLM.from_pretrained(
144
- model_id,
145
- device_map="cpu",
146
- torch_dtype=torch.float32
147
- )
148
 
149
- ai_generator = pipeline(
150
- "text-generation",
151
- model=model,
152
- tokenizer=tokenizer
153
- )
154
- print("✅ Qwen2.5-0.5B-Instruct (Superfast Mode) Ready!")
155
  except Exception as e:
156
  print(f"❌ Model Loading Failed: {e}")
157
 
158
- # --- AI TEXT GENERATION FIX ---
159
- def _generate_text(messages, max_tokens: int):
160
- tokenizer = ai_generator.tokenizer
161
-
162
- # 1. Use Native Chat Template for Qwen (Makes answers highly accurate & stops hallucinations)
163
- try:
164
- prompt = tokenizer.apply_chat_template(
165
- messages,
166
- tokenize=False,
167
- add_generation_prompt=True
168
- )
169
- except Exception:
170
- prompt = "\n".join([f"{m['role'].capitalize()}: {m['content']}" for m in messages]) + "\nAssistant: "
171
-
172
- # 2. Greedy Decoding (do_sample=False) + Return Setup
173
- result = ai_generator(
174
- prompt,
175
- max_new_tokens=max_tokens,
176
- do_sample=False,
177
- truncation=True,
178
- return_full_text=False, # ❌ FIX: No more repeating the query in answers!
179
- pad_token_id=tokenizer.eos_token_id if tokenizer.eos_token_id is not None else 0
180
- )
181
-
182
- text_out = result[0]['generated_text'].strip()
183
- return text_out
184
-
185
- async def run_ai_task(messages, max_tokens: int, query_key: str):
186
  if not ai_generator: return "⚠️ AI Model is starting up."
187
- if query_key in AI_RESPONSE_CACHE: return AI_RESPONSE_CACHE[query_key]
188
-
189
  async with ai_semaphore:
190
- response = await asyncio.to_thread(_generate_text, messages, max_tokens)
191
- if len(AI_RESPONSE_CACHE) > MAX_CACHE_SIZE: AI_RESPONSE_CACHE.clear()
192
- AI_RESPONSE_CACHE[query_key] = response
 
 
 
 
 
 
 
 
 
193
  return response
194
 
195
- def chunk_text(text, chunk_size=80):
196
- words = text.split()
197
- return [" ".join(words[i:i+chunk_size]) for i in range(0, len(words), chunk_size)]
198
-
199
- async def scrape_and_find_best_chunk(url, query):
200
- try:
201
- async with aiohttp.ClientSession() as session:
202
- async with session.get(url, timeout=1.5) as resp:
203
- if resp.status == 200:
204
- html = await resp.text()
205
- soup = BeautifulSoup(html, "html.parser")
206
- text = " ".join([tag.get_text(strip=True) for tag in soup.find_all(['p', 'h1', 'h2'])])
207
- if not text: return ""
208
-
209
- chunks = chunk_text(text)
210
- query_tokens = set(tokenize(query))
211
-
212
- best_chunk, max_score = "", 0
213
- for chunk in chunks:
214
- score = len(query_tokens.intersection(tokenize(chunk)))
215
- if score > max_score:
216
- max_score, best_chunk = score, chunk
217
- if max_score >= max(1, len(query_tokens) // 2): break
218
- return best_chunk
219
- except:
220
- return ""
221
- return ""
222
-
223
- async def perform_core_search(raw_query: str):
224
- if not raw_data or not bm25_index: return [], "Search index not ready.", 0
225
-
226
- query = raw_query.strip().lower()
227
- query_tokens = tokenize(query)
228
- user_wants_adult = any(aw in query for aw in ADULT_KEYWORDS)
229
-
230
- scores = bm25_index.get_scores(query_tokens) if query_tokens else [1.0] * len(raw_data)
231
- scored_results, seen_urls = [], set()
232
- query_clean = query.replace(" ", "")
233
-
234
- for index, score in enumerate(scores):
235
- if score > 0.1 or not query_tokens:
236
- doc = raw_data[index]
237
- doc_url = doc["url"]
238
- if doc_url in seen_urls: continue
239
- if not user_wants_adult and doc["_is_adult"]: continue
240
-
241
- final_score, domain = score, doc["_fast_domain"]
242
- domain_clean = domain.replace(" ", "")
243
- doc_title, doc_desc = doc["_fast_title"], doc["_fast_desc"]
244
-
245
- if query_clean == domain_clean or domain_clean.startswith(query_clean + "."):
246
- final_score *= 50.0 if not doc["_fast_path"] else 10.0
247
- elif query_clean in domain_clean:
248
- final_score *= 25.0 if not doc["_fast_path"] else 5.0
249
-
250
- if query in doc_title: final_score *= 5.0
251
- elif all(w in doc_title for w in query_tokens): final_score *= 3.0
252
- if query in doc_desc: final_score *= 2.0
253
-
254
- stats = URL_STATS.get(doc_url, {"user_visits": 0, "ai_visits": 0, "is_trusted": False})
255
- if stats.get("is_trusted", False): final_score *= 2.0
256
- final_score += (stats.get("user_visits", 0) * 0.05) + (stats.get("ai_visits", 0) * 0.1)
257
-
258
- seen_urls.add(doc_url)
259
- doc_copy = {k: v for k, v in doc.items() if not k.startswith("_")}
260
- doc_copy["trust_stats"] = stats
261
- scored_results.append((final_score, doc_copy))
262
-
263
- scored_results.sort(key=lambda x: x[0], reverse=True)
264
- best_score = scored_results[0][0] if scored_results else 0
265
- prediction = "High confidence" if best_score > 15 else "Medium confidence" if best_score > 5 else "Low confidence"
266
-
267
- return [{"match_score": round(s, 2), **doc} for s, doc in scored_results[:100]], prediction, best_score
268
-
269
  @asynccontextmanager
270
  async def lifespan(app: FastAPI):
271
  asyncio.create_task(asyncio.to_thread(load_and_index_dataset))
272
  asyncio.create_task(asyncio.to_thread(load_ai_models))
273
  yield
274
 
275
- app = FastAPI(title="Nebulix Fast Qwen AI Search", lifespan=lifespan)
276
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
277
 
278
  @app.get("/suggest")
279
  async def get_suggestions(q: str = Query(..., min_length=1)):
280
- q = q.lower().strip()
281
- matches = [word for word, count in suggestion_tuples if word.startswith(q)][:7]
282
  return {"query": q, "suggestions": matches}
283
 
284
- @app.get("/search")
285
- async def pure_search(q: str = Query(...)):
286
- req_id = str(uuid.uuid4())
287
- results, prediction, _ = await perform_core_search(q)
288
- return {"request_id": req_id, "query": q, "results_count": len(results), "results": results}
289
 
290
- @app.get("/aimode")
291
- async def ai_mode_short(q: str = Query(...)):
292
  req_id = str(uuid.uuid4())
293
- results, _, _ = await perform_core_search(q)
294
- if not results: return {"request_id": req_id, "ai_response": "I couldn't find any results.", "results": []}
295
-
296
- context = " ".join([f"{r.get('title', '')} - {r.get('description', '')}" for r in results[:2]])
297
- messages = [
298
- {"role": "system", "content": "You are a concise search AI. Provide a direct, 1-sentence answer based on the context. Highlight keywords in (parentheses)."},
299
- {"role": "user", "content": f"Context: {context}\n\nQuestion: {q}"}
300
- ]
301
- ai_reply = await run_ai_task(messages, 50, query_key=f"short_{q}")
302
- return {"request_id": req_id, "ai_response": ai_reply, "results": results}
 
 
303
 
304
- async def process_rag_chat(q: str):
305
  req_id = str(uuid.uuid4())
306
- needs_rag = any(w in q.lower() for w in ["what", "who", "where", "how", "why", "tell me", "explain", "latest", "best", "news"])
307
- results, _, _ = await perform_core_search(q)
308
 
309
- rag_context = ""
 
 
 
 
 
 
 
 
310
  used_sources = []
311
 
312
- if needs_rag and results:
313
- top_urls = results[:3]
314
- tasks = [scrape_and_find_best_chunk(doc["url"], q) for doc in top_urls]
315
- scraped_chunks = await asyncio.gather(*tasks)
316
 
317
- context_parts = []
318
- for i, chunk in enumerate(scraped_chunks):
319
- doc = top_urls[i]
320
- url = doc["url"]
321
- if chunk:
322
- context_parts.append(f"Source [{i+1}] ({url}): {chunk}")
323
- used_sources.append(doc)
324
- else:
325
- context_parts.append(f"Source [{i+1}] ({url}): {doc.get('description', '')}")
326
- used_sources.append(doc)
327
 
328
- rag_context = "\n".join(context_parts)
329
-
330
- system_prompt = "You are a Smart AI Search Assistant. Provide a short, direct answer. ENCLOSE IMPORTANT KEYWORDS IN (PARENTHESES). If you use information from a Source, cite it."
 
 
 
 
 
 
 
 
 
 
 
331
  messages = [
332
  {"role": "system", "content": system_prompt},
333
- {"role": "user", "content": f"Live Context Data:\n{rag_context}\n\nQuestion: {q}" if needs_rag else f"Question: {q}"}
334
  ]
335
 
336
- ai_reply = await run_ai_task(messages, 120, query_key=f"chat_{q}_{needs_rag}")
337
- return {"request_id": req_id, "ai_response": ai_reply, "rag_used": needs_rag, "sources": used_sources, "results": results[:15]}
 
 
 
 
 
 
 
 
 
338
 
339
- @app.get("/aimodechat")
340
- async def ai_mode_chat_get(q: str = Query(...)): return await process_rag_chat(q)
 
341
 
342
- @app.post("/aimodechat")
343
- async def ai_mode_chat_post(q: str = Form(...)): return await process_rag_chat(q)
 
344
 
345
  if __name__ == "__main__":
346
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
4
  import os
5
  import uvicorn
6
  import re
7
+ import math
8
+ import time
9
  import multiprocessing
10
  import urllib.parse
11
+ import unicodedata
12
+ import difflib
13
+ from datetime import datetime
14
+ from collections import Counter, defaultdict
15
  from contextlib import asynccontextmanager
16
+ from typing import List, Dict, Any, Tuple, Set
17
+
18
  from fastapi import FastAPI, Query, Form
19
  from fastapi.responses import JSONResponse
20
  from fastapi.middleware.cors import CORSMiddleware
21
  from huggingface_hub import hf_hub_download
 
 
22
  import aiohttp
23
  from bs4 import BeautifulSoup
24
+ import torch
25
 
26
  # Hugging Face imports for Q4 Quantization
27
  from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline, BitsAndBytesConfig
28
 
29
+ # --- CONFIGURATION & MULTIPROCESSING ---
30
  TOTAL_CORES = multiprocessing.cpu_count()
31
  AI_CORES = max(1, int(TOTAL_CORES * 0.7))
32
  torch.set_num_threads(AI_CORES)
33
  print(f"🖥️ Total Cores: {TOTAL_CORES} | Dedicated AI Cores: {AI_CORES}")
34
 
 
 
 
35
  MAX_CONCURRENT_AI = max(1, AI_CORES // 2)
36
  ai_semaphore = asyncio.Semaphore(MAX_CONCURRENT_AI)
37
  ai_generator = None
38
+ HF_TOKEN = os.getenv("HF_TOKEN")
39
+
40
+ # Cache Structures
41
  AI_RESPONSE_CACHE = {}
42
+ MAX_AI_CACHE_SIZE = 5000
43
 
44
+ CONTENT_CACHE = {}
45
+ MAX_CONTENT_CONCURRENCY = 10
46
+ content_semaphore = asyncio.Semaphore(MAX_CONTENT_CONCURRENCY)
47
 
48
+ # --- 1. DETERMINISTIC PREPROCESSING & CONSTANTS ---
49
+ STOP_WORDS = {"the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for", "of", "with", "is", "are", "by", "this", "that", "it"}
50
  ADULT_KEYWORDS = {
51
  "porn", "sex", "nude", "nsfw", "xxx", "adult", "erotic", "onlyfans",
52
  "mms", "chudai", "bhabhi", "devar", "savita bhabhi", "xvideos", "pornhub",
 
56
  "sex tape", "milf", "stepmom", "incest", "playboy", "scandal", "leaked"
57
  }
58
 
59
+ EXPANSION_DICT = {
60
+ "usa": ["united", "states"],
61
+ "uk": ["united", "kingdom"],
62
+ "ai": ["artificial", "intelligence"],
63
+ "ml": ["machine", "learning"],
64
+ "nlp": ["natural", "language", "processing"],
65
+ "js": ["javascript"]
66
+ }
67
+
68
+ WEIGHTS = {
69
+ "W_BM25": 1.0, "W_TITLE": 4.0, "W_HEADINGS": 3.0, "W_DESCRIPTION": 2.0, "W_BODY": 1.0,
70
+ "W_URL": 1.5, "W_DOMAIN": 1.5, "W_PHRASE": 2.5, "W_EXACT": 3.0, "W_FRESHNESS": 1.0,
71
+ "W_QUALITY": 1.0, "W_POPULARITY": 0.5, "W_PROXIMITY": 1.5
72
+ }
73
+
74
+ MAX_RESULTS_PER_DOMAIN = 3
75
+ STRICT_RESULT_LIMIT = 20
76
+
77
+ def normalize_text(text: str) -> str:
78
+ if not text: return ""
79
+ text = unicodedata.normalize('NFKD', str(text)).lower()
80
+ text = re.sub(r'[\'’]', '', text)
81
+ text = re.sub(r'[^a-z0-9\-]', ' ', text)
82
+ return " ".join(text.split())
83
+
84
+ def tokenize(text: str, remove_stopwords: bool = True) -> List[str]:
85
+ tokens = normalize_text(text).split()
86
+ if remove_stopwords:
87
+ tokens = [t for t in tokens if t not in STOP_WORDS]
88
+ return tokens
89
+
90
+ # --- 2. DETERMINISTIC QUERY INTENT CLASSIFIER ---
91
+ def detect_query_intent(query_tokens: List[str]) -> Dict[str, Any]:
92
+ intents = []
93
+
94
+ news_terms = {"latest", "news", "today", "breaking", "recent", "updates", "current"}
95
+ if any(term in query_tokens for term in news_terms): intents.append("NEWS")
96
+
97
+ def_terms = {"what", "who", "definition", "meaning"}
98
+ if any(term in query_tokens for term in def_terms): intents.append("DEFINITION")
99
+
100
+ how_terms = {"how", "tutorial", "guide", "steps", "learn", "documentation"}
101
+ if any(term in query_tokens for term in how_terms): intents.append("HOW_TO")
102
+
103
+ comp_terms = {"vs", "versus", "compare", "comparison", "difference", "best"}
104
+ if any(term in query_tokens for term in comp_terms): intents.append("COMPARISON")
105
+
106
+ local_terms = {"near", "restaurant", "restaurants", "location", "weather"}
107
+ if any(term in query_tokens for term in local_terms): intents.append("LOCAL")
108
+
109
+ if len(query_tokens) <= 2 and not intents: intents.append("NAVIGATIONAL")
110
+
111
+ return {"intent": intents[0] if intents else "GENERAL", "signals": intents}
112
+
113
+
114
+ # --- 3. DEDICATED WEB CONTENT EXTRACTION ---
115
+ async def extract_page_content(url: str) -> Dict[str, Any]:
116
+ """Downloads webpage safely, extracts pure text, and caches it."""
117
+ if url in CONTENT_CACHE:
118
+ return CONTENT_CACHE[url]
119
+
120
+ async with content_semaphore:
121
+ try:
122
+ async with aiohttp.ClientSession() as session:
123
+ async with session.get(url, timeout=4.0, headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) NebulixBot"}) as resp:
124
+ if resp.status != 200:
125
+ return {"url": url, "title": "", "content": "", "success": False, "error": f"HTTP {resp.status}"}
126
+
127
+ html = await resp.text()
128
+ soup = BeautifulSoup(html, "html.parser")
129
+
130
+ # Clean irrelevant elements
131
+ for tag in soup(['script', 'style', 'nav', 'footer', 'header', 'aside', 'noscript', 'form', 'button', 'iframe']):
132
+ tag.decompose()
133
+
134
+ blocks = []
135
+ for tag in soup.find_all(['h1', 'h2', 'h3', 'p', 'article', 'main', 'li']):
136
+ text = tag.get_text(separator=' ', strip=True)
137
+ if len(text) > 30: # Skip very tiny menu stubs missed by decompose
138
+ blocks.append(text)
139
+
140
+ # Deduplicate contiguous identical text
141
+ clean_blocks = []
142
+ seen_blocks = set()
143
+ for b in blocks:
144
+ if b not in seen_blocks:
145
+ seen_blocks.add(b)
146
+ clean_blocks.append(b)
147
+
148
+ content_str = "\n\n".join(clean_blocks)
149
+ page_title = soup.title.string.strip() if soup.title and soup.title.string else ""
150
+
151
+ result = {
152
+ "url": url,
153
+ "title": page_title,
154
+ "content": content_str,
155
+ "success": True,
156
+ "timestamp": time.time()
157
+ }
158
+ CONTENT_CACHE[url] = result
159
+ return result
160
+ except asyncio.TimeoutError:
161
+ return {"url": url, "title": "", "content": "", "success": False, "error": "Timeout"}
162
+ except Exception as e:
163
+ return {"url": url, "title": "", "content": "", "success": False, "error": str(e)}
164
+
165
+ def rank_chunks_for_rag(text: str, query_tokens: List[str]) -> List[str]:
166
+ """Deterministically extracts the most relevant chunks from an article for context."""
167
  if not text: return []
168
+ chunks = text.split("\n\n")
169
+ q_set = set(query_tokens)
170
+ scored_chunks = []
171
+
172
+ for c in chunks:
173
+ c_toks = set(tokenize(c))
174
+ score = len(q_set.intersection(c_toks))
175
+ if score > 0:
176
+ scored_chunks.append((score, c))
177
+
178
+ if not scored_chunks and chunks:
179
+ scored_chunks = [(0, c) for c in chunks[:3]] # Fallback to top paragraphs
180
+
181
+ scored_chunks.sort(reverse=True, key=lambda x: x[0])
182
+ return [c for score, c in scored_chunks[:3]] # Max 3 context chunks per source
183
+
184
+
185
+ # --- 4. CORE DETERMINISTIC SEARCH INDEX ---
186
+ class HybridSearchIndex:
187
+ def __init__(self):
188
+ self.docs = []
189
+ self.total_docs = 0
190
+ self.vocab = set()
191
+ self.df = defaultdict(int)
192
+ self.avg_doc_len = 0.0
193
+ self.inverted_index = defaultdict(set)
194
+ self.suggestions = []
195
+ self.k1 = 1.5
196
+ self.b = 0.75
197
+
198
+ def build(self, raw_data: List[dict]):
199
+ print("📥 Building Advanced Deterministic Search Index...")
200
+ total_len = 0
201
+ suggestion_counter = Counter()
202
+
203
+ for doc_id, doc in enumerate(raw_data):
204
+ self.docs.append(doc)
205
+ title_tokens = tokenize(doc.get("_fast_title", ""))
206
+ desc_tokens = tokenize(doc.get("_fast_desc", ""))
207
+ domain_tokens = tokenize(doc.get("_fast_domain", ""))
208
+ all_tokens = title_tokens + desc_tokens + domain_tokens
209
+
210
+ doc["_token_len"] = len(all_tokens)
211
+ total_len += len(all_tokens)
212
+
213
+ for term in set(all_tokens):
214
+ self.inverted_index[term].add(doc_id)
215
+ self.df[term] += 1
216
+ self.vocab.add(term)
217
+
218
+ if not doc.get("_is_adult", False):
219
+ clean_title = [w for w in title_tokens if w not in ADULT_KEYWORDS and len(w) > 2]
220
+ for w in clean_title: suggestion_counter[w] += 1
221
+ for j in range(len(clean_title) - 1): suggestion_counter[f"{clean_title[j]} {clean_title[j+1]}"] += 2
222
+ d_clean = doc.get("_fast_domain", "").split('.')[0]
223
+ if len(d_clean) > 2 and d_clean not in ADULT_KEYWORDS: suggestion_counter[d_clean] += 5
224
+
225
+ self.total_docs = len(self.docs)
226
+ self.avg_doc_len = total_len / max(1, self.total_docs)
227
+ self.suggestions = suggestion_counter.most_common(40000)
228
+ print(f"🚀 Indexing Complete! {self.total_docs} records ready.")
229
+
230
+ def idf(self, term: str) -> float:
231
+ df = self.df.get(term, 0)
232
+ return math.log(1.0 + (self.total_docs - df + 0.5) / (df + 0.5))
233
+
234
+ def _get_candidates(self, query_tokens: List[str]) -> Set[int]:
235
+ candidates = set()
236
+ for term in query_tokens:
237
+ if term in self.inverted_index: candidates.update(self.inverted_index[term])
238
+
239
+ if len(candidates) < 10:
240
+ for term in query_tokens:
241
+ if term not in self.inverted_index:
242
+ matches = difflib.get_close_matches(term, self.vocab, n=2, cutoff=0.8)
243
+ for m in matches: candidates.update(self.inverted_index[m])
244
+ return candidates
245
+
246
+ def calculate_features(self, doc: dict, query_tokens: List[str], query_str: str) -> Dict[str, float]:
247
+ title = doc.get("_fast_title", "")
248
+ desc = doc.get("_fast_desc", "")
249
+ domain = doc.get("_fast_domain", "")
250
+ url = doc.get("_fast_path", "")
251
+
252
+ title_toks = tokenize(title, False)
253
+ desc_toks = tokenize(desc, False)
254
+ query_toks_full = tokenize(query_str, False)
255
+
256
+ scores = {"bm25": 0.0, "title": 0.0, "phrase": 0.0, "exact": 0.0, "url": 0.0, "domain": 0.0, "quality": 0.0, "freshness": 0.0, "proximity": 0.0}
257
+
258
+ doc_len = doc.get("_token_len", 1)
259
+ for term in query_tokens:
260
+ tf = title_toks.count(term) * 2 + desc_toks.count(term)
261
+ if tf > 0:
262
+ tf_num = tf * (self.k1 + 1)
263
+ tf_den = tf + self.k1 * (1 - self.b + self.b * (doc_len / self.avg_doc_len))
264
+ scores["bm25"] += self.idf(term) * (tf_num / tf_den)
265
+
266
+ q_norm = normalize_text(query_str)
267
+ if q_norm and q_norm in normalize_text(title): scores["exact"] = 1.0
268
+ elif q_norm and q_norm in normalize_text(desc): scores["exact"] = 0.5
269
+
270
+ if len(query_toks_full) > 1:
271
+ phrase_overlap = 0
272
+ for i in range(len(query_toks_full) - 1):
273
+ bigram = f"{query_toks_full[i]} {query_toks_full[i+1]}"
274
+ if bigram in normalize_text(title): phrase_overlap += 1.0
275
+ elif bigram in normalize_text(desc): phrase_overlap += 0.5
276
+ scores["phrase"] = phrase_overlap / (len(query_toks_full) - 1)
277
+
278
+ title_overlap = sum(1 for t in query_tokens if t in title_toks)
279
+ scores["title"] = title_overlap / max(1, len(query_tokens))
280
+
281
+ q_joined = "".join(query_tokens)
282
+ domain_clean = domain.split('.')[0]
283
+ if q_joined == domain_clean or domain.startswith(q_joined + "."): scores["domain"] = 1.0
284
+ elif q_joined in domain: scores["domain"] = 0.5
285
+ if any(t in url for t in query_tokens): scores["url"] = 0.5
286
+
287
+ if len(query_tokens) > 1:
288
+ positions = [title_toks.index(t) for t in query_tokens if t in title_toks]
289
+ if len(positions) > 1:
290
+ scores["proximity"] = 1.0 / (1.0 + (max(positions) - min(positions)))
291
+
292
+ year_match = re.search(r'\b(202[0-6])\b', url + " " + desc)
293
+ if year_match:
294
+ scores["freshness"] = math.exp(-(datetime.now().year - int(year_match.group(1))) / 2.0)
295
+ else: scores["freshness"] = 0.3
296
+
297
+ q_score = 0.0
298
+ if len(title_toks) >= 3: q_score += 0.2
299
+ if len(desc_toks) >= 10: q_score += 0.2
300
+ if doc.get("url", "").startswith("https"): q_score += 0.2
301
+ if len(title_toks) > 0 and (len(set(title_toks)) / len(title_toks)) < 0.5: q_score -= 0.3
302
+ scores["quality"] = max(0.0, min(1.0, q_score))
303
+
304
+ return scores
305
+
306
+ def search(self, query: str, debug: bool = False) -> Tuple[List[dict], Dict[str, Any]]:
307
+ query_norm = normalize_text(query)
308
+ query_tokens = tokenize(query_norm)
309
+
310
+ expanded_tokens = list(query_tokens)
311
+ for t in query_tokens:
312
+ if t in EXPANSION_DICT: expanded_tokens.extend(EXPANSION_DICT[t])
313
+ query_tokens = list(dict.fromkeys(expanded_tokens))
314
+
315
+ intent_info = detect_query_intent(query_tokens)
316
+ intent = intent_info["intent"]
317
+ user_wants_adult = any(aw in query_norm for aw in ADULT_KEYWORDS)
318
+
319
+ candidates_ids = self._get_candidates(query_tokens)
320
+ scored_results = []
321
+ seen_domains = defaultdict(int)
322
+ seen_urls = set()
323
+
324
+ for doc_id in candidates_ids:
325
+ doc = self.docs[doc_id]
326
+ if not user_wants_adult and doc.get("_is_adult"): continue
327
+
328
+ base_url = doc.get("url", "").split("?")[0].rstrip("/")
329
+ if base_url in seen_urls: continue
330
+
331
+ features = self.calculate_features(doc, query_tokens, query_norm)
332
+ norm_bm25 = features["bm25"] / (features["bm25"] + 10.0)
333
+
334
+ w_freshness = WEIGHTS["W_FRESHNESS"] + (3.0 if intent == "NEWS" else 0.0)
335
+ w_domain = WEIGHTS["W_DOMAIN"] + (4.0 if intent == "NAVIGATIONAL" else 0.0)
336
+ w_title = WEIGHTS["W_TITLE"] + (2.0 if intent == "DEFINITION" else 0.0)
337
+ w_phrase = WEIGHTS["W_PHRASE"] + (1.5 if intent in ("DEFINITION", "COMPARISON") else 0.0)
338
+
339
+ final_score = (
340
+ WEIGHTS["W_BM25"] * norm_bm25 + w_title * features["title"] + w_phrase * features["phrase"] +
341
+ WEIGHTS["W_EXACT"] * features["exact"] + WEIGHTS["W_URL"] * features["url"] +
342
+ w_domain * features["domain"] + w_freshness * features["freshness"] +
343
+ WEIGHTS["W_QUALITY"] * features["quality"] + WEIGHTS["W_PROXIMITY"] * features["proximity"]
344
+ )
345
+
346
+ if final_score > 0.05:
347
+ doc_out = {
348
+ "title": doc.get("_fast_title", ""),
349
+ "description": doc.get("_fast_desc", ""),
350
+ "url": doc.get("url", ""),
351
+ "domain": doc.get("_fast_domain", ""),
352
+ "match_score": round(final_score, 4)
353
+ }
354
+ if debug:
355
+ doc_out["_debug_features"] = features
356
+ doc_out["_debug_intent"] = intent_info
357
+
358
+ scored_results.append((final_score, doc_out, base_url, doc["_fast_domain"]))
359
+
360
+ scored_results.sort(key=lambda x: x[0], reverse=True)
361
+
362
+ diversified_results = []
363
+ for score, doc_info, b_url, domain in scored_results:
364
+ if seen_domains[domain] >= MAX_RESULTS_PER_DOMAIN: continue
365
+ seen_domains[domain] += 1
366
+ seen_urls.add(b_url)
367
+ diversified_results.append(doc_info)
368
+ if len(diversified_results) >= STRICT_RESULT_LIMIT: break # STRICT LIMIT
369
+
370
+ # Assign final rank
371
+ for idx, item in enumerate(diversified_results): item["rank"] = idx + 1
372
+
373
+ return diversified_results, intent_info
374
+
375
+ # --- INITIALIZATION ---
376
+ search_index = HybridSearchIndex()
377
 
378
  def load_and_index_dataset():
 
 
379
  dataset_path = "dataset.jsonl"
 
380
  if HF_TOKEN:
381
+ try: dataset_path = hf_hub_download(repo_id="Nebulixlabs/Ultimate-Search-Engine-Dataset", filename="dataset.jsonl", repo_type="dataset", token=HF_TOKEN)
382
+ except Exception: pass
 
 
 
 
 
 
 
383
 
384
+ raw_data = []
 
 
385
  if os.path.exists(dataset_path):
386
  with open(dataset_path, "r", encoding="utf-8") as f:
387
  for i, line in enumerate(f):
 
392
  url = doc.get("url", "")
393
  title_str = doc.get("title", "")
394
  desc_str = doc.get("description", "")
 
395
  parsed_url = urllib.parse.urlparse(url)
396
+
397
+ doc["_fast_domain"] = parsed_url.netloc.lower().replace("www.", "")
 
 
 
 
 
398
  doc["_fast_path"] = parsed_url.path.strip("/")
399
+ doc["_fast_title"] = title_str
400
+ doc["_fast_desc"] = desc_str
401
+ doc["_is_adult"] = doc.get("tag", "general").lower() in ["adult", "nsfw"] or any(aw in (title_str+desc_str+url).lower() for aw in ADULT_KEYWORDS)
402
  raw_data.append(doc)
 
 
 
 
 
 
 
 
 
 
 
 
403
  except: pass
404
+ search_index.build(raw_data)
 
 
 
 
405
 
406
+ # --- QWEN AI GENERATION MODULE (FOR RAG ONLY) ---
407
  def load_ai_models():
408
  global ai_generator
409
  print("🤖 Loading Qwen/Qwen2.5-0.5B-Instruct...")
 
411
  model_id = "Qwen/Qwen2.5-0.5B-Instruct"
412
  tokenizer = AutoTokenizer.from_pretrained(model_id)
413
 
 
414
  if torch.cuda.is_available():
415
+ quant_config = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=torch.float16, bnb_4bit_quant_type="nf4")
416
+ model = AutoModelForCausalLM.from_pretrained(model_id, quantization_config=quant_config, device_map="auto")
 
 
 
 
 
 
 
 
 
417
  else:
418
+ model = AutoModelForCausalLM.from_pretrained(model_id, device_map="cpu", torch_dtype=torch.float32)
 
 
 
 
 
 
 
419
 
420
+ ai_generator = pipeline("text-generation", model=model, tokenizer=tokenizer)
421
+ print("✅ Qwen AI Ready!")
 
 
 
 
422
  except Exception as e:
423
  print(f"❌ Model Loading Failed: {e}")
424
 
425
+ async def run_ai_task(messages, max_tokens: int, query_key: str, skip_cache: bool = False):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426
  if not ai_generator: return "⚠️ AI Model is starting up."
427
+ if not skip_cache and query_key in AI_RESPONSE_CACHE: return AI_RESPONSE_CACHE[query_key]
428
+
429
  async with ai_semaphore:
430
+ def _gen():
431
+ tokenizer = ai_generator.tokenizer
432
+ try: prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
433
+ except: prompt = "\n".join([f"{m['role'].capitalize()}: {m['content']}" for m in messages]) + "\nAssistant: "
434
+
435
+ result = ai_generator(prompt, max_new_tokens=max_tokens, do_sample=False, truncation=True, return_full_text=False)
436
+ return result[0]['generated_text'].strip()
437
+
438
+ response = await asyncio.to_thread(_gen)
439
+ if not skip_cache:
440
+ if len(AI_RESPONSE_CACHE) > MAX_AI_CACHE_SIZE: AI_RESPONSE_CACHE.clear()
441
+ AI_RESPONSE_CACHE[query_key] = response
442
  return response
443
 
444
+ # --- FASTAPI APP ENDPOINTS ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
445
  @asynccontextmanager
446
  async def lifespan(app: FastAPI):
447
  asyncio.create_task(asyncio.to_thread(load_and_index_dataset))
448
  asyncio.create_task(asyncio.to_thread(load_ai_models))
449
  yield
450
 
451
+ app = FastAPI(title="Nebulix Fast Hybrid Search Engine", lifespan=lifespan)
452
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
453
 
454
  @app.get("/suggest")
455
  async def get_suggestions(q: str = Query(..., min_length=1)):
456
+ q = normalize_text(q).strip()
457
+ matches = [word for word, count in search_index.suggestions if word.startswith(q)][:7]
458
  return {"query": q, "suggestions": matches}
459
 
460
+ @app.get("/content")
461
+ async def get_content_endpoint(url: str = Query(...)):
462
+ """Dedicated endpoint to just extract webpage content."""
463
+ res = await extract_page_content(url)
464
+ return res
465
 
466
+ @app.get("/search")
467
+ async def pure_search(q: str = Query(...), content: bool = Query(False), debug: bool = Query(False)):
468
  req_id = str(uuid.uuid4())
469
+ results, intent_info = await asyncio.to_thread(search_index.search, q, debug)
470
+
471
+ # Strictly limit to 20 results output
472
+ top_20 = results[:STRICT_RESULT_LIMIT]
473
+
474
+ if content and top_20:
475
+ tasks = [extract_page_content(r["url"]) for r in top_20]
476
+ extracted_data = await asyncio.gather(*tasks)
477
+ for r, ext in zip(top_20, extracted_data):
478
+ r["content"] = ext.get("content", "") if ext.get("success") else ""
479
+
480
+ return {"request_id": req_id, "query": q, "results_count": len(top_20), "results": top_20}
481
 
482
+ async def process_aimode(q: str, include_content: bool, debug: bool):
483
  req_id = str(uuid.uuid4())
 
 
484
 
485
+ # 1. Deterministic Search (Max 20 internally outputted)
486
+ results, intent_info = await asyncio.to_thread(search_index.search, q, debug)
487
+ top_20 = results[:STRICT_RESULT_LIMIT]
488
+ query_tokens = tokenize(q)
489
+
490
+ # Bypass AI cache if it's a News/Latest query
491
+ is_news = "NEWS" in intent_info["signals"]
492
+
493
+ rag_context_parts = []
494
  used_sources = []
495
 
496
+ if top_20:
497
+ # 2. Extract content ONLY for Top ranked sources
498
+ tasks = [extract_page_content(r["url"]) for r in top_20]
499
+ extracted_data = await asyncio.gather(*tasks)
500
 
501
+ # 3. Deterministic Chunk Selection for Context
502
+ valid_sources_count = 0
503
+ for i, (r, ext) in enumerate(zip(top_20, extracted_data)):
504
+ if include_content:
505
+ r["content"] = ext.get("content", "") if ext.get("success") else ""
 
 
 
 
 
506
 
507
+ if ext.get("success") and ext.get("content") and valid_sources_count < 5: # Build context from top 5 valid pages
508
+ best_chunks = rank_chunks_for_rag(ext["content"], query_tokens)
509
+ if best_chunks:
510
+ rag_context_parts.append(f"[Source {valid_sources_count+1}]\nTitle: {r['title']}\nURL: {r['url']}\nRelevant Content: {' ... '.join(best_chunks)}")
511
+ used_sources.append({"id": valid_sources_count+1, "title": r["title"], "url": r["url"]})
512
+ valid_sources_count += 1
513
+
514
+ rag_context = "\n\n".join(rag_context_parts)
515
+ system_prompt = (
516
+ "You are an intelligent search assistant. Answer the user's query DIRECTLY based ONLY on the provided Live Context Data. "
517
+ "Do not invent facts. If the context contains the answer, summarize it clearly and cite the sources used (e.g., [Source 1]). "
518
+ "If the context does not contain the answer, state that you do not have enough information."
519
+ )
520
+
521
  messages = [
522
  {"role": "system", "content": system_prompt},
523
+ {"role": "user", "content": f"Live Context Data:\n{rag_context}\n\nQuestion: {q}" if rag_context else f"Question: {q}"}
524
  ]
525
 
526
+ # 4. Generate AI Answer (Only Step utilizing LLM)
527
+ ai_reply = await run_ai_task(messages, max_tokens=150, query_key=f"ai_{q}", skip_cache=is_news)
528
+
529
+ return {
530
+ "request_id": req_id,
531
+ "query": q,
532
+ "ai_response": ai_reply,
533
+ "results_count": len(top_20),
534
+ "results": top_20,
535
+ "sources": used_sources
536
+ }
537
 
538
+ @app.get("/aimode")
539
+ async def ai_mode_get(q: str = Query(...), content: bool = Query(False), debug: bool = Query(False)):
540
+ return await process_aimode(q, content, debug)
541
 
542
+ @app.post("/aimode")
543
+ async def ai_mode_post(q: str = Form(...), content: bool = Form(False), debug: bool = Form(False)):
544
+ return await process_aimode(q, content, debug)
545
 
546
  if __name__ == "__main__":
547
  uvicorn.run(app, host="0.0.0.0", port=7860)