节省 90% 存储!源码级揭秘腾讯云 ES 向量搜索的优化之道

Git Rebase 详解与实战使用指南 本文系统讲解 Git Rebase 的原理与实战,揭示其如何通过线性化提交历史提升代码可读性。对比 Merge 与 Rebase 的差异,详解基础用法、交互式变基(如 squash、reword)、冲突处理及常见陷阱。强调“仅对本地分支 rebase”“强推用 --force-with-lease”等最佳实践,并推荐使用 git pull --rebase 保持开发主线整洁,助力团队协作高效有序。 阅读详情

点击蓝字⬆ 关注我们

b6fe9367822937d71d45cf9985f8644d.gif

本文共计6751字 预计阅读时长21分钟

导语

在上一篇文章《ES8 向量功能窥探系列(一):混合搜索功能初探与增强》中,我们初步探讨了 Elasticsearch 8.x 的混合搜索功能,包括kNN 查询流程、RRF 融合算法以及相关的功能增强。本篇文章将继续这一系列,聚焦于 Elasticsearch 中向量数据的存储与优化。我们将深入分析向量数据的索引构成、读写流程以及量化技术,旨在为读者提供对 Elasticsearch 向量索引存储机制的全面理解。同时也将介绍腾讯云 ES AI搜索增强版,自研向量增强插件 v-pack 带来的节省 70% - 90% 存储的技术细节解析。

1. 存储

关于 ES 倒排索引等存储构成,网上早已有很多文章进行过解析,不做赘述。我们会重点探究在 ES8 引入向量功能后所发生的变化。本文所引用代码均为 ES 8.16.1 版本。

1.1 源码概览

ES8 为向量引入了两种新的字段 mapping 类型,sparse_vector和dense_vector。前者并没有引入新的索引类型,它可以认为只是在BM25的基础上为每个 term 增加了权重值,且使用场景远少于后者,我们不做展开讨论。实际上新的的索引类型,全部由dense_vector类型引入,我们来深入分析下。

从 ES 源码上看,有如下文件类型被引入:

public enum LuceneFilesExtensions {
    ...
    // kNN vectors format
    VEC("vec", "Vector Data", false, true),
    VEX("vex", "Vector Index", false, true),
    VEM("vem", "Vector Metadata", true, false),
    VEMF("vemf", "Flat Vector Metadata", true, false),
    VEMQ("vemq", "Scalar Quantized Vector Metadata", true, false),
    VEQ("veq", "Scalar Quantized Vector Data", false, true),
    VEMB("vemb", "Binarized Vector Metadata", true, false),
    VEB("veb", "Binarized Vector Data", false, true);
    ...
}

1.1.1 索引类型

我们看下 Lucene 中对向量索引编码的关键定义。可以发现引入了两种 Vector 相关编码,它们之前是继承关系,提供了不同的Reader和Writer,事实上KnnVectorsWriter&FlatVectorsWriter,以及KnnVectorsReader&FlatVectorsReader有着同样的继承关系。

public abstract class KnnVectorsFormat implements NamedSPILoader.NamedSPI {
  ...
  /** Returns a {@link KnnVectorsWriter} to write the vectors to the index. */
  public abstract KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException;


  /** Returns a {@link KnnVectorsReader} to read the vectors from the index. */
  public abstract KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException;
  ...
}
public abstract class FlatVectorsFormat extends KnnVectorsFormat {
  ...
  /** Returns a {@link FlatVectorsWriter} to write the vectors to the index. */
  @Override
  public abstract FlatVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException;


  /** Returns a {@link KnnVectorsReader} to read the vectors from the index. */
  @Override
  public abstract FlatVectorsReader fieldsReader(SegmentReadState state) throws IOException;
  ...
}

相关源码暂不做展开,先做整体概括。从索引类型上进行归纳,可以分为两种类型。

1.1.1.1 Flat

Flat 索引是最基础的向量索引方式,它将所有的向量数据直接存储在一个连续的数组中。这种方法的优点是简单直接,便于快速访问,但缺点是随着数据量的增加,检索效率会显著下降。

1.1.1.2 HNSW

HNSW(Hierarchical Navigable Small World)是一种基于图的索引方法,用于加速 kNN 搜索。在 Elasticsearch 中,HNSW 通过构建多层图结构,使得搜索过程可以快速定位到目标区域,从而提高搜索效率。这种方法在处理大规模向量数据时表现优异,是当前向量搜索领域的主流索引技术之一。

1.1.2 文件类型

和 Lucene 以往的文件类型相似,引入新的索引一般会同时引入两种文件类型,向量索引继承了这一约定。

1.1.2.1 Metadata

元数据信息,描述了向量的维度、编码、相似度度量方式、字段在索引中的offset等等,以及HNSW图索引的层数、连接数等等信息。如存储目录下的.vem,.vemf,.vemq文件,它们都只占用很小的空间。

1.1.2.2 Data

索引数据,用于kNN搜索所需的关键结构。如存储目录下的.vex,.vec,.veq文件,它们都占用了相对较多的空间。

1.2 存储概览

使用esrally的so_vectors数据集创建索引,进入索引的存储目录,看到结构如下,包含了多个上面提到的.ve*类型的文件:

$ tree -h
.
├── [  65]  _0_0.doc
├── [  65]  _0_0.pos
├── [6.0M]  _0_0.tim
├── [542K]  _0_0.tip
├── [ 262]  _0_0.tmd
├── [117M]  _0_ES812Postings_0.doc
├── [134M]  _0_ES812Postings_0.pos
├── [ 46M]  _0_ES812Postings_0.tim
├── [1.0M]  _0_ES812Postings_0.tip
├── [ 681]  _0_ES812Postings_0.tmd
├── [2.9G]  _0_ES814HnswScalarQuantizedVectorsFormat_0.vec
├── [ 68K]  _0_ES814HnswScalarQuantizedVectorsFormat_0.vem
├── [ 157]  _0_ES814HnswScalarQuantizedVectorsFormat_0.vemf
├── [ 182]  _0_ES814HnswScalarQuantizedVectorsFormat_0.vemq
├── [736M]  _0_ES814HnswScalarQuantizedVectorsFormat_0.veq
├── [ 57M]  _0_ES814HnswScalarQuantizedVectorsFormat_0.vex
├── [1.2M]  _0_ES87BloomFilter_0.bfi
├── [ 103]  _0_ES87BloomFilter_0.bfm
├── [8.2K]  _0.fdm
├── [ 12G]  _0.fdt
├── [471K]  _0.fdx
├── [1.8K]  _0.fnm
├── [6.1M]  _0.kdd
├── [ 22K]  _0.kdi
├── [ 212]  _0.kdm
├── [ 25M]  _0_Lucene90_0.dvd
├── [1.7K]  _0_Lucene90_0.dvm
├── [1.9M]  _0.nvd
├── [ 139]  _0.nvm
├── [ 995]  _0.si
├── [ 349]  segments_3d
└── [   0]  write.lock


0 directories, 32 files

查看索引的mapping如下:

{
  "so_vectors": {
    "mappings": {
      "properties": {
        "acceptedAnswerId": {
          "type": "keyword"
        },
        "body": {
          "type": "text"
        },
        "creationDate": {
          "type": "date"
        },
        "questionId": {
          "type": "keyword"
        },
        "tags": {
          "type": "keyword"
        },
        "title": {
          "type": "text"
        },
        "titleVector": {
          "type": "dense_vector",
          "dims": 768,
          "index": true,
          "similarity": "cosine",
          "index_options": {
            "type": "int8_hnsw",
            "m": 16,
            "ef_construction": 100
          }
        },
        "type": {
          "type": "keyword"
        },
        "user": {
          "type": "keyword"
        },
        "userId": {
          "type": "keyword"
        }
      }
    }
  }
}

注意到只有 titleVector 字段使用了dense_vector类型。后面的篇幅我们会通过修改该字段的 mapping 配置后重建索引,来分析一下向量索引的构成。

1.3 小结

初步归纳向量的索引类型:

47400db7fc24ef3520afc69ea3b38165.png

2. 索引

