想测一个向量数据库,最省事的办法是什么?
写几行脚本,随机生成一批向量,灌进去,建索引,再发一批查询。数据量对了,维度对了,参数也对齐了,看起来挺公平。
接下来,看到召回率上不去,就开始调参数;调完还是不理想,心里可能已经有了判断:这个数据库的向量检索不太行。
先别急。你刚刚测到的搜索难度,和业务里的文本检索,是一回事吗?
这次在 seekdb 上做的对照实验,就从这个问题开始。数据库、数据规模、向量维度、建图参数都保持一致,只换向量的来源。结果,召回曲线差了很远。
先说清楚:这三组向量各自想回答什么
真实文本向量:贴近这次要测的检索场景。
把公开问答数据中的段落和问题交给同一个文本编码模型,变成向量。查询来自真实问题,库里放真实段落,保留问题与文本之间原有的联系。这一组回答:在这份文本检索负载上,索引能找回多少精确近邻?
随机生成的向量:看看常见的“随手造数据”测出了什么。
直接随机生成一批数,再把每条向量的长度统一,条数和维度与真实组对齐。查询也单独随机生成。这一组方便、可重复,适合做合成测试。放在这里,是为了检验它能否代替上面的真实文本负载。
打乱结构的向量:再追问一步,真实数据里的数值关系重要吗?
从真实向量出发,把每个位置上的数字分别在不同向量之间打乱,再统一长度。原来属于同一条文本的那些数字被拆散了,查询也单独这样处理。这样做,是想观察:保留一部分数值分布特征、破坏原有组合关系后,结果会怎样?
所以,这里既有真实负载,也有两个目的不同的对照。具体怎么生成、怎么打乱,放在文末,先看结果。
同样的搜索参数,找回来的差这么多
先把指标翻译成人话:先在数据库外用穷举得到每个查询最相近的 10 条向量,再检查近似索引找回了其中多少。这个比例就是下面的ANN Recall@10。每组都和自己的精确答案比较。
图 1:seekdb 的同一套索引配置,搜索参数 ef_search 固定为 80。柱越长,找回的精确近邻越多;细误差棒为 95% bootstrap 区间。这里评估近邻查找,不是问答正确率。
真实文本向量找回了98.03%,随机生成的向量是24.11%,打乱结构的向量是29.13%。
只看随机组,很容易得出“这个参数下召回很差”的印象。把同样的参数用到这份真实文本数据上,看到的却是另一条曲线。
这三个结果都是真实测量。问题在于:只把条数、维度和索引参数对齐,还不能让随机数据代表真实业务。
而且,这个差距并没有伴随同样悬殊的单次延迟。在这个参数点,三组的客户端 p95 分别是 0.640、0.698、0.733 毫秒。p95 可以理解为:约 95% 的查询在这个时间以内完成。具体计时范围见附录。
为什么换了数据,索引就像换了一道题?
想象一下,你在找一段讨论数据库索引的文字。
文本编码模型的目标,是让意思相关的文本在向量空间里更接近。具体能做到什么程度,取决于模型和语料,但这些向量之间的关系,毕竟来自真实的语言内容。
随机生成一堆数,没有复制这层关系。即便维度、长度和数据量一样,“谁和谁挨着”“查询附近的候选有多难区分”,也可能完全不同。
HNSW 会沿着近邻图逐步寻找更接近查询的位置,因此数据的邻接关系会影响搜索。可以把它理解为:路线图变了,同一个搜索设置面对的路况也变了。关于算法本身,可参见 HNSW 原论文。
这次还检查了真实数据里的近邻关系:真实组中,查询与最近邻的平均 cosine 相似度更高;排在第 10、11 位的邻居之间,差距也更大。换句话说,这份数据中,最靠近查询的那些点更突出,前 10 名的边界更容易区分。
打乱结构后,均值向量的长度与真实组仍然很接近,近邻关系和召回表现却明显变了。这提醒我们,光看几个数值统计,未必能看出完整的搜索难度。
这些检查给出了和结果相符的线索。不过,我们没有测量搜索轨迹、内在维度或聚类程度,不能宣称“已经证明某一种几何性质导致了全部差距”。完整诊断值放在附录。
多找一会儿,能把差距补回来吗?
前面只看了一个参数点。选型时还得看整条曲线。
把 ef_search 当作一个控制搜索范围的旋钮就好:通常调大以后,搜索更充分,也会增加开销。它不是“实际访问了多少个点”的计数,不同数据在同一个设置下也不一定做同样多的工作。
图 2:横轴是搜索参数,从左到右逐步调大。上图越高,精确近邻找回得越全;下图越低,客户端 p95 延迟越小。三组各测了 7 个参数点,误差棒为 95% bootstrap 区间。
随着搜索范围扩大,三组召回率都在提高,但到达同一个目标所需的条件不同。
以95% 近邻召回为目标,真实组在这次测过的参数点里,首次达标的是 ef_search=80。前一个点 ef_search=40 是 94.79%。
两个对照一路调到 ef_search=640,仍然没有过线:随机组为 80.94%,打乱组为 84.31%。同一参数下,真实组是 99.97%。
再把速度和召回放到一起看:
图 3:越靠左,延迟越低;越靠上,近邻找回得越全。每个点都是实测结果,横纵误差棒对应延迟与召回的 95% 区间。连线只连接已测点,没有推算未测参数。
这也决定了本次实验不能给出一个很诱人的说法:“达到同样召回率,快了多少倍。”
因为两个对照根本还没有达到 95%。要比较相同召回目标下的速度,就得继续扩大搜索范围或调整建图参数,让各组先到达目标。没达标,就写没达标。
下次测试,随机数据可以留,真实负载要补上
随机向量有它很合适的位置:检查接口能不能跑通、维度是否正确、索引是否生效,做可重复的回归测试,或专门构造压力与边界场景。明确告诉读者“这是某一种合成负载”,这些结果就有清楚的用途。
但如果你要据此选数据库、做容量规划,建议至少再补上这些步骤:
- 带上真实查询和真实语料。尽量接近目标业务,保留两者的关系,记录编码模型和预处理方式。
- 先确认索引准备好了。异步建索引时,写入成功不等于索引已经可测;同时检查状态和查询计划。
- 把召回曲线和延迟曲线一起画出来。先设业务能接受的召回目标,再比较达标配置的开销,保留没有达标的点。
- 逐步加回业务条件。过滤、正文 payload、并发、更新、网络和缓存状态,都要在真实部署条件下继续测。
这次实验的范围也很具体:一份英文问答语料、一个模型、一种 HNSW 配置、一台机器上的静态小规模数据。它没有证明“真实向量总比随机向量好搜”,也不能直接替你预测另一个业务。
它说明的是:随机向量测出来很难搜,不能直接推断你的真实文本也一样难搜。
下次看到一张向量数据库跑分图,除了问“多少条、多少维、什么参数”,再多问一句:这些向量,和我要搜的东西有多像?
复现附录
1. 数据和对照怎么做
三组都是30,000 条语料向量、384 维、500 个正式查询,另有 100 个查询用于预热和 pilot。向量为 float32,使用 cosine 距离。
真实组从 HotpotQA dev distractor 上下文中抽取 30,000 个去重段落。先保留上述 600 个问题的 1,196 个支持段落,再从其余段落中按固定种子补足。标题和正文一起用 all-MiniLM-L6-v2 编码,再做 L2 归一化,也就是把向量的整体长度统一。数据背景见 HotpotQA 原论文 和 官方数据与许可;模型信息见 固定版本模型卡。
随机组为语料和查询分别生成独立高斯随机向量,再做 L2 归一化。这就是正文所说的“随机生成的向量”,对应证据数据里的gaussian。
打乱组对真实矩阵的每一列独立打乱行次序,语料与查询分开处理,再做 L2 归一化,对应shuffled。这会打散跨坐标的联合结构;如果仅对所有向量做同一个坐标置换,cosine 距离完全不变,起不到本次对照的作用。打乱当下,每列的经验边际分布不变;重新归一化后各分量会再变化,不能声称最终精确保留了每维边际。
这是构造的同分布问答负载,支持段落被有意保留。查询与语料没有文本完全重合,但不能据此把它称为跨领域测试。模型训练数据与公开数据的潜在重叠也无法排除。
2. 数据库和机器配置
| 项目 | 本次实际配置 |
|---|---|
| 引擎 | seekdb SERVER 1.4.0.0;HNSW / VSAG |
| 连接与表 | PyMySQL,本机回环连接;表中仅有 ID 和向量,没有正文 payload、过滤或重排 |
| 建图参数 | M=16;ef_construction=256 |
| 搜索参数 | ef_search=10、20、40、80、160、320、640 |
| 截断 | 1,147 / 30,000 段落超过 256 token(3.8233%);600 个查询均未超长 |
| 编码依赖 | sentence-transformers 5.1.1;transformers 4.56.2;torch 2.8.0+cpu |
| 随机种子 | 20261010;派生种子和插入次序见脚本;未控制引擎内部所有随机性 |
3. 开始计时前,先排除哪些假差异
确认异步索引就绪。每次装载后,执行CALL dbms_index_manager.refresh(),再检查索引计数与表行数一致、is_complete=1、sync_fail_cnt=0,并保留 DDL、索引信息和等待时间。表行数达到 30,000 不单独构成通过条件。官方接口对阻塞刷新行为和版本要求的说明见 refresh_index 文档。
确认存进去的向量没变。全量回读实际存储的 float32 分量,与准备阶段数组逐值比对。为避免默认字符串输出舍入,通过 ARRAY_MAP 将分量转换为 DOUBLE 后导出。
独立计算精确答案。在数据库外穷举排名,float64 累加并显式处理范数,不把 float32 归一化后的范数视为精确的 1。每个分布、每次构建再抽查 10 个查询,验证数据库精确 SQL 与独立真值一致。
确认查询真的走了正确路径。ANN 计划必须出现 idx_vec 的 VECTOR INDEX SCAN;精确 SQL 不能走该 ANN 路径。每次设置 ef_search 后回读生效值。正式记录中的 324 次 ANN 计划检查、9 次精确计划检查、324 次 ef 回读均通过,返回数量、重复 ID 和范围检查也通过。
4. 召回、重复测量与置信区间
每组都与自己的精确 top-10 比较。主指标允许 cosine 差值不超过 10⁻⁷ 的边界并列(tie-aware),并将并列得分限制在剩余名额内,避免错误补偿遗漏的更优近邻;同时保留严格 ID Recall。ef_search=80 时,两种口径在三组中相同。
索引重新构建三次,各次使用新的插入顺序,同一次构建内三组使用相同顺序。每次构建测五轮,分布、ef_search 和轮次按块交错;块内查询顺序随机,每块先用独立的 100 个问题预热。共 315 个测量块、157,500 条查询记录,不同的评测问题只有 500 个。
图中的 95% 区间对构建与查询簇重采样,保留簇内全部五轮,bootstrap 1,000 次。真实组在 ef_search=80 的 Recall 区间为 97.60%–98.43%,三次构建均值范围为 97.94%–98.10%。只有三次构建,区间对建图随机性的估计仍有限,也不覆盖更换语料、模型或机器的不确定性。