k-最近邻 (kNN) 搜索编辑

k-最近邻 (kNN) 搜索根据相似度指标找到与查询向量k个最接近的向量。

kNN 的常见用例包括

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

先决条件编辑

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

    设计您的向量,以便根据相似度指标,文档向量越接近查询向量,其匹配度就越好。

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

    • create_indexmanage 以创建具有 dense_vector 字段的索引
    • createindexwrite 以将数据添加到您创建的索引中
    • read 以搜索索引

kNN 方法编辑

Elasticsearch 支持两种 kNN 搜索方法

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

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

近似 kNN编辑

与其他类型的搜索相比,近似 kNN 搜索具有特定的资源要求。特别是,所有向量数据都必须适合节点的页面缓存才能高效。请参阅近似 kNN 搜索调整指南,了解有关配置和大小调整的重要说明。

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

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

    • 一个 similarity 值。此值确定用于根据查询向量和文档向量之间的相似性对文档进行评分的相似度指标。有关可用指标的列表,请参阅similarity 参数文档。 similarity 设置默认为 cosine
    response = client.indices.create(
      index: 'image-index',
      body: {
        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'
            }
          }
        }
      }
    )
    puts response
    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 查询(专家案例)运行搜索。

    response = client.search(
      index: 'image-index',
      body: {
        knn: {
          field: 'image-vector',
          query_vector: [
            -5,
            9,
            -12
          ],
          k: 10,
          num_candidates: 100
        },
        fields: [
          'title',
          'file-type'
        ]
      }
    )
    puts response
    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 以提高速度或准确性编辑

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

您可以增加 num_candidates 以获得更准确的结果,但代价是搜索速度变慢。具有较高 num_candidates 值的搜索会考虑来自每个分片的更多候选者。这需要更多时间,但搜索更有可能找到真正的前 k 个最近邻。

同样,您可以减少 num_candidates 以获得更快的搜索,但结果可能不太准确。

使用字节向量进行近似 kNN编辑

近似 kNN 搜索 API 除了支持 float 值向量之外,还支持 byte 值向量。使用knn 选项 搜索 dense_vector 字段,该字段的element_type 设置为 byte 且已启用索引。

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

    response = client.indices.create(
      index: 'byte-image-index',
      body: {
        mappings: {
          properties: {
            "byte-image-vector": {
              type: 'dense_vector',
              element_type: 'byte',
              dims: 2
            },
            title: {
              type: 'text'
            }
          }
        }
      }
    )
    puts response
    PUT byte-image-index
    {
      "mappings": {
        "properties": {
          "byte-image-vector": {
            "type": "dense_vector",
            "element_type": "byte",
            "dims": 2
          },
          "title": {
            "type": "text"
          }
        }
      }
    }
  2. 索引您的数据,确保所有向量值都是介于 [-128, 127] 范围内的整数。

    response = client.bulk(
      index: 'byte-image-index',
      refresh: true,
      body: [
        {
          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'
        }
      ]
    )
    puts response
    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] 范围内的整数。

    response = client.search(
      index: 'byte-image-index',
      body: {
        knn: {
          field: 'byte-image-vector',
          query_vector: [
            -5,
            9
          ],
          k: 10,
          num_candidates: 100
        },
        fields: [
          'title'
        ]
      }
    )
    puts response
    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 搜索编辑

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

dense_vector 的默认索引类型是 int8_hnsw

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

response = client.indices.create(
  index: 'quantized-image-index',
  body: {
    mappings: {
      properties: {
        "image-vector": {
          type: 'dense_vector',
          element_type: 'float',
          dims: 2,
          index: true,
          index_options: {
            type: 'int8_hnsw'
          }
        },
        title: {
          type: 'text'
        }
      }
    }
  }
)
puts response
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 向量。

    response = client.bulk(
      index: 'quantized-image-index',
      refresh: true,
      body: [
        {
          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'
        }
      ]
    )
    puts response
    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 向量。

    response = client.search(
      index: 'quantized-image-index',
      body: {
        knn: {
          field: 'image-vector',
          query_vector: [
            0.1,
            -2
          ],
          k: 10,
          num_candidates: 100
        },
        fields: [
          'title'
        ]
      }
    )
    puts response
    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 个结果进行重新评分。这提供了两全其美的优势,即快速搜索和准确的评分。

