近似 k-NN 搜索
标准 k 最近邻 (k-NN) 搜索方法使用暴力法计算相似度,该方法测量查询与多个点之间的最近距离,从而产生精确结果。这在许多应用程序中都非常有效。然而,在维度极高的大型数据集情况下,这会产生扩展性问题,降低搜索效率。近似 k-NN 搜索方法可以通过采用更有效地重组索引并降低可搜索向量维度的工具来克服这一问题。使用此方法需要牺牲准确性,但能显著提高搜索处理速度。
OpenSearch 中的近似 k-NN 搜索方法使用来自 NMSLIB、Faiss 和 Lucene 库的近似最近邻 (ANN) 算法来支持 k-NN 搜索。这些搜索方法采用 ANN 来改善大型数据集的搜索延迟。在 OpenSearch 提供的三种搜索方法中,该方法为大型数据集提供了最佳的搜索可扩展性。当数据集达到数十万个向量时,此方法是首选方法。
有关 OpenSearch 支持的算法的信息,请参阅 方法和引擎。
OpenSearch 在索引期间为每个 knn-vector
字段/Lucene 段对构建向量的本地库索引,该索引可用于在搜索期间高效查找查询向量的 k 个最近邻居。要了解有关 Lucene 段的更多信息,请参阅 Apache Lucene 文档。这些本地库索引在搜索期间加载到本地内存中并由缓存管理。要了解有关将本地库索引预加载到内存中的更多信息,请参阅 Warmup API。此外,您可以使用 Stats API 查看哪些本地库索引已加载到内存中。
由于本地库索引是在索引期间构建的,因此无法在索引上应用过滤器,然后使用此搜索方法。所有过滤器都应用于 ANN 搜索产生的结果。
开始使用近似 k-NN
要使用近似搜索功能,您必须首先创建一个向量索引,并将 index.knn
设置为 true
。此设置会告知 OpenSearch 为该索引创建本地库索引。
接下来,您必须添加一个或多个 knn_vector
数据类型的字段。以下示例使用 faiss
引擎创建一个包含两个 knn_vector
字段的索引。
PUT my-knn-index-1
{
"settings": {
"index": {
"knn": true,
"knn.algo_param.ef_search": 100
}
},
"mappings": {
"properties": {
"my_vector1": {
"type": "knn_vector",
"dimension": 2,
"space_type": "l2",
"method": {
"name": "hnsw",
"engine": "faiss",
"parameters": {
"ef_construction": 128,
"m": 24
}
}
},
"my_vector2": {
"type": "knn_vector",
"dimension": 4,
"space_type": "innerproduct",
"method": {
"name": "hnsw",
"engine": "faiss",
"parameters": {
"ef_construction": 256,
"m": 48
}
}
}
}
}
}
在前面的示例中,两个 knn_vector
字段都使用方法定义进行配置。此外,knn_vector
字段也可以使用模型进行配置。有关更多信息,请参阅 k-NN 向量。
knn_vector
数据类型支持浮点向量,对于 NMSLIB、Faiss 和 Lucene 引擎,其维度计数可通过 dimension
映射参数设置为高达 16,000。
在 OpenSearch 中,编解码器处理索引的存储和检索。OpenSearch 使用自定义编解码器将向量数据写入本地库索引,以便底层 k-NN 搜索库可以读取它。
创建索引后,您可以向其中添加一些数据。
POST _bulk
{ "index": { "_index": "my-knn-index-1", "_id": "1" } }
{ "my_vector1": [1.5, 2.5], "price": 12.2 }
{ "index": { "_index": "my-knn-index-1", "_id": "2" } }
{ "my_vector1": [2.5, 3.5], "price": 7.1 }
{ "index": { "_index": "my-knn-index-1", "_id": "3" } }
{ "my_vector1": [3.5, 4.5], "price": 12.9 }
{ "index": { "_index": "my-knn-index-1", "_id": "4" } }
{ "my_vector1": [5.5, 6.5], "price": 1.2 }
{ "index": { "_index": "my-knn-index-1", "_id": "5" } }
{ "my_vector1": [4.5, 5.5], "price": 3.7 }
{ "index": { "_index": "my-knn-index-1", "_id": "6" } }
{ "my_vector2": [1.5, 5.5, 4.5, 6.4], "price": 10.3 }
{ "index": { "_index": "my-knn-index-1", "_id": "7" } }
{ "my_vector2": [2.5, 3.5, 5.6, 6.7], "price": 5.5 }
{ "index": { "_index": "my-knn-index-1", "_id": "8" } }
{ "my_vector2": [4.5, 5.5, 6.7, 3.7], "price": 4.4 }
{ "index": { "_index": "my-knn-index-1", "_id": "9" } }
{ "my_vector2": [1.5, 5.5, 4.5, 6.4], "price": 8.9 }
然后,您可以使用 knn
查询类型对数据运行 ANN 搜索。
GET my-knn-index-1/_search
{
"size": 2,
"query": {
"knn": {
"my_vector2": {
"vector": [2, 3, 5, 6],
"k": 2
}
}
}
}
返回结果的数量
在前面的查询中,k
表示每个图搜索返回的邻居数量。您还必须包含 size
参数,指示您希望查询返回的最终结果数量。
对于 NMSLIB 和 Faiss 引擎,k
表示分片所有段返回的最大文档数。对于 Lucene 引擎,k
表示分片返回的文档数。k
的最大值为 10,000。
对于任何引擎,每个分片都会向协调节点返回 size
个结果。因此,协调节点收到的总结果数为 size * 分片数
。协调节点合并从所有节点收到的结果后,查询将返回前 size
个结果。
下表提供了各种引擎在不同场景下返回结果数量的示例。对于这些示例,假设段和分片中包含的文档数量足以返回表中指定的结果数量。
大小 | k | 主分片数量 | 每个分片的段数量 | 返回结果数量,Faiss/NMSLIB | 返回结果数量,Lucene |
---|---|---|---|---|---|
10 | 1 | 1 | 4 | 4 | 1 |
10 | 10 | 1 | 4 | 10 | 10 |
10 | 1 | 2 | 4 | 8 | 2 |
只有当 k
小于 size
时,Faiss/NMSLIB 返回的结果数量才与 Lucene 返回的结果数量不同。如果 k
和 size
相等,所有引擎将返回相同数量的结果。
从 OpenSearch 2.14 开始,您可以对径向搜索使用 k
、min_score
或 max_distance
。
从模型构建向量索引
对于 OpenSearch 支持的某些算法,本地库索引在使用前需要进行训练。训练每个新创建的段会非常昂贵,因此,OpenSearch 引入了模型概念,该模型在段创建期间初始化本地库索引。您可以通过调用 Train API 并传入训练数据的源和模型的方法定义来创建模型。训练完成后,模型会被序列化到一个 k-NN 模型系统索引中。然后,在索引期间,模型会从该索引中提取以初始化段。
要训练模型,您首先需要一个包含训练数据的 OpenSearch 索引。训练数据可以来自任何 knn_vector
字段,其维度与您要创建的模型的维度相匹配。训练数据可以与您计划索引的数据相同,也可以来自单独的数据集。要创建训练索引,请发送以下请求。
PUT /train-index
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 0
},
"mappings": {
"properties": {
"train-field": {
"type": "knn_vector",
"dimension": 4
}
}
}
}
请注意,索引设置中未设置 index.knn
。这确保您不会为该索引创建本地库索引。
您现在可以向索引添加一些数据。
POST _bulk
{ "index": { "_index": "train-index", "_id": "1" } }
{ "train-field": [1.5, 5.5, 4.5, 6.4]}
{ "index": { "_index": "train-index", "_id": "2" } }
{ "train-field": [2.5, 3.5, 5.6, 6.7]}
{ "index": { "_index": "train-index", "_id": "3" } }
{ "train-field": [4.5, 5.5, 6.7, 3.7]}
{ "index": { "_index": "train-index", "_id": "4" } }
{ "train-field": [1.5, 5.5, 4.5, 6.4]}
完成训练索引的索引后,您可以调用 Train API。
POST /_plugins/_knn/models/my-model/_train
{
"training_index": "train-index",
"training_field": "train-field",
"dimension": 4,
"description": "My model description",
"method": {
"name": "ivf",
"engine": "faiss",
"parameters": {
"encoder": {
"name": "pq",
"parameters": {
"code_size": 2,
"m": 2
}
}
}
}
}
有关方法参数的更多信息,请参阅 IVF 训练要求。
Train API 在训练作业开始后立即返回。要检查作业状态,请使用 Get Model API。
GET /_plugins/_knn/models/my-model?filter_path=state&pretty
{
"state": "training"
}
一旦模型进入 created
状态,您就可以创建一个将使用此模型初始化其本地库索引的索引。
PUT /target-index
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"index.knn": true
},
"mappings": {
"properties": {
"target-field": {
"type": "knn_vector",
"model_id": "my-model"
}
}
}
}
最后,您可以将要搜索的文档添加到索引中。
POST _bulk
{ "index": { "_index": "target-index", "_id": "1" } }
{ "target-field": [1.5, 5.5, 4.5, 6.4]}
{ "index": { "_index": "target-index", "_id": "2" } }
{ "target-field": [2.5, 3.5, 5.6, 6.7]}
{ "index": { "_index": "target-index", "_id": "3" } }
{ "target-field": [4.5, 5.5, 6.7, 3.7]}
{ "index": { "_index": "target-index", "_id": "4" } }
{ "target-field": [1.5, 5.5, 4.5, 6.4]}
数据摄入后,可以像任何其他 knn_vector
字段一样进行搜索。