Cing
Published on 2025-01-08 / 31 Visits
0

ES 的 wildcard 类型字段

背景

从 KStack 上看到一些讲解 ES 性能优化的文章,其中关于通配查询给出的建议是 ngram 分词或者使用 7.10 版本以上的 wildcard 字段类型,遂继续在 KStack 上搜索 wildcard 关键字,发现并没有相关讲解文章

xx 作为一个 B 端业务,搜索用户名是一个非常常规的需求,这个场景刚好就可以使用 wildcard 类型来做

鉴于其他 ES 优化建议 KStack 上已经有很多文章了,所以本篇文章就不多赘述了,只写一下和 wildcard 相关的内容

倒排索引结构

Term Index 不会包含所有的 term,只包含一些 term 前缀。通过 Term Index 可以快速地定位到 Term Dictionary 的某个 offset,然后从这个位置再往后顺序查找

其中 Term Index 在内存中存储,Term Dictionary 和 Posting List 在磁盘中。一次搜索的步骤就是:

  1. 搜索 Term Index 树找到对应 Term Dictionary 中的 offset,因为匹配到的可能只是一个前缀,需要再顺序往后找,直到匹配

  2. 通过 Term Dictionary 找到对应的 Posting List

Wildcard 字段类型原理

那么 wildcard 字段类型究竟是如何实现高效的通配符搜索的呢?

关于这一点网上能搜到的资料也不多,我就尽我所能将我搜到的资料整合并加一些自己的理解来分析一下,如有不对还请见谅

新的通配符字段通过两种数据结构自动加速通配符和正则表达式搜索

  1. 所有字符串中出现的 3 个字符序列的 "n-gram" 索引

  2. 完整原始文档值的 "二进制文档值" 存储

第一个数据结构用于快速但粗略地缩小候选对象范围

第二个数据结构则用于通过自动机查询验证由 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)

  1. 略慢一些,因为文档值是从压缩的 32 个块中检索出来的

  2. 略慢一些,因为使用 n-gram 进行的近似匹配需要验证

  3. Keyword 字段只访问每个唯一值一次,而 Wildcard 字段会评估每个可能的值

  4. 如果启用了 "允许昂贵查询" 的设置

  5. 取决于公共前缀 —— Keyword 字段基于公共前缀进行压缩,而 Wildcard 字段则是整个值的 LZ4 压缩

  6. 具体会因内容而异,测试索引日志文件时,Wildcard 字段花费了 499 秒,而 Keyword 字段则为 365 秒

什么时候用 Keyword 什么时候用 Wildcard

放到 xx 业务来说,用户名搜索虽然长度不长,但确实有百万以上的唯一值,并且搜索位置可能是任意位置,所以是适合使用 Wildcard 字段类型的

评估是否使用时的测试

测试环境

条件有限,测试不严谨,主要是为了可行性验证

构建测试数据

在 ES 中创建 3 个索引

  1. test1 使用 keyword

  2. test2 使用 keyword 数组存储自行切词,例如:王小明就存储:王小明,小明,明 这三个值,这样是可以利用前缀索引的

  3. 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

参考文章

https://www.elastic.co/cn/blog/find-strings-within-strings-faster-with-the-new-elasticsearch-wildcard-field

https://opensearch.org/docs/latest/field-types/supported-field-types/wildcard/

https://juejin.cn/post/7087770224472096805