Elasticsearch使用wildcard字段模糊匹配
在 Elasticsearch 中,普通 text 字段做前后通配符查询(如 *keyword*)时,必须扫描整个倒排索引词典,数据量一大就会拖垮集群。这是通配符查询最古老的问题——没有前缀或后缀作为锚点,无法利用索引的跳跃优化。
wildcard 字段类型就是为了这个场景设计的:它不是普通倒排索引,内部存储方式专门针对通配符匹配做了优化。但代价是索引体积更大、写入开销更重,这是一个取舍,不是白捡的性能提升。
以下记录从 text 迁移到 wildcard 的完整操作与避坑要点。
版本说明
本文写于 2024-12。wildcard 字段类型自 ES 7.9 引入,7.x/8.x 用法一致;_update_by_query 的 slices / wait_for_completion 等参数用法未变。
# 1. wildcard 字段为什么快
wildcard 类型的内部结构不同于 text 或 keyword:
- N-gram 索引:将字段值拆成固定长度的 n-gram(默认 3-gram),建立倒排索引用于快速筛选
- 原值存储:同时保留原始完整值,用于最终精确校验
查询时的工作流程:
- 先用 n-gram 索引快速筛掉绝大部分不可能匹配的文档(大幅缩小候选集)
- 对候选集中的文档,用原始值做精确的通配符模式匹配
例如匹配 *abc* 时,n-gram 索引先找出所有包含 abc 三元组的文档,避免全表扫描。
代价:
- 索引体积比
keyword大 20-40% - 写入速度下降(需要生成 n-gram)
- 不适合短值匹配或频繁更新的场景
# 2. 什么时候不该用 wildcard
wildcard 是专门为「两端都不确定」的场景准备的,以下情况有更好的替代方案:
| 场景 | 推荐类型 | 原因 |
|---|---|---|
前缀匹配(abc*) | keyword + prefix query | prefix query 利用 term 字典的有序性,不需要 wildcard 的开销 |
| 精确匹配(完整值) | keyword | 精确匹配只需一次字典查找 |
| 全文检索(分词匹配) | text + 分词器 | text 基于分词的倒排索引更适合自然语言搜索 |
只有当查询模式是 *keyword* 或 ?eyword 这种「前后或中间有通配符」的场景,才值得换 wildcard。
# 3. 代价怎么量化
wildcard 的索引体积代价不能靠猜,要实测验证是否值得换。
# 3.1 索引体积对比
迁移前后用 _cat/indices 对比同一索引的存储大小:
GET _cat/indices/mytestindex_2024_11?v&h=index,docs.count,store.size
或在测试索引上建两个字段(一个 keyword 一个 wildcard)灌同一批数据,用 _disk_usage 看按字段拆分的磁盘占用:
GET mytestindex/_disk_usage?run_expensive_tasks=true
响应中每个字段的 total_in_bytes 就是该字段实际占用的磁盘空间。
# 3.2 查询性能对比
同一条通配符查询分别打到 keyword 字段和 wildcard 字段,对比 took 耗时:
GET mytestindex/_search
{
"profile": true,
"query": {
"wildcard": {
"mytest_info2": "*keyword*"
}
}
}
2
3
4
5
6
7
8
9
took是总耗时(毫秒)profile输出会拆解成rewrite_time、build_scorer等阶段,看具体是哪一段慢
注意:单次 took 受缓存影响,要清缓存后多跑几次取分布:
POST mytestindex/_cache/clear
不要拿一次结果下结论。
# 3.3 回撤判据
如果实测下来 store.size 涨幅超出可接受范围,而查询模式其实只有前缀通配(abc*),退回 keyword + prefix query 更划算——不要为了用新特性而用。
# 4. 更新字段映射
在索引中添加新字段 mytest_info2,定义为 wildcard 类型:
PUT mytestindex_2024_11/_mapping
{
"properties": {
"mytest_info2": {
"type": "wildcard"
}
}
}
PUT mytestindex_2024_12/_mapping
{
"properties": {
"mytest_info2": {
"type": "wildcard"
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
注意
不能在已有字段上直接改类型,只能通过添加新字段 + 数据迁移来实现。字段类型是写入时确定并固化在段文件中的结构。
# 5. 数据迁移脚本
使用 _update_by_query API 将 mytest_info 的值复制到 mytest_info2,同时只更新目标字段为空的文档:
POST mytestindex_2024_11,mytestindex_2024_12/_update_by_query?wait_for_completion=false&slices=8
{
"script": {
"lang": "painless",
"source": "ctx._source['mytest_info2'] = ctx._source['mytest_info']"
},
"query": {
"bool": {
"must_not": [
{
"exists": {
"field": "mytest_info2"
}
}
],
"must": [
{
"exists": {
"field": "mytest_info"
}
}
]
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 5.1 参数原理
wait_for_completion=false:
- 请求立即返回 task id,任务在后台继续执行
- 避免单条 HTTP 请求因任务超时(默认 1 分钟)而中断
- 可随时用
_tasks/<task_id>查询进度
slices=8:
- 将任务按索引分片切分成 8 个并行子任务
- 每个子任务处理一部分分片,充分利用集群资源
- slice 数建议不超过节点数 * 2
conflicts=proceed(可选):
- 默认遇到版本冲突(文档在更新期间被其他操作修改)会中止任务
- 加上
conflicts=proceed跳过冲突文档继续处理其他
# 5.2 异步任务监控
# 查看所有 update_by_query 任务
GET _tasks?detailed=true&actions=*/_update_by_query
# 查看特定任务
GET _tasks/<task_id>
2
3
4
5
响应中的 created 表示本次更新创建的文档数,updated 表示修改的文档数,version_conflicts 显示冲突次数。
# 6. 坑与边界
# 6.1 基于快照的更新盲区
_update_by_query 在执行时会先对索引做快照,只处理快照时刻符合条件的文档。快照期间新写入的文档不会被处理。
如果数据持续写入,需要:
- 第一次
update_by_query处理存量 - 任务完成后,根据时间戳或版本号做第二次增量补漏
# 6.2 段合并压力
大批量 update 会产生大量新段和 deleted docs(旧版本标记为删除),跑完后建议观察段合并情况:
GET mytestindex_2024_11/_stats/segments
如果 deleted 数量接近 docs.count,考虑手动触发段合并或等待后台合并完成。
# 7. 验证数据完整性
# 7.1 统计已迁移文档数
GET mytestindex_2024_11/_count
{
"query": {
"exists": {
"field": "mytest_info2"
}
}
}
GET mytestindex_2024_11/_count
{
"query": {
"exists": {
"field": "mytest_info"
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
两数相减得出待迁移量,用百分比表示覆盖率。
# 7.2 抽样校验值一致性
GET mytestindex_2024_11/_search
{
"size": 10,
"_source": ["mytest_info", "mytest_info2"],
"query": {
"bool": {
"must": [
{"exists": {"field": "mytest_info2"}},
{"exists": {"field": "mytest_info"}}
]
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
抽样对比两字段值是否相等,确认迁移逻辑无遗漏。
# 8. 可复用要点
- 只处理存量数据:用
exists+must_not组合避免对已处理文档重复写入 - 永远异步执行:
wait_for_completion=false防止 HTTP 超时,配合 task id 轮询进度 - 先评估场景:前缀/精确匹配用
keyword,全文检索用text,只有真正需要通配符时才用wildcard - 监控段合并:大批量 update 后检查
deleted文档比例,必要时手动触发 merge
Agent 可直接解析的元数据块
{
"_meta": {
"version": "1.0",
"article_id": "es-wildcard-field",
"scope": "Elasticsearch 7.x+",
"last_verified": "2024-12"
},
"quick_start": [
"PUT <index>/_mapping 添加字段 type:wildcard",
"POST <index>/_update_by_query?wait_for_completion=false&slices=<N> 执行迁移",
"GET _tasks?actions=*/_update_by_query 监控进度",
"GET <index>/_count with exists query 统计覆盖率"
],
"safety_rules": [
"不能在已有字段上直接改类型",
"wait_for_completion=false 避免超时",
"大批量更新后检查段合并状态"
],
"verification": [
"exists 查询统计已迁移文档数",
"sample search 校验字段值一致性",
"_stats/segments 观察 deleted docs"
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24