Elasticsearch ES|QL:让全文本搜索无处不在

MATCH 与 TO_TEXT 的组合 🚀

在从未建立索引的数据上享受真正的全文本搜索 ------ 计算列、未映射字段、实时拼接的字符串,甚至来自 S3 的联合数据,统统不在话下。


📌 引言:

搜索的"最后一公里"难题。

Elasticsearch 以强大的倒排索引著称,但索引并非万能。在实际工作中,我们经常遇到这样的场景:

  • 某个字段只在极少数的故障排查时才需要搜索(例如堆栈跟踪),平时索引它只会浪费磁盘和堆内存;
  • 关键字字段(keyword)被大量用于聚合和排序,但突然有一天你想对它做"分词"搜索;
  • 跨多个索引查询时,同一个字段在不同索引中类型不一致(有的 text,有的 keyword),甚至有的索引根本没映射该字段。

过去,面对这些情况,你只能:

  • 重新索引(耗时耗钱),或者
  • 退而求其次,使用 LIKERLIKE 做简单的模式匹配,却忍受着大小写、词边界、词干等问题带来的漏报和误报。

现在,Elasticsearch 9.5(技术预览版)和 Elastic Cloud Serverless 带来了一对"黄金搭档"------ MATCH + TO_TEXT,让以上所有烦恼迎刃而解。🎉


🧩 核心问题:ES|QL 中为何需要真正的全文本搜索?

ES|QL 是 Elasticsearch 的新一代查询语言,它允许你像流水线一样组合命令(FROMEVALWHEREKEEP ...)。它本身已经支持 LIKE(通配符)和 RLIKE(正则表达式)来搜索字符串,但这两种方式都只是字符层面的模式匹配,并不理解"词语"的概念。

举个例子:你想在日志中查找包含单词 "fox" 的消息。

  • LIKE "*fox*" 会漏掉 "Fox"(大小写),却误匹配 "Outfoxed"(子字符串)和 "FOXTROT" ------ 完全不是你要的狐狸。
  • RLIKE 可以勉强解决大小写,但词边界问题(fox,fox!fox?)会让正则表达式变得又臭又长,维护起来如同噩梦。

真正的全文本搜索(full‑text search)会:

  • 将查询词和待搜索内容都通过分析器(analyzer)处理;
  • 分词、小写化、去除停用词、提取词干(stemming);
  • 然后进行词元(token)对词元的匹配,保证只匹配完整的单词且忽略大小写和标点。

这正是 MATCH 的拿手好戏。而 TO_TEXT 则负责把任何字符串"变成"可分析的文本,即使它从未被映射为 text 类型。


🔧 解决方案:MATCH + TO_TEXT 组合技

1. TO_TEXT ------ 将任何字符串"文本化"

TO_TEXT 是 ES|QL 中首个生成 text 类型 的转换函数。在这之前,所有由 ES|QL 表达式生成的字符串都是 keyword 类型(即原样精确比较)。TO_TEXT(x) 明确告诉 ES|QL:"请把这个值当作全文本对待,后续要用分析器处理它。"

2. MATCH ------ 现在接受任何表达式作为第一个参数

过去,MATCH 的第一个参数必须是已映射的 text 字段,才能利用倒排索引。现在,它可以接受任何表达式:

  • EVAL 生成的列;
  • 内联函数结果(如 CONCAT);
  • 直接从源文档加载的未映射字段 (需配合 SET unmapped_fields="load")。

这样一来,你可以在查询生命周期内临时创建"文本",并立即对它进行全文本搜索。

一个简单的例子 🌰

esql 复制代码
FROM cooking_blog
| EVAL summary = TO_TEXT(CONCAT(title, description))
| WHERE MATCH(summary, "pancakes")
| KEEP title, author

解释

  • CONCAT(title, description) 拼接两个字段;
  • TO_TEXT 将其转为文本类型;
  • MATCH 对临时生成的 summary 执行全文本搜索,匹配含 pancakes 的文档。
  • 整个过程无需任何预先索引或映射配置。

⚙️ 运行时分析 vs. 倒排索引

