# 07 — Hybrid Search

The full hybrid pipeline: BM25 keyword scoring + vector similarity, combined via RRF fusion. Uses a deterministic mock `EmbeddingProvider` so the example runs without any external model or GPU.

## What's new

- **EmbeddingProvider** — the interface for embedding text into vectors
- **MockEmbeddingProvider** — a vocabulary-based mock that produces meaningful similarity
- **Hybrid fusion** — keyword and vector signals combined via RRF
- **QuerySignal in action** — graph proximity signal fires because vector candidates exist

## Key concepts

### EmbeddingProvider

The `EmbeddingProvider` interface has four methods:

```ts
interface EmbeddingProvider {
  embed(texts: Iterable<string>): Promise<Float32Array[]>; // index-time
  embedQuery(texts: Iterable<string>): Promise<Float32Array[]>; // search-time
  readonly dimensions: number;
  initialize(): Promise<void>;
  dispose(): Promise<void>;
}
```

`embed` and `embedQuery` may differ — some models apply a task-specific prefix (e.g., `"passage: "` vs `"query: "`) to improve retrieval quality.

Pass `null` for keyword-only mode (examples 01-06). Pass a real provider for hybrid search.

### Mock embedding strategy

The mock creates a fixed vocabulary of domain words. Each text becomes a vector where dimension `i` is `1.0` if `vocab[i]` appears in the text, else `0.0`. Vectors are L2-normalized for cosine similarity.

This is just for illustration — real providers use trained transformer models (ONNX, Bedrock Titan, etc.). The wiring is the same regardless of the model.

### Hybrid ranking

With both keyword and vector signals, RRF fusion rewards documents that rank well in _both_:

```
=== "contract rate" ===
  CONTRACT   [keyword:#1, vector:#1]  ← ranked first in both signals
  STATEMENT  [keyword:#2, vector:#6]  ← keyword is strong, vector weak
  ACCOUNT    [vector:#2]              ← vector only (no keyword match)
```

This is the core value of hybrid search: keyword catches exact term matches, vector catches semantic similarity, and RRF combines them without needing to tune weights.

### QuerySignal with vector candidates

In [Example 06](../06-custom-pipeline/), the `QuerySignal` didn't fire because keyword-only mode produces no vector candidates. Here, the vector stage populates candidate IDs, so the proximity signal can boost graph neighbors of top results.

With a small corpus, most neighbors already appear in the vector results. The proximity signal has more impact on larger corpora where vector search returns a sparse subset.

## Running

```bash
npx tsx examples/07-hybrid-search/main.ts
```

## Expected output

```
Indexed 6 tables (hybrid: keyword + vector)

=== "contract rate" (keyword + vector) ===
  0.0769  CONTRACT  [keyword:#1, vector:#1]
  0.0693  STATEMENT  [keyword:#2, vector:#6]
  0.0370  ACCOUNT  [vector:#2]
  0.0357  PAYMENT  [vector:#3]
  0.0345  VENDOR  [vector:#4]

=== "royalty agreement" (semantic via vector) ===
  0.0769  CONTRACT  [keyword:#1, vector:#1]
  0.0741  STATEMENT  [keyword:#2, vector:#2]
  0.0680  TERRITORY  [keyword:#3, vector:#6]
  0.0357  PAYMENT  [vector:#3]
  0.0345  VENDOR  [vector:#4]

=== "distribution partner" (vector + proximity) ===
  0.0769  VENDOR  [keyword:#1, vector:#1]
  0.0370  ACCOUNT  [vector:#2]
  0.0357  PAYMENT  [vector:#3]
  0.0345  STATEMENT  [vector:#4]
  0.0333  TERRITORY  [vector:#5]
```
