广告位 · 页头(后台-模块管理-新建模块后替换此区域)

SQL如何实现高效的模糊全文检索?使用MATCH AGAINST

数据库里做模糊匹配,最常见的写法是 LIKE '%关键词%'。这个语法在数据量小的时候完全没有问题,但一旦表行数达到几十万甚至上百万,查询延迟会迅速升高。原因在于 LIKE 的前导通配符让数据库无法利用 B+ 树索引,只能选择全表扫描,而且每一行都要做字符串匹配,CPU 和 I/O 消耗都非常大。以一张 100 万行的文章表为例,执行 LIKE '%database%' 时,MySQL 的 EXPLAIN 结果会显示 type=ALL、rows=1000000,这意味着数据库要逐行读取并比较。

SQL如何实现高效的模糊全文检索?使用MATCH AGAINST

要解决这个问题,可以使用 MySQL 内置的全文索引和 MATCH AGAINST 语法。全文索引不是简单地把整段文字放进一棵树里,而是先对文本进行分词,然后建立倒排索引。倒排索引保存的是词语到文档编号的映射关系,查询时只要定位到关键词对应的文档列表,再按相关性排序返回结果,不需要扫描全部数据。这也是为什么 MATCH AGAINST 在执行全文检索时通常比 LIKE 快一个数量级以上。

为什么 LIKE 模糊查询在大数据量下会失效

普通 B+ 树索引依赖值的顺序排列,例如在 name 字段上建索引,可以快速定位以某个前缀开头的记录。但 LIKE '%关键词%' 要求关键词可以出现在字段的任意位置,索引有序性完全派不上用场。即使写成 LIKE '关键词%',虽然理论上能走索引,但实际优化器也可能因为选择性不够而选择全表扫描。更要命的是,如果通配符在开头,索引根本不会被考虑。

来看一个简单的执行计划。假设 articles 表有一百万行,content 字段没有全文索引,只有普通索引 idx_content,执行下面这条查询时,MySQL 会放弃 idx_content 并扫描全表。

EXPLAIN SELECT * FROM articles WHERE content LIKE '%database%';

执行结果中的 type 列通常是 ALL,rows 列接近表总行数。即使 content 列上有普通索引,优化器也知道对中间包含的匹配方式,索引无法帮助减少扫描范围。此时在高并发场景下,这样的查询会迅速占满数据库连接和磁盘 I/O,拖慢整个实例。

全文索引的思路则完全不同。它在写入阶段就把文本拆成词语列表,每个词语对应一组文档编号。查询某个关键词时,直接去倒排表里取出包含该词的文档集合,再根据词频、文档长度等因素计算相关性得分。这个过程不需要遍历所有行,数据量越大优势越明显。因此,如果你的业务经常需要根据关键词搜索标题、正文、备注等长文本字段,应该优先考虑全文索引,而不是用 LIKE 硬扛。

MATCH AGAINST 基础用法与全文索引创建

在 MySQL 的 InnoDB 引擎中,全文索引只能创建在 CHAR、VARCHAR 和 TEXT 类型的列上。你可以对单个列或多个列建立全文索引,查询时也必须引用完全相同的列顺序。创建全文索引的语法和普通索引类似,只是多了一个 FULLTEXT 关键字。

ALTER TABLE articles ADD FULLTEXT INDEX ft_title_content (title, content);

建成之后,就可以用 MATCH AGAINST 进行自然语言模式查询。自然语言模式会计算每个文档与查询词的相关性,并把结果按得分降序返回。查询语句示例如下。

SELECT title, content,
       MATCH(title, content) AGAINST('database' IN NATURAL LANGUAGE MODE) AS relevance
FROM articles
WHERE MATCH(title, content) AGAINST('database' IN NATURAL LANGUAGE MODE);

这里在 SELECT 和 WHERE 里各写了一次 MATCH AGAINST,目的是同时获得相关性分数并过滤结果。如果只需要过滤,WHERE 子句中的 MATCH AGAINST 就够了。需要注意,MATCH 中列出的列必须和全文索引创建时完全一致,否则会报错。如果你的表没有全文索引,直接使用 MATCH AGAINST 也会提示找不到 FULLTEXT 索引。

另外,MySQL 默认的全文索引分词器是为英文等空格分隔语言设计的,它会按空格和标点切词,同时对常见词和太短的词进行过滤。这个默认行为对中文非常不友好,因为中文句子没有自然的空格分隔,整句话可能被当成一个词,导致搜索不到结果。这个痛点会在下一节重点解决。

中文场景的全文检索:ngram 分词器

MySQL 内置的 ngram 解析器专门用来处理中文、日文、韩文等没有明显词边界的语言。它的原理是把文本切成连续的 N 个字符片段,比如设置 ngram_token_size=2,那么“全文检索”会被切成“全文”“文检”“检索”三个 token。查询时也会对搜索词做同样的切分,然后去倒排索引中匹配。这种方式虽然会产生一些无意义的组合,但能有效支持中文全文搜索。

要使用 ngram 解析器,需要在创建全文索引时通过 WITH PARSER ngram 指定。同时可以调整 ngram_token_size 参数,默认值是 2,适合大多数中文双字词场景。如果希望匹配更长的词,可以改成 3 或 4,但索引体积会变大,查询性能也会受到一定影响。

