Personal LLM Wiki
Private local-first knowledge system that turns Markdown notes into an agent-readable wiki with hybrid retrieval.
The Problem
Most personal knowledge bases are readable by humans but hard for agents to use safely. The files contain useful context, but retrieval is inconsistent, raw source material mixes with synthesis, and agents can overreach when they are handed the whole vault.
The LLM Wiki separates durable knowledge from raw material, gives agents a narrow query path, and keeps private source data out of public surfaces.
What I Built
The Obsidian Vault LLM Wiki is a private, local-first knowledge system for turning notes, source material, decisions, projects, and operating context into a readable wiki that humans can browse and LLM agents can query.
It is not presented as a public repo because the value is the architecture and retrieval method, not the private note contents.
File Structure
The vault uses a small number of stable zones.
Obsidian Vault
|-- _system/ # workspace instructions, memory, indexes, logs, eval notes
|-- brain/ # operating rules and voice guidance
|-- wiki/ # compiled knowledge: concepts, entities, sources, work
|-- raw/ # immutable source material, filed by domain
|-- tools/ # scripts, commands, plugins, prompts, skills, agents
|-- templates/ # reusable note and entity templates
|-- outputs/ # drafts, health reports, QA, snapshots
|-- writing/ # author-owned writing and suggestion sidecars
`-- .vector/ # gitignored retrieval indexes and embeddings
The public description only names the system shape. It does not publish notes, source files, filenames from private material, or graph relationships.
Architecture
Uses the standard knowledge-graph pipeline (see /toolkit/patterns/knowledge-graphs) — what differs here: dense-vector + BM25 hybrid retrieval fused with Personalized PageRank reranking over the note graph, and every retrieval-side model is small and local (Llama 3.2 3B, nomic-embed-text) rather than a frontier model.
The vault is a Python CLI (vault_vector.py, wiki_index.py, llmwiki.py, stdlib-first — numpy is used opportunistically when installed, not required) plus an Obsidian plugin (TypeScript, obsidian-vault-search). Markdown with YAML frontmatter is the only source of truth; the vector store and SQLite index are gitignored, derived, and fully rebuildable from the Markdown. Two paths run through this system: an ingest/write path (maintainer-directed, gated) and a query/read path (tools/scripts/llmwiki, the only surface worker agents get).
flowchart LR
subgraph Ingest["ingest — maintainer-directed, writes gated"]
A["raw/ingest/ file or URL"] --> B["/ingest classify<br/>-> wiki/*.md, status: seedling"]
B --> C["vault_vector.py embed<br/>Ollama nomic-embed-text"]
B --> D["wiki_index.py rebuild<br/>SQLite + FTS5"]
end
C --> VEC[(".vector store<br/>manifest.json + .vec, int8")]
D --> DB[(".vector/wiki.db<br/>chunks/aliases/edges/FTS5")]
subgraph Query["query — read-only via llmwiki"]
Q[query] --> G["deterministic safety gates<br/>blocks write/admin verbs"]
G --> I["intent classifier<br/>Ollama llama3.2:3b"]
I --> EX["exact filesystem lookup<br/>page/title/alias/path"]
I --> SEM["vector + lexical + FTS5 fusion"]
SEM -->|relational query| PPR["Personalized PageRank<br/>typed edges + wikilinks"]
EX --> EV["evidence packet"]
SEM --> EV
PPR --> EV
EV --> ANS["template, or Ollama llama3.2:3b<br/>synthesis over evidence only"]
end
VEC --> SEM
DB --> SEM
ANS --> OUT["answer + citations<br/>checked by 50/50 + 11/11 + 15/15 eval suites"]
subgraph Plugin["Obsidian plugin — human surface"]
K["Cmd+K modal"] --> QM[Quick]
K --> FM[Facet]
K --> SM["Semantic, '?' query"]
SM -->|desktop + Ollama| CLI2["vault_vector.py CLI"]
SM -->|Mac/iOS offline| OND["on-device ONNX<br/>nomic-embed-text-v1.5<br/>transformers.js"]
end
CLI2 --> VEC
VEC -.synced store.-> OND
Models — which, where, why:
| Stage | Model | Where it runs | Why there |
|---|---|---|---|
| Embeddings (default) | Ollama nomic-embed-text, 768-dim, ~274MB | vault_vector.py embed, ingest + query | Small local footprint; asymmetric task prefixes (search_document: / search_query:, required by nomic v1.5) keep corpus and query embeddings in the same region of the space |
| Embeddings (opt-in cloud) | OpenAI text-embedding-3-small, 1536-dim | vault_vector.py embed --provider openai | Only invoked explicitly, for cases where local embedding quality isn’t enough — not the default path for private vault content |
| Embeddings (on-device, mobile) | ONNX nomic-embed-text-v1.5 via transformers.js | Obsidian plugin semantic mode, Mac/iOS without Ollama | Runs fully offline inside the plugin’s WebView; no daemon dependency, so semantic search still works on iOS where Ollama can’t run |
| Intent classification | Ollama llama3.2:3b (LLMWIKI_INTENT_MODEL, env-overridable) | llmwiki.py intent classifier | Small/fast enough for a single one-of-N label per query (fact, explain, draft, compare, plan, relationship, …); a regex/heuristic classifier substitutes when Ollama is down, so the CLI degrades instead of failing |
| Answer synthesis | Ollama llama3.2:3b default (LLMWIKI_ANSWER_MODEL, swappable) | llmwiki.py answer() | Kept small deliberately — it’s constrained to phrase over a deterministic evidence packet, not to recall facts from its own weights; known high-risk artifact shapes (resume outlines, positioning memos) bypass it entirely and render from templates |
| Ingest classify/synthesize | Whatever local Ollama model the maintainer session (Claude Code / Codex) has selected — not a pinned model | /ingest, maintainer-only | Ingest is a human-directed maintenance action, not an automated pipeline call, so the model choice follows the session, not a fixed config |
Tools & infra — which, why:
- SQLite (
.vector/wiki.db) — page metadata, chunks, aliases, frontmatter-typed graph edges, and an FTS5 virtual table in one file. Rebuilt from Markdown on everywiki_index.py rebuild; never hand-edited. - Custom binary vector store (
.vector/embeddings.manifest.json+.vector/embeddings.vec) — a from-scratch “nvec1” format, int8-quantized by default (4x smaller than float32, ~0 measured recall loss, small enough to sync to iOS); the earlier plain.vector/embeddings.jsonfull-JSON store is kept only as a legacy read fallback, not the current write path — this corrects an earlier version of this write-up. - Ollama — local model runtime for both embeddings (
nomic-embed-text) and generation (llama3.2:3b), reached athttp://127.0.0.1:11434; the default backend everywhere except mobile. - Personalized PageRank — pure-Python power iteration (deliberately no networkx/scipy dependency), seeded by vector-search scores, walked over typed frontmatter edges + wikilinks. Only invoked for relationship/dependency/impact-shaped queries, so ordinary semantic queries don’t pay the graph cost.
- transformers.js + ONNX Runtime Web — on-device embedding inference bundled into the Obsidian plugin (~3MB), the only runtime dependency the plugin ships; gives Mac/iOS a fully offline semantic-search path.
- esbuild + TypeScript — builds the Obsidian plugin (
obsidian-vault-searchrepo:main.ts,modal.ts,search.ts,semantic.ts,ondevice.ts,ranking.ts,facet.ts). llmwiki(Python CLI) — the sole read-only query surface handed to worker/agent sessions; blocks write/admin verbs (embed,ingest,update,promote,supersede,lint) and unsafe flags (--save,--force) at the CLI layer, deterministically, before any model is invoked.
Quality boundaries. Draft (status: seedling) and low-confidence (confidence: low) pages remain searchable but are downweighted 0.7x each (compounding) at rerank, and prefixed [seedling] / [low-conf] in results. Personal-only and investment-scoped material is excluded from general professional queries unless the query explicitly asks for that scope. This is a retrieval-quality boundary, not a cryptographic privacy boundary.
How it works
Ingest (maintainer-directed, writes gated):
- A file lands in
raw/ingest/, or a URL is pasted into a drop note;/ingestclassifies it against a rubric and drafts a page atwiki/<type>/<slug>.mdwithstatus: seedling. - The raw source moves to its typed
raw/<domain>/subfolder immutably; the event is appended to_system/log.md. vault_vector.py embedre-chunks changed wiki pages (H2-aware) plus selected tool/control docs, embeds each chunk with Ollamanomic-embed-textusing the document-side task prefix, and writes the updated binary vector store; a tracker records stale/changed/pending material so unchanged chunks are skipped.wiki_index.py rebuildregenerates.vector/wiki.dbfrom the same pages: metadata, chunks, aliases, frontmatter-typed edges, and an FTS5 virtual table for BM25 lookups.
Query (read-only, tools/scripts/llmwiki):
- A query enters
llmwiki, which first runs deterministic safety gates that block write/admin verbs and unsafe flags — the model is never asked whether a write is allowed. - Ollama
llama3.2:3blabels the query’s intent (fact, explain, draft, compare, plan, critique, raw_search, relationship, …); a regex/heuristic classifier substitutes if Ollama is unreachable. - Exact page/title/alias/path or quoted-phrase queries short-circuit to a live-filesystem scan and skip the vector store entirely.
- Conceptual queries fuse three signals: cosine similarity against the vector store (query-side task prefix), live lexical/title/id scoring, and — when
wiki.dbis fresh — FTS5/BM25 hits. - Relationship, dependency, or impact-shaped queries add a Personalized PageRank walk over typed edges and wikilinks, seeded by the vector hits, surfacing connected pages pure semantic similarity would miss.
- If embedding or retrieval exceeds an 8-second timeout (
LLMWIKI_RETRIEVAL_TIMEOUT), the CLI falls back to ranked live-file matches instead of failing. - Retrieved pages are downweighted for
seedlingstatus andlowconfidence, then assembled into an evidence packet, with canonical source pages injected for known request shapes (career, writing, design). - High-risk artifact shapes (resume outlines, positioning memos, comparisons) render through deterministic templates; open-ended questions get grounded synthesis from Ollama
llama3.2:3b, instructed to use only the evidence packet. - A citation/eval suite checks required facts, required source IDs, and forbidden labels before any routing-weight or prompt change ships — see Results.
Plugin (the same stores, reached from inside Obsidian):
Cmd+Kopens the Vault Search modal: Quick matches filename/alias; Facet filterskey:valuefrontmatter tokens (type:,status:,capability:, …); Semantic (?prefix) runs a vector query. On desktop with Ollama running, Semantic shells out tovault_vector.pyagainst the same store the CLI uses. On Mac/iOS without Ollama, it runs the on-device ONNXnomic-embed-text-v1.5path viatransformers.js, entirely offline, reading a synced copy of the same binary.vector/embeddings.manifest.json+.vector/embeddings.vecstore (falling back to legacyembeddings.jsononly if the binary store is absent).
Tech stack
Markdown + YAML frontmatter (source of truth), Python (vault_vector.py, wiki_index.py, llmwiki.py — stdlib-first; numpy optional, used when present), SQLite + FTS5 (.vector/wiki.db), a from-scratch binary vector store (int8-quantized .vector/embeddings.manifest.json + .vector/embeddings.vec), Ollama (nomic-embed-text for embeddings, llama3.2:3b for intent + answer generation, both local), Personalized PageRank (pure-Python power iteration) for graph rerank, and an Obsidian plugin in TypeScript (esbuild, transformers.js + ONNX Runtime Web for on-device mobile inference). OpenAI text-embedding-3-small is available as an explicit opt-in embedding fallback; nothing else leaves the machine by default.
Plugin Support
The Obsidian Vault Search plugin makes the retrieval system usable inside Obsidian.
It has three modes:
| Mode | What It Does |
|---|---|
| Quick | Finds files and aliases quickly from the local vault index. |
| Facet | Filters frontmatter with queries like type:entity, status:sprout, or capability:embedding. |
| Semantic | Uses ?query to search the vector layer. |
On desktop, semantic mode can shell out to vault_vector.py and use the full hybrid stack: local embeddings, FTS5/BM25, and graph rerank. On Mac or iOS, the plugin can run on-device with transformers.js and a local ONNX nomic-embed-text-v1.5 model, then compare the query embedding against the synced binary vector store (see Architecture).
That gives the vault two working surfaces: a terminal command for agents and a native Obsidian search flow for day-to-day use.
Agent Access Model
Worker LLMs get a read-only query surface rather than broad vault access. The stable command is tools/scripts/llmwiki, which can answer from the derived indexes, show raw retrieval hits, and inspect graph relationships.
Maintenance commands such as ingest, edit, embed, promote, supersede, and saved-output writes stay reserved for trusted maintainer sessions. That keeps the public story simple: agents can use the knowledge system without being handed the underlying private archive.
Results
- Answer-quality eval suites (checked before any routing-weight or intent-prompt change ships): 50/50 on the core case set, 11/11 on long-query cases, 15/15 on phrasing-variation cases, as last recorded in the repo’s architecture notes. Each case checks required facts, required source IDs, and forbidden response labels — not a generic LLM-judge score.
- ⚠️ not measured: retrieval-quality ablation (hybrid vs. vector-only precision/recall, rerank lift from the Personalized PageRank step) against a labeled query set. The eval suites above test end-to-end answer correctness, not which retrieval signal contributed how much.
Impact
The system treats Markdown as the durable human-readable record and retrieval as a rebuildable layer. That keeps the vault portable while still giving agents structured access to context, evidence, and relationships.
The important idea is not “a vector database over notes.” It is a controlled knowledge workflow: raw sources become wiki pages, wiki pages become indexed chunks, retrieval combines vector, lexical, and graph signals, and agents only receive the narrow access path needed for the task.