2.1 无索引

2.1.1 索引构成

...
      "titleVector": {
        "type": "dense_vector",
        "dims": 768,
        "index": false
      },
      ...

修改 mapping,在上述配置的作用下,我们注意到.ve*类型的文件全部消失,再次说明了.ve*是向量字段专用的索引文件类型。

$ tree -h
.
├── [  65]  _13_0.doc
├── [  65]  _13_0.pos
├── [6.0M]  _13_0.tim
├── [542K]  _13_0.tip
├── [ 262]  _13_0.tmd
├── [117M]  _13_ES812Postings_0.doc
├── [134M]  _13_ES812Postings_0.pos
├── [ 46M]  _13_ES812Postings_0.tim
├── [1.0M]  _13_ES812Postings_0.tip
├── [ 681]  _13_ES812Postings_0.tmd
├── [1.2M]  _13_ES87BloomFilter_0.bfi
├── [ 103]  _13_ES87BloomFilter_0.bfm
├── [8.2K]  _13.fdm
├── [ 12G]  _13.fdt
├── [467K]  _13.fdx
├── [1.7K]  _13.fnm
├── [6.2M]  _13.kdd
├── [ 22K]  _13.kdi
├── [ 212]  _13.kdm
├── [2.9G]  _13_Lucene90_0.dvd
├── [1.8K]  _13_Lucene90_0.dvm
├── [1.9M]  _13.nvd
├── [ 139]  _13.nvm
├── [ 705]  _13.si
├── [ 389]  segments_q
└── [   0]  write.lock


0 directories, 26 files

2.1.2 关键类型

分片目录中,存储占比较高的文件类型,.fdt和.dvd,分别是行存和列存文件。由于未启用向量索引,我们有理由认为该配置下,向量字段只会引发行存和列存的存储大小增长。

f92d6be156abe495883d89a2c5321f86.png

2.1.3 写入分析

由于写入链路的源码较长,笔者将源码中关键链路简化为流程图

4180b4fa0625d9b5c2349025186e845b.png

2.1.4 读取分析

由于未建立向量索引,所以无法使用knn语法进行向量搜索,但可以使用script语法变相进行向量搜索。

POST so_vector/_search
{
  "query": {
    "script_score": {
      "query": {
        "match_all": {}
      },
      "script": {
        "source": "cosineSimilarity(params.queryVector, 'titleVector') + 1.0",
        "params": {
          "queryVector": [ ... ]
        }
      }
    }
  }
}

该语法可以工作是由于向量数据存在于列存文件.dvd中,在计算向量相似度时,script 会读取.dvd获取文档向量值,与查询向量值queryVector进行cosine相似度计算。而.fdt中存储的向量数据,仅会在查询结果的_source中进行展示。

此时并未使用任何索引,实际上执行的搜索是暴力搜索 (Exact, brute-force kNN)。因为遍历了所有的向量并做了相似度计算,它可以获取到完全精确的结果。但随着数据规模的增长, 它的扫描量和计算量也大幅增长,极易出现资源开销高,响应速度慢的情况。

2.2 Flat 索引

2.2.1 索引构成

...
      "titleVector": {
        "type": "dense_vector",
        "dims": 768,
        "index": true,
        "similarity": "cosine",
        "index_options": {
          "type": "flat"
        }
      },
      ...

在上述配置的作用下,产生了.vec和.vemf文件,结合上一节的分析,我们知道它们分别是 Flat 索引的 data 文件和 metadata 文件。此外,_1m_ES813FlatVectorFormat_0.vec,按 Lucene 的规范,中间的ES813FlatVectorFormat实际是编码名,这里我们留个印象。

$ tree -h
.
├── [  65]  _1m_0.doc
├── [  65]  _1m_0.pos
├── [6.1M]  _1m_0.tim
├── [540K]  _1m_0.tip
├── [ 262]  _1m_0.tmd
├── [118M]  _1m_ES812Postings_0.doc
├── [134M]  _1m_ES812Postings_0.pos
├── [ 46M]  _1m_ES812Postings_0.tim
├── [1.0M]  _1m_ES812Postings_0.tip
├── [ 669]  _1m_ES812Postings_0.tmd
├── [2.9G]  _1m_ES813FlatVectorFormat_0.vec
├── [ 141]  _1m_ES813FlatVectorFormat_0.vemf
├── [1.2M]  _1m_ES87BloomFilter_0.bfi
├── [ 103]  _1m_ES87BloomFilter_0.bfm
├── [ 662]  _1m.fdm
├── [ 12G]  _1m.fdt
├── [ 52K]  _1m.fdx
├── [1.8K]  _1m.fnm
├── [6.3M]  _1m.kdd
├── [ 22K]  _1m.kdi
├── [ 212]  _1m.kdm
├── [ 26M]  _1m_Lucene90_0.dvd
├── [1.8K]  _1m_Lucene90_0.dvm
├── [1.9M]  _1m.nvd
├── [ 139]  _1m.nvm
├── [ 770]  _1m.si
├── [ 392]  segments_v
└── [   0]  write.lock


0 directories, 28 files

2.2.2 关键类型

分片目录中,与“无索引”的配置相比,头部文件的大小未发生显著变化。.dvd文件大幅缩小到 26MB,.vec代替了.dvd,并且有趣的是同样为 2.9GB 的大小。在“无索引”的配置下,列存.dvd大部分存储由向量字段占据,在配置了向量字段的 Flat 索引后,该部分存储几乎 1:1 地转移到.vec类型的向量索引数据文件中。

3847a9d4d9347f141dd0d15da08c8833.png

结合 1.1.1.1 章节的概述,我们知道 Flat 索引的存储是将所有的向量数据直接存储在一个连续的数组,与列存的做法几乎一致,可以完美地解释这一变化。在从源码分析也能得到相同的结论。

public class DenseVectorFieldMapper extends FieldMapper {
    ...
    @Override
    public void parse(DocumentParserContext context) throws IOException {
        ...
        if (fieldType().indexed) {
            parseKnnVectorAndIndex(context);
        } else {
            parseBinaryDocValuesVectorAndIndex(context);
        }
    }
    ...
}

2.2.3 写入分析

刚刚提到的ES813FlatVectorFormat编码,源码有如下定义:

public class ES813FlatVectorFormat extends KnnVectorsFormat {


    static final String NAME = "ES813FlatVectorFormat";
    
    ...
    @Override
    public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException {
        return new ES813FlatVectorWriter(format.fieldsWriter(state));
    }


    @Override
    public KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException {
        return new ES813FlatVectorReader(format.fieldsReader(state));
    }
    ...
}

ES813FlatVectorWriter实际是Lucene99FlatVectorsWriter的代理模式实现。笔者同样将源码中关键链路简化为流程图。

38ef1364267a877dc899157fcb56dc7a.png

2.2.4 读取分析

“无索引”和“Flat 索引”在存储上来看是基本上无差异的,而唯一的差异点就是前者无法使用knn查询语法,而后者可以使用。

POST so_vector/_search
{
  "knn": {
    "field": "titleVector",
    "query_vector": [ ... ],
    "k": 10,
    "num_candidates": 100
  }
}

底层同样执行了暴力搜索,原理上与“无索引”的script查询方式没有任何区别。与“无索引”唯一不同的是,获取文档向量值是通过读取.vec文件。事实上根本上的区别是,“无索引”的向量检索方式,由于未引入新的索引类型,它甚至在 ES 7.x 的版本上也适用;而“Flat 索引”是在 ES 8.x 版本提供了专用的knn查询、存储语法后的规范实现。所以在 ES 8.x 上建议优先使用”Flat 索引“。

同样的,.fdt中存储的向量数据,仅会在查询结果的_source中进行展示。

2.3 HNSW 索引

2.3.1 索引构成

...
      "titleVector": {
        "type": "dense_vector",
        "dims": 768,
        "index": true,
        "similarity": "cosine",
        "index_options": {
          "type": "hnsw"
        }
      },
      ...

