CAntoniadis commited on
Commit
1f03c0f
·
verified ·
1 Parent(s): e4bc698

Reranker adjustments to account for title spam and in text spam of entities (ie Luffy)

Browse files
Files changed (1) hide show
  1. simple_search_engine/search_engine.py +73 -137
simple_search_engine/search_engine.py CHANGED
@@ -8,89 +8,37 @@ import re
8
  CORPUS_DIR = "../corpus" # Now points to the unified directory
9
 
10
  class HeuristicReranker:
11
- """
12
- A simple, classic, pre-neural reranker for search results.
13
-
14
- This reranker is intentionally:
15
- - Non-AI (no embeddings, no transformers)
16
- - Deterministic (same input → same output)
17
- - Explainable (every score adjustment has a clear reason)
18
-
19
- It is designed to run AFTER an initial TF-IDF retrieval step.
20
-
21
- Conceptually:
22
- TF-IDF answers: "Is this document related to the query?"
23
- This reranker answers: "Is this document actually a good match?"
24
- """
25
-
26
  def __init__(
27
  self,
28
  coverage_weight: float = 1.0,
29
- # title_weight: float = 0.8,
30
  length_penalty_weight: float = 0.2,
31
  ideal_length: int = 1500,
32
  ):
33
- """
34
- Initialize the reranker and its tuning parameters.
35
-
36
- Each weight controls how much influence a particular heuristic has
37
- on the final ranking.
38
-
39
- - coverage_weight:
40
- How strongly we reward documents that contain *all* query terms.
41
- This fixes a common TF-IDF failure mode where one repeated word
42
- dominates the score.
43
-
44
- Not USED
45
- - title_weight:
46
- How strongly we reward query matches in the document title.
47
- Titles are usually concise and high-signal.
48
-
49
- - length_penalty_weight:
50
- How strongly we penalize very long documents.
51
- Long documents often mention many topics but answer none clearly.
52
-
53
- - ideal_length:
54
- The document length (in characters) that we consider "reasonable".
55
- Documents longer than this are softly penalized.
56
- """
57
  self.coverage_weight = coverage_weight
58
- # self.title_weight = title_weight
59
  self.length_penalty_weight = length_penalty_weight
60
  self.ideal_length = ideal_length
61
 
62
  def _tokenize(self, text: str) -> list[str]:
63
- """
64
- Convert text into a list of lowercase word tokens.
65
-
66
- This is a deliberately simple tokenizer:
67
- - lowercase
68
- - split on word boundaries
69
- - no stemming or lemmatization
70
-
71
- Why so simple?
72
- Because this reranker is meant to be:
73
- - predictable
74
- - fast
75
- - easy to reason about
76
- """
77
  return re.findall(r"\b\w+\b", text.lower())
78
 
79
- def _coverage_score(self, query_tokens, doc_tokens) -> float:
80
- """
81
- Compute how many query terms appear in the document.
 
 
82
 
83
- Example:
84
- Query tokens: ["python", "error", "handling"]
85
- Document tokens contain: ["python", "handling"]
 
 
86
 
87
- Coverage score = 2 / 3 0.67
 
88
 
89
- Why this matters:
90
- TF-IDF can over-score documents that repeat a single query term.
91
- Coverage ensures documents that mention *all* query terms
92
- are ranked higher.
93
- """
94
  if not query_tokens:
95
  return 0.0
96
 
@@ -98,99 +46,87 @@ class HeuristicReranker:
98
  matched = sum(1 for t in query_tokens if t in doc_token_set)
99
  return matched / len(query_tokens)
100
 
101
- #def _title_boost(self, query_tokens, title: str) -> float:
102
- #"""
103
- #Compute a boost based on query terms appearing in the title.
104
 
105
- #Titles are usually:
106
- #- concise
107
- #- intentional
108
- #- descriptive of the main topic
109
 
110
- #Therefore, a document whose title matches the query
111
- #is often more relevant than one where the match is buried
112
- #deep in the body text.
113
- #"""
114
- #if not title or not query_tokens:
115
- # return 0.0
116
 
117
- #title_tokens = set(self._tokenize(title))
118
- #matched = sum(1 for t in query_tokens if t in title_tokens)
119
- #return matched / len(query_tokens)
 
 
 
120
 
121
- def _length_penalty(self, doc_length: int) -> float:
122
- """
123
- Compute a penalty for very long documents.
 
124
 
125
- Why penalize length?
126
- - Long documents tend to match many queries accidentally
127
- - They often contain background, boilerplate, or tangential info
128
 
129
- This penalty is:
130
- - soft (never removes a document)
131
- - linear (easy to reason about)
132
- """
133
- if doc_length <= self.ideal_length:
134
  return 0.0
135
 
136
- return (doc_length - self.ideal_length) / self.ideal_length
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
  def rerank(self, query: str, candidates: list[dict]) -> list[dict]:
139
- """
140
- Rerank a list of candidate documents.
141
-
142
- Inputs:
143
- - query:
144
- The original user query string.
145
- - candidates:
146
- A list of documents already retrieved by TF-IDF.
147
- Each candidate is expected to contain:
148
- - "score" (TF-IDF score)
149
- - "text" (document body)
150
- - "title" (optional, but recommended)
151
-
152
- Output:
153
- - The same documents, reordered by a refined relevance score.
154
-
155
- Important:
156
- This method does NOT retrieve new documents.
157
- It only reorders existing candidates.
158
- """
159
  query_tokens = self._tokenize(query)
 
160
 
161
  reranked = []
162
  for c in candidates:
163
  doc_text = c["text"]
