DolphinDB for AI:高性能向量数据库使用指南

原创
2024/07/15 16:04
阅读数 130

在一些现代应用场景中,例如搜索引擎(如 Elasticsearch)和 AI 生成模型 ,主要工作负载来源于对海量数据的高效检索和快速响应。这些应用场景要求系统能够在庞大的数据集中,以低延迟来完成高精度的相似度搜索和推荐任务。这类任务通常涉及到对向量数据的存储和查询(查询任务是指在给定查询向量的情况下,检索数据库中与该查询向量相似度最高的 K 个向量)。

DolphinDB 从 2.0 版本开始就支持存储向量数据类型,也就是 Array Vector。但为了满足对海量数据进行相似度匹配的需求,以及未来在检索增强生成(RAG)中的应用,还需要提供对海量向量数据进行近似检索(即向量检索)的能力。基于这个想法,DolphinDB 在 V3.00.1中推出了向量数据库(VectorDB),并以 TSDB 做为底层存储引擎对向量检索提供了支持

VectorDB 具备以下 3 个特点:

  • 向量近似性检索:通过添加索引,支持高效的向量相似度查询,显著提高向量检索速度和精度。
  • 索引持久化支持:向量索引与其他二级索引(如 ZoneMap)一起持久化至磁盘,系统重启后只需要从磁盘读取向量索引,而无需重新构建索引。
  • 混合搜索(Hybrid Search)支持:混合搜索结合了结构化的检索(如 SQL 查询语句中的 where 条件)和向量检索。混合搜索可以在搜索过程中同时利用向量数据的其他属性来提供更加准确和相关的搜索结果。例如在电商搜索中,用户可以根据一些特定条件(如品牌、颜色)和上传图片结合的方式搜索产品。混合搜索适用于综合多个属性进行查询的场景。

1. 向量数据库创建

在创建向量数据库表之前,我们需要先调用 database 函数(函数的详细说明可以参考:database)创建一个底层存储引擎为 TSDB 的分布式数据库。以下给出示例:

dbName = "dfs://test"
db = database(dbName, HASH, [INT, 3], engine=`TSDB)

在上面的示例中,我们创建了一个库名为 test 且分区方式为 HASH 分区的分布式数据库并获取该数据库的句柄。

1.1 向量数据库表的生成

在 DolphinDB 中,可以通过调用 createPartitionedTable 函数或 create 语句来创建一个分布式表。在创建表的过程中,可以为向量字段设置索引。

通过 createPartitionedTable 函数创建分区表

接口详细说明可参考:createPartitionedTable

 createPartitionedTable 接口中的 indexes 参数,用于指定需要建立向量索引的列、向量索引的类型以及向量数据的维度,具体参数说明如下:

indexes={colName: "vectorindex(type={t}, dim={d})"}

// 示例
t = table(100:0, `id`devicedId`vectorColumn, [INT, INT, FLOAT[]])
createPartitionedTable(
  db, 
  t, 
  `pt,  
  partitionColumns=`id,
  sortColumns=`id`devicedId, 
  keepDuplicates=ALL,
  indexes={"vectorColumn": "vectorindex(type=flat, dim=128)"}
)
  • colName :要建立向量索引的向量数据列(类型为 Float 的 Array Vector)列名。
  • type:向量索引类型,目前仅支持 Flat, IVF, PQ, IVFPQ, HNSW(大小写不敏感)。
  • dim:正整数,表示向量的维度,分区表中的向量数据的维度必须同参数 dim 中指定的维度一致。需要注意当创建的索引类型为 PQ 或 IVFPQ 时, dim 必须为4的倍数。

通过 create 语句创建分区表

通过 SQL 方式创建分区表的语法如下:

create table dbPath.tableName (
    schema[columnDescription]
)
[partitioned by partitionColumns],
[sortColumns],
[keepDuplicates=ALL]

