k-近邻 (kNN) 搜索

edit

一个k近邻(kNN)搜索找到与查询向量最接近的k个向量,通过相似性度量来衡量。

kNN的常见用例包括:

  • 基于自然语言处理(NLP)算法的关联排序
  • 产品推荐和推荐引擎
  • 图像或视频的相似性搜索

先决条件

edit
  • 要运行kNN搜索,您必须能够将数据转换为有意义的向量值。您可以使用 在Elasticsearch中使用自然语言处理(NLP)模型创建这些向量,或者在Elasticsearch外部生成它们。向量可以作为dense_vector字段值添加到文档中。查询表示为具有相同维度的向量。

    设计你的向量,使得基于相似度度量的文档向量越接近查询向量,匹配度越好。

  • 要完成本指南中的步骤,您必须具备以下 索引权限

    • create_indexmanage 以创建带有dense_vector字段的索引
    • createindexwrite 以向您创建的索引添加数据
    • read 以搜索索引

kNN 方法

edit

Elasticsearch 支持两种 kNN 搜索方法:

在大多数情况下,您会希望使用近似kNN。近似kNN以较慢的索引和不太完美的准确性为代价,提供了更低的延迟。

精确的暴力kNN保证准确的结果,但在处理大数据集时扩展性不佳。使用这种方法,script_score查询必须扫描每个匹配的文档以计算向量函数,这可能导致搜索速度变慢。然而,您可以通过使用查询来限制传递给函数的匹配文档数量,从而改善延迟。如果您将数据过滤到一小部分文档,使用这种方法可以获得良好的搜索性能。

近似kNN

edit

与其他类型的搜索相比,近似kNN搜索有特定的资源需求。特别是,所有向量数据必须适合节点的页面缓存,以确保其高效性。请参阅近似kNN搜索调优指南以获取关于配置和大小调整的重要说明。

要运行近似的kNN搜索,请使用knn选项 来搜索一个或多个启用了索引的dense_vector字段。

  1. 显式映射一个或多个dense_vector字段。近似kNN搜索需要以下映射选项:

    • 一个similarity值。此值确定用于根据查询和文档向量之间的相似性对文档进行评分的相似性度量。有关可用度量的列表,请参阅similarity参数文档。similarity设置默认为cosine
    PUT image-index
    {
      "mappings": {
        "properties": {
          "image-vector": {
            "type": "dense_vector",
            "dims": 3,
            "similarity": "l2_norm"
          },
          "title-vector": {
            "type": "dense_vector",
            "dims": 5,
            "similarity": "l2_norm"
          },
          "title": {
            "type": "text"
          },
          "file-type": {
            "type": "keyword"
          }
        }
      }
    }
  2. 索引您的数据。

    POST image-index/_bulk?refresh=true
    { "index": { "_id": "1" } }
    { "image-vector": [1, 5, -20], "title-vector": [12, 50, -10, 0, 1], "title": "moose family", "file-type": "jpg" }
    { "index": { "_id": "2" } }
    { "image-vector": [42, 8, -15], "title-vector": [25, 1, 4, -12, 2], "title": "alpine lake", "file-type": "png" }
    { "index": { "_id": "3" } }
    { "image-vector": [15, 11, 23], "title-vector": [1, 5, 25, 50, 20], "title": "full moon", "file-type": "jpg" }
    ...
  3. 使用knn选项knn查询(专家案例)运行搜索。

    POST image-index/_search
    {
      "knn": {
        "field": "image-vector",
        "query_vector": [-5, 9, -12],
        "k": 10,
        "num_candidates": 100
      },
      "fields": [ "title", "file-type" ]
    }

文档的 文档 _score 是由查询和文档向量之间的相似性决定的。有关 kNN 搜索分数如何计算的更多信息,请参见 similarity

在版本8.0中添加了对近似kNN搜索的支持。在此之前,dense_vector字段不支持在映射中启用index。如果你在8.0之前创建了一个包含dense_vector字段的索引,那么为了支持近似kNN搜索,必须使用设置index: true的新字段映射对数据进行重新索引,这是默认选项。

调整近似kNN以提高速度或准确性

edit

为了收集结果,kNN搜索API在每个分片上找到num_candidates个近似的最近邻候选者。搜索计算这些候选向量与查询向量之间的相似度,从每个分片中选择最相似的k个结果。然后,搜索将每个分片的结果合并,以返回全局的前k个最近邻。

