1、背景
在生产使用中,Elasticsearch 除了精确匹配的要求,也会有模糊查询的场景。
2、解决方案探讨
面对这种问题 ,传统的解决方案有两种:
2.1 方案一:ngram 分词器
使用 ngram 分词器对存入的数据进行精细化的拆分,利用细颗粒度的 token 进行快速的召回。
这是一个利用空间换时间的方案,细化查询所需的词根内容,利用精确匹配结果大范围的命中来达到模糊效果。
PUT test-005 { "settings": { "index.max_ngram_diff": 10, "analysis": { "analyzer": { "my_analyzer": { "tokenizer": "my_tokenizer" } }, "tokenizer": { "my_tokenizer": { "type": "ngram", "min_gram": 3, "max_gram": 10, "token_chars": [ "letter", "digit" ] } } } }, "mappings": { "properties": { "title": { "type": "text", "analyzer": "my_analyzer", "fields": { "keyword": { "type": "keyword" } } } } } } POST test-005/_bulk {"index":{"_id":1}} {"title":"英文官网承认刘强东一度被捕的原因是涉嫌性侵"} {"index":{"_id":2}} {"title":"别提了朋友哥哥刘强东窗事发了"} {"index":{"_id":3}} {"title":"刘强东施效颦,没想到竟然收获了流量"} {"index":{"_id":4}} {"title":"刘强东是谁?我不认识"} POST test-005/_search { "query": { "match_phrase": { "title": "刘强东" } } }
- 优点:召回快,性能消耗小;
- 缺点:有不小的空间消耗,颗粒度越细,消耗越大。同时,有一定的学习成本,需要对分词器有成熟的了解,不适合新手。
这里有个明显的使用案例,如下图所示,使用 ngram 的 test2 索引比原来使用 keyword 的索引空间大小大了接近10倍。
2.2 方案二:wildcard 查询
使用 wildcard 查询,这是一项支持通配符的模糊检索功能,有点类似 SQL 中的 like 匹配。
为了实现通配符和正则表达式的查询,Ealsticsearch 依赖的 Lucene4.0 会将输入的字符串模式构建成一个DFA (Deterministic Finite Automaton),而带有通配符的pattern构造出来的DFA可能会很复杂,开销很大。
具体分析:
https://elasticsearch.cn/article/171
https://elasticsearch.cn/article/186
- 优点:使用简单,也不需要额外的存储资源。
- 缺点:性能消耗巨大,滥用则可能会造成线上事故。
面对两个各有所长,甚至有点“卧龙凤雏”的方案,ES 在 7.9 版本推出了 wildcard 字段类型来解决模糊匹配的场景需求。
3、wildcard 类型使用详解
Elasticsearch 的 wildcard 字段类型最早在 7.9 版本中引入。这个版本加入了对 wildcard 类型的支持,旨在改善模糊匹配的查询效率和性能,特别是在处理大量文本数据时。这一新特性主要针对了之前版本中 wildcard 查询的性能问题,提供了更高效的方式来处理通配符和正则表达式的搜索需求。
https://www.elastic.co/guide/en/elasticsearch/reference/7.9/release-highlights.html
我们先来看下 wildcard 类型怎么使用:
先定义一个 wildcard 类型的字段
PUT my-index-000001 { "mappings": { "properties": { "my_wildcard": { "type": "wildcard" } } } }
为其写入一个文档
PUT my-index-000001/_doc/1 { "my_wildcard" : "This string can be quite lengthy" }
然后使用 wildcard 查询如下所示:
GET my-index-000001/_search { "query": { "wildcard": { "my_wildcard": "*quite*lengthy" } } }
结果为
{ "took" : 6, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 1, "relation" : "eq" }, "max_score" : 3.8610575, "hits" : [ { "_index" : "my-index-000001", "_type" : "_doc", "_id" : "1", "_score" : 3.8610575, "_source" : { "my_wildcard" : "This string can be quite lengthy" } } ] } }
有时候我们需要忽略大小写,可以在 wildcard 查询使用 case_insensitive 参数。
GET my-index-000001/_search { "query": { "wildcard": { "my_wildcard": { "value": "*Quite*lengthy", "case_insensitive": true } } } }
4、wildcard 原理
关于 wildcard 字段的实现,官方在推出该字段的时候发布了相关的说明:
新的 wildcard 字段使用以下两种数据结构以这种方式自动加速通配符和正则表达式搜索:
- 字符串中所有3个字符序列的 n-gram 索引。
- 完整原始文档值的 “二进制 doc value” 存储。第一点,底层还是 ngram 的分词去实现模糊查询的场景,但是这里的 ngram 颗粒度是 3,从功能上满足了模糊查询的需求和保证了 wildcard 查询的高性能。
第二点,使用了 ES 中常见的正排+列存数据存储格式 doc value,在这里一个主要的效果就是在自动查询验证由 n-gram 语法匹配产生匹配候选的同时利用了doc value格式相对较高的压缩比。
5、测试
现在来看下 wildcard 实际的表现。
5.1 空间大小
如下图所示,可以看到使用 wildcard 字段的索引与原索引相差不大。
5.2 查询效率
查询dsl | keyword类型 | wildcard类型 |
wildcard:”红豆” | 715ms | 71ms |
wildcard:”006-612014” | 633ms | 22ms |
wildcard:”55” | 584ms | 188ms |
wildcard:”11” | 1359ms | 357ms |
注:这里省却了索引详细信息,只需知道是同一个索引的比对测试。
综上所述,在模糊搜索字段区分度很低的情况下 如:模糊查询单个数字,此时优化效率rt大概是之前的1/3左右,区分度高的场景rt大概是之前的1/15左右,有明显效果。
6、小结
1.可以说 wildcard 字段类型满足了模糊查询的主要需求,同时也提供了相对较高的查询性能;
2.wildcard 针对于 ngram 分词器有着不小的空间优势。
3.wildcard 虽然有着不小的优势,但是查询效率与数据的区分度有着很强的关联,在一些区分度较低的场景下效率与性能消耗依旧很严重。
4.相比 ES 在精确查询场景优秀的性能表现(即 term keyword 的高效,平稳在毫秒级的返回),wildcard 字段在模糊查询场景下的使用还是需要研发人员根据实际场景测试选择。
7、作者介绍
金多安,Elastic 认证专家,Elastic资深运维工程师,死磕Elasticsearch知识星球嘉宾,星球Top活跃技术专家,搜索客社区日报责任编辑
铭毅天下审稿并做了部分微调。
推荐阅读
更短时间更快习得更多干货!
和全球 近2000+ Elastic 爱好者一起精进!
比同事抢先一步学习进阶干货!