Day 28|Doris Search 全景:倒排索引、BM25、HNSW、IVF、Hybrid Search 与 RRF
课程阶段:第六阶段|湖仓、搜索与 AI 数据基础设施
学习目标: 建立 Apache Doris 统一检索的完整坐标系,能够设计全文检索、向量检索、Hybrid Search 与 RRF 排名融合方案,并掌握表结构、索引构建、质量评测、容量规划、上线治理和故障排查方法。
版本快照: 2026-08-18 核验,Apache Doris 4.1.3 为 Latest,4.0.8 为 Stable。正文以 4.0 稳定分支的倒排索引、BM25、HNSW 和基础 Hybrid Search 为主体,4.1 的 IVF、IVF_ON_DISK 与量化单独标注。
课程承接: Day 11 已经讲过索引体系,Day 14 讲过 VARIANT,Day 26–27 讲过湖仓和存算分离。Day 28 把结构化过滤、全文检索、语义检索和分析计算放到同一条 SQL 链路中。

一、这次重做的范围与原则
原培训材料第 345–363 页从“索引即过滤”开始,讲了 ZoneMap、BloomFilter、NGram BloomFilter 和 Inverted Index,随后解释 Term Dictionary、Posting List、索引写入成本与索引选型。这套框架适合建立 Data Skipping 的基础认知。
Day 28 沿用其中三条主线:
- 查询性能来自尽早缩小候选数据;
- 倒排索引通过词典和 Posting List 定位 RowID;
- 索引会增加存储、导入 CPU 和后台维护成本。
当前 Doris 4.x 已经把搜索能力扩展到 BM25、SEARCH()、ANN 向量索引、Hybrid Search、IVF、IVF_ON_DISK 和向量量化。本文将原材料的索引基础和这些新能力连接起来,全文只讨论 Doris 官方资料能够支撑的机制、语法和边界。

二、统一检索要处理三类信号
一条企业检索请求通常同时包含词法、语义和结构化约束。
用户搜索:
“找出当前租户最近 30 天的 Doris 权限故障文档,关键词要包含权限或 Access Denied,语义上接近‘用户登录成功但查询被拒绝’,返回最相关的 20 条。”
这句话可以拆成三组条件。
2.1 词法信号
“权限”“Access Denied”“Doris”都属于明确词法。错误码、版本号、产品型号、机构名和法律条款也属于这一类。全文检索擅长保证某个词、全部词、短语或模式被命中,结果具有较强可解释性。
2.2 语义信号
“没有查询权限”“RBAC 授权失败”“Access denied”“用户可登录但无法查询”表达方式不同,业务含义接近。Embedding 将这些内容映射到向量空间,ANN 负责从大规模向量中快速找到近邻。
2.3 结构化信号
租户、ACL、时间、文档类型、产品版本、地域和状态决定结果能否返回。RAG 场景中,这组条件直接关系到权限正确性,应该进入数据库 WHERE 或 Row Policy,在候选集合形成之前生效。
Doris 的价值来自三类信号共享一张表、一套权限、一套 SQL 和一套 MPP 执行引擎。检索结果可以继续做 JOIN、聚合、窗口函数和趋势分析。

三、全文检索从倒排索引开始
普通 LIKE '%keyword%' 往往需要逐行读取字符串。倒排索引在写入时完成分词,并维护:
Term Dictionary
Token → 词典项
Posting List
Token → 命中的 RowID 列表
假设三条文档经过分词后形成:
doris → [1, 2]
全文 → [1]
向量 → [2, 3]
rag → [3]
查询:
WHERE body MATCH_ANY 'doris 向量'
执行器可以读取 doris 和 向量 的 Posting List,执行并集后得到候选 RowID。未命中的行不需要解码完整正文。
Doris 当前全文检索支持 STRING、TEXT、VARCHAR 和 ARRAY<STRING> 等列上的倒排索引。文本谓词会由 Nereids 下推到索引层,然后和普通结构化谓词共同参与后续执行。

