Skip to content

Recall

Recall ranks captured sources by semantic similarity to your query:

Terminal window
khiipd recall "Inca knot record system"
khiipd recall "building a second brain with AI" --limit 5
khiipd recall "quipu" --min-score 0.3 # drop clearly-unrelated hits
khiipd recall "quipu" --view full # nested {capture, score} records instead of lean rows

How 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 typed Capture plus 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:

    FieldWhat it is
    idCapture ULID — drill down with GET /api/v1/captures/{id}
    urlSource URL
    sourcex / web / wiki / pdf / youtube / reddit
    titleCapture 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 to full — the shape the shipped Obsidian plugin parses. Pass ?view=lean for the lean row.
  • CLI khiipd recall defaults to --view lean; --view full restores the nested records.
  • MCP recall and list_captures are lean-only — the flat row is the contract for agents, with get_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:

SourcePrimary textexcerpt_from
X / Twitterthe tweet texttext
Redditthe post body_text (empty on link / media posts)body_text
Webthe article body_textbody_text
Wikipediathe lead section textsections
YouTubethe video descriptiondescription
PDFthe head of the page textspages
(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.