ES|QL|全文函数能写进 OR 了,同字段多词还是别这么写

举报
岳涛 发表于 2026/11/09 08:31:59 2026/11/09
【摘要】 9.0 之前,ES|QL 里 MATCH(或其它全文函数)不能出现在 OR 里——写了会报错。

9.0 之前,ES|QL 里 MATCH(或其它全文函数)不能出现在 OR 里——写了会报错。9.0 分两步放开了这个限制。这个限制为什么存在,比"现在能用了"更值得知道。

说明

实测环境:本地起的 Elasticsearch 9.4.2 单节点实例(官方 tar 包直接跑,堆内存 512MB,安全与机器学习关闭),curl 连本地 9200 端口,_query 接口走 ES|QL;测试文档在实验时现场写入。图 4 出自这个实例。

来源和边界先交代:本文的依据出自 v9.4.2 标签下的官方文件(9.0.0.yml、EsqlCapabilities.java),文中引用的原文均来自这些文件。示例取自官方示例;哪些是文档写明、哪些是我的推断,正文里逐处标注。

01-9.0.0

图 1:官方文件 9.0.0.yml(v9.4.2 标签)。来源:GitHub elastic/elasticsearch v9.4.2 9.0.0.yml

官方条目

9.0 的 changelog 里有两条相关条目。

放开限制(9.0.0.yml 第 1585–1588 行 @ v9.4.2):

ESQL - Allow full text functions disjunctions for non-full text functions

加打分(9.0.0.yml 第 2023 行 @ v9.4.2):

ES|QL - Add scoring for full text functions disjunctions

两条都只有一行 summary。下面的内容来自源码。

02-EsqlCapabilities

图 2:官方文件 EsqlCapabilities.java(v9.4.2 标签)。来源:GitHub elastic/elasticsearch v9.4.2 EsqlCapabilities.java

先说什么是析取

析取(disjunction) 就是逻辑上的"或"(OR)。

在 ES|QL 里大致长这样:

FROM logs
| WHERE MATCH(message, "timeout") OR MATCH(message, "refused")

这条查询在 9.0 之前是不合法的。

为什么以前不行

这段是理解,官方没解释过,但源码里的注释支持这个推测。

原因在全文检索和普通过滤的执行方式不一样。

普通条件(=、<、范围、term 之类)是"是或否"的:每条文档要么符合,要么不符合。这类条件可以自由组合,AND、OR 都行。

全文函数不一样。它返回的不只是"是否符合",还有一个相关度分数。MATCH(message, "timeout") 告诉你"符合,且相关度 0.73"。

问题来了:OR 两边都有分数,合并之后的分数怎么算?

  • 取大的?(max)
  • 相加?(sum)
  • 还是别的?

这不是一个有唯一正确答案的问题——不同的合并方式会给出不同的排序结果。而 ES|QL 的早期实现里,这块没有定论,所以直接禁止。

这是稳妥的做法:宁可报错,也不要给出一个"看起来能用但排序说不清"的行为。报错你能立刻发现;排序不对则要很久才被察觉。

03-esql-search-tutorial.md

图 3:官方页面 esql-search-tutorial.md(v9.4.2 标签)。来源:GitHub elastic/elasticsearch v9.4.2 docs/reference/query-languages/esql/esql-search-tutorial.md

两条条目的区别

源码里有两个 capability,注释把它们说清楚了(EsqlCapabilities.java 第 1019–1022 行、第 1039–1042 行 @ v9.4.2):

/**
 * Full text functions can be used in disjunctions
 */
FULL_TEXT_FUNCTIONS_DISJUNCTIONS,
/**
 * Full text functions can be used in disjunctions as they are implemented in compute engine
 */
FULL_TEXT_FUNCTIONS_DISJUNCTIONS_COMPUTE_ENGINE,

两条注释的区别在第二句:有没有 “as they are implemented in compute engine”。

理解是分了两步:

  • 第一步(第一个 capability):先支持,可能是在某个执行路径上实现;
  • 第二步(带 _COMPUTE_ENGINE):改成在计算引擎里实现——这是更通用的实现位置,不再依赖能否翻译成 Lucene 查询。

(说明:这两步的先后顺序和具体差异是推断。从 capability 的注释和命名判断它们是两个阶段,但未查证各自的引入版本和切换时机。)

"在计算引擎实现"是条演进线索

这个词值得解释一下,因为它是理解 ES|QL 演进的线索。

ES|QL 有两种执行路径:

  • 能下推给 Lucene 的:把查询翻译成 Lucene 查询,交给倒排索引执行。快,但只能表达 Lucene 支持的操作;
  • 走计算引擎(compute engine)的:ES 自己实现的列式计算。通用,什么都能算,但不一定能利用倒排索引。

早期 ES|QL 的很多功能依赖"下推到 Lucene"。好处是快,坏处是遇到 Lucene 表达不了的结构就无能为力——而全文函数的 OR 组合恰好可能属于这类。

改到计算引擎里实现,意味着不再依赖"能不能翻译成 Lucene 查询"。这是从"能用"走向"通用"的一步。

