语料检索——走图,而不只是走树
上级:corpus
现状(已落地): corpus_search / corpus_read / corpus_list / corpus_links / corpus_grep / corpus_map / corpus_peek / corpus_resolve —— 八个工具,跑在 Meilisearch 词法索引之上(Postgres 全文作退路),外加树形导航(沿 parent_id 逐级走)。没有向量搜索——这是一个刻意的、长期成立的决定:相关性 = 内容主人自己打下的 [[links]],不是嵌入相似度。
corpus_links 最终落成一个 1 跳 的 op,返回 {outgoing, backlinks},而不是最初设想的有界深度 BFS —— 要不要往下爬由 agent 自己决定(对某个邻居再调一次)。每一个邻居都按授权 glob 重新过滤(一条隐藏笔记链到这里,不能被反向边泄露出来)。
当初敲定的设计约束,以及它们守住了没有:
- (a)索引严格是派生产物——Postgres 仍是唯一真相源,写入时增量更新,开机和 vault 同步后跑
ReindexOwner(ephemeral-over-stateful)。守住了。 也正是它让"给一台已经在跑的实例挂上索引"这件事是安全的:开机自动回填,没有空索引的窗口。 - (b)ACL 搭在搜索句柄上——每一条命中都经
corpus_read用的那同一个allowsCorpusURI重新过一遍,于是搜索不会变成绕开暴露公式的第二条路径(acl-and-quota-granularity)。守住了。 - (c)Meili 的混合搜索 / embedder 保持关闭。守住了。
- (d)只有一个搜索句柄,两个消费方。被违背了——而且值得知道。 实际有两个:
corpus.search(owner/admin 那个 op)直接打 Postgres 的SearchNotes,永远不碰 Meili;corpus_search(访客 agent 的工具)走 Meili 那条 lister。名字几乎一样,路完全不同 —— 拿 admin 面去量检索改动,说明不了访客那一侧的任何事。
CJK 那条事实,说准
这里原来的措辞是「PG 的全文搜索在 CJK 上偏弱」。这个说法把它说轻了,而说轻本身正是索引在 dev 有、在 prod 没有、躺了几个月没人发现的原因。Postgres 全文根本不切分中文:to_tsvector('english', …) 把一整段连续中文塔成一个词元,于是查询只有在逐字等于正文里那一段连续中文时才命中。
挂索引之前在线上量的:无限衍义 找得到它那篇;限衍、三个序列、资格证 全是 0;三张资格证 连"标题里就有这五个字"的那篇都找不到 —— 因为正文里那一段是 与三张资格证。英文完全不受影响(unlimited semiosis → 8 条)—— 双语语料就是这样从一侧看完全正常,把这个洞无限期地藏住。
部署上的教训比检索本身更大。 dev 的栈里有 Meili;发出去的 prod compose 里没有,默认走退路。于是所有搜索 e2e —— 包括专门为这条缺陷写的那个 spec —— 测的都是 prod 从不走的那条路,而且第一次跑就绿。check-search-index-shipped 现在守着这两套栈不再分叉(deployment、mechanical-guardrails)。
已关闭(2026-08-31,bd45353f4): corpus_search 以前在分词器表示不了查询时返回一个裸 [],告诉 agent 的是"这儿没有",而不是"这条路看不了你这个查询"。现在结果带一个 note 字段,只在空结果时填上,说明空不等于语料里没有这个话题,并点名 corpus_grep(穷举、字面)是该换的那条路(backend/internal/corpus/usecase/corpus_search_wire.go:41;工具描述也在开头就这么说,corpus_index_tooldesc.go:30;守卒是 e2e/test/corpus-search-says-when-it-cannot-see-the-query.spec.ts)。有命中时永远不贴这条 note——每条结果都提醒一遍就是噪音。