您可以增加 num_candidates 以提高结果的准确性,但代价是搜索速度会变慢。使用高值进行搜索时,num_candidates 会从每个分片中考虑更多的候选者。这会花费更多时间,但搜索找到真实 k 个最近邻的概率更高。

同样地,您可以减少 num_candidates 以加快搜索速度,但可能会导致结果的准确性降低。

使用字节向量的近似kNN

edit

近似kNN搜索API除了支持float值向量外,还支持byte值向量。使用knn选项来搜索一个dense_vector字段,并将其element_type设置为byte并启用索引。

  1. 显式映射一个或多个dense_vector字段,并将element_type设置为byte并启用索引。

    PUT byte-image-index
    {
      "mappings": {
        "properties": {
          "byte-image-vector": {
            "type": "dense_vector",
            "element_type": "byte",
            "dims": 2
          },
          "title": {
            "type": "text"
          }
        }
      }
    }
  2. 索引您的数据,确保所有向量值都在范围 [-128, 127] 内的整数。

    POST byte-image-index/_bulk?refresh=true
    { "index": { "_id": "1" } }
    { "byte-image-vector": [5, -20], "title": "moose family" }
    { "index": { "_id": "2" } }
    { "byte-image-vector": [8, -15], "title": "alpine lake" }
    { "index": { "_id": "3" } }
    { "byte-image-vector": [11, 23], "title": "full moon" }
  3. 使用knn选项运行搜索, 确保query_vector值在范围[-128, 127]内的整数。

    POST byte-image-index/_search
    {
      "knn": {
        "field": "byte-image-vector",
        "query_vector": [-5, 9],
        "k": 10,
        "num_candidates": 100
      },
      "fields": [ "title" ]
    }

注意:除了标准的字节数组外,还可以为query_vector参数提供一个十六进制编码的字符串值。例如,上面的搜索请求也可以表示如下,这将产生相同的结果

POST byte-image-index/_search
{
  "knn": {
    "field": "byte-image-vector",
    "query_vector": "fb09",
    "k": 10,
    "num_candidates": 100
  },
  "fields": [ "title" ]
}

字节量化kNN搜索

edit

如果你想提供float向量,但又希望节省byte向量的内存,你可以使用量化功能。量化允许你提供float向量,但在内部它们被索引为byte向量。此外,原始的float向量仍然保留在索引中。

默认的索引类型为 dense_vectorint8_hnsw

要使用量化,您可以在 dense_vector 映射中使用索引类型 int8_hnswint4_hnsw 对象。

PUT quantized-image-index
{
  "mappings": {
    "properties": {
      "image-vector": {
        "type": "dense_vector",
        "element_type": "float",
        "dims": 2,
        "index": true,
        "index_options": {
          "type": "int8_hnsw"
        }
      },
      "title": {
        "type": "text"
      }
    }
  }
}
  1. 索引你的 float 向量。

    POST quantized-image-index/_bulk?refresh=true
    { "index": { "_id": "1" } }
    { "image-vector": [0.1, -2], "title": "moose family" }
    { "index": { "_id": "2" } }
    { "image-vector": [0.75, -1], "title": "alpine lake" }
    { "index": { "_id": "3" } }
    { "image-vector": [1.2, 0.1], "title": "full moon" }
  2. 使用knn选项运行搜索。在搜索时,float向量会自动量化为byte向量。

    POST quantized-image-index/_search
    {
      "knn": {
        "field": "image-vector",
        "query_vector": [0.1, -2],
        "k": 10,
        "num_candidates": 100
      },
      "fields": [ "title" ]
    }

由于原始的 float 向量仍然保留在索引中,您可以选择使用它们进行重新评分。这意味着,您可以使用 int8_hnsw 索引快速搜索所有向量,然后仅对前 k 个结果进行重新评分。这提供了两全其美的效果,即快速搜索和准确评分。

POST quantized-image-index/_search
{
  "knn": {
    "field": "image-vector",
    "query_vector": [0.1, -2],
    "k": 15,
    "num_candidates": 100
  },
  "fields": [ "title" ],
  "rescore": {
    "window_size": 10,
    "query": {
      "rescore_query": {
        "script_score": {
          "query": {
            "match_all": {}
          },
          "script": {
            "source": "cosineSimilarity(params.query_vector, 'image-vector') + 1.0",
            "params": {
              "query_vector": [0.1, -2]
            }
          }
        }
      }
    }
  }
}

