LLM-and-RAG-Application-GenAI / src /search_engine.py
Redlion007's picture
Add src modules, tests, CI workflow, and Codecov config
ae2d710
Raw
History Blame Contribute Delete
1.91 kB
import numpy as np
from typing import List, Dict
from src.config import CONFIG
try:
from sentence_transformers import SentenceTransformer as _SentenceTransformer
except ImportError: # pragma: no cover
_SentenceTransformer = None # type: ignore
try:
from rank_bm25 import BM25Okapi as _BM25Okapi
except ImportError: # pragma: no cover
_BM25Okapi = None # type: ignore
class HybridSearchEngine:
"""Combines Dense (Embeddings) and Sparse (BM25) for precision."""
def __init__(self, documents: List[Dict], model=None, bm25=None):
self.docs = documents
if model is not None and bm25 is not None:
self.model = model
self.bm25 = bm25
self.embeddings = self.model.encode(
[d["content"] for d in documents], normalize_embeddings=True
)
else:
self.model = _SentenceTransformer(CONFIG["embedding_model"])
self.embeddings = self.model.encode(
[d["content"] for d in documents], normalize_embeddings=True
)
tokenized_corpus = [d["content"].lower().split() for d in documents]
self.bm25 = _BM25Okapi(tokenized_corpus)
def search(self, query: str, top_k: int = 5) -> List[Dict]:
q_emb = self.model.encode([query], normalize_embeddings=True)[0]
dense_ranks = np.argsort(-np.dot(self.embeddings, q_emb))
bm25_ranks = np.argsort(-self.bm25.get_scores(query.lower().split()))
k = 60
scores = np.zeros(len(self.docs))
for rank, idx in enumerate(dense_ranks):
scores[idx] += CONFIG["dense_weight"] * (1 / (rank + k))
for rank, idx in enumerate(bm25_ranks):
scores[idx] += (1 - CONFIG["dense_weight"]) * (1 / (rank + k))
return [
dict(self.docs[i], rrf_score=scores[i])
for i in np.argsort(-scores)[:top_k]
]