Elasticsearch 中的 wildcard 字段类型详解
Elasticsearch 中的 wildcard 字段类型详解
Elasticsearch(ES)中的wildcard字段类型是一种专门用于优化通配符搜索的字段类型。它通过n-gram索引和二进制文档值存储两种数据结构,实现了既保证搜索准确性又提高搜索效率的目标。本文将详细介绍wildcard字段的工作原理,并与keyword字段进行对比,帮助读者更好地理解其应用场景和性能特点。
倒排索引结构
Term Index 不会包含所有的 term,只包含一些 term 前缀。通过 Term Index 可以快速地定位到 Term Dictionary 的某个 offset,然后从这个位置再往后顺序查找
其中 Term Index 在内存中存储,Term Dictionary 和 Posting List 在磁盘中。一次搜索的步骤就是:
- 搜索 Term Index 树找到对应 Term Dictionary 中的 offset,因为匹配到的可能只是一个前缀,需要再顺序往后找,直到匹配
- 通过 Term Dictionary 找到对应的 Posting List
Wildcard 字段类型原理
新的通配符字段通过两种数据结构自动加速通配符和正则表达式搜索
- 所有字符串中出现的 3 个字符序列的 "n-gram" 索引
- 完整原始文档值的 "二进制文档值" 存储
第一个数据结构用于快速但粗略地缩小候选对象范围
第二个数据结构则用于通过自动机查询验证由 n-gram 索引筛选出的候选匹配对象
这样,既保证了搜索的准确性,又提高了搜索效率
怎么存
详解一下上述的第一条规则
通配符字段类型会将字段值拆分(用 n-gram)为长度 <= 3 的子串,并将这些子串写入索引
例如:字符串 "test" 被拆分为字符串 "t"、"te"、"tes"、"e"、"es" 和 "est" 这六个字串写入 ES 中
怎么查
在搜索时,ES 同样将搜索词按照上述规则拆分
例如:搜索 "test" 时,ES 会对 "tes" AND "est" 进行索引搜索
如果搜索项包含少于三个字符,ES 将使用长度为一或两个字符的字符子串。
例如:搜索 "t" 时,ES 会对 "t" 进行搜索,搜索 "te" 时,ES 会对 "te" 进行搜索、
粗略搜索完后在和保存的原始值对比看看是不是真的符合
Keyword 和 Wildcard 的对比
特性 | Keyword | Wildcard |
---|---|---|
排序速度 | 快 | 略慢 (见 *1) |
集合速度 | 快 | 略慢 (见 *1) |
前缀搜索速度 (foo*) | 快 | 略慢 (见 *2) |
低区分度前导搜索 (*foo) | 快 | 慢 (见 *3) |
高区分度前导搜索 (*foo) | 糟糕 | 快得多 |
Term 搜索 全值匹配 (foo) | 快 | 没那么快 (见 *2) |
模糊查询 | 支持 (见 *4) | 支持 |
正则表达式查询 | 支持 (见 *4) | 支持 |
范围查询 | 支持 (见 *4) | 支持 |
支持 highlighting | 支持 | 不支持 |
Searched by "all field" queries(不知道咋翻译,放这了 X_X) | 支持 | 支持 |
大部分是唯一值对硬盘的消耗 | 高 (见 *5) | 低 (见 *5) |
大部分是相同值对硬盘的消耗 | 低 (见 *5) | 中 (见 *5) |
一个字段的最大字符数 | 256 for default JSON string mappings, 32,766 Lucene max | 无限 |
Supports normalizers in mappings | 支持 | 不支持 |
索引速度 | 快 | 慢 (见 *6) |
- 略慢一些,因为文档值是从压缩的 32 个块中检索出来的
- 略慢一些,因为使用 n-gram 进行的近似匹配需要验证
- Keyword 字段只访问每个唯一值一次,而 Wildcard 字段会评估每个可能的值
- 如果启用了 "允许昂贵查询" 的设置
- 取决于公共前缀 —— Keyword 字段基于公共前缀进行压缩,而 Wildcard 字段则是整个值的 LZ4 压缩
- 具体会因内容而异,测试索引日志文件时,Wildcard 字段花费了 499 秒,而 Keyword 字段则为 365 秒
评估是否使用时的测试
测试环境
条件有限,测试不严谨,主要是为了可行性验证
构建测试数据
在 ES 中创建 3 个索引
- test1 使用 keyword
- test2 使用 keyword 数组存储自行切词,例如:王小明就存储:王小明,小明,明 这三个值,这样是可以利用前缀索引的
- test3 使用 wildcard 字段类型
构建测试数据用的代码
构建了 1000W 随机中文字符,每个字符串长度在 5 - 10 个
public void generateTestData() {
List<Test1> list1 = new ArrayList<>(10000);
List<Test2> list2 = new ArrayList<>(10000);
List<Test3> list3 = new ArrayList<>(10000);
for (int i = 0; i < 1000; i++) {
list1.clear();
list2.clear();
list3.clear();
for (int j = 0; j < 10000; j++) {
StringBuilder sb = new StringBuilder();
for (int k = 0; k < RandomUtil.randomInt(5, 10); k++) {
char c = RandomUtil.randomChinese();
sb.append(c);
}
list1.add(new Test1(sb.toString()));
list3.add(new Test3(sb.toString()));
List<String> inner = new ArrayList<>();
for (int k = 0; k < sb.length(); k++) {
inner.add(sb.substring(k, sb.length()));
}
list2.add(new Test2(inner));
}
operations.save(list1);
operations.save(list2);
operations.save(list3);
}
}
硬盘占用
可以看到占用硬盘为 前缀切词 > wildcard > keyword
搜索速度
单字符
test1 和 test3 搜索潼
test2 搜索潼*
类型 | 耗时 |
---|---|
keyword | 127ms |
前缀切词 | 4ms |
wildcard | 165ms |
双字符
test1 和 test3 搜索縇潼
test2 搜索縇潼*
类型 | 耗时 |
---|---|
keyword | 79ms |
前缀切词 | 3ms |
wildcard | 16ms |
本来还想在多测一些字符长度的情况的,但后来 docker 容器误删了 X_X
大致可以看出,wildcard 在硬盘空间占用和搜索性能之间做到了不错的平衡
线上测试
由于线上没有 keyword 和前缀切词,直接用了 wildcard 所以就没有和其他类型的对比了,大概看下在这样的数据量下,搜索一次的耗时
单索引数据量是 8209269
搜索福耗时 36ms
搜索幸福一生耗时 12ms