Chapter 4 · semantic embeddings (shipped v0.10)

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.

Explain like I’m 5 Sometimes you don’t know the exact name of what you’re looking for. Semantic search understands what you meant and points to the right starting spot, then the normal map-walking takes over from there.

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.

Analogy

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

A meaning-based query feeding PageRank

Click a natural-language query. Gold rings = semantic (KNN) seeds chosen by meaning; the brightness that spreads is the PageRank walk from them.

illustrative

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.

crates/travsr-plugin-host/src/embed_sidecar.rs · embed_supervisor.rs

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:

ModelDimensionsParamsBest for
arctic-embed-m-v1.5768109Mrecommended, best retrieval accuracy
arctic-embed-m-v1.5-256256109Msame model, ~2.7x smaller vectors (Matryoshka)
bge-small-en-v1.538433Mfastest, any machine
bge-base-en-v1.5768109Mstronger on technical vocabulary
bge-large-en-v1.51024335Mmaximum BGE accuracy
crates/travsr-plugin-host/src/embed_catalog.toml

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.

migrations v16–v18 · embed.db split

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.

crates/travsr-analysis/src/skeleton.rs

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.

crates/travsr-mcp/src/seed.rs (rrf_fuse, rrf_k=60) · travsr-retrieval/src/ppr.rs

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.

crates/travsr-mcp/src/seed.rs (semantic_validate · knn_oracle)

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.

KNN_BUDGET_MS = 600 · crates/travsr-mcp/src/tools.rs
Next: how the whole thing is wired →