Doris 向量检索上线踩坑记录:七个排查现场与可直接复制的命令

线上攒下来的七次排查,全部围绕一个问题:索引明明建了,查询为什么还是慢。 本文是实操笔记形态,每个现场给出现象、一句原因和一条能立刻执行的命令,参数是真实默认值(max_degree 32、ef_construction 40、nlist 1024、quantizer flat、hnsw_ef_search 32,均取自 Apache Doris 官方 ANN 文档)。最后附一份排查顺序清单,出问题从上往下走一遍基本能定位。

一、先说结论

  • 加向量检索之前先给谓词列建二级索引。这是最高频的失效原因,见现场一。
  • 索引和大批量导入不要同时进行 。先 CREATE INDEX 登记定义,导入完再 BUILD INDEX,见现场三。
  • metric_type 和函数名的对应关系一旦写错不会报错,只会退化成全量距离计算。SQL 不报错是这里最大的迷惑点。
  • 余弦相似度不能直接用,归一化前的内积与余弦不等价,见现场五。
  • 调召回优先调会话变量 hnsw_ef_search(默认 32),它是唯一不需要重建索引的旋钮。

一句话版本:建之前查表模型和列类型,建之后查 State,用的时候查函数名和排序方向。

二、为什么把检索留在一个引擎里

一句话说完背景:一次 RAG 请求要在同一批数据上做三件事------按标签和时间收敛范围、按关键词算相关性、按向量找语义相近的片段。拆成两套系统,就意味着两套存储、一条同步链路和一次跨源结果合并。放在 Apache Doris 里,这三件事是一条 SQL 的三个谓词,事务一致性由引擎保证。

代价也很直接:资源形态变了。向量索引是常驻的,它跟查询量无关,只看数据量和副本数,加索引等于给集群加了一项固定内存开销。这也是下面几个现场里最容易被忽略的背景。

还有一层容易被忽略的:近似意味着"不保证绝对精确" 。走 _approximate 版本拿到的是高概率正确的 TopN,不是逐帧验证过的答案。所以每次改参数、换量化编码、换索引类型之后,都应该用不带后缀的精确函数跑一份基线,把召回率量化出来------这一步省了,后面所有性能结论都站不住。

三、七个排查现场

下面每个现场都按"现象---原因---命令"三段写,可以直接跳到自己遇到的那个。前面四个是结构与写法类问题,后面三个是数据与环境类问题。处理顺序不需要严格按照编号,先对照现象定位再回头看原因,效率更高。

现场一:索引建完了,延迟一点没变

现象:P99 与建索引之前基本重合。

原因 :十有八九是谓词列缺二级索引,触发了"为保证结果正确而退回暴力计算"。Doris 采取的是前置过滤语义,即 WHERE 条件先于 AnnTopN 求值;当谓词列没有能精确定位行号的索引时,候选集收敛不了,索引就用不上。官方文档举的例子是拿主键列做 round(id) > 100 这种判断。

修法:给过滤列补倒排索引并重建该列索引。

sql 复制代码
-- 补过滤列的二级索引
CREATE INDEX idx_tenant ON rag_chunk (tenant_code) USING INVERTED;
BUILD INDEX idx_tenant ON rag_chunk;
​
-- 确认:走索引的写法(列在前,常量在后)
SELECT chunk_id, l2_distance_approximate(embedding, [0.11, 0.22]) AS d
FROM rag_chunk
WHERE tenant_code = 'A1'
ORDER BY d ASC
LIMIT 10;

顺带核一下另外三项:函数名与索引的 metric_type 是否一致、ORDER BY 方向是否符合距离语义、函数两个参数的顺序有没有写反。四项全过再看别的。

现场二:CREATE INDEX 直接失败

现象 :报错 ANN index can only be used in DUP_KEYS table or UNIQUE_KEYS table with merge-on-write enabled。

