混合检索:多表多路检索
本文面向 SQL 用户,介绍 Doris hybrid_search() 在多表融合检索和模型精排Reranker场景下的用法。
概览
hybrid_search() 支持三类常用模式。选择哪种模式,取决于候选数据是否已经准备完成,以及是否需要 Fusion 或模型精排:
| 模式 | 适用场景 | 写法 |
|---|---|---|
| 单表融合 | 同一张表上同时做 BM25、ANN 等多路召回 | ORDER BY hybrid_search(JSON, score_1, score_2, ...) |
| 多表融合 | 文本、向量、标签等召回数据分布在多张表中 | FROM t1 HYBRID t2 ON ... ORDER BY hybrid_search(...) |
| Pure Rerank | 已经通过子查询、JOIN、CTE、UNION 构造好候选集,只需要模型精排 | ORDER BY hybrid_search(JSON),不带召回表达式 |
多表融合的执行心智模型是:
1每一路独立召回 -> 按 ON 指定的 row-key 合并 -> RRF/WEIGHT 融合打分 -> TopK
它不是普通 SQL JOIN,它是多路搜索结果按 row-key 做 union-like合并;某一路未召回到的列会以 NULL 输出,因此实际查询中建议用COALESCE() 取统一主键。
前提条件
使用前请确认:
bm25_score()使用的文本列已创建倒排索引。ann_distance()使用的向量列已创建向量索引,查询向量维度与索引维度一致。- 需要启用模型精排时,联系集群管理员确认已配置 Reranker 服务。
- 需要启用 embedding 时,联系集群管理员确认已配置 embedding 服务。
Reranker 调用失败时默认降级返回 Fusion 或原始候选顺序,可通过 Session 变量控制:
1SET reranker_fallback_on_failure = true; -- 默认:失败时降级
2SET reranker_fallback_on_failure = false; -- 失败时报错
多表融合检索
基本语法
1SELECT ...
2FROM table_0 t0
3HYBRID table_1 t1 ON t0.id = t1.doc_id
4[HYBRID table_2 t2 ON t0.id = t2.doc_id ...]
5ORDER BY hybrid_search(
6 '<ranker_json>',
7 <bucket_0_score_expr>,
8 <bucket_1_score_expr>
9 [, <bucket_2_score_expr> ...]
10)
11LIMIT <k>;
规则:
HYBRID ... ON ...只支持等值条件,可用AND连接多个等值键。hybrid_search()中的 score 表达式个数必须等于 HYBRID 召回路数。- 第 N 个 score 表达式只能引用第 N 路召回路的列。
- score 表达式必须直接是
bm25_score()或ann_distance()。 - 排序方向只支持默认降序或显式
DESC,不支持ASC。 - 同一层
FROM中不能把HYBRID和普通JOIN混用;如需普通 JOIN,请放到子查询或 CTE 中。
支持的召回表达式:
1bm25_score(text_col, 'any', 'query terms')
2bm25_score(text_col, 'all', 'query terms')
3bm25_score(text_col, 'phrase', 'query phrase')
4
5ann_distance(vector_col, [0.1, 0.2, 0.3, 0.4])
6ann_distance(vector_col, embedding('query text', 'text', 'embedding_model'))
HYBRID ... ON 语义示例
HYBRID ... ON 中的 ON 用来声明多路召回结果的 row-key 对齐关系。它不是普通
JOIN 的过滤条件,也不会要求两边都命中后才输出。可以把它理解为:
1docs 路独立召回 topN
2doc_embeddings 路独立召回 topN
3按 d.id = e.doc_id 对齐相同文档
4没有对齐上的一侧补 NULL
5再计算融合分数并排序
例如有两张表:
1docs
2+-----+---------------------+
3| id | title |
4+-----+---------------------+
5| 101 | Apple market value |
6| 102 | Apple product news |
7| 104 | Finance overview |
8+-----+---------------------+
9
10doc_embeddings
11+--------+-----------+
12| doc_id | embedding |
13+--------+-----------+
14| 101 | [...] |
15| 103 | [...] |
16| 104 | [...] |
17+--------+-----------+
执行:
1SELECT
2 COALESCE(d.id, e.doc_id) AS doc_id,
3 d.title,
4 e.doc_id AS embedding_doc_id,
5 __RANK
6FROM docs d
7HYBRID doc_embeddings e ON d.id = e.doc_id
8ORDER BY hybrid_search(
9 '{"rerank":"RRF","c":60,"candidates":3}',
10 bm25_score(d.title, 'any', 'apple finance'),
11 ann_distance(e.embedding, [0.1, 0.2, 0.3, 0.4])
12)
13LIMIT 10;
假设两路独立召回结果如下:
1docs 路 BM25 召回
2+------+------------+
3| id | local_rank |
4+------+------------+
5| 101 | 1 |
6| 102 | 2 |
7| 104 | 3 |
8+------+------------+
9
10doc_embeddings 路 ANN 召回
11+--------+------------+
12| doc_id | local_rank |
13+--------+------------+
14| 103 | 1 |
15| 101 | 2 |
16| 104 | 3 |
17+--------+------------+
则输出形态类似:
1+--------+--------------------+------------------+----------+
2| doc_id | title | embedding_doc_id | __RANK |
3+--------+--------------------+------------------+----------+
4| 101 | Apple market value | 101 | 0.032522 |
5| 104 | Finance overview | 104 | 0.031746 |
6| 103 | NULL | 103 | 0.016393 |
7| 102 | Apple product news | NULL | 0.016129 |
8+--------+--------------------+------------------+----------+
101、104同时被两路召回,按d.id = e.doc_id合并到同一行。103只被向量路召回,所以docs侧列为NULL。102只被 BM25 路召回,所以doc_embeddings侧列为NULL。ON只负责指定“哪个 key 表示同一个对象”,不表示必须两边都存在。- 多列
ON表示复合 row-key,例如ON a.tenant_id = b.tenant_id AND a.id = b.doc_id。
RRF 融合示例
1SELECT
2 COALESCE(d.id, e.doc_id) AS doc_id,
3 d.title,
4 d.content,
5 __RANK
6FROM docs d
7HYBRID doc_embeddings e ON d.id = e.doc_id
8ORDER BY hybrid_search(
9 '{"rerank":"RRF","c":60,"candidates":100}',
10 bm25_score(d.content, 'any', 'apple market cap'),
11 ann_distance(e.embedding, [0.1, 0.2, 0.3, 0.4])
12)
13LIMIT 10;
说明:
docs d是第 0 路,doc_embeddings e是第 1 路。- 第一个 score 表达式引用
d.content,第二个 score 表达式引用e.embedding。 __RANK是最终排序分数。多表融合未启用模型精排时,__RANK表示融合分数。candidates控制每路召回和后续融合候选规模。未配置时,多表场景默认使用
max(LIMIT, hybrid_search_reranker_default_candidates)。
RRF 主要使用每路的局部排名做融合,而不是直接比较 BM25 分数和 ANN 距离。因此它适合 不同召回方式分数尺度不一致的场景。某个 row-key 被多路召回时,通常会比只被单路召回更靠前。
WEIGHT 融合示例
1SELECT
2 COALESCE(d.id, e.doc_id) AS doc_id,
3 d.title,
4 __RANK
5FROM docs d
6HYBRID doc_embeddings e ON d.id = e.doc_id
7ORDER BY hybrid_search(
8 '{"rerank":"WEIGHT","weights":[0.4,0.6],"candidates":200}',
9 bm25_score(d.content, 'all', 'apple market cap'),
10 ann_distance(e.embedding, [0.1, 0.2, 0.3, 0.4])
11)
12LIMIT 10;
weights 数组长度必须与召回路数一致。上例中 BM25 权重为 0.4,
ANN 权重为 0.6。
WEIGHT 更依赖各路分数的可比性和权重设置。仍以上面的 row-key 对齐结果为例,如果配置
"weights":[0.2,0.8],向量路权重更高,只在向量路命中的 103 可能排到更靠前:
1+--------+--------------------+------------------+---------+
2| doc_id | title | embedding_doc_id | __RANK |
3+--------+--------------------+------------------+---------+
4| 101 | Apple market value | 101 | 0.91 |
5| 103 | NULL | 103 | 0.80 |
6| 104 | Finance overview | 104 | 0.55 |
7| 102 | Apple product news | NULL | 0.18 |
8+--------+--------------------+------------------+---------+
上表的分数用于说明排序形态;真实 __RANK 会由每路召回分数、距离归一化和权重共同决定。
如果无法确定各路分数是否可比,建议先使用 RRF。
三路融合示例
1SELECT
2 COALESCE(d.id, e.doc_id, t.doc_id) AS doc_id,
3 d.title,
4 t.tag,
5 __RANK
6FROM docs d
7HYBRID doc_embeddings e ON d.id = e.doc_id
8HYBRID doc_tags t ON d.id = t.doc_id
9ORDER BY hybrid_search(
10 '{"rerank":"RRF","candidates":100}',
11 bm25_score(d.content, 'any', 'apple'),
12 ann_distance(e.embedding, [0.1, 0.2, 0.3, 0.4]),
13 bm25_score(t.tag, 'any', 'finance')
14)
15LIMIT 10;
Reranker 模型精排
Fusion + Rerank
在融合召回结果之后,可以追加 reranker 对象启用模型精排:
1SELECT
2 COALESCE(d.id, e.doc_id) AS doc_id,
3 d.title,
4 d.content,
5 __RANK
6FROM docs d
7HYBRID doc_embeddings e ON d.id = e.doc_id
8ORDER BY hybrid_search(
9 '{
10 "rerank": "RRF",
11 "c": 60,
12 "candidates": 50,
13 "reranker": {
14 "model": "jina-reranker-v2-base-multilingual",
15 "query": "apple market cap",
16 "text_columns": ["d.title", "d.content"],
17 "text_join": "\n"
18 }
19 }',
20 bm25_score(d.content, 'any', 'apple market cap'),
21 ann_distance(e.embedding, [0.1, 0.2, 0.3, 0.4])
22)
23LIMIT 10;
执行过程:
1多路召回 -> RRF/WEIGHT 融合取 top candidates -> 调用 Reranker -> 按模型分数排序 -> LIMIT
启用 Reranker 后,__RANK 表示模型返回的相关性分数,或后处理后的最终分数。
Pure Rerank
Pure Rerank 不做 BM25/ANN 召回,只对上游 SQL 结果精排。适合业务已通过 JOIN、CTE、UNION、过滤等方式构造候选集的场景。
1SELECT id, title, content, author_name, __RANK
2FROM (
3 SELECT
4 d.id,
5 d.title,
6 d.content,
7 a.name AS author_name
8 FROM docs d
9 JOIN authors a ON d.author_id = a.id
10 WHERE d.tenant_id = 100
11 ORDER BY d.publish_time DESC
12 LIMIT 200
13) candidate
14ORDER BY hybrid_search(
15 '{
16 "candidates": 100,
17 "reranker": {
18 "model": "jina-reranker-v2-base-multilingual",
19 "query": "apple market cap",
20 "text_columns": ["title", "content", "author_name"],
21 "text_join": "\n"
22 }
23 }'
24)
25LIMIT 10;
Pure Rerank 规则:
hybrid_search()只有一个 JSON 参数,不带bm25_score()或ann_distance()。- JSON 必须包含
reranker对象。 candidates控制送入 Reranker 的最大候选行数;如果上游行数更多,会按上游输入顺序截取。
JSON 参数说明
顶层参数
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
rerank |
string | Fusion 模式填写,Pure Rerank 不填 | - | 融合方式,支持 RRF、WEIGHT |
c |
number | 否 | 60 |
RRF 参数,只能与 "rerank":"RRF" 搭配 |
weights |
number[] | WEIGHT 模式必填 | - | 各路权重,长度必须等于召回路数 |
candidates |
int | 否 | 见下方说明 | 候选数量上限,范围 [1, hybrid_search_reranker_max_candidates],当前最大默认 4000 |
reranker |
object | 使用模型精排时必填 | - | Reranker 配置 |
注意:
未显式配置 candidates 时,Pure Rerank 和带 Reranker 的融合查询使用默认值 10。多表纯 Fusion 查询会至少使用
max(LIMIT, hybrid_search_reranker_default_candidates) 作为每路召回规模。生产查询建议显式配置
candidates,便于稳定控制质量和延迟。
reranker 参数
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
model |
string | 是 | - | 传给 Reranker 服务的模型名 |
query |
string | 是 | - | 用户查询文本 |
text_columns |
string[] | 是 | - | 参与精排的文本列,类型必须为 VARCHAR、CHAR 或 STRING |
text_join |
string | 否 | \n |
多列文本拼接分隔符 |
text_columns 在多表 HYBRID 中可以写限定名,例如 d.content;
如果列名在多路中不唯一,建议使用限定名。Pure Rerank 的上游如果存在同名列,
建议在子查询中先用 alias 消歧。
WHERE 与 SELECT 注意事项
多表 HYBRID 的 WHERE 谓词会按列归属路由到对应 召回路:
1SELECT COALESCE(d.id, e.doc_id) AS doc_id, d.title
2FROM docs d
3HYBRID doc_embeddings e ON d.id = e.doc_id
4WHERE d.title LIKE 'A%' -- 路由到 docs 路
5 AND e.doc_id BETWEEN 1 AND 1000
6ORDER BY hybrid_search(
7 '{"rerank":"RRF"}',
8 bm25_score(d.content, 'any', 'apple'),
9 ann_distance(e.embedding, [0.1, 0.2, 0.3, 0.4])
10)
11LIMIT 10;
限制:
- 不支持跨召回路的条件。
WHERE中不能引用__RANK、__SCORE_*等虚拟列。- 多表输出中,未命中某一路的业务列可能为
NULL,需要按业务语义处理。
常见错误与修正
| 错误写法 | 原因 | 修正方式 |
|---|---|---|
INNER HYBRID / LEFT HYBRID |
HYBRID 没有 JOIN 修饰符 |
直接写 HYBRID |
ORDER BY hybrid_search(...) ASC |
只支持降序 | 使用默认排序或 DESC |
| score 表达式个数少于召回路数 | 每路必须有一个召回分数 | 按召回路顺序补齐 score 表达式 |
| 第 1 个 score 引用第 2 路表 | score 表达式不能跨路 | 调整 score 表达式顺序 |
{"reranker":{"candidates":100}} |
candidates 位置错误 |
改为 {"candidates":100,"reranker":{...}} |
text_columns 引用数值列 |
Reranker 只接受文本列 | 在子查询中 CAST 为文本并起 alias |
推荐实践
- 优先使用 RRF 作为默认融合策略;只有明确知道各路召回分数可比或有调参依据时,再使用 WEIGHT。
candidates应大于最终LIMIT;候选过小会影响召回质量,过大会增加召回和 Reranker 延迟。- Pure Rerank 上游候选集较大时,先在子查询中用业务过滤、时间排序或轻量召回缩小候选,再送入 Reranker。
- 多表
HYBRID的 row-key 类型和数量要保持一致;不同表的主键类型不一致时,建议在建表或子查询投影中统一类型。 - 生产环境建议保留
reranker_fallback_on_failure=true,并通过监控 Reranker 请求失败和降级指标观察服务质量。
评价此篇文章
