mmap-worker / app /rag /sparse.py
jugalgajjar's picture
deploy: hybrid retrieval + reranker
7499087
Raw
History Blame Contribute Delete
2.4 kB
"""BM25-style sparse vectors via fastembed, used for the lexical branch of
hybrid retrieval. Qdrant fuses the dense and sparse branches with RRF."""
import logging
import time
from threading import Lock
log = logging.getLogger("mmap.sparse")
SPARSE_MODEL_NAME = "Qdrant/bm25"
SPARSE_VECTOR_NAME = "bm25"
_model_lock = Lock()
_model = None
def get_sparse_model():
global _model
if _model is None:
with _model_lock:
if _model is None:
from fastembed import SparseTextEmbedding # type: ignore[import-not-found]
last: Exception | None = None
for attempt in range(1, 6):
try:
_model = SparseTextEmbedding(model_name=SPARSE_MODEL_NAME)
break
except Exception as exc: # noqa: BLE001
last = exc
if attempt == 5:
break
backoff = attempt * 5
log.warning(
"sparse model load failed (attempt %d/5): %s — retry in %ds",
attempt,
exc,
backoff,
)
time.sleep(backoff)
if _model is None:
raise RuntimeError(
f"could not load sparse model after 5 attempts: {last}"
) from last
return _model
def encode_passages(texts: list[str]) -> list[tuple[list[int], list[float]]]:
"""Encode chunk texts as (indices, values) pairs suitable for
qdrant_client.models.SparseVector. Order matches input."""
if not texts:
return []
out: list[tuple[list[int], list[float]]] = []
for emb in get_sparse_model().embed(texts):
out.append(
(
[int(i) for i in emb.indices.tolist()],
[float(v) for v in emb.values.tolist()],
)
)
return out
def encode_query(text: str) -> tuple[list[int], list[float]]:
"""Encode a query string with the query-side tokenizer."""
embs = list(get_sparse_model().query_embed([text]))
if not embs:
return [], []
emb = embs[0]
return (
[int(i) for i in emb.indices.tolist()],
[float(v) for v in emb.values.tolist()],
)