内存优化向量
向量搜索操作可能消耗大量内存,尤其是在处理大规模部署时。UDB-SX 提供了多种策略,可在保持搜索性能的同时优化内存使用。您可以在优先考虑低延迟或低成本的不同工作负载模式之间进行选择,应用各种压缩级别以减少内存占用,或使用字节向量、二进制向量等替代向量表示形式。这些优化技术使您能够根据特定用例需求,在内存消耗、搜索性能和成本之间取得平衡。
向量工作负载模式
向量搜索需要在搜索性能和运营成本之间取得平衡。虽然内存搜索提供了最低的延迟,但基于磁盘的搜索通过减少内存使用提供了一种更具成本效益的方法,尽管这会导致搜索延迟略有增加。要在这些方法之间进行选择,请在您的 knn_vector 字段配置中使用 mode 映射参数。该参数根据您的优先级(低延迟或低成本)为 k-NN 参数设置适当的默认值。为了进一步优化,您可以在 k-NN 字段映射中覆盖这些默认参数值。
UDB-SX 支持以下向量工作负载模式。
| 模式 | 默认引擎 | 描述 |
|---|---|---|
in_memory (默认) |
faiss |
优先考虑低延迟搜索。此模式使用 faiss 引擎,不应用任何量化。它使用 UDB-SX 中向量搜索的默认参数值进行配置。 |
on_disk |
faiss |
优先考虑低成本向量搜索,同时保持较高的召回率。默认情况下,on_disk 模式使用量化和重打分来执行两阶段方法以检索最近邻。on_disk 模式仅支持 float 向量类型。 |
要创建使用 on_disk 模式进行低成本搜索的向量索引,请发送以下请求:
PUT test-index
{
"settings": {
"index": {
"knn": true
}
},
"mappings": {
"properties": {
"my_vector": {
"type": "knn_vector",
"dimension": 3,
"space_type": "l2",
"mode": "on_disk"
}
}
}
}
压缩级别
compression_level 映射参数用于选择一个量化编码器,该编码器按给定比例减少向量内存消耗。下表列出了可用的 compression_level 值。
| 压缩级别 | 支持的引擎 |
|---|---|
1x |
faiss、lucene 和 nmslib(已弃用) |
2x |
faiss |
4x |
lucene |
8x |
faiss |
16x |
faiss |
32x |
faiss |
例如,如果为 768 维的 float32 索引传递 32x 的 compression_level,则每个向量的内存将从 4 * 768 = 3072 字节减少到 3072 / 32 = 846 字节。在内部,可能会使用二进制量化(将 float 映射到 bit)来实现这种压缩。
如果设置了 compression_level 参数,则不能在 method 映射中指定 encoder。大于 1x 的压缩级别仅支持 float 向量类型。
下表列出了可用工作负载模式的默认 compression_level 值。
| 模式 | 默认压缩级别 |
|---|---|
in_memory |
1x |
on_disk |
32x |
要创建具有 16x compression_level 的向量字段,请在映射中指定 compression_level 参数。此参数将 on_disk 模式的默认压缩级别从 32x 覆盖为 16x,以牺牲更大的内存占用来获得更高的召回率和准确性:
PUT test-index
{
"settings": {
"index": {
"knn": true
}
},
"mappings": {
"properties": {
"my_vector": {
"type": "knn_vector",
"dimension": 3,
"space_type": "l2",
"mode": "on_disk",
"compression_level": "16x"
}
}
}
}
将量化结果重打分为全精度
为了在保持量化节省内存的同时提高召回率,您可以使用两阶段搜索方法。在第一阶段,使用量化向量从索引中检索 oversample_factor * k 个结果,并对分数进行近似计算。在第二阶段,将这些 oversample_factor * k 个结果的全精度向量从磁盘加载到内存中,并针对全精度查询向量重新计算分数。然后将结果减少到前 k 个。
默认的重打分行为由支持的 k-NN 向量字段的 mode 和 compression_level 决定:
对于
in_memory模式,默认不应用重打分。对于
on_disk模式,默认重打分基于配置的compression_level。每个compression_level提供一个默认的oversample_factor,如下表所示。
| 压缩级别 | 默认重打分 oversample_factor |
|---|---|
32x (默认) |
3.0 |
16x |
2.0 |
8x |
2.0 |
4x |
无默认重打分 |
2x |
无默认重打分 |
要显式应用重打分,请在量化索引的查询中提供 rescore 参数并指定 oversample_factor:
GET /my-vector-index/_search
{
"size": 2,
"query": {
"knn": {
"target-field": {
"vector": [2, 3, 5, 6],
"k": 2,
"rescore" : {
"oversample_factor": 1.2
}
}
}
}
}
或者,将 rescore 参数设置为 true 以使用默认的 oversample_factor 值 1.0:
GET /my-vector-index/_search
{
"size": 2,
"query": {
"knn": {
"target-field": {
"vector": [2, 3, 5, 6],
"k": 2,
"rescore" : true
}
}
}
}
oversample_factor 是一个介于 1.0 到 100.0(含)之间的浮点数。第一阶段的结果数量计算为 oversample_factor * k,并保证在 100 到 10,000(含)之间。如果计算结果小于 100,则结果数量设置为 100。如果计算结果大于 10,000,则结果数量设置为 10,000。
重打分仅支持 faiss 引擎。
如果未使用量化,则不需要重打分,因为返回的分数已经是完全精确的。
字节向量
默认情况下,k-NN 向量是 float 向量,其中每个维度为 4 字节。如果您想节省存储空间,可以使用带有 faiss 或 lucene 引擎的 byte 向量。在 byte 向量中,每个维度是一个 [-128, 127] 范围内的有符号 8 位整数。
字节向量仅支持 lucene 和 faiss 引擎。不支持 nmslib 引擎。
在 k-NN 基准测试中,使用 byte 向量而非 float 向量,显著减少了存储和内存使用量,提高了索引吞吐量并降低了查询延迟。此外,召回精度并未受到太大影响(请注意,召回率可能取决于多种因素,例如使用的量化技术和数据分布)。
使用 byte 向量时,与使用 float 向量相比,预计会损失一些召回精度。字节向量适用于大规模应用程序和优先考虑减少内存占用、愿意以最小召回损失为代价的用例。
当使用带有 faiss 引擎的 byte 向量时,我们建议使用单指令多数据 (SIMD) 优化,这有助于显著降低搜索延迟并提高索引吞吐量。
在 k-NN 插件 2.9 版本中引入的可选 data_type 参数定义了向量的数据类型。该参数的默认值为 float。
要使用 byte 向量,请在为索引创建映射时将 data_type 参数设置为 byte。
示例:HNSW
以下示例创建一个使用 lucene 引擎和 hnsw 算法的字节向量索引:
PUT test-index
{
"settings": {
"index": {
"knn": true,
"knn.algo_param.ef_search": 100
}
},
"mappings": {
"properties": {
"my_vector": {
"type": "knn_vector",
"dimension": 3,
"data_type": "byte",
"space_type": "l2",
"method": {
"name": "hnsw",
"engine": "lucene",
"parameters": {
"ef_construction": 100,
"m": 16
}
}
}
}
}
}
创建索引后,像往常一样摄入文档。确保向量中的每个维度都在支持的 [-128, 127] 范围内:
PUT test-index/_doc/1
{
"my_vector": [-126, 28, 127]
}
PUT test-index/_doc/2
{
"my_vector": [100, -128, 0]
}
查询时,请务必使用 byte 向量:
GET test-index/_search
{
"size": 2,
"query": {
"knn": {
"my_vector": {
"vector": [26, -120, 99],
"k": 2
}
}
}
}
示例:IVF
ivf 方法需要一个训练步骤,该步骤创建一个模型并对其进行训练,以便在创建段期间初始化原生库索引。
首先,创建一个将包含字节向量训练数据的索引。指定 faiss 引擎和 ivf 算法,并确保 dimension 与您要创建的模型的维度匹配:
PUT train-index
{
"mappings": {
"properties": {
"train-field": {
"type": "knn_vector",
"dimension": 4,
"data_type": "byte"
}
}
}
}
首先,将包含字节向量的训练数据摄入训练索引:
PUT _bulk
{ "index": { "_index": "train-index", "_id": "1" } }
{ "train-field": [127, 100, 0, -120] }
{ "index": { "_index": "train-index", "_id": "2" } }
{ "train-field": [2, -128, -10, 50] }
{ "index": { "_index": "train-index", "_id": "3" } }
{ "train-field": [13, -100, 5, 126] }
{ "index": { "_index": "train-index", "_id": "4" } }
{ "train-field": [5, 100, -6, -125] }
然后,创建并训练名为 byte-vector-model 的模型。该模型将使用 train-index 中 train-field 的训练数据进行训练。指定 byte 数据类型:
POST _plugins/_knn/models/byte-vector-model/_train
{
"training_index": "train-index",
"training_field": "train-field",
"dimension": 4,
"description": "model with byte data",
"data_type": "byte",
"method": {
"name": "ivf",
"engine": "faiss",
"space_type": "l2",
"parameters": {
"nlist": 1,
"nprobes": 1
}
}
}
要检查模型训练状态,请调用 Get Model API:
GET _plugins/_knn/models/byte-vector-model?filter_path=state
训练完成后,state 将更改为 created。
接下来,创建一个将使用训练后的模型初始化其原生库索引的索引:
PUT test-byte-ivf
{
"settings": {
"index": {
"knn": true
}
},
"mappings": {
"properties": {
"my_vector": {
"type": "knn_vector",
"model_id": "byte-vector-model"
}
}
}
}
将要搜索的包含字节向量的数据摄入创建的索引:
PUT _bulk?refresh=true
{"index": {"_index": "test-byte-ivf", "_id": "1"}}
{"my_vector": [7, 10, 15, -120]}
{"index": {"_index": "test-byte-ivf", "_id": "2"}}
{"my_vector": [10, -100, 120, -108]}
{"index": {"_index": "test-byte-ivf", "_id": "3"}}
{"my_vector": [1, -2, 5, -50]}
{"index": {"_index": "test-byte-ivf", "_id": "4"}}
{"my_vector": [9, -7, 45, -78]}
{"index": {"_index": "test-byte-ivf", "_id": "5"}}
{"my_vector": [80, -70, 127, -128]}
最后,搜索数据。请务必在 k-NN 向量字段中提供一个字节向量:
GET test-byte-ivf/_search
{
"size": 2,
"query": {
"knn": {
"my_vector": {
"vector": [100, -120, 50, -45],
"k": 2
}
}
}
}
内存估算
在最佳情况下,字节向量所需的内存是 32 位向量所需内存的 25%。
HNSW 内存估算
分层可导航小世界 (HNSW) 所需的内存估计为 1.1 * (dimension + 8 * m) 字节/向量,其中 m 是在图构建期间为每个元素创建的最大双向链接数。
举例来说,假设有 100 万个向量,dimension 为 256,m 为 16。内存需求估算如下:
1.1 * (256 + 8 * 16) * 1,000,000 ~= 0.39 GB
IVF 内存估算
倒排文件索引 (IVF) 所需的内存估计为 1.1 * ((dimension * num_vectors) + (4 * nlist * dimension)) 字节/向量,其中 nlist 是将向量划分到的桶的数量。
举例来说,假设有 100 万个向量,dimension 为 256,nlist 为 128。内存需求估算如下:
1.1 * ((256 * 1,000,000) + (4 * 128 * 256)) ~= 0.27 GB
量化技术
如果您的向量是 float 类型,您需要先将它们转换为 byte 类型,然后再摄入文档。这种转换是通过 量化数据集(降低其向量的精度)来完成的。Faiss 引擎支持多种量化技术,例如标量量化 (SQ) 和乘积量化 (PQ)。量化技术的选择取决于您使用的数据类型,并且会影响召回值的准确性。以下各节描述了用于为 L2 和 余弦相似度 空间类型量化 k-NN 基准测试数据的标量量化算法。提供的伪代码仅用于说明目的。
用于 L2 空间类型的标量量化
以下示例伪代码说明了在 L2 空间类型的欧几里得数据集基准测试中使用的标量量化技术。欧几里得距离具有平移不变性。如果将 $$x$$ 和 $$y$$ 都平移相同的 $$z$$,那么距离保持不变 ($$\lVert x-y\rVert =\lVert (x-z)-(y-z)\rVert$$)。
# 随机数据集(创建随机数据集的示例)
dataset = np.random.uniform(-300, 300, (100, 10))
# 随机查询集(创建随机查询集的示例)
queryset = np.random.uniform(-350, 350, (100, 10))
# 值的数量
B = 256
# 索引:
# 获取最小值和最大值
dataset_min = np.min(dataset)
dataset_max = np.max(dataset)
# 平移坐标使其为非负
dataset -= dataset_min
# 归一化到 [0, 1]
dataset *= 1. / (dataset_max - dataset_min)
# 分桶到 256 个值
dataset = np.floor(dataset * (B - 1)) - int(B / 2)
# 查询:
# 裁剪(如果查询集范围超出数据集范围)
queryset = queryset.clip(dataset_min, dataset_max)
# 平移坐标使其为非负
queryset -= dataset_min
# 归一化
queryset *= 1. / (dataset_max - dataset_min)
# 分桶到 256 个值
queryset = np.floor(queryset * (B - 1)) - int(B / 2)
用于余弦相似度空间类型的标量量化
以下示例伪代码说明了在余弦相似度空间类型的角度数据集基准测试中使用的标量量化技术。余弦相似度不具有平移不变性 ($$cos(x, y) \neq cos(x-z, y-z)$$)。
以下伪代码适用于正数:
# 对于正数
# 索引和查询:
# 获取训练数据集的最大值
max = np.max(dataset)
min = 0
B = 127
# 归一化到 [0,1]
val = (val - min) / (max - min)
val = (val * B)
# 获取整数和小数部分
int_part = floor(val)
frac_part = val - int_part
if 0.5 < frac_part:
bval = int_part + 1
else:
bval = int_part
return Byte(bval)
以下伪代码适用于负数:
# 对于负数
# 索引和查询:
# 获取训练数据集的最小值
min = 0
max = -np.min(dataset)
B = 128
# 归一化到 [0,1]
val = (val - min) / (max - min)
val = (val * B)
# 获取整数和小数部分
int_part = floor(var)
frac_part = val - int_part
if 0.5 < frac_part:
bval = int_part + 1
else:
bval = int_part
return Byte(bval)
二进制向量
通过从浮点向量切换到二进制向量,您可以将内存成本降低 32 倍。使用二进制向量索引可以降低运营成本,同时保持较高的召回性能,使大规模部署更加经济高效。
以下 k-NN 搜索类型支持二进制格式:
近似 k-NN:仅支持用于具有 HNSW 和 IVF 算法的 Faiss 引擎的二进制向量。
脚本评分 k-NN:允许在脚本评分中使用二进制向量。
Painless 扩展:允许使用带有 Painless 脚本扩展的二进制向量。
要求
在 UDB-SX k-NN 插件中使用二进制向量有几个要求:
二进制向量索引的
data_type必须为binary。二进制向量索引的
space_type必须为hamming。二进制向量索引的
dimension必须是 8 的倍数。您必须将二进制数据转换为 [-128, 127] 范围内的 8 位有符号整数 (
int8)。例如,8 位二进制序列0, 1, 1, 0, 0, 0, 1, 1必须转换为其等效的字节值99才能用作二进制向量输入。
示例:HNSW
要创建具有 Faiss 引擎和 HNSW 算法的二进制向量索引,请发送以下请求:
PUT /test-binary-hnsw
{
"settings": {
"index": {
"knn": true
}
},
"mappings": {
"properties": {
"my_vector": {
"type": "knn_vector",
"dimension": 8,
"data_type": "binary",
"space_type": "hamming",
"method": {
"name": "hnsw",
"engine": "faiss"
}
}
}
}
}
然后摄入一些包含二进制向量的文档:
PUT _bulk
{"index": {"_index": "test-binary-hnsw", "_id": "1"}}
{"my_vector": [7], "price": 4.4}
{"index": {"_index": "test-binary-hnsw", "_id": "2"}}
{"my_vector": [10], "price": 14.2}
{"index": {"_index": "test-binary-hnsw", "_id": "3"}}
{"my_vector": [15], "price": 19.1}
{"index": {"_index": "test-binary-hnsw", "_id": "4"}}
{"my_vector": [99], "price": 1.2}
{"index": {"_index": "test-binary-hnsw", "_id": "5"}}
{"my_vector": [80], "price": 16.5}
查询时,请务必使用二进制向量:
GET /test-binary-hnsw/_search
{
"size": 2,
"query": {
"knn": {
"my_vector": {
"vector": [9],
"k": 2
}
}
}
}
响应包含最接近查询向量的两个向量:
响应
{
"took": 8,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"max_score": 0.5,
"hits": [
{
"_index": "test-binary-hnsw",
"_id": "2",
"_score": 0.5,
"_source": {
"my_vector": [
10
],
"price": 14.2
}
},
{
"_index": "test-binary-hnsw",
"_id": "5",
"_score": 0.25,
"_source": {
"my_vector": [
80
],
"price": 16.5
}
}
]
}
}
示例:IVF
IVF 方法需要一个训练步骤,该步骤创建一个模型并对其进行训练,以便在创建段期间初始化原生库索引。
首先,创建一个将包含二进制向量训练数据的索引。指定 Faiss 引擎和 IVF 算法,并确保 dimension 与您要创建的模型的维度匹配:
PUT train-index
{
"mappings": {
"properties": {
"train-field": {
"type": "knn_vector",
"dimension": 8,
"data_type": "binary"
}
}
}
}
将包含二进制向量的训练数据摄入训练索引:
批量摄入请求
PUT _bulk
{ "index": { "_index": "train-index", "_id": "1" } }
{ "train-field": [1] }
{ "index": { "_index": "train-index", "_id": "2" } }
{ "train-field": [2] }
{ "index": { "_index": "train-index", "_id": "3" } }
{ "train-field": [3] }
{ "index": { "_index": "train-index", "_id": "4" } }
{ "train-field": [4] }
{ "index": { "_index": "train-index", "_id": "5" } }
{ "train-field": [5] }
{ "index": { "_index": "train-index", "_id": "6" } }
{ "train-field": [6] }
{ "index": { "_index": "train-index", "_id": "7" } }
{ "train-field": [7] }
{ "index": { "_index": "train-index", "_id": "8" } }
{ "train-field": [8] }
{ "index": { "_index": "train-index", "_id": "9" } }
{ "train-field": [9] }
{ "index": { "_index": "train-index", "_id": "10" } }
{ "train-field": [10] }
{ "index": { "_index": "train-index", "_id": "11" } }
{ "train-field": [11] }
{ "index": { "_index": "train-index", "_id": "12" } }
{ "train-field": [12] }
{ "index": { "_index": "train-index", "_id": "13" } }
{ "train-field": [13] }
{ "index": { "_index": "train-index", "_id": "14" } }
{ "train-field": [14] }
{ "index": { "_index": "train-index", "_id": "15" } }
{ "train-field": [15] }
{ "index": { "_index": "train-index", "_id": "16" } }
{ "train-field": [16] }
{ "index": { "_index": "train-index", "_id": "17" } }
{ "train-field": [17] }
{ "index": { "_index": "train-index", "_id": "18" } }
{ "train-field": [18] }
{ "index": { "_index": "train-index", "_id": "19" } }
{ "train-field": [19] }
{ "index": { "_index": "train-index", "_id": "20" } }
{ "train-field": [20] }
{ "index": { "_index": "train-index", "_id": "21" } }
{ "train-field": [21] }
{ "index": { "_index": "train-index", "_id": "22" } }
{ "train-field": [22] }
{ "index": { "_index": "train-index", "_id": "23" } }
{ "train-field": [23] }
{ "index": { "_index": "train-index", "_id": "24" } }
{ "train-field": [24] }
{ "index": { "_index": "train-index", "_id": "25" } }
{ "train-field": [25] }
{ "index": { "_index": "train-index", "_id": "26" } }
{ "train-field": [26] }
{ "index": { "_index": "train-index", "_id": "27" } }
{ "train-field": [27] }
{ "index": { "_index": "train-index", "_id": "28" } }
{ "train-field": [28] }
{ "index": { "_index": "train-index", "_id": "29" } }
{ "train-field": [29] }
{ "index": { "_index": "train-index", "_id": "30" } }
{ "train-field": [30] }
{ "index": { "_index": "train-index", "_id": "31" } }
{ "train-field": [31] }
{ "index": { "_index": "train-index", "_id": "32" } }
{ "train-field": [32] }
{ "index": { "_index": "train-index", "_id": "33" } }
{ "train-field": [33] }
{ "index": { "_index": "train-index", "_id": "34" } }
{ "train-field": [34] }
{ "index": { "_index": "train-index", "_id": "35" } }
{ "train-field": [35] }
{ "index": { "_index": "train-index", "_id": "36" } }
{ "train-field": [36] }
{ "index": { "_index": "train-index", "_id": "37" } }
{ "train-field": [37] }
{ "index": { "_index": "train-index", "_id": "38" } }
{ "train-field": [38] }
{ "index": { "_index": "train-index", "_id": "39" } }
{ "train-field": [39] }
{ "index": { "_index": "train-index", "_id": "40" } }
{ "train-field": [40] }
然后,创建并训练名为 test-binary-model 的模型。该模型将使用 train-index 中 train-field 的训练数据进行训练。指定 binary 数据类型和 hamming 空间类型:
POST _plugins/_knn/models/test-binary-model/_train
{
"training_index": "train-index",
"training_field": "train-field",
"dimension": 8,
"description": "model with binary data",
"data_type": "binary",
"space_type": "hamming",
"method": {
"name": "ivf",
"engine": "faiss",
"parameters": {
"nlist": 16,
"nprobes": 1
}
}
}
要检查模型训练状态,请调用 Get Model API:
GET _plugins/_knn/models/test-binary-model?filter_path=state
训练完成后,state 将更改为 created。
接下来,创建一个将使用训练后的模型初始化其原生库索引的索引:
PUT test-binary-ivf
{
"settings": {
"index": {
"knn": true
}
},
"mappings": {
"properties": {
"my_vector": {
"type": "knn_vector",
"model_id": "test-binary-model"
}
}
}
}
将要搜索的包含二进制向量的数据摄入创建的索引:
PUT _bulk?refresh=true
{"index": {"_index": "test-binary-ivf", "_id": "1"}}
{"my_vector": [7], "price": 4.4}
{"index": {"_index": "test-binary-ivf", "_id": "2"}}
{"my_vector": [10], "price": 14.2}
{"index": {"_index": "test-binary-ivf", "_id": "3"}}
{"my_vector": [15], "price": 19.1}
{"index": {"_index": "test-binary-ivf", "_id": "4"}}
{"my_vector": [99], "price": 1.2}
{"index": {"_index": "test-binary-ivf", "_id": "5"}}
{"my_vector": [80], "price": 16.5}
最后,搜索数据。请务必在 k-NN 向量字段中提供一个二进制向量:
GET test-binary-ivf/_search
{
"size": 2,
"query": {
"knn": {
"my_vector": {
"vector": [8],
"k": 2
}
}
}
}
响应包含最接近查询向量的两个向量:
响应
GET /_plugins/_knn/models/my-model?filter_path=state
{
"took": 7,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"max_score": 0.5,
"hits": [
{
"_index": "test-binary-ivf",
"_id": "2",
"_score": 0.5,
"_source": {
"my_vector": [
10
],
"price": 14.2
}
},
{
"_index": "test-binary-ivf",
"_id": "3",
"_score": 0.25,
"_source": {
"my_vector": [
15
],
"price": 19.1
}
}
]
}
}
内存估算
使用以下公式估算二进制向量所需的内存量。
HNSW 内存估算
可以使用以下公式估算 HNSW 所需的内存,其中 m 是在图构建期间为每个元素创建的最大双向链接数:
1.1 * (dimension / 8 + 8 * m) bytes/vector
IVF 内存估算
可以使用以下公式估算 IVF 所需的内存,其中 nlist 是将向量划分到的桶的数量:
1.1 * (((dimension / 8) * num_vectors) + (nlist * dimension / 8))
后续步骤
k-NN 查询
基于磁盘的向量搜索
向量量化