对比 Flat 索引,向量相关文件除了.vec和.vemf外,还产生了.vex和.vem文件。且.vec和.vemf几乎维持了原大小。合理得出:

  1. .vex和.vem文件是 HNSW 索引的 data 文件和 metadata 文件

  2. 在 HNSW 索引模式下,除了 HNSW 索引,还会生成一套 Flat 索引

此外,我们注意到.ve*文件中间的编码名全部变成了Lucene99HnswVectorsFormat。

$ tree -h
.
├── [  65]  _1z_0.doc
├── [  65]  _1z_0.pos
├── [6.1M]  _1z_0.tim
├── [542K]  _1z_0.tip
├── [ 262]  _1z_0.tmd
├── [118M]  _1z_ES812Postings_0.doc
├── [134M]  _1z_ES812Postings_0.pos
├── [ 46M]  _1z_ES812Postings_0.tim
├── [1.0M]  _1z_ES812Postings_0.tip
├── [ 681]  _1z_ES812Postings_0.tmd
├── [1.2M]  _1z_ES87BloomFilter_0.bfi
├── [ 103]  _1z_ES87BloomFilter_0.bfm
├── [ 663]  _1z.fdm
├── [ 12G]  _1z.fdt
├── [ 51K]  _1z.fdx
├── [1.8K]  _1z.fnm
├── [6.3M]  _1z.kdd
├── [ 22K]  _1z.kdi
├── [ 212]  _1z.kdm
├── [ 26M]  _1z_Lucene90_0.dvd
├── [1.8K]  _1z_Lucene90_0.dvm
├── [2.9G]  _1z_Lucene99HnswVectorsFormat_0.vec
├── [ 68K]  _1z_Lucene99HnswVectorsFormat_0.vem
├── [ 145]  _1z_Lucene99HnswVectorsFormat_0.vemf
├── [ 55M]  _1z_Lucene99HnswVectorsFormat_0.vex
├── [1.9M]  _1z.nvd
├── [ 139]  _1z.nvm
├── [ 850]  _1z.si
├── [ 390]  segments_12
└── [   0]  write.lock


0 directories, 30 files

2.3.2 关键类型

与“Flat 索引”的配置相比,分片目录中除了新产生了.vex和.vem文件外,几乎没有变化。.vex作为 HNSW 索引的 Data 文件,却也只占了 55MB。事实上,.vex仅仅存储了 HNSW 图的结构,不包含任何实际数据。

70c35e9bee44410067bdcc775230a198.png

2.3.3 写入分析

Lucene99HnswVectorsFormat编码的定义如下:

public final class Lucene99HnswVectorsFormat extends KnnVectorsFormat {
  ...
  /** The format for storing, reading, and merging vectors on disk. */
  private static final FlatVectorsFormat flatVectorsFormat =
      new Lucene99FlatVectorsFormat(FlatVectorScorerUtil.getLucene99FlatVectorsScorer());
  ...
  public Lucene99HnswVectorsFormat(
      int maxConn, int beamWidth, int numMergeWorkers, ExecutorService mergeExec) {
    super("Lucene99HnswVectorsFormat");
    ...
  }


  ...
  @Override
  public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException {
    return new Lucene99HnswVectorsWriter(
        state,
        maxConn,
        beamWidth,
        flatVectorsFormat.fieldsWriter(state),
        numMergeWorkers,
        mergeExec);
  }


  @Override
  public KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException {
    return new Lucene99HnswVectorsReader(state, flatVectorsFormat.fieldsReader(state));
  }
  ...
}

注意到源码中在Lucene99HnswVectorsFormat编码中定义了一个内部编码Lucene99FlatVectorsWriter。这也就解释了为什么除了 HNSW 索引外,还会生成一套 Flat 索引。这种 HNSW 和 Flat 编码组合的方式在 Lucene 9.9 引入了 该 PR 后普遍使用。写入的关键链路如下:

43b98a43e020584a4b45054c59a2cfcc.png

2.3.4 读取分析

.vex只存储了 HNSW 图的结构,HNSW 图中的每个节点对应一个向量的值,.vex中的关键单元仅存储了自身节点编号(node id)和最大 m 个近邻节点的编号列表。通过节点编号(node id)可以快速定位到.vec文件中的关键单元,获取其存储的向量的原始值,以及它对应的文档编号(doc id)。所以,.vec、.vemf这套 Flat 索引才被完整保留,.vex的存储大小才如此小,否则.vex无法单独工作。

.fdt和.vec的作用,在前文中已经分析过了,不再赘述。关于 HNSW 和 Flat 索引文件的内部的结构分析,本文限于篇幅,也不做过多展开。

由于存在 Flat 索引,所以同样支持精确的暴力搜索,此外,.vex带来了近似 kNN(Approximate kNN)的搜索能力,顾名思义,近似 kNN 不给出完全精确的结果,但可以提供更快的查询响应和更少的计算开销。在基于 HNSW 索引的 kNN 搜索中,如果存在 BM25 的查询对 kNN 搜索进行过滤,实际还有一个简单的 CBO 策略,对底层执行近似 kNN 搜索(approximateSearch)还是暴力搜索(exactSearch),给出更优的选择。

abstract class AbstractKnnVectorQuery extends Query {
  ...
  private TopDocs getLeafResults(
      LeafReaderContext ctx,
      Weight filterWeight,
      TimeLimitingKnnCollectorManager timeLimitingKnnCollectorManager)
      throws IOException {
    ...
    if (filterWeight == null) {
      return approximateSearch(ctx, liveDocs, Integer.MAX_VALUE, timeLimitingKnnCollectorManager);
    }
    ...
    final int cost = acceptDocs.cardinality();
    QueryTimeout queryTimeout = timeLimitingKnnCollectorManager.getQueryTimeout();


    if (cost <= k) {
      // If there are <= k possible matches, short-circuit and perform exact search, since HNSW
      // must always visit at least k documents
      return exactSearch(ctx, new BitSetIterator(acceptDocs, cost), queryTimeout);
    }


    // Perform the approximate kNN search
    // We pass cost + 1 here to account for the edge case when we explore exactly cost vectors
    TopDocs results = approximateSearch(ctx, acceptDocs, cost + 1, timeLimitingKnnCollectorManager);
    if (results.totalHits.relation() == TotalHits.Relation.EQUAL_TO
        // Return partial results only when timeout is met
        || (queryTimeout != null && queryTimeout.shouldExit())) {
      return results;
    } else {
      // We stopped the kNN search because it visited too many nodes, so fall back to exact search
      return exactSearch(ctx, new BitSetIterator(acceptDocs, cost), queryTimeout);
    }
  }

2.4 小结

进一步对向量索引类型进行归纳总结:

f843b5dcdbc1479563d18ec60eb465a9.png

3. 行存裁剪优化

3.1 发现向量的冗余存储

在上一章节提到的几种索引模式中,我们不难发现以下几点:

  1. 向量字段无论索引与否,.fdt占据了整个存储的绝大部分

  2. 向量原始数值除了存储在.fdt外,无论索引与否,都有额外的结构进行存储。(.dvd或.vec)

这说明在默认配置下,无论索引与否,向量数据都存了两份。事实上,过往 Lucene 的其它索引类型也都采用这样的做法。然而来到向量,情况则发生了以下变化:

  1. 由于向量数据的不可解读性,在实际使用中,只需要计算相似度指导召回和距离评分,不需要作为查询结果返回。而前者是依赖读取.dvd和.vec,后者是依赖读取.fdt。极少数场景才需要在结果中返回向量(如如业务开发时调试,或进行 reindex)

  2. .fdt对于大量出现的向量数值仍然采用 json 字符串存储,缺少有效的压缩手段,较.dvd和.vec的存储高了不少

