前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >ES8 向量功能窥探系列(二):向量数据的存储与优化

ES8 向量功能窥探系列(二):向量数据的存储与优化

原创
作者头像
Rassyan
发布于 2025-02-20 13:22:32
发布于 2025-02-20 13:22:32
5590
举报

导语

在上一篇文章《ES8 向量功能窥探系列(一):混合搜索功能初探与增强》中,我们初步探讨了 Elasticsearch 8.x 的混合搜索功能,包括 kNN 查询流程、RRF 融合算法以及相关的功能增强。本篇文章将继续这一系列,聚焦于 Elasticsearch 中向量数据的存储与优化。我们将从向量数据的索引构成,读写流程,一直到量化技术,一步步带读者对 Elasticsearch 向量索引存储机制形成全面理解。同时也将解读腾讯云 ES 向量增强版,如何助力业务实现节省 70% - 90% 存储的优化。

1. 存储

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

1.1 源码概览

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

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

代码语言:java
AI代码解释
复制
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 相关编码,它们之前是继承关系,提供了不同的ReaderWriter,事实上KnnVectorsWriter&FlatVectorsWriter,以及KnnVectorsReader&FlatVectorsReader有着同样的继承关系。

代码语言:java
AI代码解释
复制
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;
  ...
}
代码语言:java
AI代码解释
复制
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 存储概览

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

代码语言:bash
AI代码解释
复制
$ 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如下:

代码语言:json
AI代码解释
复制
{
  "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 小结

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

索引数据文件

元信息文件

索引类型

.vec

.vemf

Flat

.vex

.vem

HNSW

.veq

.vemq

Flat

.veb

.vemb

Flat

2. 索引

2.1 无索引
2.1.1 索引构成
代码语言:json
AI代码解释
复制
      ...
      "titleVector": {
        "type": "dense_vector",
        "dims": 768,
        "index": false
      },
      ...

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

代码语言:bash
AI代码解释
复制
$ 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语法变相进行向量搜索。

代码语言:json
AI代码解释
复制
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 索引构成
代码语言:json
AI代码解释
复制
      ...
      "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实际是编码名,这里我们留个印象。

代码语言:bash
AI代码解释
复制
$ 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 索引的存储是将所有的向量数据直接存储在一个连续的数组,与列存的做法几乎一致,可以完美地解释这一变化。在从源码分析也能得到相同的结论。

代码语言:java
AI代码解释
复制
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编码,源码有如下定义:

代码语言:java
AI代码解释
复制
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查询语法,而后者可以使用。

代码语言:json
AI代码解释
复制
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 索引构成
代码语言:json
AI代码解释
复制
      ...
      "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

代码语言:bash
AI代码解释
复制
$ 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编码的定义如下:

代码语言:java
AI代码解释
复制
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),给出更优的选择。

代码语言:java
AI代码解释
复制
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 小结

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

index_options.type 启用方法

索引具体类型

编码

核心索引数据文件

核心元信息文件

flat

原始向量 Flat 索引

ES813FlatVectorFormat

.vec

.vemf

hnsw

HNSW 索引

Lucene99HnswVectorsFormat

.vex

.vem

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

代码语言:json
AI代码解释
复制
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语法的使用。

代码语言:java
AI代码解释
复制
    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"
            );
        }
        ...
    }
代码语言:java
AI代码解释
复制
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作为DenseVectorFieldTypeDocValueFormat
  • 不改动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所占用的存储得到了显著的改变。

代码语言:bash
AI代码解释
复制
$ 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% 以上。这一显著的效果也说明,只要涉及了向量搜索,无论是纯向量还是混合场景,占据存储的大头基本来到了向量这里。

默认索引配置

裁剪向量行存

纯向量搜索场景:vectors (250w docs) rally 的dense_vector 数据集,只有一列 96 维的向量字段

4.78GB

1.09GB

节省 77% 磁盘

混合搜索场景:so_vector (200w docs) rally 的so_vector数据集,是 StackOverflow 的问答数据,有 10 列字段,1 列是 768 维的向量

32.21GB

9.10GB

节省 72% 磁盘

3.5 腾讯云 ES 的贡献

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

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

4. 量化

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