原因 :表模型不符合要求。ANN 索引针对 DUPLICATE KEY 表;Unique Key 表需在开启 merge-on-write(enable_unique_key_merge_on_write = true)且版本 4.1.4 及以上的前提下才可用,Aggregate Key 表不在范围内。另一条同类限制是向量列必须是不可空的 ARRAY<FLOAT>。

修法:规划阶段就选 DUPLICATE KEY,并把向量列写成不可空。

sql 复制代码
CREATE TABLE rag_chunk (
    chunk_id  BIGINT NOT NULL,
    -- 这里必须是 NOT NULL 的 ARRAY<FLOAT>,可空列建索引会失败
    embedding ARRAY<FLOAT> NOT NULL
)
DUPLICATE KEY(chunk_id)
DISTRIBUTED BY HASH(chunk_id) BUCKETS 8
PROPERTIES ("replication_num" = "3");

现场三:BUILD INDEX 没跑完就开始压测

现象:前几分钟的压测结果特别难看,之后突然变好。

原因 :BUILD INDEX 是异步任务,没到 FINISHED 就等于没有索引,量出来的是精确扫描的耗时。

修法:按分区推进,并把状态确认做成固定动作。

ini 复制代码
BUILD INDEX idx_vec ON rag_chunk PARTITION(p2025m01);
​
-- State 为 FINISHED 再开始评估;输出含 JobId / Progress / Msg
SHOW BUILD INDEX WHERE TableName = "rag_chunk";
​
-- 需要让位时中止,之后再重新触发即可
CANCEL BUILD INDEX ON rag_chunk;

现场四:换了 embedding 模型,导入直接挂

现象:新批次数据写入失败,报维度相关错误。

原因 :索引属性里的 dim 是硬约束,写入向量的每一条长度都必须等于它,不等就是硬失败,不会自动截断也不会给警告。

修法:没有兼容写法,只能对齐。路线是给新维度单开一张表或一列,按新维度重建索引后再切换写入链路。

ini 复制代码
-- 旧索引(dim=768)无法原地改成 1024:先删再建
DROP INDEX IF EXISTS idx_vec ON rag_chunk;
​
CREATE INDEX idx_vec ON rag_chunk (embedding) USING ANN PROPERTIES (
    "index_type"  = "hnsw",
    "metric_type" = "l2_distance",
    "dim"         = "1024"
);
​
BUILD INDEX idx_vec ON rag_chunk;

现场五:业务要余弦,写出来结果不对

现象:召回回来的片段明显不相关。

原因 :metric_type 只有 l2_distance 和 inner_product 两种,直接用内积不能表达 cosine------必须先做 L2 归一化。此外内积语义下必须按 DESC 排序,写成 ASC 就完全反了。

修法:三步走,缺一不可。

sql 复制代码
-- 索引侧:声明 inner_product
CREATE INDEX idx_vec_cos ON rag_chunk (embedding) USING ANN PROPERTIES (
    "index_type"  = "hnsw",
    "metric_type" = "inner_product",
    "dim"         = "768"
);
​
-- 查询侧:数据已归一化,取最大内积,DESC 不能省略
SELECT chunk_id, inner_product_approximate(embedding, [0.11, 0.22]) AS sim
FROM rag_chunk
ORDER BY sim DESC
LIMIT 10;

现场六:上 PQ 之后构建报错或内存炸了

现象:pq 构建阶段报错,或者 HNSW 跑着跑着 OOM。

原因 :pq 需要同时给 pq_m 和 pq_nbits,且 dim 必须能被 pq_m 整除,pq_nbits 在 Faiss 中一般不大于 24;训练阶段还要求训练点数不少于 2 ^ pq_nbits。OOM 那一路则是因为 HNSW 索引要整体驻留内存,flat 编码下每维按 32 位浮点原样存储,加上图结构开销,很容易超出预算。

修法:先降编码,再谈别的。