  3. 索引发生段合并时,向量字段只依赖.dvd和.vec即可完成

综上所述,在.fdt行存结构中保存向量数据,实际是一种冗余。极少数场景,应该可以由.dvd或.vec提供原始向量值来实现。.dvd和.vec都可以认为是列存类型的保存方式,在 ES 中有一个 docvalue_fields 的语法,可以在结果中获取列存。但在尝试该语法后,我们发现dense_vector竟然不支持列存获取。

POST so_vector/_search
{
  "docvalue_fields": ["titleVector"]
}


{
  "error": {
    "root_cause": [
      {
        "type": "illegal_argument_exception",
        "reason": "Field [titleVector] of type [dense_vector] doesn't support docvalue_fields or aggregations"
      }
    ],
    "type": "search_phase_execution_exception",
    "reason": "all shards failed",
    ...
  },
  "status": 400
}

3.2 让列存可以获取向量

在翻阅代码实现后,发现了是由于在dense_vector字段上聚合和排序没有实际意义,所以同时影响了向量字段docvalue_fields语法的使用。

public static final class DenseVectorFieldType extends SimpleMappedFieldType {
        ...
        @Override
        public DocValueFormat docValueFormat(String format, ZoneId timeZone) {
            throw new IllegalArgumentException(
                "Field [" + name() + "] of type [" + typeName() + "] doesn't support docvalue_fields or aggregations"
            );
        }
        ...
    }
final class VectorDVLeafFieldData implements LeafFieldData {
    ...
    @Override
    public SortedBinaryDocValues getBytesValues() {
        throw new UnsupportedOperationException("String representation of doc values for vector fields is not supported");
    }
    ...
}

那么我们要做的是,通过源码改造,保持dense_vector字段排序、聚合的列存禁用,但允许docvalue_fields语法获取,如此即可保证不损失功能的前提下,对行存.fdt中的向量进行有效裁剪。

3.3 源码改造细节

笔者在 ES 社区发现官方已经列出了相应的 Issue。得益于 Elasticsearch 源码优良设计和复用能力,这一改造最终简化为3点:

  • 注册DenseVectorDocValueFormat作为DenseVectorFieldType的DocValueFormat。

  • 不改动VectorDVLeafFieldData#getBytesValues,保证排序和聚合的禁用状态。

  • 覆盖实现LeafFieldData#getFormattedValues,分别兼容使用.dvd和.vec的情况,以及element_type分别为float32&byte&bit的情况。

笔者按照以上思路完成开发和测试验证,提交了 PR,最终这一功能也被社区所采纳。

如此一来,列存中的向量数据可以直接获取,向量行存就没有存在意义了。腾讯云 ES 在自研的 v-pack 插件中,使SourceFieldMapper对 _source 中的dense_vector字段进行默认的裁剪,将上一章节中写入流程图的.fdt写入链路优化为如下流程。

b9a5f0e69c85f01657decc0bc7023fef.png

3.4 增强功能成效

以”HNSW 索引“为例,与 2.3.2 章节的索引关键类型进行对比,可以发现.fdt所占用的存储得到了显著的改变。

$ tree -h
.
├── [  65]  _2f_0.doc
├── [  65]  _2f_0.pos
├── [6.1M]  _2f_0.tim
├── [540K]  _2f_0.tip
├── [ 262]  _2f_0.tmd
├── [118M]  _2f_ES812Postings_0.doc
├── [134M]  _2f_ES812Postings_0.pos
├── [ 46M]  _2f_ES812Postings_0.tim
├── [1.0M]  _2f_ES812Postings_0.tip
├── [ 669]  _2f_ES812Postings_0.tmd
├── [1.2M]  _2f_ES87BloomFilter_0.bfi
├── [ 103]  _2f_ES87BloomFilter_0.bfm
├── [ 789]  _2f.fdm
├── [601M]  _2f.fdt
├── [ 63K]  _2f.fdx
├── [1.8K]  _2f.fnm
├── [6.3M]  _2f.kdd
├── [ 22K]  _2f.kdi
├── [ 212]  _2f.kdm
├── [ 26M]  _2f_Lucene90_0.dvd
├── [1.8K]  _2f_Lucene90_0.dvm
├── [2.9G]  _2f_Lucene99HnswVectorsFormat_0.vec
├── [ 68K]  _2f_Lucene99HnswVectorsFormat_0.vem
├── [ 145]  _2f_Lucene99HnswVectorsFormat_0.vemf
├── [ 56M]  _2f_Lucene99HnswVectorsFormat_0.vex
├── [1.9M]  _2f.nvd
├── [ 139]  _2f.nvm
├── [ 850]  _2f.si
├── [ 393]  segments_19
└── [   0]  write.lock


0 directories, 30 files

061626be341d98bd4e166f50c2c79129.png

**下面的表格概括了在纯向量搜索场景和混合搜索场景,该功能的最终效果,对于存储的节省均达到了 70% 以上。**这一显著的效果也说明,只要涉及了向量搜索,无论是纯向量还是混合场景,占据存储的大头基本来到了向量这里。

89a30f0a8e722bbb52f26ae1b7f1f01d.png

3.5 腾讯云 ES 的贡献

列存拉取向量的能力已贡献给社区,按 Elasticsearch 官方的计划,将会在 8.17 版本进行功能的发布。

而腾讯云 ES 自发完成了该功能实现,并且在腾讯云 ES 新发布的 ES 8.16.1 版本上,提前提供了该功能。我们在自研的 v-pack 向量增强插件上对该功能进行了默认开启。无需任何配置,即可以直接使用到降低 70% 存储的优化特性。

4. 量化

量化是向量搜索领域,乃至 AI 模型领域的常用技术,它通过一定算法对原始向量进行压缩,得到字节数占用较小的,量化后的向量表示。量化后的向量所需内存大幅变小,降低配置门槛,也减少内存交换压力,收益很高,但也会带来一定程度的准召率损失。用量化向量技术做向量搜索,就好比将彩色图片全部黑白化,再使用黑白化的照片进行查找相似的图片,在效果轻微损失的前提下降低了保存代价,在这个比喻中,“黑白化”就可以理解为是一种量化方法。

4.1 标量量化索引

4.1.1 索引构成

...
      "titleVector": {
        "type": "dense_vector",
        "dims": 768,
        "index": true,
        "similarity": "cosine",
        "index_options": {
          "type": "int8_hnsw"
        }
      },
      ...

对比 HNSW 索引,.ve*文件中间的编码名全部变成了ES814HnswScalarQuantizedVectorsFormat。向量相关文件除了.vec、.vemf,.vex、.vem两套索引外,新产生了.veq和.vemq文件。所以多出来这套索引就是int8_hnsw吗?不是的。

$ tree -h
.
├── [  65]  _1o_0.doc
├── [  65]  _1o_0.pos
├── [6.1M]  _1o_0.tim
├── [540K]  _1o_0.tip
├── [ 262]  _1o_0.tmd
├── [118M]  _1o_ES812Postings_0.doc
├── [134M]  _1o_ES812Postings_0.pos
├── [ 46M]  _1o_ES812Postings_0.tim
├── [1.0M]  _1o_ES812Postings_0.tip
├── [ 669]  _1o_ES812Postings_0.tmd
├── [2.9G]  _1o_ES814HnswScalarQuantizedVectorsFormat_0.vec
├── [ 69K]  _1o_ES814HnswScalarQuantizedVectorsFormat_0.vem
├── [ 157]  _1o_ES814HnswScalarQuantizedVectorsFormat_0.vemf
├── [ 182]  _1o_ES814HnswScalarQuantizedVectorsFormat_0.vemq
├── [737M]  _1o_ES814HnswScalarQuantizedVectorsFormat_0.veq
├── [ 56M]  _1o_ES814HnswScalarQuantizedVectorsFormat_0.vex
├── [1.2M]  _1o_ES87BloomFilter_0.bfi
├── [ 103]  _1o_ES87BloomFilter_0.bfm
├── [ 662]  _1o.fdm
├── [600M]  _1o.fdt
├── [ 51K]  _1o.fdx
├── [2.0K]  _1o.fnm
├── [6.3M]  _1o.kdd
├── [ 22K]  _1o.kdi
├── [ 212]  _1o.kdm
├── [ 26M]  _1o_Lucene90_0.dvd
├── [1.9K]  _1o_Lucene90_0.dvm
├── [1.9M]  _1o.nvd
├── [ 139]  _1o.nvm
├── [ 994]  _1o.si
├── [ 392]  segments_z
└── [   0]  write.lock


0 directories, 32 files

4.1.2 关键类型

与“HNSW 索引”的配置相比,编码变为了ES814HnswScalarQuantizedVectorsFormat。与“HNSW 索引”的配置相比,存储占比的变化在于,产生了一个 737MB 大小的.veq文件,量级不同于.vec和.vex,介于两者之间。基于我们刚刚对 HNSW 索引的分析,我们知道 HNSW 索引不存储向量值本身,大小是到不了这个级别的。从 1.1 源码得知,.veq其实是 Scalar Quantized Vector Data,Flat 索引类型,标量量化的数据文件。事实上,在int8_hnsw的配置下,.vec是原始 32bit 的float32的向量,.veq是标量量化后所得到的 8bit 的int8(有符号的int7)的向量,所以存储大小差不多是前者的 1/4。而.vex的作用未发生变化,仍然是由原始向量构建的 HNSW 图结构,.vex的节点编号(node id)可以通过.vec文件快速获取到原始float32向量,也能通过.veq文件快速获取到量化后的int8向量。

fd415797dc77580e3acedb7a0c65abba.png

基于前文所提的行存裁剪优化,.fdt已经不再是文件占比的大头。

4.1.3 写入分析

ES814HnswScalarQuantizedVectorsFormat编码的定义如下:

public final class ES814HnswScalarQuantizedVectorsFormat extends KnnVectorsFormat {
    static final String NAME = "ES814HnswScalarQuantizedVectorsFormat";
    ...
    public ES814HnswScalarQuantizedVectorsFormat(int maxConn, int beamWidth, Float confidenceInterval, int bits, boolean compress) {
        super(NAME);
        ...
        this.flatVectorsFormat = new ES814ScalarQuantizedVectorsFormat(confidenceInterval, bits, compress);
    }