// 示例
create table "dfs://test"."pt"(
     id INT,
     deviceId SYMBOL,
     vectorColumn FLOAT[] [indexes="vectorindex(type=flat, dim=128)"]
 )
 partitioned by id,
 sortColumns=`id`deviceId,
 keepDuplicates=ALL

在通过 SQL 创建分区表时,可以在 columnDescription 指定向量索引。在上面的示例中,我们创建了一张表名为pt的分区表,并为 vectorColumn 列建立向量索引,索引类型为 Flat,向量维度为128。

1.2 向量检索和查询

搜索表 table 中符合 where 条件(若有)且距离查询向量 queryVector 最相近的 TOPK 条数据, 可以通过下面 的 SQL 模板来完成。

SELECT {col1,...,coln} 
FROM table [where] 
ORDER BY rowEuclidean(<VECTOR_COLUMN_NAME>, queryVector) 
LIMIT <TOPK>

字段含义:

  • rowEuclidean:计算向量距离的函数。在向量检索中通过向量之间的 L2 距离来衡量两个向量的相似度,L2 距离越近则向量越相似。该函数有两个参数:
    • <VECTOR_COLUMN_NAME> :表的向量数据列。
    • queryVector:给定的查询向量。
  • limit<TOPK>:指定检索与查询向量最相似的 TOPK 条数据。
  • where:可选子句,若查询语句提供了 where 条件,将会搜索与给定的查询向量相近并且符合 where 条件的 TOPK 条数据。

1.3 相关限制

目前要使用向量索引对向量检索进行加速需要满足一定的限制条件,对于不满足这些限制条件的情况,在做向量检索时会对向量数据进行穷举搜索。本节中将对这些限制条件进行说明。

建表限制:

  • 目前仅支持 TSDB 分区表,且需要设置为非去重表,即 keepDuplicates=ALL。
  • 建立向量索引列的数据类型只能为 FLOAT[]。
  • 现在最多只能指定一个列为向量索引列。
  • 插入向量索引列的数据维度必须与创建索引时指定的维度(dim)一致。

搜索限制:

只有在满足下列约束的情况下查询才会使用向量索引加速检索,否则只会取出所有 Level File 上的数据穷举搜索

  • 查询语句中 order by 的数据仅包含 rowEuclidean(<vectorCol>, queryVec) ,且仅支持升序排序。
  • 已为 rowEuclidean 第一个参数指定的列创建过向量索引。
  • 若指定 where 条件, where 条件中不包含sortColumns 中的任意列。
  • 查询语句不包含 group by, having 等其他子句。
  • 不能使用表连接查询。

其他说明:

向量索引仅在 Level File 刷写至磁盘时才会建立,因此内存缓冲区中的向量数据没有索引,无法利用向量索引加速检索。如果在进行向量检索时内存缓冲区中存在数据,则需要对内存中的向量数据进行穷举搜索,并将搜索结果与从 Level File 中搜索的结果进行合并。因此,为了确保向量检索的效率,建议在向量检索前,若内存缓冲区中存在数据,可以通过 flushTSDBCache 强制将内存缓存区中的数据写入磁盘,然后再进行查询。

2. 原理介绍

DolphinDB 底层采用 Meta 开源的向量检索库 Faiss 为向量索引的构建以及搜索查询提供支持。当且仅当在建分区表时指定了需要建立向量索引的表列(该列的类型需为 FLOAT[] )时,TSDB 才会为指定列构建、存储向量索引

2.1 向量索引构建

TSDB 的数据存储在磁盘中的 Level File 文件中,向量索引以 Level File 为单位进行构建,即每个 Level File 都有其对应的向量索引。每当有新的 Level File 生成(即后台刷盘,compaction 合并数据)时,后台线程会为Level File 中的向量数据根据指定的索引类型建立向量索引。目前支持的向量索引类型有 Flat, PQ, IVF, IVFPQ, HNSW,以下对这些索引类型做简要介绍:

  • Flat

Flat 提供精确的向量最近邻检索。其在构建索引时无需进行训练,直接对向量进行穷举搜索,在查询时会计算查询向量与系统中存储的所有向量的相似度。

  • PQ(Product Quantization)

