Back to releases
v0.9.0

Search evidence lineage and replayable retrieval (v0.9.0)

Every retrieval now has a stable identity, verifiable citations, and full lineage. Admins can replay a search with the original query within the retention window — no more guessing at RAG recall drift.

RetrievalObservabilityEvidence lineage

Retrieval is the hardest part of RAG to debug: the same question might return three hits today and two tomorrow, and it’s hard to pin down whether the difference comes from the model, the chunking, or the Generation snapshot. v0.9.0 gives every retrieval a stable identity, records the exact version, chunk, and citation each result traces back to, and adds an admin replay entry point that doesn’t expose internal IDs to the public API.

Every retrieval gets a search_id

Each successful retrieval creates a SearchRun that exposes an opaque search_id (UUID) and links to one or more Generation snapshots. The ID only points to “which index version was used for this retrieval” — it never leaks the Generation ID to API callers, since Generation is an internal concept that doesn’t appear in the public protocol.

Single-knowledge-base REST still returns a []SearchResult array with an unchanged body; run metadata travels via the X-Search-ID, X-Retrieval-Status, and X-Generation-IDs response headers. Multi-KB retrieval and MCP extend the wrapper structure with search_id, retrieval_status, and related fields.

Results trace back to a specific version and chunk

SearchResult now carries full lineage:

  • DocumentRevisionID — which version of the document was hit.
  • IndexGenerationID — which index build it came from.
  • Citation — includes ContentSHA256, the UTF-8 SHA-256 of the returned content, which you can recompute independently to verify that “the evidence you see” matches “the evidence the retriever returned.”

Source Anchor still only encodes position (page / row / offset) and carries no content. Position and content are kept separate, so you can both locate the original text and verify the content hasn’t been tampered with.

Retrieval status is distinguishable

retrieval_status splits outcome state into four explicit buckets:

  • available — results returned normally.
  • empty — zero results (note: not a failure).
  • degraded — a fallback was triggered, e.g. rerank was unavailable and the system fell back to pure hybrid recall.
  • failed — a failure, which must carry a non-empty failure_class telling you what kind.

This matters for debugging RAG: zero results and a failure are different things, and a rerank fallback is not the same as a normal recall. The protocol and logs can tell them apart.

Admins can replay

A workspace owner / admin can replay a retrieval within the SearchRun retention window (default 168h) using the original query:

  • Replay is pinned to the Generation, topK, and rerank snapshot recorded on the original SearchRun — it never falls back to the current active Generation, so you can actually reproduce the original results.
  • A query-hash mismatch returns 409 search_query_mismatch; if a referenced Generation has already been cleaned up, it returns 409 generation_not_available.
  • Replay re-checks the caller’s access to each knowledge base and creates a new SearchRun that points back to the original via replay_of_id, forming a traceable replay chain.
  • Replay only accepts a browser Session identity (owner/admin); a Bearer API Key gets 403 — API keys should not have replay capability.

Privacy boundary: the raw query is never stored

A SearchRun only stores run metadata — never the raw query, body text, vectors, or credentials. The query survives only as a query_hash (SHA-256 of the trimmed query, prefixed with sha256:v1:) — enough to verify query consistency during replay, but not enough to recover the original text. This matches Langhuan’s standing log-redaction policy: the raw query never enters a span, a log entry, or persistence.

For cleanup, the SearchRun retention never exceeds the retired-Generation retention — search_run_generations references Generation via ON DELETE RESTRICT, ensuring you never end up with “a Generation is still referenced but its SearchRun was deleted first” dangling state.