重建索引不等于每个进程都忘掉旧结果
CodeRAG 把索引保存到 SQLite,又在服务进程里缓存 BM25 对象。这样重复查询不必每次重新加载全部代码块,但也多了一份需要保持一致的状态。
假设 HTTP 服务正在使用版本 A,另一个终端通过 CLI 重建出版本 B。如果服务只在自己处理重建请求时清缓存,就看不到别的进程做出的变化,后续搜索仍可能返回已删除的代码。
当前做法是每次 HTTP 查询都核对数据库里的仓库版本。版本没变就复用缓存;变了就加载新快照。缓存命中不是“这个仓库曾经查过”,而是“缓存对应的版本仍与数据库一致”。
先得到完整快照,再替换旧快照
扫描过程中发生权限或读取错误时,如果直接把已经扫到的一部分覆盖旧索引,会让一次失败操作变成一个看似成功、实际缺文件的新版本。
项目扫描器会把关键遍历错误向上传播,不把静默漏扫当成完成;重建时以新的仓库快照替换旧快照,删除的源文件也不应继续残留在索引里。
源码变化仍需要明确触发重新索引。当前没有实时文件监听,数据库版本的检查也不会代替源码扫描。这两件事分别保证“什么时候生成新数据”和“读者是否仍在使用旧数据”。
缓存容量要说明计量口径
HTTP 缓存最多保留 8 个仓库,代码块内容累计 UTF-8 字节预算为 8 MiB。较大的仓库仍然可以搜索,只是不进入缓存。
这里不能把 8 MiB 写成进程内存上限。Python 对象、词频、倒排表和其他结构都要占空间;源内容字节数只是便于控制和观察的一项预算。
这与 MiniRedis 的网络背压有一个共同点:说“限制了内存”之前,先问限制的是哪部分,临时对象和其他缓冲有没有算进去。
不同仓库的分数不能直接拼在一起
跨仓库搜索时,一个容易想到的办法是:每个仓库各查 Top K,再把分数合并排序。但 BM25 用到了文档频率和平均长度;各仓库分别计算的分数不一定在同一标尺上。
当前实现复用已有倒排表,同时把参与仓库的代码块数量、平均长度和查询词文档频率汇总起来,使用统一统计评分。这样不必每次为合并语料重新分词和建表,也没有直接拿各仓库独立得分相互比较。
实现没有先做局部 Top K 截断,再期待全局排名总能正确。过滤条件与统一语料的统计口径也要保持清楚,否则“代码复用以后结果没变”就缺少依据。
顺序不同,会得到不同的 Top K
假设前五个最高分块里,有四个都来自同一文件。如果先截取五个,再按文件去重,就可能只剩两个文件,而后面原本还有其他相关文件可选。
unique_files 的做法是先保留各文件的最佳块,再选 Top K。最后使用堆选择,并定义分数相同时按路径、起始行等字段排序,保证重构以后同分结果的次序仍可检查。
这部分在 backend/retrieval/bm25.py,缓存逻辑在 backend/retrieval/cache.py。阅读时可以自己构造几个同分块,检查每个排序条件在什么情况下生效。
优化应该留下等价性证据
项目有专门的检索等价性测试,用来比较复用倒排表后的结果。这样的测试关注候选、过滤、去重和排序是否一致;它不证明任意业务问题都能找到正确实现。
多仓库也不是跨仓库的同一时刻事务快照。响应给出了实际参与的版本,便于解释某一次查询究竟读到了什么。
我现在更愿意把缓存问题拆成三问:数据什么时候更新,缓存怎样发现更新,多个来源是否使用同一个评分口径。回答清楚以后,再讨论快了多少才有意义。