Back to projects
Active

Personal LLM Wiki

Private local-first knowledge system that turns Markdown notes into an agent-readable wiki with hybrid retrieval.

Private Repo
Obsidian Markdown Python SQLite FTS5 Ollama transformers.js (ONNX) Personalized PageRank TypeScript (Obsidian Plugin)

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:

StageModelWhere it runsWhy there
Embeddings (default)Ollama nomic-embed-text, 768-dim, ~274MBvault_vector.py embed, ingest + querySmall 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-dimvault_vector.py embed --provider openaiOnly 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.jsObsidian plugin semantic mode, Mac/iOS without OllamaRuns fully offline inside the plugin’s WebView; no daemon dependency, so semantic search still works on iOS where Ollama can’t run
Intent classificationOllama llama3.2:3b (LLMWIKI_INTENT_MODEL, env-overridable)llmwiki.py intent classifierSmall/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 synthesisOllama 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/synthesizeWhatever local Ollama model the maintainer session (Claude Code / Codex) has selected — not a pinned model/ingest, maintainer-onlyIngest 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 every wiki_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.json full-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 at http://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-search repo: 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):

  1. A file lands in raw/ingest/, or a URL is pasted into a drop note; /ingest classifies it against a rubric and drafts a page at wiki/<type>/<slug>.md with status: seedling.
  2. The raw source moves to its typed raw/<domain>/ subfolder immutably; the event is appended to _system/log.md.
  3. vault_vector.py embed re-chunks changed wiki pages (H2-aware) plus selected tool/control docs, embeds each chunk with Ollama nomic-embed-text using the document-side task prefix, and writes the updated binary vector store; a tracker records stale/changed/pending material so unchanged chunks are skipped.
  4. wiki_index.py rebuild regenerates .vector/wiki.db from the same pages: metadata, chunks, aliases, frontmatter-typed edges, and an FTS5 virtual table for BM25 lookups.

Query (read-only, tools/scripts/llmwiki):

  1. 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.
  2. Ollama llama3.2:3b labels the query’s intent (fact, explain, draft, compare, plan, critique, raw_search, relationship, …); a regex/heuristic classifier substitutes if Ollama is unreachable.
  3. Exact page/title/alias/path or quoted-phrase queries short-circuit to a live-filesystem scan and skip the vector store entirely.
  4. Conceptual queries fuse three signals: cosine similarity against the vector store (query-side task prefix), live lexical/title/id scoring, and — when wiki.db is fresh — FTS5/BM25 hits.
  5. 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.
  6. If embedding or retrieval exceeds an 8-second timeout (LLMWIKI_RETRIEVAL_TIMEOUT), the CLI falls back to ranked live-file matches instead of failing.
  7. Retrieved pages are downweighted for seedling status and low confidence, then assembled into an evidence packet, with canonical source pages injected for known request shapes (career, writing, design).
  8. 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.
  9. 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):

  1. Cmd+K opens the Vault Search modal: Quick matches filename/alias; Facet filters key:value frontmatter tokens (type:, status:, capability:, …); Semantic (? prefix) runs a vector query. On desktop with Ollama running, Semantic shells out to vault_vector.py against the same store the CLI uses. On Mac/iOS without Ollama, it runs the on-device ONNX nomic-embed-text-v1.5 path via transformers.js, entirely offline, reading a synced copy of the same binary .vector/embeddings.manifest.json + .vector/embeddings.vec store (falling back to legacy embeddings.json only 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:

ModeWhat It Does
QuickFinds files and aliases quickly from the local vault index.
FacetFilters frontmatter with queries like type:entity, status:sprout, or capability:embedding.
SemanticUses ?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.