近似 k-NN 搜索

标准的 k-最近邻搜索方法采用暴力计算方式,通过测量查询点与多个点之间的最近距离来计算相似性,从而得出精确结果。这在许多应用中效果良好。然而,对于维度极高、数据量极大的数据集,这会产生可扩展性问题,从而降低搜索效率。近似 k-NN 搜索方法通过采用更有效地重组索引和降低可搜索向量维度的工具,可以克服这一问题。使用这种方法需要牺牲一定的准确性,但能显著提高搜索处理速度。

UDB-SX 中的近似 k-NN 搜索方法使用来自 NMSLIBFaissLucene 库的近似最近邻算法来驱动 k-NN 搜索。这些搜索方法利用 ANN 来提高大型数据集的搜索延迟。在 UDB-SX 提供的三种搜索方法中,此方法为大型数据集提供了最佳的搜索可扩展性。当数据集达到数十万个向量时,此方法是首选方法。

有关 UDB-SX 支持的算法的信息,请参阅 方法与引擎

在索引过程中,UDB-SX 会为每个 knn-vector 字段/Lucene 段对构建一个向量的原生库索引,该索引可在搜索期间用于高效查找查询向量的 k 个最近邻。要了解更多关于 Lucene 段的信息,请参阅 Apache Lucene 文档。这些原生库索引在搜索期间被加载到原生内存中,并由缓存管理。要了解更多关于将原生库索引预加载到内存中的信息,请参阅 预热 API。此外,您可以使用统计 API 查看哪些原生库索引已加载到内存中。

由于原生库索引是在索引构建期间创建的,因此无法先在索引上应用过滤器再使用此搜索方法。所有过滤器都是在 ANN 搜索产生的结果上应用的。

开始使用近似 k-NN

要使用近似搜索功能,您必须首先创建一个将 index.knn 设置为 true 的向量索引。此设置告诉 UDB-SX 为该索引创建原生库索引。

接下来,您必须添加一个或多个 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
            }
          }
        }
    }
  }
}

在 UDB-SX 中,编解码器负责索引的存储和检索。UDB-SX 使用自定义编解码器将向量数据写入原生库索引,以便底层的 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 个结果。

下表提供了几种场景下不同引擎返回结果数量的示例。对于这些示例,假设段和分片中包含的文档数量足以返回表中指定的结果数。

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 返回的结果数不同。如果 ksize 相等,所有引擎返回的结果数相同。

从 UDB-SX 2.14 开始,您可以在径向搜索中使用 kmin_scoremax_distance 参数。

基于模型构建向量索引

对于 UDB-SX 支持的一些算法,原生库索引需要先经过训练才能使用。训练每个新创建的段成本很高,因此 UDB-SX 引入了模型的概念,在段创建期间初始化原生库索引。您可以通过调用训练 API 并传入训练数据源和模型的方法定义来创建模型。训练完成后,模型会被序列化到 k-NN 模型系统索引中。然后,在索引期间,从该索引中提取模型以初始化段。

要训练模型,您首先需要一个包含训练数据的 UDB-SX 索引。训练数据可以来自任何 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]}

完成对训练索引的索引操作后,您可以调用训练 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
        }
      }
    }
  }
}

训练 API 会在训练作业启动后立即返回。要检查作业状态,请使用获取模型 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 字段一样进行搜索。