当你的业务需要匹配 Y01、Y02 这样的固定前缀编码,如何在亿级数据中实现毫秒级响应?
引言:一个常见的业务场景
想象这样一个需求:系统中存储了大量编码数据(如物料编码、订单编号、设备序列号),格式类似于 Y01010101、Y02020305、Y03040506。业务需要筛选出所有以 Y01 或 Y02 开头的记录,并且要支持多个前缀的灵活组合查询。
看起来很简单对吧?如果用 prefix 查询,一行 JSON 就能搞定。但当你把数据量扩大到 1 亿条 ,并发请求达到 1000 QPS 时,prefix 查询可能会把你的 ES 集群 CPU 打满。
本文将系统性地分析 Elasticsearch 中前缀匹配的四种方案,从性能、磁盘占用、维护成本三个维度进行对比,并给出针对不同场景的最佳实践。
一、四种备选方案速览
| 方案 | 实现方式 | 查询性能 | 磁盘占用 | 适用场景 |
|---|---|---|---|---|
| 方案1:regexp 正则匹配 | {"regexp":{"code":"Y0[12].*"}} |
⭐⭐⭐ | 极低 | 前缀数量少(≤5个) |
| 方案2:bool + 多个 prefix | {"bool":{"should":[...]}} |
⭐ | 极低 | 坚决不推荐 |
| 方案3:edge_ngram 分词 | 索引时预处理 + terms 查询 |
⭐⭐⭐⭐⭐ | 中等 | 大数据量 + 高频查询 |
| 方案4:index_prefixes | 索引时预处理 | ⭐⭐⭐⭐⭐ | 中等 | 大数据量 + 高频查询 |
主要讲讲 方案3 和 方案4
二、方案深度解析
先说普通的 prefix 前缀匹配
在默认情况下,如果你对一个普通的 keyword 字段使用 prefix 查询(比如查以 Y01 开头的编码):
json
{ "query": { "prefix": { "code": "Y01" } } }
ES的底层引擎)必须去遍历整个倒排索引的词典(Term Dictionary),找出所有以 Y01 开头的词条(比如 Y0101、Y0102、Y0103......)。
如果匹配到的词条有几万个,Lucene 就需要把这几万个词条组合成一个巨大的 BooleanQuery 去拉取文档。这个"展开并重写(Rewrite)"的过程极其消耗 CPU 和内存,在高并发下集群很容易直接挂掉。
regexp 正则匹配(兼顾性能与灵活性)
json
{
"query": {
"regexp": {
"code": "Y0[12].*"
}
}
}
原理 :将多个前缀合并为一条正则表达式,一次扫描完成所有匹配。相比多个 prefix 查询,减少了索引遍历次数。
性能 :在数据量千万级、前缀数量 ≤5 的情况下,响应时间通常在 50~200ms。
优点:无需修改索引 Mapping,零成本上线。
缺点:正则表达式引擎在扫描倒排索引时仍有计算开销,数据量过亿时性能会明显下降。
edge_ngram 分词 + terms 精确查询
代价 :空间换时间:磁盘膨胀 N 倍(N = 字段平均长度)。
数据处理
Mapping 结构
json
{
"settings": {
"analysis": {
"analyzer": {
"prefix_analyzer": {
"tokenizer": "keyword",
"filter": "edge_ngram_filter"
}
},
"filter": {
"edge_ngram_filter": {
"type": "edge_ngram",
"min_gram": 3,
"max_gram": 9
}
}
}
},
"mappings": {
"properties": {
"code": {
"type": "text",
"analyzer": "prefix_analyzer",
"search_analyzer": "keyword",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}
查询
json
{
"query": {
"terms": {
"code": ["Y01", "Y02"]
}
}
}
原理 :
将 code 分词存储,建立倒排索引,如数据 Y01120154 被分成 7 种长度的分词
json
{
"tokens": [
{
"token": "Y01",
"start_offset": 0,
"end_offset": 9,
"type": "word",
"position": 0
},
{
"token": "Y011",
"start_offset": 0,
"end_offset": 9,
"type": "word",
"position": 0
},
{
"token": "Y0112",
"start_offset": 0,
"end_offset": 9,
"type": "word",
"position": 0
},
{
"token": "Y01120",
"start_offset": 0,
"end_offset": 9,
"type": "word",
"position": 0
},
{
"token": "Y011201",
"start_offset": 0,
"end_offset": 9,
"type": "word",
"position": 0
},
{
"token": "Y0112015",
"start_offset": 0,
"end_offset": 9,
"type": "word",
"position": 0
},
{
"token": "Y01120154",
"start_offset": 0,
"end_offset": 9,
"type": "word",
"position": 0
}
]
}
ES 使用 terms 查询 直接命中倒排索引中的词条 Y01 和 Y02,通过位图(BitSet)快速合并结果。整个过程是 O(1) 级别的磁盘定位,CPU 消耗极低。
局限性 :只适用于字段值恰好介于 分词子集
index_prefixes 前缀优化
index_prefixes 参数允许对词前缀进行索引,以加快前置搜索速度
原理力分词类似用隐藏字段 _index_prefix 承接分词内容
配置
json
"code": {
"type": "text",
"analyzer": "keyword", // 💡 编码类 text 必须用 keyword 分词器,保留原始大写!
"search_analyzer": "keyword", // 💡 查询时也保持原样
"index_prefixes": {
"min_chars": 3,
"max_chars": 9
}
},
参数
min_chars: 索引的最小前缀长度。必须大于0,默认为2。其价值是包容性的。
max_chars : 索引前缀的最大长度。必须低于20,默认是5。其价值是包容性的。
按长度对字段进行切分:
3-9
拿 Y01120154 举例
Y01\Y011\Y0112\Y0120\Y01201\Y012015\Y0120154
与分词类似
查询: 还是使用 prefix
json
{
"query": {
"prefix": {
"code": {
"value": "Y01"
}
}
}
该方式存在性能爆点:
也就是索引穿透,如果输入的查询为 'Y0' 不在 min_chars 和 max_chars,ES会退化为普通的 prefix 查询导致性能下降
三、写在最后
Elasticsearch 的查询性能优化,本质上是在索引时的工作量 与查询时的响应时间之间做权衡。
prefix查询:索引时轻松,查询时痛苦(需要遍历所有前缀)。index_prefixes: 与分词类似,但是会存在索引穿透问题edge_ngram分词:索引时多一份工作,查询时享受极速。
对于 Y01、Y02 这样的固定长度编码场景,edge_ngram 配合 min=max=3 是教科书级别的优化方案------它以几乎为零的磁盘膨胀代价,换来与精确 terms 查询同等级别的极致性能。
希望本文能帮助你在实际项目中做出正确的技术决策。如果你有其他优化场景,欢迎在评论区讨论交流!
附: es 查询计划查看
在查询中加上 profile:true

terms 查询:

index_prefixes 查询:

prefix 查询;

延伸阅读:
本文为技术分享,欢迎转载但请注明出处。