过滤的 kNN 搜索

edit

kNN 搜索 API 支持使用过滤器来限制搜索。搜索将返回与过滤器查询匹配的前 k 个文档。

以下请求执行按 file-type 字段过滤的近似 kNN 搜索:

POST image-index/_search
{
  "knn": {
    "field": "image-vector",
    "query_vector": [54, 10, -2],
    "k": 5,
    "num_candidates": 50,
    "filter": {
      "term": {
        "file-type": "png"
      }
    }
  },
  "fields": ["title"],
  "_source": false
}

过滤器在近似kNN搜索期间应用,以确保返回k个匹配的文档。这与后过滤方法形成对比,在后过滤方法中,过滤器在近似kNN搜索完成后应用。后过滤的一个缺点是,即使有足够的匹配文档,它有时也会返回少于k个结果。

近似kNN搜索和过滤

edit

与传统查询过滤不同,在传统查询过滤中,更严格的过滤通常会导致更快的查询,而在使用HNSW索引进行近似kNN搜索时应用过滤器可能会降低性能。这是因为搜索HNSW图需要额外的探索来获取满足过滤条件的num_candidates

为了避免显著的性能下降,Lucene 为每个段实现了以下策略:

  • 如果过滤后的文档数量小于或等于num_candidates,搜索将绕过HNSW图,并在过滤后的文档上使用暴力搜索。
  • 在探索HNSW图时,如果探索的节点数量超过满足过滤条件的文档数量,搜索将停止探索图并切换到对过滤后的文档进行暴力搜索。

结合近似kNN与其他特征

edit

您可以通过提供knn选项query来执行混合检索

POST image-index/_search
{
  "query": {
    "match": {
      "title": {
        "query": "mountain lake",
        "boost": 0.9
      }
    }
  },
  "knn": {
    "field": "image-vector",
    "query_vector": [54, 10, -2],
    "k": 5,
    "num_candidates": 50,
    "boost": 0.1
  },
  "size": 10
}

此搜索找到全局前 k = 5 个向量匹配项,将它们与 match 查询的匹配项结合,并最终返回得分最高的 10 个结果。knnquery 匹配项通过一个析取结合,就像你在它们之间进行布尔 操作一样。前 k 个向量结果代表所有索引分片中的全局最近邻。

每次命中的得分是 knnquery 得分的总和。您可以指定一个 boost 值来为总和中的每个得分赋予权重。在上面的示例中,得分将计算为

score = 0.9 * match_score + 0.1 * knn_score

knn 选项也可以与 aggregations 一起使用。 通常,Elasticsearch 会计算与搜索匹配的所有文档的聚合。 因此,对于近似的 kNN 搜索,聚合是在最接近的 k 个文档上计算的。如果搜索还包括一个 query,那么聚合是在 knnquery 匹配的组合集上计算的。

执行语义搜索

edit

kNN 搜索使您能够通过使用先前部署的 文本嵌入模型 来执行语义搜索。与基于搜索词的字面匹配不同,语义搜索根据搜索查询的意图和上下文含义检索结果。

在底层,文本嵌入NLP模型从您提供的输入查询字符串生成一个密集向量,称为model_text。然后,它会在一个包含使用相同文本嵌入机器学习模型创建的密集向量的索引中进行搜索。搜索结果在语义上是相似的,这是由模型学习到的。

执行语义搜索:

  • 你需要一个包含输入数据的密集向量表示的索引来搜索,
  • 你必须使用与创建输入数据的密集向量时相同的文本嵌入模型进行搜索,
  • 文本嵌入NLP模型的部署必须启动。

引用部署的文本嵌入模型或模型部署在 query_vector_builder 对象中,并将搜索查询作为 model_text 提供:

(...)
{
  "knn": {
    "field": "dense-vector-field",
    "k": 10,
    "num_candidates": 100,
    "query_vector_builder": {
      "text_embedding": { 
        "model_id": "my-text-embedding-model", 
        "model_text": "The opposite of blue" 
      }
    }
  }
}
(...)

要执行的自然语言处理任务。它必须是 text_embedding