使用 PQ 构建向量索引会对向量数据进行训练。训练过程中会将一个向量划分为数个子向量,并对每个子向量进行乘积量化(Product Quantization),从而压缩每个向量的存储空间。

  • IVF(Inverted File Index)

使用IVF构建向量索引会对向量数据进行训练。训练过程中会采用聚类算法(k-means)将向量数据划分为多个簇(cluster),然后为每个簇建立倒排索引,倒排索引记录着各个簇中包含了哪些向量数据。在查询时IVF会先计算出与查询向量距离较近的簇,然后对这些簇中的向量进行相似度计算,从而降低计算量,提高搜索效率。

  • IVFPQ(Inverted File Index with Product Quantization)

IVFPQ 是 IVF 和 PQ 的结合,其同样需要在构建向量索引时对向量数据进行训练,训练过程中将向量数据划分为多个簇,并对每个簇的向量数据通过 PQ 的方式进行乘积量化,使用 IVFPQ 索引能够大幅减少存储空间和计算时间。

  • HNSW(Hierarchical Navigable Small World)

HNSW 基于图算法为向量数据构建多层次的导航图结构,实现高精度和高速度的近似最近领搜索。在查询过程中 HNSW 会根据查询向量从导航图的顶层开始查找最近的向量,然后在更低的层次中进一步搜索,逐渐逼近目标向量。每一层的搜索都是局部的,从而大幅提高了搜索效率。

2.2 向量数据的检索

向量检索主要分为普通向量检索和混合检索两种方式。本节将介绍普通向量检索和混合检索的概念,并讲解 VectorDB 针对这两种检索的具体实现方法。

普通向量检索

普通向量检索即在查询语句中没有指定 where 语句进行条件过滤,查询时将会计算查询向量与所有存储向量数据之间的相似度,以此来找到与查询向量最相似的数据。

普通向量检索的流程如下:

  1. 首先,遍历磁盘上所有 Level File,读取每个 Level File 中存储的向量索引。通过索引搜索得到 N * K 个初
    步查询结果。
  2. 其次,对内存缓冲区中的数据(若有)进行穷举搜索,得到共 (N + 1) * K 个查询结果。
  3. 最后,将这 (N +1) * K 个音询结果根据与査询向量的 L2 距离从小到大进行归并排序,取出并返回与音
    询向量最相近的 K 条数据。

混合检索

混合检索即在査询语句中指定 where 语句进行条件过滤,查询时将根据给定的过滤条件,获取与查询向量相似且满足过滤条件的数据。

混合检索的流程如下:

  1. 根据 where 条件对数据进行初步过滤。
  2. 对过滤后的数据按照 Level File 进行划分,确保同一 Level File 中的数据被划分在一起。
  3. 对每个 Level File 中的数据使用其对应的索引进行相似性搜索。
  4. 合并各个 Level File 的搜索结果,并从中选出与查询向量最相近的 K 条数据作为最终的返回结果。

3. 使用示例

下面示例将展示如何创建带有向量索引的 TSDB 分区表,以及如何使用查询语句进行普通向量检索(查询中不包含 where 条件)和混合搜索(查询中包含 where 条件),并给出了搜索结果。示例中的数据文件(vectord128num10000.csv)已于文末给出,文件中包含了10000条数据,并且向量数据的维度为128。

3.1 建立向量索引

下面的脚本创建了带有向量索引的 TSDB 分区表,并将数据文件中的数据导入表中。

login(`admin, `123456);
drop table if exists "dfs://vectorcsv"."test_table"; 
drop table if exists t;

// 数据文件路径
sift1m = "/PATH/TO/vectord128num10000.csv";

schema = extractTextSchema(sift1m);
// 将vectorCol列转换为FLOAT[]类型
update schema set type = "FLOAT[]" where name = 'vectorCol'; 

dbName = "dfs://vectorcsv";
// 创建数据库,存储引擎为TSDB
db = database(directory=dbName, partitionType=RANGE, partitionScheme= 0 10 20 ,engine="TSDB");
// 读取数据文件
t = loadText(sift1m, schema = schema, arrayDelimiter="|");

