How the notes Qdrant collection is built from the Obsidian vault — which notes are in scope, why scaffolding pages are dropped, and how the indexer stays incremental and resumable. Code lives in kb-vectors/obsidian_index.py; the search side is 2026-08-31-telep-kb-mcp-server.
Selection rules
Arrived at after several iterations. The unit of selection is a project, not a file:
- Each directory under
projects/ - Each top-level directory in the vault
- All vault-root
.mdfiles together, as one pseudo-unit
Then:
- A unit is excluded entirely if ANY note in it carries the frontmatter tag
personal. Opt-out, not opt-in — one private note takes its whole project out of the index. - Within an included unit,
LOG.mdandTOPICS.mdare skipped as scaffolding. index.md,moc-tagged project overview pages, andAgent Landing.mdare KEPT.
personalon any note removes the entire project from searchThe tag is evaluated per-unit, not per-file. Tagging one note
personalinsideprojects/<x>/silently drops every note in<x>from thenotescollection. If a project stops appearing in agent search results, grep its folder for the tag before debugging the indexer.
Current scope: 28 units excluded, 16 included, 218 notes, ~979 chunks.
Why LOG.md / TOPICS.md are dropped but index.md is kept
Both are generated index pages — near-pure wikilink lists. Homelab’s pair alone was 159 chunks at ~0.9 link density. They embed to noise and, being long, actively displace real answers in the top-k. Their content is derived restatement of notes that are already indexed individually, so nothing is lost.
Genuine prose pages measured 0.09–0.34 link density — an order of magnitude apart from the scaffolding.
A link-density heuristic was tried and rejected
Thresholding on link density looks principled and fails in practice: it split identical file roles incoherently — tatabanya
TOPICS.mdat 0.42 was kept while esp32TOPICS.mdat 0.67 was dropped. The metric tracks project size (a big project’s TOPICS has proportionally more prose around its links), not page quality. Exclude by filename/role, not by a measured proxy for it, when the role is already knowable.
Incremental and resumable
The indexer never calls delete_collection. Per note it stores a sha256 content_hash in the Qdrant payload and:
- unchanged note → skipped, no embedding
- changed note → re-embedded and upserted immediately, not batched to the end of the run
- note deleted, or newly excluded → its points are deleted
Immediate upsert is the property that makes it resumable: a run killed halfway (which happens — see 2026-08-31-nvidia-drm-host-crash-embedding-pass) leaves every already-processed note durably indexed, and the next run picks up exactly where it stopped.
Verified live:
| Run | Result |
|---|---|
| 1 | embedded 2 |
| 2 | re-embedded 0 (hashes matched) |
| 3 | re-embedded only the edited note |
| 4 | purged the deleted note’s points |
Current state
The full index build is INCOMPLETE
Only 27 points across 5 of 218 notes are in
notes. The full embedding pass hard-crashes the host — see 2026-08-31-nvidia-drm-host-crash-embedding-pass.telep-kb-obsidian-sync.timeris disabled for the same reason. Agent searches againstnoteswill look mostly empty until that is resolved; this is not a retrieval bug.
Related
- 2026-08-31-telep-kb-mcp-server — the MCP tools that query this collection
- 2026-08-31-nvidia-drm-host-crash-embedding-pass — the blocker on completing the build
- 2026-07-24-kb-vectorize-complete — the
manualspipeline this mirrors (uuid5 ids, idempotent upsert) - 2026-08-08-telep-master-agent-impl — the host’s vault clone at
/home/levander/obsidianthat this indexes - kb-agent-api
- telep-mainframe