4.1 标量量化索引
4.1.1 索引构成
代码语言:json
AI代码解释
复制
      ...
      "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吗?不是的。

代码语言:bash
AI代码解释
复制
$ 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编码的定义如下:

代码语言:java
AI代码解释
复制
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 索引构成
代码语言:json
AI代码解释
复制
      ...
      "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文件。

代码语言:bash
AI代码解释
复制
$ 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编码的定义如下:

代码语言:java
AI代码解释
复制
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

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

4.3 小结

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

index_options.type 启用方法

索引具体类型

编码

核心索引数据文件

核心元信息文件

flat

原始向量 Flat 索引

ES813FlatVectorFormat

.vec

.vemf

hnsw

HNSW 索引

Lucene99HnswVectorsFormat

.vex

.vem

int8_hnsw / int4_hnsw

标量量化 HNSW 索引

ES814HnswScalarQuantizedVectorsFormat

.veq

.vemq

bbq_hnsw

二进制量化 HNSW 索引

ES816HnswBinaryQuantizedVectorsFormat

.veb

.vemb

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

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 的召回率和排序并未发生变化。

代码语言:json
AI代码解释
复制
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编码,定义如下:

代码语言:java
AI代码解释
复制
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 增强功能成效
代码语言:json
AI代码解释
复制
      ...
      "titleVector": {
        "type": "dense_vector",
        "dims": 768,
        "index": true,
        "similarity": "cosine",
        "index_options": {
          "type": "int8_only_hnsw"
        }
      },
      ...

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

代码语言:bash
AI代码解释
复制
$ 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% 以上。

默认索引配置

行存裁剪

量化裁剪

纯向量搜索场景:vectors (250w docs)

4.78GB

1.09GB

节省 77% 磁盘

449.16MB

节省 91% 磁盘

混合搜索场景:so_vector (200w docs)

32.21GB

9.10GB

节省 72% 磁盘

3.38GB