164
  doc_tokens = self._tokenize(doc_text)
165
 
166
- # Start with the original TF-IDF score.
167
- # This represents basic lexical relevance.
168
- score = c["score"]
169
-
170
- # Add a boost if most or all query terms appear in the document.
171
- score += self.coverage_weight * self._coverage_score(
172
- query_tokens, doc_tokens
173
- )
174
-
175
- # Add a boost if query terms appear in the title.
176
- #score += self.title_weight * self._title_boost(
177
- # query_tokens, c.get("title", "")
178
- #)
 
179
 
180
- # Subtract a penalty if the document is very long.
181
- score -= self.length_penalty_weight * self._length_penalty(
182
- len(doc_text)
183
- )
184
-
185
- # Store the adjusted score alongside the original document data.
186
  reranked.append({**c, "score": score})
187
 
188
- # Sort documents by final score, highest first.
189
  reranked.sort(key=lambda x: x["score"], reverse=True)
190
-
191
  return reranked
192
 
193
-
194
  class SimpleSearchEngine:
195
  def __init__(self, corpus_dir: str = CORPUS_DIR):
196
  self.corpus_dir = corpus_dir
 
8
  CORPUS_DIR = "../corpus" # Now points to the unified directory
9
 
10
  class HeuristicReranker:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  def __init__(
12
  self,
13
  coverage_weight: float = 1.0,
 
14
  length_penalty_weight: float = 0.2,
15
  ideal_length: int = 1500,
16
  ):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  self.coverage_weight = coverage_weight
 
18
  self.length_penalty_weight = length_penalty_weight
19
  self.ideal_length = ideal_length
20
 
21
  def _tokenize(self, text: str) -> list[str]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  return re.findall(r"\b\w+\b", text.lower())
23
 
24
+ def _is_entity_query(self, query_tokens: list[str], query: str) -> bool:
25
+ if len(query_tokens) > 2:
26
+ return False
27
+
28
+ lowered = query.lower()
29
 
30
+ forbidden_terms = {
31
+ "vs", "episode", "season", "arc",
32
+ "fight", "battle", "when", "what",
33
+ "how", "why", "where"
34
+ }
35
 
36
+ if any(term in lowered for term in forbidden_terms):
37
+ return False
38
 
39
+ return True
40
+
41
+ def _coverage_score(self, query_tokens, doc_tokens) -> float:
 
 
42
  if not query_tokens:
43
  return 0.0
44
 
 
46
  matched = sum(1 for t in query_tokens if t in doc_token_set)
47
  return matched / len(query_tokens)
48
 
49
+ def _length_penalty(self, doc_length: int) -> float:
50
+ if doc_length <= self.ideal_length:
51
+ return 0.0
52
 
53
+ return (doc_length - self.ideal_length) / self.ideal_length
 
 
 
54
 
55
+ def _episodic_penalty(self, title: str, url: str) -> float:
56
+ text = f"{title} {url}".lower()
 
 
 
 
57
 
58
+ patterns = [
59
+ "season",
60
+ "episode",
61
+ "transliteration",
62
+ "list_of",
63
+ ]
64
 
65
+ penalty = 0.0
66
+ for p in patterns:
67
+ if p in text:
68
+ penalty += 1.0
69
 
70
+ digit_count = sum(c.isdigit() for c in text)
71
+ penalty += digit_count * 0.1
 
72
 
73
+ return penalty
74
+
75
+ def _entity_title_match(self, query_tokens: list[str], title: str) -> float:
76
+ if not title:
 
77
  return 0.0
78
 
79
+ title_tokens = self._tokenize(title)
80
+
81
+ if title_tokens == query_tokens:
82
+ return 3.0
83
+
84
+ if all(t in title_tokens for t in query_tokens):
85
+ return 1.5
86
+
87
+ return 0.0
88
+
89
+ def _mention_spam_penalty(self, query_tokens: list[str], doc_tokens: list[str]) -> float:
90
+ if len(query_tokens) != 1:
91
+ return 0.0
92
+
93
+ term = query_tokens[0]
94
+ freq = doc_tokens.count(term)
95
+
96
+ if freq <= 10:
97
+ return 0.0
98
+
99
+ return (freq - 10) * 0.05
100
 
101
  def rerank(self, query: str, candidates: list[dict]) -> list[dict]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  query_tokens = self._tokenize(query)
103
+ is_entity = self._is_entity_query(query_tokens, query)
104
 
105
  reranked = []
106
  for c in candidates:
107
  doc_text = c["text"]
108
  doc_tokens = self._tokenize(doc_text)
109
 
110
+ if is_entity:
111
+ score = 0.2 * c["score"]
112
+ score += self._entity_title_match(query_tokens, c.get("title", ""))
113
+ score -= self._episodic_penalty(c.get("title", ""), c.get("url", ""))
114
+ score -= self._mention_spam_penalty(query_tokens, doc_tokens)
115
+ score -= 0.1 * self._length_penalty(len(doc_text))
116
+ else:
117
+ score = c["score"]
118
+ score += self.coverage_weight * self._coverage_score(
119
+ query_tokens, doc_tokens
120
+ )
121
+ score -= self.length_penalty_weight * self._length_penalty(
122
+ len(doc_text)
123
+ )
124
 
 
 
 
 
 
 
125
  reranked.append({**c, "score": score})
126
 
 
127
  reranked.sort(key=lambda x: x["score"], reverse=True)
 
128
  return reranked
129
 
 
130
  class SimpleSearchEngine:
131
  def __init__(self, corpus_dir: str = CORPUS_DIR):
132
  self.corpus_dir = corpus_dir