MATCH 在表达式上执行时,完全绕过了 Lucene 的倒排索引 ,而是在查询时逐行分析。它的工作流程如下:
#mermaid-svg-pqAirDA4HBInVDC3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-pqAirDA4HBInVDC3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-pqAirDA4HBInVDC3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-pqAirDA4HBInVDC3 .error-icon{fill:#552222;}#mermaid-svg-pqAirDA4HBInVDC3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-pqAirDA4HBInVDC3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-pqAirDA4HBInVDC3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pqAirDA4HBInVDC3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pqAirDA4HBInVDC3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-pqAirDA4HBInVDC3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pqAirDA4HBInVDC3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pqAirDA4HBInVDC3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-pqAirDA4HBInVDC3 .marker.cross{stroke:#333333;}#mermaid-svg-pqAirDA4HBInVDC3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-pqAirDA4HBInVDC3 p{margin:0;}#mermaid-svg-pqAirDA4HBInVDC3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-pqAirDA4HBInVDC3 .cluster-label text{fill:#333;}#mermaid-svg-pqAirDA4HBInVDC3 .cluster-label span{color:#333;}#mermaid-svg-pqAirDA4HBInVDC3 .cluster-label span p{background-color:transparent;}#mermaid-svg-pqAirDA4HBInVDC3 .label text,#mermaid-svg-pqAirDA4HBInVDC3 span{fill:#333;color:#333;}#mermaid-svg-pqAirDA4HBInVDC3 .node rect,#mermaid-svg-pqAirDA4HBInVDC3 .node circle,#mermaid-svg-pqAirDA4HBInVDC3 .node ellipse,#mermaid-svg-pqAirDA4HBInVDC3 .node polygon,#mermaid-svg-pqAirDA4HBInVDC3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pqAirDA4HBInVDC3 .rough-node .label text,#mermaid-svg-pqAirDA4HBInVDC3 .node .label text,#mermaid-svg-pqAirDA4HBInVDC3 .image-shape .label,#mermaid-svg-pqAirDA4HBInVDC3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-pqAirDA4HBInVDC3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-pqAirDA4HBInVDC3 .rough-node .label,#mermaid-svg-pqAirDA4HBInVDC3 .node .label,#mermaid-svg-pqAirDA4HBInVDC3 .image-shape .label,#mermaid-svg-pqAirDA4HBInVDC3 .icon-shape .label{text-align:center;}#mermaid-svg-pqAirDA4HBInVDC3 .node.clickable{cursor:pointer;}#mermaid-svg-pqAirDA4HBInVDC3 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-pqAirDA4HBInVDC3 .arrowheadPath{fill:#333333;}#mermaid-svg-pqAirDA4HBInVDC3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-pqAirDA4HBInVDC3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-pqAirDA4HBInVDC3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pqAirDA4HBInVDC3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-pqAirDA4HBInVDC3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pqAirDA4HBInVDC3 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-pqAirDA4HBInVDC3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-pqAirDA4HBInVDC3 .cluster text{fill:#333;}#mermaid-svg-pqAirDA4HBInVDC3 .cluster span{color:#333;}#mermaid-svg-pqAirDA4HBInVDC3 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-pqAirDA4HBInVDC3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-pqAirDA4HBInVDC3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-pqAirDA4HBInVDC3 .icon-shape,#mermaid-svg-pqAirDA4HBInVDC3 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pqAirDA4HBInVDC3 .icon-shape p,#mermaid-svg-pqAirDA4HBInVDC3 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-pqAirDA4HBInVDC3 .icon-shape .label rect,#mermaid-svg-pqAirDA4HBInVDC3 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pqAirDA4HBInVDC3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-pqAirDA4HBInVDC3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-pqAirDA4HBInVDC3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} text 类型

(来自 TO_TEXT 或索引映射)
keyword / 数值 / 日期等
词元对词元
精确比较
查询输入
MATCH 第一个参数的类型?
分析器处理值

分词、小写化、词干等
不分析,原值比较
生成词元列表
生成原生常量
匹配逻辑
若任意值词元等于任意查询词元 -> 匹配
值 == 查询常量 -> 匹配
返回匹配行

与此同时,查询字符串也会被分析成一组词元。对于每一行,ES|QL 会按照上表处理。

处理方式速查表 📊

表达式类型 处理方式 匹配行为
text (通过 TO_TEXT 或来自索引映射) 分析器将值分词、小写化等 词元对词元比较:任意值词元 等于 任意查询词元 即匹配(OR 语义)
keywordIP日期数值 不分析,查询常量转换为原生类型 每行精确比较(值 == 常量)
未映射字段(加载为 keyword) 若包裹 TO_TEXT,则按 text 处理;否则按 keyword 精确比较 同上

💡 重点 :运行时 MATCH 的语义与针对索引字段下推给 Lucene 的 match 查询完全一致,只是性能不同 ------ 倒排索引在查询时几乎不扫描文档,而运行时分析需要逐行计算。但它的灵活性是无价的。


🎯 实战用例

以前无法搜索的数据,现在轻松搞定

1️⃣ 搜索未映射字段(无需添加映射)

你有大量日志,其中 stack_trace 字段从未映射(因为平时不需要搜索,索引它太浪费)。现在要排查 NullPointerException

esql 复制代码
SET unmapped_fields="load";
FROM app_logs
| WHERE MATCH(TO_TEXT(stack_trace), "java.lang.NullPointerException")
| KEEP @timestamp, service.name, message
  • SET unmapped_fields="load" 告诉 ES|QL 从 _source 中加载所有未映射字段(作为 keyword)。
  • TO_TEXT(stack_trace) 将其转为 text,然后 MATCH 进行分词匹配。
  • 仅当需要时才付出计算成本,日常映射保持轻量。

