perf(cite): cbeta_id 索引消除全表扫描;置信度恒为 1.00 时不再逐行标注 - #6
Merged
Conversation
The index-build notice used to print right after the read-write open succeeded, before CREATE INDEX ran. That open only proves the database file itself is writable — SQLite defers creating its rollback journal until the first write, so a directory that blocks new files still lets the file open cleanly. Whenever a stale data.sqlite.lock already existed (left by any earlier cite/data update run), this printed the notice on every single cite call while the index silently never got built. Move the eprintln! to fire only once create_cbeta_index succeeds, and rewrite it in the completed tense. Replace the comment that asserted the false open-time premise with the real reason: writability can't be predicted before the write, so the notice tracks actual success instead. Add a regression test reproducing the leak (pre-existing lock file + read-only directory), a positive-control assertion that the first successful build does announce itself, and stderr/index assertions on the existing read-only test that previously passed for the wrong reason.
The cite_degrades_gracefully_when_the_data_dir_is_read_only test verifies an early fallback path where lock-file creation fails due to directory permissions. Because this path exits before index building, its assertions are trivially satisfied regardless of wiring. Document this limitation to prevent misinterpretation as a proof of correct index integration.
Finding 1: Fixed false example in conf_tag documentation comment. - Changed "0.995 would render as '1.00'" to "0.9951 would render as '1.00'" - 0.995 actually formats to "0.99" in IEEE 754 f64 - Verified no other false statements in the comment Finding 2: Added parallel text assertions to two tests to prevent false positives. - confidence_rounding_to_one_hides_the_tag: now checks both MITRA absence and parallel presence - absent_confidence_shows_no_tag: now checks both MITRA absence and parallel presence - Mirrors pattern used in perfect_confidence_shows_no_tag
懒建索引把查询路径变成了写入目标,却没有把候选路径那套边车纪律带过来。 `CREATE INDEX` 在 588 MB 数据上溢出页缓存后,日志会带着 magic 头落盘; 进程若死在这个窗口里,只读连接从此回滚不了它: - `cite` / `parallel` / `data verify` 一律退出 1,还谎称 dataset incompatibility 并建议 `data update`; - `data update` 无效 —— rename 只搬主文件,陈旧日志原地不动; - 随后任何读写打开都会把旧日志重放进新库,588 MB 静默损坏; - `data clean` 也不管活跃路径的边车,清完只剩一份孤儿日志。 三处修复: 1. `open_read_only_db` 打开后先做一次真实读取。被 SQLITE_READONLY_ROLLBACK 挡住时,以 SQLITE_OPEN_READ_WRITE(不带 CREATE)重开一次,让 SQLite 自己 回滚。区分"热日志"与"真正只读安装"的唯一判据就是这次重开后的读取能否 成功:只读文件会被内核拒绝而静默降级为只读打开,只读目录则删不掉日志, 两者都失败,于是回落到原有错误路径。全程不删任何文件。 2. `clean_data` 与 `install_candidate` 复用同一套边车纪律清理活跃路径的 `-journal`/`-shm`/`-wal`。前者主文件正被删除,后者主文件正被整体替换, 且两者都在 operation lock 之下 —— 只有这两处安全,查询打开路径绝不能删。 3. 该状态下的文案改为说明真实原因(上次写入被中断、回滚需要写权限)并给出 可操作建议,不再报不兼容、不再劝人重下 183 MB。 顺带把 `CBETA_INDEX_DDL` 与 `CBETA_INDEX_NAME` 收敛到同一个字面量,索引名 不可能再分叉;README 的体积数字随全新安装自带索引更新为 578 MB / 761 MiB, 并限定"首次 cite 补建索引"只适用于升级前已下载的数据。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
两项独立的小改进,共同点是都在修正"实测暴露、但没人报告过"的问题。
1.
cite不再全表扫描。parallels表除 FTS 外没有任何索引,cite的WHERE cbeta_id = ?1 COLLATE NOCASE因此扫描全部 908,620 行——实测端到端 2.34 s。现在建(cbeta_id COLLATE NOCASE, juan_num, id)索引;COLLATE NOCASE不可省,BINARY 索引服务不了该比较的等值查找,实测只能退化成索引扫描。索引是对已下载产物的本地优化,不进
schema.sql:那是兼容性校验的参照物(所有已下载的数据文件都没有这个索引,把它列进去会让"缺索引"看起来像"数据不兼容"),而且该文件会被 Python 导出管线逐字执行,索引若在 schema 里会在 908,620 行插入之前建立、显著拖慢导出。两条创建路径:安装/更新时在候选文件上建好(失败传播并触发候选清理,不发布半成品);已有数据在首次
cite时补建(失败一律静默退回全表扫描)。提示只在索引真的建成之后才打印。实测:首次
cite0.632 s(含建索引,数据目录 +17.2 MB)→ 之后 0.001 s,约 632 倍。有无索引的输出diff完全一致。2. 恒为 1.0 的置信度标签不再逐行输出。 全库 908,620 行的
confidence都是 1.0,于是每行末尾的[MITRA 1.00]不携带任何信息。现在标签只在能显示出非1.00的数字时才出现;判据是格式化后的字符串而非原始浮点,避免 0.9951 这类值"显示成[MITRA 1.00]却又被当作有信息量"的自相矛盾。--json的confidence字段与query.rs里按置信度排序的逻辑一律未动(后者现在是空转,但对未来有真实分数的数据是正确的)。一个在审查中发现并修掉的严重缺陷
懒建索引让查询路径第一次向活跃数据库写入——此前
parallel/texts/cite全都只读打开。这两件事各自没问题,合起来不安全:数据库是 rollback-journal 模式。17 MB 的
CREATE INDEX打在 588 MB 文件上会超出默认 2 MB 页缓存 → 溢出 → 同步日志并写 magic header → 日志变"热"。进程若在该窗口死亡(Ctrl-C、agent 超时、OOM、断电),热日志留在磁盘,而只读连接无法回滚它。后果链已被复现:所有查询 exit 1 并报"dataset incompatibility";data update无效(替换只 rename 主文件,从不碰-journal);随后旧日志会被重放进刚下载的新库,造成database disk image is malformed;data clean同样无效(清理逻辑只匹配候选/下载临时文件的 sidecar)。唯一出路是手动rm data.sqlite-journal,而工具里没有任何提示。三项修复:
SQLITE_OPEN_READ_WRITE(不带CREATE)重开让 SQLite 自己回滚,再转回只读。代码从不自己删日志;不带CREATE意味着自愈不会凭空造出空库。判据不是"重开是否成功"而是"重开后能否真的读一次"——SQLite 在内核拒绝O_RDWR时会静默降级为只读并返回成功(sqlite3.c的unixOpen),只看 open 会把真正只读的安装一并吞掉。真只读安装仍得到原有错误。clean_data、install_candidate替换前后),使data clean/data update成为真正的恢复手段,且永不把新库发布到一份外来日志之下。该函数是pub(super),"只在这两处删"由模块系统强制——查询打开路径上不存在任何 sidecar 删除。Verification
本机默认 stable 为 1.88,低于 MSRV 1.95,故用
rustup run指定工具链;MSRV 检查用的正是 1.95.0。真实数据验证(588 MB / 908,620 行 / 1016 部 Taishō):
cite(含建索引)citediff[MITRA出现次数.lock残留)并发经实测:6 个
parallel读进程在建索引期间并发,全部退出 0、stderr 干净。Checklist
cargo +stable fmt --all --checkpasses.(以 1.96.0 代 stable,理由见上)cargo +stable clippy --all-targets --locked -- -D warningspasses.(同上)cargo +stable test --all --lockedpasses.(同上)cargo +1.95.0 test --all --lockedpasses when the change affects Rust code or dependencies.CONTRIBUTING.mdpass.(normalize.rs与data-pipeline均未改动,归一化 Rust/Python 一致性契约未受影响)Cargo.lockupdate and retain the MSRV.(未新增任何依赖,Cargo.toml/Cargo.lock未改动)DATA_LICENSE; code changes are acceptable under MIT OR Apache-2.0.已知遗留(建议另开 issue,均不阻塞)
update_data_publishes_without_the_previous_datasets_journal分不清替换前/后两个清理调用点(删掉任一仍绿)。SQLITE_IOERR_DELETE)无自动化测试——那是自愈真正改写主库的唯一分支,目前只有手工验证。is_hot_journal_error的字符串兜底比需要的宽(rusqlite 无条件启用扩展码,只比extended_code即可);今日不会误触发,但安全性依赖注释约定。busy_timeout:两进程同时撞上热日志时,失败方会打印"当前数据不可写"的诊断(瞬态,重跑即好)。fojin data status走读写路径,真只读 + 热日志时仍抛原始英文错误(非回归;危险的一半已由 sidecar 清理闭合)。texts仍是全表扫描(约 0.9 s)。本轮明确排除:加索引要付 +34 MB 而只省 0.8 s,而它真正的病因是把全部分组取回 Rust 再按归一化标题过滤——预聚合小表才是根治,但那需要重新导出数据。examples/claude/commands/parallel.md指示 agent 向用户展示confidence,而该值全库恒为 1.0——与本次从人类输出去掉标签的初衷不一致。