Spaces:
Runtime error
Runtime error
| """ | |
| rag/bm25_index.py | |
| ----------------- | |
| BM25Okapi index for lexical retrieval over chunk texts. | |
| Tokenisation strategy: | |
| Lowercase + strip punctuation EXCEPT hyphens and parentheses. | |
| Rationale: Indian regulatory text contains tokens like "91-day", "4(2)(b)", | |
| "Section-51A" whose internal punctuation carries semantic meaning. | |
| Standard whitespace tokenisation after these targeted strips preserves them. | |
| BM25 parameters: | |
| k1=1.5 β term frequency saturation (standard Okapi default) | |
| b=0.75 β document length normalisation (standard Okapi default) | |
| These are not tuned; the corpus is too small to warrant grid search. | |
| """ | |
| import pickle | |
| import re | |
| from pathlib import Path | |
| from rank_bm25 import BM25Okapi | |
| from rag.models import ChunkRecord | |
| # Strip punctuation that is NOT part of regulatory term identifiers | |
| _STRIP_RE = re.compile(r"[^\w\s\-()]") | |
| class BM25Index: | |
| def __init__(self) -> None: | |
| self._bm25: BM25Okapi | None = None | |
| self._chunks: list[ChunkRecord] = [] | |
| # ββ Tokenisation ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def tokenise(text: str) -> list[str]: | |
| text = text.lower() | |
| text = _STRIP_RE.sub(" ", text) | |
| return text.split() | |
| # ββ Build βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def build(self, chunks: list[ChunkRecord]) -> None: | |
| self._chunks = list(chunks) | |
| tokenised = [self.tokenise(c.text) for c in chunks] | |
| self._bm25 = BM25Okapi(tokenised, k1=1.5, b=0.75) | |
| # ββ Query βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def search(self, query: str, k: int) -> list[tuple[ChunkRecord, float]]: | |
| """Return up to k (chunk, bm25_score) pairs, descending by score.""" | |
| if self._bm25 is None: | |
| raise RuntimeError("BM25Index not built. Call build() or load() first.") | |
| tokens = self.tokenise(query) | |
| scores = self._bm25.get_scores(tokens) | |
| top_k = min(k, len(self._chunks)) | |
| ranked = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)[:top_k] | |
| return [(self._chunks[i], float(scores[i])) for i in ranked] | |
| # ββ Persistence βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def save(self, index_dir: Path) -> None: | |
| index_dir = Path(index_dir) | |
| index_dir.mkdir(parents=True, exist_ok=True) | |
| with open(index_dir / "bm25.pkl", "wb") as fh: | |
| pickle.dump((self._bm25, self._chunks), fh) | |
| def load(cls, index_dir: Path) -> "BM25Index": | |
| obj = cls() | |
| with open(Path(index_dir) / "bm25.pkl", "rb") as fh: | |
| obj._bm25, obj._chunks = pickle.load(fh) | |
| return obj | |
| def size(self) -> int: | |
| return len(self._chunks) | |