CAntoniadis commited on
Commit
d2dadf2
·
verified ·
1 Parent(s): ff800db

Update simple_search_engine/search_engine.py

Browse files
Files changed (1) hide show
  1. simple_search_engine/search_engine.py +49 -45
simple_search_engine/search_engine.py CHANGED
@@ -4,16 +4,28 @@ from typing import List, Dict
4
  from sklearn.feature_extraction.text import TfidfVectorizer
5
  from sklearn.metrics.pairwise import linear_kernel
6
  from rank_bm25 import BM25Okapi
 
 
 
 
 
 
 
7
 
8
- CORPUS_DIR = "../corpus" # Now points to the unified directory
9
 
10
  class SimpleSearchEngine:
11
  def __init__(self, corpus_dir: str = CORPUS_DIR):
12
  self.corpus_dir = corpus_dir
13
  self.documents: List[Dict] = []
 
 
14
  self.vectorizer: TfidfVectorizer | None = None
15
  self.doc_tfidf = None
16
 
 
 
 
 
17
  def _load_documents(self):
18
  docs = []
19
  doc_id = 0
@@ -32,7 +44,6 @@ class SimpleSearchEngine:
32
  with open(path, "r", encoding="utf-8") as f:
33
  data = json.load(f)
34
 
35
- # Extract TF-IDF specific text
36
  text = data.get("tf_idf_text", "").strip()
37
  url = data.get("url", "")
38
  title = data.get("title", os.path.splitext(fname)[0])
@@ -45,13 +56,12 @@ class SimpleSearchEngine:
45
  "title": title,
46
  "url": url,
47
  "path": path,
48
- "text": text, # Used for indexing
49
  })
50
  doc_id += 1
51
 
52
  except Exception as e:
53
  print(f"Error reading {path}: {e}")
54
- continue
55
 
56
  self.documents = docs
57
  print(f"Loaded {len(self.documents)} documents from {self.corpus_dir}")
@@ -64,63 +74,57 @@ class SimpleSearchEngine:
64
  print("No documents found to index.")
65
  return
66
 
 
67
  self.vectorizer = TfidfVectorizer(analyzer="word", token_pattern=r"\b\w+\b")
68
  self.doc_tfidf = self.vectorizer.fit_transform(texts)
69
  print("TF-IDF index built.")
70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  def search(self, query: str, top_k: int = 5):
72
  if self.vectorizer is None or self.doc_tfidf is None:
73
  raise RuntimeError("Index not built. Call build_index() first.")
74
-
75
- # 1) Retrieve a larger candidate set with TF-IDF
76
- initial_k = max(top_k * 5, 20)
77
-
78
  query_vec = self.vectorizer.transform([query])
79
  similarities = linear_kernel(query_vec, self.doc_tfidf).flatten()
80
- top_idx = similarities.argsort()[::-1][:initial_k]
81
-
82
- candidates = []
83
  for idx in top_idx:
84
  doc = self.documents[idx]
85
- candidates.append({
86
- "score": float(similarities[idx]), # TF-IDF score
87
- "title": doc["title"],
88
- "url": doc["url"],
89
- "path": doc["path"],
90
- "text": doc["text"], # full text for reranking
91
- })
92
-
93
- # 2) Return final top_k results
94
- results = []
95
- for doc in candidates[:top_k]:
96
  snippet = doc["text"][:200] + ("..." if len(doc["text"]) > 200 else "")
97
  results.append({
98
- "score": doc["score"],
99
  "title": doc["title"],
100
  "url": doc["url"],
101
  "path": doc["path"],
102
  "snippet": snippet,
103
  })
104
-
105
- return results
106
 
107
- if __name__ == "__main__":
108
- engine = SimpleSearchEngine(corpus_dir=CORPUS_DIR)
109
- engine.build_index()
110
-
111
- while True:
112
- query = input("\nEnter your query (or 'quit'): ").strip()
113
- if query.lower() in {"quit", "exit"}:
114
- break
115
-
116
- try:
117
- hits = engine.search(query, top_k=5)
118
- print(f"\nTop results for: {query!r}")
119
- for i, h in enumerate(hits, start=1):
120
- print(f"\n[{i}] {h['title']} (score={h['score']:.4f})")
121
- print(f" Source: {h['url']}")
122
- print(f" Snippet: {h['snippet']}")
123
- except RuntimeError as e:
124
- print(e)
125
-
126
- # end of code
 