    @Override
    public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException {
        return new Lucene99HnswVectorsWriter(state, maxConn, beamWidth, flatVectorsFormat.fieldsWriter(state), 1, null);
    }


    @Override
    public KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException {
        return new Lucene99HnswVectorsReader(state, flatVectorsFormat.fieldsReader(state));
    }
    ...
}

与”HNSW 索引“类似,源码中也存在一个内部 Flat 编码ES814ScalarQuantizedVectorsFormat。但由于量化的引入,编码之间的关系变得更加复杂,笔者将源码中编码的关系进行类图展示:

15ae6a7e97b88ced820ba1ab9491660f.png

同样将源码中关键链路简化为流程图。可以看到在Lucene99ScalarQuantizedVectorsWriter这一关键链路中,写入动作伴随着量化,生成了新的.veq文件类型。

add1e6bfdef6853a80534575ec09cf88.png

4.1.4 读取分析

.fdt、.vec和.vex的作用不再赘述。在前文中提到,通过.veq文件可以获取到量化后的int8向量。事实上,.veq和.vec的读写方式和格式都如出一辙,唯一不同的就是向量值类型。由于 HNSW 图的.vex文件无法独立工作,它只能快速获取指定节点已构建好的m个近邻,如需相似度计算,则必须读取 Flat 索引。我们重点来分析采用”标量量化索引后“,什么时候需要读取.vec,什么时候需要读取.veq。

通过指定 HNSW 图节点编号读取向量值这个动作,无论是在.veq还是.vec中,都是一次随机读取,如果操作系统内存未做缓存,随着查询的增多,则会产生较大随机读盘,容易给整个集群带来压力和延迟。然而.veq的大小是.vec的 1/4。完全在 page cache 中缓存的代价要小于前者,发生 page cache 交换的概率也要小于前者,所以 ES 中量化优化的本质是,尽量使用读.veq的动作,代替读.vec的动作,从而减少内存开销和系统压力。

所以在量化后,需要读取.vec的场景,只有当索引发生段合并,重新构建 HNSW 图这一时刻。其余的读取动作,全部由.veq代替,与queryVector进行量化后的相似度计算。

4.2 二进制量化索引

二进制量化是 ES 8.16 引入的新的量化方式,它基于最新出炉的RabitQ算法,将 32bit 的向量,极限量化到 1bit。

4.2.1 索引构成

...
      "titleVector": {
        "type": "dense_vector",
        "dims": 768,
        "index": true,
        "similarity": "cosine",
        "index_options": {
          "type": "bbq_hnsw"
        }
      },
      ...

与使用int8_hnsw类似,对比 HNSW 索引,.ve*文件中间的编码名全部变成了ES816HnswBinaryQuantizedVectorsFormat。向量相关文件除了.vec、.vemf,.vex、.vem两套索引外,新产生了.veb和.vemb文件。

$ tree -h
.
├── [  65]  _22_0.doc
├── [  65]  _22_0.pos
├── [6.0M]  _22_0.tim
├── [542K]  _22_0.tip
├── [ 262]  _22_0.tmd
├── [118M]  _22_ES812Postings_0.doc
├── [134M]  _22_ES812Postings_0.pos
├── [ 46M]  _22_ES812Postings_0.tim
├── [1.0M]  _22_ES812Postings_0.tip
├── [ 681]  _22_ES812Postings_0.tmd
├── [103M]  _22_ES816HnswBinaryQuantizedVectorsFormat_0.veb
├── [2.9G]  _22_ES816HnswBinaryQuantizedVectorsFormat_0.vec
├── [ 68K]  _22_ES816HnswBinaryQuantizedVectorsFormat_0.vem
├── [3.2K]  _22_ES816HnswBinaryQuantizedVectorsFormat_0.vemb
├── [ 157]  _22_ES816HnswBinaryQuantizedVectorsFormat_0.vemf
├── [ 61M]  _22_ES816HnswBinaryQuantizedVectorsFormat_0.vex
├── [1.2M]  _22_ES87BloomFilter_0.bfi
├── [ 103]  _22_ES87BloomFilter_0.bfm
├── [ 663]  _22.fdm
├── [600M]  _22.fdt
├── [ 51K]  _22.fdx
├── [1.9K]  _22.fnm
├── [6.3M]  _22.kdd
├── [ 22K]  _22.kdi
├── [ 212]  _22.kdm
├── [ 26M]  _22_Lucene90_0.dvd
├── [1.8K]  _22_Lucene90_0.dvm
├── [1.9M]  _22.nvd
├── [ 139]  _22.nvm
├── [ 995]  _22.si
├── [ 390]  segments_12
└── [   0]  write.lock


0 directories, 32 files

4.2.2 关键类型

同为量化索引,对比标量量化可以发现。.veb的大小下降为.veq的约 1/8,这与两者的量化程度分别为 8bit 和 1bit 吻合。.vex大小始终未发生太大波动,因为它只包含 HNSW 图的节点和边信息,并不包含向量原始值。

编码变为了ES816HnswBinaryQuantizedVectorsFormat。

a21f0477fc1dc3cd3642c0a423493cde.png

4.2.3 写入分析

ES816HnswBinaryQuantizedVectorsFormat编码的定义如下:

public class ES816HnswBinaryQuantizedVectorsFormat extends KnnVectorsFormat {
    public static final String NAME = "ES816HnswBinaryQuantizedVectorsFormat";
    ...
    private static final FlatVectorsFormat flatVectorsFormat = new ES816BinaryQuantizedVectorsFormat();
    ...
    @Override
    public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException {
        return new Lucene99HnswVectorsWriter(state, maxConn, beamWidth, flatVectorsFormat.fieldsWriter(state), numMergeWorkers, mergeExec);
    }


