跳到主要内容

向量搜索

在生成式 AI 的应用中,单纯依赖大模型自身的参数“记忆”存在明显局限:一方面,模型知识具有时效性,无法覆盖最新信息;另一方面,完全依赖模型直接“生成”容易产生幻觉(Hallucination)。因此,RAG(检索增强生成)应运而生。其核心目标不是让模型凭空构造答案,而是从外部知识库中检索出与用户查询最相关的 Top-K 信息片段,作为生成依据。为实现这一点,需要一种机制衡量“用户查询”与“知识库文档”之间的语义相关性。向量表示正是常用手段:将查询与文档统一编码为语义向量后,可通过向量相似度衡量相关程度。随着预训练模型的发展,生成高质量语义向量已成主流,RAG 的检索阶段也演化为一个标准的向量相似度搜索问题——从大规模向量集合中找出与查询最相似的 K 个向量(候选知识片段)。需要注意,RAG 的向量检索不限于文本,也可扩展到多模态:图片、语音、视频等数据同样可以编码为向量供生成模型使用。例如,用户上传图片后,系统先检索相关描述或知识片段,再辅助生成解释性内容;在医学问答中,可检索病例资料与医学文献,生成更准确的诊断建议。

暴力搜索

Apache Doris 自 2.0 版本起支持基于向量距离的最近邻搜索,通过 SQL 实现向量搜索是一个自然且简单的过程。

SELECT id,
l2_distance(embedding, [1.0, 2.0, xxx, 10.0]) AS distance
FROM vector_table
ORDER BY distance
LIMIT 10;

当数据量不大(小于 100 万行)时,Apache Doris 的精确最近邻(K-Nearest Neighbor)搜索性能足以满足需求,可获得 100% 召回与 100% 精确。但随着数据进一步增长,用户通常愿意牺牲少量召回与精度以换取显著的查询加速,此时问题就转化为向量近似最近邻搜索(Approximate Nearest Neighbor,ANN)。

近似最近邻搜索

Apache Doris 自 4.0 版本开始正式支持 ANN 搜索。系统未引入额外数据类型,向量仍以定长数组存储;针对向量距离检索,我们基于 Faiss 实现了新的 ANN 索引类型。 以下以常见的 SIFT 数据集为例,建表示例如下:

CREATE TABLE sift_1M (
id int NOT NULL,
embedding array<float> NOT NULL COMMENT "",
INDEX ann_index (embedding) USING ANN PROPERTIES(
"index_type"="hnsw",
"metric_type"="l2_distance",
"dim"="128",
"quantizer"="flat"
)
) ENGINE=OLAP
DUPLICATE KEY(id) COMMENT "OLAP"
DISTRIBUTED BY HASH(id) BUCKETS 1
PROPERTIES (
"replication_num" = "1"
);
  • index_type: hnsw 表示使用 Hierarchical Navigable Small World 算法
  • metric: l2_distance 表示使用 L2 距离作为距离函数
  • dim: 128 表示向量维度为 128
  • quantizer: flat 表示按原始 float32 存储各维度
参数是否必填支持/可选值默认值说明
index_type仅支持:hnsw(无)指定所使用的 ANN 索引算法。当前只支持 HNSW。
metric_typel2_distanceinner_product(无)指定向量相似度/距离度量方式。L2 为欧氏距离,inner_product 可用于余弦相似时需先归一化向量。
dim正整数 (> 0)(无)指定向量维度,后续导入的所有向量的维度必须与此一致,否则报错。
max_degree正整数32HNSW 图中单个节点的最大邻居数(M),影响索引内存与搜索性能。
ef_construction正整数40HNSW 构建阶段的候选队列大小(efConstruction),越大构图质量越好但构建更慢。
quantizerflatsq8sq4flat指定向量编码/量化方式:flat 为原始存储,sq8/sq4 为对称量化(8/4 bit)以降低内存占用。

通过 S3 TVF 导入数据:

INSERT INTO sift_1M
SELECT *
FROM S3("uri" =
"https://selectdb-customers-tools-bj.oss-cn-beijing.aliyuncs.com/sift_database.tsv", "format" = "csv");

select count(*) from sift_1M
--------------

+----------+
| count(*) |
+----------+
| 1000000 |
+----------+

SIFT 数据集同时发布了一组 ground truth,用于校验结果。下面选取一组向量,先使用精确距离函数进行 TopN 召回:

