把 3072 维截成 256 维,第一轮只取 100 个候选,再加载全维重排,这条链路很容易讲清,也很容易被误解成“既省内存又绝对无损”。关键限制是:全维重排只看得到第一轮留下来的文档。
Matryoshka Representation Learning 让表示在训练时适配不同粒度。它不是说任意一个向量模型,随便截掉后半段都保留同样的语义。MRL 原论文 的研究结果有自己的模型与任务条件,不宜直接替换成私有知识库的性能表。
两份成本账,不能只记低维索引
低维索引常驻内存,全维向量放在磁盘或另一层存储,这样才有机会减少内存驻留。若实现里仍把全库完整向量存在 Python 字典中,就没有演示存储分层。磁盘点读还会引入 I/O、缓存与并发控制成本,不是“全维点积很短,所以端到端免费”。
评测时保留三份结果:全维精确搜索作为参考、低维候选池、候选池内全维重排。第一份与第三份的差异可以帮助区分候选遗漏和重排计算的问题。再单独比较 ANN 与低维精确搜索,避免把近似索引误差也归因于维度截断。
一个只有两份文档的反例
A 与 B 在第一维相同,低维排序只能按稳定规则打破平局。如果候选池只有一个名额,可能恰好留下 A;完整向量明明能认出 B,却没有机会看到它。例子的数字是手工构造,目的是证明限制存在,不是衡量发生频率。
内图的全维存储与低维驻留是两层部署概念;本地代码为了展示排序反例使用小字典,不能据此声称已经节省真实内存。
import math def unit(v): norm = math.sqrt(sum(x*x for x in v)) if norm == 0: raise ValueError("zero vector") return [x/norm for x in v] def dot(a, b): if len(a) != len(b): raise ValueError("dimension mismatch") return sum(x*y for x, y in zip(a, b)) vectors = {"A": [0.8, 0.6], "B": [0.8, -0.6]} query = [0.8, -0.6] def cascade(candidate_k): if not 1 <= candidate_k <= len(vectors): raise ValueError("invalid candidate depth") q_low = unit(query[:1]) candidates = sorted(vectors, key=lambda d: ( -dot(unit(vectors[d][:1]), q_low), d))[:candidate_k] return sorted(candidates, key=lambda d: (-dot(unit(vectors[d]), unit(query)), d)) assert cascade(1) == ["A"] assert cascade(2)[0] == "B" assert dot(unit(vectors["B"]), unit(query)) > dot(unit(vectors["A"]), unit(query)) print("candidate_k=1 misses B; candidate_k=2 reranks B first")例子跑通以后,还要验证什么
这段代码已在本地 macOS、CPython 3.9.6 下运行,实际输出为candidate_k=1 misses B; candidate_k=2 reranks B first。它验证的是例子中的数据约束与分支,不包含线上服务的吞吐或延迟测量。
没有使用经过 MRL 训练的模型、真实磁盘向量存储或 ANN。上线方案要记录模型支持的维度、截断后归一化方式、候选保真率、全维读取字节数和冷缓存尾延迟。若目标只是减少向量字段体积,也不能把索引拓扑和服务进程的所有内存等比例减掉。
延伸阅读
- Matryoshka Representation Learning:2022 年提出的表示学习方法;本文未复现论文训练。
- Universal I/O in Qdrant:2026 年 10 月 12 日对本地与远程读取成本差异的工程说明,不引用其延迟为本文测量。