(说明:这段关于两条执行路径的描述属 ES|QL 的通用架构认知,未逐个核实这条改动具体走了哪条路径。)

报错长什么样,以及怎么绕过

既然 9.0 之前会报错,那如果你在老版本上遇到,会看到什么?这一点未实测,无法给出原文。但根据 ES|QL 的报错习惯,大概率是一句"全文函数不支持在此处使用"之类的提示,而不是莫名其妙的语法错误。

老版本上想表达"命中任意一个",有三个办法:

一,一个 MATCH 里写多个词。 前面提过,这是首选:

FROM logs
| WHERE MATCH(message, "timeout refused unreachable")

MATCH 内部默认按 OR 语义处理多个词,等价于你要的 OR 链。

二,用 IN / 多值比较代替非全文部分。 如果其中一部分其实不是全文条件(比如状态码),那部分本来就支持 OR,问题只在全文那一侧。

三,升级。 如果这个需求频繁出现,升级到 9.0+ 是根本解法。

列这三个是因为"这个限制让查询写不了"是个具体的困境,而多数情况下第一条就能解决——不需要升级,也不需要绕。

这也说明了一件事:限制之所以能维持那么久,一部分原因是它的替代品足够好用。真正需要跨字段 OR 全文条件的场景,比想象中少。

现在能怎么写

放开之后,下面这类写法是合法的:

FROM logs
| WHERE MATCH(message, "timeout") OR MATCH(message, "refused")

混合普通条件也可以(这是第一条 changelog 里 “for non-full text functions” 的意思):

FROM logs
| WHERE MATCH(message, "timeout") OR status_code == 500

打分是第二条条目加的。也就是说放开语法和加上打分是两件事——先允许你写,再让它的分数可用。

(说明:上面两条查询按语法规则构造,未实际执行过;具体的函数签名和参数形式以你版本的文档为准。)

分数怎么合并

这是最想说清楚、也最没法给确定答案的一点。

OR 两边都有分数时,ES 怎么合并?按 ES 的传统,Lucene 的 BooleanQuery 在 SHOULD 子句下默认是求和(再配合协调因子等),但也有其它模式。

ES|QL 里具体采用哪种,未从源码确认,所以不写死一个结论。

如果你依赖排序结果,这里是必须实测的地方。 做法很简单:

FROM logs
| WHERE MATCH(message, "timeout") OR MATCH(message, "refused")
| SORT _score DESC
| LIMIT 10

跑一遍看排序是否符合预期。尤其是"两个词都命中"的文档排在哪——这是最能体现合并规则的地方。

(说明:_score 在 ES|QL 里的可用性未确认。ES|QL 的 SCORE 讲 SCORE 函数,那里提到过相关用法,可以参考。这段示例同样是构造的,未执行。)

和 Query DSL 的 bool 查询放一起看

如果你熟悉 Query DSL,看到这里应该会觉得眼熟——bool 查询里的 should 就是干这个的:

{
  "query": {
    "bool": {
      "should": [
        { "match": { "message": "timeout" } },
        { "match": { "message": "refused" } }
      ]
    }
  }
}

DSL 里这件事从来都是支持的,分数合并也有成熟的规则(should 下的子句分数累加,配合 minimum_should_match 等参数)。

所以这条改动本质上是在补 ES|QL 相对 DSL 的差距。

这个视角能帮你判断一件事:ES|QL 目前还不是"DSL 的完整替代"。它在逐步补齐能力,某些结构现在能写了,某些还不行。遇到写不了的,先看 DSL 里对应怎么写、ES|QL 是否已支持——换了语言不等于所有能力都在。

判断:ES|QL 的定位是查询语言层面的统一,不是要一次性搬完 DSL 的全部表达力。所以补齐会持续进行,短期内"某个结构不支持"仍会遇到。遇到时先找替代写法,比硬等支持更实际。

OR 分支多了之后性能和分数怎么变

OR 的分支数量多了之后,性能和分数会怎么变,不确定。

逻辑上,分支越多要评估的全文条件越多,开销越大。但具体增长是线性还是更差,没有数据也未实测。

建议:如果 OR 里有五六个以上的全文函数,先测一下延迟。这属于"能写不代表该这么写"的地方。

另外:同样的语义如果在 DSL 里能写出来,可以先拿 DSL 跑一遍做对照。两组结果对比着看,比单独看一边更容易判断 ES|QL 的行为是否符合预期。

该不该这么用

放开限制不等于推荐这么用。判断:

适合:

  • 两三个同义/近义词做 OR(timeout OR refused OR unreachable);
  • 全文条件和非全文条件混合;
  • 你确实需要"命中任意一个"的语义。

不太适合:

  • 一大串词做 OR —— 这通常说明你该用的是一个 MATCH 里写多个词。MATCH(message, "timeout refused unreachable") 本身就会按 OR 语义处理,而且走的是 Lucene 的原生路径,更直接也更可能更快;
  • 需要精细控制各分支权重 —— 那应该看 SCORE 或 Query DSL 的 bool 查询。

第一条特别要说:很多时候你以为需要写 OR,其实一个 MATCH 就够了。 放开限制解决的是"跨字段、跨类型"的组合需求,不是"同字段多词"。

