分析型数据库PostgreSQL版的向量分析特性针对非结构化数据检索分析,具备丰富功能和优异性能,与普通的检索系统有较大的差异,主要体现在下面的几点:
  • 结构化和非结构化混合分析

    例如,可以检索与输入图片中的连衣裙似度最高、价格在100元到200元之间且上架时间在最近1个月以内的产品。

  • 支持数据实时更新

    传统的向量分析系统中数据只能按照T+1更新,不支持数据实时写入。分析型数据库PostgreSQL版向量分析支持数据实时更新和查询。

  • 支持向量分析碰撞

    分析型数据库PostgreSQL版向量分析支持KNN-Join,即比较一堆向量与另外一堆向量的相似度,类似于spark中的KNN-Join操作,这种场景计算量巨大,分析型数据库PostgreSQL版针对该场景做了大量优化。

    典型的应用场景有商品去重,计算新加入的商品与历史商品库中有哪些是相似的。人脸聚类,计算一段时间内的人脸库中,哪些人脸是同一个人。

  • 易用性

    分析型数据库PostgreSQL版向量分析申请即可使用,支持标准SQL,简化开发流程。同时,分析型数据库PostgreSQL版向量分析内置常用特征提取和属性提取,也支持集成第三方特征提取服务。

结构化数据和非结构化关联分析

以检索与输入图片中的连衣裙相似度最高、价格在100元到200元之间且上架时间在最近1个月以内的产品为例,有下列商品库,其中商品表products的字段设计如下:

字段 类型 说明
Id Char(64) 商品
Name Varchar(256) 商品名称
Price real 价格
InTime timestamp 入库时间
Url Varchar(256) 图片链接
Feature real(512) 图片特征

检索的SQL如下:

Select id, price from products where price > 100 and price <=200 and InTime > ‘2019-03-01 00:00:00’ and InTime <= ‘2019-03-31 00:00:00’ order by l2_distance(array[10,2.0,…, 512.0], feature) desc limit 100;

其中l2_distance为向量检索定义的距离函数,它包含下列2个参数:

参数 举例 说明
Feature1 array[10,2.0,…, 512.0] 需要查询的特征向量
Feature2 Feature 底库中的向量列

分析型数据库PostgreSQL版向量分析支持“欧氏距离“、“点积距离” 及 “汉明距离” 三种距离函数,分别适用于不同的情景。

非结构化数据写入

分析型数据库PostgreSQL版向量分析支持数据实时写入,立等可查。数据的写入方式与传统数据库一样,使用insert语句插入向量数据。

向量检索与分析

以上述的商品库为例,实现商品去重的逻辑,计算最近1天加入的商品与上个月的商品库里面有哪些是相似的,它的SQL如下:

select A.id as ida, B.id as idb from products A join products B on dp_distance(A.feature, B.feature) > 0.9 where A.inTime > '2019-03-31 00:00:00' and B.InTime >= '2019-02-01 00:00:00' and  B.InTime < '2019-02-29 00:00:00';

上述SQL把'2019-03-31 00:00:00'后写入的数据与2月份的数据做笛卡尔积,把向量点积距离大于0.9的商品对应的id提取出来。基于分析型数据库PostgreSQL版强大的优化器,用户无需写子查询,直接使用描述性的SQL即可。

易用性

向量分析完整继承了分析型数据库PostgreSQL版向量分析的所有商业工具和生态,并支持常用的特征提取模型,还支持与第三方特征提取服务集成。

  • 申请即可使用:开通分析型数据库PostgreSQL版服务即可使用向量分析。
  • 分析型数据库PostgreSQL版全面兼容PostgreSQL协议和SQL2003,降低开发成本。
  • 内置常用特征提取模块,支持集成第三方特征提取服务。

为了让您对非结构化数据拥有更多的自主控制权,您可以把非结构化数据保存在OSS或者图片服务器上(下图使用OSS),非结构化数据的保存地址即URL存储在分析型数据库PostgreSQL版中,整体架构如下所示。

