灯下哥谭 灯下哥谭
首页
关于
  • Hermes Agent 平台
  • Claude Code
  • OpenClaw
  • GPU 推理节点运维
  • DeepSeek Harness
  • MySQL 运维知识地图
  • Elasticsearch 运维知识地图
  • Redis 运维知识地图
  • TiDB 体系
  • DBA 常用 SQL 与命令
  • Nginx 运维知识地图
  • Prometheus 监控
  • Docker
  • Systemd
  • Iptables
  • Firewalld
  • Sshd
  • MySQL8 运维 SOP 手册
  • MySQL 实战 45 讲(读书笔记)
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

灯下哥谭

灯还亮着
首页
关于
  • Hermes Agent 平台
  • Claude Code
  • OpenClaw
  • GPU 推理节点运维
  • DeepSeek Harness
  • MySQL 运维知识地图
  • Elasticsearch 运维知识地图
  • Redis 运维知识地图
  • TiDB 体系
  • DBA 常用 SQL 与命令
  • Nginx 运维知识地图
  • Prometheus 监控
  • Docker
  • Systemd
  • Iptables
  • Firewalld
  • Sshd
  • MySQL8 运维 SOP 手册
  • MySQL 实战 45 讲(读书笔记)
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • MySQL

  • Redis

  • 高性能KV

  • TiDB

  • Elasticsearch

    • Elasticsearch 运维知识地图:从安装配置到排障加速恢复
    • Elasticsearch 集群安装:RPM 裸机生产部署 vs docker-compose 快速起集群
    • 给Elasticsearch集群添加用户密码
    • Elasticsearch 分片和副本:容量怎么规划,改分片数为什么这么麻烦
    • Elasticsearch集群节点磁盘使用分配不均解决办法
    • Elasticsearch 索引模板与映射:Composable 模板、mapping 与动态模板
    • Elasticsearch 分页查询三种方案:from/size、search_after、scroll 怎么选
    • Elasticsearch字符串搜索方式
    • Elasticsearch使用wildcard字段模糊匹配
      • 1. wildcard 字段为什么快
      • 2. 什么时候不该用 wildcard
      • 3. 代价怎么量化
        • 3.1 索引体积对比
        • 3.2 查询性能对比
        • 3.3 回撤判据
      • 4. 更新字段映射
      • 5. 数据迁移脚本
        • 5.1 参数原理
        • 5.2 异步任务监控
      • 6. 坑与边界
        • 6.1 基于快照的更新盲区
        • 6.2 段合并压力
      • 7. 验证数据完整性
        • 7.1 统计已迁移文档数
        • 7.2 抽样校验值一致性
      • 8. 可复用要点
    • Elasticsearch 数据迁移方案对比:esm vs Logstash
    • Nginx Mirror 模块实现三套ES写入网关
    • ES排障两件套:慢查询日志阈值配置 + tcpdump 抓包看真实请求
    • ES 集群恢复太慢?三个参数加速节点/分片恢复
    • Elasticsearch 常用 DSL 语句(速查表)
    • ES 集群 Yellow 复盘:1023 个副本永远分配不出去,问题不在磁盘
  • 数据管道

  • 其他数据库

  • 数据库
  • Elasticsearch
灯下哥谭
2024-12-07
目录

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:

  1. N-gram 索引:将字段值拆成固定长度的 n-gram(默认 3-gram),建立倒排索引用于快速筛选
  2. 原值存储:同时保留原始完整值,用于最终精确校验

查询时的工作流程:

  • 先用 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
1

或在测试索引上建两个字段(一个 keyword 一个 wildcard)灌同一批数据,用 _disk_usage 看按字段拆分的磁盘占用:

GET mytestindex/_disk_usage?run_expensive_tasks=true
1

响应中每个字段的 total_in_bytes 就是该字段实际占用的磁盘空间。

# 3.2 查询性能对比

同一条通配符查询分别打到 keyword 字段和 wildcard 字段,对比 took 耗时:

GET mytestindex/_search
{
  "profile": true,
  "query": {
    "wildcard": {
      "mytest_info2": "*keyword*"
    }
  }
}
1
2
3
4
5
6
7
8
9
  • took 是总耗时(毫秒)
  • profile 输出会拆解成 rewrite_time、build_scorer 等阶段,看具体是哪一段慢

注意:单次 took 受缓存影响,要清缓存后多跑几次取分布:

POST mytestindex/_cache/clear
1

不要拿一次结果下结论。

# 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"
    }
  }
}
1
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"
          }
        }
      ]
    }
  }
}
1
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>
1
2
3
4
5

响应中的 created 表示本次更新创建的文档数,updated 表示修改的文档数,version_conflicts 显示冲突次数。

# 6. 坑与边界

# 6.1 基于快照的更新盲区

_update_by_query 在执行时会先对索引做快照,只处理快照时刻符合条件的文档。快照期间新写入的文档不会被处理。

如果数据持续写入,需要:

  1. 第一次 update_by_query 处理存量
  2. 任务完成后,根据时间戳或版本号做第二次增量补漏

# 6.2 段合并压力

大批量 update 会产生大量新段和 deleted docs(旧版本标记为删除),跑完后建议观察段合并情况:

GET mytestindex_2024_11/_stats/segments
1

如果 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"
    }
  }
}
1
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"}}
      ]
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

抽样对比两字段值是否相等,确认迁移逻辑无遗漏。

# 8. 可复用要点

  1. 只处理存量数据:用 exists + must_not 组合避免对已处理文档重复写入
  2. 永远异步执行:wait_for_completion=false 防止 HTTP 超时,配合 task id 轮询进度
  3. 先评估场景:前缀/精确匹配用 keyword,全文检索用 text,只有真正需要通配符时才用 wildcard
  4. 监控段合并:大批量 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"
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#性能优化#Elasticsearch
上次更新: 9/11/2026

← Elasticsearch字符串搜索方式 Elasticsearch 数据迁移方案对比:esm vs Logstash→

最近更新
01
DeepSeek Harness 实战 06|学习笔记:插件、工具、技能不在同一个维度上 原创
09-11
02
DeepSeek Harness 实战 05|让两个编码 Agent 共用一份长期记忆 原创
09-09
03
DeepSeek Harness 实战 04|学习笔记:从「已知限制」里读出三处设计张力 原创
09-08
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式