用于从查询字符串生成密集向量的文本嵌入模型的ID。使用与生成索引中输入文本嵌入相同的模型。你可以在model_id参数中使用deployment_id的值。

模型生成稠密向量表示的查询字符串。

有关如何部署训练好的模型并使用它来创建文本嵌入的更多信息,请参阅此端到端示例

搜索多个kNN字段

edit

除了混合检索,您还可以一次搜索多个kNN向量字段:

POST image-index/_search
{
  "query": {
    "match": {
      "title": {
        "query": "mountain lake",
        "boost": 0.9
      }
    }
  },
  "knn": [ {
    "field": "image-vector",
    "query_vector": [54, 10, -2],
    "k": 5,
    "num_candidates": 50,
    "boost": 0.1
  },
  {
    "field": "title-vector",
    "query_vector": [1, 20, -52, 23, 10],
    "k": 10,
    "num_candidates": 10,
    "boost": 0.5
  }],
  "size": 10
}

此搜索找到全局前 k = 5 个与 image-vector 匹配的向量,以及全局前 k = 10 个与 title-vector 匹配的向量。 这些最高值随后与来自 match 查询的匹配项结合,并返回前10个文档。 多个 knn 条目和 query 匹配项通过一个析取结合, 就像你在它们之间进行布尔 运算一样。前 k 个向量结果表示跨所有索引分片的全球最近邻。

上述配置的提升值的文档评分将是:

score = 0.9 * match_score + 0.1 * knn_score_image-vector + 0.5 * knn_score_title-vector

使用预期相似度搜索kNN

edit

虽然kNN是一个强大的工具,但它总是尝试返回k个最近的邻居。因此,当使用knnfilter时,你可能会过滤掉所有相关的文档,只剩下不相关的文档进行搜索。在这种情况下,knn仍然会尽力返回k个最近的邻居,即使这些邻居在向量空间中可能相距很远。

为了缓解这种担忧,knn 子句中提供了一个 similarity 参数。该值是向量被视为匹配项所需的最小相似度。使用此参数的 knn 搜索流程如下:

  • 应用任何用户提供的filter查询
  • 探索向量空间以获取k个向量
  • 不要返回任何距离超过配置的similarity的向量

similarity 是在被转换为 _score 并应用 boost 之前的真实 相似度

对于每个配置的相似性,这里是对应的倒置_score函数。这是为了如果你想要从_score的角度进行过滤,你可以进行这个小的转换以正确地拒绝不相关的结果。

  • l2_norm: sqrt((1 / _score) - 1)
  • cosine: (2 * _score) - 1
  • dot_product: (2 * _score) - 1
  • max_inner_product:

    • _score < 1: 1 - (1 / _score)
    • _score >= 1: _score - 1

这里是一个示例。在这个示例中,我们搜索给定的 query_vectork 个最近邻。然而,应用了 filter 并且要求找到的向量之间至少具有提供的 similarity

POST image-index/_search
{
  "knn": {
    "field": "image-vector",
    "query_vector": [1, 5, -20],
    "k": 5,
    "num_candidates": 50,
    "similarity": 36,
    "filter": {
      "term": {
        "file-type": "png"
      }
    }
  },
  "fields": ["title"],
  "_source": false
}

在我们的数据集中,唯一一个文件类型为png的文档的向量是[42, 8, -15][42, 8, -15][1, 5, -20]之间的l2_norm距离是41.412,这大于配置的相似度36。这意味着,这次搜索将不会返回任何结果。

嵌套 kNN 搜索

edit

文本超出特定模型的标记限制并需要在构建嵌入之前进行分块是很常见的。当使用nesteddense_vector时,您可以在不复制顶级文档元数据的情况下实现最近段落检索。

这是一个简单的段落向量索引,用于存储向量和一些用于过滤的顶级元数据。

PUT passage_vectors
{
    "mappings": {
        "properties": {
            "full_text": {
                "type": "text"
            },
            "creation_time": {
                "type": "date"
            },
            "paragraph": {
                "type": "nested",
                "properties": {
                    "vector": {
                        "type": "dense_vector",
                        "dims": 2,
                        "index_options": {
                            "type": "hnsw"
                        }
                    },
                    "text": {
                        "type": "text",
                        "index": false
                    }
                }
            }
        }
    }
}

