LEANN 的重算式向量索引
检索架构Python向量检索有个不太好看的账:索引往往比原始数据还大。六千万条文本切块,用传统办法建索引要 201 GB,而那堆文本本身没那么大。想在自己笔记本上索引邮件、浏览记录、聊天记录,这道门槛就卡在硬盘上。
LEANN 的选择是索引里干脆不存向量,只存一张剪过的图,搜索时走到哪一跳就现算哪一跳的向量。同样那六千万条,索引降到 6 GB。
论文是 arXiv:2506.08276,伯克利那批人写的,作者名单里有 Ion Stoica、Matei Zaharia、Joseph Gonzalez。
存储开销
摘要里的原话是索引「需要存储高维嵌入和大量索引元数据,其总大小可能是原始数据(例如文本块)的数倍」。占地方的是那一堆浮点数,文本本身反倒是小头。
一条 768 维的 float32 向量是 3 KB,六千万条就是 180 GB。存储曲线跟着向量维度和条数走,跟你实际有多少字没多大关系。
LEANN 的做法是把这部分从磁盘上删掉,改成用的时候现算。论文说索引最多能缩小到原来的五十分之一,占原始数据约 5%,而检索精度和延迟保持在可比范围。仓库里给的对比表是这样:
六千万条维基文本:传统 201 GB,LEANN 6 GB,省 97%
两百一十万条 DPR:3.8 GB 对 324 MB,省 91%
七十八万封邮件:2.4 GB 对 79 MB,省 97%
三万八千条浏览记录:130 MB 对 6.4 MB,省 95%
这几个数是项目自己报的,我没复现。要当真得知道它们省略了什么:没写用的哪个嵌入模型、多少维、什么机器,而这三样直接决定分子和分母。后两行还标着是「部分个人数据」的测试结果。仓库里有 benchmarks/run_evaluation.py 可以自己跑,会自动下评测数据。
检索流程
先把查询嵌一次,这是整个流程里唯一必然发生的一次嵌入。然后进图,走一跳,需要跟哪些邻居比距离,就把这些节点的编号报出去,等向量算回来,比完再决定下一跳往哪走。
「报出去」这四个字是字面意思。后端不是自己写的,是 faiss 和 DiskANN 的分叉,加上 ZeroMQ 和 msgpack 两个子模块。C++ 那侧走到需要向量的时候,把一个装着 node_ids 的 protobuf 从 ZeroMQ 发出来,Python 那侧收到编号、查回原文、成批过一遍嵌入模型,再把向量送回去。图遍历中途是真的停在一次跨进程往返上等着的。
我第一次读到这儿以为是自己看错了目录,跨进程做热路径,通常是设计出了问题才会这样。但它解释了另外几个机制为什么必须存在。既然每一跳都要付一次模型前向的代价,那就得让需要算的点尽可能少、尽可能扎堆:一跳要的邻居攒成一批送过去,GPU 才吃得饱;两级搜索先粗后精,把昂贵的精算留给真有希望的候选;剪枝则从源头减少要走的边。这几样单看都像常规优化,放在这个前提下才是必需品。
图剪枝
论文管这套叫高度数保留剪枝。图索引的邻接表本身也占地方,如果把向量删了但邻接表膨胀起来,账就白算了。
思路是保住高度数的枢纽节点、砍掉冗余连接。近邻图里少数节点承担了绝大部分的连通性,删掉它们的边会让搜索路径变长甚至走不通,而大量低度数节点之间的边是可以省的。剪完之后用压缩稀疏行格式存,省下来的空间不会被邻接表吃回去。
后端
默认是 HNSW,走完全重算,存储省得最狠。另一个是 DiskANN,用乘积量化过的向量做图遍历、再实时重排,README 说这条路速度和精度的折中更好。
区别落到实处就是:HNSW 那条路索引里真的没有向量,DiskANN 那条留了一份压缩过的粗向量用来导航,精确距离仍然靠现算。前者省到极致,后者少几次往返。
参数
检索侧的默认值摆在 api.py 里:候选队列 complexity=64、beam_width=1、prune_ratio=0.0、recompute_embeddings=True,剪枝策略默认 global,另有 local 和 proportional 两档。
recompute_embeddings 可以关,关掉就退回普通的存向量模式,这在索引不大、又想要极限延迟的时候有意义。剪枝比率默认是零,也就是不额外剪,需要更省的时候再往上调。
顺带一提它还带一条 BM25 通路,用 SQLite 的 FTS5 建全文索引,和向量结果做融合,分词那里对中日韩做了 ngram 处理。这条跟主线关系不大,但对中文语料是实打实的差别。
安装与使用
装完之后建索引、搜索、对话是三条命令的事(下面这几条抄自 README,我没在本机跑过):
uv pip install leann
leann build my-docs --docs ./documents
leann search my-docs "这里写你想找的东西"
leann ask my-docs --interactive仓库里给了一大堆现成的接入:文件系统、Apple Mail、浏览器历史、微信、iMessage、ChatGPT 与 Claude 的会话记录、Slack、Twitter 书签。还有一个 MCP 服务,可以直接给 Claude Code 当语义搜索用,它自带的只有 grep 那种关键词匹配。
生成模型那侧支持本地引擎(Ollama、vLLM 之类)和云端 API 两种,README 的推荐是本地,理由跟整个项目一致,数据不出机器。
代价
省下来的磁盘换成了算力。
每次检索都要跑嵌入模型,所以机器得扛得动这个模型。门槛没有消失,只是从硬盘挪到了显卡,原来是装不下,现在是跑不动。
模型没就绪就搜不了。传统索引把向量落盘之后,检索本身是纯 IO 加算术,进程重启就能用;LEANN 得先把嵌入服务拉起来,那是一次冷启动。
延迟的说法要看清限定词。论文写的是 RAG 应用下延迟「可比」,不是更快。RAG 本来就要等一次大模型生成,检索多花的几十毫秒被盖住了;换成对延迟敏感的在线检索场景,这个结论不一定成立。
还有一处是我从子模块清单读出来的:真正的算法在 faiss 和 DiskANN 的分叉里,也就是 yichuan-w/faiss 和 yichuan-w/DiskANN。仓库主体是 Python 编排层,一百七十多个 py 文件,C++ 那侧要拉子模块才看得到。想读实现的话别只克隆主仓库。
要不要用,我的判断是分场景。个人机器上索引邮件、聊天记录、浏览历史这类,值得,因为那个量级传统索引根本放不下,而这些数据你本来也不会拿去查十万次。反过来,如果索引不大、机器没有像样的 GPU、或者检索延迟直接顶在用户面前,就别折腾了,recompute_embeddings=False 关掉重算之后它退回成一个普通索引,那还不如一开始就用别的。分界线大概在这批数据的索引装不装得下,装得下就没必要,装不下才是它存在的理由。