# Understanding the FTS5 + Entity-Match + Link-Neighbour RRF Retrieval Pipeline in ai-memory

> Explore the ai-memory retrieval pipeline combining FTS5, entity-match, and link-neighbor RRF for unified search ranking. Learn how these techniques create a deterministic result.

- Repository: [Fabio Akita/ai-memory](https://github.com/akitaonrails/ai-memory)
- Tags: deep-dive
- Published: 2026-08-27

---

**The ai-memory retrieval pipeline combines FTS5 full-text search, entity-based lexical matching, and link-neighbour graph traversal using Reciprocal Rank Fusion (RRF) to produce a single deterministic ranking of candidate pages.**

The **akitaonrails/ai-memory** repository implements a sophisticated hybrid search system that fuses multiple independent retrieval streams into one coherent result set. This **FTS5 + entity-match + link-neighbour RRF retrieval pipeline** processes queries through distinct lexical and structural channels before applying a mathematically rigorous fusion algorithm to determine final relevance.

## The Four-Stream Retrieval Architecture

The pipeline operates on three always-active streams plus one conditional stream when vector embeddings are configured. Each stream generates an independent 1-based rank for matching documents.

### FTS5 Full-Text Search

The **FTS5** stream leverages SQLite's native FTS5 extension to perform BM25-styled ranking over page content. In [`crates/ai-memory-store/src/reader.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/reader.rs), this stream executes full-text queries against the built-in FTS5 index and returns raw BM25 scores that are subsequently transformed into rank positions for fusion.

### Entity-Match Lexical Indexing

The **entity-match** stream queries a specialized lexical index built from the `entities` front-matter field of each markdown page. This index stores normalized noun tokens extracted during ingestion. When a query matches entity tokens, the stream produces a distinct rank based on inverse-frequency scoring, separate from the BM25 scores generated by FTS5.

### Link-Neighbour Graph Traversal

The **link-neighbour** stream operates on the wiki-link graph structure implicit in markdown connections. A page becomes a candidate if it neighbours (links to or is linked by) a page that directly matches the query. The graph distance determines the neighbour's rank contribution, capturing structural relationships that purely lexical searches miss.

### Optional Vector Similarity (Fourth Stream)

When `AI_MEMORY_EMBEDDING_PROVIDER` is configured, a fourth stream activates to compute cosine similarity between query embeddings and pre-computed page vectors. According to the architecture documentation, this cosine similarity rank becomes an additional RRF contribution alongside the three core streams.

## Reciprocal Rank Fusion Implementation

The core fusion logic resides in [`crates/ai-memory-store/src/reader.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/reader.rs) at lines 3649-3663. For each document *d*, the system calculates:

```

score(d) = Σ 1 / (k + rank_i(d))

```

Where **k = 60** (the RRF constant) and the summation runs across all active streams. A lower rank (e.g., 1st place) yields a higher reciprocal contribution. Documents appearing in multiple streams accumulate higher fused scores, surfacing pages that demonstrate relevance across lexical, entity, and structural dimensions.

## Authority Multiplier and Post-Processing

After RRF fusion, the pipeline applies a **bounded authority multiplier** defined in [`crates/ai-memory-store/src/reader.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/reader.rs) lines 3753-3839. This multiplier adjusts scores based on:

- **Page kind** (explanatory vs. reference vs. journal)
- **Tier** (hierarchical importance)
- **Pinned status** (manually prioritized content)
- **Front-matter tags** (categorical metadata)

The MCP server endpoint in [`crates/ai-memory-mcp/src/server.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-mcp/src/server.rs) (lines 1810-1815) exposes this pipeline via the `memory_query` HTTP endpoint. Additionally, the system supports optional LLM-based reranking through [`crates/ai-memory-llm/src/reranker.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-llm/src/reranker.rs), which can reorder results after the initial RRF computation when the `reranker` option is enabled.

## Practical Usage Examples

### Basic Query Using Default Streams

Query the default three-stream pipeline (FTS5 + entity + graph) without vector search:

```rust
use ai_memory_mcp::client::MemoryClient;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let client = MemoryClient::new("http://127.0.0.1:49374")?;
    let resp = client
        .memory_query("how does the recall-eval test work?")
        .await?;
    println!("{:#?}", resp.hits);
    Ok(())
}

```

### Enabling Vector-Based RRF

Activate the fourth stream by configuring an embedding provider before server startup:

```rust
// Set environment variables before initializing the server
std::env::set_var("AI_MEMORY_EMBEDDING_PROVIDER", "openai");
std::env::set_var("OPENAI_API_KEY", "sk-…");

// Queries now automatically include cosine similarity in the RRF calculation

```

### Requesting Per-Hit Explanations

Inspect individual stream contributions and graph provenance using the explain flag:

```rust
let resp = client
    .memory_query("explain: what is the RRF pipeline")
    .explain(true)
    .await?;
println!("{:#?}", resp.explain);

```

The explanation includes per-stream ranks, matched entities, raw BM25 scores, RRF contributions, and link-neighbour graph traversal paths as documented in [`ARCHITECTURE.md`](https://github.com/akitaonrails/ai-memory/blob/main/ARCHITECTURE.md).

## Summary

- **FTS5 + entity-match + link-neighbour RRF retrieval pipeline** combines three orthogonal retrieval signals into a unified ranking using Reciprocal Rank Fusion with k=60.
- Each stream (FTS5 BM25, entity lexical matching, graph neighbours, optional vector cosine) generates independent 1-based ranks that are fused via the formula Σ 1/(60 + rank).
- Post-fusion authority weighting in [`reader.rs`](https://github.com/akitaonrails/ai-memory/blob/main/reader.rs) adjusts results based on page metadata, tier, and pinned status.
- The architecture is fully deterministic and explainable, with optional LLM reranking available for final refinement.
- Client implementations interact through the MCP server endpoint defined in [`server.rs`](https://github.com/akitaonrails/ai-memory/blob/main/server.rs), with configuration managed via environment variables for embedding providers.

## Frequently Asked Questions

### How does the RRF formula handle documents that appear in multiple streams?

Documents appearing in multiple streams receive cumulative contributions from each stream's reciprocal rank. A document ranked 1st in FTS5 and 3rd in entity-match contributes `1/61 + 1/63` to its final score, while a document ranked 1st in only one stream contributes only `1/61`. This multiplicative effect naturally boosts pages that demonstrate relevance across lexical, entity, and structural dimensions simultaneously.

### What is the significance of the k=60 constant in the RRF implementation?

The constant **k = 60** serves as a damping factor that prevents top-ranked documents from dominating the fused score entirely. By adding 60 to each rank in the denominator, the algorithm ensures that lower-ranked documents (e.g., rank 50 vs. rank 1) still contribute meaningful scores to the final ranking. This value is hardcoded in [`reader.rs`](https://github.com/akitaonrails/ai-memory/blob/main/reader.rs) lines 3649-3663 and follows established information retrieval best practices for RRF stability.

### Can I disable specific streams like link-neighbour graph search?

The three core streams (FTS5, entity-match, link-neighbour) are always active in the current implementation as defined in the architecture. However, the optional vector stream only activates when `AI_MEMORY_EMBEDDING_PROVIDER` is explicitly configured. To effectively disable graph influence, you would need to filter results post-query, though the pipeline itself does not expose per-stream toggles in the public API exposed by [`server.rs`](https://github.com/akitaonrails/ai-memory/blob/main/server.rs).

### Where is the LLM reranking logic implemented relative to the RRF fusion?

The optional LLM reranker operates as a **post-processing step** after RRF fusion and authority weighting complete. Located in [`crates/ai-memory-llm/src/reranker.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-llm/src/reranker.rs), this component receives the already-fused and authority-adjusted result list, then uses language model calls to potentially reorder the top-k candidates. It does not replace the RRF calculation but rather refines its output when explicitly requested via the `reranker` option in the query configuration.