通过上述映射,我们可以索引多个段落向量,同时存储各个段落的文本。

POST passage_vectors/_bulk?refresh=true
{ "index": { "_id": "1" } }
{ "full_text": "first paragraph another paragraph", "creation_time": "2019-05-04", "paragraph": [ { "vector": [ 0.45, 45 ], "text": "first paragraph", "paragraph_id": "1" }, { "vector": [ 0.8, 0.6 ], "text": "another paragraph", "paragraph_id": "2" } ] }
{ "index": { "_id": "2" } }
{ "full_text": "number one paragraph number two paragraph", "creation_time": "2020-05-04", "paragraph": [ { "vector": [ 1.2, 4.5 ], "text": "number one paragraph", "paragraph_id": "1" }, { "vector": [ -1, 42 ], "text": "number two paragraph", "paragraph_id": "2" } ] }

该查询看起来与典型的kNN搜索非常相似:

POST passage_vectors/_search
{
    "fields": ["full_text", "creation_time"],
    "_source": false,
    "knn": {
        "query_vector": [
            0.45,
            45
        ],
        "field": "paragraph.vector",
        "k": 2,
        "num_candidates": 2
    }
}

请注意,尽管我们有4个总向量,我们仍然返回两个文档。在嵌套的dense_vectors上进行kNN搜索将始终在顶级文档上多样化顶部结果。这意味着,将返回"k"个顶级文档,按其最近的段落向量(例如"paragraph.vector")进行评分。

{
    "took": 4,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 2,
            "relation": "eq"
        },
        "max_score": 1.0,
        "hits": [
            {
                "_index": "passage_vectors",
                "_id": "1",
                "_score": 1.0,
                "fields": {
                    "creation_time": [
                        "2019-05-04T00:00:00.000Z"
                    ],
                    "full_text": [
                        "first paragraph another paragraph"
                    ]
                }
            },
            {
                "_index": "passage_vectors",
                "_id": "2",
                "_score": 0.9997144,
                "fields": {
                    "creation_time": [
                        "2020-05-04T00:00:00.000Z"
                    ],
                    "full_text": [
                        "number one paragraph number two paragraph"
                    ]
                }
            }
        ]
    }
}

如果你想根据一些顶级文档元数据进行过滤,可以通过在knn子句中添加filter来实现。

filter 将始终覆盖顶级文档元数据。这意味着您不能基于 nested 字段元数据进行过滤。

POST passage_vectors/_search
{
    "fields": [
        "creation_time",
        "full_text"
    ],
    "_source": false,
    "knn": {
        "query_vector": [
            0.45,
            45
        ],
        "field": "paragraph.vector",
        "k": 2,
        "num_candidates": 2,
        "filter": {
            "bool": {
                "filter": [
                    {
                        "range": {
                            "creation_time": {
                                "gte": "2019-05-01",
                                "lte": "2019-05-05"
                            }
                        }
                    }
                ]
            }
        }
    }
}

现在我们已经根据顶级"creation_time"进行了过滤,并且只有一个文档落在这个范围内。

{
    "took": 4,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 1,
            "relation": "eq"
        },
        "max_score": 1.0,
        "hits": [
            {
                "_index": "passage_vectors",
                "_id": "1",
                "_score": 1.0,
                "fields": {
                    "creation_time": [
                        "2019-05-04T00:00:00.000Z"
                    ],
                    "full_text": [
                        "first paragraph another paragraph"
                    ]
                }
            }
        ]
    }
}

嵌套 kNN 搜索与内部命中

edit

此外,如果您想为匹配的文档提取最近的段落,您可以向knn子句提供inner_hits

在使用 inner_hits 和多个 knn 子句时,请务必指定 inner_hits.name 字段。否则,可能会发生命名冲突并导致搜索请求失败。

POST passage_vectors/_search
{
    "fields": [
        "creation_time",
        "full_text"
    ],
    "_source": false,
    "knn": {
        "query_vector": [
            0.45,
            45
        ],
        "field": "paragraph.vector",
        "k": 2,
        "num_candidates": 2,
        "inner_hits": {
            "_source": false,
            "fields": [
                "paragraph.text"
            ],
            "size": 1
        }
    }
}

现在搜索时,结果将包含找到的最接近的段落。

