Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -16,29 +16,27 @@ from collections import Counter, defaultdict
|
|
| 16 |
from contextlib import asynccontextmanager
|
| 17 |
from typing import List, Dict, Any, Tuple, Set
|
| 18 |
|
| 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
|
| 27 |
-
from transformers import
|
| 28 |
|
| 29 |
# --- CONFIGURATION & MULTIPROCESSING ---
|
| 30 |
TOTAL_CORES = multiprocessing.cpu_count()
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
print(f"🖥️ Total Cores
|
| 34 |
|
| 35 |
-
MAX_CONCURRENT_AI = max(1, AI_CORES)
|
| 36 |
-
ai_semaphore = asyncio.Semaphore(MAX_CONCURRENT_AI)
|
| 37 |
-
ai_generator = None
|
| 38 |
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 39 |
|
| 40 |
-
|
| 41 |
-
|
|
|
|
| 42 |
|
| 43 |
# --- DENSE RETRIEVAL GLOBALS (FAISS + E5) ---
|
| 44 |
embed_tokenizer = None
|
|
@@ -67,16 +65,16 @@ EXPANSION_DICT = {
|
|
| 67 |
"js": ["javascript"]
|
| 68 |
}
|
| 69 |
|
| 70 |
-
#
|
| 71 |
WEIGHTS = {
|
| 72 |
"W_BM25": 1.0, "W_TITLE": 4.0, "W_HEADINGS": 3.0, "W_DESCRIPTION": 2.0, "W_BODY": 1.0,
|
| 73 |
"W_URL": 1.5, "W_DOMAIN": 1.5, "W_PHRASE": 2.5, "W_EXACT": 3.0, "W_FRESHNESS": 1.0,
|
| 74 |
"W_QUALITY": 1.0, "W_POPULARITY": 0.5, "W_PROXIMITY": 1.5,
|
| 75 |
-
"W_ROOT_DOMAIN": 8.0, "W_DENSE":
|
| 76 |
}
|
| 77 |
|
| 78 |
-
MAX_RESULTS_PER_DOMAIN =
|
| 79 |
-
STRICT_RESULT_LIMIT =
|
| 80 |
|
| 81 |
def normalize_text(text: str) -> str:
|
| 82 |
if not text: return ""
|
|
@@ -93,42 +91,26 @@ def tokenize(text: str, remove_stopwords: bool = True) -> List[str]:
|
|
| 93 |
|
| 94 |
# --- E5 POOLING FUNCTION ---
|
| 95 |
def average_pool(last_hidden_states, attention_mask):
|
| 96 |
-
"""Mean pooling for E5 dense embeddings"""
|
| 97 |
last_hidden = last_hidden_states.masked_fill(~attention_mask[..., None].bool(), 0.0)
|
| 98 |
return last_hidden.sum(dim=1) / attention_mask.sum(dim=1)[..., None]
|
| 99 |
|
| 100 |
# --- 2. QUERY INTENT CLASSIFIER ---
|
| 101 |
def detect_query_intent(query_tokens: List[str]) -> Dict[str, Any]:
|
| 102 |
intents = []
|
| 103 |
-
news_terms = {"latest", "news", "today", "breaking", "recent", "updates", "current"
|
| 104 |
if any(term in query_tokens for term in news_terms): intents.append("NEWS")
|
| 105 |
def_terms = {"definition", "meaning", "kya", "matlab"}
|
| 106 |
if any(term in query_tokens for term in def_terms): intents.append("DEFINITION")
|
| 107 |
how_terms = {"tutorial", "guide", "steps", "learn", "how", "kaise"}
|
| 108 |
if any(term in query_tokens for term in how_terms): intents.append("HOW_TO")
|
| 109 |
-
comp_terms = {"vs", "versus", "compare", "comparison", "difference", "best"
|
| 110 |
if any(term in query_tokens for term in comp_terms): intents.append("COMPARISON")
|
| 111 |
-
local_terms = {"near", "restaurant", "restaurants", "location", "weather"
|
| 112 |
if any(term in query_tokens for term in local_terms): intents.append("LOCAL")
|
| 113 |
if len(query_tokens) <= 2 and not intents: intents.append("NAVIGATIONAL")
|
| 114 |
return {"intent": intents[0] if intents else "GENERAL", "signals": intents}
|
| 115 |
|
| 116 |
-
# --- 3.
|
| 117 |
-
def rank_chunks_for_rag(text: str, query_tokens: List[str]) -> List[str]:
|
| 118 |
-
if not text: return []
|
| 119 |
-
chunks = text.split("\n\n")
|
| 120 |
-
q_set = set(query_tokens)
|
| 121 |
-
scored_chunks = []
|
| 122 |
-
for c in chunks:
|
| 123 |
-
c_toks = set(tokenize(c))
|
| 124 |
-
score = len(q_set.intersection(c_toks))
|
| 125 |
-
if score > 0: scored_chunks.append((score, c))
|
| 126 |
-
if not scored_chunks and chunks: scored_chunks = [(0, c) for c in chunks[:3]]
|
| 127 |
-
scored_chunks.sort(reverse=True, key=lambda x: x[0])
|
| 128 |
-
return [c for score, c in scored_chunks[:2]]
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
# --- 4. CORE DETERMINISTIC + DENSE HYBRID INDEX ---
|
| 132 |
class HybridSearchIndex:
|
| 133 |
def __init__(self):
|
| 134 |
self.docs = []
|
|
@@ -139,9 +121,7 @@ class HybridSearchIndex:
|
|
| 139 |
self.df = defaultdict(int)
|
| 140 |
self.avg_doc_len = 0.0
|
| 141 |
|
| 142 |
-
# Link dataset doc_id -> Internal List Index for FAISS matching
|
| 143 |
self.dataset_id_to_internal = {}
|
| 144 |
-
|
| 145 |
self.inverted_index = defaultdict(lambda: array.array('I'))
|
| 146 |
self.suggestion_counter = Counter()
|
| 147 |
self.suggestions = []
|
|
@@ -151,7 +131,6 @@ class HybridSearchIndex:
|
|
| 151 |
def add_document(self, doc_json: dict):
|
| 152 |
doc_id = len(self.docs)
|
| 153 |
|
| 154 |
-
# Map original dataset ID for FAISS metadata linking
|
| 155 |
dataset_doc_id = doc_json.get("doc_id", "")
|
| 156 |
if dataset_doc_id:
|
| 157 |
self.dataset_id_to_internal[dataset_doc_id] = doc_id
|
|
@@ -170,7 +149,6 @@ class HybridSearchIndex:
|
|
| 170 |
|
| 171 |
url = doc_json.get("url", "")
|
| 172 |
parsed_url = urllib.parse.urlparse(url)
|
| 173 |
-
|
| 174 |
domain_str = sys.intern(parsed_url.netloc.lower().replace("www.", ""))
|
| 175 |
|
| 176 |
clean_doc = {
|
|
@@ -192,7 +170,6 @@ class HybridSearchIndex:
|
|
| 192 |
content_tokens = tokenize(clean_doc["_fast_content"])[:250]
|
| 193 |
|
| 194 |
all_tokens = title_tokens + desc_tokens + domain_tokens + headings_tokens + snippet_tokens + content_tokens
|
| 195 |
-
|
| 196 |
clean_doc["_token_len"] = len(all_tokens)
|
| 197 |
self.total_len += len(all_tokens)
|
| 198 |
|
|
@@ -217,10 +194,9 @@ class HybridSearchIndex:
|
|
| 217 |
self.avg_doc_len = self.total_len / max(1, self.total_docs)
|
| 218 |
self.suggestions = self.suggestion_counter.most_common(20000)
|
| 219 |
self.high_freq_vocab = [word for word, count in Counter(self.df).most_common(15000)]
|
| 220 |
-
|
| 221 |
self.suggestion_counter.clear()
|
| 222 |
gc.collect()
|
| 223 |
-
print(f"🚀 Indexing Complete!
|
| 224 |
|
| 225 |
def idf(self, term: str) -> float:
|
| 226 |
df = self.df.get(term, 0)
|
|
@@ -257,17 +233,11 @@ class HybridSearchIndex:
|
|
| 257 |
|
| 258 |
doc_len = doc.get("_token_len", 1)
|
| 259 |
avg_dl = max(1.0, self.avg_doc_len)
|
| 260 |
-
|
| 261 |
content_lower = content.lower()
|
| 262 |
snippet_lower = snippet.lower()
|
| 263 |
|
| 264 |
for term in query_tokens_sorted:
|
| 265 |
-
tf = (title_toks.count(term) * 4 +
|
| 266 |
-
headings_toks.count(term) * 3 +
|
| 267 |
-
desc_toks.count(term) * 2 +
|
| 268 |
-
snippet_lower.count(term) * 2 +
|
| 269 |
-
content_lower.count(term))
|
| 270 |
-
|
| 271 |
if tf > 0:
|
| 272 |
tf_num = tf * (self.k1 + 1)
|
| 273 |
tf_den = tf + self.k1 * (1 - self.b + self.b * (doc_len / avg_dl))
|
|
@@ -370,24 +340,23 @@ class HybridSearchIndex:
|
|
| 370 |
intent = intent_info["intent"]
|
| 371 |
user_wants_adult = any(aw in query_norm for aw in ADULT_KEYWORDS)
|
| 372 |
|
| 373 |
-
# 1. Lexical Retrieval (BM25)
|
| 374 |
candidates_ids = self._get_candidates(sorted_tokens)
|
| 375 |
|
| 376 |
-
# 2. Dense Semantic Retrieval (FAISS
|
| 377 |
dense_scores_map = {}
|
| 378 |
if faiss_index is not None and embed_model is not None and embed_tokenizer is not None:
|
| 379 |
try:
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
)
|
| 384 |
-
with torch.no_grad():
|
| 385 |
embed_outputs = embed_model(**embed_inputs)
|
| 386 |
xq = average_pool(embed_outputs.last_hidden_state, embed_inputs["attention_mask"])
|
| 387 |
xq = F.normalize(xq, p=2, dim=1).numpy()
|
| 388 |
|
| 389 |
-
# Fetch Top
|
| 390 |
-
D, I = faiss_index.search(xq, k=
|
| 391 |
|
| 392 |
for score, idx in zip(D[0], I[0]):
|
| 393 |
if 0 <= idx < len(faiss_metadata):
|
|
@@ -395,9 +364,9 @@ class HybridSearchIndex:
|
|
| 395 |
if ds_id in self.dataset_id_to_internal:
|
| 396 |
internal_id = self.dataset_id_to_internal[ds_id]
|
| 397 |
dense_scores_map[internal_id] = float(score)
|
| 398 |
-
candidates_ids.add(internal_id) #
|
| 399 |
except Exception as e:
|
| 400 |
-
print(f"⚠️ FAISS
|
| 401 |
|
| 402 |
scored_results = []
|
| 403 |
seen_domains = defaultdict(int)
|
|
@@ -418,11 +387,10 @@ class HybridSearchIndex:
|
|
| 418 |
w_title = WEIGHTS["W_TITLE"] + (2.0 if intent == "DEFINITION" else 0.0)
|
| 419 |
w_phrase = WEIGHTS["W_PHRASE"] + (1.5 if intent in ("DEFINITION", "COMPARISON") else 0.0)
|
| 420 |
|
| 421 |
-
# Fetch Dense Score (Cosine Sim typically 0.70 - 1.0)
|
| 422 |
dense_score = dense_scores_map.get(doc_id, 0.0)
|
| 423 |
features["dense_score"] = dense_score
|
| 424 |
|
| 425 |
-
#
|
| 426 |
dense_boost = max(0.0, dense_score - 0.75) * WEIGHTS["W_DENSE"]
|
| 427 |
|
| 428 |
final_score = (
|
|
@@ -438,11 +406,10 @@ class HybridSearchIndex:
|
|
| 438 |
WEIGHTS["W_ROOT_DOMAIN"] * features["root_domain"] +
|
| 439 |
WEIGHTS["W_HEADINGS"] * features["headings"] +
|
| 440 |
WEIGHTS["W_BODY"] * features["body"] +
|
| 441 |
-
dense_boost
|
| 442 |
)
|
| 443 |
|
| 444 |
-
|
| 445 |
-
is_semantic_match = dense_score >= 0.82
|
| 446 |
|
| 447 |
if len(sorted_tokens) > 0:
|
| 448 |
top_term = sorted_tokens[0]
|
|
@@ -452,16 +419,16 @@ class HybridSearchIndex:
|
|
| 452 |
doc_headings = tokenize(doc.get("_fast_headings", ""), False)
|
| 453 |
|
| 454 |
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:
|
| 455 |
-
if is_semantic_match: final_score *= 0.
|
| 456 |
-
else: final_score *= 0.1
|
| 457 |
|
| 458 |
if len(sorted_tokens) > 1:
|
| 459 |
second_term = sorted_tokens[1]
|
| 460 |
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:
|
| 461 |
-
if is_semantic_match: final_score *= 0.
|
| 462 |
else: final_score *= 0.4
|
| 463 |
|
| 464 |
-
if final_score > 0.
|
| 465 |
doc_out = {
|
| 466 |
"doc_id": str(doc_id),
|
| 467 |
"title": doc.get("_fast_title", ""),
|
|
@@ -486,7 +453,7 @@ class HybridSearchIndex:
|
|
| 486 |
seen_domains[domain] += 1
|
| 487 |
seen_urls.add(b_url)
|
| 488 |
diversified_results.append(doc_info)
|
| 489 |
-
if len(diversified_results) >= STRICT_RESULT_LIMIT: break
|
| 490 |
|
| 491 |
for idx, item in enumerate(diversified_results): item["rank"] = idx + 1
|
| 492 |
return diversified_results, intent_info, corrected_query_str
|
|
@@ -496,22 +463,18 @@ search_index = HybridSearchIndex()
|
|
| 496 |
# --- DATA LOADER (FAISS & LEXICAL) ---
|
| 497 |
def load_dense_retriever():
|
| 498 |
global embed_tokenizer, embed_model, faiss_index, faiss_metadata
|
| 499 |
-
print("🧠 Loading E5-Small-v2 Embedder
|
| 500 |
try:
|
| 501 |
embed_tokenizer = AutoTokenizer.from_pretrained("intfloat/e5-small-v2")
|
| 502 |
-
|
| 503 |
-
embed_model.eval()
|
| 504 |
print("✅ E5 Embedder Ready!")
|
| 505 |
except Exception as e:
|
| 506 |
print(f"❌ E5 Loading Failed: {e}")
|
| 507 |
|
| 508 |
-
print("📥 Downloading
|
| 509 |
try:
|
| 510 |
-
dense_path = snapshot_download(
|
| 511 |
-
repo_id="Nebulixlabs/embedded-data",
|
| 512 |
-
repo_type="dataset",
|
| 513 |
-
token=HF_TOKEN
|
| 514 |
-
)
|
| 515 |
index_file = os.path.join(dense_path, "dataset_embeddings.index")
|
| 516 |
meta_file = os.path.join(dense_path, "metadata.json")
|
| 517 |
|
|
@@ -519,23 +482,15 @@ def load_dense_retriever():
|
|
| 519 |
faiss_index = faiss.read_index(index_file)
|
| 520 |
with open(meta_file, "r", encoding="utf-8") as f:
|
| 521 |
faiss_metadata = json.load(f)
|
| 522 |
-
print(f"✅
|
| 523 |
-
else:
|
| 524 |
-
print("⚠️ FAISS index files not found in repo.")
|
| 525 |
except Exception as e:
|
| 526 |
print(f"❌ FAISS Loading Failed: {e}")
|
| 527 |
|
| 528 |
def load_and_index_dataset():
|
| 529 |
-
print("📦
|
| 530 |
try:
|
| 531 |
-
dataset_path = snapshot_download(
|
| 532 |
-
repo_id="Nebulixlabs/web-search-dataset",
|
| 533 |
-
repo_type="dataset",
|
| 534 |
-
allow_patterns="data/*.jsonl",
|
| 535 |
-
token=HF_TOKEN
|
| 536 |
-
)
|
| 537 |
except Exception as e:
|
| 538 |
-
print("⚠️ Direct HF Hub fetch failed, attempting local fallback. Error:", e)
|
| 539 |
dataset_path = "."
|
| 540 |
|
| 541 |
data_folder = os.path.join(dataset_path, "data")
|
|
@@ -544,175 +499,69 @@ def load_and_index_dataset():
|
|
| 544 |
if os.path.exists(data_folder):
|
| 545 |
jsonl_files = [f for f in os.listdir(data_folder) if f.endswith(".jsonl")]
|
| 546 |
docs_processed = 0
|
| 547 |
-
|
| 548 |
for file in jsonl_files:
|
| 549 |
-
|
| 550 |
-
with open(file_path, "r", encoding="utf-8") as f:
|
| 551 |
for line in f:
|
| 552 |
if not line.strip(): continue
|
| 553 |
try:
|
| 554 |
-
|
| 555 |
-
search_index.add_document(doc)
|
| 556 |
docs_processed += 1
|
| 557 |
-
|
| 558 |
if docs_processed % 50000 == 0:
|
| 559 |
gc.collect()
|
| 560 |
-
print(f"🔄
|
| 561 |
-
except Exception:
|
| 562 |
-
pass
|
| 563 |
-
|
| 564 |
search_index.finalize_build()
|
| 565 |
|
| 566 |
-
# --- AI GENERATION MODULE ---
|
| 567 |
-
def load_ai_models():
|
| 568 |
-
global ai_generator
|
| 569 |
-
print("🤖 Loading HuggingFaceTB/SmolLM2-360M-Instruct (Ultra Fast 4-Bit)...")
|
| 570 |
-
try:
|
| 571 |
-
model_id = "HuggingFaceTB/SmolLM2-360M-Instruct"
|
| 572 |
-
tokenizer = AutoTokenizer.from_pretrained(model_id)
|
| 573 |
-
quant_config = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=torch.float16, bnb_4bit_quant_type="nf4")
|
| 574 |
-
|
| 575 |
-
if torch.cuda.is_available():
|
| 576 |
-
model = AutoModelForCausalLM.from_pretrained(model_id, quantization_config=quant_config, device_map="auto")
|
| 577 |
-
else:
|
| 578 |
-
model = AutoModelForCausalLM.from_pretrained(model_id, device_map="cpu", torch_dtype=torch.float32)
|
| 579 |
-
|
| 580 |
-
ai_generator = pipeline("text-generation", model=model, tokenizer=tokenizer)
|
| 581 |
-
print("✅ Fast 4-Bit AI Ready!")
|
| 582 |
-
except Exception as e:
|
| 583 |
-
print(f"❌ Model Loading Failed: {e}")
|
| 584 |
-
|
| 585 |
-
async def run_ai_task(messages, max_tokens: int, query_key: str):
|
| 586 |
-
if not ai_generator: return "⚠️ AI Model is starting up."
|
| 587 |
-
if query_key in AI_RESPONSE_CACHE: return AI_RESPONSE_CACHE[query_key]
|
| 588 |
-
|
| 589 |
-
try: await asyncio.wait_for(ai_semaphore.acquire(), timeout=0.1)
|
| 590 |
-
except asyncio.TimeoutError:
|
| 591 |
-
return "⚠️ AI Servers are fully occupied right now. Displaying your ultra-fast search results below!"
|
| 592 |
-
|
| 593 |
-
try:
|
| 594 |
-
def _gen():
|
| 595 |
-
tokenizer = ai_generator.tokenizer
|
| 596 |
-
try: prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
|
| 597 |
-
except: prompt = "\n".join([f"{m['role'].capitalize()}: {m['content']}" for m in messages]) + "\nAssistant: "
|
| 598 |
-
result = ai_generator(prompt, max_new_tokens=max_tokens, do_sample=False, truncation=True, return_full_text=False)
|
| 599 |
-
return result[0]['generated_text'].strip()
|
| 600 |
-
|
| 601 |
-
response = await asyncio.to_thread(_gen)
|
| 602 |
-
if len(AI_RESPONSE_CACHE) > MAX_AI_CACHE_SIZE: AI_RESPONSE_CACHE.clear()
|
| 603 |
-
AI_RESPONSE_CACHE[query_key] = response
|
| 604 |
-
return response
|
| 605 |
-
finally:
|
| 606 |
-
ai_semaphore.release()
|
| 607 |
-
|
| 608 |
-
|
| 609 |
# --- FASTAPI APP ENDPOINTS ---
|
| 610 |
@asynccontextmanager
|
| 611 |
async def lifespan(app: FastAPI):
|
| 612 |
-
# Added Dense Retriever Loader here
|
| 613 |
asyncio.create_task(asyncio.to_thread(load_dense_retriever))
|
| 614 |
asyncio.create_task(asyncio.to_thread(load_and_index_dataset))
|
| 615 |
-
asyncio.create_task(asyncio.to_thread(load_ai_models))
|
| 616 |
yield
|
| 617 |
|
| 618 |
-
app = FastAPI(title="Nebulix Fast Hybrid
|
| 619 |
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
|
| 620 |
|
| 621 |
@app.get("/suggest")
|
| 622 |
async def get_suggestions(q: str = Query(..., min_length=1)):
|
| 623 |
q = normalize_text(q).strip()
|
| 624 |
-
matches = [word for word, count in search_index.suggestions if word.startswith(q)][:
|
| 625 |
return {"query": q, "suggestions": matches}
|
| 626 |
|
| 627 |
@app.get("/search")
|
| 628 |
async def pure_search(q: str = Query(...), content: bool = Query(False), debug: bool = Query(False)):
|
| 629 |
req_id = str(uuid.uuid4())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 630 |
results, intent_info, corrected_query = await asyncio.to_thread(search_index.search, q, debug)
|
| 631 |
-
top_20 = results[:STRICT_RESULT_LIMIT]
|
| 632 |
|
| 633 |
-
for
|
|
|
|
|
|
|
|
|
|
| 634 |
if content: r["content"] = r.get("_internal_content", "")
|
| 635 |
r.pop("_internal_content", None)
|
| 636 |
|
| 637 |
-
|
| 638 |
"request_id": req_id,
|
| 639 |
"query": q,
|
| 640 |
"corrected_query": corrected_query,
|
| 641 |
-
"results_count": len(
|
| 642 |
-
"results":
|
| 643 |
-
}
|
| 644 |
-
|
| 645 |
-
async def process_aimode(q: str, include_content: bool, debug: bool, max_ai_tokens: int):
|
| 646 |
-
req_id = str(uuid.uuid4())
|
| 647 |
-
cache_key = f"ai_{normalize_text(q)}_{max_ai_tokens}"
|
| 648 |
-
|
| 649 |
-
results, intent_info, corrected_query = await asyncio.to_thread(search_index.search, q, debug)
|
| 650 |
-
top_20 = results[:STRICT_RESULT_LIMIT]
|
| 651 |
-
|
| 652 |
-
if cache_key in AI_RESPONSE_CACHE:
|
| 653 |
-
for r in top_20:
|
| 654 |
-
if include_content: r["content"] = r.get("_internal_content", "")
|
| 655 |
-
r.pop("_internal_content", None)
|
| 656 |
-
|
| 657 |
-
return {
|
| 658 |
-
"request_id": req_id, "query": q, "corrected_query": corrected_query,
|
| 659 |
-
"ai_response": AI_RESPONSE_CACHE[cache_key], "results_count": len(top_20),
|
| 660 |
-
"results": top_20, "sources": [], "cached": True
|
| 661 |
-
}
|
| 662 |
-
|
| 663 |
-
query_tokens = tokenize(q)
|
| 664 |
-
rag_context_parts = []
|
| 665 |
-
used_sources = []
|
| 666 |
-
|
| 667 |
-
if top_20:
|
| 668 |
-
valid_sources_count = 0
|
| 669 |
-
for r in top_20[:3]:
|
| 670 |
-
doc_content = r.get("_internal_content", "")
|
| 671 |
-
if include_content: r["content"] = doc_content
|
| 672 |
-
|
| 673 |
-
if doc_content and valid_sources_count < 3:
|
| 674 |
-
best_chunks = rank_chunks_for_rag(doc_content, query_tokens)
|
| 675 |
-
if best_chunks:
|
| 676 |
-
rag_context_parts.append(f"[{valid_sources_count+1}]: {' ... '.join(best_chunks)}")
|
| 677 |
-
used_sources.append({"id": valid_sources_count+1, "title": r["title"], "url": r["url"]})
|
| 678 |
-
valid_sources_count += 1
|
| 679 |
-
|
| 680 |
-
for r in top_20: r.pop("_internal_content", None)
|
| 681 |
-
|
| 682 |
-
system_prompt = (
|
| 683 |
-
"You are a helpful AI. Answer directly using the context. "
|
| 684 |
-
"CRITICAL RULE: You MUST write EXACTLY one short, complete sentence. "
|
| 685 |
-
"Keep it under 15 words. End your sentence with a full stop (.)."
|
| 686 |
-
)
|
| 687 |
-
combined_input = f"Context:\n{chr(10).join(rag_context_parts)}\n\nQuery: {q}"
|
| 688 |
-
|
| 689 |
-
if ai_generator:
|
| 690 |
-
tokenizer = ai_generator.tokenizer
|
| 691 |
-
encoded_input = tokenizer.encode(combined_input, add_special_tokens=False)
|
| 692 |
-
if len(encoded_input) > 90:
|
| 693 |
-
encoded_input = encoded_input[:90]
|
| 694 |
-
combined_input = tokenizer.decode(encoded_input, skip_special_tokens=True)
|
| 695 |
-
|
| 696 |
-
messages = [
|
| 697 |
-
{"role": "system", "content": system_prompt},
|
| 698 |
-
{"role": "user", "content": combined_input}
|
| 699 |
-
]
|
| 700 |
-
|
| 701 |
-
ai_reply = await run_ai_task(messages, max_tokens=max_ai_tokens, query_key=cache_key)
|
| 702 |
-
|
| 703 |
-
return {
|
| 704 |
-
"request_id": req_id, "query": q, "corrected_query": corrected_query,
|
| 705 |
-
"ai_response": ai_reply, "results_count": len(top_20),
|
| 706 |
-
"results": top_20, "sources": used_sources, "cached": False
|
| 707 |
}
|
| 708 |
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
|
| 713 |
-
|
| 714 |
-
async def ai_mode_post(q: str = Form(...), content: bool = Form(False), debug: bool = Form(False)):
|
| 715 |
-
return await process_aimode(q, content, debug, max_ai_tokens=20)
|
| 716 |
|
| 717 |
if __name__ == "__main__":
|
| 718 |
-
uvicorn.run(app, host="0.0.0.0", port=7860)
|
|
|
|
| 16 |
from contextlib import asynccontextmanager
|
| 17 |
from typing import List, Dict, Any, Tuple, Set
|
| 18 |
|
| 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
|
|
|
|
| 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 ""
|
|
|
|
| 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 = []
|
| 100 |
+
news_terms = {"latest", "news", "today", "breaking", "recent", "updates", "current"}
|
| 101 |
if any(term in query_tokens for term in news_terms): intents.append("NEWS")
|
| 102 |
def_terms = {"definition", "meaning", "kya", "matlab"}
|
| 103 |
if any(term in query_tokens for term in def_terms): intents.append("DEFINITION")
|
| 104 |
how_terms = {"tutorial", "guide", "steps", "learn", "how", "kaise"}
|
| 105 |
if any(term in query_tokens for term in how_terms): intents.append("HOW_TO")
|
| 106 |
+
comp_terms = {"vs", "versus", "compare", "comparison", "difference", "best"}
|
| 107 |
if any(term in query_tokens for term in comp_terms): intents.append("COMPARISON")
|
| 108 |
+
local_terms = {"near", "restaurant", "restaurants", "location", "weather"}
|
| 109 |
if any(term in query_tokens for term in local_terms): intents.append("LOCAL")
|
| 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 = []
|
|
|
|
| 121 |
self.df = defaultdict(int)
|
| 122 |
self.avg_doc_len = 0.0
|
| 123 |
|
|
|
|
| 124 |
self.dataset_id_to_internal = {}
|
|
|
|
| 125 |
self.inverted_index = defaultdict(lambda: array.array('I'))
|
| 126 |
self.suggestion_counter = Counter()
|
| 127 |
self.suggestions = []
|
|
|
|
| 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
|
|
|
|
| 149 |
|
| 150 |
url = doc_json.get("url", "")
|
| 151 |
parsed_url = urllib.parse.urlparse(url)
|
|
|
|
| 152 |
domain_str = sys.intern(parsed_url.netloc.lower().replace("www.", ""))
|
| 153 |
|
| 154 |
clean_doc = {
|
|
|
|
| 170 |
content_tokens = tokenize(clean_doc["_fast_content"])[:250]
|
| 171 |
|
| 172 |
all_tokens = title_tokens + desc_tokens + domain_tokens + headings_tokens + snippet_tokens + content_tokens
|
|
|
|
| 173 |
clean_doc["_token_len"] = len(all_tokens)
|
| 174 |
self.total_len += len(all_tokens)
|
| 175 |
|
|
|
|
| 194 |
self.avg_doc_len = self.total_len / max(1, self.total_docs)
|
| 195 |
self.suggestions = self.suggestion_counter.most_common(20000)
|
| 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)
|
|
|
|
| 233 |
|
| 234 |
doc_len = doc.get("_token_len", 1)
|
| 235 |
avg_dl = max(1.0, self.avg_doc_len)
|
|
|
|
| 236 |
content_lower = content.lower()
|
| 237 |
snippet_lower = snippet.lower()
|
| 238 |
|
| 239 |
for term in query_tokens_sorted:
|
| 240 |
+
tf = (title_toks.count(term) * 4 + headings_toks.count(term) * 3 + desc_toks.count(term) * 2 + snippet_lower.count(term) * 2 + content_lower.count(term))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
if tf > 0:
|
| 242 |
tf_num = tf * (self.k1 + 1)
|
| 243 |
tf_den = tf + self.k1 * (1 - self.b + self.b * (doc_len / avg_dl))
|
|
|
|
| 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):
|
|
|
|
| 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)
|
|
|
|
| 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 = (
|
|
|
|
| 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]
|
|
|
|
| 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),
|
| 434 |
"title": doc.get("_fast_title", ""),
|
|
|
|
| 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
|
|
|
|
| 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 |
|
|
|
|
| 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")
|
|
|
|
| 499 |
if os.path.exists(data_folder):
|
| 500 |
jsonl_files = [f for f in os.listdir(data_folder) if f.endswith(".jsonl")]
|
| 501 |
docs_processed = 0
|
|
|
|
| 502 |
for file in jsonl_files:
|
| 503 |
+
with open(os.path.join(data_folder, file), "r", encoding="utf-8") as f:
|
|
|
|
| 504 |
for line in f:
|
| 505 |
if not line.strip(): continue
|
| 506 |
try:
|
| 507 |
+
search_index.add_document(json.loads(line))
|
|
|
|
| 508 |
docs_processed += 1
|
|
|
|
| 509 |
if docs_processed % 50000 == 0:
|
| 510 |
gc.collect()
|
| 511 |
+
print(f"🔄 Indexed {docs_processed} docs...")
|
| 512 |
+
except Exception: pass
|
|
|
|
|
|
|
| 513 |
search_index.finalize_build()
|
| 514 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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")
|
| 526 |
async def get_suggestions(q: str = Query(..., min_length=1)):
|
| 527 |
q = normalize_text(q).strip()
|
| 528 |
+
matches = [word for word, count in search_index.suggestions if word.startswith(q)][:5]
|
| 529 |
return {"query": q, "suggestions": matches}
|
| 530 |
|
| 531 |
@app.get("/search")
|
| 532 |
async def pure_search(q: str = Query(...), content: bool = Query(False), debug: bool = Query(False)):
|
| 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:
|
| 549 |
if content: r["content"] = r.get("_internal_content", "")
|
| 550 |
r.pop("_internal_content", None)
|
| 551 |
|
| 552 |
+
final_response = {
|
| 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 |
|
| 564 |
+
return final_response
|
|
|
|
|
|
|
| 565 |
|
| 566 |
if __name__ == "__main__":
|
| 567 |
+
uvicorn.run(app, host="0.0.0.0", port=7860, log_level="warning")
|