bi_agent / memory /sparse_index.py
Prerna43's picture
agent
38830c1
Raw
History Blame Contribute Delete
3.43 kB
from __future__ import annotations
import json
import math
from collections import Counter
from pathlib import Path
from rag.schemas import DocumentChunk, RetrievalHit
class SparseKeywordIndex:
def __init__(self, index_path: str) -> None:
self.index_path = Path(index_path)
self.index_path.parent.mkdir(parents=True, exist_ok=True)
self._records: dict[str, dict] = self._load()
def upsert(self, chunks: list[DocumentChunk]) -> None:
for chunk in chunks:
term_counts = Counter(self._tokenize(chunk.text))
self._records[chunk.chunk_id] = {
"document_id": chunk.document_id,
"text": chunk.text,
"metadata": chunk.metadata,
"term_counts": dict(term_counts),
"length": sum(term_counts.values()),
}
self._save()
def search(self, query: str, top_k: int = 5) -> list[RetrievalHit]:
query_terms = self._tokenize(query)
if not query_terms or not self._records:
return []
doc_freq = Counter()
for record in self._records.values():
for token in set(record["term_counts"]):
doc_freq[token] += 1
total_docs = max(len(self._records), 1)
avg_doc_length = sum(record["length"] for record in self._records.values()) / total_docs
scores: list[tuple[float, str, dict]] = []
for chunk_id, record in self._records.items():
score = 0.0
for term in query_terms:
tf = record["term_counts"].get(term, 0)
if not tf:
continue
idf = math.log(1 + (total_docs - doc_freq[term] + 0.5) / (doc_freq[term] + 0.5))
score += idf * ((tf * 2.2) / (tf + 1.2 * (1 - 0.75 + 0.75 * record["length"] / max(avg_doc_length, 1))))
if score > 0:
scores.append((score, chunk_id, record))
scores.sort(key=lambda item: item[0], reverse=True)
hits = []
for score, chunk_id, record in scores[:top_k]:
hits.append(
RetrievalHit(
chunk_id=chunk_id,
text=record["text"],
score=score,
metadata=record["metadata"],
)
)
return hits
def list_documents(self) -> list[dict]:
seen: dict[str, dict] = {}
for record in self._records.values():
document_id = record.get("document_id", "")
metadata = record.get("metadata", {})
if document_id and document_id not in seen:
seen[document_id] = {
"document_id": document_id,
"file_name": metadata.get("file_name", ""),
}
return sorted(seen.values(), key=lambda item: item["file_name"])
def _load(self) -> dict[str, dict]:
if not self.index_path.exists():
return {}
try:
return json.loads(self.index_path.read_text(encoding="utf-8"))
except Exception:
return {}
def _save(self) -> None:
self.index_path.write_text(json.dumps(self._records, ensure_ascii=True, indent=2), encoding="utf-8")
@staticmethod
def _tokenize(text: str) -> list[str]:
return [token.strip(".,:;!?()[]{}\"'").lower() for token in text.split() if token.strip()]