SELECT id, l2_distance(embedding,     [0,11,77,24,3,0,0,0,28,70,125,8,0,0,0,0,44,35,50,45,9,0,0,0,4,0,4,56,18,0,3,9,16,17,59,10,10,8,57,57,100,105,125,41,1,0,6,92,8,14,73,125,29,7,0,5,0,0,8,124,66,6,3,1,63,5,0,1,49,32,17,35,125,21,0,3,2,12,6,109,21,0,0,35,74,125,14,23,0,0,6,50,25,70,64,7,59,18,7,16,22,5,0,1,125,23,1,0,7,30,14,32,4,0,2,2,59,125,19,4,0,0,2,1,6,53,33,2]) as distance FROM sift_1M ORDER BY distance limit 10
--------------

+--------+----------+
| id | distance |
+--------+----------+
| 178811 | 210.1595 |
| 177646 | 217.0161 |
| 181997 | 218.5406 |
| 181605 | 219.2989 |
| 821938 | 221.7228 |
| 807785 | 226.7135 |
| 716433 | 227.3148 |
| 358802 | 230.7314 |
| 803100 | 230.9112 |
| 866737 | 231.6441 |
+--------+----------+
10 rows in set (0.29 sec)

当使用 l2_distanceinner_product 时,Doris 需要计算查询向量与 1,000,000 个候选向量之间的距离,再通过 TopN 算子得到全局结果。使用 l2_distance_approximate / inner_product_approximate 可触发索引执行路径:

SELECT id, l2_distance_approximate(embedding,     [0,11,77,24,3,0,0,0,28,70,125,8,0,0,0,0,44,35,50,45,9,0,0,0,4,0,4,56,18,0,3,9,16,17,59,10,10,8,57,57,100,105,125,41,1,0,6,92,8,14,73,125,29,7,0,5,0,0,8,124,66,6,3,1,63,5,0,1,49,32,17,35,125,21,0,3,2,12,6,109,21,0,0,35,74,125,14,23,0,0,6,50,25,70,64,7,59,18,7,16,22,5,0,1,125,23,1,0,7,30,14,32,4,0,2,2,59,125,19,4,0,0,2,1,6,53,33,2]) as distance FROM sift_1M ORDER BY distance limit 10
--------------

+--------+----------+
| id | distance |
+--------+----------+
| 178811 | 210.1595 |
| 177646 | 217.0161 |
| 181997 | 218.5406 |
| 181605 | 219.2989 |
| 821938 | 221.7228 |
| 807785 | 226.7135 |
| 716433 | 227.3148 |
| 358802 | 230.7314 |
| 803100 | 230.9112 |
| 866737 | 231.6441 |
+--------+----------+
10 rows in set (0.02 sec)

可以看到使用 ANN 索引后,查询耗时从约 290 ms 降至约 20 ms。 Doris 中,ANN 索引建立在 segment 粒度;由于表是分布式的,各 segment 返回局部 TopN 后,TopN 算子会将多个 tablet 的结果归并生成全局 TopN。

需要注意:当 l2_distance 作为索引 metric 时,distance 越小表示越接近;inner_product 则相反,值越大越接近。因此若使用 inner_product,必须 ORDER BY dist DESC 才能通过索引获得 TopN。

近似范围搜索

除了常见的 TopN 最近邻搜索(即返回与目标向量最近的前 N 条记录)之外,向量检索中还有一类常见的查询方式是 基于距离阈值的范围搜索。 这类查询不返回固定数量,而是找出所有与目标向量距离满足条件的数据点。例如:查找距离大于或小于某阈值的向量。范围搜索在需要“足够相似”或“足够不相似”候选集的场景中很有用:推荐系统中可获取“接近但不完全相同”内容以增加多样性;异常检测中可定位远离正常模式的数据点。 一个典型的 SQL 为:

SELECT  count(*) FROM sift_1M  WHERE l2_distance_approximate(embedding, [0,11,77,24,3,0,0,0,28,70,125,8,0,0,0,0,44,35,50,45,9,0,0,0,4,0,4,56,18,0,3,9,16,17,59,10,10,8,57,57,100,105,125,41,1,0,6,92,8,14,73,125,29,7,0,5,0,0,8,124,66,6,3,1,63,5,0,1,49,32,17,35,125,21,0,3,2,12,6,109,21,0,0,35,74,125,14,23,0,0,6,50,25,70,64,7,59,18,7,16,22,5,0,1,125,23,1,0,7,30,14,32,4,0,2,2,59,125,19,4,0,0,2,1,6,53,33,2]) > 300
--------------