// 创建分区表, KeepDuplicates为ALL。并指定列vectorCol建立向量索引,其中索引类型为flat,向量的维度为128
test_table = createPartitionedTable(db, t, `test_table, `cola, , `cola, ALL,
indexes={"vectorCol": "vectorindex(type=flat, dim=128)"});

// 往分区表写入数据,并强制刷盘建立索引
test_table.append!(t);
pnodeRun(flushTSDBCache);

3.2 普通向量检索(查询语句不包含 where 条件)

下面的脚本展示了如何在查询表 test_table 中搜索 vectorCol 中与查询向量距离最相近的10条数据,并给出了查询结果。

// 查询语句
queryVec = [1.0,3.0,11.0,110.0,62.0,22.0,4.0,0.0,43.0,21.0,22.0,18.0,6.0,28.0,64.0,
    9.0,11.0,1.0,0.0,0.0,1.0,40.0,101.0,21.0,20.0,2.0,4.0,2.0,2.0,9.0,18.0,
    35.0,1.0,1.0,7.0,25.0,108.0,116.0,63.0,2.0,0.0,0.0,11.0,74.0,40.0,101.0,
    116.0,3.0,33.0,1.0,1.0,11.0,14.0,18.0,116.0,116.0,68.0,12.0,5.0,4.0,2.0,
    2.0,9.0,102.0,17.0,3.0,10.0,18.0,8.0,15.0,67.0,63.0,15.0,0.0,14.0,116.0,
    80.0,0.0,2.0,22.0,96.0,37.0,28.0,88.0,43.0,1.0,4.0,18.0,116.0,51.0,5.0,
    11.0,32.0,14.0,8.0,23.0,44.0,17.0,12.0,9.0,0.0,0.0,19.0,37.0,85.0,18.0,
    16.0,104.0,22.0,6.0,2.0,26.0,12.0,58.0,67.0,82.0,25.0,12.0,2.0,2.0,25.0,
    18.0,8.0,2.0,19.0,42.0,48.0,11.0]
select * from test_table 
order by rowEuclidean(vectorCol, queryVec) 
limit 10
// 查询结果
cola colb  vectorCol                                                                          
---- ----- -----------------------------------------------------------------------------------
3    36538 [0,0,0,0,0,23,33,11,0,0,0,0,5,56,127,1,0,0,0,0,5,25,111,19,5,1,0,0,0,0...]         
3    36267 [0,0,0,0,0,21,23,6,0,0,0,0,3,65,81,0,0,0,0,0,6,46,113,10,6,1,0,0,0,2...]           
5    2176  [0,1,3,0,5,86,60,0,0,0,3,0,2,103,118,9,0,0,0,3,8,42,118,54,2,0,0,1,5,34...]        
5    3752  [1,5,3,20,36,4,2,2,12,41,10,1,1,8,69,23,5,12,12,20,16,9,78,39,50,0,0,10,20,10...]  
8    68299 [0,0,0,1,19,48,8,0,0,0,1,4,5,82,50,0,17,0,0,1,0,24,67,61,16,0,0,5,23,7...]         
3    49874 [0,0,0,0,4,48,26,0,0,0,0,0,0,99,107,0,0,0,0,0,2,48,93,25,0,1,13,30,81,34...]       
8    882   [29,32,21,1,1,10,4,6,19,13,8,1,0,62,77,5,10,0,0,0,0,34,125,72,5,0,0,0,3,40...]     
1    87578 [8,4,24,34,27,10,0,4,14,1,3,13,28,42,100,29,2,0,0,6,10,55,107,26,2,2,11,15,9,26...]
2    4009  [0,1,6,13,57,65,33,7,5,9,8,3,21,101,115,17,21,12,6,4,3,18,86,49,8,6,10,8,7,6...]   
7    2837  [17,31,49,0,0,6,17,25,24,27,22,0,0,41,77,44,12,8,9,0,0,30,101,82,14,1,8,5,8,16...]   

3.3 混合搜索(查询语句中包含 where 条件)

下面的脚本展示了如何在查询表 test_table 中搜索 colb 小于 5000 且 vectorCol 中与查询向量距离最相近的 10 条数据,并给出了查询结果。

// 查询语句
queryVec = [1.0,3.0,11.0,110.0,62.0,22.0,4.0,0.0,43.0,21.0,22.0,18.0,6.0,28.0,64.0,
    9.0,11.0,1.0,0.0,0.0,1.0,40.0,101.0,21.0,20.0,2.0,4.0,2.0,2.0,9.0,18.0,
    35.0,1.0,1.0,7.0,25.0,108.0,116.0,63.0,2.0,0.0,0.0,11.0,74.0,40.0,101.0,
    116.0,3.0,33.0,1.0,1.0,11.0,14.0,18.0,116.0,116.0,68.0,12.0,5.0,4.0,2.0,
    2.0,9.0,102.0,17.0,3.0,10.0,18.0,8.0,15.0,67.0,63.0,15.0,0.0,14.0,116.0,
    80.0,0.0,2.0,22.0,96.0,37.0,28.0,88.0,43.0,1.0,4.0,18.0,116.0,51.0,5.0,
    11.0,32.0,14.0,8.0,23.0,44.0,17.0,12.0,9.0,0.0,0.0,19.0,37.0,85.0,18.0,
    16.0,104.0,22.0,6.0,2.0,26.0,12.0,58.0,67.0,82.0,25.0,12.0,2.0,2.0,25.0,
    18.0,8.0,2.0,19.0,42.0,48.0,11.0]
select * from test_table 
where colb < 5000 
order by rowEuclidean(vectorCol, queryVec) 
limit 10
// 查询结果
cola colb vectorCol                                                                            
---- ---- -------------------------------------------------------------------------------------
5    2176 [0,1,3,0,5,86,60,0,0,0,3,0,2,103,118,9,0,0,0,3,8,42,118,54,2,0,0,1,5,34...]          
5    3752 [1,5,3,20,36,4,2,2,12,41,10,1,1,8,69,23,5,12,12,20,16,9,78,39,50,0,0,10,20,10...]    
8    882  [29,32,21,1,1,10,4,6,19,13,8,1,0,62,77,5,10,0,0,0,0,34,125,72,5,0,0,0,3,40...]       
2    4009 [0,1,6,13,57,65,33,7,5,9,8,3,21,101,115,17,21,12,6,4,3,18,86,49,8,6,10,8,7,6...]     
7    2837 [17,31,49,0,0,6,17,25,24,27,22,0,0,41,77,44,12,8,9,0,0,30,101,82,14,1,8,5,8,16...]   
2    190  [31,27,11,0,0,6,4,6,16,15,10,0,1,60,66,6,11,0,0,0,0,37,127,66,6,0,0,1,5,41...]       
4    3615 [58,18,0,8,86,27,7,9,81,16,0,0,0,104,108,43,10,0,0,0,4,46,101,50,28,1,0,0,3,6...]    
5    816  [16,21,4,18,27,13,15,12,8,12,2,11,15,58,85,14,22,21,6,1,4,14,115,49,5,16,10,1,0,1...]
3    1045 [0,1,23,4,0,10,14,0,0,1,10,8,3,83,38,0,20,1,0,0,0,61,101,25,75,20,0,0,4,4...]        
1    1884 [3,0,9,24,22,23,12,10,25,0,0,1,6,26,50,80,18,0,0,6,41,27,60,50,5,7,6,6,23,23...]   

4. 索引选用建议

本节将总结分析不同索引类型(Flat, PQ, IVF, IVFPQ, HNSW)之间的优劣,并给出它们的适用场景,以便更好地帮助用户决策在不同的场景中选取什么类型的索引。

索引类型 索引构建复杂度 检索速度 检索精度
Flat 由于是穷举搜索,因此不需要复杂的索引构建过程。 穷举搜索,计算量大,检索速度慢。 穷举搜索,检索精度最高。
PQ 在构建索引时需要训练乘积量化器,内存的占用与除 Flat 以外的其他索引相比更小。 通过对向量数据进行乘积量化,能够对数据进行更快速的近似搜索,检索速度较 Flat 更快。 乘积量化可能会导致信息损失,检索精度最低。
IVF 构建索引时需要对向量数据进行聚类训练,并构建倒排索引,索引构建复杂度相对较高。 通过聚类将数据划分为多个簇,查询时只需搜索少量簇,检索速度较 PQ更快。 对向量数据做聚类划分,仅对少量簇进行穷举搜索,检索精度较 Flat 低。
IVFPQ 构建索引时需要对向量数据进行聚类训练,同时训练乘积量化器,因此索引的构建和维护要高于单独使用 IVF 或 PQ。 结合了 IVF 和 PQ,在保证较高的搜索精度的同时进一步提升了查询速度,检索速度比单独使用 IVF 或 PQ更快。 通过簇分割减少搜索空间,再通过量化进一步加速搜索,检索精度比单独使用 PQ 更高,但与单独使用 IVF 相比较低。
HNSW 构建索引时不需要进行训练,但是需要在构建索引时动态地构建和维护多层次导航图。构建多层次的图结构复杂度高耗时长。相比其他索引,HNSW 对内存的占用最高。 基于图结构,通过分层的近邻搜索实现快速的近似最近邻搜索,在超大规模数据量(十亿级别)的情况下检索速度最快。 HNSW 接近于精确最近邻搜索,检索精度与 IVF 相近。

上面表格展示了各索引类型的构建复杂度、检索速度以及检索精度三方面的情况。

注1:由于向量索引构建的时机是在后台 Level File 持久化至磁盘时,因此后台持久化任务开始执行时索引构建复杂度会在很大程度上影响系统性能(耗时、内存以及 CPU 资源的占用)。特别是对于对内存占用较高的索引(如 HNSW),在后台持久化任务执行时可能会产生高额的内存占用。

根据表格中各索引类型的对比,以下给出各索引类型适用的场景:

  • Flat:适用于数据规模在数百至数万级别的向量数据,或需要最高精度的场景。
  • PQ:适用于数据规模在数十万至数千万级别的向量数据,且对搜索精度要求不高的场景。常见应用场景如大型数据库、视频库等。
  • IVF:适用于数据规模在数万至数百万级别的向量数据,常见应用场景如图片检索、文本检索等。
  • IVFPQ:适用于数据规模在数百万至数千万级别的向量数据,需要在检索速度和精度之间找到最佳平衡的场景。常见应用场景如大型推荐系统、社交网络中的用户匹配。
  • HNSW:适用于数据规模在数亿至数十亿级别的向量数据,并对检索速度,精度和动态更新有高要求的场景,如实时推荐系统、在线搜索、RAG等。

注2:若要达到 HNSW 预期的检索效率,要求建立 HNSW 索引的数据量至少为上亿级别。而目前分区表在绝大多数应用场景下各个分区的数据量不会太多(通常在百万至千万级别),且向量索引是以一个 Level File 的数据为单位建立,因此如果选用 HNSW 作为索引很难达到该索引算法预期的效果。因此绝大部分场景下,更推荐使用 IVF 或 IVFPQ,它们对内存的占用不高,检索速度更快同时还有着不俗的检索精确度(检索召回率约在90%左右)。

5. 小结

DolphinDB 通过引入向量检索技术,结合其强大的海量数据存储和处理能力,能够在更多领域发挥优势。这包括改进推荐系统,实现高效图像和视频搜索,提升 AI 生成模型的准确性以及多样性等。向量检索技术的引入不仅拓宽了 DolphinDB 的应用范围,也提升了 DolphinDB 对海量向量数据的处理和分析效率。

附录

示例数据文件

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
0 评论
0 收藏
0
分享
返回顶部
顶部