5.0 把 percolator 的存储从"保留类型
.percolator"改成"字段类型percolator",并让它变成近实时:注册查询后要等 refresh 才可见。percolate / multi percolate API 废弃,改用能放进 search / msearch 的percolate查询。同一节还有几条容易被漏掉的:percolate 不再改 mapping,percolator 文档会被普通搜索返回,含now的 range 查询不再被接受,脚本语言默认值变了,percolate 统计删除,percolator 变成独立模块(Java 客户端要加依赖并加载插件)。本文按 v5.6.16 逐条核对,并指出词项抽取这个机制在源码里对应什么。
说明
实测环境:本地起的 Elasticsearch 5.6.16 单节点实例(官方 tar 包直接跑,堆内存 512MB,机器学习关闭),curl 连本地 9200 端口。本文「实测」小节的命令和输出都出自这个实例。
来源和边界先交代:本文的依据出自 v5.6.16 标签下的官方文件(percolator.asciidoc、PercolatorFieldMapper.java、IndexSettings.java),文中引用的原文均来自这些文件。示例取自官方示例;哪些是文档写明、哪些是我的推断,正文里逐处标注。
图 1:官方文件 percolator.asciidoc(v5.6.16 标签)。来源:GitHub elastic/elasticsearch v5.6.16 percolator.asciidoc
官方说明
5.0 迁移文档 Percolator 一节(migrate_5_0/percolator.asciidoc 第 4–11 行):
Previously percolators were activated in real-time, i.e. as soon as they were indexed. Now, changes to the
percolatequery are visible in near-real time, as soon as the index has been refreshed. This change was required because, in indices created from 5.0 onwards, the terms used in a percolator query are automatically indexed to allow for more efficient query selection during percolation.
因果是:5.0 起 percolator 查询里用到的词项会被抽取并索引,percolate 时先用这些词项筛掉绝大多数不可能匹配的查询。既然要走索引,就得遵守索引的可见性规则——refresh 之后才可见,于是从实时变成近实时。
图 2:官方文件 PercolatorFieldMapper.java(v5.6.16 标签)。来源:GitHub elastic/elasticsearch v5.6.16 PercolatorFieldMapper.java
.percolator 类型 → percolator 字段类型
The
.percolatortype can no longer be used to index percolator queries. Instead apercolatorfield type must be configured prior to indexing percolator queries.
Indices with a
.percolatortype created on a version before 5.0.0 can still be used, but new indices no longer accept the.percolatortype.
1.0 时 percolator 从专用索引 _percolator 改成索引内的保留类型 .percolator(见 1.0 里 percolator 的重做);5.0 再往前一步,改成在 mapping 里声明一个 percolator 类型的字段:
PUT my_index
{
"mappings": {
"my_type": {
"properties": {
"query": { "type": "percolator" },
"title": { "type": "text" }
}
}
}
}
注册查询就是往这个字段索引一篇文档:
PUT my_index/my_type/1
{
"query": { "match": { "title": "quick brown fox" } }
}
老索引(5.0 之前建带 .percolator 类型的)还能继续用,新索引不接受 .percolator 类型。
我的建议是按官方说的重建:
However it is strongly recommended to reindex any indices containing percolator queries created prior upgrading to Elasticsearch 5. By doing this the
percolatequery utilize the extracted terms thepercolatorfield type extracted from the percolator queries and potentially execute many times faster.
"可能快很多倍"是官方的原话(potentially execute many times faster),没有给数字。
图 3:官方文件 IndexSettings.java(v5.6.16 标签)。来源:GitHub elastic/elasticsearch v5.6.16 IndexSettings.java
词项抽取在源码里是什么
官方只说"查询里的词项被自动索引",源码能看到具体形态(PercolatorFieldMapper 第 89–107 行):percolator 字段会带一个名为 extracted_terms 的 keyword 子字段,用来存从查询里抽取出的词项;percolate 时先用 TermInSetQuery 在这个子字段上做候选筛选(第 198–217 行),只有候选通过的查询才会真正跑一遍匹配。
所以"可能快很多倍"的来源是:多数注册查询根本不用执行,一次集合查询就被筛掉了。也正因为代价是"每条注册查询都要额外索引一份词项",注册侧的写入成本会上升——这一点官方没有量化。
percolate API → percolate 查询
Percolator and multi percolate APIs have been deprecated and will be removed in the next major release. These APIs have been replaced by the
percolatequery that can be used in the search and multi search APIs.
调用方式从专用端点变成普通搜索里的一个查询:
GET my_index/_search
{
"query": {
"percolate": {
"field": "query",
"document": { "title": "quick brown fox" }
}
}
}
好处是它能和别的查询、聚合、排序组合(比如先按业务字段过滤 percolator 查询集合,再 percolate),也能放进 msearch 一次发多个。
被废弃的端点在 2.x 的 REST API 定义里是 /{index}/{type}/_percolate 和 /_mpercolate。
percolate 不再改 mapping
The
percolatequery no longer modifies the mappings. Before the percolate API could be used to dynamically introduce new fields to the mappings based on the fields in the document being percolated. This no longer works, because these unmapped fields are not persisted in the mapping.
以前 percolate 一个文档时,文档里出现的新字段会被动态加进 mapping。5.0 起这些字段不持久化,所以不会再改 mapping。
后果是:被 percolate 的文档里如果有 mapping 中不存在的新字段,percolator 查询引用这些字段时匹配不到。正确做法是字段先在 mapping 里定义好。
percolator 文档会被搜索返回
Documents with the
.percolatetype were previously excluded from the search response, unless the.percolatetype was specified explicitly in the search request. Now, percolator documents are treated in the same way as any other document and are returned by search requests.
以前带 .percolate 类型的文档默认不出现在搜索结果里;5.0 起它们就是普通文档,会被搜出来。
实际影响:对某个索引做 match_all 的全量扫描(导出、重建之类)会多出一批 percolator 查询文档。要排除就在查询里显式过滤掉该字段或类型。
同一节里另外四条
这几条原文没写,同样属于这次迁移的范围:
- percolate 已有文档时不能再带 source(第 45–49 行):用已有文档 percolate 时,同时在
percolate查询里指定 document source 不再被允许,以前允许且会被忽略; - percolate 统计删除(第 51–53 行):因为 percolator 不再缓存查询;
- 含
now的 range 查询不再被接受(第 55–58 行):percolator 查询里不能再用基于当前时间的 range; - 脚本的默认语言变了(第 60–74 行):percolator 查询里含脚本(
script查询、function_score的脚本函数)且没写lang的,5.0 起按 Painless 解析。存在 5.0 之前创建的索引里的这类查询,沿用script.legacy.default_lang的值,该设置默认是 Groovy。重建索引时官方给了两条路:把脚本迁到 Painless(推荐),或者显式写上lang。
最后一条和 5.0 换默认脚本语言是同一件事,只是这里指出了它落在 percolator 上的具体后果:老索引里存的查询脚本,在重建前后可能按不同语言解析。
percolator 变成独立模块
The percolator is no longer part of the core elasticsearch dependency. It has moved to the percolator module. Therefor when using the percolator feature from the Java client the new percolator module should also be on the classpath. Also the transport client should load the percolator module as plugin.
迁移文档给的示例是 TransportClient.builder().addPlugin(PercolatorPlugin.class).build()(第 76–90 行)。注意这段示例用的是 5.0 之前的构造写法,5.0 起 transport client 的构造换成了 PreBuiltTransportClient——它本身已经预装了 PercolatorPlugin。所以按 5.0 的写法,不需要再单独 add 这个插件,但 percolator 模块要在 classpath 上。这一段是本文按两处文档推断的,没有实测,落地时按你所用版本的 Java 客户端文档确认。
反查查询变成一等公民
把 0.90 → 1.0 → 5.0 三步连起来看,percolator 的位置一直在往"索引内部的普通能力"移动:
| 版本 | 存储位置 | 用法 |
|---|---|---|
| 0.90 | 专用索引 _percolator + 专用 API | 一套独立的机制 |
| 1.0 | 索引内的保留类型 .percolator | 跟业务数据同索引,但仍是特殊类型 |
| 5.0 | mapping 里的 percolator 字段类型 + 普通查询 | 就是一个字段、一个查询 |
每一步都在去掉特殊性。到 5.0,注册查询就是写一个文档(和写业务数据没区别),匹配文档就是一次普通搜索(和其它查询没区别)。
带来的两个实际好处:
- 运维统一:不需要为 percolator 单独维护索引、单独的 API、单独的权限模型;
- 能用 ES 的全部能力:既然是普通搜索,聚合、过滤、分页、排序都能用;既然是普通文档,副本、快照都照常生效。
代价是失去实时性(要走索引、遵守 refresh 语义),换来的可维护性和词项抽取带来的候选筛选。
近实时这一点怎么适应
从实时变成近实时,是这次改动里对业务影响最直接的一条。默认 index.refresh_interval 是 1 秒(IndexSettings.java 第 108 行),所以注册查询后默认最多 1 秒可见。
- 能容忍延迟的(多数场景):告警规则、分类打标这类够用,不会要求"规则刚创建就立刻生效";
- 要求立即可见的:写入时带
?refresh=true,或者调小refresh_interval。代价是更频繁的 refresh 会增加 segment 数量、影响索引吞吐。我的建议是只对注册查询的请求加 refresh,别改整个索引的refresh_interval:
# 注册 percolator 查询时显式刷新(5.x 要带 type)
curl -XPOST 'localhost:9200/my_index/my_type/1?refresh=true' -H 'Content-Type: application/json' -d '{
"query": { "match": { "title": "error" } }
}'
这样普通数据写入仍走默认节奏,只有注册查询这类低频操作付出 refresh 代价。
- 代码里别再假设"注册即可查":以前注册完立刻 percolate 一定能匹配到,现在可能匹配不到。代码里有"注册后立即验证"的逻辑(比如单元测试),要么加 refresh,要么改成轮询等待。这类测试在升级后会变成偶发失败(取决于 refresh 时机),排查起来费时间。
升级 5.0 的处理顺序
- 为 percolator 新建索引,mapping 里声明
percolator类型字段; - 把老索引里的 percolator 查询 reindex 过去(这一步能拿到词项抽取的性能收益);
- 代码里
/{index}/{type}/_percolate、/_mpercolate改成 search / msearch +percolate查询; - 检查有没有依赖"percolate 会动态加字段"的行为,改成预先定义 mapping;
- 全量扫描类请求加上对 percolator 文档的排除条件;
- 检查 percolator 查询里有没有基于
now的 range,有就要改; - percolator 查询里的脚本显式写
lang,或借重建迁到 Painless; - Java 项目确认 percolator 模块在 classpath 上;
- 接受近实时语义:注册查询后如需立即可见就显式 refresh。
在 5.6.16 上跑一遍
注册一条 percolator 查询,然后分别在 refresh 前后各查一次:
图 4:本地 5.6.16 单节点实例上的实测输出。上:注册完立刻查;下:refresh 之后查
curl -s -X PUT "http://localhost:9200/perc" \
-H 'Content-Type: application/json' \
-d '{"mappings":{"doc":{"properties":{"message":{"type":"text"},"query":{"type":"percolator"}}}}}'
curl -s -X POST "http://localhost:9200/perc/doc?refresh=false" \
-H 'Content-Type: application/json' -d '{"query":{"match":{"message":"hello"}}}'
curl -s -X POST "http://localhost:9200/perc/_search?filter_path=hits.total" \
-H 'Content-Type: application/json' \
-d '{"query":{"percolate":{"field":"query","document_type":"doc","document":{"message":"hello world"}}}}'
注册完立刻查返回 {"hits":{"total":0}};POST /perc/_refresh 之后再查返回 {"hits":{"total":1}}。
这就是 percolator 变成字段类型之后的直接后果:注册的查询是普通文档,走的是和普通写入一样的可见性规则。写入返回 201 不等于能查到——它只说明文档进了 Lucene,要等 refresh 之后才对搜索可见。
结论是:把 percolator 用在"注册完马上要能用"的流程里(比如实时规则下发后立刻匹配),必须自己加 refresh,或者接受一个 refresh_interval 的延迟。这是 5.0 之后才有的坑,2.x 的 percolator 是独立的注册接口,没有这层可见性问题。
写在最后
percolator 从特殊类型变成普通字段类型,看着是简化,实际是把一个特例收进了常规体系——之后它能用普通索引的 mapping、分片、副本这些能力。
代价是用法变了,存量的 .percolator 索引要重建。这类"特例常规化"的改动在 ES 各版本里反复出现,遇到时按"要重建索引"来准备通常不会错。
近实时这条最容易被忽略:它不报错,只是"刚注册的查询偶尔匹配不到"。升级前把"注册后立即验证"的代码找出来,比事后排查偶发失败便宜。
参考
本文对照 Elasticsearch
v5.6.16标签的官方文档核对,引文逐字取自原文。图 4 的 refresh 前后对照来自本地 5.6.16 单节点实例。
