B2 · docs
Deep dive · the discovery run

Inside b2 similar

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.

Free & local
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.

Prerequisite — a real-model index. 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 initb2 reindex (real model) → b2 similar.

1The pipeline, per anchor

crates/b2-core/src/discover.rs — the candidates fn · crates/b2-cli/src/main.rs

You 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.

anchor note · its stored chunk vectors db::chunks_for_note — read back, never re-embed KNN each vector over chunks_vec db::vector_search — full-vault pool, exact score each other note by best chunk-pair (max-sim) max over anchor-chunk × note-chunk · keep the evidence chunk graph · already-linked reachable_within(anchor, 1) subtract anchor + its 1-hop neighbours the “∖ already-connected” set — near but not yet linked rank by score · take top N sort desc · tie-break b2id · truncate(limit) — deterministic list candidates — path · title · score · evidence CandidateNote { note_b2id, score, evidence_chunk_id }
vectors — KNN & max-sim (no model) graph — the ∖ already-linked exclusion the ranked list you read

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.

Ground truth: discover::candidates(conn, anchor, limit) — for each of the anchor's stored chunk vectors, db::vector_search over the full chunks_vec pool; a HashMap<note, (best_score, chunk)> keeps each note's max-sim and the chunk that achieved it; graph::reachable_within(anchor, 1) is subtracted; the result sorts by score with a b2id tie-break and truncates to limit. Returns empty when there's no embedding space, the anchor has no chunks, or limit == 0. Vector-only; re-embeds nothing.

2What a run costs

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.

The only cost is the index

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.

Scale & the tuning lever

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
Deterministic and re-runnable. Candidate generation is pure vector math over stored data — same index, same anchor, same ranked list every time (ties broken by b2id). Run it as often as you like; it writes nothing and changes nothing.