    @Override
    public KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException {
        return new Lucene99HnswVectorsReader(state, flatVectorsFormat.fieldsReader(state));
    }
    ...
}

与标量量化类似,它的写入关键链路如图:

9057aba28e3b159c994011b710f4a841.png

4.2.4 读取分析

.veb的大小做到了.vec的 1/32,.veq的 1/8,它们都是 Flat 索引,承载 HNSW 索引定位向量值的任务。这也就意味着,如果做了二进制量化,可以比不做量化的 HNSW 索引,节省 97%(31/32)的内存开销!这即便放在向量搜索整个业界,也是遥遥领先的。同样,二进制量化也只有当索引发生段合并,重新构建 HNSW 图这一时刻,需要读取.vec。

4.3 小结

进一步对向量索引类型进行归纳总结:

腾讯云ChatBI适用于期望能提升数据分析效率的行业,它能有效解决以下痛点场景:

88d433aa325783db5f43dffc1b767cc7.png

需要一提的是,所有的量化都是有损失的,具体的影响就是向量查询召回率的下降,量化程度越高,召回率的下降越明显。ES 提供的量化方式,在公开的数据集上对召回率影响较为理想,均给出了个位数百分比的召回率下降。实际使用中,具体的下降程度和向量数据集有关。可以粗略理解为:如果原始向量的冗余程度高,则量化后仍能表达出向量的核心含义;如果原始向量的冗余程度低,则量化后可能承载不了原始向量所有的表达,会损失掉的表达就多。ES 8.16 默认已经给出了int8_hnsw的量化索引存储方式,该量化方式已经在较多公开数据集上得到了较好的效果。其它更深度的量化,建议经过实际业务场景测试后再选择使用。

在研究的过程中,笔者还发现了官方在此功能上的 bug,也已将 PR 贡献给社区进行了修复。

5. 量化裁剪优化

5.1 发现进一步的冗余存储

在进行了行存裁剪后,我们发现存储的大头不再是.fdt,而是变成了.vec。在上一章节介绍的量化索引中,注意到除了保存了一份.vec原始向量外,还保存了一份量化后的向量.veq或.veb。量化技术实现了内存的低门槛,降低到了原始内存开销的 25% - 97%(从 32bit 量化到 8bit 甚至 1bit),然而对于磁盘来讲存储却是上升了,上升的幅度等同于.vec大小的 25% - 97%。那么是否可以连.vec都舍弃掉,将磁盘存储也得到内存的优化效果?

5.2 re-hydrate

答案是可以的。量化实际是将原始高位向量压缩成低位向量的一种算法,如果把量化比作”脱水“,那这类算法函数的逆运算,就可以实现反向”复水“得到原来的向量。当然由于低位不能完全表示高位,在精度上会有一定损失,但它带来的是磁盘存储的进一步下降,对于存储有强烈需求的客户仍然具有很高的实际意义。

参考 4.1.2 小节,如果在int8_hnsw的索引中裁剪掉.vec,能在行存裁剪的基础上进一步节省 67% 的存储,相比行存裁剪前的原始大小节省了 91%。参考 4.2.2 小节,如果在bbq_hnsw的索引中裁剪掉.vec,能在行存裁剪的基础上进一步节省 78% 的存储,相比行存裁剪前的原始大小节省了 95%。由于越深程度的量化,越难以精确”复水“还原回原始向量,91% 和 95% 的差距已不悬殊,没有必要为 4% 的磁盘存储做更大牺牲,所以量化裁剪这一优化,我们选择在int8_hnsw上进行。在实际的测试中,int8_hnsw上进行的量化裁剪,对于召回率的影响很小,仅有个位数百分比,在前文使用的 so_vector 数据集上,kNN 的召回率和排序并未发生变化。

POST so_vectors,so_vectors_int8_only_hnsw/_search?track_total_hits=true
{
  "_source": false,
  "knn": {
    "field": "titleVector",
    "query_vector": [
      -0.04791254550218582,
      0.03103054128587246,
      ...
    ],
    "k": 10,
    "num_candidates": 100
  },
  "size": 10
}


{
  "took": 5,
  "timed_out": false,
  "_shards": {
    "total": 6,
    "successful": 6,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 10,
      "relation": "eq"
    },
    "max_score": 0.9985881,
    "hits": [
      {
        "_index": "so_vectors_int8_only_hnsw",
        "_id": "dbYN2ZMBfvoRs6nTTpRh",
        "_score": 0.9985881
      },
      {
        "_index": "so_vectors_int8_only_hnsw",
        "_id": "4bgO2ZMBfvoRs6nT6kJe",
        "_score": 0.99840474
      },
      {
        "_index": "so_vectors",
        "_id": "dbYN2ZMBfvoRs6nTTpRh",
        "_score": 0.99803877
      },
      {
        "_index": "so_vectors",
        "_id": "4bgO2ZMBfvoRs6nT6kJe",
        "_score": 0.99797726
      },
      {
        "_index": "so_vectors_int8_only_hnsw",
        "_id": "jbYN2ZMBfvoRs6nTPnpV",
        "_score": 0.893219
      },
      {
        "_index": "so_vectors_int8_only_hnsw",
        "_id": "-bgO2ZMBfvoRs6nT1Sh5",
        "_score": 0.8931048
      },
      {
        "_index": "so_vectors",
        "_id": "jbYN2ZMBfvoRs6nTPnpV",
        "_score": 0.89258575
      },
      {
        "_index": "so_vectors",
        "_id": "-bgO2ZMBfvoRs6nT1Sh5",
        "_score": 0.892323
      },
      {
        "_index": "so_vectors",
        "_id": "jrwU2ZMBfvoRs6nT7n3Q",
        "_score": 0.8819847
      },
      {
        "_index": "so_vectors_int8_only_hnsw",
        "_id": "jrwU2ZMBfvoRs6nT7n3Q",
        "_score": 0.8813083
      }
    ]
  }
}

5.3 源码改造细节

引入VPackHnswScalarQuantizedOnlyVectorsFormat编码,定义如下:

public final class VPackHnswScalarQuantizedOnlyVectorsFormat extends KnnVectorsFormat {
    static final String NAME = "VPackHnswScalarQuantizedOnlyVectorsFormat";
    ...
    public ES814HnswScalarQuantizedVectorsFormat(int maxConn, int beamWidth, Float confidenceInterval, int bits, boolean compress) {
        super(NAME);
        ...
        this.flatVectorsFormat = new VPackScalarQuantizedOnlyVectorsFormat(confidenceInterval, bits, compress);
    }


    @Override
    public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException {
        return new Lucene99HnswVectorsWriter(state, maxConn, beamWidth, flatVectorsFormat.fieldsWriter(state), 1, null);
    }


    @Override
    public KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException {
        return new Lucene99HnswVectorsReader(state, flatVectorsFormat.fieldsReader(state));
    }
    ...
}

写入的关键链路:

277618d18af834ee4fa1be5a3b5cb6b5.png

5.4 增强功能成效

...
      "titleVector": {
        "type": "dense_vector",
        "dims": 768,
        "index": true,
        "similarity": "cosine",
        "index_options": {
          "type": "int8_only_hnsw"
        }
      },
      ...

对比标量量化索引,与 4.1.2 章节的索引关键类型进行对比,可以发现整体存储进一步下降。

$ tree -h
.
├── [  65]  _56_0.doc
├── [  65]  _56_0.pos
├── [6.0M]  _56_0.tim
├── [542K]  _56_0.tip
├── [ 262]  _56_0.tmd
├── [117M]  _56_ES812Postings_0.doc
├── [134M]  _56_ES812Postings_0.pos
├── [ 45M]  _56_ES812Postings_0.tim
├── [1.0M]  _56_ES812Postings_0.tip
├── [ 681]  _56_ES812Postings_0.tmd
├── [1.2M]  _56_ES87BloomFilter_0.bfi
├── [ 103]  _56_ES87BloomFilter_0.bfm
├── [ 68K]  _56_ESHnswScalarQuantizedOnlyVectorsFormat_0.vem
├── [ 181]  _56_ESHnswScalarQuantizedOnlyVectorsFormat_0.vemq
├── [736M]  _56_ESHnswScalarQuantizedOnlyVectorsFormat_0.veq
├── [ 56M]  _56_ESHnswScalarQuantizedOnlyVectorsFormat_0.vex
├── [ 663]  _56.fdm
├── [599M]  _56.fdt
├── [ 51K]  _56.fdx
├── [1.9K]  _56.fnm
├── [6.4M]  _56.kdd
├── [ 22K]  _56.kdi
├── [ 212]  _56.kdm
├── [ 26M]  _56_Lucene90_0.dvd
├── [1.8K]  _56_Lucene90_0.dvm
├── [1.9M]  _56.nvd
├── [ 139]  _56.nvm
├── [ 902]  _56.si
├── [ 391]  segments_5o
└── [   0]  write.lock