3.1 倒排索引可以处理哪些查询
当前常用操作包括:
| 操作 | 语义 | 适用场景 |
|---|---|---|
MATCH_ANY |
任意 Token 命中 | 普通关键词 OR |
MATCH_ALL |
全部 Token 命中 | 多关键词 AND |
MATCH_PHRASE |
词序和邻近关系匹配 | 短语、日志固定语句 |
MATCH_PHRASE_PREFIX |
短语前缀 | 输入联想、前缀搜索 |
MATCH_REGEXP |
Token 级正则 | 模式化检索 |
SEARCH() |
跨列与布尔组合 | 复杂检索 DSL |
示例:
SELECT id, title
FROM docs
WHERE body MATCH_PHRASE 'vector search';
跨列组合:
SELECT id, title
FROM docs
WHERE SEARCH(
'title:Doris AND (category:database OR tags:ANY(vector search))'
);
SEARCH() 从 4.0 起提供统一入口,适合将多个字段、布尔逻辑和全文条件组织成一条表达式。
3.2 全文检索、NGram BloomFilter 与普通 LIKE 的边界
原培训材料同时讲过倒排索引和 NGram BloomFilter。两者都能加速字符串检索,适用范围不同。
NGRAM_BF 针对 LIKE '%abc%' 这类子串匹配,将原字符串切成固定长度的 N-Gram,再用 BloomFilter 判断目标片段是否“确定不存在”。它保留了 LIKE 的子串语义,存在概率型误判,最终仍需读取候选数据完成确认。
倒排索引面向 Token 语义。分词以后,“running shoes”可以按词检索、短语检索和相关性排序,Posting List 还能和其他文本条件组合。日志中的错误码、Trace ID、URL Path 等整串字段,可以使用不分词或 keyword 方案;自然语言正文更适合分词倒排索引。
选型时可以先问三个问题:
- 用户输入的是子串,还是词和短语?
- 结果只需要过滤,还是需要 BM25 排名?
- 查询需要大小写、同义词、停用词和多语言处理吗?
子串语义明确、没有相关性排序时,NGram BloomFilter 可能更轻量。需要词法理解、短语、布尔逻辑和排名时,倒排索引更合适。两类索引都需要用真实数据测存储放大与导入吞吐,不能仅凭功能表选择。
四、Analyzer 决定“什么算同一个词”
全文检索质量首先取决于分词。Doris 3.1 起支持自定义 Analyzer,处理链可以包含:
Character Filter
→ Tokenizer
→ Token Filter
Character Filter 在分词前处理 HTML、标点和业务符号。Tokenizer 决定如何切分,当前常见选择包括 standard、ngram、edge_ngram、keyword 和 icu。Token Filter 用于大小写、ASCII 折叠和复合词处理。
例如:
FE Master
FE leader
主节点
三种表达可能指向同一个概念。Analyzer 只能解决其中的词法归一;跨语言或同义语义通常还需要同义词治理、Query Rewrite 或向量召回。
产品型号和错误码经常需要 keyword,自然语言正文更适合标准或语言相关分词。将所有 STRING 列都套用同一个 Analyzer,会造成索引膨胀和检索质量问题。

4.1 Analyzer 需要版本化
修改 Analyzer 会改变 Token 和 Posting List。生产系统应记录:
analyzer_name
analyzer_version
tokenizer
char_filter
token_filter
stopword_version
synonym_version
上线新 Analyzer 时,建议构建影子索引并使用固定 Query Set 做 A/B,保留旧索引作为回滚路径。
4.2 用 Query Log 设计 Analyzer
Analyzer 设计最好从线上 Query Log 开始。可以抽取最近一到四周的查询,按照意图分组:
产品与组件名
错误码与版本号
自然语言问题
中英文混写
路径、URL、IP 和 Trace ID
错别字与输入联想
然后对每组检查“查询 Token”和“文档 Token”能否对齐。
例如用户输入:
Doris FE 选主超时
文档可能写成:
Frontend leader election timeout
仅靠中文标准分词无法建立完整对应关系。工程上可以把“FE / Frontend”“选主 / leader election”放入同义词或 Query Rewrite 层,向量检索负责处理更宽泛的语义表达。
Analyzer 评审表至少记录:字段、语种、Tokenizer、大小写规则、停用词、同义词、是否支持 Phrase、平均 Token 数、索引大小和典型 Query。这样可以避免某次“优化分词”悄悄改变全部搜索结果。
4.3 Phrase、Prefix 和 Regex 的成本意识
MATCH_PHRASE 需要词位置信息,索引体积和写入成本通常高于只保存 Token 命中的模式。MATCH_PHRASE_PREFIX 和 Regex 面向更复杂的匹配,候选范围和 CPU 成本会随 Query 变化。
生产 API 应限制:
- 最大 Query 长度;
- 最大 Token 数;
- Regex 复杂度;
- 单次候选数量;
- Query Timeout;
- 高成本操作的调用角色。
检索能力越丰富,查询治理越重要。开放给内部 Agent 或终端用户时,可以通过固定 Query Template、SQL Block Rule 和 Workload Group 控制资源风险。
五、BM25:从“命中”走到“相关性排序”
倒排索引可以得到命中集合。搜索页面和 RAG 往往只需要最相关的若干条,Doris 4.0 起通过 score() 暴露 BM25 相关性。
BM25 综合考虑:
- TF:查询词在当前文档中的出现频率;
- IDF:查询词在整个语料中的稀有程度;
- 文档长度:避免长文本仅凭词多获得过高分数。
官方当前给出的默认参数为:
k1 = 1.2
b = 0.75
boost = 1.0

