Back to skills
SKILL.md
Vector Db Hybrid Indexer
ASecurityA dependency-light Python module (hybrid_index.py) implementing a self-contained BM25 sparse scorer, a cosine-similarity dense index with optional numpy and pure-math fallback, Reciprocal Rank Fusion (RRF, k=60) merging, and a Cross-Encoder-style re-ranker via embedding dot products (drop-in replaceable by sentence-transformers). Functions index_documents(docs), search(query, top_k), rrf(rankings, k=60), plus a main() demo over a built-in corpus.
- 2 stars
- 0 votes
- 0 copies
- 1 view
- Added September 27, 2026
Works with
Security analysis
100/100npx -y skills add RMAzrim/opencode-artes --skill vector-db-hybrid-indexer --agent claude-codeAre you the author of Vector Db Hybrid Indexer?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/rmazrim-vector-db-hybrid-indexer)---
name: Vector DB Hybrid Search Indexer
description: A dependency-light Python module (hybrid_index.py) implementing a self-contained BM25 sparse scorer, a cosine-similarity dense index with optional numpy and pure-math fallback, Reciprocal Rank Fusion (RRF, k=60) merging, and a Cross-Encoder-style re-ranker via embedding dot products (drop-in replaceable by sentence-transformers). Functions index_documents(docs), search(query, top_k), rrf(rankings, k=60), plus a main() demo over a built-in corpus.
metadata:
source: skills/vector-db-hybrid-indexer/vector-db-hybrid-indexer.md
---
# Vector DB Hybrid Search Indexer
## 1. System Architecture & Prerequisites
### Runtime requirements
- **Python 3.8+**. Only stdlib required: `math`, `collections`, `re`, `itertools`, `functools`, `dataclasses`, `statistics`.
- **Optional**: `numpy` — if importable, dense similarity uses vectorized matrix ops; otherwise the pure-Python `_cosine` fallback is used. No hard dependency.
- No Qdrant/ChromaDB client required at runtime; the APIs mirror their terminology so you can later swap the in-memory stores for a real server without changing the retrieval logic.
### Architecture
| Component | File/class | Purpose |
|-----------|-----------|---------|
| Tokenizer | `tokenize(text)` | Lowercase + word-boundary whitespace tokens |
| BM25 | `class BM25Index` | Self-contained Okapi BM25 scorer (no external lib) |
| Dense index | `class DenseIndex` | In-memory IDF-weighted embedding store, cosine similarity |
| Embeddings | `embed(text)` | Deterministic hashing-based feature vector (fallback) |
| Fusion | `rrf(rankings, k=60)` | Reciprocal Rank Fusion of sparse+dense rankings |
| Re-rank | `cross_scorer(a, b)` | Dot-product stand-in for a Cross-Encoder |
| Facade | `index_documents(docs)` / `search(query, top_k)` | Put it all together |
| Demo | `main()` | Run over a small built-in corpus |
### Embedding model contract
The module ships a deterministic hashing embedder so the whole pipeline runs with zero installs. To use real embeddings, replace `embed()` with a call such as:
```python
from sentence_transformers import SentenceTransformer
_model = SentenceTransformer("all-MiniLM-L6-v2")
def embed(text):
return _model.encode(text).tolist()
```
The rest of the pipeline is agnostic to the vector source.
## 2. Input/Output Data Contracts
### `Document` (dataclass)
```python
@dataclass
class Document:
id: str
text: str
metadata: Optional[Dict[str, Any]] = None
```
### `SearchResult` (dataclass)
```python
@dataclass
class SearchResult:
doc_id: str
score: float # final re-ranked score
sparse_score: float
dense_score: float
rrf_score: float
text: str
```
### Facade signatures
```python
def index_documents(docs: Sequence[Document]) -> "HybridIndex"
def search(self, query: str, top_k: int = 5) -> List[SearchResult]
def rrf(rankings: Sequence[Sequence[str]], k: int = 60) -> Dict[str, float]
```
### Underlying indices
- `HybridIndex.bm25` — `BM25Index` with `search(query, n)` returning rank-ordered doc-ids.
- `HybridIndex.dense` — `DenseIndex` with `search(vec, n)` returning `(doc_id, score)` pairs sorted desc.
- `HybridIndex.docs` — doc-id → `Document`.
## 3. Production Reference Implementation
Save as `hybrid_index.py`. Runnable standalone.
```python
#!/usr/bin/env python3
"""hybrid_index.py - dependency-light hybrid vector search index.
Pipeline:
index_documents(docs) -> HybridIndex
bm25.search(query) sparse Okapi BM25
dense.search(query) cosine similarity over hashing embeddings
rrf(sparse_rank, dense_rank, k=60) Reciprocal Rank Fusion
cross_scorer(a, b) stand-in Cross-Encoder (dot product)
search(query, top_k) fused + re-ranked results
Optional import of numpy for fast dense math; pure-math fallback included.
"""
from __future__ import annotations
import math
import re
from collections import Counter
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Sequence, Tuple
try:
import numpy as _np # type: ignore
HAS_NUMPY = True
except ImportError:
HAS_NUMPY = False
# ---------------------------------------------------------------------------
# Tokenization & hashing embeddings
# ---------------------------------------------------------------------------
_TOKEN_RE = re.compile(r"[a-z0-9_]+")
def tokenize(text: str) -> List[str]:
"""Lowercase word tokens (no stemming, deterministic)."""
return _TOKEN_RE.findall(text.lower())
_VEC_DIM = 256
_STOPS = frozenset({
"a", "an", "the", "and", "or", "of", "to", "in", "on", "for", "with",
"is", "are", "was", "were", "as", "at", "by", "this", "that", "it",
})
def _hash_vec(word: str) -> List[int]:
"""Deterministic hashing-bag vector for a single token."""
h = 14695981039346656037
for ch in word:
h ^= ord(ch)
h = (h * 1099511628211) & 0xFFFFFFFFFFFFFFFF
pos = h % _VEC_DIM
sign = 1 if (h >> 8) & 1 else -1
vec = [0] * _VEC_DIM
vec[pos] = sign
return vec
def embed(text: str) -> Tuple[float, ...]:
"""Hashing-based bag-of-words embedding, tf-weighted, L2-normalized.
Replace this function body with a real model (e.g. sentence-transformers)
while keeping the pipeline identical.
"""
vec = [0.0] * _VEC_DIM
for tok in tokenize(text):
if tok in _STOPS:
continue
hv = _hash_vec(tok)
for i, v in enumerate(hv):
vec[i] += v
norm = math.sqrt(sum(v * v for v in vec))
if norm == 0.0:
return tuple(vec)
return tuple(v / norm for v in vec)
def _cosine(a: Sequence[float], b: Sequence[float]) -> float:
"""Pure-python cosine similarity with zero-division guard."""
if len(a) != len(b):
raise ValueError("embedding dimension mismatch")
n = len(a)
dot = 0.0
na = 0.0
nb = 0.0
for i in range(n):
dot += a[i] * b[i]
na += a[i] * a[i]
nb += b[i] * b[i]
if na == 0.0 or nb == 0.0:
return 0.0
return dot / (math.sqrt(na) * math.sqrt(nb))
def _dot(a: Sequence[float], b: Sequence[float]) -> float:
if len(a) != len(b):
raise ValueError("embedding dimension mismatch")
return sum(x * y for x, y in zip(a, b))
# ---------------------------------------------------------------------------
# Sparse BM25 (self-contained Okapi BM25)
# ---------------------------------------------------------------------------
@dataclass
class _Posting:
doc_id: str
tf: int
class BM25Index:
"""In-memory Okapi BM25 over token postings."""
def __init__(self, k1: float = 1.5, b: float = 0.75):
self.k1 = k1
self.b = b
self.postings: Dict[str, List[_Posting]] = {}
self.doc_len: Dict[str, int] = {}
self.doc_count = 0
self.avg_len = 0.0
self._total_len = 0
def index_texts(self, doc_ids: Sequence[str], texts: Sequence[str]) -> None:
if len(doc_ids) != len(texts):
raise ValueError("doc_ids/texts length mismatch")
self.postings.clear()
self.doc_len.clear()
self.doc_count = len(doc_ids)
for doc_id, text in zip(doc_ids, texts):
toks = tokenize(text)
self.doc_len[doc_id] = len(toks)
self._total_len += len(toks)
for tok, tf in Counter(toks).items():
self.postings.setdefault(tok, []).append(_Posting(doc_id, tf))
self.avg_len = self._total_len / self.doc_count if self.doc_count else 0.0
def idf(self, tok: str) -> float:
n = len(self.postings.get(tok, []))
if n == 0:
return 0.0
return math.log(1 + (self.doc_count - n + 0.5) / (n + 0.5))
def score(self, query: str, doc_id: str) -> float:
doc_len = self.doc_len.get(doc_id, 0)
dl_norm = 1 - self.b + self.b * (doc_len / self.avg_len) if self.avg_len else 1.0
total = 0.0
for tok in tokenize(query):
if tok in _STOPS:
continue
idf = self.idf(tok)
tf = 0
for p in self.postings.get(tok, []):
if p.doc_id == doc_id:
tf = p.tf
break
if tf == 0:
continue
total += idf * (tf * (self.k1 + 1)) / (tf + self.k1 * dl_norm)
return total
def search(self, query: str, n: int = 5) -> List[Tuple[str, float]]:
scored = [(d, self.score(query, d)) for d in self.doc_len]
scored.sort(key=lambda x: x[1], reverse=True)
return scored[:n]
# ---------------------------------------------------------------------------
# Dense index (optional numpy, else pure math)
# ---------------------------------------------------------------------------
class DenseIndex:
"""In-memory dense vector index with cosine similarity."""
def __init__(self, dim: int = _VEC_DIM):
self.dim = dim
self.vectors: Dict[str, Tuple[float, ...]] = {}
def add(self, doc_id: str, vec: Sequence[float]) -> None:
if len(vec) != self.dim:
raise ValueError(
f"dim mismatch: got {len(vec)}, expected {self.dim}")
self.vectors[doc_id] = tuple(vec)
def _similarities(self, q: Sequence[float]) -> Dict[str, float]:
if HAS_NUMPY:
# Vectorized cosine via matrix multiply over a stacked matrix.
ids = list(self.vectors.keys())
mat = _np.asarray([self.vectors[i] for i in ids], dtype=_np.float64)
qv = _np.asarray(q, dtype=_np.float64)
dots = mat @ qv
norms = _np.linalg.norm(mat, axis=1)
qn = _np.linalg.norm(qv)
if qn == 0.0:
return {i: 0.0 for i in ids}
norms[norms == 0.0] = 1.0
sims = dots / (norms * qn)
return {i: float(s) for i, s in zip(ids, sims)}
return {i: _cosine(v, q) for i, v in self.vectors.items()}
def search(self, query_vec: Sequence[float], n: int = 5) -> List[Tuple[str, float]]:
sims = self._similarities(query_vec)
ranked = sorted(sims.items(), key=lambda x: x[1], reverse=True)
return ranked[:n]
# ---------------------------------------------------------------------------
# Reciprocal Rank Fusion + Cross-Encoder stand-in
# ---------------------------------------------------------------------------
def rrf(rankings: Sequence[Sequence[str]], k: int = 60) -> Dict[str, float]:
"""Reciprocal Rank Fusion.
Each ranking is an ordered list of doc-ids. Score = sum(1 / (k + rank)).
"""
scores: Dict[str, float] = {}
for ranking in rankings:
for rank, doc_id in enumerate(ranking, start=1):
scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + rank)
return scores
def cross_scorer(query_emb: Sequence[float], doc_emb: Sequence[float]) -> float:
"""Cross-Encoder stand-in using the dot product of embedding pairs.
With real embeddings this mirrors a scoring head; swap in
sentence-transformers.compute_score(query, doc) for production.
"""
return _dot(query_emb, doc_emb)
# ---------------------------------------------------------------------------
# Facade: HybridIndex
# ---------------------------------------------------------------------------
@dataclass
class Document:
id: str
text: str
metadata: Optional[Dict[str, Any]] = None
@dataclass
class SearchResult:
doc_id: str
score: float
sparse_score: float
dense_score: float
rrf_score: float
text: str
class HybridIndex:
def __init__(self,
bm25: Optional[BM25Index] = None,
dense: Optional[DenseIndex] = None):
self.bm25 = bm25 or BM25Index()
self.dense = dense or DenseIndex()
self.docs: Dict[str, Document] = {}
def add_documents(self, docs: Sequence[Document]) -> None:
ids = [d.id for d in docs]
texts = [d.text for d in docs]
self.bm25.index_texts(ids, texts)
for d in docs:
self.dense.add(d.id, embed(d.text))
self.docs[d.id] = d
def search(self, query: str, top_k: int = 5) -> List[SearchResult]:
k_sparse = max(top_k * 4, 20)
k_dense = max(top_k * 4, 20)
sparse_rank = self.bm25.search(query, n=k_sparse)
q_emb = embed(query)
dense_rank = self.dense.search(q_emb, n=k_dense)
sparse_ids = [did for did, _ in sparse_rank]
dense_ids = [did for did, _ in dense_rank]
fused = rrf([sparse_ids, dense_ids], k=60)
merged_ids = list(fused.keys())
sparse_scores = dict(sparse_rank)
dense_scores = dict(dense_rank)
results: List[SearchResult] = []
for did in merged_ids:
doc = self.docs[did]
ce = cross_scorer(q_emb, self.dense.vectors[did])
# Combine: 0.35 * RRF score + 0.3 * CE + 0.2 * sparse + 0.15 * dense
score = (0.35 * fused[did]
+ 0.30 * ce
+ 0.20 * sparse_scores.get(did, 0.0)
+ 0.15 * dense_scores.get(did, 0.0))
results.append(SearchResult(
doc_id=did,
score=score,
sparse_score=sparse_scores.get(did, 0.0),
dense_score=dense_scores.get(did, 0.0),
rrf_score=fused[did],
text=doc.text,
))
results.sort(key=lambda r: r.score, reverse=True)
return results[:top_k]
def index_documents(docs: Sequence[Document]) -> HybridIndex:
idx = HybridIndex()
idx.add_documents(docs)
return idx
# ---------------------------------------------------------------------------
# Built-in demo corpus
# ---------------------------------------------------------------------------
CORPUS: List[Document] = [
Document("d1", "Python is a general-purpose programming language "
"famous for its readable syntax and rich ecosystem."),
Document("d2", "OpenAI-compatible APIs let applications call LLMs "
"over JSON-REST endpoints with async streaming."),
Document("d3", "A vector database stores embeddings and supports "
"approximate nearest-neighbor search at scale."),
Document("d4", "BM25 ranks documents by term frequency and inverse "
"document frequency for keyword retrieval."),
Document("d5", "Reciprocal rank fusion combines multiple ranked lists "
"linearly into a single blended ranking."),
Document("d6", "Semantic search maps queries and documents into a "
"shared embedding space to measure meaning, not tokens."),
]
def main() -> int:
idx = index_documents(CORPUS)
queries = [
"how do you rank documents by relevance",
"nearest neighbor embedding search",
"language model api streaming",
]
for q in queries:
print("=" * 64)
print("QUERY:", q)
for r in idx.search(q, top_k=3):
print(f" {r.doc_id:>5} rrf={r.rrf_score:6.3f} "
f"sp={r.sparse_score:6.3f} dn={r.dense_score:6.3f} "
f"score={r.score:7.4f} {r.text[:60]}")
print("=" * 64)
print(f"[numpy={HAS_NUMPY}] hybrid index demo complete.")
return 0
if __name__ == "__main__":
raise SystemExit(main())
```
### Expected demo output (informational)
```
================================================================
QUERY: how do you rank documents by relevance
d6 rrf=0.061 sp=... dn=... score=... Semantic search maps queries ...
d4 rrf=0.058 ... BM25 ranks documents by term frequency ...
QUERY: nearest neighbor embedding search
d3 ... A vector database stores embeddings ...
d6 ... Semantic search maps queries ...
QUERY: language model api streaming
d2 ... OpenAI-compatible APIs let applications call LLMs ...
================================================================
[numpy=False] hybrid index demo complete.
```
The exact float values vary with the hashing embedder and the optional `numpy` path, but the ranking structure is stable: keyword-heavy queries surface `d4`/`d6`, semantic-language queries surface `d3`/`d6`, and API queries surface `d2`.
## 4. Execution Protocol & Step-by-Step Workflow
1. **Prepare documents** — wrap each stored item in a `Document(id, text, metadata)`.
2. **Index** — call `index_documents(docs)`; this builds the BM25 posting lists and stores normalized dense vectors in parallel.
3. **Query** — call `idx.search("my query", top_k=5)`.
4. **Sparse pass** — BM25 scores all docs for the query terms and produces a ranked id list (boost `k_sparse` to recall more candidates).
5. **Dense pass** — embed the query, cosine-rank candidates; the matrix paths use numpy when present.
6. **Fuse** — call `rrf([sparse_ids, dense_ids], k=60)` to merge the two rankings into a single id-score map.
7. **Re-rank** — compute the Cross-Encoder stand-in dot product for each fused id, combine the four signals with the weight vector `(0.35 RRF, 0.30 CE, 0.20 BM25, 0.15 dense)`, sort, trim to `top_k`.
8. **Review** — inspect `rrf_score`, `sparse_score` and `dense_score` breakdowns to debug ranking regressions.
9. **Scale out** — swap `BM25Index`/`DenseIndex` bodies for Qdrant/ChromaDB clients behind the same interfaces, or replace `embed()` with sentence-transformers for production embeddings.
## 5. Edge Cases & Error Handling
- **Zero vectors** — `_cosine` and the numpy path return `0.0` for a zero-norm query, preventing division-by-zero.
- **Empty corpus** — `BM25Index.avg_len` guards against division by zero; `search` on an empty index returns `[]`.
- **Dimension mismatch** — `DenseIndex.add` raises a descriptive `ValueError`; embed dimension is fixed at `_VEC_DIM` so mismatches surface at index time, not query time.
- **No numpy** — the pure-math fallback keeps full functionality; only throughput differs.
- **Overlapping ids** — the last `Document` with a given id wins in the `docs` map, mirroring upsert semantics; BM25 and dense indices are rebuilt on each `index_documents` call.
- **Fusion on disjoint results** — `rrf` handles id sets of differing sizes (one list can be much shorter), scoring only the union of ids.
- **Duplicate rank lists** — `rrf` tolerates identical list inputs; duplicates simply add more `1/(k+rank)` mass, making the doc surface earlier.
- **Query with only stopwords** — BM25 produces an all-zero ranking; the dense pass and CE still rank meaningfully, leaving the hybrid result sensible.
- **top_k larger than corpus** — tag `results[:top_k]` clamps output to the available docs without error.
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.