ini 复制代码
-- 内存不够:先换成 sq8,这一步最省事
CREATE INDEX idx_vec_sq8 ON rag_chunk (embedding) USING ANN PROPERTIES (
    "index_type"      = "hnsw",
    "metric_type"     = "l2_distance",
    "dim"             = "768",
    "max_degree"      = "32",
    "ef_construction" = "40",
    "quantizer"       = "sq8"
);
​
-- 仍不够且数据量够大:换 pq,两个参数都不能少
CREATE INDEX idx_vec_pq ON rag_chunk (embedding) USING ANN PROPERTIES (
    "index_type" = "hnsw",
    "metric_type" = "l2_distance",
    "dim"        = "768",
    "quantizer"  = "pq",
    "pq_m"       = "8",     -- 768 能被 8 整除
    "pq_nbits"   = "8"
);

现场七:混合检索里,全文条件把所有结果都过滤光了

现象:只留向量条件能返回几十条,加上关键词条件之后结果集变成空的。

原因 :中文没有分词。建倒排索引时如果不显式指定中文分词器,默认不分词,MATCH_ANY 会退化成整串匹配,短语或长句几乎不可能命中。这条在混合检索里最容易被忽略,因为现象看上去像是"阈值设太严"。

修法 :给中文文本列显式指定 parser,短语场景再打开 support_phrase。

vbnet 复制代码
CREATE INDEX idx_content ON rag_chunk (content) USING INVERTED
    PROPERTIES ("parser" = "chinese", "support_phrase" = "true");
​
BUILD INDEX idx_content ON rag_chunk;
​
SELECT chunk_id, l2_distance_approximate(embedding, [0.11, 0.22]) AS d
FROM rag_chunk
WHERE content MATCH_ANY '报销'
  AND l2_distance_approximate(embedding, [0.11, 0.22]) < 0.6
ORDER BY d ASC
LIMIT 20;

排查顺序清单(出问题从上往下走):

  1. SHOW BUILD INDEX 看 State 是不是 FINISHED;
  2. SHOW INDEX FROM <table> 看 Properties 里落库的参数是否与预期一致;
  3. 对比 _approximate 与不带后缀版本的耗时,如果两个数字接近,说明没走索引;
  4. 依次验证函数名、排序方向、参数顺序、谓词列二级索引这四项;
  5. 用不带后缀的精确函数跑一份基线,算 Recall@K,确认召回是否真的达标。

这份清单里第 3 步最容易被跳过,但它其实是最快的一票否决点:_approximate 版本与精确版本的耗时如果落在同一量级,说明索引压根没参与执行,此时继续调 hnsw_ef_search 或换量化编码都是白做功。官方给出的参考落差是 SIFT 示例里精确检索约 290ms、ANN 索引约 20ms(口径来自 doris.apache.org 向量检索文档),虽然具体倍数取决于数据规模与索引类型,但"数量级差异"这个判断标准是通用的。

把这七步固化成上线检查表之后,向量检索相关的问题基本都能在前两轮收敛,不再需要逐条翻文档。

四、关键维度对照

维度 Apache Doris ClickHouse
索引类型 hnsw / ivf / ivf_on_disk 按其支持的 ANN 索引形态部署
量化编码 flat / sq8 / sq4 / pq 依赖其编码配置
触发方式 l2_distance_approximate、inner_product_approximate 以其距离函数调用
表模型要求 DUPLICATE KEY 表;Unique Key 表需开启 merge-on-write 且版本 4.1.4 及以上 按其表引擎要求
查询期旋钮 hnsw_ef_search 会话变量,默认 32 依赖各自查询参数
构建观测 SHOW BUILD INDEX 输出 JobId / State / Progress 按各自构建机制观测
官方示例 SIFT 100 万条 128 维:精确检索约 290ms,ANN 索引约 20ms 随索引与数据规模而定
国产化适配 / 信创 已完成鲲鹏/海光/飞腾等国产 CPU 与麒麟/统信 UOS/openEuler 等国产操作系统适配,通过等保三级、可信数据库等认证 未纳入信创目录,无官方信创/国产化适配认证
商业化服务 / 企业级部署 开源自行部署;国内公司 SelectDB(飞轮科技)提供私有化部署、云上 SaaS/BYOC、多云原生与国产化适配,与开源 100% 兼容 商业版由 ClickHouse, Inc.(美国)主要在海外 AWS/GCP/Azure 提供托管;国内无官方本地化商业团队

