检索是 RAG 里最难调试的一环:同一个问题,今天召回三条、明天召回两条,你却很难说清差异来自模型、分块、还是 Generation 快照。v0.9.0 给每次检索建立一个稳定身份,把结果能回到的具体版本、分块和引用都记下来,并提供一个不暴露内部 ID 给公开 API 的管理员回放入口。
每次检索都有一个 search_id
每次成功检索会创建一个 SearchRun,对外暴露一个不透明的 search_id(UUID),并关联到一个或多个 Generation 快照。这个 ID 只指向「这次检索用的是哪一版索引」,不会泄露 Generation ID 给 API 调用方——Generation 是内部概念,公开协议里你看不到它。
单库 REST 仍然返回 []SearchResult 数组,响应体不变;运行元数据通过 X-Search-ID、X-Retrieval-Status、X-Generation-IDs 响应头携带。多库检索和 MCP 在 wrapper 结构里扩展 search_id、retrieval_status 等字段。
结果能回到具体版本和分块
SearchResult 现在带完整血缘:
- DocumentRevisionID —— 命中的是文档的哪个版本。
- IndexGenerationID —— 来自哪一次索引构建。
- Citation —— 含
ContentSHA256,是返回正文的 UTF-8 SHA-256,可以独立复算校验,确保「你看到的证据」和「检索器返回的证据」一致。
Source Anchor 仍然只表达位置(页码 / 行列 / 偏移),不含内容;位置和内容分开,让你既能定位原文,又能验证内容没被篡改。
检索状态可区分
retrieval_status 把结果状态明确分成四档:
available—— 正常返回结果。empty—— 0 结果(注意:不是失败)。degraded—— 触发了降级,例如 rerank 不可用后回退到纯混合召回。failed—— 失败,必须带非空failure_class,告诉你失败发生在哪一类。
这对排查 RAG 问题很关键:0 结果和失败是两回事,rerank fallback 和正常召回也不一样,协议和日志里都能区分开。
管理员可以回放
Workspace owner / admin 可以在 SearchRun 保留期内(默认 168h)用原查询回放一次检索:
- 回放固定使用原 SearchRun 记录的 Generation、topK 和 rerank 快照,不会 fallback 到当前 active Generation——这样才能复现当时的结果。
- 查询 hash 不一致会返回 409
search_query_mismatch;涉及的 Generation 已被清理则返回 409generation_not_available。 - 回放会重新校验调用方对每个知识库的访问权限,并创建一个新的 SearchRun,通过
replay_of_id指向原 run,形成可追溯的回放链。 - 回放只接受浏览器 Session 身份(owner/admin),Bearer API Key 返回 403——API Key 不该有回放能力。
隐私边界:不存查询原文
SearchRun 只保存运行元数据,不保存原始查询、正文、向量或凭证。查询只以 query_hash(SHA-256 of trimmed query,带前缀 sha256:v1:)的形式留存,够用来校验回放时的查询一致性,但无法反推原文。这与琅嬛一贯的日志脱敏策略一致:query 原文不进 span、不进日志、不进持久化。
清理上,SearchRun 的保留期不会超过 retired Generation 的保留期——search_run_generations 通过 ON DELETE RESTRICT 引用 Generation,确保不会出现「Generation 还在引用、SearchRun 却先被删了」的悬空状态。