Abhishek
Initialize project files and updated hackathon tags
50f83f4
Raw
History Blame Contribute Delete
4.55 kB
from __future__ import annotations
import math
import re
from collections import Counter, defaultdict
from dataclasses import dataclass
from typing import Iterable
TOKEN_RE = re.compile(r"[a-z0-9]+")
STOPWORDS = {
"a", "an", "and", "are", "as", "at", "be", "by", "for", "from",
"has", "have", "in", "is", "it", "of", "on", "or", "that", "the",
"this", "to", "with", "you", "your", "will", "when", "if", "can",
"may", "not", "do", "does", "there", "we", "they", "their", "into",
"over", "under", "than", "then", "them", "our", "was", "were",
}
def tokenize(text: str) -> list[str]:
tokens = [t for t in TOKEN_RE.findall(text.lower()) if t not in STOPWORDS]
return tokens
@dataclass
class SearchHit:
score: float
kind: str
record_id: int
title: str
text: str
citation: str
metadata: dict
class TfIdfIndex:
def __init__(self, docs: list[dict[str, object]]):
self.docs = docs
self.doc_terms: list[Counter[str]] = []
self.doc_norms: list[float] = []
self.idf: dict[str, float] = {}
self._build()
def _build(self) -> None:
doc_freq = defaultdict(int)
raw_terms: list[Counter[str]] = []
for doc in self.docs:
terms = Counter(tokenize(str(doc.get("text", ""))))
raw_terms.append(terms)
for term in terms:
doc_freq[term] += 1
total_docs = max(len(self.docs), 1)
self.idf = {
term: math.log((1 + total_docs) / (1 + df)) + 1.0
for term, df in doc_freq.items()
}
self.doc_terms = raw_terms
self.doc_norms = [self._norm(tf) for tf in self.doc_terms]
def _norm(self, tf: Counter[str]) -> float:
total = 0.0
for term, count in tf.items():
total += (count * self.idf.get(term, 0.0)) ** 2
return math.sqrt(total) or 1.0
def query(self, text: str, top_k: int = 5) -> list[tuple[int, float]]:
q_tf = Counter(tokenize(text))
if not q_tf or not self.docs:
return []
q_norm = self._norm(q_tf)
scores: list[tuple[int, float]] = []
for i, doc_tf in enumerate(self.doc_terms):
dot = 0.0
for term, q_count in q_tf.items():
if term not in doc_tf:
continue
dot += (q_count * self.idf.get(term, 0.0)) * (doc_tf[term] * self.idf.get(term, 0.0))
score = dot / (q_norm * self.doc_norms[i])
if score > 0:
scores.append((i, score))
scores.sort(key=lambda item: item[1], reverse=True)
return scores[:top_k]
class CombinedSearchIndex:
def __init__(self, section_docs: list[dict[str, object]], job_docs: list[dict[str, object]] | None = None):
self.section_docs = section_docs
self.job_docs = job_docs or []
self.section_index = TfIdfIndex(section_docs)
self.job_index = TfIdfIndex(job_docs) if job_docs else None
def search_sections(self, query: str, top_k: int = 5) -> list[SearchHit]:
hits = []
for idx, score in self.section_index.query(query, top_k=top_k):
doc = self.section_docs[idx]
hits.append(
SearchHit(
score=score,
kind="manual_section",
record_id=int(doc["record_id"]),
title=str(doc["title"]),
text=str(doc["text"]),
citation=str(doc["citation"]),
metadata=dict(doc.get("metadata", {})),
)
)
return hits
def search_jobs(self, query: str, top_k: int = 5) -> list[SearchHit]:
if not self.job_index:
return []
hits = []
for idx, score in self.job_index.query(query, top_k=top_k):
doc = self.job_docs[idx]
hits.append(
SearchHit(
score=score,
kind="job",
record_id=int(doc["record_id"]),
title=str(doc["title"]),
text=str(doc["text"]),
citation=str(doc.get("citation", "")),
metadata=dict(doc.get("metadata", {})),
)
)
return hits
def merge_search_results(*groups: list[SearchHit], top_k: int = 5) -> list[SearchHit]:
merged = [hit for group in groups for hit in group]
merged.sort(key=lambda hit: hit.score, reverse=True)
return merged[:top_k]