0 directories, 30 files

e0d37b234f5cb03072bddc4dbac3031d.png

下面的表格概括了在纯向量搜索场景和混合搜索场景,该功能的最终效果,对于存储的节省均达到了 90% 以上。

7e4d96d167357c504fc16c7a2f69feb3.png

5.5 腾讯云 ES 的贡献

**腾讯云 ES 自发完成了该功能实现,并且在腾讯云 ES 新发布的 ES 8.16.1 版本上,集成在了腾讯云自研的 v-pack 向量增强插件中。通过字段 mapping 配置中指定"index_options.type": "int8_only_hnsw"来启用,即可以直接使用到降低 90% 存储的优化特性。**不同于行存裁剪,量化裁剪对召回率有微弱的影响,用户可以自行判断是否启用。虽然该功能相对实验性,但它进一步降低了 ES 向量搜索的存储门槛,拓展了不同读写流程。后续我们仅需要对”复水“算法进行优化,即可不断弱化这一影响,从而使其得到更广泛的使用。

6. 总结

6.1 内容回顾

本文较为深入地探索了 Elasticsearch 8.16.1 的向量存储,包括其索引构成、读写流程、量化技术,整体理清了向量功能新增的索引类型之间的关系,帮助读者进一步理解 Elasticsearch 的向量搜索功能。同时介绍了腾讯云 ES 在向量功能上对 ES 社区的多个 PR 贡献,以及介绍了腾讯云 ES 最新发布的 8.16.1 版本的 v-pack 向量增强插件,带来的「行存裁剪」和「量化裁剪」能力,分别可以做到节省 70% 和 90% 存储的优化。

3016c77b3eb7eb4514d01a74090d7628.png

6.2 系列展望

笔者希望这个文章系列,对 ES8 引入的向量功能各个方面都能够进行解析和探索,以达到科普和共同进步的目的。也希望更多有向量/混合搜索需求的用户,可以尝试和使用腾讯云 ES 向量搜索增强版,如果您也有独到的需求或想法,可以联系到腾讯云 ES 团队,我们将竭尽全力与您共同探索与解决。目前腾讯云 ES 紧跟社区步伐,发布了 8.16.1 版本,集成了自研的 v-pack 向量增强插件,不断地增加我们的自研功能和优化,我们会在后面带来更多的剖析。由于水平有限,如果文章中有错误之处,敬请谅解,笔者十分愿意进行探讨和勘误;如果您有感兴趣的 ES 向量功能点,可以进行留言,笔者也会纳入后续系列文章的选题考虑。

END

关注腾讯云大数据╳探索数据的无限可能

679c4183a7ac3778ccffa1fb3bff1fec.png

⏬点击阅读原文

了解更多产品详情

outside_default.png

我知道你在看哟

afd1d23db71030d5ae92ac07597b67b0.png

PCIe-4.2.3.2 Processing of Ordered Sets During Flit Mode Data Stream 动态门控检测(Gated Detection):绝不能在非预定时钟周期进行全时段 OS 匹配。这能节省大量动态功耗。并行容错比较器:必须实现 64-bit 宽的容错匹配器(5/8 表决),这对时序(Timing)要求较高,需要考虑多级流水线(Pipeline)设计。变长状态机(Variable Length FSM):SKP 子状态机必须具有Count_8B计数器,并且要有上限强退保护(Max Count = 6)。控制信号广播。 阅读详情

相关推荐

从零到一:用AXI DataMover在Zynq MPSoC上实现高效DMA传输(附Vitis HLS驱动示例)

本文详细介绍了如何在Xilinx Zynq UltraScale+ MPSoC平台上使用AXI DataMover实现高效的DMA数据传输。通过解析AXI DataMover架构、Vivado工程配置、Vitis HLS驱动开发及性能优化技巧,帮助开发者快速构建高性能数据传输通道,并提供了可直接复用的代码示例。

weixin_42520796的博客 425

告别规则黑盒:基于JVS-Rules的风控规则治理实践——可视化决策编排+血缘追踪+版本快照

可读’——通过决策流图谱与业务语义节点,让非技术人员理解逻辑;‘可溯’——依赖规则版本快照与执行日志,实现任意交易的全链路回放;‘可担责’——基于血缘追踪与权限基线,清晰界定业务定义、技术实现、合规验证三方责任。这三者共同构成风控规则治理的三角支柱,将隐性经验转化为可版本化、可审计、可协同的组织级数字资产。

weixin_42054392的博客 423

用Python+LLM实现小说自动转剧本:从爬取到生成的完整实战指南(附GitHub源码)

本文提供了一份完整的实战指南,详细介绍了如何利用Python和大语言模型(LLM)构建一个自动化的小说转剧本工具。文章从网络爬虫获取文本开始,逐步讲解了使用LLM进行智能解析与结构化、集成专业模型进行情绪分析与动作生成,最终合成并输出格式规范的剧本文档。通过模块化架构设计和附带的GitHub源码,帮助开发者掌握从数据获取到内容生成的全流程,实现高效的小说剧本转换器。

xray4的博客 413

git checkout 与 git switch 的区别

git checkout 与 git switch 的区别

choumin的专栏 195

【5G演进】4G到5G演进之网络架构与协议栈

4G到5G演进之网络架构与协议栈

Learning makes me happy (0^◇^0) 5530

12、探索新型高能量密度材料:FOX-7的化学进展

本文全面探讨了新型高能量密度材料FOX-7的化学进展,包括其独特的分子结构、优异的热稳定性、光谱性质以及爆炸性能。对比传统高爆炸药RDX,FOX-7展现出更低的敏感性和更高的安全性。文章还介绍了FOX-7的合成方法、异构体研究、与其结构相关的化合物以及富氮高能材料的最新发展。通过实验和计算方法,研究人员不断优化这类材料的性能,以满足军事和工业领域的高要求。

sea99的博客 350

如何“无视“模型的上下文长度

本文阐述了在有限上下文窗口下实现“无限”对话体验的五层机制:分层窗口管理(保留近期对话、摘要早期内容)、跨会话记忆结构化存储与定期整理、按需加载技能目录、子代理隔离处理长任务、检索代替记忆。核心原则为“压流水不压事实”“检索即拉取”“写少读准”,通过压缩、迁移、按需注入与审计日志,实现高效、可追溯、低污染的长程交互。

m0_63486540的博客 355

【动态导通电阻】p-GaN HEMTs正向和反向导通下的动态导通电阻

2024年,浙江大学的研究团队通过多组双脉冲测试方法,深入研究了两种商用p-GaN栅极HEMTs在正向和反向导通模式下的动态导通电阻(RON)特性。研究发现,肖特基型p-GaN栅极HEMTs在反向导通时动态RON比正向导通高3%-5%,而混合漏极嵌入栅注入晶体管在反向导通时动态RON降级较轻,降低约10%。此外,随着漏极电流(IDS)的增加,两种器件在反向导通时动态RON先减小后增加,形成“谷底”现象。

荷塘阅色 827

CommonJS 和 ES Module 区别,前端模块化一次讲透

模块化是 JS 工程化的基石。和。Node.js 早期默认用 CommonJS,也就是我们熟悉的require;而 ES Module 是 ES6 官方推出的标准模块方案,使用importexport。很多初学者容易混淆:什么时候用 require?什么时候用 import?两者能不能互相混用?导出导入的值有什么差别?本篇从语法、加载机制、值引用、使用场景、底层差异逐个拆解。适合人群:刚接触工程化、学习 Node、准备面试的前端同学。老 Node 项目、后端脚本、工具脚本。

2603_95923710的博客 192

23 RAG 为什么答不准:召回、分块、排序的常见坑

RAG 答不准时,不要先怪模型。本文把常见失败拆成召回不到、召回不准、分块不完整、排序靠后、上下文污染和生成不忠实六类,并给出从 Top-K 检查到评估集的排查流程。

老于的笔记的专栏 246