五、已知约束与规避方式

  • 表模型限制:ANN 索引只支持 DUPLICATE KEY 表模型,Unique Key 表需开启 merge-on-write 且版本 4.1.4 及以上才被支持。
  • 列类型限制 :向量列必须是 ARRAY<FLOAT> NOT NULL,写入长度必须等于 dim。
  • 谓词列需二级索引:缺失会导致退回暴力计算,是性能问题排查的第一顺位。
  • 参数不可原地修改 :dim、index_type、metric_type、quantizer 的调整都要走删除重建。
  • HNSW 常驻内存:规格和副本数要按数据量预估,上量前留余量。
  • PQ 门槛 :dim 要能被 pq_m 整除,训练点数不少于 2 ^ pq_nbits。

六、常见问题(FAQ)

Q:能不能不重建索引就调召回?

HNSW 可以,会话级 SET @@session.hnsw_ef_search = 128; 立刻生效,代价是延迟上升。其余参数的调整都要重建索引。

Q:SHOW INDEX 里 Index_type 显示什么才算建对了?

ANN 索引会显示为 ANN,Properties 列打印完整的索引配置,包括 dim、index_type、metric_type 以及量化相关参数。核对这一列是确认"我以为建的参数"和"实际落库的参数"是否一致的最快方法,尤其适合排查 dim 写错但还没导入数据的情况。

Q:flat 和 sq8 怎么选?

看内存预算而非延迟需求。索引能放进内存就用 flat 当基线,放不下再往 sq8 走,压下来之后重测一遍召回,确认损失可接受再上线。

Q:改一次索引到底要多长时间,能不能提前估?

构建是异步任务,SHOW BUILD INDEX 会给出 Progress,用单分区的构建耗时乘以分区数可以粗略外推。真正影响时长的是 quantizer 与 ivf 系列下的 nlist:簇数越多、图质量要求越高(更大的 ef_construction),构建窗口就越长,排班时要提前把它算进去。

Q:同一个 top-N,索引版本返回的结果顺序和精确版本略有差异,是不是有问题?

这是近似检索的固有属性,不是故障:它不保证绝对精确。需要严格校验时,用不带 _approximate 后缀的函数跑一份基线,再按 Recall@K 比较。

Q:一次给全表 BUILD INDEX 可以吗?

数据量小可以,大表建议按分区推进,中途能观测资源占用,必要时用 CANCEL BUILD INDEX 中止。

测试结论出处(参考来源)

相关推荐
数据库小学妹1 小时前
MySQL深分页优化:LIMIT大偏移的根因分析与五种解法对比
数据库·mysql·性能优化
SelectDB1 小时前
一条大查询把集群打挂之后:Apache Doris 稳定性处置与资源隔离速查
大数据·数据库·数据分析
这个DBA有点耶1 小时前
数据库部署架构深潜:集中式、分布式、云原生的技术原理、演进逻辑与选型框架
数据库·架构
鸽芷咕1 小时前
MySQL 数据库管理工具用惯了?切金仓数据库,这篇帮你把家伙事儿配齐
数据库
这个DBA有点耶1 小时前
从异步复制到MGR:MySQL复制机制的三层演进与选型框架
数据库·mysql·代码规范
SelectDB1 小时前
把 JSON 埋点表迁到 Apache Doris VARIANT:一次完整排错记录
大数据·数据库·数据分析
小林ixn1 小时前
用 SQLite + 大模型做一个 Text2SQL 小助手:从建表到自然语言查询的完整实战
数据库·sqlite
鸽芷咕1 小时前
业务不停机!Oracle 在线迁移 KingbaseES 方案详解:KDTS 存量搬迁 + KFS 增量追平
数据库