使用 SVS Vamana 和 Milvus 进行搜索
SVS(可扩展向量搜索)是英特尔实现 Vamana 图算法的高性能向量搜索库。 在 Milvus 中,您可以通过FAISS 索引直通功能访问 SVS Vamana:将 `index_type ` 设置为 `FAISS `,并提供一个 `SVSVamana ` 索引生成器字符串,Milvus 便会通过 FAISS 的 SVS 集成来构建和搜索索引。
Vamana 是DISKANN同样采用的图结构,但 SVS Vamana 将整个图保存在内存中。它还可以使用LeanVec或LVQ 对向量进行压缩,这既能减少内存占用、加快搜索速度,又能保持极高的召回率。
先决条件
- Milvus 3.0.0 或更高版本,需从源代码编译且启用 SVS
- PyMilvus 3.0.0 或更高版本
- 若使用
LVQ*或LeanVec*存储,需配备英特尔CPU;FP32、FP16和SQ8存储可在任何平台上运行 - 熟悉FAISS 索引工厂字符串
SVS 未包含在 Milvus 的默认构建中。若要使用SVSVamana 工厂,请通过make USE_SVS=ON 从源代码构建 Milvus。在未包含 SVS 的构建中,索引构建会失败,因为 Faiss 无法识别该工厂字符串。
LVQ 和 LeanVec 压缩仅在 Intel CPU 上运行,且在支持 AVX-512 的 CPU 上运行速度更快。在其他 CPU 上,使用LVQ* 和LeanVec* 工厂字符串时,索引构建将失败。
SVS Vamana 的工作原理
Vamana 图
SVS Vamana 构建一个单层邻近图。每个向量是一个节点,边连接可能为近邻的向量。为了构建该图,SVS 对每个节点x 执行以下操作:
贪婪搜索:SVS 搜索当前已构建的图,并维护一个大小为
construction_window_size的候选列表。邻居剪枝:已访问的节点成为x 的候选邻居。SVS 按它们与x 距离由小到大的顺序进行考虑。 如果存在一个距离更近且已被保留的邻居
n,且满足alpha * distance(n, c) <= distance(x, c),则跳过候选邻居c。这保留了长距离边并剔除了冗余的短距离边,因此搜索可以在图中跳跃,而不是一次只移动一个最近邻。当图的度数达到上限时,剪枝过程停止。反向边:对于x 的每个新邻居,SVS 会添加一条指向x的反向边,并剪枝该邻居的边列表(若其长度超过图的度数)。
剪枝过程分两轮进行:首先使用alpha = 1 ,然后使用配置的alpha 。更大的图度和construction_window_size 会生成更密集、质量更高的图,但会消耗更多内存且构建时间更长。
在搜索阶段,SVS会贪婪地遍历图,并保留迄今为止观察到的距离查询最近的候选节点(search_window_size )。它总是优先探索距离最近且尚未访问的候选节点。当当前节点的所有邻居与查询的距离均大于窗口内最差候选节点的距离时,搜索即告结束。更大的search_window_size 值会探索更广范围的图,这虽能提高召回率,但也会增加延迟。
LeanVec 压缩
LeanVec同时减少了每个向量的维度数和每维度的位数。LeanVec 索引为每个向量保存两种表示形式:
主表示(降维):将向量线性投影至较少维度的结果。图搜索在该主数据集上进行,因此大部分距离计算都在低维空间中完成。这既降低了计算需求,也减少了内存带宽需求。默认情况下,降维后的维度为原始维度的二分之一(
dim / 2)。次要表示(全维度):向量在原始维度的表示形式。SVS 会针对该次要数据集,对主要图搜索返回的候选项进行重新排序,以弥补因降维而损失的准确率。
这两种表示形式均采用LVQ(局部自适应向量量化)进行量化。工厂字符串中的存储组件负责选择编码方式。例如,LeanVec4x8 针对主数据集采用每维 4 位,针对次级数据集采用每维 8 位。即使在重新排序步骤中,也绝不会读取全精度向量。
默认情况下,LeanVec 会分别针对索引向量和查询学习独立的投影,以支持来自与索引向量不同分布的查询(即分布外查询,OOD)。 这在跨模态工作负载(如文本到图像检索)中效果尤为显著,当查询与索引向量来自同一分布时同样表现良好。Milvus 目前尚不支持将查询作为投影计算的输入,因此当前仅使用基础向量。OOD 训练会增加构建时间。 若需以牺牲部分查询性能为代价来加快构建速度,请将 `ood_training ` 设置为 `False`。
LVQ 压缩
对于维度少于约 512 的数据集,跳过降维并仅使用 LVQ 量化进行压缩可能效果更好。 与 LeanVec 类似,LVQ 索引会保留用于快速搜索的主表示,以及用于重新排序的残差表示。例如,LVQ4x4 为主表示分配 4 位,为残差分配 4 位。LVQ4x0 和LVQ8x0 会跳过重新排序步骤,如果其召回率足以满足您的工作负载需求,则可加快查询速度。
选择工厂字符串
SVS Vamana 的工厂字符串形式为SVSVamana<degree>[,<storage>] ,其中<degree> 表示图中每个节点的最大边数。
| 工厂字符串 | 存储 | 注释 |
|---|---|---|
SVSVamana64 | FP32 | 未压缩向量。 |
SVSVamana64,FP16 | 16 位浮点数 | 仅需FP32 一半的向量内存,且精度损失极小。 |
SVSVamana64,SQ8 | 8 位标量量化 | 仅需FP32 四分之一的向量内存。 |
SVSVamana64,LVQ4x0,LVQ8x0 | LVQ为主,无残差 | 仅限英特尔CPU。不进行重新排序。 |
SVSVamana64,LVQ4x4,LVQ4x8 | LVQ为主,带残差 | 仅限英特尔CPU。 |
SVSVamana64,LeanVec4x4,LeanVec4x8,LeanVec8x8 | LeanVec 主分级和次分级 | 仅限英特尔 CPU。添加后缀_<dim> 可设置降维维度。例如,LeanVec4x8_128 将主数据集降维至 128 维。若不添加后缀,降维维度默认为dim / 2 。 |
SVSVamana64,LeanVec4x8 是一个不错的起点。在此基础上:
若需在一定内存开销下提高召回率,请使用
_<dim>后缀增加LeanVec的维度。若需在牺牲部分召回率的前提下减小内存占用,请降低 LeanVec 的维度。
对于低维向量,请尝试使用
LVQ8。在非英特尔 CPU 上,请使用
FP16或SQ8。
构建索引
以下示例构建了一个图度为 64 且采用 LeanVec 压缩的 SVS Vamana 索引。在构建索引之前,请展开“设置”代码块并运行它。
import random
from pymilvus import DataType, MilvusClient
client = MilvusClient(uri="http://localhost:19530")
collection_name = "svs_vamana_example"
if client.has_collection(collection_name):
client.drop_collection(collection_name)
rng = random.Random(42)
vectors = [[rng.random() for _ in range(256)] for _ in range(3000)]
schema = client.create_schema(auto_id=False, enable_dynamic_field=False)
schema.add_field("id", DataType.INT64, is_primary=True)
schema.add_field("vector", DataType.FLOAT_VECTOR, dim=256)
client.create_collection(collection_name=collection_name, schema=schema)
client.insert(
collection_name=collection_name,
data=[{"id": i, "vector": vector} for i, vector in enumerate(vectors)],
)
client.flush(collection_name=collection_name)
将index_type 设置为FAISS ,并使用faiss_index_name 选择SVS Vamana配方。
index_params = client.prepare_index_params()
index_params.add_index(
field_name="vector",
index_name="svs_vamana_index",
index_type="FAISS",
metric_type="L2",
params={
# SVSVamana64 -> Vamana graph degree 64
# LeanVec4x8 -> 4-bit primary, 8-bit secondary
# _128 -> LeanVec dim 128 (omit the suffix for dim / 2)
"faiss_index_name": "SVSVamana64,LeanVec4x8_128",
"construction_window_size": 200, # build-time search width
"ood_training": True, # OOD on (default)
},
)
client.create_index(collection_name=collection_name, index_params=index_params)
client.load_collection(collection_name=collection_name)
搜索索引
search_window_size 控制召回率与延迟之间的权衡,就像ef 对HNSW的作用一样。请将search_buffer_capacity 设置为至少与该值相同。这两个参数的默认值均为10,且当search_window_size 大于search_buffer_capacity 时,SVS将拒绝该搜索请求。
search_params = {
"params": {
"search_window_size": 100, # wider = higher recall, lower QPS
"search_buffer_capacity": 100, # must be >= search_window_size
}
}
res = client.search(
collection_name=collection_name,
data=[vectors[0]],
anns_field="vector",
limit=10,
search_params=search_params,
)
如果limit 大于search_window_size ,SVS 会将该搜索的窗口值提高到limit 。
SVS Vamana 参数
以下构建参数应放置在params 文件中(位于add_index() 目录下),紧邻faiss_index_name 文件。索引构建完成后,您将无法更改这些参数。
| 参数 | 默认值 | 描述 |
|---|---|---|
construction_window_size | 40 | 在图构建过程中,用于查找每个节点邻居的候选项列表大小。较大的值可提高图质量和召回率,但构建时间会更长。请将其设置为至少等于图的度数。200 是常见的选择。 |
alpha | 1.2 对于L2 ,请参见0.95 ;对于IP ,请参见COSINE | 修剪强度。数值越远离1,修剪越激进,更倾向于保留长距离边而非冗余的短距离边。对于L2 ,请使用大于1的数值;对于IP 和COSINE ,请使用小于1的数值。除非您在数据上测得更优的数值,否则请保留默认值。 |
graph_max_degree | 工厂字符串中的度数,或32 | 每个节点的最大边数。该设置将覆盖工厂字符串中的度数。值在 [32, 128] 范围内的设置适用于大多数工作负载。较高的值可以提高召回率,但会消耗更多内存且构建时间更长。 |
prune_to | 图的度减去 4 | 节点超过图度数后,在修剪后保留的边数。 |
max_candidate_pool_size | 200 | 修剪节点邻居时考虑的候选节点最大数量。该值应大于construction_window_size 。 |
use_full_search_history | True | 修剪时是否考虑构建搜索过程中访问过的每个节点,而不仅仅是最终候选节点。 |
leanvec_d | _<dim> 工厂字符串后缀或dim / 2 | 仅限LeanVec。主数据集的降维因子。该参数将覆盖工厂字符串后缀。较小的数值可减少内存占用并加快图搜索速度,但会降低召回率。 |
ood_training | True 适用于 LeanVec | 仅限 LeanVec。是否为索引向量和查询分别学习投影。设置False 可加快构建速度。若将True 设置为 LeanVec 以外的其他工厂,将导致设置失败。 |
以下搜索参数应设置在search_params.params 中。您也可以将其设为构建参数,这将更改索引在未设置这些参数的搜索中使用的默认值。
| 参数 | 默认值 | 描述 |
|---|---|---|
search_window_size | 10 | 遍历图时保留的最近候选项数量。数值越大,召回率和延迟越高。 |
search_buffer_capacity | 10 | 搜索缓冲区的容量。必须大于或等于search_window_size 。在大多数情况下,将其设置为search_window_size 。对于 LeanVec,请尝试设置为search_window_size 的 1.5 倍。 |
了解更多
- 请阅读FAISS 索引直通参考文档,了解通用工厂字符串契约、限制及错误处理。
- 与DISKANN 中的磁盘端 Vamana 实现进行比较。
- 请参阅SVS 代码库,了解构建细节、性能调优以及底层 C++ API。