向量检索有个不太好看的账:索引往往比原始数据还大。六千万条文本切块,用传统办法建索引要 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 可以自己跑,会自动下评测数据。

检索流程

LEANN 的一次检索

先把查询嵌一次,这是整个流程里唯一必然发生的一次嵌入。然后进图,走一跳,需要跟哪些邻居比距离,就把这些节点的编号报出去,等向量算回来,比完再决定下一跳往哪走。

「报出去」这四个字是字面意思。后端不是自己写的,是 faiss 和 DiskANN 的分叉,加上 ZeroMQ 和 msgpack 两个子模块。C++ 那侧走到需要向量的时候,把一个装着 node_ids 的 protobuf 从 ZeroMQ 发出来,Python 那侧收到编号、查回原文、成批过一遍嵌入模型,再把向量送回去。图遍历中途是真的停在一次跨进程往返上等着的。

我第一次读到这儿以为是自己看错了目录,跨进程做热路径,通常是设计出了问题才会这样。但它解释了另外几个机制为什么必须存在。既然每一跳都要付一次模型前向的代价,那就得让需要算的点尽可能少、尽可能扎堆:一跳要的邻居攒成一批送过去,GPU 才吃得饱;两级搜索先粗后精,把昂贵的精算留给真有希望的候选;剪枝则从源头减少要走的边。这几样单看都像常规优化,放在这个前提下才是必需品。

图剪枝

论文管这套叫高度数保留剪枝。图索引的邻接表本身也占地方,如果把向量删了但邻接表膨胀起来,账就白算了。

思路是保住高度数的枢纽节点、砍掉冗余连接。近邻图里少数节点承担了绝大部分的连通性,删掉它们的边会让搜索路径变长甚至走不通,而大量低度数节点之间的边是可以省的。剪完之后用压缩稀疏行格式存,省下来的空间不会被邻接表吃回去。

后端

默认是 HNSW,走完全重算,存储省得最狠。另一个是 DiskANN,用乘积量化过的向量做图遍历、再实时重排,README 说这条路速度和精度的折中更好。

区别落到实处就是:HNSW 那条路索引里真的没有向量,DiskANN 那条留了一份压缩过的粗向量用来导航,精确距离仍然靠现算。前者省到极致,后者少几次往返。

参数

检索侧的默认值摆在 api.py 里:候选队列 complexity=64beam_width=1prune_ratio=0.0recompute_embeddings=True,剪枝策略默认 global,另有 localproportional 两档。

recompute_embeddings 可以关,关掉就退回普通的存向量模式,这在索引不大、又想要极限延迟的时候有意义。剪枝比率默认是零,也就是不额外剪,需要更省的时候再往上调。

顺带一提它还带一条 BM25 通路,用 SQLite 的 FTS5 建全文索引,和向量结果做融合,分词那里对中日韩做了 ngram 处理。这条跟主线关系不大,但对中文语料是实打实的差别。

安装与使用

装完之后建索引、搜索、对话是三条命令的事(下面这几条抄自 README,我没在本机跑过):

bash
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/faissyichuan-w/DiskANN。仓库主体是 Python 编排层,一百七十多个 py 文件,C++ 那侧要拉子模块才看得到。想读实现的话别只克隆主仓库。

要不要用,我的判断是分场景。个人机器上索引邮件、聊天记录、浏览历史这类,值得,因为那个量级传统索引根本放不下,而这些数据你本来也不会拿去查十万次。反过来,如果索引不大、机器没有像样的 GPU、或者检索延迟直接顶在用户面前,就别折腾了,recompute_embeddings=False 关掉重算之后它退回成一个普通索引,那还不如一开始就用别的。分界线大概在这批数据的索引装不装得下,装得下就没必要,装不下才是它存在的理由。