+----------+
| count(*) |
+----------+
| 999271 |
+----------+
1 row in set (0.19 sec)

在 Doris 中,这类基于范围的向量搜索同样通过 ANN 索引 来加速执行。通过 ANN 索引,系统能够快速筛选出候选向量集合,然后再计算精确的近似距离,从而显著降低计算开销、提升查询效率。目前支持的范围查询条件包括 >, >=, <, <=

组合搜索

Compound Search 指在同一条 SQL 中同时进行 ANN TopN 与 Range 条件过滤,返回满足范围约束的 TopN。

SELECT id, l2_distance_approximate(embedding, [0,11,77,24,3,0,0,0,28,70,125,8,0,0,0,0,44,35,50,45,9,0,0,0,4,0,4,56,18,0,3,9,16,17,59,10,10,8,57,57,100,105,125,41,1,0,6,92,8,14,73,125,29,7,0,5,0,0,8,124,66,6,3,1,63,5,0,1,49,32,17,35,125,21,0,3,2,12,6,109,21,0,0,35,74,125,14,23,0,0,6,50,25,70,64,7,59,18,7,16,22,5,0,1,125,23,1,0,7,30,14,32,4,0,2,2,59,125,19,4,0,0,2,1,6,53,33,2]) as dist FROM sift_1M  WHERE l2_distance_approximate(embedding, [0,11,77,24,3,0,0,0,28,70,125,8,0,0,0,0,44,35,50,45,9,0,0,0,4,0,4,56,18,0,3,9,16,17,59,10,10,8,57,57,100,105,125,41,1,0,6,92,8,14,73,125,29,7,0,5,0,0,8,124,66,6,3,1,63,5,0,1,49,32,17,35,125,21,0,3,2,12,6,109,21,0,0,35,74,125,14,23,0,0,6,50,25,70,64,7,59,18,7,16,22,5,0,1,125,23,1,0,7,30,14,32,4,0,2,2,59,125,19,4,0,0,2,1,6,53,33,2]) > 300 ORDER BY dist limit 10
--------------

+--------+----------+
| id | dist |
+--------+----------+
| 243590 | 300.005 |
| 549298 | 300.0317 |
| 429685 | 300.0533 |
| 690172 | 300.0916 |
| 123410 | 300.1333 |
| 232540 | 300.1649 |
| 547696 | 300.2066 |
| 855437 | 300.2782 |
| 589017 | 300.3048 |
| 930696 | 300.3381 |
+--------+----------+
10 rows in set (0.12 sec)

对于 Compound Search,一个关键点是谓词过滤与 TopN 的执行顺序:若先做谓词过滤再在剩余集合上取 TopN,称为“前过滤”;反之为“后过滤”。后过滤通常更快,但可能显著降低召回,因此 Doris 采用前过滤策略。 在 Doris 中,Compound Search 的两个阶段均可通过索引加速。但在某些场景(如第一阶段 Range 过滤率极高)双阶段同时使用索引可能导致召回下降。Doris 会自适应判断是否对两阶段均使用索引,依据谓词过滤率与索引类型综合决策。

带过滤条件的 ANN 搜索

带过滤条件的 ANN 搜索是指在执行 ANN TopN 之前先应用其他谓词过滤,返回满足条件的 TopN。 下面用一个 8 维示例说明混合搜索流程。

create table ann_with_fulltext (
id int not null,
embedding array<float> not null,
comment String not null,
value int null,
INDEX idx_comment(`comment`) USING INVERTED PROPERTIES("parser" = "english") COMMENT 'inverted index for comment',
INDEX ann_embedding(`embedding`) USING ANN PROPERTIES("index_type"="hnsw","metric_type"="l2_distance","dim"="8")
) duplicate key (`id`)
distributed by hash(`id`) buckets 1
properties("replication_num"="1");

INSERT INTO ann_with_fulltext VALUES
(1, [0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8], 'this is about music', 10),
(2, [0.2,0.1,0.5,0.3,0.9,0.4,0.7,0.1], 'sports news today', 20),
(3, [0.9,0.8,0.7,0.6,0.5,0.4,0.3,0.2], 'latest music trend', 30),
(4, [0.05,0.06,0.07,0.08,0.09,0.1,0.2,0.3], 'politics update',40)

