AlloyDB ScaNN 用四层树把向量检索推进到百亿规模

2026-08-21 38 预计阅读时间: 1 分钟
来源: cloud.google.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:9 分钟

当企业级 Agent 把知识库、商品目录、日志和多模态内容都转成 embedding,向量总量很容易从千万跨过十亿,并继续逼近百亿。此时问题不再只是“能不能建索引”,而是索引训练是否装得进内存、构建成本是否可控,以及查询延迟会不会随数据量失去约束。AlloyDB 的 ScaNN 索引近期以预览版四层树架构应对这一挑战:在内部测试中,10 billion vectors 场景可达到 95% recall 与不高于 51 ms 的 p95 延迟。

百亿向量为什么会压垮传统树形索引

树形 ANN 索引的核心思路是先把向量空间切成簇,查询时只进入少量候选分区,而不是遍历全部向量。但层数不足时,每个分区仍然过大;层数增加得不合理时,训练、采样和遍历又会制造新的计算与内存压力。

来源所述的旧版 AlloyDB ScaNN 树主要采用两层或三层配置。以数据规模 N 粗略理解其搜索空间:

  • 两层树的基本复杂度约为 O(N^(1/2))
  • 三层树进一步缩小为 O(N^(1/3))
  • 四层树将层级分区细化到约 O(N^(1/4))

这不是说一次查询只做一个数学幂次运算,而是说明:每增加一层质量可控的分区,后续需要深入扫描的向量空间会显著收缩。对百亿级数据集而言,这种结构性削减比单纯堆更多计算资源更关键。

四层树如何兼顾召回率与构建效率

AlloyDB ScaNN 的四层树采用自顶向下的层级划分。顶层负责粗筛,向下逐级细分,最终将查询导向更小、更相关的候选区域。架构的价值不只是“多一层”,而是让树在扩大数据规模时仍维持可用的分支粒度。

来源还列出了几项用于降低召回损失、提升结构质量的增强:

  • Top-K branch:查询时保留多个高相关分支,避免过早沿单一路径决策而漏掉近邻。
  • SOAR:配合索引设计提高近邻检索效果。具体参数和行为应以实际 AlloyDB 版本文档为准。
  • centroid adjustment:调整聚类中心,减少分区边界带来的误分流。
  • balanced tree shape:构建更均衡的树形结构,防止少数超大节点同时拖慢查询和训练。

这里存在明确的工程取舍:分区越细,单次查询扫描的候选越少,但路径选择错误会更容易伤害 recall;保留更多分支可以补回召回率,却会增加计算。因此,生产配置不能只盯住延迟,必须同时验收 recall、p95/p99 和成本。

内存不是附属问题,而是百亿构建的前提

在 10 billion vectors 场景,训练样本本身就可能超过可用内存。四层树通过平衡树形与采样优化降低这一风险:当内存受限时,系统使用压缩后的采样集合,在性能与精度之间做权衡;均衡的层级结构则让较小样本仍能形成质量较高的分区。

这意味着“把全部原始向量放进训练集”并不是必需目标,甚至通常不可行。更可靠的目标是让样本覆盖数据分布、避免热门类别淹没长尾类别,并在真实查询集上持续验证召回。对持续写入的数据,索引构建窗口、增量维护策略和重建触发条件也需要纳入容量规划。

可以这样实践:先建立可比较的 SQL 基准

四层树是 AlloyDB ScaNN 的预览能力,具体 DDL、索引选项和可用区域可能随版本变化。下面示例刻意只建立一个可复用的 PostgreSQL 兼容基准骨架:将 embedding 列、距离算子与索引创建语句替换成当前 AlloyDB ScaNN 文档要求的语法,再用同一批查询评估不同树配置。

-- 假设:当前实例已启用所需的向量扩展或 AlloyDB 向量能力。
CREATE TABLE document_chunks (
  id BIGINT PRIMARY KEY,
  tenant_id BIGINT NOT NULL,
  content TEXT NOT NULL,
  embedding vector(768) NOT NULL
);

-- 仅用于验证 SQL 检索链路;生产索引请替换为当前 AlloyDB ScaNN 官方语法。
-- CREATE INDEX document_chunks_scann_idx
--   ON document_chunks USING scann (embedding <distance_operator>)
--   WITH (<current_scann_options>);

-- 使用参数化查询传入 query_embedding,并限制租户边界。
SELECT id, content, embedding <=> $1 AS distance
FROM document_chunks
WHERE tenant_id = $2
ORDER BY embedding <=> $1
LIMIT 20;

执行前应确认三件事:向量维度必须与模型输出一致;距离度量必须与 embedding 模型训练方式匹配;多租户过滤条件是否能与向量检索共同获得合理执行计划。不要把示例中的注释索引语句直接当作预览版 API 契约。

还可以用下面的 Python 脚本生成可重复的查询负载。它不依赖某个专有 SDK,适合先验证应用端连接、参数绑定和延迟采集;运行前设置 DATABASE_URL,并安装 psycopg[binary]numpy

import os
import time
import numpy as np
import psycopg

DSN = os.environ["DATABASE_URL"]
DIM = 768
TENANT_ID = 42

def unit_vector(dim: int) -> list[float]:
    v = np.random.default_rng().normal(size=dim).astype(np.float32)
    v /= np.linalg.norm(v)
    return v.tolist()

query = unit_vector(DIM)
sql = """
SELECT id, embedding <=> %s::vector AS distance
FROM document_chunks
WHERE tenant_id = %s
ORDER BY embedding <=> %s::vector
LIMIT 20
"""

with psycopg.connect(DSN) as conn:
    with conn.cursor() as cur:
        started = time.perf_counter()
        cur.execute(sql, (query, TENANT_ID, query))
        rows = cur.fetchall()
        elapsed_ms = (time.perf_counter() - started) * 1000

print(f"returned={len(rows)} latency_ms={elapsed_ms:.2f}")

该脚本测到的是端到端客户端观察延迟,不等同于服务端索引延迟。正式压测应固定查询集、预热连接池、分别记录 p50/p95/p99,并用精确近邻结果或可信标注集计算 recall@K。

上线前的检查清单

  • 用真实业务查询验证 recall@K,而不是仅用随机向量。
  • 分别观察过滤查询和无过滤查询;租户、权限、时间范围等条件会改变候选集与执行计划。
  • 明确索引构建和重建对写入吞吐、维护窗口与成本的影响。
  • 将内存限制下的采样质量作为监控对象,特别关注数据分布漂移和长尾召回。
  • 在预览能力进入关键路径前,确认区域支持、版本行为、配额和回退方案。

四层树解决的是百亿向量搜索中的一个基础矛盾:通过更深但经过平衡设计的层级,压缩每次查询的有效搜索空间,同时避免训练阶段因采样而耗尽内存。它不替代正确的 embedding、数据分片和评估体系,但为 PostgreSQL 兼容数据库承载超大规模 Agent 检索提供了更现实的索引路径。


相关推荐