在数据库和搜索系统的日常应用中,模糊查询与拼写纠错已成为提升用户体验与系统智能化不可或缺的功能。如何在保证查询准确性的同时,实现极致低延迟和低内存消耗?
什么是 N-Gram Index?
N-Gram Index 是一种基于字符串切分的倒排索引结构,主要用于模糊匹配、全文检索以及自动补全等场景。这项技术常应用于类似 Elasticsearch 这样的文本搜索引擎,或 AI 数据检索场景中。
其基本原理是将字符串按照长度为 n 的滑动窗口进行切分,得到多个子串,其中 n 被称为 gram。举个例子,比如对字符串 "hello" 进行切分(假设 n=2),则会得到 "he"、"el"、"ll" 和 "lo" 这几个子串。

N-Gram 的基本使用流程是:在插入数据时,将目标列按 n 进行切分;在查询时,也会将 like 查询条件按相同方式切分,最后判断查询子串是否为数据集的子集,从而实现高效匹配。

目前,Databend 作为一款高性能数据湖仓,支持 N-Gram Index 和全文索引(Full-text Index)两种文本索引,可以满足各类文本检索加速需求。全文索引在数据库中是非常重要的组成部分,承担着加速和优化文本数据查询的功能。如果没有高效的全文索引技术,数据库就难以实现快速的关键词搜索和语义匹配能力。

通过以上表格,我们可以看到 N-Gram Index 和全文索引的区别对比:
在擅长场景方面,N-Gram Index 更适合加速利用 like 查询的数据过滤操作,而全文索引则适用于文档搜索和内容发现场景,类似于 Elasticsearch 那样直接通过分词获取内容;
典型用法上,N-Gram Index 直接使用日志分析,精确子串匹配;全文索引是内容搜索,模糊匹配;
查询语法方面,N-Gram Index 可以直接在 SQL 中用 WHERE col LIKE '%text%'
查询,支持 like 表达式的查询,无需修改 SQL 语法;而全文索引则需使用 match 方法(WHERE MATCH(col, 'text')
)实现相关匹配;
N-Gram Index 的高级特性包括支持大小写不敏感的匹配,而全文索引则支持相关性评分、模糊搜索和布尔查询;
N-Gram Index 能直接加速现有的 LIKE 查询,迁移成本低;全文索引则提供更专业的搜索功能,支持 Elasticsearch 兼容语法,替代传统 LIKE;
N-Gram Index 索引实现
N-Gram Index 的整体流程是建立在 Bloom Index 的基础上,将切分后的词组插入 Bloom Filter 中,因此流程基本与 Bloom Index 类似。

上图是一个 N-Gram Index 流程图,左上角展示了一个包含 ID 和 Name 列的数据表。当我们在 Name 列建立 N-Gram 索引时,如 "hello" 和 "Databend",会被切分成多个子串,插入到右下角的 Bloom Filter 中。在查询 LIKE 时,同样对条件进行切分,然后判断这些子串是否全部存在于相应的 Bloom Filter 中。每个数据块(block)会有一个或多个布隆过滤器(Bloom Filter),用来快速判断是否命中索引。如果所有子串都能在 Bloom Filter 中找到,则保留该数据块,否则跳过。这就是整个工作流程的核心。
通过这种方式,我们可以利用 N-Gram 词组有效过滤数据块,提升查询效率。

目前,N-Gram Index的存储采用索引文件复用的方式,将 N-Gram Index和 Bloom Filter 存储在同一个文件中,从而减少表的索引读取次数,加快查询速度。
在上图中,以数据块为单位,每个 block 都对应一个或多个过滤器,包括 N-Gram Index和 Bloom Index,用于对应的数据列。

在 Bloom Filter 的实现上,N-Gram Index 使用了 SBBF(Split Block Bloom Filter),这是 Google 论文中提出的一种优化方案,能提升 Bloom Filter 在 CPU 缓存和高并发环境下的表现。我们在实际代码实现中也遇到了一些挑战,因此选择采用 SBBF。
目前 Databend 的 Bloom Index 默认使用的是 XOR8 Filter,而 N-Gram Index 选择 SBBF 的原因,是因为 N-Gram 这样的切分算法在针对较大字符串时,可能切分出较多的子字符串,这导致需要插入到 Bloom Filter 中的数据量增大许多倍,XOR8 是保持指定假阳率情况下动态根据数据量进行扩容,Bloom Filter 的文件过大,导致即使数据过滤大部分后,反而因为加载 Bloom Filter 文件过久造成查询耗时更长。
右边是一张 SBBF 的原理简图。SBBF 的操作原理类似分片技术,将 N-Gram 子串分配到不同节点,每个节点为 8bits。通过哈希计算出定位每个 bit 在过滤器中的具体位置。
N-Gram Index 使用演示

