Exactly what happens when you run b2 similar: a pure, local vector KNN
over the embeddings your index already holds, surfacing the notes most similar in meaning to one you name
— minus the ones you've already linked — each with the passage that made it similar.
No model call, no network, no cost. You read the list; b2 link commits the ones worth
keeping. A companion to the architecture's candidate engine.
b2 similar makes zero API calls and touches no network.
Discovery is a vector KNN over your already-stored embeddings — it reads vectors straight back
and re-embeds nothing, so a run is instant and costs nothing per note, per pair, or per token. The only
embedding work is the one-time b2 reindex that built those vectors in the first place.
You are the precision gate — you decide which surfaced notes deserve a real connection and author
it. There is no suggestion queue, nothing "inert until accepted": a connection exists only once you write
it.
similar reads the vectors + typed
graph a prior b2 reindex wrote — it reads stored vectors
straight back and never re-embeds, so it needs no model and no API key of its own (it calls
nothing). But if that index was built under B2_EMBEDDER=fake, the vectors are blake3 hashes,
not semantics, and the neighbours it surfaces are meaningless. For real discovery the order is
b2 init → b2 reindex (real model) → b2 similar.candidates fn · crates/b2-cli/src/main.rsYou name one note — the anchor. B2 reads its stored chunk vectors, finds the nearest chunks across the whole vault, scores every other note by its best chunk-pair match, subtracts the notes already one hop away in the graph (the ones you've linked), ranks what's left, and lists the top N. Every step is vector math and a graph read — no model, no network.
A candidate is a note near the anchor in embedding space but not already connected —
the complement of the graph, not the intersection. Generation is deliberately permissive:
it over-produces and leaves the judgment to you. Two honesty notes baked into the code:
it is passage↔passage — the anchor is represented by the vectors already in chunks_vec,
never by an embed_query of its text (bge's asymmetric query prefix is the wrong side); and the
1-hop exclusion is the graph's only role — graph-distant "bridge" notes ride along unboosted, so a
transitively-related note two hops away still surfaces.
The short version: nothing, and it's instant. b2 similar is a local KNN
over vectors already on disk — it makes no network request, loads no model, and spends no tokens. There is
no per-note, per-pair, or per-token bill because there is no model in the loop at all.
Embedding happens once, in b2 reindex,
when your vectors are written. similar reads those vectors straight back; running
it a hundred times costs exactly what running it zero times does. Re-embedding only happens when a note
changes and you reindex again.
Cost scales with chunk count (the KNN pool), not with
a token budget — it's CPU-bound vector math, milliseconds on a personal vault. The one knob is
--limit (how many candidates to list); it changes what you read, never what you pay. No
API key, no config table, no rate limit.
# the real path — build the index once, then discover for free, forever b2 init # provision the real embedder (one time) b2 reindex # embed the vault — the only embedding cost b2 similar espresso-extraction.md # instant local KNN — no model, no network, no tokens
b2id). Run
it as often as you like; it writes nothing and changes nothing.The list is candidates, not connections. You pick the ones worth keeping and commit them. Two ways, both authored in Markdown — B2 supplies the mechanics, you supply the judgment and the type:
Type a bare [[path|title]] (an untyped
references edge) or a typed - <verb> [[path|title]] — … line in your
note's body. B2 reads it on the next reindex; it never writes your body.
b2 linkb2 link <src> <dst> [--type <verb>]
[--explanation …] appends one typed-link string to the source note's frontmatter
relations: — Markdown-first, never the body — then re-projects that note so the
edge materializes as origin=frontmatter, active.
b2 similar espresso-extraction.md surfaced
Grind size as its nearest unlinked note. You agree it's load-bearing — the shot
depends on the grind — so you commit it with the verb you choose from the core vocabulary:
# 1 — surface the nearest not-yet-linked notes (free, local, instant) b2 similar espresso-extraction.md 0.8143 Grind size (notes/dialing-in-grind.md) Finer grinds raise extraction pressure; past a point the shot chokes and turns bitter… 0.7920 Water temperature (notes/brew-temp.md) Temperature stability matters as much as the target — a swing mid-shot muddies the cup… 0.7614 Tamping (notes/tamping.md) An even tamp keeps the puck flat so water can't channel down one side… # stderr: 3 candidate(s) · a local KNN over stored vectors · no model call. # 2 — commit the one worth keeping; you pick the type (defaults to `references`) b2 link espresso-extraction.md notes/dialing-in-grind.md --type supports \ --explanation "grind size sets the extraction pressure" linked espresso-extraction.md supports→ Grind size wrote frontmatter relations: (body byte-identical) · reindexed · 1 edge active
What changed on disk is one line, in the source note's frontmatter — the body is untouched:
relations: + - "supports [[dialing-in-grind.md|Grind size]] — grind size sets the extraction pressure"
suggested
or rejected status and no review queue to survive a rebuild — drop the index, reindex, and the
edge comes back because it's in your note. A committed edge is pristine: just the
<verb> [[path|title]] line, no by, confidence, or provenance
stapled on. To undo one, delete its relations: entry; it's gone on the next reindex.