假设用户输入查询向量 [0.1,0.1,0.2,0.2,0.3,0.3,0.4,0.4],只在 comment 含 “music” 的文档中检索最相似的前 2 条:

SELECT id, comment,
l2_distance_approximate(embedding, [0.1,0.1,0.2,0.2,0.3,0.3,0.4,0.4]) AS dist
FROM ann_with_fulltext
WHERE comment MATCH_ANY 'music' -- 先用倒排索引过滤
ORDER BY dist ASC -- 在过滤后的结果集上做 ANN TopN
LIMIT 2;

+------+---------------------+----------+
| id | comment | dist |
+------+---------------------+----------+
| 1 | this is about music | 0.663325 |
| 3 | latest music trend | 1.280625 |
+------+---------------------+----------+
2 rows in set (0.04 sec)

带过滤条件的 ANN 搜索要想利用向量索引加速 TopN,需要确保涉及的过滤列具备倒排等二级索引。

查询参数

除了在构建 HNSW 索引时可指定参数外,查询阶段也可通过会话变量调节行为。

  • hnsw_ef_search:HNSW索引的EF搜索参数。ef_search 用来控制搜索阶段时 candidates 队列的最大长度,ef_search 越大则搜索的精度越高,代价是搜索的耗时越高。默认值为 32。

  • hnsw_check_relative_distance:是否启用相对距离检查机制,以提升HNSW搜索的准确性。默认为 true。

  • hnsw_bounded_queue: 是否使用有界优先队列来优化HNSW的搜索性能。默认为 true。

向量量化

采用 FLAT 编码时,HNSW 索引(原始向量 + 图结构)可能占用大量内存。HNSW 必须全量驻留内存才能工作,因此在超大规模数据集上易成瓶颈。 向量量化通过压缩 FLOAT32 减少内存开销。Doris 当前支持两种标量量化:INT8 与 INT4(SQ8 / SQ4)。以 SQ8 为例:

CREATE TABLE sift_1M (
id int NOT NULL,
embedding array<float> NOT NULL COMMENT "",
INDEX ann_index (embedding) USING ANN PROPERTIES(
"index_type"="hnsw",
"metric_type"="l2_distance",
"dim"="128",
"quantizer"="sq8" -- 指定使用 INT8 进行量化
)
) ENGINE=OLAP
DUPLICATE KEY(id) COMMENT "OLAP"
DISTRIBUTED BY HASH(id) BUCKETS 1
PROPERTIES (
"replication_num" = "1"
);

在 768 维的 Cohere-MEDIUM-1M 与 Cohere-LARGE-10M 数据集测试中,SQ8 可将索引大小压缩至 FLAT 的约 1/3。 数据集

数据集向量维度存储/索引方案总磁盘占用数据部分索引部分备注
Cohere-MEDIUM-1M768DDoris (FLAT)5.647 GB (2.533 + 3.114)2.533 GB3.114 GB1M 向量,原始 + HNSW FLAT 索引
Cohere-MEDIUM-1M768DDoris SQ INT83.501 GB (2.533 + 0.992)2.533 GB0.992 GBINT8 对称量化
Cohere-LARGE-10M768DDoris (FLAT)56.472 GB (25.328 + 31.145)25.328 GB31.145 GB10M 向量
Cohere-LARGE-10M768DDoris SQ INT835.016 GB (25.329 + 9.687)25.329 GB9.687 GBINT8 量化,索引显著减小

量化会带来额外构建开销,原因是构建阶段需要大量距离计算,且每次计算需对量化值解码。以 128 维向量为例,随行数增长构建时间上升,SQ 相比 FLAT 可能引入约 10 倍构建成本。

ANN-SQ-BUILD_COSTS

性能调优

向量搜索是典型的二级索引点查场景。若对 QPS 与延迟要求较高,可参考以下建议。经调优,在 FE 32C 64GB + BE 32C 64GB 机器上,Doris 可达到 3000+ QPS(数据集:Cohere-MEDIUM-1M)。

查询性能

并发方案QPS平均延迟 (s)P99 延迟 (s)CPU 使用率召回率
240Doris3340.43990.0713681680.16339982540%91.00%
240Doris SQ INT83188.63590.0747288520.16037019540%88.26%
240Doris SQ INT42818.22910.0846638680.17482681543%80.38%
240Doris 暴力计算3.678725.55487882629.363227973100%100.00%
480Doris4155.72200.1133872710.26108607560%91.00%
480Doris SQ INT83833.11300.1230402140.27691286750%88.26%
480Doris SQ INT43431.05380.1376369950.28163124957%80.38%
480Doris 暴力计算3.678725.55487882629.363227973100%100.00%