我们使用的数据集是 amazon_reviews 数据集,总计 1.5 亿条亚马逊产品的客户评论,数据存储在 AWS S3 中的 snappy 压缩 Parquet 文件中,压缩后约 49GB,可在公开数据平台下载。
演示环境中,我们分别用 Bloom Index 和 N-Gram Index 建表并插入 2010~2015 年的数据。对比插入速度和查询耗时。
需要说明的是,N-Gram Index 的插入速度会受到词组切分的影响。切分长字符串会产生百倍甚至更多的子串,显著增加索引构建时间。查询环节,N-Gram Index 的查询性能显著优于 Bloom Index。同样的数据集,Bloom Index 查询耗时 13 秒,在 N-Gram Index 下,仅需 1.1 秒,且需要扫描的数据量也从 52GibB 降低到 444MibB。相对于 Bloom Index,N-Gram Index 的查询加速超 10 倍以上,大幅减少了 CPU 和 IO 开销。

总结与技巧
Databend支持 N-Gram索引,使其成为构建高性能、可扩展、且成本效益突出的文本检索系统的理想选择。它能够有效解决传统数据库在处理大规模模糊查询、全文搜索以及复杂 LIKE 模式匹配中的性能瓶颈,为文本分析与检索类应用提供强大的查询加速能力与灵活性,尤其适用于云原生场景下对实时性和资源效率的双重要求。
实际应用中,我们提供了一些使用技巧:
- 根据被索引的数据选择适当的 gram_size。例如:身份证这类子串重复性较高的数据可以适当将 gram_size 提高到 8 以上而避免子串 hash 冲突频繁而索引效果不佳。
例:select * from table where id like '%110105199003079999%'
(随机身份证)
- 当子串重复可能性低时,根据数据量以及大概的平均数据行长度而提高 bloom_size, 提供更大的空间减少 hash 冲突(过大的 bloom_size 反而影响加载 Filter 的速度)
N-Gram Index 属于 Databend 企业版特性,欢迎大家试用体验,如有兴趣也欢迎进群交流。
Q&A
问题一:正常 Like 查询是大小写敏感的,所谓"不敏感"是指什么?
黎泽仁: 目前 N-Gram 的分词是基于字符进行的。如果是中文文本,"中文"这两个字会被识别为两个字符,标点和空格也都参与分词。因此整体上,索引对大小写不敏感,即查询和存储过程中均支持大小写不敏感,不会因为大小写不同影响匹配结果。我们之所以采用 SBBF,是为了更好地控制过滤器大小和误报率,弥补 XOR8 Filter 大小不可控的问题。
在 N-Gram 场景下,可以接受更高的误报率(假阳性率),以此换取过滤器体积的合理控制,从而实现高效过滤和性能平衡。
问题二:一是中文是如何分词的?
黎泽仁: 对于中文,目前直接按照字符分词。虽然有些处理方式会按字节或单字节切分,但 N-Gram Index 在 Databend 中是直接按字符处理的,包括标点和空格都参与分词。归根结底,这里的"字符"既可理解为单个汉字,也可以理解为字节视角下的字符数量。
问题三:既然说要复用布隆过滤器,为什么又说用了两种不同的过滤器?
黎泽仁: XOR8 Filter 在空间不可控方面存在局限,因此我们采用 SBBF,可以根据场景调整参数、优化空间占用和误报率,适配不同的索引需求。
关于 Databend
Databend 是一款开源、弹性、低成本,基于对象存储也可以做实时分析的新式湖仓。期待您的关注,一起探索云原生数仓解决方案,打造新一代开源 Data Cloud。
👨💻 Databend Cloud:databend.cn
📖 Databend 文档:docs.databend.cn
💻 Wechat:Databend
✨ GitHub:github.com/databendlab...