response = client.search(
  index: 'quantized-image-index',
  body: {
    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
                ]
              }
            }
          }
        }
      }
    }
  }
)
puts response
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 搜索编辑

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

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

response = client.search(
  index: 'image-index',
  body: {
    knn: {
      field: 'image-vector',
      query_vector: [
        54,
        10,
        -2
      ],
      k: 5,
      num_candidates: 50,
      filter: {
        term: {
          "file-type": 'png'
        }
      }
    },
    fields: [
      'title'
    ],
    _source: false
  }
)
puts response
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 搜索和过滤编辑

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

为了避免明显的性能缺陷,Lucene 对每个段实现了以下策略

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

将近似 kNN 与其他功能结合编辑

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

response = client.search(
  index: 'image-index',
  body: {
    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
  }
)
puts response
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 向量字段

response = client.search(
  index: 'image-index',
  body: {
    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
  }
)
puts response
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
}

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

具有上述配置的 boost 的文档的评分将是

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

使用预期相似度搜索 kNNedit

虽然 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_vector 以获取 k 个最近邻。但是,应用了 filter 并要求找到的向量之间至少具有提供的 similarity

response = client.search(
  index: 'image-index',
  body: {
    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
  }
)
puts response
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
                    }
                }
            }
        }
    }
}

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

response = client.bulk(
  index: 'passage_vectors',
  refresh: true,
  body: [
    {
      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'
        }
      ]
    }
  ]
)
puts response
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 搜索非常相似

response = client.search(
  index: 'passage_vectors',
  body: {
    fields: [
      'full_text',
      'creation_time'
    ],
    _source: false,
    knn: {
      query_vector: [
        0.45,
        45
      ],
      field: 'paragraph.vector',
      k: 2,
      num_candidates: 2
    }
  }
)
puts response
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"
                    ]
                }
            }
        ]
    }
}

如果您想根据一些顶级文档元数据进行过滤怎么办?您可以通过将 filter 添加到您的 knn 子句来实现。

filter 将始终在顶级文档元数据上。这意味着您无法根据 nested 字段元数据进行过滤。

response = client.search(
  index: 'passage_vectors',
  body: {
    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'
                }
              }
            }
          ]
        }
      }
    }
  }
)
puts response
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

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

当使用 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 参数来调整这些参数

response = client.indices.create(
  index: 'image-index',
  body: {
    mappings: {
      properties: {
        "image-vector": {
          type: 'dense_vector',
          dims: 3,
          similarity: 'l2_norm',
          index_options: {
            type: 'hnsw',
            m: 32,
            ef_construction: 100
          }
        }
      }
    }
  }
)
puts response
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

精确 kNNedit

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

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

    response = client.indices.create(
      index: 'product-index',
      body: {
        mappings: {
          properties: {
            "product-vector": {
              type: 'dense_vector',
              dims: 5,
              index: false
            },
            price: {
              type: 'long'
            }
          }
        }
      }
    )
    puts response
    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 查询 来匹配所有文档。但是,匹配所有文档可能会显著增加搜索延迟。

    response = client.search(
      index: 'product-index',
      body: {
        query: {
          script_score: {
            query: {
              bool: {
                filter: {
                  range: {
                    price: {
                      gte: 1000
                    }
                  }
                }
              }
            },
            script: {
              source: "cosineSimilarity(params.queryVector, 'product-vector') + 1.0",
              params: {
                "queryVector": [
                  -0.5,
                  90,
                  -10,
                  14.8,
                  -156
                ]
              }
            }
          }
        }
      }
    )
    puts response
    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]
            }
          }
        }
      }
    }