全文搜索(每本书一个 FlexSearch 索引,磁盘持久化)
父级:key-designs
core/src/search/(SearchEngine、SearchIndex、SearchService、SearchIndexStore、aggregateConsecutive,以及 searchTokenize/searchIndexVersion)。
每本书一个 FlexSearch 索引,粒度是句子。 每条 CachedSentence 的显示文本就是一个文档;一次查询返回按相关度排序的句子命中,aggregateConsecutive 再把它们折叠成连续的段落(这样一个跨两句话的短语会读成一条结果,而不是两条)。结果携带句子的原始偏移量,因此从一次搜索命中跳转时,会通过 coordinate backbone 落到正确的页面上。
"恢复而不是重建"是承重的那个决定。 把整本书倒进一个全新的 FlexSearch 索引开销很大,所以索引会被序列化成一个磁盘 blob(通过 SearchIndexStore 的 exportSections/importSections),以内容哈希 + SEARCH_INDEX_VERSION 为键。重新打开书时,importSections 能在 500 毫秒内恢复索引,而不必重新分词建索引;版本号一旦提升,旧 blob 就作废并触发重建。一个门面层(markBuilding/attach/detach)把索引的生命周期和分页协调在一起(两者消费的是同一条句子流),这样搜索就不会自己再单独分词一遍。
它是支柱而不是一个可有可无的勾选项,原因在于:一个重度读者会不停地搜索,而书大到每次打开都重新建索引是无法接受的——所以"持久化 + 恢复 + 版本号"这套机制才是实质所在,这和 dynamic-pagination 以及句子缓存"版本化并恢复、而不是重新计算"的做法完全对称。