Whoosh底层数据格式揭秘从变长整数到压缩Posting块【免费下载链接】whooshPure-Python full-text search library项目地址: https://gitcode.com/gh_mirrors/who/whoosh如果你正在使用或学习Whoosh——这个号称纯 Python 全文搜索引擎的经典库那么一定好奇过它索引出来的.pst、.trm文件里到底存了什么为什么一个纯 Python 实现也能做到不错的检索速度答案藏在它的底层数据格式里。这篇文章不贴大段代码带你从最底层的变长整数编码出发一路看到压缩 Posting 块的完整设计帮你彻底看懂 Whoosh 的存储引擎。Whoosh 索引文件里都有什么Whoosh 的索引由若干**段Segment**组成每个段在磁盘上对应一组小文件。以默认的 W3 编码器src/whoosh/codec/whoosh3.py中的W3Codec为例常用扩展名有.trm词项索引Term Index用于快速定位词项.pst词项倒排列表Postings正文索引的核心.vps向量倒排列表Vector Postings存放词向量.col按文档组织的数值列Columns这些文件由src/whoosh/filedb/structfile.py中的结构文件读写类统一封装所有整数、字符串、pickle 对象都通过它落地。而理解这套格式的钥匙就是变长整数。变长整数一个字节起步的数字压缩术普通整数固定占 4 字节可索引里的文档号、频率、偏移量大多是小数字固定宽度太浪费。Whoosh 在src/whoosh/util/varints.py中实现了经典的varint变长整数编码每个字节只用低 7 位存数据最高位作为还有后续字节的延续标志数字越小占用字节越少0~127 只需 1 字节128~16383 只需 2 字节编码器varint()会优先查一个 512 项的预计算缓存_varint_cache避免重复计算解码器read_varint()顺着延续标志逐字节累加直到最高位为 0为了让节约更彻底Whoosh 还提供了Zig-Zag 有符号变长编码signed_varint把负数映射到正数再编码小负数同样只占 1 字节。在structfile.py中write_varint/read_varint、write_svarint/read_svarint就是它的薄封装另外还有一个更快的write_tagint/read_tagint用 1 字节阈值 16/32 位兜底适合绝大多数数字都很小的场景。倒排索引的骨架TermInfo 统计信息光有数字还不够每个词项还需要一段统计头来描述自己的倒排列表。W3TermInfo用struct.Struct(!BfIBBfII)打包成固定 27 字节标志位、总权重浮点文档频率doc freq最小/最大字段长度压缩为 0~255 的单字节最大权重、最小/最大文档号搜索时评分器靠这些统计信息就能快速估算相关性甚至不必读取完整的倒排列表就能跳过不合适的块。而倒排列表本体则被切成一个个Posting 块。Posting 块压缩存储的核心单元W3PostingsWriter也在whoosh3.py中负责把同一个词项的倒排记录攒成块。默认参数是blocklimit128每块最多 128 条 posting、compression3zlib 压缩级别、inlinelimit1。整个块按以下步骤落盘见_write_block第一个块前先写入 4 字节魔数W3Bl用于标识编码器版本对 ID、权重、值做三种瘦身minify组成元组后pickle 序列化若序列化结果超过 20 字节用 zlib 以级别 3 压缩写出块头信息posting 数量、块内最大 ID、最大权重、压缩级别、最小/最大长度字节最后一块的块长度写成负数读取端据此知道列表结束ID 用差值编码同一词项的文档号是递增的_mini_ids()调用src/whoosh/util/numlists.py里的delta_encode把 [5, 9, 22] 变成 [5, 4, 13]——差值小varint 占的字节就少。解码端用delta_decode反向还原。权重按情况特判_mini_weights()很聪明如果所有权重都是 1.0直接存None一个字节如果全部相同只存一个浮点数只有真正各不相同时才存完整数组。值按定长与否处理_mini_values()根据字段格式的fixed_value_size()决定不定长值存元组定长值如 4 字节整数直接拼接成连续字节串读取时按固定步长切分即可几乎零开销。读取端的懒加载与块级跳过写入这么讲究读取端W3LeafMatcher的优化同样精彩懒加载_read_ids()、_read_weights()、_read_values()各自独立谁先被用到才触发_read_data()解压避免不必要的 zlib 解压块级跳过每个块头都携带最大 ID、最大权重、长度范围等信息。执行skip_to(docid)或skip_to_quality(minweight)时可以直接跳过整个不满足条件的块——这正是提前终止搜索能跑这么快的原因内联如果某词项的倒排记录极少finish_postings()会把它直接内联进 TermInfoset_inline连 .pst 文件都不用访问更激进的压缩武器Simple16 与 GInts除了默认的varint pickle zlib组合numlists.py还内置了两套整数数组压缩算法供不同编码器选用Simple16源自 WWW 2008 论文的经典算法把 28 位塞进一个 32 位整数一次压进最多 28 个 1 位数字提供 16 种数字个数 × 位宽的组合适合高度规则的小整数流GInts模仿 Google 的 Packed Ints每 4 个整数前写一个 key 字节用 4 个 2 位字段声明每个数占 1~4 字节小结一整套为小数字优化的格式回头看 Whoosh 的底层数据格式思路非常统一尽可能压缩小数字、延迟一切昂贵操作。变长整数负责把文档号、频率压到最小差值编码 权重特判 定长拼接负责瘦身pickle zlib 做最后一道压缩而块级统计信息 懒加载让搜索在不解压的情况下就能跳过大量无关数据。理解了这条链路你再看src/whoosh/codec/whoosh3.py、src/whoosh/util/varints.py、src/whoosh/util/numlists.py这些源码就会豁然开朗——所谓高性能纯 Python 全文搜索靠的不是魔法而是每一层数据格式的精心设计。下次遇到检索慢的问题不妨先想想是不是某个词项的高频 posting 块让你不得不解压了太多数据【免费下载链接】whooshPure-Python full-text search library项目地址: https://gitcode.com/gh_mirrors/who/whoosh创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考