Recall
Recall ranks captured sources by semantic similarity to your query:
khiipd recall "Inca knot record system"khiipd recall "building a second brain with AI" --limit 5khiipd recall "quipu" --min-score 0.3 # drop clearly-unrelated hitskhiipd recall "quipu" --view full # nested {capture, score} records instead of lean rowsHow it works
Recall uses the default MiniLM-L6 ONNX model (downloaded once on first use, ~80MB) to embed a per-source embed-text composition of each capture’s typed payload — a deterministic projection of the structured fields that matter for that source (title, body, author, key entities) rather than a raw text dump. Queries are embedded the same way, and recall returns cosine top-k matches.
This runs locally, at zero LLM cost, and works offline after that one-time model fetch. Quality is whatever MiniLM-L6 gives you — good enough for “find that thing I captured about X,” and pluggable for more (below).
Recall returns one hit per supersession chain: when a capture has been superseded by a refetch, only the current head is ranked — not every historical version.
One coverage caveat: a capture whose embedding failed at capture time is invisible to
recall (the daemon’s live /docs notes this too). khiipd refetch <id> --re-extract
re-embeds it in place, offline.
Lean vs full hits
A recall response can come back in one of two per-hit shapes, chosen by a
view parameter:
-
full— nested{capture, score}records: the complete typedCaptureplus its cosine score. This is the unchanged pre-0.2.4 shape. -
lean— a flat, token-lean row per hit, built for LLM agents whose tool-result budgets a full payload would blow past. Each lean hit carries:Field What it is idCapture ULID — drill down with GET /api/v1/captures/{id}urlSource URL sourcex/web/wiki/pdf/youtube/reddittitleCapture title (never truncated) authorByline when known (never truncated) recorded_atWhen Khiip captured it vault_pathThe note’s path in your vault descriptionThe source’s own summary, when it has one excerptHead of the source’s primary text (see below) excerpt_fromWhich payload field the excerpt came from payload_charsCharacter length of the full JSON payload — budget a drill-down before you fetch it scoreRaw cosine similarity (lean recall hits only; see score interpretation)
The list shape (GET /api/v1/captures) uses the same lean row minus
score. The response envelope — query, embedder_model,
embedder_dimension, results — is identical in both views; only the
per-hit shape differs.
The lean field set is an append-only contract: new fields may be added in a later release, but existing ones are never renamed or removed.
Defaults differ by surface, on purpose:
- REST (
/api/v1/recall,/api/v1/captures) defaults tofull— the shape the shipped Obsidian plugin parses. Pass?view=leanfor the lean row. - CLI
khiipd recalldefaults to--view lean;--view fullrestores the nested records. - MCP
recallandlist_capturesare lean-only — the flat row is the contract for agents, withget_capture(id)as the drill-down. There is no full/verbose flag over MCP.
Excerpts
A lean hit’s excerpt is the head of that source’s primary text —
enough to orient you without pulling the whole payload. It’s composed by
the daemon (never re-derived client-side), hard-capped at 500
characters, with a single … appended only when the text was actually
truncated — an excerpt that lands exactly at the cap carries no ellipsis.
There are no highlight markers.
excerpt_from names the payload field the excerpt was drawn from:
| Source | Primary text | excerpt_from |
|---|---|---|
| X / Twitter | the tweet text | text |
the post body_text (empty on link / media posts) | body_text | |
| Web | the article body_text | body_text |
| Wikipedia | the lead section text | sections |
| YouTube | the video description | description |
| the head of the page texts | pages | |
| (no typed payload) | — (empty excerpt) | none |
excerpt_from reports the field it consulted, so a source whose primary
field happened to be empty still gets a sensible provenance value with an
empty excerpt; none appears only when a capture has no typed payload at
all (older captures, or a failed extraction). Like the field list, these
excerpt_from values are append-only.
Score interpretation
Each hit carries a raw cosine similarity score in [-1, 1] (the CLI prints it as the first column). It is not a percentage, and it is not normalized to [0, 1]:
- Higher = closer in meaning. Results are ordered by score.
- Negative or near-zero just means unrelated. Your corpus will always hold sources unrelated to a given query, and those legitimately score at or below zero — a negative score is not an error.
- Rule of thumb for the default MiniLM embedder: roughly 0.3+ is practically relevant. Between zero and ~0.3 is a gray zone — sometimes a true match phrased very differently from your query, sometimes noise. It’s a heuristic, not a hard line — skim the titles rather than trusting the number.
- Thresholds are model-relative. A useful floor for MiniLM says nothing about a different embedder (see the roadmap below); re-calibrate if you swap models.
To cut the unrelated tail instead of eyeballing it, set a floor with --min-score
(REST: min_score=; MCP: recall(..., min_score=…)). Hits below the floor are dropped
server-side — a hit exactly at the floor is kept — so you may get back fewer than
--limit results. Unset means no filtering.
Tuning
--limit N— how many results to return (default 10).--min-score X— drop hits scoring below a cosine floor in [-1, 1] (default: no filter; see score interpretation).- Recall is by meaning, not keyword — “how LLMs actually work” can surface a captured talk transcript and an article that never use those exact words.
Pluggable embedders (roadmap)
The embedder sits behind a Protocol so it can be swapped without touching the rest of the substrate. Planned (no committed version):
- Local LLM via Ollama (better quality, still free + local)
- BYOK (OpenAI / Anthropic / Gemini) for best-quality embeddings
- BM25 keyword fallback for exact-term recall
Switching embedders requires a re-embed of the corpus (a backfill); the captured payloads themselves don’t change.