向量 KNN 检索

更新时间:
复制 MD 格式

Lindorm 图引擎可以将向量作为顶点属性存储,并通过向量引擎提供的近似最近邻检索能力,在 Gremlin 查询中查找与查询向量最相似的顶点。该能力适用于语义检索、相似内容推荐、以图关系扩展向量召回结果等场景。本文以文档检索为例,介绍向量检索得分,以及使用hasVector对顶点执行 KNN(K-Nearest Neighbor)检索。

前提条件

  • 已开通 Lindorm 图引擎,并已配置访问白名单。

  • 已开通 Lindorm 向量引擎。

  • 已获取图引擎的 HTTP 连接地址、数据库名、用户名和密码。

向量检索得分说明

说明

详细介绍与计算方法可以参考向量引擎的相关文档附录:向量检索得分(_score)的计算方法

图引擎根据向量引擎返回的distance计算得分:

score = 1 / (1 + distance)

L2COSINE的距离及得分范围如下。

距离类型

distance 定义

distance 范围

_score 范围

L2

sum((query[i] - vector[i])²),即平方欧氏距离

[0, +∞)

(0, 1]

COSINE

1 - cosineSimilarity(query, vector)

[0, 2]

[1/3, 1]

典型得分如下:

场景

distance

_score

L2:向量完全相同

0

1

L2:平方距离为 1

1

0.5

L2:平方距离为 4

4

0.2

COSINE:同方向

0

1

COSINE:正交

1

0.5

COSINE:反方向

2

1/3

使用得分时,请注意以下事项:

  • COSINE的理论最低得分是1/3,不是0。如果业务向量的余弦相似度只位于[0,1],得分范围实际为[0.5,1]

  • L2的得分可以无限接近0

  • 不同距离类型的得分分布不同。建议分别根据业务数据校准阈值,不要直接共用同一个阈值。

对顶点执行 KNN 检索

对顶点的向量检索通常包括以下步骤:

  1. 使用hasVector召回与查询向量最相似的顶点。

  2. 根据需要返回相似度得分、过滤低相关结果,或继续执行图遍历。

hasVector KNN 检索基本语法

hasVector提供以下两种调用形式:

hasVector(vectorProperty, queryVector, topK)
hasVector(vectorProperty, queryVector, topK, searchParams)

参数说明如下。

参数

是否必选

说明

vectorProperty

已定义向量索引的顶点属性名。

queryVector

查询向量。数组元素必须为数值,维度必须与属性定义一致。

topK

从向量索引召回的最大结果数,必须大于0

searchParams

检索参数 Map,例如min_scorewith_score和索引检索参数。

hasVector必须紧跟只包含一个顶点类型的hasLabel使用。例如,以下查询返回document中与[1,0,0,0]最接近的 4 个顶点 ID:

g.V().hasLabel('document')
  .hasVector('embedding', [1.0d, 0.0d, 0.0d, 0.0d], 4)
  .id()

对应的 curl 请求如下:

curl -sS -u "$GRAPH_AUTH" -X POST \
  "$GRAPH_ENDPOINT/gremlin/$GRAPH_DB" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  --data '{
    "gremlin": "g.V().hasLabel(\"document\").hasVector(\"embedding\",[1.0d,0.0d,0.0d,0.0d],4).id()"
  }'

示例数据的返回可能如下:

{
  "requestId": "<request-id>",
  "status": {
    "message": "",
    "code": 200,
    "attributes": {
      "@type": "g:Map",
      "@value": []
    }
  },
  "result": {
    "data": {
      "@type": "g:List",
      "@value": [
        "doc_same",
        "doc_scaled",
        "doc_orthogonal",
        "doc_opposite"
      ]
    },
    "meta": {
      "@type": "g:Map",
      "@value": []
    }
  }
}

业务结果位于result.data["@value"]中。以上示例依次返回doc_samedoc_scaleddoc_orthogonaldoc_opposite;结果按照_score从大到小返回。

hasVector KNN 检索常见用法

返回顶点及其业务属性

使用valueMap(true)返回顶点 ID、Label 和所有属性:

g.V().hasLabel('document')
  .hasVector('embedding', [1.0d, 0.0d, 0.0d, 0.0d], 1)
  .valueMap(true)

默认不开启with_score,因此结果中不包含_score

如果只需要部分业务属性,可以显式指定属性名:

g.V().hasLabel('document')
  .hasVector('embedding', [1.0d, 0.0d, 0.0d, 0.0d], 4)
  .valueMap('title', 'category')

返回相似度得分

在第四个参数中设置with_score=true

g.V().hasLabel('document')
  .hasVector(
    'embedding',
    [1.0d, 0.0d, 0.0d, 0.0d],
    4,
    {"with_score": true}
  )
  .valueMap('title', 'category', '_score')

下面展示解开 GraphSON 类型包装后的逻辑结果:

[
  {"title":["Exact match"],"category":["guide"],"_score":[1.0]},
  {"title":["Same direction different length"],"category":["guide"],"_score":[0.5]},
  {"title":["Orthogonal vector"],"category":["reference"],"_score":[0.33333334]},
  {"title":["Opposite vector"],"category":["archive"],"_score":[0.2]}
]

GraphSON 中的_score按顶点属性的多值形式返回,因此在valueMap()结果中通常是只包含一个浮点数的列表。客户端需要按列表形式解析。

如果只需要得分数值列表,可以使用:

g.V().hasLabel('document')
  .hasVector(
    'embedding',
    [1.0d, 0.0d, 0.0d, 0.0d],
    4,
    {"with_score": true}
  )
  .values('_score')
  .fold()

使用min_score过滤低相关结果

min_score用于设置最低相关性得分。以下查询先从向量索引中最多召回 100 个顶点,再仅保留得分不低于0.8的结果:

g.V().hasLabel('document')
  .hasVector(
    'embedding',
    [1.0d, 0.0d, 0.0d, 0.0d],
    100,
    {"min_score": 0.8, "with_score": true}
  )
  .valueMap('title', '_score')

min_score与最大距离的换算关系如下:

maxDistance = 1 / min_score - 1

对于L2COSINE,建议将min_score设置在(0,1]0表示基本不进行距离过滤;大于1时不会命中这两类距离的结果。对于COSINE,阈值不大于1/3时,理论上无法过滤有效的非零向量。

调整索引检索参数

searchParams支持以下参数。

参数

取值范围

说明

min_score

>= 0

最低相关性得分。

with_score

布尔值

是否返回_score,默认值为false

ef_search

整数[1,1000]

HNSW 检索的候选宽度。

nprobe

整数[1,1000000]

IVF 检索时访问的聚类中心数。

reorder_factor

浮点数[0,200]

扩大候选集,并基于原始向量进行重排。

参数应与索引类型匹配。例如,HNSW 索引主要使用ef_search,IVF 索引主要使用nprobe,量化索引可使用reorder_factor。图引擎会校验并透传这些参数,但不保证与当前索引类型不匹配的参数能够改变召回结果。未知参数会导致查询报错。

以下示例扩大向量召回范围、设置最低得分,并最终返回前 20 个结果:

g.V().hasLabel('document')
  .hasVector(
    'embedding',
    [1.0d, 0.0d, 0.0d, 0.0d],
    100,
    {
      "min_score": 0.75,
      "with_score": true,
      "ef_search": 100,
      "reorder_factor": 10
    }
  )
  .limit(20)
  .valueMap('title', '_score')

向量召回后继续过滤或遍历

可以在hasVector后继续使用普通 Gremlin 步骤。例如,以下查询在向量召回结果中筛选状态为online的顶点:

g.V().hasLabel('document')
  .hasVector(
    'embedding',
    [1.0d, 0.0d, 0.0d, 0.0d],
    100,
    {"with_score": true}
  )
  .has('status', 'online')
  .limit(10)
  .valueMap('title', 'status', '_score')

topK限制的是向量索引的召回数量,后续过滤不会自动补充候选。例如,topK=10的 10 个候选中只有 3 个满足status='online',查询最多返回 3 个结果。需要返回更多过滤后结果时,应适当增大topK,再使用limit()限制最终数量。

重要

普通属性过滤必须放在hasVector之后。hasVector会作为向量检索的起始步骤,位于它之前的普通过滤步骤不会参与最终查询。请勿使用以下写法:

// 错误示例:status 过滤不会按预期生效
g.V().hasLabel('document')
  .has('status', 'online')
  .hasVector('embedding', [1.0d, 0.0d, 0.0d, 0.0d], 100)

使用参数绑定

查询向量和检索参数可以通过请求的bindings传入,避免在 Gremlin 字符串中拼接较长的向量:

curl -sS -u "$GRAPH_AUTH" -X POST \
  "$GRAPH_ENDPOINT/gremlin/$GRAPH_DB" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  --data '{
    "gremlin": "g.V().hasLabel(\"document\").hasVector(\"embedding\",queryVector,100,params).limit(10).valueMap(\"title\",\"_score\")",
    "bindings": {
      "queryVector": [1.0, 0.0, 0.0, 0.0],
      "params": {
        "min_score": 0.8,
        "with_score": true,
        "ef_search": 100
      }
    }
  }'

使用限制

  • hasVector仅用于顶点向量属性的 KNN 检索。

  • hasVector前必须有且只有一个hasLabel,不能省略 Label,也不能同时指定多个 Label。

  • hasVectortopK必须大于0

  • 查询向量必须为数值数组,维度必须与 Schema 定义一致。

  • _score是保留属性名,只在设置with_score=true的本次查询中存在,并且不可修改或删除。

  • with_score只接受布尔值truefalse或对应的字符串形式,其他值会导致查询报错。

  • 未知的searchParams参数会导致查询报错。

  • 普通属性过滤应放在hasVector之后;如需保证过滤后的结果数量,应扩大topK后再使用limit()