5.1 score() 的激活条件
当前相关性计算需要同时满足:
SELECT显式调用score();WHERE至少包含一个支持评分的MATCH_*或SEARCH条件;- 查询形成
ORDER BY score() ... LIMIT K的 Top-N。
SELECT
id,
title,
score() AS relevance
FROM docs
WHERE body MATCH_ANY 'apache doris vector search'
ORDER BY relevance DESC
LIMIT 20;
非分词倒排索引只承担精确过滤,不计算 BM25。
5.2 BM25 分值怎样解释
BM25 分值为正数,没有固定上界。分值取决于本次 Query、Token 和当前语料分布。可靠用法是在同一次 Query 内比较相对大小。两个不同 Query 都得到 3.0,无法据此判断相关程度一致。
官方 Key Feature 文档还指出,当前 Doris 使用 Lucene 默认参数,k1 和 b 没有开放为按 Query 动态调节的运行参数。课程实验以 Analyzer、候选集合和排序结果为重点,不设计“每条 SQL 临时调 BM25 参数”的方案。
5.3 一个 BM25 排名变化的例子
假设语料中“database”几乎每篇文档都出现,“delete bitmap”只出现在少量 Doris 内核文档中。查询:
database delete bitmap
BM25 会给“delete bitmap”更高区分权重。正文很长、重复出现大量公共词的文档,也不会无限提高得分,因为 k1 控制词频饱和,b 对文档长度做归一化。
这也解释了一个生产现象:同一条 Query 在大量新文档导入后,排名可能变化。新增语料会改变总行数、某个 Token 的文档频率和平均文档长度。质量回归要固定数据快照或记录语料版本,排名异常排查也要检查最近的数据增量。
5.4 BM25 适合承担哪一段排序
BM25 非常适合第一阶段关键词 Top-N,也适合最终结果仍以词法相关性为主的搜索。以下场景需要增加其他信号:
- 时间新鲜度;
- 文档权威等级;
- 产品版本匹配;
- 用户点击反馈;
- 向量语义相似度;
- 业务库存、价格或风险等级。
可以先用 BM25 得到可控候选,再由 SQL 加入业务字段排序。涉及 ANN 双路召回时,RRF 比直接拼接绝对分数更容易解释和治理。
六、向量在 Doris 中怎样存
Doris 当前没有单独的 VECTOR 列类型,Embedding 使用固定维度:
embedding ARRAY<FLOAT> NOT NULL
ANN 索引声明算法、距离和维度:
INDEX idx_embedding (embedding) USING ANN
PROPERTIES (
"index_type" = "hnsw",
"metric_type" = "l2_distance",
"dim" = "768"
)
每行实际向量长度必须等于 dim,向量列不能为 NULL。导入链路还需要检测 NaN、Infinity、零向量和模型版本混写。