节省 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 向量功能点,可以进行留言,笔者也会纳入后续系列文章的选题考虑。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Elasticsearch BBQ与OpenSearch FAISS:向量搜索性能对比
基于二进制量化的向量搜索:Elasticsearch使用BBQ技术比OpenSearch搭配FAISS快5倍。我们收到社区要求,希望能解释Elasticsearch与OpenSearch在语义搜索/向量搜索方面的性能差异。因此,我们进行了一系列性能测试,以提供清晰、数据驱动的比较。
点火三周
2025/04/22
1220
Elasticsearch BBQ与OpenSearch FAISS:向量搜索性能对比
向 AI Search 迈进,腾讯云 ES 自研 v-pack 向量增强插件揭秘!
2025 年 1 月,腾讯云 ES 团队上线了 Elasticsearch 8.16.1 AI 搜索增强版,此发布版本重点提升了向量搜索、混合搜索的能力,为 RAG 类的 AI Search 场景保驾护航。除了紧跟 ES 官方在向量搜索上的大幅优化动作外,腾讯云 ES 还在此版本上默认内置了一个全新的插件 —— v-pack 插件。v-pack 名字里的"v"是 vector 的意思,旨在提供更加丰富、强大的向量、混合搜索能力。本文将对该版本 v-pack 插件所包含的功能做大体的介绍。
Rassyan
2025/03/21
2960
向 AI Search 迈进,腾讯云 ES 自研 v-pack 向量增强插件揭秘!
在Elasticsearch中如何选择精确和近似的kNN搜索
语义搜索 是一个用于相关度排序的强大工具。它不仅使用关键词,还考虑文档和查询的实际含义。
点火三周
2024/05/26
5680
在Elasticsearch中如何选择精确和近似的kNN搜索
Elasticsearch vs. OpenSearch: 向量搜索性能比较
TLDR: Elasticsearch 的速度高达 OpenSearch 的 12 倍 - 我们在 Elastic 收到了大量关于 Elasticsearch 和 OpenSearch 性能差异的询问,特别是在语义搜索/向量搜索方面。为了解决这个问题,我们进行了性能测试,以提供一个清晰的数据驱动比较。结果显示,Elasticsearch 的向量搜索速度高达 OpenSearch 的 12 倍,因此需要的计算资源更少。这反映了 Elastic 在巩固 Lucene 作为最佳向量数据库方面的专注,适用于搜索和检索用例。
点火三周
2024/07/05
7730
Elasticsearch vs. OpenSearch: 向量搜索性能比较
Elasticsearch向量搜索进化史:从7.x到8.15的关键创新
回顾在Elasticsearch从最早版本到最新8.15.0版本中,关于速度、规模和相关性的所有变化,真是令人惊叹,我们已经走了多远。
点火三周
2024/09/11
8350
Elasticsearch向量搜索进化史:从7.x到8.15的关键创新
向量检索(RAG)之向量数据库研究
研究内容主要包括:是否开源,支持的功能有哪些(是否支持暴力检索,支持哪些索引),是否有可视化界面,是否支持标量过滤。
码之有理
2025/03/05
6910
Lucene 中的标量量化:如何优化存储和搜索向量
HNSW 是一种功能强大且灵活的存储和搜索向量的方法,但它需要大量内存才能快速运行。例如,查询 100 万个 768 维度的 float32 向量大约需要 3GB 的 RAM。一旦开始搜索大量向量,这将变得非常昂贵。通过字节量化可以节省大约 75% 的内存。Lucene 以及 Elasticsearch 早已支持字节向量的索引构建,但这些向量的构建一直是用户的责任。这种情况即将改变,因为我们在 Lucene 中引入了 int8 标量量化。
点火三周
2024/07/03
3500
Lucene 中的标量量化:如何优化存储和搜索向量
突破性进展:在 Elasticsearch 和 Lucene 中应用更好的二进制量化 (BBQ) 实现高效向量搜索
嵌入模型输出的 float32 向量通常过大,不利于高效处理和实际应用。Elasticsearch 支持 int8 标量量化以减小向量大小,同时保持性能。其他方法会降低检索质量,不适合实际使用。在 Elasticsearch 8.16 和 Lucene 中,我们引入了更好的二进制量化 (Better Binary Quantization, BBQ),这是一种新方法,基于新加坡南洋理工大学研究人员提出的“RaBitQ”技术的见解开发。
点火三周
2024/11/18
3240
突破性进展:在 Elasticsearch 和 Lucene 中应用更好的二进制量化 (BBQ) 实现高效向量搜索
京东ES支持ZSTD压缩算法上线了:高性能,低成本
导读 京东ES支持ZSTD压缩算法上线了,这是一种高性能、低成本的压缩算法,能够提高数据存储和传输的效率,同时降低存储和带宽成本。ZSTD算法是一种快速压缩算法,可提供比其他压缩算法更高的压缩比和更快的压缩速度。这意味着,京东ES用户可以更高效地存储和传输数据,同时节省存储和带宽成本。此外,ZSTD算法还具有更好的可扩展性和鲁棒性,可满足大规模分布式系统的需求。因此,京东ES支持ZSTD压缩算法上线,将为用户带来更高的性能、更低的成本和更好的体验。
京东技术
2024/03/18
3630
京东ES支持ZSTD压缩算法上线了:高性能,低成本
解读向量索引
向量嵌入是从图像、文本和音频等数据源转换而来的数字表示,旨在通过为每个项目创建一个数学向量来捕捉其语义或特征。这种表示方式使得计算系统更容易理解这些数据,并且与机器学习模型兼容,从而能够识别不同项之间的关系和相似性。
半吊子全栈工匠
2024/11/07
5110
解读向量索引
RAG实战|向量数据库LanceDB指南
LanceDB是一个开源的用 Rust 实现的向量数据库(https://github.com/lancedb/lancedb),它的主要特点是:
用户1904552
2025/03/31
1700
RAG实战|向量数据库LanceDB指南
使用byte类型节省向量空间
Elasticsearch 在 8.6 中引入了一种新型向量!该向量具有 8 位整数维度,其中每个维度的范围为 -128, 127。这比当前具有 32 位浮点维度的向量小 4 倍,这可以节省大量空间。
点火三周
2023/07/04
1.5K0
使用byte类型节省向量空间
ES8 向量功能窥探系列(一):混合搜索功能初探与增强
Elasticsearch 8.x 引入了强大的向量搜索功能,使得在大规模数据集上进行高效的k近邻(kNN)搜索成为可能。向量搜索在许多应用场景中都非常重要,例如RAG、推荐系统、图像搜索等等。本文旨在深入浅出地剖析Elasticsearch 8.x的kNN搜索和混合搜索功能,介绍其实现原理和关键技术点。同时,我们还将解读腾讯云ES对社区做出的相关贡献,通过源码级别的解读,帮助读者更好地理解和应用Elasticsearch的向量搜索功能。
Rassyan
2024/07/02
2K0
ES8 向量功能窥探系列(一):混合搜索功能初探与增强
AI跑车引擎——向量数据库第二篇
在AI盛行的当下,Vector Search结合LLM的应用模式已经在应用领域逐渐成为主流,要想开好AI这辆跑车,那么首先需要有一款衬手的引擎,它就是向量数据库。这也是ChatGPT曝火后,很多向量数据库公司获得了数亿美元的融资的原因。
山行AI
2023/06/14
1.7K0
AI跑车引擎——向量数据库第二篇
Elasticsearch:普通检索和向量检索的异同?
《Elasticsearch 向量搜索的工程化实战》文章一经发出,收到很多留言。读者对向量检索和普通检索的区别充满了好奇,所以就有了今天的文章。
铭毅天下
2022/02/09
4.9K0
Lucene索引文件解析
Lucene作为最优秀的开源搜索引擎,内部实现了复杂的架构和算法,用来支撑对海量数据的存储和搜索。Lucene的存储和搜索都与底层的索引文件息息相关,Lucene发展过程中,也不断对索引文件格式进行优化和调整:
Yiwenwu
2024/05/25
4470
Lucene索引文件解析
Elasticsearch Relevance Engine---为AI变革提供高级搜索能力[ES向量搜索、常用配置参数、聚合功能等详解]
今天要介绍的 Elasticsearch Relevance Engine™ (ESRE™),提供了多项用于创建高度相关的 AI 搜索应用程序的新功能。ESRE 站在 Elastic 这个搜索领域的巨人肩膀之上,并基于两年多的 Machine Learning 研发成就构建而成。Elasticsearch Relevance Engine 将 AI 的最佳实践与 Elastic 的文本搜索进行了结合。ESRE 为开发人员提供了一整套成熟的检索算法,并能够与大型语言模型 (LLM) 集成。不仅如此,ESRE 还可通过已经得到 Elastic 社区信任的简单、统一的 API 访问,因此世界各地的开发人员都可以立即开始使用它来提升搜索相关性。
汀丶人工智能
2023/10/19
8020
Elasticsearch Relevance Engine---为AI变革提供高级搜索能力[ES向量搜索、常用配置参数、聚合功能等详解]
Elasticsearch 中的向量搜索:设计背后的基本原理
您有兴趣了解 Elasticsearch 向量搜索的特性以及设计是什么样子吗?一如既往,设计决策有利有弊。本博客旨在详细介绍我们在 Elasticsearch 中构建向量搜索时候如何做各种选择。
点火三周
2023/07/11
2.4K0
Elasticsearch 中的向量搜索:设计背后的基本原理
复合索引:向量搜索的高级策略
在向量搜索领域,我们拥有多种索引方法和向量处理技术,它们使我们能够在召回率、响应时间和内存使用之间做出权衡。虽然单独使用特定技术如倒排文件(IVF)、乘积量化(PQ)或分层导航小世界(HNSW)通常能够带来满意的结果,但为了实现最佳性能,我们往往采用复合索引。
用户3578099
2024/07/15
4980
复合索引:向量搜索的高级策略
【搜索引擎】Apache Solr 神经搜索
Sease[1] 与 Alessandro Benedetti(Apache Lucene/Solr PMC 成员和提交者)和 Elia Porciani(Sease 研发软件工程师)共同为开源社区贡献了 Apache Solr 中神经搜索的第一个里程碑。
架构师研究会
2022/06/08
1.1K0
【搜索引擎】Apache Solr 神经搜索
推荐阅读
相关推荐
Elasticsearch BBQ与OpenSearch FAISS:向量搜索性能对比
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档