图 1. 特征提取集成
  1. 通过分析型数据库PostgreSQL版控制台注册特征提取服务。
  2. 非结构化数据保存到OSS,同时返回访问的URL。
  3. 非结构化数据的存储地址即URL保存在分析型数据库PostgreSQL版中。
  4. 通过Web App调用分析型数据库PostgreSQL版的自定义函数生成向量特征,分析型数据库PostgreSQL版后台通过调用特征提取服务从OSS读取非结构化数据,提取特征,并把特征向量保存在分析型数据库PostgreSQL版中。所有这些操作只需要一条SQL便可轻松完成,SQL语句示例如下。
    
    select feature_extractor('clothes','https://xxx/1036684144_687583347.jpg');  
    
    insert into product(id, url, feature) values(0, 'http://xxx/1036684144_687583347.jpg', feature_extractor('clothes','https://xxx/1036684144_687583347.jpg')); 
    feature_extractor为商品特征提取的自定义函数,传入商品图片URL,提取商品特征向量,该向量可以用来做商品检索和属性提取。
    说明 feature_extractor第一个参数区别要提取的属性类型,仅支持FACE、CLOTHES、TEXT。

    对于常用的人脸特征提取、文本特征提取BERT模型以及服装特征提取也已经内置于分析型数据库PostgreSQL版服务中,您也可以使用您自己的特征提取服务。

向量索引的使用

当前向量检索系统提供两种检索方式:暴力扫描和hnsw索引检索,分别适用于不同的业务场景。

  • 暴力扫描:召回率100%,延迟与数据量成正比。当数据量很大的时候,查询检索的性能呈现较为明显的下降。
  • hnsw索引检索:经过参数配置之后,精度可以达到98%以上。当数据量很大的时候,查询检索的性能不会有较为明显的下降。

使用示例:

--暴力扫描 
SELECT id, price 
FROM products 
WHERE price > 100 AND price <=200 AND InTime > '2019-03-01 00:00:00' AND InTime <= '2019-03-31 00:00:00' 
ORDER BY l2_distance(ARRAY[10,2.0,…, 512.0], feature) DESC 
LIMIT 100;

--hnsw索引检索
SELECT id, price 
FROM products 
WHERE price > 100 AND price <=200 AND InTime > '2019-03-01 00:00:00' ANDInTime <= '2019-03-31 00:00:00' 
ORDER BY feature <-> ARRAY [10,2.0,…, 512.0]::REAL [] DESC 
LIMIT 100;

创建带向量列的表和索引

本节将通过具体示例,介绍如何在建表时创建向量列、创建向量索引。

说明 当前使用的相关语法,符合标准的SQL98语法。
  1. 创建用户表。

    语法:

    CREATE TABLE [TABLE_NAME]
    (  
        C1 DATATYPE,  
        C2 DATATYPE,  
        ......,  
        CN FLOAT2[],  
        PRIMARY KEY(一列或者多列)
    );
    说明 表名需在同一模式中的其它表、 序列、索引、视图或外部表名字中唯一。执行该操作将在当前数据库中创建一个新的空白表,该表将由执行此命令的用户所拥有。

    示例:

    在TEST空间下创建FACE_TABLE表,C2为向量列,C1为主键。NOT NULL表示字段不允许出现空值。

    CREATE TABLE FACE_TABLE (  
    C1 INT,  
    C2 REAL[]       NOT NULL,  
    C3 TIMESTAMP    NOT NULL,  
    C4 VARCHAR(20)  NOT NULL,  
    PRIMARY KEY (C1));
    说明 对于向量列,使用的是PostgreSQL中自带的数组类型来做向量列的存储。在本案例中,REAL[]在PostgreSQL中定义的是存储REAL类型的数组。
  2. 创建索引。

    索引是加速数据检索的一种特殊表查询,hnsw索引是一个指向表中数据的指针,通过对图中各个节点的遍历,找到与输入向量相似的向量。索引有助于加快向量检索中的SELECT查询和WHERE子句,但会降低使用UPDATEINSERT语句时的数据写入效率。

    语法:

    CREATE INDEX [INDEX_NAME]
    ON [TABLE_NAME]   
    USING ANN(COLUMN_NAME) 
    WITH (DIM=$DIMENSION);
    说明
    • INDEX_NAME:索引名。
    • SCHEMA_NAME:索引表空间名。
    • TABLE_NAME:表名。
    • COLUMN_NAME:向量索引列名。
    • DIMENSION:特征向量⻓度。要求不低于64,且不超过8192。该参数主要用于向量插入时候的检测,当维度不匹配的时候,系统将提示相关错误信息。

    示例:

    CREATE INDEX C2_IDX 
    ON TEST.FACE_TABLE
    USING ANN(C2) 
    WITH(DIM=1024);

插入数据

当前向量检索系统使用INSERT INTO语句用于向表中插入新记录,可以插入一行也可以同时插入多行。段列必须和数据值数量相同,且顺序对应。对于向量列来说,value值输入的是一个向量数组。

