Vector & hybrid search
TopGun ships with tri-hybrid search built in: exact key lookup, BM25 full-text (via tantivy), and semantic vector search (HNSW ANN with embeddings), all combined with Reciprocal Rank Fusion (RRF) into a single ranked result. Subscriptions update in real time as records change — no polling, no separate search index to keep in sync.
For BM25 predicates and useQuery live subscriptions without embeddings, see Search & live queries. For exposing search to AI agents over MCP, see MCP Server.
Search methods
The HybridSearchMethod type controls which search strategies run:
type HybridSearchMethod = 'exact' | 'fullText' | 'semantic';
| Method | When to use |
|---|---|
'exact' | Key-based lookup; zero latency, no index required |
'fullText' | BM25 ranked full-text search on string fields; works offline against local data |
'semantic' | HNSW approximate nearest-neighbour in embedding space; requires server-side embedding config |
Pass one or more methods to run them together; RRF fuses the ranked lists into a single score per result.
React hooks
useHybridSearch
One-shot search that re-runs whenever query or options change:
import { useHybridSearch } from '@topgunbuild/react';
function DocSearch({ query }: { query: string }) {
const { results, loading } = useHybridSearch('docs', query, {
methods: ['fullText', 'semantic'],
k: 10,
minScore: 0.3,
});
if (loading) return <p>Searching…</p>;
return (
<ul>
{results.map((r) => (
<li key={r.key}>
{r.key} — score {r.score.toFixed(3)}
<small>
{' '}(BM25: {r.methodScores?.fullText?.toFixed(3)},
semantic: {r.methodScores?.semantic?.toFixed(3)})
</small>
</li>
))}
</ul>
);
}
Each result is a HybridSearchClientResult:
| Field | Type | Description |
|---|---|---|
key | string | Record key in the map |
score | number | RRF-fused score across all methods |
methodScores | Record<string, number> | Per-method score breakdown |
value | T | undefined | Record value (present when includeValue: true) |
useVectorSearch
Pure vector (HNSW ANN) search, bypassing BM25 and exact strategies:
import { useVectorSearch } from '@topgunbuild/react';
function SimilarProducts({ embedding }: { embedding: number[] }) {
const { results, loading } = useVectorSearch('products', embedding, {
k: 5,
includeValue: true,
});
if (loading) return null;
return <ul>{results.map((r) => <li key={r.key}>{r.key}</li>)}</ul>;
}
useHybridSearchSubscribe
Live subscription — results re-rank automatically as records are written:
import { useHybridSearchSubscribe } from '@topgunbuild/react';
function LiveResults({ query }: { query: string }) {
const { results } = useHybridSearchSubscribe('docs', query, {
methods: ['semantic'],
k: 5,
});
return (
<ul>
{results.map((r) => (
<li key={r.key}>{r.key} ({r.score.toFixed(3)})</li>
))}
</ul>
);
}
For the full hook option signatures see the React reference.
Client methods
Use the client methods directly in non-React code (Node.js scripts, background jobs, MCP tools):
// Full tri-hybrid search
const results = await client.hybridSearch('products', 'wireless headphones', {
methods: ['exact', 'fullText', 'semantic'],
k: 20,
predicate: (item) => item.inStock === true,
});
// results: HybridSearchClientResult[] — key, score, methodScores
// Pure vector search
const similar = await client.vectorSearch('products', queryEmbedding, { k: 10 });
client.hybridSearch accepts HybridSearchClientOptions:
| Option | Type | Default | Description |
|---|---|---|---|
methods | HybridSearchMethod[] | ['fullText'] | Which strategies to run |
k | number | 10 | Max results to return |
queryVector | number[] | — | Pre-computed embedding (skips auto-embed) |
predicate | (value: T) => boolean | — | Pre-filter applied before ranking |
includeValue | boolean | false | Include record value in results |
minScore | number | — | Discard results below this RRF score |
For the full client API see the Client reference.
RRF score fusion
When two or more methods run, TopGun uses Reciprocal Rank Fusion to merge the ranked lists:
RRF score = Σ 1 / (k_rrf + rank_i)
where k_rrf = 60 by default. This produces a single score per result plus a methodScores map so you can inspect each method’s contribution. The fused score is always in (0, 1].
Semantic search and embedding providers
Semantic search works by embedding query text and record fields into a shared vector space, then finding the nearest neighbours with HNSW ANN.
Embeddings are generated server-side. TopGun supports:
- Local Ollama (e.g.
nomic-embed-text) — zero egress cost, runs on-device - Any OpenAI-compatible HTTP endpoint — OpenAI, Together AI, Cohere, or a self-hosted model serving the
/v1/embeddingsAPI
Records are auto-vectorized on write — no separate indexing pipeline is needed.
Server configuration
The embedding provider and which maps are auto-vectorized are configured entirely through server environment variables — there is no TypeScript client import. Set the provider, its dimension, and the maps/fields to embed, then start the server:
# --- Local Ollama (zero egress) ---
TOPGUN_EMBEDDING_PROVIDER=ollama
TOPGUN_EMBEDDING_MODEL=nomic-embed-text
TOPGUN_EMBEDDING_DIMENSION=768
# --- OR any OpenAI-compatible endpoint ---
# TOPGUN_EMBEDDING_PROVIDER=openai
# TOPGUN_EMBEDDING_BASE_URL=https://api.openai.com
# TOPGUN_EMBEDDING_API_KEY=sk-...
# TOPGUN_EMBEDDING_MODEL=text-embedding-3-small
# TOPGUN_EMBEDDING_DIMENSION=1536
# Which maps/fields to auto-embed on write (dimension must match the provider)
TOPGUN_VECTOR_MAPS='{"products":{"fields":["title","description"],"dimension":768}}'
# Optional: directory where the HNSW index is persisted across restarts
TOPGUN_VECTOR_INDEX_PATH=./data/vectors
On startup the server logs a single line showing the active provider, dimension, and the maps it will auto-embed, so you can confirm the configuration from the logs. For the full variable reference see the Server reference.
Semantic search requires an embedding provider
The 'exact' and 'fullText' methods work fully offline against local data. The 'semantic' method requires a running server with TOPGUN_EMBEDDING_PROVIDER configured. Without a provider, 'semantic' returns an explicit error rather than empty or meaningless results — it never silently fakes a ranking. Offline clients that include 'semantic' gracefully fall back to the remaining methods.
HNSW index
TopGun builds an HNSW (Hierarchical Navigable Small World) approximate nearest-neighbour index over each configured map. HNSW provides sub-linear query time at high recall — typical k=10 queries over millions of vectors complete in single-digit milliseconds.
The index is persisted to TOPGUN_VECTOR_INDEX_PATH and loaded into memory at server start. Writes auto-insert into the live index so there is no explicit rebuild step.
Real-time deltas
useHybridSearchSubscribe maintains a live WebSocket subscription. When any record in the map changes, the server re-ranks that record’s neighbourhood and pushes deltas to subscribers. This means your search UI reflects fresh data without re-issuing queries.
The subscription uses the same CRDT merge semantics as the rest of TopGun: offline writes are queued and the subscription reconciles when connectivity resumes.
AI agents via MCP
The bundled topgun_search MCP tool routes through the same tri-hybrid RRF pipeline. AI agents using Claude Desktop, Cursor, or any MCP-compatible client call it identically to client.hybridSearch. See the MCP Server guide and the MCP reference.
Related
- Search & live queries — BM25 only, predicates,
useQuery - MCP Server guide — AI agents on live CRDT data
- React reference — full hook signatures
- Client reference —
client.hybridSearch/client.vectorSearchfull options - Server reference —
VectorConfig,TOPGUN_VECTOR_INDEX_PATH, embedding provider config - MCP reference —
topgun_searchand all 8 MCP tools