4
  from sklearn.feature_extraction.text import TfidfVectorizer
5
  from sklearn.metrics.pairwise import linear_kernel
6
  from rank_bm25 import BM25Okapi
7
+ import re
8
+ import numpy as np
9
+
10
+ CORPUS_DIR = "../corpus"
11
+
12
+ def _tokenize(text: str):
13
+ return re.findall(r"\b\w+\b", text.lower())
14
 
 
15
 
16
  class SimpleSearchEngine:
17
  def __init__(self, corpus_dir: str = CORPUS_DIR):
18
  self.corpus_dir = corpus_dir
19
  self.documents: List[Dict] = []
20
+
21
+ # TF-IDF
22
  self.vectorizer: TfidfVectorizer | None = None
23
  self.doc_tfidf = None
24
 
25
+ # BM25
26
+ self.bm25: BM25Okapi | None = None
27
+ self._bm25_tokens = []
28
+
29
  def _load_documents(self):
30
  docs = []
31
  doc_id = 0
 
44
  with open(path, "r", encoding="utf-8") as f:
45
  data = json.load(f)
46
 
 
47
  text = data.get("tf_idf_text", "").strip()
48
  url = data.get("url", "")
49
  title = data.get("title", os.path.splitext(fname)[0])
 
56
  "title": title,
57
  "url": url,
58
  "path": path,
59
+ "text": text,
60
  })
61
  doc_id += 1
62
 
63
  except Exception as e:
64
  print(f"Error reading {path}: {e}")
 
65
 
66
  self.documents = docs
67
  print(f"Loaded {len(self.documents)} documents from {self.corpus_dir}")
 
74
  print("No documents found to index.")
75
  return
76
 
77
+ # TF-IDF
78
  self.vectorizer = TfidfVectorizer(analyzer="word", token_pattern=r"\b\w+\b")
79
  self.doc_tfidf = self.vectorizer.fit_transform(texts)
80
  print("TF-IDF index built.")
81
 
82
+ # BM25
83
+ self._bm25_tokens = [_tokenize(t) for t in texts]
84
+ self.bm25 = BM25Okapi(self._bm25_tokens)
85
+ print("BM25 index built.")
86
+
87
+ def search_bm25(self, query: str, top_k: int = 5):
88
+ if self.bm25 is None:
89
+ raise RuntimeError("BM25 index not built. Call build_index() first.")
90
+
91
+ q_tokens = _tokenize(query)
92
+ scores = np.array(self.bm25.get_scores(q_tokens), dtype=float)
93
+
94
+ ranked_idx = scores.argsort()[::-1][:top_k]
95
+
96
+ results = []
97
+ for idx in ranked_idx:
98
+ doc = self.documents[int(idx)]
99
+ snippet = doc["text"][:200] + ("..." if len(doc["text"]) > 200 else "")
100
+ results.append({
101
+ "score": float(scores[int(idx)]),
102
+ "title": doc["title"],
103
+ "url": doc["url"],
104
+ "path": doc["path"],
105
+ "snippet": snippet,
106
+ })
107
+
108
+ return results
109
+
110
  def search(self, query: str, top_k: int = 5):
111
  if self.vectorizer is None or self.doc_tfidf is None:
112
  raise RuntimeError("Index not built. Call build_index() first.")
113
+
 
 
 
114
  query_vec = self.vectorizer.transform([query])
115
  similarities = linear_kernel(query_vec, self.doc_tfidf).flatten()
116
+ top_idx = similarities.argsort()[::-1][:top_k]
117
+
118
+ results = []
119
  for idx in top_idx:
120
  doc = self.documents[idx]
 
 
 
 
 
 
 
 
 
 
 
121
  snippet = doc["text"][:200] + ("..." if len(doc["text"]) > 200 else "")
122
  results.append({
123
+ "score": float(similarities[idx]),
124
  "title": doc["title"],
125
  "url": doc["url"],
126
  "path": doc["path"],
127
  "snippet": snippet,
128
  })
 
 
129
 
130
+ return results