语法:

INSERT INTO [TABLE_NAME] (C1, C2, C3,...,CN)
VALUES (V1, V2, V3,...,VN);
说明
  • TABLE_NAME:表名。
  • C1, C2, C3,...,CN:表中的字段名。
  • V1, V2, V3,...,VN:字段对应的值。

示例:

执⾏下述命令向FACE_TABLE表中写⼊数据,对向量列输入的是一个REAL[]的数组。

INSERT INTO FACE_TABLE 
VALUES(1, ARRAY[1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8]::REAL[], current_timestamp, 'person1');

INSERT INTO FACE_TABLE 
VALUES(2, ARRAY[2,3,4,5,6,7,8,9,2,3,4,5,6,7,8,9,2,3,4,5,6,7,8,9,2,3,4,5,6,7,8,9,2,3,4,5,6,7,8,9,2,3,4,5,6,7,8,9,2,3,4,5,6,7,8,9,2,3,4,5,6,7,8,9]::REAL[], current_timestamp, 'person2');

INSERT INTO FACE_TABLE 
VALUES(3, ARRAY[0,1,2,3,4,5,6,7,0,1,2,3,4,5,6,7,0,1,2,3,4,5,6,7,0,1,2,3,4,5,6,7,0,1,2,3,4,5,6,7,0,1,2,3,4,5,6,7,0,1,2,3,4,5,6,7,0,1,2,3,4,5,6,7]::REAL[], current_timestamp, 'person3');

查询数据

向量检索系统利用SELECT语句用于从数据库中查询数据。结果被存储在一个结果表中,称为结果集。

语法:

SELECT C1, C2, ..., CN
FROM [TALBE_NAME]
ORDER BY C2 <-> ARRAY[DATA]::REAL[] 
LIMIT 100;
说明
  • C1, C2,...CN:表中字段名。其中,C2为向量列。
  • TALBE_NAME:表名。
  • ARRAY[DATA]::REAL[]:要查询的向量。

示例:

执⾏下述命令从FACE_TABLE表中找到与输入的向量列距离最相近的前100条向量。

SELECT C1, C2
FROM FACE_TABLE
ORDER BY C2 <-> ARRAY[0,1,2,3,4,5,6,7,0,1,2,3,4,5,6,7,0,1,2,3,4,5,6,7,0,1,2,3,4,5,6,7,0,1,2,3,4,5,6,7,0,1,2,3,4,5,6,7,0,1,2,3,4,5,6,7,0,1,2,3,4,5,6,7]::REAL[] 
LIMIT 100;

float2类型的使用

本节将通过具体示例,为您介绍半浮点数压缩数据列的定义和相关的操作。当前向量检索系统中,会将图片、声音、文本转化成高维浮点数数组进行存储,将占用大量的存储空间。为降低存储成本,压缩存储空间,为您提供了float2压缩存储模式。

说明 由于系统需要进行float2和float4之间的转换,所以对数据库的读写性能方面会有一定的影响。

半精度浮点数(float2)是一种被计算机使用的二进制浮点数据类型。半精度浮点数使用2个字节(16位)来存储,来存储之前4个字节(32位)的float4的数据。IEEE 754标准指定了一个binary16需具备如下的格式:

  • Sign bit(符号位): 1 bit。
  • Exponent width(指数位宽): 5 bits。
  • Significand precision(尾数精度): 11 bits (有10位被显式存储)。
按如下顺序排列:float2数据类型除非指数位全是0,否则就会假定隐藏的起始位是1。因此只有10位尾数在内存中被显示出来,而总精度是11位。根据IEEE 754标准,虽然尾数只有10位,但是尾数精度是11位的(log10(211)≈ 3.311 十进制数)。
0 01111 0000000000 = 1
0 01111 0000000001 = 1 + 2−10 = 1.0009765625 (1之后的最接近的数)1 
10000 0000000000 = −2 

0 11110 1111111111 = 65504  (max half precision) 

0 00001 0000000000 = 2−14 ≈ 6.10352 × 10−5 (最小正指数)
0 00000 1111111111 = 2−14 - 2−24 ≈ 6.09756 × 10−5 (最大尾数)
0 00000 0000000001 = 2−24 ≈ 5.96046 × 10−8 (最小正尾数) 

0 00000 0000000000 = 0
1 00000 0000000000 = −0 