2️⃣ 关键字字段的"临时"全文本搜索(无需重新索引)

product_name 被映射为 keyword 用于聚合和排序,但你现在想搜索"wireless noise cancelling headphones" ------ 希望分词匹配,而不是精确匹配。以前只能重建索引,现在:

esql 复制代码
FROM products
| WHERE MATCH(TO_TEXT(product_name), "wireless noise cancelling headphones")
| KEEP product_name, brand, price

TO_TEXT 让 keyword 值在查询时被分析,从而支持全文本匹配。如果日后该搜索成为高频操作,再考虑调整映射也不迟。


3️⃣ 跨索引、不同映射的字段统一搜索

假设 2025 年的索引中 messagekeyword,2026 年的索引中是 text。你想跨两个索引搜索 "connection reset":

esql 复制代码
FROM logs-2025, logs-2026
| EVAL msg = TO_TEXT(message)
| WHERE MATCH(msg, "connection reset")

TO_TEXT 统一了类型,无论底层的原始类型如何,查询时都会被分析,匹配结果一致。

更进一步,如果 error_details 只在部分索引中映射,而另一些索引中未映射,你可以这样:

esql 复制代码
SET unmapped_fields="load";
FROM logs-2025, logs-2026
| WHERE MATCH(TO_TEXT(error_details), "timeout")

ES|QL 会自动检测到该字段可能未映射,并逐行评估 整个 MATCH,而不是尝试下推 Lucene。你无需关心哪些索引有该字段,查询总能返回正确结果。


🆚 为什么 MATCH 胜过 LIKE 和 RLIKE?

特性 LIKE(通配符) RLIKE(正则) MATCH + TO_TEXT
大小写不敏感 ❌ 需额外处理 ✅ 可做到,但麻烦 ✅ 自动(通过分析器)
词边界识别 ❌ 无法区分单词 ⚠️ 可做到,但表达式复杂 ✅ 自动分词
词干提取(stemming) ✅(待支持多语言分析器)
停用词处理
同义词支持 ✅(未来支持)
可读性 ✅ 简单 ❌ 复杂易错 ✅ 简洁自然
性能 较慢(扫描) 更慢(正则引擎) 逐行分析,比正则快

LIKERLIKE 是"字符游戏",而 MATCH 是"语义理解" ------ 前者适合简单过滤,后者适合真正的用户搜索场景。


🚧 当前限制与未来路线图

作为技术预览版(Elasticsearch 9.5),目前存在一些限制,正在积极解决:

限制 说明 计划
相关性得分 运行时 MATCH 不影响 _score 正在开发中,未来将支持
查询选项 暂不支持模糊性(fuzziness)、操作符(operator)等 计划添加
分析器配置 默认使用标准分析器,不可自定义 即将支持 36 种语言分析器
短语匹配 目前只有 MATCH,没有 MATCH_PHRASE MATCH_PHRASE 已在 Serverless 中可用,9.6 进入 Stack
向量搜索 未来将支持对运行时 dense_vector 执行 kNN

📌 如果搜索成为日常高频操作,仍然建议将字段索引为 text 以利用倒排索引的性能优势;而 TO_TEXT 则为你提供了"临时"或"低频"场景下的绝佳解决方案。


相关推荐
智购科技自动贩卖机1 小时前
自动售货机嵌入式系统安全加固实践:从Go语言国密算法到硬件防拆的工程化落地
大数据·人工智能·后端·安全·golang·系统安全
Elasticsearch1 小时前
一字段,一份副本:Elasticsearch 列式存储如何丢弃倒排索引
elasticsearch
TDengine (老段)1 小时前
TDgpt 概览 — AI 增强的时序数据库
大数据·数据库·人工智能·物联网·时序数据库·iot·tdengine
IT研究室1 小时前
最新大数据毕业设计选题推荐-基于大数据的城市空气污染物浓度数据分析与可视化-大数据-Spark-Hadoop-Bigdata
大数据·数据分析·课程设计
LIXIUPING20141 小时前
微服务分布式日志环境完整搭建实战|从服务改造到 ELK 集群部署、全流程可落地
elk·elasticsearch·logstash·filebeat·分布式日志
starzy19903 小时前
Flink基础之Session-Cluster原理详解:共享集群的利与弊
大数据·flink
starzy19904 小时前
Flink基础之Per-Job-Cluster原理详解:被 Application 取代的模式
java·大数据·flink
IT界的老黄牛10 小时前
Jenkins 监控一条龙:接入、看板 9964、11 条告警规则直接抄走
jenkins·grafana·prometheus·监控·ci-cd·告警规则
大大大大晴天11 小时前
每天认识一个组件:数据治理Apache Atlas
大数据