为什么ES中不推荐使用wildcard查询
在lucene中AutomatonQuery类的作用中,我们说了 AutomatonQuery
性能很高。
那么,为什么 Elasticsearch 的官方文档和所有最佳实践都强烈建议不要使用前缀通配符(leading wildcard),比如 *text
?
答案在于 AutomatonQuery
的高效是有条件的,而前缀通配符恰好破坏了这个条件。
核心原因:与有序词项词典(Term Dictionary)的交互方式
Lucene 的性能秘诀在于它的倒排索引,其中一个关键部分是词项词典(Term Dictionary)。这个词典存储了索引中所有唯一的词项,并且是按字典序(alphabetically)排序的。
这个“按字典序排序”的特性至关重要。我们来对比两种通配符查询:
1. 后缀通配符查询(Trailing Wildcard)- 性能很高 (text*
)
当你执行一个像 text*
这样的查询时:
- 构建自动机:Lucene 构建一个自动机,它匹配以
text
开头的任何字符串。 - 遍历词典:由于词典是按字典序排序的,所有以
text
开头的词项(如textbook
,testing
,texture
)都连续地存放在一起。 - 高效查找:Lucene 的查询执行器可以非常高效地:
- **直接定位(Seek)**到词典中第一个以
text
开头的词项。这就像在字典里查 “text” 这个单词一样快。 - 然后,它只需要顺序扫描该位置之后的少量词项,直到遇到第一个不以
text
开头的词项为止。 - 整个过程非常快,因为它只需要检查词典中一个很小的、连续的片段。
- **直接定位(Seek)**到词典中第一个以
这正是 AutomatonQuery
高性能的典型场景。
2. 前缀通配符查询(Leading Wildcard)- 性能灾难 (*text
)
当你执行一个像 *text
这样的查询时:
- 构建自动机:Lucene 构建一个自动机,它匹配以
text
结尾的任何字符串。 - 遍历词典:问题来了。因为词典是按开头字母排序的,所以以
text
结尾的词项(如context
,subtext
,hypertext
)会分散在整个词典的各个角落。 - 低效查找:为了找到所有匹配项,
AutomatonQuery
别无选择,只能:- 从词典的第一个词项(比如 “a”)开始。
- 逐个检查词典中的每一个词项,看它是否以
text
结尾。 - 这个过程会一直持续到词典的最后一个词项。
这相当于对整个字段的词项词典进行了一次全量扫描(Full Scan)。
这为什么在 Elasticsearch 中是灾难性的?
- 规模放大:一个 Lucene 索引(在 ES 中称为分片/Shard)的词项词典可能包含数百万甚至数千万个唯一词项。对单个分片进行全量扫描已经非常耗费 CPU。
- 分布式特性:一个 Elasticsearch 索引通常由多个分片组成,这些分片分布在集群的多个节点上。当你执行一个
*text
查询时,这个全量扫描的操作会在所有相关的分片上并行执行。 - 集群影响:这个查询会瞬间占用大量 CPU 资源,导致集群的查询延迟飙升,影响所有其他正在进行的查询,甚至可能因为负载过高而导致节点不稳定。它被称为“Query of Death”(死亡查询)之一。
总结:理论与实践的鸿沟
- 理论上:
AutomatonQuery
本身是一个高效的算法。 - 实践中:它的性能高度依赖于它所操作的数据结构。当与一个有序的词典结合使用时,对于前缀友好的模式(如
text*
),它可以利用“有序”这一特性进行快速定位,性能极高。但对于前缀不友好的模式(如*text
),它无法利用“有序”特性,被迫退化为全量扫描,性能极差。
Elasticsearch 作为一个大规模、多租户的系统,必须优先保证整个集群的稳定性和性能。因此,它强烈建议避免那些会导致资源浩劫的操作,而前缀通配符查询正是其中的典型代表。
那么,如何解决需要匹配“中间”或“结尾”的需求?
既然直接用 *text
不行,Elasticsearch 社区发展出了一些标准的解决方案,核心思想是在索引时做一些额外的工作,将慢查询转换为快查询。
-
reverse
Token Filter(反转词元过滤器):
这是解决前缀通配符最经典的方法。- 索引时:对原始词项(如
context
)建立一个反转后的版本(txetnoc
)并存入另一个子字段。 - 查询时:当用户想搜
*text
时,你将查询条件反转为txet*
,然后去查询那个反转后的字段。 - 效果:一个慢速的、全量扫描的前缀通配符查询,变成了一个高速的、可以快速定位的后缀通配符查询。
- 索引时:对原始词项(如
-
n-gram
Token Filter(N元分词过滤器):- 索引时:将一个词项(如
text
)拆分成一系列的小片段(n-grams)。例如,2-grams 会拆成te
,ex
,xt
;3-grams 会拆成tex
,ext
。 - 查询时:当用户搜索
*text*
或*tex*
时,可以被转换为对tex
这个 n-gram 的精确匹配查询,速度非常快。 - 缺点:会极大地增加索引的体积。
- 索引时:将一个词项(如
AutomatonQuery
本身是把好刀,但用它来砍一个四处分散的目标(前缀通配符),就只能把整个场地犁一遍了。而用它来砍一个整齐排列的目标(后缀通配符),则快如闪电。Elasticsearch 的建议正是基于这种实际应用场景的深刻理解。