Spaces:
Runtime error
Runtime error
| 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 | |
| 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] | |