{
    "took": 4,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 2,
            "relation": "eq"
        },
        "max_score": 1.0,
        "hits": [
            {
                "_index": "passage_vectors",
                "_id": "1",
                "_score": 1.0,
                "fields": {
                    "creation_time": [
                        "2019-05-04T00:00:00.000Z"
                    ],
                    "full_text": [
                        "first paragraph another paragraph"
                    ]
                },
                "inner_hits": {
                    "paragraph": {
                        "hits": {
                            "total": {
                                "value": 2,
                                "relation": "eq"
                            },
                            "max_score": 1.0,
                            "hits": [
                                {
                                    "_index": "passage_vectors",
                                    "_id": "1",
                                    "_nested": {
                                        "field": "paragraph",
                                        "offset": 0
                                    },
                                    "_score": 1.0,
                                    "fields": {
                                        "paragraph": [
                                            {
                                                "text": [
                                                    "first paragraph"
                                                ]
                                            }
                                        ]
                                    }
                                }
                            ]
                        }
                    }
                }
            },
            {
                "_index": "passage_vectors",
                "_id": "2",
                "_score": 0.9997144,
                "fields": {
                    "creation_time": [
                        "2020-05-04T00:00:00.000Z"
                    ],
                    "full_text": [
                        "number one paragraph number two paragraph"
                    ]
                },
                "inner_hits": {
                    "paragraph": {
                        "hits": {
                            "total": {
                                "value": 2,
                                "relation": "eq"
                            },
                            "max_score": 0.9997144,
                            "hits": [
                                {
                                    "_index": "passage_vectors",
                                    "_id": "2",
                                    "_nested": {
                                        "field": "paragraph",
                                        "offset": 1
                                    },
                                    "_score": 0.9997144,
                                    "fields": {
                                        "paragraph": [
                                            {
                                                "text": [
                                                    "number two paragraph"
                                                ]
                                            }
                                        ]
                                    }
                                }
                            ]
                        }
                    }
                }
            }
        ]
    }
}

索引注意事项

edit

对于近似kNN搜索,Elasticsearch将每个段的密集向量值存储为一个HNSW图。由于构建这些图的成本很高,为近似kNN搜索索引向量可能需要大量时间。您可能需要增加客户端请求的超时时间,以用于索引和批量请求。近似kNN调整指南包含了关于索引性能的重要指导,以及索引配置如何影响搜索性能。

除了搜索时的调优参数外,HNSW算法还具有索引时的参数,这些参数在构建图的成本、搜索速度和准确性之间进行权衡。在设置dense_vector映射时,您可以使用index_options参数来调整这些参数:

PUT image-index
{
  "mappings": {
    "properties": {
      "image-vector": {
        "type": "dense_vector",
        "dims": 3,
        "similarity": "l2_norm",
        "index_options": {
          "type": "hnsw",
          "m": 32,
          "ef_construction": 100
        }
      }
    }
  }
}

近似kNN搜索的限制

edit
  • 在使用跨集群搜索中的kNN搜索时,不支持ccs_minimize_roundtrips选项。
  • Elasticsearch使用HNSW算法来支持高效的kNN搜索。与大多数kNN算法一样,HNSW是一种近似方法,它牺牲了结果的准确性以提高搜索速度。这意味着返回的结果并不总是真正的k个最近邻。

近似kNN搜索总是使用 dfs_query_then_fetch 搜索类型,以便在分片之间收集全局前 k 个匹配项。在运行kNN搜索时,您不能显式设置 search_type

精确kNN

edit

