前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >节省 90% 存储!源码级揭秘腾讯云 ES 向量搜索的优化之道

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

作者头像
腾讯QQ大数据
发布2025-02-24 10:58:56
发布2025-02-24 10:58:56
13300
代码可运行
举报
运行总次数:0
代码可运行

本文共计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 源码上看,有如下文件类型被引入:

代码语言:javascript
代码运行次数:0
复制
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有着同样的继承关系。

代码语言:javascript
代码运行次数:0
复制
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;
  ...
}
代码语言:javascript
代码运行次数:0
复制
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*类型的文件:

代码语言:javascript
代码运行次数:0
复制
$ 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如下:

代码语言:javascript
代码运行次数:0
复制
{
  "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 小结

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

2. 索引

2.1 无索引

2.1.1 索引构成

代码语言:javascript
代码运行次数:0
复制
 ...
      "titleVector": {
        "type": "dense_vector",
        "dims": 768,
        "index": false
      },
      ...

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

代码语言:javascript
代码运行次数:0
复制
$ 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,分别是行存和列存文件。由于未启用向量索引,我们有理由认为该配置下,向量字段只会引发行存和列存的存储大小增长。

2.1.3 写入分析

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

2.1.4 读取分析

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

代码语言:javascript
代码运行次数:0
复制
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 索引构成

代码语言:javascript
代码运行次数:0
复制
      ...
      "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实际是编码名,这里我们留个印象。

代码语言:javascript
代码运行次数:0
复制
$ 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类型的向量索引数据文件中。

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

代码语言:javascript
代码运行次数:0
复制
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编码,源码有如下定义:

代码语言:javascript
代码运行次数:0
复制
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的代理模式实现。笔者同样将源码中关键链路简化为流程图。

2.2.4 读取分析

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

代码语言:javascript
代码运行次数:0
复制
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 索引构成

代码语言:javascript
代码运行次数:0
复制
      ...
      "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。

代码语言:javascript
代码运行次数:0
复制
$ 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 图的结构,不包含任何实际数据。

2.3.3 写入分析

Lucene99HnswVectorsFormat编码的定义如下:

代码语言:javascript
代码运行次数:0
复制
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 后普遍使用。写入的关键链路如下:

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),给出更优的选择。

代码语言:javascript
代码运行次数:0
复制
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 小结

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

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竟然不支持列存获取。

代码语言:javascript
代码运行次数:0
复制
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语法的使用。

代码语言:javascript
代码运行次数:0
复制
    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"
            );
        }
        ...
    }
代码语言:javascript
代码运行次数:0
复制
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写入链路优化为如下流程。

3.4 增强功能成效

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

代码语言:javascript
代码运行次数:0
复制
$ 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

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

3.5 腾讯云 ES 的贡献

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

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

4. 量化

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

4.1 标量量化索引

4.1.1 索引构成

代码语言:javascript
代码运行次数:0
复制
      ...
      "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吗?不是的。

代码语言:javascript
代码运行次数:0
复制
$ 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向量。

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

4.1.3 写入分析

ES814HnswScalarQuantizedVectorsFormat编码的定义如下:

代码语言:javascript
代码运行次数:0
复制
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。但由于量化的引入,编码之间的关系变得更加复杂,笔者将源码中编码的关系进行类图展示:

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

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 索引构成

代码语言:javascript
代码运行次数:0
复制
      ...
      "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文件。

代码语言:javascript
代码运行次数:0
复制
$ 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。

4.2.3 写入分析

ES816HnswBinaryQuantizedVectorsFormat编码的定义如下:

代码语言:javascript
代码运行次数:0
复制
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));
    }
    ...
}

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

4.2.4 读取分析

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

4.3 小结

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

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

需要一提的是,所有的量化都是有损失的,具体的影响就是向量查询召回率的下降,量化程度越高,召回率的下降越明显。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 的召回率和排序并未发生变化。

代码语言:javascript
代码运行次数:0
复制
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编码,定义如下:

代码语言:javascript
代码运行次数:0
复制
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));
    }
    ...
}

写入的关键链路:

5.4 增强功能成效

代码语言:javascript
代码运行次数:0
复制
      ...
      "titleVector": {
        "type": "dense_vector",
        "dims": 768,
        "index": true,
        "similarity": "cosine",
        "index_options": {
          "type": "int8_only_hnsw"
        }
      },
      ...

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

代码语言:javascript
代码运行次数:0
复制
$ 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

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

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% 存储的优化。

6.2 系列展望

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-02-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯云大数据 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档