更新时间:2026-04-24 GMT+08:00
分享

通过嵌套字段实现向量检索

在处理长篇幅文档(如技术手册、法律条文)时,通常需要将文档按段落或固定长度切分为多个语义块,并分别为每个块生成特征向量。传统的扁平化存储结构会导致文档元数据(如ID、标题)与多个向量之间的对应关系难以维护,且检索时无法精准定位是哪一个段落触发了匹配。为了解决这个问题,CSS向量数据库支持了嵌套字段(Nested)中的向量检索:它允许您在单条主文档中封装多个“向量子文档”,实现“一主多从”的结构化存储。在查询时,只需一次请求即可扫描所有嵌套向量,并支持通过评分模式选取最相关的段落返回,极大简化了长文本检索的架构复杂度。

功能介绍

在Elasticsearch/OpenSearch中,普通的object字段在底层会被“扁平化”处理,导致数组中不同子字段的关联关系丢失。而Nested类型将每个嵌套对象作为独立的“隐藏文档”存储,确保了每个嵌套对象中的“段落文本”与其对应的“向量”在检索时能够保持关联。

当您对Nested字段进行向量检索时,系统会经历以下过程:

  1. 子文档扫描:在父文档内部遍历所有子文档的向量。
  2. 局部评分:计算每个子向量与查询向量的相似度。
  3. 分值聚合:根据score_mode决定父文档的最终得分。score_mode通常设为max,表示只要文档中有一个段落与查询最相关,该文档就会被排在前面。

约束限制

  • 仅Elasticsearch 7.10.2和OpenSearch 2.19.0版本的集群支持在Nested字段中使用向量检索。
  • 嵌套字段会增加底层的文档总数,对于极大规模数据集,建议评估分片的承载压力。
  • 嵌套字段不支持使用rescore语句进行重打分查询,仅支持使用query.vector语句(参见标准向量查询)进行重打分查询。

创建带有Nested字段的向量索引

执行以下命令,创建一个带有Nested字段的向量索引,该索引包含一个id字段,类型为keyword,包含一个embedding字段,类型为nested。embedding嵌套字段包含两个子字段chunk和emb,其中chunk为keyword类型,emb为vector类型。

PUT my_index
{
  "settings": {
    "index.vector": true
  },
  "mappings": {
    "properties": {
      "id": {
        "type": "keyword"
      },
      "embedding": {
        "type": "nested",        // 声明嵌套类型
        "properties": {
          "chunk": {
            "type": "keyword"
          },
          "emb": {               // 子字段中的向量
            "type": "vector",
            "dimension": 2,
            "indexing": true,
            "algorithm": "GRAPH",
            "metric": "euclidean"
          }
        }
      }
    }
  }
}

导入多向量数据

执行以下命令,以数组形式一次性写入主文档及其关联的多个切片向量。每条文档包含了2条向量数据。

POST my_index/_bulk
{"index":{}}
{"id": 1, "embedding": [{"chunk":1,"emb": [1, 1]}, {"chunk":2,"emb": [2, 2]}]}
{"index":{}}
{"id": 2, "embedding": [{"chunk":1,"emb": [2, 2]}, {"chunk":2,"emb": [3, 3]}]}
{"index":{}}
{"id": 3, "embedding": [{"chunk":1,"emb": [3, 3]}, {"chunk":2,"emb": [4, 4]}]}

执行嵌套向量检索

在嵌套路径中搜索最相似的段落,并按最大相关度对文档排序。Nested字段需要使用nested查询,查询时需要指定path参数以指明要查询的嵌套路径,以及必须设置score_mode为max,表示文档的得分为该文档中所有向量与查询向量相似度的最大值。

  • 标准查询

    查询与向量[1, 1]最相似的Top10文档。

    GET my_index/_search
    {
      "_source": {"excludes": ["embedding"]}, // 仅返回主文档ID,隐藏复杂的向量数组
      "query": {
        "nested": {
          "path": "embedding",                // 必须指定嵌套字段路径
          "score_mode": "max",                // 子文档分值合并到父文档的策略:取最匹配的段落得分
          "query": {
            "vector": {
              "embedding.emb": {
                "vector": [1, 1],
                "topk": 10
              }
            }
          }
        }
      }
    }

    查询结果示例如下:

    {
      "took" : 2,
      "timed_out" : false,
      "_shards" : {
        "total" : 1,
        "successful" : 1,
        "skipped" : 0,
        "failed" : 0
      },
      "hits" : {
        "total" : {
          "value" : 3,
          "relation" : "eq"
        },
        "max_score" : 1.0,
        "hits" : [
          {
            "_index" : "my_index",
            "_type" : "_doc",
            "_id" : "Hc4Vc5QBSxCnghau22AE",
            "_score" : 1.0,
            "_source" : {
              "id" : 1
            }
          },
          {
            "_index" : "my_index",
            "_type" : "_doc",
            "_id" : "Hs4Vc5QBSxCnghau22AE",
            "_score" : 0.33333334,
            "_source" : {
              "id" : 2
            }
          },
          {
            "_index" : "my_index",
            "_type" : "_doc",
            "_id" : "H84Vc5QBSxCnghau22AE",
            "_score" : 0.11111111,
            "_source" : {
              "id" : 3
            }
          }
        ]
      }
    }
  • 前置过滤查询

    先筛选出id取值为["2", "3"]的文档,再返回与查询向量[1, 1]最相似的Top10文档。

    GET my_index/_search
    {
      "query": {
        "nested": {
          "path": "embedding",                // 必须指定嵌套字段路径
          "score_mode": "max",                // 子文档分值合并到父文档的策略:取最匹配的段落得分
          "query": {
            "vector": {
              "embedding.emb": {
                "vector": [1, 1],
                "topk": 10,
                "filter": {
                  "terms": {"id": ["2", "3"]}
                }
              }
            }
          }
        }
      }
    }

    查询结果示例如下:

    {
      "took" : 3,
      "timed_out" : false,
      "_shards" : {
        "total" : 1,
        "successful" : 1,
        "skipped" : 0,
        "failed" : 0
      },
      "hits" : {
        "total" : {
          "value" : 2,
          "relation" : "eq"
        },
        "max_score" : 0.33333334,
        "hits" : [
          {
            "_index" : "my_index",
            "_type" : "_doc",
            "_id" : "3t0ZypcB-Tff59gMTZO2",
            "_score" : 0.33333334,
            "_source" : {
              "id" : 2,
              "embedding" : [
                {
                  "chunk" : 1,
                  "emb" : [
                    2,
                    2
                  ]
                },
                {
                  "chunk" : 2,
                  "emb" : [
                    3,
                    3
                  ]
                }
              ]
            }
          },
          {
            "_index" : "my_index",
            "_type" : "_doc",
            "_id" : "390ZypcB-Tff59gMTZO2",
            "_score" : 0.11111111,
            "_source" : {
              "id" : 3,
              "embedding" : [
                {
                  "chunk" : 1,
                  "emb" : [
                    3,
                    3
                  ]
                },
                {
                  "chunk" : 2,
                  "emb" : [
                    4,
                    4
                  ]
                }
              ]
            }
          }
        ]
      }
    }

相关文档