要运行精确的 kNN 搜索,请使用带有向量函数的 script_score 查询。

  1. 显式映射一个或多个dense_vector字段。如果你不打算将该字段用于近似kNN,请将index映射选项设置为false。这可以显著提高索引速度。

    PUT product-index
    {
      "mappings": {
        "properties": {
          "product-vector": {
            "type": "dense_vector",
            "dims": 5,
            "index": false
          },
          "price": {
            "type": "long"
          }
        }
      }
    }
  2. 索引您的数据。

    POST product-index/_bulk?refresh=true
    { "index": { "_id": "1" } }
    { "product-vector": [230.0, 300.33, -34.8988, 15.555, -200.0], "price": 1599 }
    { "index": { "_id": "2" } }
    { "product-vector": [-0.5, 100.0, -13.0, 14.8, -156.0], "price": 799 }
    { "index": { "_id": "3" } }
    { "product-vector": [0.5, 111.3, -13.0, 14.8, -156.0], "price": 1099 }
    ...
  3. 使用搜索API来运行一个包含script_score查询的查询,其中包含一个向量函数

    为了限制传递给向量函数的匹配文档数量,我们建议您在script_score.query参数中指定一个过滤查询。如果需要,您可以在此参数中使用match_all查询来匹配所有文档。然而,匹配所有文档可能会显著增加搜索延迟。

    POST product-index/_search
    {
      "query": {
        "script_score": {
          "query" : {
            "bool" : {
              "filter" : {
                "range" : {
                  "price" : {
                    "gte": 1000
                  }
                }
              }
            }
          },
          "script": {
            "source": "cosineSimilarity(params.queryVector, 'product-vector') + 1.0",
            "params": {
              "queryVector": [-0.5, 90.0, -10, 14.8, -156.0]
            }
          }
        }
      }
    }

量化向量的过采样和重新评分

edit

所有形式的量化都会导致一定的精度损失,并且随着量化级别的增加,精度损失也会增加。 一般来说,我们发现: - int8 几乎不需要重新评分 - int4 在需要更高精度和更大召回率的场景中需要一些重新评分。通常,通过1.5倍到2倍的过采样可以恢复大部分的精度损失。 - bbq 需要重新评分,除非在特别大的索引或专门为量化设计的模型上。我们发现,通常3倍到5倍的过采样就足够了。但对于维度较少或量化效果不佳的向量,可能需要更高的过采样。

有两种主要的方法来进行过采样和重新评分。第一种是利用重新评分部分_search请求中。

这里是一个使用顶层 knn 搜索进行过采样并通过 rescore 重新排序结果的示例:

POST /my-index/_search
{
  "size": 10, 
  "knn": {
    "query_vector": [0.04283529, 0.85670587, -0.51402352, 0],
    "field": "my_int4_vector",
    "k": 20, 
    "num_candidates": 50
  },
  "rescore": {
    "window_size": 20, 
    "query": {
      "rescore_query": {
        "script_score": {
          "query": {
            "match_all": {}
          },
          "script": {
            "source": "(dotProduct(params.queryVector, 'my_int4_vector') + 1.0)", 
            "params": {
              "queryVector": [0.04283529, 0.85670587, -0.51402352, 0]
            }
          }
        }
      },
      "query_weight": 0, 
      "rescore_query_weight": 1 
    }
  }
}

返回的结果数量,注意它只有10个,我们将以2倍的比例进行过采样,收集20个最近的邻居。

从KNN搜索返回的结果数量。这将使用每个HNSW图的50个候选者进行近似的KNN搜索,并使用量化向量,返回根据量化评分最相似的20个向量。此外,由于这是顶层的knn对象,所有分片的20个最佳结果将在重新评分之前汇总。与rescore结合使用时,这是以2x的比例进行过采样,意味着根据量化评分收集20个最近邻,并使用更高保真度的浮点向量进行重新评分。

要重新评分的查询结果数量,如果希望重新评分所有结果,请将其设置为与k相同的值

重新评分结果的脚本。脚本评分将直接与最初提供的float32向量进行交互。

原始查询的权重,这里我们简单地丢弃原始分数

重打分查询的权重,这里我们只使用重打分查询

第二种方法是使用knn查询script_score查询按分片进行评分。通常,这意味着每个分片会有更多的重新评分,但这可以提高整体召回率,代价是计算成本增加。

POST /my-index/_search
{
  "size": 10, 
  "query": {
    "script_score": {
      "query": {
        "knn": { 
          "query_vector": [0.04283529, 0.85670587, -0.51402352, 0],
          "field": "my_int4_vector",
          "num_candidates": 20 
        }
      },
      "script": {
        "source": "(dotProduct(params.queryVector, 'my_int4_vector') + 1.0)", 
        "params": {
          "queryVector": [0.04283529, 0.85670587, -0.51402352, 0]
        }
      }
    }
  }
}

返回的结果数量

执行初始搜索的 knn 查询,这是按分片执行的

用于初始近似knn搜索的候选数量。这将使用量化向量进行搜索,并返回每个分片的前20个候选者以进行评分

用于评分结果的脚本。脚本评分将直接与最初提供的float32向量进行交互。