脚本正则的定位
regexp query 只能匹配词项,遇到「跨字段组合判断」「对原文逐字符校验」「需要环视与反向引用」时只能用脚本。Painless 是 Java 语法子集,可直接写正则字面量和匹配操作符,本质调用 java.util.regex。代价明确:脚本逐文档执行,不利用倒排索引,只适合候选集已经被 term、range 过滤压到很小的情况。
基本信息
| 适用版本 | Elasticsearch 6.x 起用 Painless 替换 Groovy |
|---|---|
| 执行位置 | script query / script_fields / runtime field 等 |
| 正则接口 | /pattern/ 字面量、=~(查找)、==~(完整匹配)、java.util.regex.Pattern |
| 取值方式 | doc['f'].value 取 keyword/数值;或 params._source['f'] 取原文 |
| 缓存特性 | 脚本编译结果有缓存,匹配结果不进 query cache |
| 成本 | 对候选文档逐条求值,线性于文档数 |
典型使用场景
- 原文级校验:keyword 里存了整串 JSON、URL 或 UA,要按整体形态判断(走倒排索引做不到)
- 跨字段组合:例如 a 字段匹配某个模式且 b 字段落在某区间
- 正则带环视或反向引用:词项级 regexp 查询不支持,只能落到脚本里的 Java 正则
- 自定义打分或运行时字段:用命中结果算分、抽子串生成新字段
- 一次性数据治理:导出前统计不规范值(务必限时间范围与小索引)
Painless 脚本示例
Painless 脚本示例
{
"query": {
"bool": {
"filter": [ { "term": { "app": "gateway" } } ], # 先用索引条件压小候选集
"must": [
{
"script": {
"script": {
"lang": "painless",
"source": "def v = doc['uri.keyword'].value; if (v == null) return false; return v ==~ /\\/api\\/v[0-9]+\\/.*/;"
}
}
}
]
}
}
}
# 说明:==~ 要求整串匹配,=~ 只要求出现;两处反斜杠是 JSON 转义所致接口与语义对照
| 写法 | 语义 | 使用注意 |
|---|---|---|
| s =~ /re/ | 部分匹配:串中出现即真 | 最短语义,等价 Java Matcher.find() |
| s ==~ /re/ | 完整匹配:整串必须符合 | 等价 Java Matcher.matches(),常用于字段值校验 |
| Pattern.compile(x) | 手动编译再复用 | 每次查询都编译开销大,重复模式优先写在脚本常量或 params 外 |
| doc['f'].value | 取 doc values(keyword/数值) | text 字段默认无 doc values,直接取会报错 |
| params._source['f'] | 取 _source 原文 | 取决于 _source 是否关闭,且解析开销更高 |
| script_fields / runtime field | 把结果作为字段返回或参与查询 | runtime field 可用字段上下文,但计算同样逐文档发生 |
脚本是集群最容易被打爆的入口:正则写在脚本里会随查询反复执行,且无法被索引优化。上线前务必把候选集压到小范围、禁止直接把用户输入拼进脚本(Painless 虽为沙箱,但正则自身的回溯开销仍需自控),并用 profile 观察耗时。
版本差异:Painless 正则字面量、=~、==~ 均在较新版本可用;老版本(2.x 之前的 Groovy 脚本)写法完全不同。生产环境统一用参数化脚本(store script 或 params),可读性与复用性都更好,具体接口以官方 Painless 文档为准。
若只是「原文包含某子串」,不要用脚本:用 match_phrase 或 wildcard 更便宜;若只是固定形态匹配,用 regexp query 打 keyword 字段。脚本只留给索引层确实表达不了的条件。