8.5 KiB
| created | path | project | tags | type | ||||
|---|---|---|---|---|---|---|---|---|
| 2026-05-17 | Sources/Homelab | vault-semantic-search |
|
project-plan |
Vault Semantic Search
Goal
Add a vault:search MCP tool that takes a query string (plus optional
metadata filters) and returns vault notes ranked by semantic similarity.
The tool runs entirely against the wiki schema and reuses the embedding
infrastructure built by the Postgres consolidation plan.
Concrete use case: "search the vault for context" returns relevant notes without needing to remember slugs or tag names. Semantic search replaces the current "list everything, filter client-side" pattern.
This plan is scoped to vault search only. A future unified memory search
spanning vault + thoughts + reflections is anticipated via a scope
parameter, but is deferred until vault search is in use and shaping
requirements are clearer.
Locked Decisions
Tool surface
- Name:
vault:search - Read-only. No writes, no side effects.
- Single MCP tool — no separate "by tag" or "by date" variants. All filtering is parameterized on this one tool.
Inputs
query(required, string) — natural-language search querytags(optional, list of strings) — restrict to notes carrying any of these tagsdomain(optional, string) — one of the schema's domain values (homelab, dev, venture, reference)status(optional, list of strings) — one or more lifecycle statusesnote_type(optional, string) — restrict to project-plan, session-notes, entity, topic-landing, or synthesisdate_range(optional, object withstartandend) — filter byupdated_attimestamp rangelimit(optional, int, default 10) — max results to return
Filters compose via AND. Vector similarity ranks within the filtered candidate set.
Output shape (minimal)
[
{
"slug": "ob1-deployment",
"title": "OB1 Main Deployment",
"path": "Sources/Homelab/ob1-deployment.md",
"score": 0.82
},
...
]
Score is the cosine similarity (or pgvector distance converted to
similarity — implementation detail). Caller uses vault:get_project
to fetch full content for any result they want to read.
Keeping content fetch separate from search keeps the search response cheap and lets callers decide when full body content is worth the round trip.
Embedding behavior
- Query embedding runs server-side inside the vault MCP server.
- The server uses the same embedding model that produced the stored
vectors in
public.embeddings(recorded in theembedding_modelcolumn). - Mismatches are not silently tolerated — if the configured query model doesn't match the model recorded on the rows being searched, the search returns an error rather than incorrect rankings.
- Embedding model selection is inherited from the consolidation plan; this plan doesn't pick or change models.
Scope
- Searches the
wikischema only. Both Sources and Wiki layers are searchable; filter vianote_typeif a caller wants Sources-only or Wiki-only. - Does not search
ob1.thoughtsorob1.reflections. That's a separate tool (or this tool extended with ascopeparameter) in a future plan.
Backend mechanics
- Vector similarity query against
public.embeddingsfiltered bysource_schema = 'wiki'. - Filters applied via WHERE clauses joining
public.embeddingstowiki.notes(or whatever the canonical wiki content table is named). - HNSW index on
public.embeddings.embeddingis the consolidation plan's responsibility; this plan reuses it.
Open Items
- Final SQL query shape — JOIN pattern between
public.embeddingsand the wiki content table depends on exact column names from the consolidation plan. Resolve at implementation time. - How to surface "no results" vs "search failed" — empty array versus error response. Likely empty array for "no results" and error for "model mismatch / query embed failed."
- Pagination —
limitis in scope; offset / cursor pagination is not. Add later if it's needed. - Caching — query embedding cost is small but non-zero. No caching in v1. If usage patterns show repeated queries, add later.
Phases
Phase 0 — Verify dependencies
- Consolidation plan's Phase 5 (vault indexing) is complete.
public.embeddingshas rows wheresource_schema = 'wiki'covering the full vault corpus. - HNSW index exists on
public.embeddings.embedding. - Embedding model is documented and reachable from the vault MCP server's host.
- The vault MCP user has read access to
public.embeddings.
Phase 1 — Tool implementation
- Add
searchfunction to the vault MCP server. - Wire to the configured embedding model for query embedding.
- Build the parameterized SQL query (vector search + filters).
- Define the response shape per the locked decisions.
- Add input validation (filter values exist in schema, date format correct, etc.).
Phase 2 — Verification
- Smoke test: search for a topic that's definitely in the vault ("MCP", "Traefik") — confirm relevant results appear in top 5.
- Negative test: search for nonsense gibberish — confirm low scores or empty results, not garbage matches.
- Filter test: search with
tags=["mcp"]— confirm results are restricted correctly. - Combined filter test: query + tag + date range — confirm AND semantics work.
- Model-mismatch test: temporarily configure a different embedding model, confirm tool errors rather than returning bad rankings.
Phase 3 — Documentation
- Add usage examples to the vault MCP server's README.
- Note the tool in CLAUDE.md so agents know to use it.
- Capture lessons learned in a session note.
Notes
Why this isn't part of the consolidation plan
The consolidation plan builds the substrate — embeddings table, vault indexing, HNSW index. This plan builds the user-facing tool that consumes the substrate. Splitting them keeps the consolidation plan focused on infrastructure and lets the search tool ship with its own small, focused scope.
Why minimal output instead of full content
Most searches are exploratory — "what do I have on X" — and only a
fraction of returned hits will actually be read. Returning bodies in
the search response means every search pays for content fetching that
mostly isn't used. Keeping search lightweight and giving callers a
separate get_project path matches the actual usage pattern.
Why a single tool with parameters instead of separate tools
A single parameterized tool (vault:search with optional tags, domain,
etc.) is more flexible than many tools (vault:search_by_tag,
vault:search_recent, etc.) and matches how natural-language queries
combine criteria. "Find recent context-window notes tagged claude-code"
is one tool call with three filters, not three tool calls composed
manually.
Why server-side embedding is the only correct choice
Each embedding model produces vectors in its own geometry. Vectors from different models aren't comparable, and a model mismatch produces silently wrong rankings — not an error, just bad results. The server owns the stored vectors and knows which model produced them, so it's the right place to ensure the query uses the same model. Letting clients embed queries themselves would require every client to know which model is correct and would fail invisibly when wrong.
Future extension: scope parameter
Anticipated shape for the unified memory search that follows:
vault:search(query, scope="vault" | "memory" | "all", ...)
scope="vault"— current behavior, wiki schema only (default)scope="memory"— ob1.thoughts and ob1.reflections onlyscope="all"— everything
Implementation extends the WHERE clause to include additional
source_schema values. The output shape grows a source field so
callers can tell vault hits from memory hits. Deferred to a separate
plan once real vault-search usage informs the design.
Flag for Lovebug
The Postgres consolidation plan (postgres-consolidation-reflection-layer,
saved 2026-05-15) was written via the now-deprecated save_note path.
The vault markdown file exists at
Sources/Homelab/postgres-consolidation-ob1-reflection-layer.md, but
the Postgres row does not — vault:get_project returns "not found" for
that slug. The plan needs to be re-registered via vault:create_artifact
so it shows up in list_projects, can have its status updated, and
participates in the Trello card projection. Not blocking for this
search plan, but worth handling before the consolidation plan goes
into execution tracking.