Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsBlogPro
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Authors
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges
  • Chrome Extension
  • Skill Manager

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

ProTermsPrivacyRefunds
Back to skills

Hybrid Search Bm25 Vector Fusion

ASecurity

Architectural patterns, rank fusion algorithms (RRF, score normalization), HNSW vector index tuning, and production code for hybrid BM25 + dense vector search systems.

8 stars
0 votes
0 copies
0 views
Added 9/29/2026
ai-agentspythongonodedatabaseperformance

Security Analysis

A100/100

Scanned 9/29/2026

$npx -y skills add hamzabellouch/agent-skills --skill hybrid-search-bm25-vector-fusion --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Hybrid Search Bm25 Vector Fusion?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Hybrid Search Bm25 Vector Fusion
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/hamzabellouch-hybrid-search-bm25-vector-fusion/badge)](https://www.skillsdirectory.com/skills/hamzabellouch-hybrid-search-bm25-vector-fusion)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
Files
SKILL.md
---
name: hybrid-search-bm25-vector-fusion
description: Architectural patterns, rank fusion algorithms (RRF, score normalization), HNSW vector index tuning, and production code for hybrid BM25 + dense vector search systems.
---

# Hybrid Search: BM25 & Vector Fusion Architecture

This skill provides architectural principles, rank fusion algorithms, HNSW vector tuning strategies, and production Python code implementations for combining sparse lexical search (BM25) with dense vector retrieval.

---

## 1. Core Architecture & Concept Comparison

Hybrid search combines the complementary strengths of **Sparse Lexical Retrieval** and **Dense Vector Retrieval**:

| Characteristic | BM25 (Sparse Lexical) | Vector Search (Dense Semantic) |
| :--- | :--- | :--- |
| **Matching Mechanism** | Inverted index token exact matching & term frequency | HNSW / IVF nearest neighbor in latent embedding space |
| **Strengths** | Exact keyword matching, rare terms, product IDs, jargon, acronyms | Conceptual similarity, paraphrasing, multi-lingual semantic alignment |
| **Weaknesses** | Vocabulary mismatch, zero recall on synonyms | Out-of-vocabulary domain shifts, failure on specific serial numbers / IDs |
| **Score Scale** | Unbounded $[0, \infty)$ non-linear BM25 score | Bounded $[-1, 1]$ (Cosine) or $[0, 1]$ (Dot Product / IP) |

---

## 2. Rank Fusion Algorithms

### 2.1 Reciprocal Rank Fusion (RRF)
RRF evaluates the rank position (not raw score) of documents across multiple retrieval streams to produce a single unified ranking.

#### Formula:
$$RRF\_Score(d \in D) = \sum_{m \in M} \frac{1}{k + r_m(d)}$$

Where:
- $M$: Set of retrieval algorithms (e.g., [BM25, Dense Vector]).
- $r_m(d)$: Rank of document $d$ in system $m$ (1-indexed).
- $k$: Smoothing constant (standard industry default: $k = 60$).

**Key Advantage**: Immune to score scale disparities across different search engines or models.

---

### 2.2 Relative Score Fusion (Min-Max Normalization + Weighted Sum)
Normalizes raw scores into $[0, 1]$ range before computing a weighted sum score.

#### Formula:
$$S_{norm}(d, m) = \frac{S(d, m) - S_{min}(m)}{S_{max}(m) - S_{min}(m)}$$

$$Score_{hybrid}(d) = \alpha \cdot S_{norm}(d, \text{bm25}) + (1 - \alpha) \cdot S_{norm}(d, \text{vector})$$

Where $\alpha \in [0, 1]$ represents the weight assigned to sparse lexical retrieval.

---

## 3. HNSW Vector Index Tuning Guidelines

Hierarchical Navigable Small World (HNSW) graphs control the speed vs. recall trade-off:

- **`m` (Number of bi-directional links per node)**:
  - Typical range: `16` to `64`.
  - Higher `m` improves recall for high-dimensional vectors (>1024 dims) at the cost of higher memory consumption and build time.
- **`ef_construction` (Depth of search during index creation)**:
  - Typical range: `64` to `512`.
  - Controls index build accuracy. Higher values increase indexing time but improve kNN search accuracy.
- **`ef_search` (Depth of search during query execution)**:
  - Typical range: `40` to `256`.
  - Higher `ef_search` improves recall@k performance at the expense of query latency.

---

## 4. Production Python Implementation

The following complete module implements async concurrent retrieval for BM25 and Dense Vector search, followed by Reciprocal Rank Fusion and Cross-Encoder re-ranking.

```python
import math
import asyncio
import logging
from typing import List, Dict, Any, Tuple, Optional

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class HybridSearchEngine:
    def __init__(self, rrf_k: int = 60):
        self.rrf_k = rrf_k

    def reciprocal_rank_fusion(
        self,
        results_list: List[List[Dict[str, Any]]],
        id_field: str = "id",
        weights: Optional[List[float]] = None
    ) -> List[Dict[str, Any]]:
        """
        Combines multiple ordered search result lists using Weighted Reciprocal Rank Fusion.

        :param results_list: List of ranked doc lists from distinct searchers.
        :param id_field: Field name containing unique document identifier.
        :param weights: Optional list of weights corresponding to each search list.
        :return: Re-ranked consolidated list of documents with fusion scores.
        """
        if weights is None:
            weights = [1.0] * len(results_list)

        doc_scores: Dict[str, float] = {}
        doc_store: Dict[str, Dict[str, Any]] = {}

        for list_idx, ranked_docs in enumerate(results_list):
            w = weights[list_idx]
            for rank, doc in enumerate(ranked_docs, start=1):
                doc_id = str(doc[id_field])
                rrf_score = w * (1.0 / (self.rrf_k + rank))

                if doc_id not in doc_scores:
                    doc_scores[doc_id] = 0.0
                    doc_store[doc_id] = doc.copy()
                
                doc_scores[doc_id] += rrf_score

        # Sort documents by calculated RRF score descending
        sorted_docs = sorted(
            doc_scores.items(), key=lambda item: item[1], reverse=True
        )

        fused_results = []
        for doc_id, score in sorted_docs:
            merged_doc = doc_store[doc_id]
            merged_doc["_hybrid_score"] = score
            fused_results.append(merged_doc)

        return fused_results

    def relative_score_fusion(
        self,
        bm25_results: List[Dict[str, Any]],
        vector_results: List[Dict[str, Any]],
        alpha: float = 0.5,
        id_field: str = "id"
    ) -> List[Dict[str, Any]]:
        """
        Normalizes scores using Min-Max scaling and computes weighted linear combination.
        """
        def normalize(docs: List[Dict[str, Any]], score_key: str) -> Dict[str, float]:
            if not docs:
                return {}
            scores = [d[score_key] for d in docs]
            min_s, max_s = min(scores), max(scores)
            if math.isclose(min_s, max_s):
                return {str(d[id_field]): 1.0 for d in docs}
            return {
                str(d[id_field]): (d[score_key] - min_s) / (max_s - min_s)
                for d in docs
            }

        bm25_norm = normalize(bm25_results, "_score")
        vector_norm = normalize(vector_results, "_score")

        doc_store = {str(d[id_field]): d for d in bm25_results + vector_results}
        combined_scores: Dict[str, float] = {}

        all_ids = set(bm25_norm.keys()).union(set(vector_norm.keys()))

        for doc_id in all_ids:
            s_bm25 = bm25_norm.get(doc_id, 0.0)
            s_vec = vector_norm.get(doc_id, 0.0)
            combined_scores[doc_id] = (alpha * s_bm25) + ((1.0 - alpha) * s_vec)

        sorted_ids = sorted(combined_scores.items(), key=lambda x: x[1], reverse=True)
        
        output = []
        for doc_id, score in sorted_ids:
            item = doc_store[doc_id].copy()
            item["_hybrid_score"] = score
            output.append(item)

        return output


# Async Execution Wrapper Example
async def simulate_lexical_search(query: str) -> List[Dict[str, Any]]:
    await asyncio.sleep(0.02) # Mock latency
    return [
        {"id": "doc1", "title": "Elasticsearch Tuning", "_score": 14.5},
        {"id": "doc2", "title": "Vector Databases Guide", "_score": 8.2},
        {"id": "doc3", "title": "Python Async Basics", "_score": 4.1},
    ]

async def simulate_vector_search(vector: List[float]) -> List[Dict[str, Any]]:
    await asyncio.sleep(0.03) # Mock latency
    return [
        {"id": "doc2", "title": "Vector Databases Guide", "_score": 0.92},
        {"id": "doc4", "title": "HNSW Algorithm Explained", "_score": 0.88},
        {"id": "doc1", "title": "Elasticsearch Tuning", "_score": 0.75},
    ]

async def run_hybrid_pipeline(query: str, query_vector: List[float]):
    search_engine = HybridSearchEngine(rrf_k=60)

    # Concurrently execute lexical and vector search streams
    bm25_task = asyncio.create_task(simulate_lexical_search(query))
    vec_task = asyncio.create_task(simulate_vector_search(query_vector))

    bm25_res, vec_res = await asyncio.gather(bm25_task, vec_task)

    # Fuse using RRF
    rrf_fused = search_engine.reciprocal_rank_fusion(
        results_list=[bm25_res, vec_res], weights=[1.0, 1.2]
    )

    logger.info("Hybrid Search RRF Top Result: %s (Score: %.4f)", 
                rrf_fused[0]["title"], rrf_fused[0]["_hybrid_score"])

if __name__ == "__main__":
    asyncio.run(run_hybrid_pipeline("search tuning", [0.1] * 1536))
```

---

## 5. Production OpenSearch Native Hybrid Search Pipeline

OpenSearch 2.11+ supports native Search Pipelines with RRF or Score Normalization:

```json
PUT /_search/pipeline/hybrid_rrf_pipeline
{
  "description": "Native Hybrid Search Pipeline with RRF",
  "phase_results_processors": [
    {
      "normalization-processor": {
        "normalization": {
          "technique": "min_max"
        },
        "combination": {
          "technique": "arithmetic_mean",
          "parameters": {
            "weights": [0.4, 0.6]
          }
        }
      }
    }
  ]
}
```

---

## 6. Anti-Patterns & Operational Pitfalls

| Anti-Pattern | Failure Mode | Corrective Action |
| :--- | :--- | :--- |
| **Direct Sum of Unnormalized Raw Scores** | BM25 scores (0–100+) dwarf vector cosine scores (0–1.0), rendering vector search useless. | Use RRF or Min-Max Relative Score Normalization before summing. |
| **Post-Filtering Vector Results** | Filters applied after vector kNN search truncate top-k candidates drastically. | Use **Single-Stage Pre-Filtering** in HNSW vector query. |
| **Static Weight Allocation for All Queries** | Fixed $\alpha$ weights perform poorly when queries vary between exact specs and conceptual prompts. | Use query classification to dynamically adjust $\alpha$ weights. |
| **High `ef_search` Under Heavy Load** | CPU usage spikes linearly with higher `ef_search` values, degrading P99 latency. | Benchmark recall@k vs. latency to find minimum acceptable `ef_search`. |

Attribution

hamzabellouchhamzabellouch
View sourceSee grades on GitHubMore from hamzabellouch →
SSkills Directory ProSkills Directory

Get any skill into Claude in one click.

Download any skill as a ZIP for Claude.ai, Claude Desktop, or .claude/skills. $9/mo.

See Pro

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills Directory ProSkills Directory

Get any skill into Claude in one click.

Download any skill as a ZIP for Claude.ai, Claude Desktop, or .claude/skills. $9/mo.

See Pro

Related Skills

Caveman

Terse caveman voice: answer first, fluff gone, every technical fact kept. Use for /caveman, "caveman mode", "talk like caveman", "be brief", "less tokens". Stays on until "stop caveman" or "normal mode".

1100021 votes

Hyperplan

Adversarial multi-agent planning skill. Self-orchestrates 5 hostile category members (unspecified-low, unspecified-high, deep, ultrabrain, artistry) via team-mode for ruthless cross-critique debate, distills only the defensible insights, then MANDATORILY hands the distilled insight bundle to the `plan` agent for executable plan formalization. Use when planning needs maximum rigor and surfacing of weak assumptions, blind spots, and over-engineering. Triggers: 'hyperplan', 'hpp', '/hyperplan', ...

698431 votes

Writing Skills

Create and manage Claude Code skills in HASH repository following Anthropic best practices. Use when creating new skills, modifying skill-rules.json, understanding trigger patterns, working with hooks, debugging skill activation, or implementing progressive disclosure. Covers skill structure, YAML frontmatter, trigger types (keywords, intent patterns), UserPromptSubmit hook, and the 500-line rule. Includes validation and debugging with SKILL_DEBUG. Examples include rust-error-stack, cargo-dep...

3931 votes

Mcp Code Execution

Routes multi-tool workflows through MCP servers for large datasets and pipelines. Use when Bash tool overhead is limiting throughput on data-heavy tasks.

3421 votes

catchup

Recovers the conversation and failed tool calls of a previous Codex, Amp, Claude Code, Antigravity, Cline, Copilot CLI, Cursor, DeepSeek Harness, Grok Build, Kimi, OpenCode, Pi Agent, or ZCode session. Use when the user says "catch up", "what did the last session do", "get me up to speed", "I switched agents", asks to recover/summarize a previous session before continuing, or asks to diagnose or report a catchup failure. Do NOT use for the current conversation, git history, or any non-agent log.

741 votes
View all in ai-agents →