向量检索进入生产环境后,团队很快会撞上一道硬约束:提高 HNSW 的搜索深度可以改善召回率,却会消耗更多 CPU 和内存带宽;降低搜索深度能够提高 QPS,又可能让 RAG 漏掉关键文档。AlloyDB 预览中的列式引擎加速 HNSW,试图通过改变索引在内存中的访问方式,缓解这组速度与准确率之间的矛盾。
加速不只是把索引搬进内存
AlloyDB 是兼容 PostgreSQL 的托管数据库,能够同时承担关系数据、全文检索和向量检索。其列式引擎本质上是内置的内存缓存:它以适合扫描的列式格式保存频繁访问的数据,并已被用于加速分析查询。
这次针对 HNSW 的变化更值得注意,因为对比基线中的 PostgreSQL HNSW 索引已经完整缓存在 shared buffers 中。换句话说,性能差异不是简单的“磁盘对内存”,而是两种内存访问架构之间的差异。
标准 PostgreSQL 即使已经命中共享缓冲区,读取索引页时仍要经过缓冲区管理器,包括:
- 查找缓冲区表并定位页面;
- pin 和 unpin 页面;
- 获取并释放相关锁;
- 维护页面替换和 LRU 状态。
这些操作对普通索引访问是必要的,但 HNSW 查询会沿图结构频繁跳转节点。在高并发下,大量细粒度、指针密集的访问会放大缓冲区管理成本。
AlloyDB 列式引擎加速 HNSW 采用了三项针对性处理:将 pgvector HNSW 索引持续固定在列式引擎内存中,使用适合图遍历的向量化内存布局,并在遍历时绕过标准 PostgreSQL 缓冲区管理路径。因此,它优化的是“如何访问已经在内存里的索引”,而不只是提高缓存命中率。
同一条曲线上的 QPS 与召回率
向量索引不能只比较单次查询延迟。HNSW 属于近似最近邻搜索,测试时至少应同时观察:
- QPS:系统每秒能处理多少次向量查询;
- Recall@K:近似搜索返回的前 K 个结果,与精确 KNN 结果重合多少;
- 尾延迟:并发压力下的 P95 或 P99 延迟;
- 资源占用:CPU、内存以及连接数是否已成为瓶颈。
来源测试使用超过 100 万条记录的 GloVe 100 Angular 数据集,每次返回 100 条结果,运行环境为 AlloyDB C4A 16 vCPU 实例。在指定召回率约为 0.95 时,列式引擎加速版本的 QPS 约提高 4.2 至 4.9 倍。反过来看,在约 350 QPS 下,召回率从约 0.78 提高到 0.94 以上,增幅约为 0.163。
这两个观察其实描述了同一件事:数据库减少了每次图遍历的基础成本,于是应用可以把节省出来的预算用于更多并发请求,或者用于更深入的 HNSW 搜索。生产团队不应把“4 倍”当成所有数据集上的固定承诺。HNSW 建图带有随机性,向量分布、维度、索引参数、并发度和实例规格都会改变曲线。
可以这样实践:建立并缓存 HNSW 索引
下面给出一个可改造的 pgvector 示例。假设已连接 AlloyDB 数据库,并且数据库允许创建 vector 扩展。示例使用 100 维向量,与上述 GloVe 测试的维度一致;实际项目必须改成嵌入模型输出的维度。
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE IF NOT EXISTS documents (
id bigserial PRIMARY KEY,
content text NOT NULL,
embedding vector(100) NOT NULL
);
CREATE INDEX documents_embedding_hnsw_idx
ON documents
USING hnsw (embedding vector_cosine_ops);
ANALYZE documents;
在 AlloyDB 实例配置中,需要将以下两个数据库标志设为 on:
google_columnar_engine.enabled=on
google_columnar_engine.enable_index_caching=on
标志生效后,将刚创建的 HNSW 索引加入列式引擎缓存:
SELECT google_columnar_engine_add_index(
'documents_embedding_hnsw_idx'
);
应用查询仍然使用标准 pgvector SQL,不需要改成专有查询接口。把下面的数组替换为模型生成的完整 100 维查询向量:
SELECT id, content,
embedding <=> '[0.12, -0.08, 0.31, 0.04, 0.19, 0.07, 0.22, -0.11, 0.05, 0.09, 0.01, -0.03, 0.15, 0.18, -0.21, 0.06, 0.13, 0.17, -0.04, 0.23, 0.02, 0.14, -0.09, 0.11, 0.08, -0.12, 0.16, 0.03, 0.24, -0.06, 0.10, 0.20, -0.14, 0.25, 0.00, 0.05, -0.07, 0.27, 0.12, -0.16, 0.09, 0.18, -0.02, 0.21, 0.04, -0.10, 0.13, 0.06, 0.17, -0.05, 0.15, 0.01, 0.19, -0.08, 0.22, 0.03, 0.11, -0.13, 0.16, 0.07, 0.20, -0.04, 0.14, 0.02, 0.18, -0.09, 0.23, 0.05, 0.12, -0.06, 0.21, 0.00, 0.10, -0.11, 0.17, 0.04, 0.15, -0.03, 0.19, 0.08, 0.13, -0.07, 0.24, 0.01, 0.16, -0.12, 0.20, 0.06, 0.14, -0.05, 0.22, 0.03, 0.11, -0.08, 0.18, 0.02, 0.15, -0.04, 0.21]'::vector AS distance
FROM documents
ORDER BY embedding <=> '[0.12, -0.08, 0.31, 0.04, 0.19, 0.07, 0.22, -0.11, 0.05, 0.09, 0.01, -0.03, 0.15, 0.18, -0.21, 0.06, 0.13, 0.17, -0.04, 0.23, 0.02, 0.14, -0.09, 0.11, 0.08, -0.12, 0.16, 0.03, 0.24, -0.06, 0.10, 0.20, -0.14, 0.25, 0.00, 0.05, -0.07, 0.27, 0.12, -0.16, 0.09, 0.18, -0.02, 0.21, 0.04, -0.10, 0.13, 0.06, 0.17, -0.05, 0.15, 0.01, 0.19, -0.08, 0.22, 0.03, 0.11, -0.13, 0.16, 0.07, 0.20, -0.04, 0.14, 0.02, 0.18, -0.09, 0.23, 0.05, 0.12, -0.06, 0.21, 0.00, 0.10, -0.11, 0.17, 0.04, 0.15, -0.03, 0.19, 0.08, 0.13, -0.07, 0.24, 0.01, 0.16, -0.12, 0.20, 0.06, 0.14, -0.05, 0.22, 0.03, 0.11, -0.08, 0.18, 0.02, 0.15, -0.04, 0.21]'::vector
LIMIT 10;
这里使用 <=> 进行余弦距离排序,因此索引选择了 vector_cosine_ops。如果模型和业务使用欧氏距离或内积,应同时调整运算符与索引操作符类,避免索引配置和查询语义不一致。
用精确 KNN 建立召回率基线
评估 ANN 时,需要先获得精确结果作为 ground truth。可以这样实践:在隔离的测试连接中暂时禁用索引扫描,执行同一批查询,保存精确 KNN 的前 K 个 ID;随后恢复索引扫描,运行 HNSW 查询并计算结果集合的重合率。
BEGIN;
SET LOCAL enable_indexscan = off;
SET LOCAL enable_bitmapscan = off;
SELECT id
FROM documents
ORDER BY embedding <=> :'query_vector'::vector
LIMIT 100;
ROLLBACK;
上述命令适合测试环境,不应直接放进线上请求链路。完整压测还应固定数据快照和查询向量集合,预热数据库,逐级提高并发,并分别记录 QPS、P95/P99 延迟和 Recall@100。为了判断列式引擎的实际收益,普通 HNSW 基线也必须充分预热,否则测到的可能只是磁盘 I/O 差异。
上线前如何选择索引
HNSW 并非 AlloyDB 中唯一的向量搜索方式。可以按业务约束做选择:
- 需要 pgvector 兼容语法、较高召回率和低延迟时,评估 HNSW;
- 数据规模和吞吐需求进一步增大时,把 AlloyDB 的 ScaNN 一并纳入基准测试;
- 审计、去重或评测任务要求 100% 召回率时,保留精确 KNN 路径;
- 内存预算紧张时,测量缓存索引后的真实占用,不要只依据压缩率描述做容量规划;
- 功能仍处于预览阶段时,确认区域、版本、支持范围和生产可用性要求。
最稳妥的采用方式,是用自己的嵌入模型、真实查询分布和目标并发绘制 QPS 与召回率曲线。列式引擎加速 HNSW 的价值不只在峰值吞吐,它更重要的意义是把原本消耗在缓冲区管理上的预算,重新分配给召回率、并发能力或更小的计算规格。