Keyword search finds the page with the right words. Semantic search finds the page with the right meaning — the one you’d recognize but can’t quite grep for. This post adds that to the vault with a local embeddings index — the retrieval half of retrieval-augmented generation, the technique introduced by Lewis et al. (2020) — built as a proper derived consumer that always knows which revision it represents.
Semantic search is another projection — a derived, revision-labeled consumer, never a second authority. Build it locally, cache by content hash so rebuilds are cheap, stamp it with the vault commit, and confirm its hits against the canonical page. Stale is allowed; stale-and-hidden is not.
Replace the stand-ins: 10.0.0.65/.66/.67 → your own Ollama node IPs (running a single machine? point every node reference at that one host); 11434 is Ollama’s default port; the commit 64dc195 is illustrative.
The index pipeline
Split each page on its headings rather than fixed byte windows, so a chunk is a coherent unit of meaning (a whole “## Recovery” section, say). Section chunks retrieve far better than arbitrary slices.
Ollama serves the embedding model on your own nodes; nomic-embed-text is a strong, compact choice — its technical report documents a model that matches or beats comparable closed embedders on standard benchmarks. Spread the work across multiple nodes for throughput.
ollama pull nomic-embed-text
# embed one chunk (the indexer loops this over all chunks, across nodes)
curl -s http://10.0.0.65:11434/api/embeddings \
-d '{"model":"nomic-embed-text","prompt":"the chunk text here"}'
Hash each chunk; only re-embed chunks whose hash changed since last build (most rebuilds touch a handful). Build the new index beside the old one and swap it in atomically, recording the vault commit so consumers can detect staleness.
node tools/embeddings/build-index.mjs . # re-embeds only changed chunks
node tools/embeddings/verify-index.mjs . # index commit == vault HEAD ?
The index is not authority. An agent’s begin step consults it read-only to find candidate pages, then confirms each hit against the canonical topic page. If the index commit lags HEAD, its hits are labeled stale — useful for discovery, never quoted as current truth.
The full chunker, multi-node embed loop, hash cache, and query tool are in the paired playbook: Build and Query the Embeddings Index.
Querying — and why to go region-first
node tools/embeddings/query.mjs "recover a locked keyring after reboot"
# → ranked chunks, each with its source page + the index commit
Because the query returns the commit alongside the hits, the caller always knows whether it’s searching current knowledge or a stale snapshot.
For AI agents using the index during a begin step, a two-stage approach produces much sharper results than a single broad query:
# Stage 1: unfiltered — just enough hits to identify the relevant brain region
node tools/canonical-vault/embeddings/query.mjs search "recover a locked keyring" 5
# Check the 'region' field on the top hits — e.g. region/laptop
# Stage 2: scoped — re-run with the winning region tag for high-quality hits
node tools/canonical-vault/embeddings/query.mjs search "recover a locked keyring" 10 region/laptop
# Optional: expand from the best hit to its topic MOC and brain-region siblings
node tools/canonical-vault/embeddings/query.mjs graph "journal/2026-06-28-elitebook-keyring-grub-fix.md"
The reason: a large vault has many regions (proxmox, nas, second-brain, blog, …) and an unscoped query surfaces noise from all of them. Stage 1 identifies the region cheaply; Stage 2 uses it as a filter. The graph step then pulls in the topic MOC and sibling pages that an unfiltered ranking would bury. This is the pattern the begin skill uses internally — it’s worth adopting in any agent that queries the index.
The swap-in box
Vector stores: any of Chroma, Qdrant, or sqlite-vec works as the backing store — the discipline (content-hash cache, atomic swap, commit stamp) matters more than the engine. Cloud embeddings: if privacy allows, a hosted embedding API drops in where Ollama is — keep everything else. Off-the-shelf RAG: whatever framework you use, make retrieval return the source revision and confirm hits against the source. The universal rule: the index is a derived, revision-labeled consumer, not a second brain.
What you have now
Concept-level search over your entire vault, running on your own hardware, that never pretends a stale snapshot is current. That completes the derived-consumer set — wiki, reader, embeddings. The next post steps back to the operations discipline that keeps all of them honest at once: keeping projections honest.
Since writing this, the index grew the generation half of RAG: a small retrieve-then-generate endpoint over the same chunks, surfaced as an Ask tab in my homelab dashboard app. The discipline carries straight through — every answer cites its vault sources by page and heading, carries the index revision it was built from, and wears a stale badge whenever that revision lags the vault:

Related posts:
- Make Your AI Agents Read the Docs First, Every Time — the begin step that consults this index read-only
- Share Your Vault Across Machines Without Split-Brain — the read-only consumer pattern this follows
- Prove Your Homelab Docs Are Actually Up to Date — verifying this index against vault HEAD
- Running Ollama on a 3-Node Proxmox LXC Cluster — the local inference backend embedding runs on
- Open WebUI Advanced Configuration — another consumer of the same Ollama cluster
Comments
Comments are powered by GitHub Discussions — sign in with a GitHub account to join the conversation.