0 11111 0000000000 = infinity
1 11111 0000000000 = −infinity 

0 01101 0101010101 = 0.333251953125 ≈ 1/3
由于尾数的位数是奇数,所以默认情况下,类似1/3的数会像双精度浮点数一样四舍五入。
对于float2和float4之间的转换,除了不同部分的移位之外,还需要注意指数的基数之间的差别(15和127)。例如,要把float2类型转换为float4类型,主要进行以下几步操作。
  1. 符号位左移16位。
  2. 指数部分加112(127与15之间的差距),左移13位(右对齐)。
  3. 尾数部分左移13位(左对齐)。
说明 Float4转换为float2的步骤与之相反。

因此当前的浮点数的压缩是损失精度的压缩,所以在进行查询计算的时候会有一定的精度的损失。在实际应用中,这种损失是满足业务的要求的。对于精度的损失。

float2压缩存储是用两个字节,来表示之前的四个字节的存储,所以对于向量列的压缩比例在0.5,即占用磁盘空间是原来的50%。

Float2类型只能表达[-65519.99, 65519.99]之间的值。如果超过取值范围,比方说大于65519,系统会输出Infinity,如果小于-65519,系统会输出-Infinity。对于向量检索来说,向量需要进行归一化处理,将取值范围归一化到[0,1]之间。不进行归一化的向量距离计算,会非常容易超过取值范围,导致距离计算的不准确。

对于向量float2与float4类型之间的相互转化,会有一定的性能上的消耗。当前float2的数组类型转换,实现了两种转换算法:
  • 针对数组中的每个float2的数据,使用C程序进行转化,每次只转换一个float2数据。
  • 对于特定的硬件(支持AVX和SSE2指令集的硬件),调用硬件特定的接口函数,每次可以支持同时转换4个float2类型。
在实际的查询的过程中,因为会用到索引等相关的遍历技术,所以不用转换很多记录。

创建使用float2数据类型的表

float2是内部定义的一个数据类型,系统实现了各种类型的转换,以及相关的各种操作符。因此,在实际系统中,一般将float2数据类型当成基本数据类型来进行相关的操作。

语法:

CREATE TABLE [TABLE_NAME]
(  
    C1 INT,  
    C2 FLOAT2[],  
    C3 VARCHAR(20),  
    PRIMARY KEY(C1)
);
说明 C2即float2向量存储列。

示例:

在FACE_TABLE表中,创建float2的向量列C2。
CREATE TABLE FACE_TABLE (  
    C1 INT PRIMARY KEY,  
    C2 FLOAT2[],  
    C3 VARCHAR(20)
);

插入数据

对已经建立好的float2类型的数组,插入相关的数据。可以用下述三种方式对float2的数组插入数据。在进行数据插入的时候,用户可以显示的定义出float2的数组,将相关的数据插入到表中(参见下述代码中的sql1);或者用户采用隐示的类型转换,系统会在内部将float4类型的数组,转换成float2类型的数组,存储到对应的表中(参见下述代码中的sql2和sql3)。

示例:

插入三条数据到创建的FACE_TABLE中。
sql1 = INSERT INTO FACE_TABLE (C1, C2, C3)
    VALUES (1, ARRAY[1.3, 2.4, 5.6]::FLOAT2[], 'name1'); 

sql2 = INSERT INTO FACE_TABLE (c1, c2, c3) 
    VALUES (2, ARRAY [3.4, 6.1, 7.6]::REAL[], 'name2'); 

sql3 = INSERT INTO FACE_TABLE (c1, c2, c3) 
    VALUES (3, ARRAY [9.5, 1.2, 0.6]::FLOAT4[],'name3');

查询数据

由于采用的是float2类型的数据,所以在显示查询结果时有一定的数据精度丢失。例如插入的是1.3,而实际查询的结果是1.2998;或者插入的是5.6,而实际查询的结果是5.60156。这种精度的损失对于向量检索来说,是可以忽略不计的。

示例:

SELECT * FROM FACE_TABLE; 
c1  |            c2             |  c3   
----+---------------------------+-------
  1 | {1.2998,2.40039,5.60156}  | name1
  2 | {3.40039,6.10156,7.60156} | name2
  3 | {9.5,1.2002,0.600098}     | name3

float2表数据的压缩比例

本示例中,建立两张表,一个是用float4类型的向量数据,一个是float2类型的向量数据,对比实际表的大小。

--CREATE TABLE 
CREATE TABLE TAB1(A FLOAT4[]);
CREATE TABLE TAB2(A FLOAT2[]); 