和别的能力放开放一起看

9.x 里 ES|QL 一直在补齐能力,前面写过几篇:

篇 补齐什么
FORK 与 INLINE STATS FORK / INLINE STATS
ES|QL 的 SCORE SCORE 函数
SAMPLE 聚合 SAMPLE 函数
本篇 全文函数的 OR
time_series 里的 nested TSDB 下的 nested

共同的模式:先禁止(因为行为没定论),等想清楚了再放开,有时还分阶段放开。

对这个节奏的评价是正面的。相比"先放出去再说、后面再改行为",先禁止的成本更低——用户的错误认知一旦形成,很难纠正。报错虽然不友好,但它不会让你产生"这个排序是对的"的错觉——排序错了比报错了更难发现,这个不对称决定了先禁止是对的。

放开之后要留意什么

限制解除往往比限制存在时更需要小心——因为没有报错提醒你了。列几条容易出问题的地方。

一,结果集会变大。 OR 天生比 AND 匹配得多。原来因为写不了 OR 而用别的方式表达的查询,换过来之后命中数可能明显增加。如果下游有分页或者条数假设,先确认一下。

二,排序可能和预期不同。 前面说过分数合并规则未确认。尤其是"只命中一边"和"两边都命中"的文档谁排前面,这个顺序在不同合并规则下会不一样。依赖排序就必须实测。

三,别把它当成性能等价物。 多个全文条件 OR,和单个 MATCH 写多个词,语义接近但执行路径未必相同。同字段多词仍建议用单个 MATCH——那走的是 Lucene 最熟悉的路径。

四,升级后老查询的行为不会变。 这条改动是"放开限制",不是"改行为"。以前能跑的查询,升级后结果不变——所以不用担心升级引入回归。这点确认过性质:它是新增能力,不是修改既有行为。

在 9.4.2 上跑一遍

9.0 放开的第一件事——全文函数能进 OR——在 9.4.2 上直接可验,官方首选的写法也一起跑一遍。

04-match-in-or

图 4:上:WHERE MATCH(t, "hello") OR MATCH(t, "world") 正常执行,命中 hello world,_score 为 0.5753641724586487。下:换成 WHERE MATCH(t, "hello world"),命中同一篇,分数完全一致。本地实测输出。

curl -s -XPOST '' \
  -H 'Content-Type: application/json' \
  -d '{"query": "FROM sc1 METADATA _score | WHERE MATCH(t, \"hello\") OR MATCH(t, \"world\") | KEEP t, _score"}'
# [["hello world", 0.5753641724586487]]

curl -s -XPOST '' \
  -H 'Content-Type: application/json' \
  -d '{"query": "FROM sc1 METADATA _score | WHERE MATCH(t, \"hello world\") | KEEP t, _score"}'
# [["hello world", 0.5753641724586487]]

这个样本里两种写法连分数都一样,因为文档同时含两个词、每个 MATCH 各贡献一份。也就是说「放开 OR」带来的主要是写法自由,不是新的算分方式。9.0 之前第一种写法会在校验阶段被拒,本机没有对应版本二进制,那一层没有做运行期对照。

结论是同字段多词仍然优先写成一个 MATCH:一次全文函数调用、一条查询条件,比拆成两个 OR 分支更短,也省掉分支数量带来的执行开销(这部分本文没有从源码确认,见边界)。我的建议是把 OR 留给「不同字段、不同语义条件」的组合,同字段的多个词一律合并进一个 MATCH,这样查询意图一眼能看出来。

边界:

  • 图 4 出自 9.4.2 单节点实例,只有一篇文档且两个词都命中,分数相等是这个样本下的结果;
  • 9.0 之前「写进 OR 会报错」来自 changelog 与 capability 开关,本地没有旧版本可跑;
  • 分数合并规则、分支数量对性能的影响没有从源码确认,也没有实测。

写在最后

这条改动让 ES|QL 的全文检索能力完整了一块:MATCH 之类的函数现在能进 OR 了。

一,9.0 之前全文函数不能出现在 OR 里,9.0 放开。 放开语法和补上打分是两条独立条目,别当成一件事。

二,放开是分两步的:先允许使用,再补上打分。源码里两个 capability 的注释反映了这一点(这一步的先后顺序是推断)。

三,能写不等于该这么写。 我的默认做法是:同字段多词先用一个 MATCH,OR 留给跨字段或混合类型的组合。老版本上遇到限制,多数情况也是这一个办法就能绕过去。

另外诚实标注了四处:"为什么以前不行"是推断;两个 capability 的前后顺序与切换时机未查证;分数合并的具体规则未从源码确认;示例查询未执行。

参考

本文对照 Elasticsearch v9.4.2 标签的 changelog 与源码核对,引文与代码逐字取自原文。"以前为什么禁止"与"两个 capability 是先后两阶段"均为推断,官方未解释设计动机与顺序;执行路径的具体归属、分数合并规则、分支数量对性能的影响仍未从源码确认或实测;「在 9.4.2 上跑一遍」里的两条查询已在本机单节点实例上实际执行(OR 与单 MATCH 的命中、分数均已实测,见该节)。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。