SET GLOBAL ngram_token_size=2;
ALTER TABLE articles ADD FULLTEXT INDEX ft_ngram_title_content (title, content)
WITH PARSER ngram;

创建好之后,中文查询就可以正常工作了。例如搜索“全文检索”,ngram 会把它切成双字片段,然后找出同时包含这些片段的文档。查询语句和英文场景一样,只是关键词换成了中文。

SELECT title
FROM articles
WHERE MATCH(title, content) AGAINST('全文检索' IN NATURAL LANGUAGE MODE);

需要注意的是,ngram_token_size 是一个全局参数,修改后需要重新创建全文索引才会生效。如果表已经存在用旧参数建立的 ngram 全文索引,需要先删除索引再重建。另外,ngram 分词会产生大量短 token,导致倒排索引比英文全文索引大得多,对大表来说需要评估额外的存储和写入开销。如果业务对中文搜索精度要求很高,还可以在应用层先做分词,再把分词结果存到单独的 keyword 列,用普通全文索引或倒排表实现更精准的匹配。

布尔模式与查询扩展:实现精确控制和召回提升

自然语言模式返回的结果已经按相关性排序,但有时我们需要更灵活的控制。布尔模式允许在查询词前加操作符来表达必须包含、必须排除、加权等语义。加号表示该词必须出现,减号表示该词必须不出现,大于号可以提升某个词的权重,小于号则降低权重。布尔模式不会自动按相关性排序,除非你指定排序条件。

SELECT title
FROM articles
WHERE MATCH(title, content) AGAINST('+MySQL -Oracle' IN BOOLEAN MODE);

上面的查询会返回包含 MySQL 且不包含 Oracle 的文章。再比如,如果你希望包含 MySQL 的文章排名更靠前,可以写成 AGAINST('>MySQL <Oracle' IN BOOLEAN MODE)。这里大于号和小于号在代码块中需要转义为实体,但在 SQL 实际执行时仍然表示加权操作。布尔模式还可以结合通配符 * 实现前缀匹配,比如 AGAINST('My*' IN BOOLEAN MODE) 可以匹配 MySQL、MyISAM 等以 My 开头的词。

查询扩展是另一种扩大召回范围的手段,通常在自然语言模式下使用 WITH QUERY EXPANSION。它的工作过程分两步:先执行一次普通全文检索,找出最相关的几行,然后把这些行中出现的高频词加入搜索词,再执行第二次查询。这样做的好处是当用户输入的关键词比较短或不够精确时,能自动找到相关文档。比如用户搜索 database,查询扩展可能会把 SQL、index、table 等相关词也纳入匹配范围。

SELECT title
FROM articles
WHERE MATCH(title, content) AGAINST('database' WITH QUERY EXPANSION);

不过查询扩展也有副作用,如果初始结果集里混入了不相关但高频的词语,第二次查询会把这些噪音词也加进来,导致结果偏离用户意图。因此它更适合语料相对集中、主题明确的场景,比如技术文档库、新闻分类库。对于开放性很强的用户生成内容,建议谨慎使用,或先通过关键词过滤限定范围。

全文索引的维护与实际应用边界

全文索引虽然在查询时很快,但写入代价高于普通索引。InnoDB 的全文索引并不是在每次 INSERT 或 UPDATE 时同步更新倒排表,而是把变更缓存在内存里,后台线程定期合并。因此刚插入的文档可能不会立即出现在全文搜索结果中,这在小数据量下影响不大,但对实时性要求极高的场景需要额外注意。你可以通过设置 innodb_ft_cache_size 控制缓存大小,或者适当调整 innodb_ft_total_cache_size 来避免频繁刷盘。

另一个容易被忽略的问题是停用词。全文索引默认会忽略一些常见词,比如英文中的 the、a、is,中文 ngram 模式下则没有默认停用词表。但如果某些词几乎出现在所有文档中,比如一个电商网站里“包邮”可能出现在 80% 的商品标题里,这种词就失去了区分度,反而会占用大量索引空间。你可以自定义停用词表并配置 innodb_ft_server_stopword_table,让全文索引跳过这些无价值词。

CREATE TABLE my_stopwords(value VARCHAR(30)) ENGINE=INNODB;
INSERT INTO my_stopwords VALUES ('包邮'),('正品');
SET GLOBAL innodb_ft_server_stopword_table = 'mydb/my_stopwords';

此外,当全文索引的数据量达到一定规模后,频繁的删除和更新会导致倒排表中的碎片增加,查询性能逐渐下降。定期执行 OPTIMIZE TABLE articles; 可以重建全文索引,清理碎片,恢复查询效率。但 OPTIMIZE TABLE 会锁表并消耗大量 I/O,应该在低峰期执行。

最后要明确全文索引的适用边界。如果只是偶尔查询几百行的小表,LIKE 完全够用;如果需要支持复杂的语义搜索、拼音搜索、同义词、自定义打分、高亮等需求,MySQL 全文索引的能力有限,此时可以考虑将数据同步到 Elasticsearch 或 Meilisearch 这类专业的搜索引擎。MATCH AGAINST 的价值在于无需引入额外组件,利用 MySQL 自身能力就能解决大部分关键词匹配场景,尤其适合中小规模项目或运维资源有限的团队。

广告位 · 文章底部(后台-模块管理替换此区域)