--INSERT DATA 
INSERT INTO TAB1 
SELECT GEN_RAND_F2_ARR (1, 1024) FROM GENERATE_SERIES (1,10000);
INSERT INTO TAB2 
SELECT GEN_RAND_F2_ARR (1, 1024) FROM GENERATE_SERIES (1,10000); 

--QUERY SIZE
SELECT PG_SIZE_PRETTY (PG_RELATION_SIZE('tab1'));
 PG_SIZE_PRETTY 
----------------
 45 MB(1 row)
 
SELECT PG_SIZE_PRETTY (PG_RELATION_SIZE('tab2')); 
 PG_SIZE_PRETTY
----------------
 21 MB(1 row)

从上述信息可查看到,使用float4数据类型的存储是45M,使用float2类型的数据存储是21M。由此可见,float2的存储大约是float4的一半。

float2表数据的压缩和解压的性能比较

当前系统提供了两个函数来进行float2与float4相互的转换:array_f16_to_f32 将float2类型的向量转化成float4类型的向量,array_f32_to_f16 将float4类型的向量转化成float2的向量。当前每个向量的长度是1024维,是在支持AVX和SSE2的指令集的机器上面进行测试的。

示例:

--CREATE TABLE 
CREATE TABLE TAB1(A FLOAT4[]);
CREATE TABLE TAB2(A FLOAT2[]); 

--INSERT TABLE
INSERT INTO TAB1 SELECT GEN_RAND_F2_ARR(1, 1024) FROM GENERATE_SERIES (1,10000);
INSERT INTO TAB2 SELECT GEN_RAND_F2_ARR(1, 1024) FROM GENERATE_SERIES (1,10000); 

\TIMING
--query size
SELECT ARRAY_F32_TO_F16(a) FROM TAB1;  
    Time: 5998.832 ms (00:05.999)
SELECT ARRAY_F16_TO_F32(a) FROM TAB2;
    Time: 5507.388 ms (00:05.507) 

距离计算

为了方便距离计算,当前的系统针对float2[]类型,提供了L2距离计算,系统在内部会将float2类型的数据,隐示的转成float4类型的数据,来计算相关的距离。

示例:

计算l2距离。

SELECT L2_DISTANCE(ARRAY[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0]::FLOAT2[], 、
    ARRAY[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0]::FLOAT2[]); 

SELECT L2_DISTANCE(ARRAY[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0]:: FLOAT4[], 
    ARRAY [0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0]:: FLOAT2[]); 

SELECT L2_DISTANCE (ARRAY[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0]:: FLOAT2[], 
    ARRAY [0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0]::FLOAT2[]);

float2的实际应用案例

对于安保系统来说,每天都会定时的将监控的图片数据存在人脸表中,公安系统会输入人脸的照片,在监控系统中查找相关的监控图片。下文将介绍float2在查询检索的应用。

  1. 创建一个表,用于存放人脸识别的相关数据。
    CREATE TABLE FACE_TABLE (
      C1 INT PRIMARY KEY,
      C2 FLOAT2[],
      C3 VARCHAR(20)
    );
    说明
    • C1:人脸的编号。
    • C2:人脸的向量。
    • C3:对应的人名。
  2. 在FACE_TABLE表中建立向量索引。
    CREATE INDEX FACE_TABLE_IDX 
    ON FACE_TABLE 
    USING ANN(C2) WITH(dim=10);
  3. 导入相关的监控数据到FACE_TABLE表中。
    INSERT INTO FACE_TABLE (C1, C2, C3)  
    VALUES (1, ARRAY[1.3, 2.4, 5.6]::FLOAT2[], 'name1'); 
    
    INSERT INTO FACE_TABLE (c1, c2, c3) 
    VALUES (2, ARRAY[3.4, 6.1, 7.6]::REAL[], 'name2'); 
    
    INSERT INTO FACE_TABLE (c1, c2, c3) 
    VALUES (3, ARRAY[9.5, 1.2, 0.6]::FLOAT4[],'name3');
  4. 输入人脸的数据,进行向量查询。
    SELECT * 
    FROM FACE_TABLE 
    ORDER BY C1 <-> ARRAY[2.81574,9.84361,8.07218]:: FLOAT2[] 
    LIMIT 10;
    说明 ARRAY[2.81574,9.84361,8.07218]:: FLOAT2[]表示需要查询的图片向量,系统会在底库中检索对应的人脸信息。