内存优化向量
向量搜索操作可能需要大量内存,尤其是在处理大规模部署时。OpenSearch 提供了多种策略来优化内存使用,同时保持搜索性能。您可以选择不同的工作负载模式,这些模式优先考虑低延迟或低成本;应用各种压缩级别以减少内存占用;或使用替代向量表示,如字节或二进制向量。这些优化技术使您能够根据具体的用例需求平衡内存消耗、搜索性能和成本。
向量工作负载模式
向量搜索需要在搜索性能和运营成本之间取得平衡。虽然内存搜索提供了最低的延迟,但基于磁盘的搜索通过减少内存使用提供了一种更具成本效益的方法,尽管它会导致搜索延迟略高。要在这些方法之间进行选择,请在 knn_vector
字段配置中使用 mode
映射参数。此参数根据您的优先级(低延迟或低成本)为 k-NN 参数设置适当的默认值。为了进一步优化,您可以在 k-NN 字段映射中覆盖这些默认参数值。
OpenSearch 支持以下向量工作负载模式。
模式 | 默认引擎 | 描述 |
---|---|---|
in_memory (默认) | faiss | 优先考虑低延迟搜索。此模式使用 faiss 引擎,不应用任何量化。它配置了 OpenSearch 中向量搜索的默认参数值。 |
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
向量类型。
从 OpenSearch 3.1 开始,启用 on_disk
模式并使用 1x
压缩级别会激活内存优化搜索。在此模式下,引擎在搜索期间按需加载数据,而不是一次性将所有数据加载到内存中。
下表列出了可用工作负载模式的默认 compression_level
值。
模式 | 默认压缩级别 |
---|---|
in_memory | 1x |
on_disk | 32x |
要创建 compression_level
为 16x
的向量字段,请在映射中指定 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 | 1.0 |
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 和 Lucene 引擎。
如果未使用量化,则无需重新评分,因为返回的分数已经完全精确。
字节向量
默认情况下,k-NN 向量是 float
向量,其中每个维度为 4 字节。如果您想节省存储空间,可以使用 faiss
或 lucene
引擎的 byte
向量。在 byte
向量中,每个维度是一个有符号的 8 位整数,范围在 [-128, 127] 内。
字节向量仅支持 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)。量化技术的选择取决于您使用的数据类型,并且会影响召回值的准确性。以下各节描述了用于对k-NN 基准测试数据进行量化的标量量化算法,用于 L2 和余弦相似度空间类型。提供的伪代码仅用于说明目的。
L2 空间类型的标量量化
以下示例伪代码说明了用于欧几里得数据集上 L2 空间类型基准测试的标量量化技术。欧几里得距离是平移不变的。如果您将 \(x\) 和 \(y\) 都平移相同的 \(z\),那么距离保持不变(\(\lVert x-y\rVert =\lVert (x-z)-(y-z)\rVert\))。
# Random dataset (Example to create a random dataset)
dataset = np.random.uniform(-300, 300, (100, 10))
# Random query set (Example to create a random queryset)
queryset = np.random.uniform(-350, 350, (100, 10))
# Number of values
B = 256
# INDEXING:
# Get min and max
dataset_min = np.min(dataset)
dataset_max = np.max(dataset)
# Shift coordinates to be non-negative
dataset -= dataset_min
# Normalize into [0, 1]
dataset *= 1. / (dataset_max - dataset_min)
# Bucket into 256 values
dataset = np.floor(dataset * (B - 1)) - int(B / 2)
# QUERYING:
# Clip (if queryset range is out of datset range)
queryset = queryset.clip(dataset_min, dataset_max)
# Shift coordinates to be non-negative
queryset -= dataset_min
# Normalize
queryset *= 1. / (dataset_max - dataset_min)
# Bucket into 256 values
queryset = np.floor(queryset * (B - 1)) - int(B / 2)
余弦相似度空间类型的标量量化
以下示例伪代码说明了用于角数据集上余弦相似度空间类型基准测试的标量量化技术。余弦相似度不是平移不变的(\(cos(x, y) \neq cos(x-z, y-z)\))。
以下伪代码适用于正数
# For Positive Numbers
# INDEXING and QUERYING:
# Get Max of train dataset
max = np.max(dataset)
min = 0
B = 127
# Normalize into [0,1]
val = (val - min) / (max - min)
val = (val * B)
# Get int and fraction values
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)
以下伪代码适用于负数
# For Negative Numbers
# INDEXING and QUERYING:
# Get Min of train dataset
min = 0
max = -np.min(dataset)
B = 128
# Normalize into [0,1]
val = (val - min) / (max - min)
val = (val * B)
# Get int and fraction values
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:仅支持 Faiss 引擎的二进制向量,采用 HNSW 和 IVF 算法。
- 脚本评分 k-NN:允许在脚本评分中使用二进制向量。
- Painless 扩展:允许将二进制向量与 Painless 脚本扩展一起使用。
要求
在 OpenSearch 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))