使用 prepared statement

常见 embedding 模型输出通常为 768 维或更高。如果将该向量作为字面量直接写入 SQL,解析耗时可能超过实际执行时间,因此建议使用 Prepared Statement。当前 Doris 不支持通过 mysql client 直接执行相关命令,需要通过 JDBC 调用。

1. 在 jdbc url 里面开启服务端 prepared statement
url = jdbc:mysql://127.0.0.1:9030/demo?useServerPrepStmts=true
2. 使用 prepared statement
// use `?` for placement holders, readStatement should be reused

PreparedStatement readStatement = conn.prepareStatement("SELECT id, l2_distance_approximate(embedding, cast (? as ARRAY<FLOAT>)) AS distance
FROM l2_distance_approximate
ORDER BY distance
LIMIT 10");

...

readStatement.setString("[0,11,77,24,3,0,0,0,28,70,125,8,0,0,0,0,44,35,50,45,9,0,0,0,4,0,4,56,18,0,3,9,16,17,59,10,10,8,57,57,100,105,125,41,1,0,6,92,8,14,73,125,29,7,0,5,0,0,8,124,66,6,3,1,63,5,0,1,49,32,17,35,125,21,0,3,2,12,6,109,21,0,0,35,74,125,14,23,0,0,6,50,25,70,64,7,59,18,7,16,22,5,0,1,125,23,1,0,7,30,14,32,4,0,2,2,59,125,19,4,0,0,2,1,6,53,33,2]");

ResultSet resultSet = readStatement.executeQuery();

减少 segment 数量

Doris 的 ANN 索引建立在 segment 上,segment 过多会引入额外开销。理想情况下,带 ANN 索引的表每个 tablet 下 segment 数不应超过 5 个。可通过调整 be.conf 中 write_buffer_sizevertical_compaction_max_segment_size 增大单 segment 大小以减少数量;建议两者设置为 10737418240(10GB)。

减少 rowset 数量

减少 rowset 数量与减少 segment 的目的相同:降低调度开销。每次导入都会生成一个 rowset,建议使用 stream load 或 INSERT INTO SELECT 做批量导入。

Ann 索引常驻内存

当前 ANN 索引算法基于内存,若查询到的 segment 索引未驻留内存会触发磁盘 I/O。为性能考虑建议常驻,可在 be.conf 中设置 enable_segment_cache_prune=false

parallel_pipeine_task_num = 1

ANN TopN 查询返回行数很少,无需高并行度,建议 SET parallel_pipeline_task_num = 1

enable_profile = false

若对延迟极其敏感,建议关闭 query profile(enable_profile=false)。

使用限制

  1. Doris 要求 ANN Index 对应的列必须是 NOT NULLABLE 的 Array<Float>,并且在后续的导入过程中,需要确保该列的每一个向量的长度均等于索引属性中指定的维度(dim),否则会报错。

  2. ANN Index 只能在 DuplicateKey 的表模型上使用。

  3. Doris 使用前过滤语意(谓词计算在AnnTopN 计算之前)当 SQL 中的谓词涉及到的列有非二级索引列时,为了保证结果的正确性,此时 Doris 会回退到暴力计算。 比如

SELECT id, l2_distance_approximate(embedding, [xxx]) AS distance
FROM sift_1M
WHERE round(id) > 100
ORDER BY distance limit 10;

虽然 id 是主键,但未在该列上构建倒排等可精确定位行号的二级索引,此类谓词在 Doris 中会在索引分析之后执行。为保证 ANN TopN 的前过滤语义,系统会回退为暴力计算。

  1. 如果 SQL 中指定的距离函数与 DDL 中索引的 metric 类型不匹配,那么此时 doris 无法通过 ANN 索引进行 TOPN 的计算,哪怕你是用的是 l2_distance_approximate/inner_product_approximate。

  2. 如果 metric 类型是 inner_product,那么只有 ORDER BY inner_product_approximate() DESC LIMIT N(这里的 DESC 不能省略)才能通过 ANN 索引加速。

  3. xxx_approximate() 函数的第一个参数为 ColumnArray,第二个参数为 CAST 或者 ArrayLiteral 时,才能触发索引分析,交换位置会回退暴力搜索。