6.1 表模型的官方口径存在差异
截至本文核验日,Doris 4.x 两份官方页面对 ANN 支持的表模型有不一致表述:
- Vector Index Overview 写明
DUPLICATE KEY或 MoWUNIQUE KEY; - Vector Index Practical Guide 的前置条件和 FAQ 仍写 Only
DUPLICATE KEY。
课程主实验统一使用 DUPLICATE KEY,这样在 4.0.8 和 4.1.3 上拥有更稳妥的可执行基线。项目计划使用 Unique Key 时,需要在目标补丁版本完成真实 DDL、更新、Compaction、索引构建和查询命中测试,并把验证结果写入 ADR。
6.2 Embedding 写入链路也属于数据工程
向量来自外部模型或 Doris EMBED() 等 AI 能力。无论调用位置在哪里,生产链路都要保存模型和输入版本。
建议把一次 Embedding 生成拆成:
读取原文
→ 文本清洗与 Chunk
→ 计算 content_hash
→ 调用 Embedding Model
→ 校验维度、数值和范数
→ 写入 embedding + model/version
→ 抽样对账
需要重点拦截:
- 维度不等于
dim; - NULL;
- NaN 或 Infinity;
- Cosine 场景中的零向量;
- 正文已变更、Embedding 未重算;
- 同一索引混入不同模型空间。
轻量实验可以使用伪向量验证 SQL 和索引流程,Recall 与业务质量评测必须使用真实模型向量。模型升级时推荐新增 embedding_v2 与 idx_embedding_v2,保持 V1 可查询,再做离线与线上灰度。
6.3 Chunk 设计会改变索引规模和召回质量
RAG 文档切得过大,一个 Chunk 可能包含多个主题,向量语义被平均,BM25 还会受到长度归一化影响。切得过小,行数、倒排索引、ANN 索引和结果去重成本都会增加,上下文又可能缺少完整语义。
可以对 256、512、1024 Token 三组 Chunk 做同一 Query Set 评测,比较 NDCG、Recall、索引大小、p99 和最终回答质量。Chunk Size 应作为检索 Schema 的一部分管理。
七、ANN 查询如何进入存储层 Top-K
向量索引只会在特定查询形态下发挥作用:
SELECT
id,
l2_distance_approximate(embedding, [ ... ]) AS dist
FROM docs
ORDER BY dist ASC
LIMIT 20;
Nereids 识别 approximate distance、ORDER BY 和 LIMIT,将查询转换为存储侧 AnnTopN。执行过程包含:
结构化 / 文本 Pre-filter
→ Segment ANN Local Top-K
→ Tablet Merge
→ 跨节点 Global Top-K

l2_distance_approximate 按升序排列,距离越小越相似。inner_product_approximate 按降序排列,值越大越相似。去掉 _approximate 后会进行精确 Brute-force 计算,适合构造 Ground Truth 或处理很小的候选集合。
Cosine Similarity 当前不能直接配置 metric_type="cosine"。官方推荐在写入前将向量做 L2 归一化,再使用 inner_product 和 inner_product_approximate。单位向量空间中,Inner Product 与 Cosine 排序一致。
7.1 Pre-filter 是否高效,取决于过滤列自身的索引
向量 Query 带有 tenant_id、category、created_at 和文本 MATCH 时,过滤列需要能尽早缩小候选。可以复用前几天学习的分区、Prefix、ZoneMap 和 Inverted Index。
理想路径:
Partition Pruning
→ tenant/category Secondary Index
→ Text Posting List
→ Candidate RowID Bitmap
→ AnnTopN
过滤列没有适当二级索引时,系统仍会维持结果正确性,执行成本可能退化为对过滤候选进行更多精确计算。Hybrid Search POC 要同时记录过滤前行数、过滤后候选数、AnnTopN 输入和最终 K。
7.2 Segment 数量直接进入 ANN 成本
ANN 索引按 Segment 构建。一个 Tablet 有 300 个小 Segment,查询需要从更多 Segment 获取 Local Top-K,再做合并。高频小批写入会放大:
- 索引构建次数;
- Segment 文件数;
- AnnTopN 局部候选数;
- Compaction 和索引重建成本。
Day 20 学过的批次治理、Group Commit 和 Compaction 会直接影响 Day 28 的 Search 性能。向量索引项目需要把写入路径和查询路径放在同一个压测场景里。