MySQL 全文索引实战:FULLTEXT、ngram 中文分词与 MATCH AGAINST 到底该怎么用

个人主页: > for_ever_love__ <(欢迎各位大佬莅临😊)
其他栏目: > 大模型开发从0到1 <
其他栏目: > iOS项目总结大全 <
其他栏目: > 我想学python了 <
其他栏目: > iOS UI <

文章目录

  • [MySQL 全文索引实战:FULLTEXT、ngram 中文分词与 MATCH AGAINST 到底该怎么用](#MySQL 全文索引实战:FULLTEXT、ngram 中文分词与 MATCH AGAINST 到底该怎么用)
    • [一、先回答:LIKE 到底差在哪](#一、先回答:LIKE 到底差在哪)
    • [二、全文索引的本质:分词器 + 倒排索引](#二、全文索引的本质:分词器 + 倒排索引)
      • [2.1 InnoDB 的倒排数据放在哪](#2.1 InnoDB 的倒排数据放在哪)
    • 三、版本演进:别再被过时教程误导
    • 四、动手:创建全文索引
      • [4.1 建表时直接定义](#4.1 建表时直接定义)
      • [4.2 合并成一个索引,还是拆成多个](#4.2 合并成一个索引,还是拆成多个)
    • [五、查询怎么写:MATCH ... AGAINST 的三种模式](#五、查询怎么写:MATCH ... AGAINST 的三种模式)
      • [5.1 自然语言模式(默认)](#5.1 自然语言模式(默认))
      • [5.2 布尔模式(生产主力)](#5.2 布尔模式(生产主力))
      • [5.3 三种模式怎么选](#5.3 三种模式怎么选)
    • [六、ngram 是怎么把中文切开的](#六、ngram 是怎么把中文切开的)
      • [6.1 ⚠️ 最致命的坑:短于 token size 的关键词搜不到](#6.1 ⚠️ 最致命的坑:短于 token size 的关键词搜不到)
    • 七、必须知道的几个参数
    • 八、停止词:中文场景几乎一定要自定义
    • 九、踩坑合集
    • [十、什么时候该换 Elasticsearch](#十、什么时候该换 Elasticsearch)
    • 十一、小结

MySQL 全文索引实战:FULLTEXT、ngram 中文分词与 MATCH AGAINST 到底该怎么用

一个模糊查询把接口拖到超时,十有八九是 LIKE '%关键词%' 干的。

本篇讲清楚 MySQL 自带的 FULLTEXT 全文索引 :它为什么快、中文要怎么分词、

布尔模式怎么写、哪些参数必须改,以及------什么时候你不该用它。

一、先回答:LIKE 到底差在哪

假设有一张工单表,要在标题和正文里搜「支付失败」:

sql 复制代码
SELECT * FROM ticket
WHERE title LIKE '%支付失败%' OR content LIKE '%支付失败%';

这条 SQL 的问题不是写法丑,而是结构性缺陷:

写法 能用索引吗 原因
col LIKE 'abc%' 前缀匹配 ✅ 前缀有序,B+Tree 能定位起点后顺序扫
col LIKE '%abc' 后缀匹配 ❌ 开头不确定,无法在有序索引里定位
col LIKE '%abc%' 任意位置 ❌ 退化为全表扫描 + 逐行字符串匹配
col REGEXP 'abc' ❌ 全表扫描,还多一层正则开销
LOCATE('abc', col) > 0 ❌ 列被函数包裹,索引直接失效

只要出现"包含某个词"的需求,普通二级索引就帮不上忙。表里 50 万行、每行正文两三千字,

意味着每次搜索要做 50 万次字符串包含判断,CPU 全花在重复劳动上。

全文索引换了思路:把这项活儿提前到写入时做 。写入时先把文本切成一堆词(token),

建一张"词 → 出现在哪些行"的倒排表;搜索时直接查倒排表,复杂度从 O(行数) 降到接近 O(命中行数)。

LIKE 是每次搜索都在全表做一次暴力匹配;全文索引是写一次词表,之后查表。

二、全文索引的本质:分词器 + 倒排索引

它不再是 B+Tree,而是两样东西:把长串切成 token 的分词器(parser) ,

和记录 token -> (doc_id, 位置...) 的倒排索引(inverted index)。

两条样例数据:

复制代码
id=1  title: "订单支付失败排查"
id=2  title: "如何优化支付接口性能"

按二元组(bigram)切词后,内部大致是:

token 出现的 doc_id
订单 / 单支 / 支付 / 付失 / 失败 1
支付 / 何优 / 优化 / 化支 / 接口 2

搜"支付",直接反查 支付 -> {1,2},不用碰原表其他行,再按相关性算分回表取行,收工。

2.1 InnoDB 的倒排数据放在哪

InnoDB 全文索引存放在一组**辅助表(auxiliary table)**里,可以通过 information_schema 观察,

前提是显式指定要看哪张表:

sql 复制代码
-- 指定观察目标,格式 '库名/表名'
SET GLOBAL innodb_ft_aux_table = 'demo/ticket';

SELECT word, doc_count, first_doc_id, last_doc_id
FROM information_schema.INNODB_FT_INDEX_TABLE LIMIT 10;   -- 已落盘的倒排词表

SELECT * FROM information_schema.INNODB_FT_INDEX_CACHE LIMIT 10;  -- 内存里未落盘的增量分词

SELECT * FROM information_schema.INNODB_FT_CONFIG;               -- 当前生效的配置项

其中 INNODB_FT_INDEX_CACHE 解释了一个新手最困惑的现象:刚 INSERT 的数据,全文索引可能查不到。

InnoDB 为了写入性能,新插入的行先进内存 FT cache,等 cache 满了、表被 OPTIMIZE、

或实例正常关闭时才合并到磁盘,所以全文索引存在延迟可见。这是设计取舍,不是 bug。

要强制落盘就 OPTIMIZE TABLE ticket;(重建表并 flush FT cache)。

⚠️ 它是一次重建,别在业务高峰对大表做;同时它也是改了分词参数后让新参数生效的唯一手段。

三、版本演进:别再被过时教程误导

大量老教程写的是"表必须是 MyISAM"、"改 ft_min_word_len 才能搜中文",这在今天完全不适用。

阶段 版本 关键能力
MyISAM 时代 5.5 及更早 只有 MyISAM 支持 FULLTEXT;无事务;无中文分词
InnoDB 支持 5.6 InnoDB 首次支持 FULLTEXT,引入上面说的 FT cache
中文可用 5.7 内置 ngram 全文解析器,官方支持中文 / 日文 / 韩文
成熟期 8.0 InnoDB 成为默认引擎,词典表可观测性更好

8.0 的标准答案:InnoDB + ngram,忘记 MyISAM。

另外要纠正:ft_min_word_len 是 MyISAM 的变量,InnoDB 对应 innodb_ft_min_token_size;

而且一旦使用 ngram 解析器,这两个都不生效 ,分词粒度改由 ngram_token_size 决定。

这是本节最值得记住的一句话。

四、动手:创建全文索引

4.1 建表时直接定义

sql 复制代码
CREATE TABLE ticket (
  id          BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  title       VARCHAR(200)    NOT NULL DEFAULT '',
  content     TEXT,
  status      TINYINT         NOT NULL DEFAULT 0,
  created_at  DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id),
  KEY idx_created (created_at),
  -- 复合全文索引:把两列当成一个"文档"来分词
  FULLTEXT KEY ft_title_content (title, content) WITH PARSER ngram
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;

关键点:只有 CHAR / VARCHAR / TEXT 能建 FULLTEXT ,别的类型会报错;

WITH PARSER ngram 是中文场景的核心,不写就用内置解析器按空格标点切词,

整段中文会被当成一个词 ,等于没用。已有表补索引用

ALTER TABLE ticket ADD FULLTEXT INDEX ft_content (content) WITH PARSER ngram;,

等价写法是 CREATE FULLTEXT INDEX ... WITH PARSER ngram。

4.2 合并成一个索引,还是拆成多个

方案 优点 缺点
复合索引 (title, content) 一条语句同时命中两列 无法只对 title 打分(MATCH 列名必须匹配索引定义)
单列 ft_title + ft_content 可分别加权、分别命中 搜两列要写两个 MATCH,评分手工融合

要给标题更高权重,就拆成两个索引再手工加权:

sql 复制代码
SELECT id, title,
       MATCH(title)   AGAINST('支付 失败' IN BOOLEAN MODE) * 3 AS s_title,
       MATCH(content) AGAINST('支付 失败' IN BOOLEAN MODE)     AS s_body
FROM ticket
WHERE MATCH(title)   AGAINST('支付 失败' IN BOOLEAN MODE)
   OR MATCH(content) AGAINST('支付 失败' IN BOOLEAN MODE)
ORDER BY (s_title + s_body) DESC
LIMIT 20;

五、查询怎么写:MATCH ... AGAINST 的三种模式

语法骨架是 MATCH (col1, col2, ...) AGAINST ('关键词串' [修饰符])。

⚠️ 硬性约束 :MATCH() 里的列顺序和数量,必须和某个 FULLTEXT 索引定义完全一致

(或是它的前缀),写错直接报 ERROR 1191: Can't find FULLTEXT index matching the column list。

索引是 (title, content) 时,MATCH(content, title) 和 MATCH(content) 都会炸。

5.1 自然语言模式(默认)

sql 复制代码
SELECT id, title, MATCH(title, content) AGAINST('支付失败') AS score
FROM ticket
WHERE MATCH(title, content) AGAINST('支付失败')
ORDER BY score DESC
LIMIT 20;

不写修饰符就是 IN NATURAL LANGUAGE MODE,会算相关性评分 。注意不加 WHERE 的话它仍会返回所有行

(只是按分数排序),所以过滤条件不能省。

关于流传很广的「50% 阈值」(词出现在超过半数行里就评 0 分、搜不到结果)------

这条规则的经典描述针对的是 MyISAM 的传统实现,InnoDB 用的是自己的 TF-IDF 变体,行为并不完全一样。

建议别背结论,直接在你的库上验证:把 1000 行里的 800 行都写上"支付",再 AGAINST('支付') 看结果集。

5.2 布尔模式(生产主力)

布尔模式下关键词串是一段表达式,这是生产环境用得最多的模式:

运算符 含义 示例
+ / - 必须包含 / 必须排除 +支付 -测试
(无符号) 可选包含,命中加分 支付 退款
> < 提高 / 降低对分数的贡献 >支付 <测试
~ 出现则降低分数 ~异常
* 通配符后缀 支付* 命中"支付失败""支付中"
" 短语,要求按顺序相邻出现 "支付 失败"
@distance 词之间的最大距离(InnoDB 支持) "订单 支付" @5
() 分组 +(支付 退款) -测试
sql 复制代码
-- 必须含"支付",含"失败"加分,含"测试"直接排除
... AGAINST('+支付 失败 -测试' IN BOOLEAN MODE)
-- 前缀通配
... AGAINST('退款*' IN BOOLEAN MODE)
-- 短语 + 邻近距离
... AGAINST('"订单 支付" @5' IN BOOLEAN MODE)

💡 默认运算符集由 ft_boolean_syntax 控制(默认 + -><()~*:""&|),

理论上能让"无符号词"默认变成 AND,但不推荐改 ------会坑到不懂这个约定的同事。

⚠️ 还有条硬限制:单个 token 超过 innodb_ft_max_token_size(默认 84)会被直接丢弃,

既不索引也搜不到,所以往全文索引里塞长 URL、Base64 是没有意义的。

5.3 三种模式怎么选

模式 算分 运算符 性能 适用场景
NATURAL LANGUAGE ✅ ❌ 中 简单搜一下,按相关度排序
BOOLEAN ✅(偏命中与否) ✅ 好 生产主力:AND / OR / NOT / 通配 / 短语
QUERY EXPANSION ✅ ❌ 差(跑两趟) 召回优先,几乎用不上

查询扩展(WITH QUERY EXPANSION)会先用原词搜一次,再把最相关文档里的高频词拿来扩展第二轮。

听着很美,但结果不可控、延迟翻倍、难以和业务过滤配合,只适合"没更好方案时的兜底"。

六、ngram 是怎么把中文切开的

n-gram 是滑窗切词 :用长度为 n 的窗口从头滑到尾,每个位置取窗口内容作为一个 token。

它不需要词典,对中文这种没有空格分词的语言特别友好。以"订单支付失败"为例:

ngram_token_size 切出的 token
1 订、单、支、付、失、败
2(默认) 订单、单支、支付、付失、失败
3 订单支、单支付、支付失、付失败

注意 2 元组切出了"单支""付失"这种无语义跨词组合,这是 n-gram 的固有代价:

索引变大(token 数约等于字符数),也可能带来少量误召回;换来的是零词典维护、零语言依赖 。

思路和 Elasticsearch 的 ngram tokenizer 完全一致,区别只在配置方式------

ES 在 mapping 里指定 analyzer,MySQL 是 WITH PARSER ngram 加一个全局 ngram_token_size。

6.1 ⚠️ 最致命的坑:短于 token size 的关键词搜不到

ngram_token_size 默认 2,于是:

sql 复制代码
-- 表里确实有 content 含 "库" 的行,但结果是 0
SELECT COUNT(*) FROM ticket WHERE MATCH(content) AGAINST('库' IN BOOLEAN MODE);

因为索引里最小单位就是两个字,"库"没进词典自然查不到。

而用户输入经常是单字("票""钱""单"),这是很真实的线上事故来源。两条路:

  1. 改成 1 :写进配置文件 ngram_token_size = 1(它是只读变量 ,不能 SET GLOBAL),
    重启实例并对所有全文索引 OPTIMIZE TABLE 重建。代价是索引体积明显膨胀、写放大更重。
  2. 应用层兜底(更推荐) :保持 2,检测到单字输入时走另一条检索路径
    (按字符拆分的辅助检索列,或 ES)。

这个取舍要提前想清楚,它直接决定索引体积和搜索体验。

七、必须知道的几个参数

参数 作用域 默认 说明
ngram_token_size 全局、只读(需重启) 2 中文分词粒度,1 表示按单字切
innodb_ft_min_token_size InnoDB、动态 3 内置解析器下 token 最小长度,ngram 下不适用
innodb_ft_max_token_size InnoDB、动态 84 token 最大长度,超长的词会被丢弃
ft_min_word_len MyISAM 4 老教程常客,InnoDB 场景跟它没关系
innodb_ft_enable_stopword InnoDB ON 是否启用停止词
innodb_ft_server_stopword_table InnoDB 空 自定义停止词表,格式 库名/表名
innodb_ft_cache_size InnoDB 8MB 每张表的 FT 插入缓存上限

实际动手时,九成情况只需要关心第一行。

八、停止词:中文场景几乎一定要自定义

停止词是不参与索引的词。MySQL 内置的 InnoDB 默认停止词表只有 36 个英文词

(SELECT * FROM information_schema.INNODB_FT_DEFAULT_STOPWORD;),

一来对中文毫无帮助,二来"的""了""是""我们"这类高频无意义字全都被索引,

白白撑大索引并稀释相关性评分。所以中文场景建议自建:

sql 复制代码
CREATE TABLE ft_stopword_cn (value VARCHAR(30) NOT NULL) ENGINE = InnoDB;

INSERT INTO ft_stopword_cn (value) VALUES
  ('的'),('了'),('是'),('在'),('和'),('我'),('你'),('他'),
  ('我们'),('你们'),('他们'),('这'),('那'),('就'),('都');
ini 复制代码
# my.cnf,建议写进配置文件避免重启丢失
[mysqld]
innodb_ft_server_stopword_table = 'demo/ft_stopword_cn'

⚠️ 停止词表是索引构建期 读取的,改完后已有索引不会自动更新 ,必须 OPTIMIZE TABLE ticket; 重建;

重建期间写放大和磁盘占用都会上来,挑低峰做;进了停止词表的字会彻底搜不到

(ngram_token_size=1 时尤其明显),挑选务必谨慎。

九、踩坑合集

坑 1|索引建了,EXPLAIN 的 key 却是 NULL :依次检查 MATCH 的列是否和索引定义完全一致、

搜索词是否被停止词过滤了、是不是搜了短于 token size 的词。

坑 2|ORDER BY 用 MATCH 分数很慢 :MySQL 要把所有命中行的分数算完再排序。

先用普通条件收窄候选集,再做全文排序------别指望优化器自动替你做:

sql 复制代码
SELECT t.id, t.title, MATCH(t.content) AGAINST('支付失败' IN BOOLEAN MODE) AS score
FROM ticket t
JOIN (
  SELECT id FROM ticket
  WHERE created_at >= '2026-01-01' AND status = 1
  ORDER BY created_at DESC LIMIT 5000
) k ON k.id = t.id
WHERE MATCH(t.content) AGAINST('支付失败' IN BOOLEAN MODE)
ORDER BY score DESC;

坑 3|以为多个 FULLTEXT 条件能一起走索引 :一次查询通常只能用一个 全文索引,

多个条件 OR 时可能 merge 也可能退化,拆成两个索引分别查更可控。

坑 4|当成万能加速器 :它对 = 比较、范围扫描、JOIN 一点用没有,只服务于 MATCH ... AGAINST。

坑 5|忽略写入代价 :每插入一行长文本都要分词并更新倒排表。

写多读少的表建议把待检索文本拆到独立窄表,让主表保持轻量。

坑 6|以为高亮、聚合也自带 :MySQL 全文只给"匹配 + 打分",

高亮、facet 聚合、拼音搜索、同义词这些 ES 里开箱即用的能力,MySQL 要么自己做要么没有。

坑 7|分区表 + 全文索引 :这个组合历史上长期受限,各版本支持程度不一,

要用分区就先在本机版本验证,别照抄文档结论。

十、什么时候该换 Elasticsearch

能力 MySQL FULLTEXT Elasticsearch
中文分词 ngram(按字滑窗,无语义) IK / jieba 等,词典可热更新
索引实时性 有 FT cache 延迟 近实时(refresh_interval 可调)
相关度调优 固定 TF-IDF 变体,几乎不可调 BM25 / 自定义打分 / boost
高亮、facet 聚合 ❌ ✅ 原生支持
同义词 / 拼音 / 纠错 ❌ ✅ 插件生态
横向扩容 ❌ 单机 ✅ 原生分片副本
运维复杂度 极低(多一个索引而已) 高(一套独立集群)
数据一致性 强一致,与主库同源 最终一致,需维护同步链路

判断线这样画:数据量几十万到百万级 、QPS 不高、只需要"包含关键词"、

团队不想维护额外组件 → MySQL FULLTEXT 完全够用,性价比极高。而只要命中下面任一条,

就该认真评估 ES------搜索是核心产品能力 (联想词、排序调优、高亮、纠错)、

数据量到千万级以上需要横向扩容、要做复杂相关性工程、多数据源统一检索。

反过来,最容易被低估的是同步链路成本 :上了 ES 就要回答"MySQL 的数据怎么可靠地同步进去",

binlog 订阅、延迟、回溯、双写一致性、索引重建,这一整套运维负担远超一个 FULLTEXT 索引。

十一、小结

  • LIKE '%x%' 因为开头不确定 用不上索引;全文索引把分词提前到写入时构建倒排索引
  • MySQL 8.0 的标准答案:InnoDB + WITH PARSER ngram,彻底抛弃 MyISAM 那套老说法
  • InnoDB 全文索引存在 FT cache 延迟,新插入的数据不一定能立刻被检索到
  • MATCH() 的列必须和某个 FULLTEXT 索引定义完全一致,否则报 1191 错误
  • 三种模式里 布尔模式是生产主力 ,支持 + - * " @n 等运算符
  • 中文最大坑:ngram_token_size 默认 2,导致单字关键词搜不到;改成 1 或在应用层兜底
  • 改了 ngram_token_size、停止词表、token 长度参数后,必须 OPTIMIZE TABLE 重建索引才生效
  • 内置停止词表只有 36 个英文词,中文场景建议自建停用词表
  • 全文索引只做"匹配 + 打分",高亮、聚合、拼音、同义词都不是它的活
  • 选型边界:百万级以内且非核心搜索 → MySQL;千万级以上或搜索是核心能力 → Elasticsearch

下一篇聊另一条路:当 MySQL 真的扛不住时,怎么用最小成本把搜索搬到 Elasticsearch,

以及同步链路怎么设计才不翻车。

相关推荐
java资料站1 小时前
五、Spring AI Alibaba · Memory · Saver(短期会话)
java·spring·microsoft
用户8356290780511 小时前
使用 Python 合并 PowerPoint 演示文稿
后端·python
weixin_461769401 小时前
VS Code 中创建 Jupyter 文件(.ipynb)
ide·python·jupyter
2601_966949651 小时前
多市场量化策略的数据接口应该如何设计:从数据层架构到策略接入
开发语言·python·数据分析·pandas·量化交易·股票数据·quantdash
全栈弄潮儿2 小时前
Python实战第1期:Python环境搭建与第一个程序
python
心易行者2 小时前
Agent应用+API端点商业化进阶实战:从单体智能体到可付费调用的API全流程
运维·服务器·人工智能·python·apache
夜之眷属2 小时前
Core dump 崩溃排查:JVM 宕机后,那份 core 文件怎么用 gdb 还原现场
java·运维·服务器·jvm
weixin199701080162 小时前
《1688图片空间API踩坑:img.upload 与 album.* 的防盗链与CDN缓存问题》(附Python源码)
开发语言·python·缓存
波加曼大王2 小时前
# vLLM不要迷信PagedAttention神话,聊聊线上藏着的内部碎片陷阱
java·架构