深度学习笔记——前向传播与反向传播、神经网络(前馈神经网络与反馈神经网络)、常见算法概要汇总

本文简要介绍深度学习的前向传播与反向传播,以及前馈神经网络与反馈神经网络。

haopinglianlian的博客 3043

【Git】从入门到精通(十)—— 撤销与恢复:git checkout与git restore

本文系统讲解Git中撤销与恢复的核心命令,重点对比git checkout与新命令git restore的使用场景。针对“改错、add错、提交错”等常见问题,提供清晰解决方案:未暂存修改用git restore <file>,误暂存用git restore --staged,历史版本恢复用--source。强调git restore语义更清晰,推荐优先使用。同时提醒撤销操作不可逆,建议先git status、git diff确认,不确定时用git stash保底。最后总结速查表,帮助初学者安全高

m0_67393827的博客 463

《Android最全面试题-Offer直通车》目录

如今Android求职是僧多粥少,竞争非常激烈,企业招聘时要求很高,本专栏汇聚android面试各种考点,各大公司Android面试题,《Android 最全面试题》将助你全方位提升,助您脱颖而出,早日拿到满意Offer。本专栏汇聚了海量优质内容,文章数量超过100 篇。凭借过硬的质量,每月新增订阅用户数以百计,如今仅需特惠价 9.9 元,限时抢购,后续将逐步恢复原价,早订阅早开启逆袭之旅。精心筹备,整理出一系列超万字精华篇章,累计数十万的 Android 面试题深度剖析。

大模型大数据攻城狮的专栏 2867

第1章第1节:什么是Elasticsearch

在传统数据库里搜"一句话中包含某个词",我们通常会写出%关键词%这样的 LIKE 查询——数据一多就慢得无法忍受,而且完全不懂"分词"和"相关度"。本节从全文检索的真实需求出发,讲清楚倒排索引这一核心思想,再介绍 Elasticsearch 是什么、能干什么,以及它在 ELK/EFK 生态中的位置。

啊渊的专栏 173

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

Claude Code做Unity项目移植(三): 使用SKILL进行自动化Git提交

本文介绍基于ClaudeCodeSkill的Unity自动化Git提交助手——unity-git-commit,通过四大模块实现提交前的智能检查与安全修复:1)校验Git仓库状态;2)验证Unity项目文件(如排除Library/Temp,确保.meta文件完整);3)依据可配置的C#命名规范自动识别并安全修复命名问题(避免序列化、反射等高危场景误改);4)生成精准提交注释,但强制人工确认。全程仅本地commit,不自动推送或破坏性操作,保障安全。核心思想为“机械化工作交AI,关键决策留人工”,显著提升提交

Aman1984的博客 644

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

【Git】从入门到精通(九)—— git diff:查看文件差异

本文全面解析Git的gitdiff命令,详解其三大核心场景:工作区vs暂存区、暂存区vs版本库、工作区vs版本库,涵盖输出格式、常用选项(如--stat、--name-only、-w)、版本比较、文件差异及外部工具集成。通过实战案例演示代码审查、提交自查、调试定位等应用场景,强调gitdiff--staged作为提交前检查的关键作用,并总结记忆口诀与常见问题解决方案,助力开发者精准掌握代码变更细节。

m0_67393827的博客 286

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

多存储协同架构:JPA + MyBatis + Elasticsearch + Redis 的职责分工与通用实现

本文阐述了在高并发订单系统中,通过JPA、MyBatis、Elasticsearch与Redis的多存储协同架构实现职责分离:JPA负责事务性写操作与简单查询;MyBatis处理复杂联查与报表统计;Elasticsearch承载海量数据下的多维检索与聚合;Redis用于缓存、防回滚暂存及分布式锁。各存储分工明确,通过异步同步机制联动,兼顾一致性、性能与扩展性,构建高效稳定的系统架构。

BADAO_LIUMANG_QIZHI的博客 246

WSL 中 Git Clone GitHub 仓库完整踩坑指南

问题最佳解法WSL 证书验证失败或重装 ca-certificates22 端口超时SSH 走缺少密钥+ 添加到 GitHub权限过宽被拒Gitee 认证失败使用 PAT 替代密码 + 清除凭据缓存--system权限拒绝改用--global或--local作用域一条黄金建议:在 WSL 中访问 GitHub,优先使用 SSH over 443。一次配置,永久解决证书、端口、认证三重问题,比 HTTPS 方式稳定得多。

dingyuana的专栏 359

用 QuantDash 读美股行情:项目目录、配置边界与 AAPL.US 数据校验实践

面向个人开发者:先划清凭证、接入点、数据口径三层配置边界,再用适配层把厂商细节隔离,最后用 AAPL.US 走通读取与校验,并整理美股行情的高频易错点。

weixin_45515140的博客 302

【Git】第3期 多人协作与企业级开发规范详解

【Git】第3期 多人协作与企业级开发规范详解

2501_93298789的博客 193

点过“不再询问”就要检查:Claude Code权限文件也可能存着密钥

Apiiro报告揭示带密钥的命令可能进入Claude Code权限文件。结合独立Git实验,解释local配置、忽略规则与历史泄露的区别,以及发现问题后的处理顺序。

2301_76711398的博客 180

给 Agent 知识库的每次写入留一笔行数账:回执带统计、提交信息带板块与行数、复盘再做 git 审计

如果你也在给 Agent 做知识库、记忆系统,或者任何"AI 往里写东西"的存储,照这个顺序落地:写入时算出行变更(只算正文、多重集计数就够了);把行数写进提交信息和工具回执(同一组数字,两个地方);给"净删多行零新增"加一道当场警告;最后在复盘流程里加 git 审计。每一步都有独立的验收标准,任何一步没过关都不要往下一步走。最后说一句判断标准:当你的 Agent 说"已写入"时,先别信,先看回执里的 (+N −M)。一行没增、删了一堆的写入,十有八九不是改写,是丢失。,觉得有用欢迎点个 star。

遇码开源社区 295

【Git】从入门到精通(十一)—— git reset 三种模式详解

Git reset用于移动HEAD指针,可选重置暂存区和工作区,有三种模式:--soft(仅移HEAD,改动保留在暂存区)、--mixed(默认,移HEAD并清暂存区,改动留工作区)、--hard(彻底清除,危险)。适用于撤销提交、取消暂存或丢弃修改。操作前需确认状态,避免数据丢失;误操作可用reflog恢复。与git revert不同,reset改写历史,不推荐用于已推送的公共分支。

m0_67393827的博客 319

Ubuntu安装Elasticsearch

Elasticsearch下载解压后,进入bin目录执行./elasticsearch启动,系统自动配置安全功能:启用认证、加密集群连接及集群状态。初始化时生成elastic用户密码(如aaaaaaa)和HTTP CA证书指纹。可通过bin/elasticsearch-reset-password -u elastic重置密码。配置Kibana时,使用提供的30分钟有效 enrollment token完成连接。

SeasonedDriverDG的博客 45

4 份 AI 写的检查脚本,2 份让 git bisect 找对提交,2 份让它指着最后一个提交说“找到了“:外加 200 个提交里 8 次测试对 138 次

你的判断只有编码进机器能读到的那个通道,才算存在。写在文字里的失败,对 git 来说等于没写:屏幕刷 7 次 FAIL,它只读进程退出那一个数字。反过来,机器读到了信号也不代表信号是真的,15% 的误报就够让二分指着隔壁那个提交。Netcraft 那串数能被引用到今天,靠的也是它把"互联网有多大"压成了一个可复算的数——顺带提醒,那页上 1995 年和 2006 年两个数用的还是不同的单位。可以带走四个动作:给 AI 写检查脚本的提示里加一句"结论只用退出码表达,0 通过、非 0 失败";

weixin_41940856的博客 329
上一篇: WeData AI助手:对接 Deepseek 满血版,构建「Data+AI」智能开发范式
下一篇: 腾讯云 X DeepSeek:四款新品接入,玩转「AI数智人」、「智能客服」、「商业分析」、「合同签署」...
腾讯云大数据
博客等级 码龄6年 348粉丝 218原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值