# How Semantic Search with Vector Embeddings Works in code-review-graph

> Discover how code-review-graph uses semantic search and vector embeddings to enhance code discovery. Learn about BM25, RRF, and accurate search results.

- Repository: [Tirth Kanani/code-review-graph](https://github.com/tirth8205/code-review-graph)
- Tags: deep-dive
- Published: 2026-08-06

---

**The code-review-graph project implements semantic search by combining BM25-based full-text search with dense vector similarity search, merging both result sets using Reciprocal Rank Fusion (RRF) to deliver accurate code discovery even when query keywords don't literally match the source text.**

The `code-review-graph` repository provides a hybrid search architecture designed for intelligent code understanding. By integrating **vector embeddings** with traditional keyword matching through SQLite's FTS5 extension, the system surfaces conceptually related functions and classes even when identifiers and docstrings don't share overlapping terms. This approach ensures users receive relevant results whether they search with exact technical terminology or natural language descriptions.

## Hybrid Search Architecture

The semantic search system operates as a **multi-layered pipeline** that gracefully degrades when components are unavailable. Rather than relying solely on vector similarity—which requires pre-computed embeddings—the architecture maintains a fast keyword search path while seamlessly blending semantic signals when present.

According to the source code in [`code_review_graph/search.py`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/search.py) (lines 9-35), three primary components orchestrate the workflow:

- **`semantic_search_nodes`** – The public API entry point defined in [`code_review_graph/tools/query.py`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/tools/query.py) (lines 699-707) that validates inputs, selects search modes, and formats final responses with contextual hints.
- **`hybrid_search`** – The core engine that executes parallel FTS5 and embedding queries, applies RRF merging, and tracks which search path contributed to each result.
- **`_embedding_search`** – The optional vector retrieval layer that loads an `EmbeddingStore` and maps nearest-neighbor vectors back to node IDs.

## How Embedding Generation Works

Before semantic search can operate, the system must generate dense vector representations for every node in the code graph. This process is handled by the `embed_graph` tool in [`code_review_graph/tools/docs.py`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/tools/docs.py) (lines 20-35).

When `embed_graph` executes, it instantiates an `EmbeddingStore` configured with either a local **sentence-transformers** model or a remote API provider. Each node's textual representation—including its qualified name, signature, and docstring—is passed through the provider to generate a fixed-dimensional vector, which is then persisted in a SQLite-backed embedding table. This indexing step runs once per repository or whenever the codebase changes significantly.

## Hybrid Query Execution and RRF Merging

When `semantic_search_nodes` receives a query, it delegates to `hybrid_search`, which executes two distinct search paths simultaneously:

1. **Full-Text Search (FTS5)** via `_fts_search`, generating `(node_id, bm25_score)` tuples from the virtual `nodes_fts` table.
2. **Vector Similarity Search** via `_embedding_search` (lines 13-44 in [`code_review_graph/search.py`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/search.py)), retrieving nearest-neighbor vectors and producing `(node_id, similarity_score)` tuples.

These result sets are merged using **Reciprocal Rank Fusion (RRF)**, implemented in the `rrf_merge` function. RRF assigns each result a reciprocal rank score calculated as `1 / (k + rank)`, where the constant `k` defaults to 60. Nodes appearing in both result sets receive higher combined scores, effectively boosting items that satisfy both keyword and semantic relevance.

After merging, the system applies additional heuristics: results are boosted based on node kind (e.g., functions vs. classes), qualified-name pattern matching, and proximity to `context_files` specified in the query.

## Fallback Mechanisms and Result Packaging

The architecture includes robust degradation paths. If `_embedding_search` returns an empty list—either because the `EmbeddingStore` is uninitialized or provider dependencies are missing—`hybrid_search` falls back to **FTS5-only** mode. If FTS5 also fails, it degrades further to a simple `LIKE` keyword search against node names.

The `_out_mode` flag populated by `hybrid_search` indicates which path succeeded: `"semantic"` for embeddings-only, `"fts"` for keyword-only, or `"hybrid"` when both contributed. The `semantic_search_nodes` tool uses this to annotate responses, attaching autogenerated hints that explain why specific nodes matched the query.

## Implementation Examples

Populate the embedding index before running semantic queries. This creates the vector representations stored in `code_review_graph/embeddings.EmbeddingStore`:

```python
from code_review_graph.tools.docs import embed_graph

embed_status = embed_graph(repo_root="/path/to/repo")
print(embed_status["summary"])

# → "Embedded 1245 new node(s). Total embeddings: 4523. Semantic search is now active."

```

Execute a semantic search with context-aware boosting. The `context_files` parameter prioritizes results from specific modules:

```python
from code_review_graph.tools.query import semantic_search_nodes

result = semantic_search_nodes(
    query="authenticate user",
    kind="Function",
    limit=10,
    context_files=["src/auth.py"],
    model="all-MiniLM-L6-v2",
    provider="local",
)
print(result["search_mode"])  # → "hybrid" or "semantic"

print(result["results"][:3])   # Top 3 matches with hybrid scores

```

For manual control over the search pipeline, invoke `hybrid_search` directly with a `GraphStore` instance:

```python
from code_review_graph.search import hybrid_search
from code_review_graph.graph import GraphStore

store = GraphStore("/path/to/repo/.crg.db")
results = hybrid_search(
    store,
    query="payment processor",
    kind="Class",
    limit=5,
    model="all-MiniLM-L6-v2",
    provider="local",
    _out_mode=None,
)

for r in results:
    print(f"{r['qualified_name']}: {r['score']}")
store.close()

```

## Summary

- **Hybrid approach**: The system combines SQLite FTS5 (BM25) with dense vector similarity to capture both literal and conceptual matches.
- **RRF merging**: Reciprocal Rank Fusion blends result sets from both search paths, using a default constant of 60 to calculate combined relevance scores.
- **Graceful degradation**: Search automatically falls back from hybrid → FTS5-only → keyword `LIKE` when embeddings or full-text indexes are unavailable.
- **Key files**: Implementation resides in [`code_review_graph/search.py`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/search.py) (hybrid engine), [`code_review_graph/tools/query.py`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/tools/query.py) (public API), and [`code_review_graph/tools/docs.py`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/tools/docs.py) (embedding generation).

## Frequently Asked Questions

### What embedding providers does code-review-graph support?

The system supports local **sentence-transformers** models via the `EmbeddingStore` class and can be extended to remote API providers. Configure the provider using the `provider="local"` parameter and specify model names like `all-MiniLM-L6-v2` when calling `embed_graph` or `semantic_search_nodes`.

### Why does the system use Reciprocal Rank Fusion instead of averaging scores?

RRF effectively normalizes scores from heterogeneous ranking systems—BM25 and cosine similarity—whose scales are incompatible. By converting ranks to reciprocal values with a constant `k=60`, the method gives high weight to top-ranked items from either system without requiring score calibration or training data.

### How do I handle repositories too large for in-memory embedding stores?

The `EmbeddingStore` abstraction in [`code_review_graph/embeddings.py`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/embeddings.py) uses SQLite as the backing database, persisting vectors to disk rather than memory. For very large codebases, you can incrementally index subsets using `embed_graph` and rely on the nearest-neighbor search to retrieve relevant vectors without loading the entire embedding space into RAM.

### What happens if my query matches no embeddings?

If `_embedding_search` returns no results, `hybrid_search` automatically falls back to the FTS5 path. Should FTS5 also return empty results, the system performs a final fallback to a simple SQL `LIKE` query on node qualified names, ensuring users always receive some form of relevant output even in sparse indexing scenarios.