Semantic Search for Your Notes, Running Locally

Give your knowledge vault semantic search with a local embeddings index — built on Ollama and nomic-embed-text, cached by content hash, and labeled with the commit it was built from so staleness is always visible.

On this page
  1. The index pipeline
  2. Querying — and why to go region-first
  3. The swap-in box
  4. What you have now

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.

The principle

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.

First: make these values your own

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

Build a revision-labeled embeddings indexchunkby sectionhash cacheskip unchangedchunksembed (Ollama)nomic-embed-textnodes .65/.66/.67atomic swapstamp commit64dc195query → top-k chunks + their commitconfirm each hit against the canonical pagebuildahomelab.dev
1Chunk pages by section

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.

2Embed with a local model on Ollama10 min

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.

On each Ollama node

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"}'
3Cache by content hash; swap atomically; stamp the commit5 min

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.

Build + verify

node tools/embeddings/build-index.mjs .     # re-embeds only changed chunks
node tools/embeddings/verify-index.mjs .    # index commit == vault HEAD ?
A derived consumer, treated like one

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

Semantic search over the vault

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:

Two-stage search: region-first

# 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

On other tools

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:

The Ask tab in the HomeLab app: a chat where answers about a paging alert and node RAM headroom carry numbered citation chips linking to vault pages, plus a footer with the vault revision and an index-stale badge
Ask the vault: numbered citations resolve to page + heading chips, and the second answer's amber badge marks it as generated from a stale index — labeled, never silently current. (Demo transcript; the addresses and revision are stand-ins.)

Related posts:

Comments

Comments are powered by GitHub Discussions — sign in with a GitHub account to join the conversation.