Semantic Search: meaning, not just words
BM25 (Chapter 3) is great when you know the right keyword. But what if you ask
“where do we retry failed payments?” and the function is actually called
backoffEnqueue(), no shared words? Semantic search handles that,
by matching meaning. The clever part: travsr adds it without breaking its
“graph-first, no vectors in the core” rule.
The tension and the resolution
Principle #1 says “algorithms first, LLM last” and travsr deliberately avoids “vector RAG”. So how can it use embeddings (which are vectors)? The answer is a careful boundary:
What it does NOT do
It does not retrieve answers by vector similarity. Embeddings never decide which code is related, never create edges, and never live in the main program.
What it DOES do
Embeddings only pick good starting points (seeds) for the PageRank walk. The graph still does the actual retrieval. Meaning helps you find the trailhead; the trail itself is real edges.
Semantic search is a local guide who understands what you meant and walks you to the right neighborhood entrance. From there you still travel the real roads (the graph). The guide never invents roads.
See it: meaning → seeds → graph walk
Click a natural-language query. Gold rings = semantic (KNN) seeds chosen by meaning; the brightness that spreads is the PageRank walk from them.
The seed selection above is hand-mapped for the demo (we can’t run a 100 MB model in a browser). The mechanism, meaning picks seeds, weighted PageRank does the rest, is exactly what the code does.
How it’s actually built (from the code today)
The model runs in a separate “sidecar” program
All the heavy embedding-generation libraries live in a
separate downloadable binary (travsr-embed), not in the main
travsr program. They talk over a simple stdin/stdout message channel. So the main binary
stays tiny and has zero embedding-generation dependencies, there’s a CI
test that fails the build if one sneaks in. The one exception is the cross-encoder
reranker (travsr-rerank, RFC-021), which runs its ONNX model
in-process, that dependency is explicitly allow-listed because reranking is judgment over
candidates the graph already found, not embedding generation.
Five models to choose from
You pick a model with travsr embed init. The bundled catalog ships two
Snowflake arctic-embed models and three BGE models, all
open-source. You can add your own at runtime via ~/.travsr/embed_catalog.toml, no
recompile needed:
| Model | Dimensions | Params | Best for |
|---|---|---|---|
arctic-embed-m-v1.5 ⭐ | 768 | 109M | recommended, best retrieval accuracy |
arctic-embed-m-v1.5-256 | 256 | 109M | same model, ~2.7x smaller vectors (Matryoshka) |
bge-small-en-v1.5 | 384 | 33M | fastest, any machine |
bge-base-en-v1.5 | 768 | 109M | stronger on technical vocabulary |
bge-large-en-v1.5 | 1024 | 335M | maximum BGE accuracy |
Embeddings live in a separate database
To avoid slowing down the main graph, embeddings are stored in their own
embed.db file (sibling of graph.db), searched with a fast
approximate-nearest-neighbor index (HNSW). The main graph write path never waits on it.
Better embedding text via AST “skeletons”
Instead of embedding just a bare name like run, travsr builds a compact
skeleton of each function, its signature, parameters, return type,
containing fields/members, and the functions it calls, across three richness tiers, and
embeds that. Ambiguous names like new or init get real
meaning.
Three seed sources, fused by RRF, then weighted PageRank
Three candidate lists run in parallel, exact/anchor matches,
lexical full-text (FTS), and semantic KNN, and are merged with
Reciprocal-Rank Fusion (constant k = 60) rather than a raw score max.
A node that ranks well in several lists rises to the top. Then ppr_weighted walks the
graph, giving higher-confidence seeds more pull. On the project’s own benchmark this lifted accuracy
from 9/10 → 10/10.
A cosine oracle checks the answer’s quality
After fusion, semantic_validate consults a cosine oracle:
it measures how close the fused candidates actually are, in meaning, to the query. If coverage and
confidence are too low, travsr abstains, returning nothing rather than plausible-looking
noise. Semantic floors are auto-calibrated per model, so each model is judged on its own scale.
Safety valve: a 600 ms circuit-breaker
If the sidecar is slow to answer (> 600 ms), travsr throws away the semantic
results and falls back to keyword seeds. Search never hangs waiting on the model. The budget is
tunable via TRAVSR_KNN_BUDGET_MS.