当结构化数据与非结构化数据需要同时检索时,您可以使用AnalyticDB PostgreSQL版向量数据库的混合查询,既支持结构化字段过滤,也支持半结构化字段过滤,同时支持和文本字段的全文检索一起进行双路召回。
混合检索简介
ANNS(Approximate Nearest Neighbors Search,近似最近邻搜索)向量索引只能解决非结构化数据检索的问题。但是实际生产环境中,经常会遇到一些结构化数据与非结构化数据需要同时检索的场景。例如需要查询特定人员在指定时间范围内是否出现在特定区域内,这里的结构化条件就是时间范围和区域范围,非结构化条件就是人脸。因此阿里云在AnalyticDB PostgreSQL版中加入了结构化数据与非结构化数据混合查询的能力。
业界在解决混合查询的问题时,一般采用两个系统,结构化数据存在数据库中,非结构化数据存在向量检索系统中;然后对两个系统求交集后再聚合得到最终的结果。这种方法一般先采用向量检索系统取出经过放大的amp(放大系数) * topk个结果,然后再通过结构化索引过滤得到最终结果。但该方法因为结构化索引过滤结果比较多,导致召回出现比较大的损失,从而无法满足最终需要topk个结果的要求。下面以结构化字段和非结构化字段的混合查询来讲解混合查询的实现原理。
混合检索实现原理
AnalyticDB PostgreSQL版将向量检索引擎FastANN作为索引插件集成到数据库中,可以在使用向量检索的同时使用结构化和半结构化索引。具体是通过优化器的RBO(Ranked-Biased Overlap)代价估算规则,生成不同的执行计划来解决结构化数据与非结构化数据需要同时检索的问题。
下文通过一个例子来说明混合检索的原理。
假设有一个带条件的拍图查找商品需求:查找与输入图片相似度最高,价格在100到200元之间,并且上架时间在最近一个月以内的前100件商品。
表和索引结构如下:
CREATE TABLE products (
id serial primary key,
name varchar(256),
price real,
inTime timestamp,
url varchar(256),
feature real[]
);
-- 设置向量列为内联模式。
ALTER TABLE products ALTER COLUMN feature SET STORAGE PLAIN;
-- 为结构化字段创建BTREE索引。
CREATE INDEX ON products(price, intime);
-- 为向量字段创建向量索引。
CREATE INDEX ON products USING ann(feature) WITH (dim=512);
-- 收集统计信息,用于生成最优的混合查询执行计划。
ANALYZE products;
采用混合查询的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
feature <-> array[10,2.0,…, 512.0]
LIMIT 100;
为了执行这条SQL,依据结构化条件列(price以及inTime)上是否有索引,以及结构化条件的选择率大小不同,AnalyticDB PostgreSQL版可能会生成如下3类不同的执行计划,并在其中选择最优的一个执行计划,以满足用户对性能和召回的要求:
第一类:暴力查询:
根据结构化条件获取所有符合条件的行,再在其中根据向量距离进行排序,选择距离最小的100条数据进行输出。这种执行计划召回为100%,但是查询速度最慢,在底库过大或符合结构化条件行数过多时查询性能低下。执行计划如下所示(简化了部分输出信息):
QUERY PLAN ----------------------------------------------------------------------------------- LIMIT -> Gather Motion 3:1 (slice1; segments: 3) Merge Key: (l2_squared_distance($0, feature)) -> LIMIT -> Sort Sort Key: (l2_squared_distance($0, feature)) -> Index Scan using products_price_idx on products Index Cond: 价格过滤条件 Filter: 时间过滤条件 Optimizer: Postgres query optimizer
第二类:纯向量查询 + 结构化过滤:
为了加速查询,先使用向量索引查询出与输入图片最接近的N行数据,再在其中根据结构化条件进行过滤。这种执行计划的优点是查询速度最快,缺点是如果结构化查询的筛选率太小(即一行数据通过过滤的概率太小),则查询最终输出的数据行数可能比用户要求的行数少。执行计划如下所示(简化了部分输出信息):
QUERY PLAN ----------------------------------------------------------------------------------- LIMIT -> Gather Motion 3:1 (slice1; segments: 3) Merge Key: ((feature <-> $0)) -> LIMIT -> Ann Index Scan using products_feature_idx on products ORDER BY: (feature <-> $0) Filter: 价格和时间列过滤条件 Optimizer: Postgres query optimizer
第三类:向量结构化混合查询:
向量结构化混合查询结合了第一类和第二类执行计划,既能使用索引,又能解决第二类执行计划返回数据变少的问题。同时,如果结构化条件列上有索引,且索引类型支持Bitmap(左子树)生成,则混合查询还可以利用其他索引来生成Bitmap,从而进一步加速混合查询。执行计划如下所示(简化了部分输出信息):
QUERY PLAN ----------------------------------------------------------------------------------- LIMIT -> Gather Motion 3:1 (slice1; segments: 3) Merge Key: ((feature <-> $0)) -> LIMIT -> Fusion Ann Scan -> Bitmap Index Scan on products_price_idx Index Cond: 价格过滤条件 -> Ann Index Scan with filter using products_feature_idx on products ORDER BY: (feature <-> $0) Filter: 时间过滤条件 Optimizer: Postgres query optimizer
上述执行计划中包含了Fusion Ann Scan节点或者Ann Index Scan with filter节点的执行计划都是使用了混合查询能力。这两种执行计划说明如下:
Ann Index Scan with filter:此节点的作用是将过滤条件下压到向量索引内部,在索引的执行过程中同时考虑过滤条件。
Fusion Ann Scan:此节点在某些结构化条件列上有索引时可能出现,其作用是根据结构化条件生成Bitmap(左子树),将Bitmap下压到向量索引(右子树)中,加速结构化条件的计算。出现Fusion Ann Scan节点时,其右子树可能是Ann Index Scan也可能是Ann Index Scan with filter。如果右子树是Ann Index Scan,说明除了Bitmap外没有其他结构化条件下压。反之会出现Ann Index Scan with filter。
混合检索使用方法
AnalyticDB PostgreSQL版向量数据库混合查询既支持结构化字段过滤,也支持半结构化字段过滤,同时也支持和文本字段的全文检索一起进行双路召回。混合查询具体可以划分为三类:
向量查询和结构化字段过滤组成的混合查询。当前支持的结构化字段类型包括:bigint、boolean、bytea、char、varchar、integer、float、double、date、smallint、timestamp和serial等所有结构化字段,这些结构化字段可以通过创建默认的BTREE索引在混合查询中进行加速。结构化字段类型的详细信息请参见数据类型。
向量查询和半结构化字段过滤组成的混合查询。当前支持的半结构化类型包括:JSON、JSONB和Array等所有半结构化字段,这些结构化字段可以通过创建GIN索引在混合查询中进行加速。半结构化类型的详细信息请参见JSON & JSONB 数据类型和Array数组类型。
向量查询和全文检索组成的双路召回。全文检索的详细内容请参见如何使用AnalyticDB PostgreSQL 版实现“一站式全文检索”业务。
在混合检索实现原理中,已经介绍了混合查询中对结构化字段类型的加速优化方式,下面举例说明半结构化字段在混合检索中的使用,以及向量检索与全文检索进行双路召回的使用。
半结构化字段在混合检索中的使用
假设某证券公司有一个股票分析文章的文本库stock_analysis_chunks,这个文本库主要包括以下字段:
字段 | 类型 | 说明 |
id | serial | 编号。 |
chunk | varchar(1024) | 股票分析文章切块后的文本块。 |
release_time | timestamp | 股票分析文章的发布时间。 |
stock_id_list | char(10)[] | 该股票分析文章涉及的公司股票ID列表。 |
url | varchar(1024) | 文本块所属文章的链接。 |
feature | real[] | 文本块的embedding向量。 |
创建文本库表stock_analysis_chunks。
CREATE TABLE stock_analysis_chunks ( id serial primary key, chunk varchar(1024), release_time timestamp, stock_id_list integer[], url varchar(1024), feature real[] ) distributed by (id);
将向量列设置为内联模式。
ALTER TABLE stock_analysis_chunks ALTER COLUMN feature SET STORAGE PLAIN;
对向量列建立向量索引。
CREATE INDEX ON stock_analysis_chunks USING ann(feature) WITH (dim=1536, distancemeasure=cosine, hnsw_m=64, pq_enable=1);
对混合查询关联的结构化与半结构化列建立索引。
-- 对结构化字段建立BTREE索引 CREATE INDEX ON stock_analysis_chunks(release_time); -- 对半结构字段建立GIN索引 CREATE INDEX ON stock_analysis_chunks USING gin(stock_id_list);
收集统计信息。
ANALYZE stock_analysis_chunks;
进行混合查询。
例如需要查找最近一个月包含某文本内容,并且涉及特定公司的股票分析文章,可以采用如下SQL进行混合查询:
SELECT id,url, cosine_similarity(feature, array[1.0, 2.0, ..., 1536.0]::real[]) as score FROM stock_analysis_chunks WHERE release_time >= '2023-07-18 00:00:00' AND release_time <= '2023-08-18 00:00:00' AND stock_id_list @> ARRAY['BABA', 'AAPL', 'MSFT', 'AMZN'] ORDER BY feature <=> array[1.0, 2.0, ..., 1536.0]::real[] LIMIT 10;
向量检索与全文检索的双路召回
双路召回是指同时使用向量检索和全文检索来进行数据召回。在大部分场景下,使用向量检索能力就可以在相似度召回场景中获得不错的召回率。但是也有某些场景,比如embedding模型不佳,或者由于查询复杂导致生成的向量与库内需要召回的数据距离较远时,仅仅使用向量相似召回无法达到预期的效果。这时为了提高召回率,可以选择使用双路召回来丰富召回的策略。双路召回一般会通过向量检索和全文检索分别召回部分数据,然后再做精排蒸馏,后处理等以获得更佳的召回效果。AnalyticDB PostgreSQL版既支持高性能向量检索,又支持高性能全文检索,因此您可以在AnalyticDB PostgreSQL版中完成向量检索与全文检索的双路召回。
AnalyticDB PostgreSQL版的双路召回是通过在一个查询里联合两个单路检索(即向量检索和全文检索)来实现双路召回。因此您可以仅通过一次查询就实现同时召回双路的结果。例如:
创建示例表。
-- vector字段为向量,to_tsvector为全文分词 CREATE TABLE IF NOT EXISTS documents( id TEXT, docname TEXT, title TEXT, vector real[], text TEXT, to_tsvector TSVECTOR);
将向量列设置为内联模式。
ALTER TABLE documents ALTER COLUMN vector SET STORAGE PLAIN;
为向量字段创建向量索引。
CREATE INDEX ON documents USING ann(vector) WITH (dim=1024, distancemeasure=cosine, hnsw_m=64, pq_enable=1);
为全文分词字段创建GIN索引。
CREATE INDEX ON documents USING gin(to_tsvector);
创建依赖函数。
CREATE OR REPLACE FUNCTION public.to_tsquery_from_text(txt text, lang regconfig DEFAULT 'english'::regconfig) RETURNS tsquery LANGUAGE sql IMMUTABLE STRICT AS $function$ SELECT to_tsquery(lang, COALESCE(string_agg(split_part(word, ':', 1), ' | '), '')) FROM (SELECT unnest(string_to_array(to_tsvector(lang, txt)::text, ' ')) AS word) AS words_only;$function$
进行双路召回查询。
WITH combined AS ( ( SELECT id, docname, text, cosine_similarity(densevec,array{embedding}::real[]) AS similarity, 1 as source FROM documents ORDER BY vector <=> array[10,2.0,…, 1024.0] LIMIT 10 ) UNION ALL ( SELECT id, docname, text, ts_rank(to_tsvector, to_tsquery_from_text('{query.query}', 'zh_cn'), 32) as similarity, 2 as source FROM documents WHERE to_tsvector@@to_tsquery_from_text('{query.query}', 'zh_cn') ORDER BY similarity desc LIMIT 10 ) ) SELECT id, docname, title, text, MAX(similarity) as similarity, BIT_OR(source) as source FROM combined GROUP BY id ORDER BY similarity DESC;
UNION
的第一个部分通过向量召回10条,第二个部分则通过全文检索召回10条;然后再根据id
进行去重操作。上述双路召回的SQL会返回小于等于20条数据的结果集。