本文为您介绍在Hologres中如何使用向量计算功能。
背景信息
Proxima是一款来自于阿里达摩院的实现向量近邻搜索的高性能软件库,相比于Faiss等开源的同类产品,Proxima在稳定性、性能等方面都要更为出色,能够提供业内性能和效果领先的基础方法模块,支持图像、视频、人脸等各种应用场景。Hologres向量查询功能与Proxima深度整合,提供高性能的向量查询服务。
Proxima简介
名词解释
特征向量:向量是一种将实体和应用代数化的一种表示,其将实体间的关系抽象成向量空间中的距离,而距离的远近代表着相似程度。例如:身高、年龄、性别、地域。
向量检索:在特征向量数据集合中进行快速搜索和匹配的方法,常涉及到的问题有KNN和RNN。
KNN(K-Nearest Neighbor):查找离查询点最近的 K 个点。
RNN(Radius Nearest Neighbor):查找离查询点某半径范围内的所有点。
Proxima的基本模型
分为索引构建和在线检索两部分:
索引构建:索引构建从原始向量数据中构建出相关索引文件,并传予在线检索模块加载使用。支持Brute Force、KD-Tree、Product Quantization、KNN Graph、LSH 等。
在线检索:在线检索加载完索引文件后,为向量检索提供查询服务。在聚类后的数据集上进行KNN和RNN搜索,用户设定检索过程中的参数。
Proxima与Hologres概念对照
Proxima概念
Hologres中的概念
特征向量
数组类型Array,仅支持固定长度数组
向量索引
一种特殊类型的Index,当前仅支持KNN/RNN的Graph索引
距离计算
一种类型的UDF:
proxima_distance()
每种距离计算对应一个UDF
KNN查询
order by distance(x, [x1, x2]) asc limit k
RNN查询
where distance(x, [x1,x2]) < r
说明当前RNN查询不支持Proxima索引。
使用Proxima进行向量计算
Hologres中使用Proxima进行向量计算的操作步骤如下:
连接Hologres。
通过开发工具连接Hologres,详情请参见连接开发工具。如果是JDBC连接,请使用Prepare Statement模式。
安装Proxima插件。
Proxima是以一个extension的方式与Hologres连通,因此在使用之前需要Superuser执行如下命令安装Proxima插件。
--安装extension create extension proxima;
Proxima插件是针对数据库级别使用的,一个数据库仅需要安装一次即可。
说明如需卸载extension请执行如下命令。
DROP EXTENSION proxima;
创建向量表。
向量在Hologres中一般用FLOAT4数组表示,创建向量表的语法如下。
说明仅列存表支持向量索引。
begin; create table feature_tb ( id bigint, <feature_col> float4[] check(array_ndims(<feature_col>) = <value> and array_length(feature_col, 1) = <value>) --定义向量 ); -- 构建向量索引 call set_table_property('feature_tb', 'proxima_vectors', '{"<feature_col>": {"algorithm":"<value>","distance_method":"<value>"}}'); commit;
参数说明如下。
分类
参数
描述
示例
向量基本属性
feature_col
向量列名称。
feature。
array_ndims
向量的维度。
构建一维且长度为4的向量示例如下。
feature float4[] check(array_ndims(feature) = 1 and array_length(feature, 1) = 4)
array_length
向量的长度。
向量索引
proxima_vectors
代表构建向量索引,其中:
algorithm:用于指定构建向量索引的算法,目前仅支持
Graph
。distance_method:用于定义构建向量索引使用的距离计算方法,目前支持三种距离计算函数:
(推荐使用)SquaredEuclidean:平方欧式距离, 查询效率最高。适合查询时使用
pm_approx_squared_euclidean_distance
。Euclidean:开方的欧式距离,仅适合查询时使用
pm_approx_euclidean_distance
,如果使用其他距离函数会利用不上索引。InnerProduct(避免使用):内积距离,会在底层转换为开方的欧式距离的计算,所以构建索引和查询索引都会多一层计算开销,比较低效,尽量避免使用,除非业务有强需求。仅适合查询时使用
pm_approx_inner_product_distance
。
min_flush_proxima_row_count:用于控制开始构建向量索引的数量,默认为20000,一般场景无需修改。
min_compaction_proxima_row_count:控制compaction时多少条数据会构建向量索引,默认值为20000,一般场景无需修改。
max_total_size_to_merge_mb:控制compaction后的文件大小上限,默认为256 MB,一般场景无需修改。
proxima_builder_thread_count:控制写入时build向量索引的线程数,默认值为4,一般场景无需修改。
说明索引需要在一定的场景下使用才能发挥更好的作用。
--使用平方欧式距离查询,构建对应的向量索引示例如下。
call set_table_property('feature_tb', 'proxima_vectors', '{"feature":{"algorithm":"Graph", "distance_method":"SquaredEuclidean", "builder_params":{"min_flush_proxima_row_count" : 20000} }}');
向量导入。
可以通过离线或者实时的方式将数据导入至向量表,可以根据业务需求选择合适的同步方式。但需要注意的是,在批量导入后,执行VACUUM和Analyze命令以提升查询效率。
VACUUM会让后端的文件compaction成更大的文件,对查询更高效。 但是VACUUM需要耗费一定的CPU资源,表的数据量越大,执行VACUUM的时间越久,当VACUMM还在执行中时,请耐心等待执行结果。
VACUUM <tablename>;
Analyze是收集统计信息, 用于优化器QO(Query Optimizer)生成较优的执行计划,提高查询性能。
analyze <tablename>;
向量查询。
Hologres支持精确和近似向量查询,其中以
pm_
开头的UDF都为精准查询,以pm_approx_
开头的UDF都为近似查询。重要对于构建向量索引的场景,只能使用
pm_approx_
开头的UDF近似查询才能命中向量索引。使用如下的方式查询Top N,近似查询中的第二个参数必须是常量值。
-- 计算内积的TOPK,此时建表里面的proxima_vector参数的distance_method需要为SquaredEuclidean select pm_approx_squared_euclidean_distance(feature, '{0.1,0.2,0.3,0.4}') as distance from feature_tb order by distance asc limit 10 ; -- 计算内积的TOPK,此时建表里面的proxima_vector参数的distance_method需要为Euclidean select pm_approx_euclidean_distance(feature, '{0.1,0.2,0.3,0.4}') as distance from feature_tb order by distance asc limit 10 ; -- 计算内积的TOPK,此时建表里面的proxima_vector参数的distance_method需要为InnerProduct select pm_approx_inner_product_distance(feature, '{0.1,0.2,0.3,0.4}') as distance from feature_tb order by distance desc limit 10 ;
向量计算UDF列表
Hologres当前版本支持的向量计算UDF如下。
精准查询
float4 pm_squared_euclidean_distance(float4[], float4[]) float4 pm_euclidean_distance(float4[], float4[]) float4 pm_inner_product_distance(float4[], float4[])
近似查询
float4 pm_approx_squared_euclidean_distance(float4[], float4[]) float4 pm_approx_euclidean_distance(float4[], float4[]) float4 pm_approx_inner_product_distance(float4[], float4[])
完整使用示例
使用Proxima进行向量计算的示例语句如下。
create extension proxima;
begin;
create table feature_tb (
id bigint,
feature float4[] check(array_ndims(feature) = 1 and array_length(feature, 1) = 4)
);
call set_table_property('feature_tb', 'proxima_vectors', '{"feature":{"algorithm":"Graph","distance_method":"SquaredEuclidean","builder_params":
{"min_flush_proxima_row_count" : 20000}}}');
end;
insert into feature_tb select i, array[random(), random(), random(), random()]::float4[] from generate_series(1, 10000) i;
analyze feature_tb;
select pm_approx_squared_euclidean_distance(feature, '{0.1,0.2,0.3,0.4}') as distance from feature_tb order by distance desc limit 10 ;
性能调优
设置向量索引的场景
数据量较小,比如几万条数据情况下,建议不设置索引直接计算即可。 或者实例资源较多的情况下但查询的数据量较少,也可以直接计算。 当直接计算满足不了需求(如延迟、吞吐等要求)时,可以考虑使用Proxima索引, 原因如下。
Proxima本身是有损索引,不保证结果的准确性,即计算出来的距离可能是有偏差的。
Proxima索引有可能导致召回的条数不足,如
limit 1000
情况下,只返回了500条。Proxima索引使用有一定难度。
设置合适的Shard Count
Shard Count越多, 实际构建Proxima索引的文件就越多, 查询吞吐就越差。所以在实际使用中,根据实例资源建议设置合理的Shard Count,最极致的场景是设置Shard Count为1,查询性能最好。但是Shard Count数越少,写入性能将会越差,在实际业务中建议综合评估。
经验证明Proxima查询使用的计算资源与Shard Count成正比,但是如果没有设置Proxima索引,即为暴力计算,不建议修改Shard Count。
--创建向量表,并放于shard count为1的table group中 begin; create table proxima_test ( group_id text not null, user_id text not null, model_version text not null, feature float4[] check(array_ndims(feature) = 1 and array_length(feature, 1) = 4), primary key(group_id,user_id,model_version)); call set_table_property('proxima_test', 'proxima_vectors', '{"feature":{"algorithm":"Graph","distance_method":"SquaredEuclidean"}}'); call set_table_property('proxima_test', 'shard_count', '1'); end;
(推荐)不带过滤条件的查询场景
在有
where
过滤条件的情况下,会影响索引使用,性能可能更差,所以推荐不带过滤条件的查询场景。对于不带过滤条件的向量检索,最极致的状态就是一个Shard上只有一个向量索引文件,这样查询就能直接落在一个Shard上。因此对于不带过滤条件的查询场景通常建表如下。
begin; CREATE TABLE feature_tb( uuid text, feature FLOAT4[] not null CHECK (array_ndims(feature)=1 and array_length(feature,1)= N)--定义向量 ) ; call set_table_property('feature_tb', 'shard_count', '?');--指定shard count,根据业务情况合理设置,若有可以不设置 call set_table_property('feature_tb', 'proxima_vectors', '{"feature":{"algorithm":"Graph","distance_method":"InnerProduct"}');--构建向量索引 end;
带过滤条件的查询场景
对于带过滤条件的向量检索,情况细分为如下常见的过滤场景。
查询场景1:字符串列为过滤条件
示例查询如下,常见的场景为在某个组织内查找对应的向量数据,例如查找班级内的人脸数据。
select pm_xx_distance(feature, '{1,2,3,4}') as d from feature_tb where uuid = 'x' order by d limit 10;
建议进行如下优化。
将uuid设置为Distribution Key,这样相同的过滤数据会保存在同一个Shard,查询时一次查询只会落到一个Shard上。
将uuid设置为表的Clustering Key,数据将会在文件内根据Clustering Key排序。
查询场景2:时间字段为过滤条件
示例查询如下,一般是根据时间字段过滤出对应的向量数据。建议将时间字段time_field设置为表的segment_key,可以快速的定位到数据所在的文件。
select pm_xx_distance(feature, '{1,2,3,4}') as d from feature_tb where time_field between '2020-08-30 00:00:00' and '2020-08-30 12:00:00' order by d limit 10;
因此对于带过滤条件的向量检索而言,其建表语句通常如下。
begin; CREATE TABLE feature_tb( time_field timestamptz not null, uuid text, feature FLOAT4[] not null CHECK (array_ndims(feature)=1 and array_length(feature,1)= N) ) ; call set_table_property('feature_tb', 'distribution_key', 'uuid'); call set_table_property('feature_tb', 'segment_key', 'time_field'); call set_table_property('feature_tb', 'clustering_key', 'uuid'); call set_table_property('feature_tb', 'proxima_vectors', '{"feature":{"algorithm":"Graph","distance_method":"InnerProduct"}}'); end; -- 如果没有按照时间过滤的话,则time_field相关的索引可以删除。
常见问题
报错
ERROR: function pm_approx_inner_product_distance(real[], unknown) does not exist
。原因:通常是因为未在数据库中执行
create extension proxima;
语句来初始化Proxima插件。解决方法:执行
create extension proxima;
语句初始化Proxima插件。报错
Writing column: feature with array size: 5 violates fixed size list (4) constraint declared in schema
。原因:由于写入到特征向量列的数据维度与表中定义的维度数不一致,导致出现该报错。
解决方法:可以排查下是否有脏数据。
报错
The size of two array must be the same in DistanceFunction, size of left array: 4, size of right array:
。原因:由于
pm_xx_distance(left, right)
中,left的维度与right的维度不一致所致。调整
pm_xx_distance(left, right)
中,left的维度与right的维度一致。实时写入报错
BackPresure Exceed Reject Limit ctxId: XXXXXXXX, tableId: YY, shardId: ZZ
。原因:实时写入作业遇到了瓶颈,产生了反压的异常,说明写入作业开销大,写入慢,通常是由于min_flush_proxima_row_count较小,而实时写入速度较大,造成写入作业实时构建索引开销大,阻塞了实时写入。
调整min_flush_proxima_row_count到更大值。
如何通过Java写入向量?
通过Java写入向量的示例如下。
private static void insertIntoVector(Connection conn) throws Exception { try (PreparedStatement stmt = conn.prepareStatement("insert into feature_tb values(?,?);")) { for (int i = 0; i < 100; ++i) { stmt.setInt(1, i); Float[] featureVector = {0.1f,0.2f,0.3f,0.4f}; Array array = conn.createArrayOf("FLOAT4", featureVector); stmt.setArray(2, array); stmt.execute(); } } }
如何通过执行计划检查是否利用上Proxima索引?
如果执行计划中存在
Proxima filter: xxxx
表明使用了索引,如下图所示;如果没有,则索引没有使用上,一般是建表语句与查询语句不匹配。
距离函数说明
Hologres支持的三种向量距离评估函数如下:
不开方的欧式距离,计算公式如下。
开方的欧氏距离,计算公式如下。
内积距离,计算公式如下。
如果您选用欧式距离进行向量计算,不开方的欧式距离与开方的欧式距离相比,可以少一个开方的计算,并且计算出的Top K记录一致。因此,不开方的欧式距离性能更好,在满足功能需求的情况下,一般建议您使用不开方的欧式距离。