MySQL 全文索引 — FULLTEXT

FULLTEXT 索引是 MySQL 中专用于**全文检索(Full-Text Search)的索引类型。它通过倒排索引(Inverted Index)**实现对文本内容的快速关键词搜索,是替代 LIKE '%keyword%' 的高效方案。


与其他索引的核心区别

维度B+Tree 索引FULLTEXT 索引
底层结构B+Tree(有序平衡树)倒排索引(词→文档列表映射)
匹配方式前缀匹配(LIKE 'abc%'分词匹配(关键词/短语搜索)
适用场景精确匹配、范围查询、排序文本内容搜索、文章检索
查询语法WHERE col LIKE '...'MATCH(col) AGAINST('...')
排序能力需额外 ORDER BY内置相关性评分排序
大数据量性能全表 LIKE 无法走索引可高效并行搜索大文本

LIKE '%keyword%' 为什么慢

LIKE '%keyword%' 前缀通配导致 B+Tree 无法从中间导航,必须全表扫描 + 逐行匹配。而 FULLTEXT 通过倒排索引直接定位包含该关键词的行,性能数量级优于 LIKE。详见 五、面试高频场景:什么情况下索引会失效


工作原理

1. 分词(Tokenization)

建索引时,MySQL 将文本按照分词规则拆分为独立的词(Token):

-- 原始文本
"The quick brown fox jumps over the lazy dog"
 
-- 分词结果(去除停用词后)
['quick', 'brown', 'fox', 'jumps', 'over', 'lazy', 'dog']

2. 倒排索引结构

┌─────────────┬──────────────────────────┐
│    word     │    document_list         │
├─────────────┼──────────────────────────┤
│ quick       │ row_id_1, row_id_5       │
│ brown       │ row_id_1                 │
│ fox         │ row_id_1, row_id_3       │
│ jumps       │ row_id_1, row_id_7       │
│ lazy        │ row_id_1, row_id_3       │
│ dog         │ row_id_1                 │
│ database    │ row_id_2, row_id_4       │
│ index       │ row_id_2, row_id_6       │
└─────────────┴──────────────────────────┘
  • 词(Word):分词后的关键词
  • 文档列表(Document List):包含该词的行记录 ID 列表
  • 每个词还附加位置信息词频,用于相关性评分

对比 B+Tree

B+Tree 是”从记录到列值”的正向映射(给定范围,找到记录),而倒排索引是”从词到记录”的反向映射(给定词,找到所有包含它的记录)。这正是”倒排”名称的由来。

3. 相关性评分

MySQL 默认使用 TF/IDF 算法 对搜索结果排序(MySQL 8.0 起支持 BM25):

  • TF(Term Frequency):词在文档中出现的频率 → 出现越多越相关
  • IDF(Inverse Document Frequency):词在整个文档集合中的稀缺度 → 越稀缺权重越高
  • N:文档总数
  • df_t:包含该词的文档数

TF-IDF 的本质

结果按相关性降序返回,相关性高的结果靠前。如果一个词在很多文档中都出现(如 “the”),IDF 很低 → 对评分贡献小。如果一个词很少出现(如 “MySQL”),IDF 很高 → 匹配到的文档排名靠前。


语法与使用

创建 FULLTEXT 索引

-- 建表时定义
CREATE TABLE article (
    id INT PRIMARY KEY,
    title VARCHAR(200),
    body TEXT,
    FULLTEXT idx_fulltext (title, body)   -- 可联合多列
) ENGINE=InnoDB;
 
-- 已存在的表添加
CREATE FULLTEXT INDEX idx_fulltext ON article(title, body);
 
-- 或使用 ALTER
ALTER TABLE article ADD FULLTEXT INDEX idx_fulltext (title, body);

多列全文索引

FULLTEXT 索引可以包含多个列(如上例的 title, body),查询时 MATCH 必须指定相同的列集合,否则索引无法使用。

查询语法

SELECT * FROM article
WHERE MATCH(title, body) AGAINST('database index' IN NATURAL LANGUAGE MODE);

MATCH 的列列表必须与 FULLTEXT 索引定义完全一致。


三种搜索模式

1️⃣ 自然语言模式(NATURAL LANGUAGE MODE)默认

-- 默认就是自然语言模式
SELECT id, title, MATCH(body) AGAINST('database index') AS relevance
FROM article
WHERE MATCH(body) AGAINST('database index');
 
-- 等价于
WHERE MATCH(body) AGAINST('database index' IN NATURAL LANGUAGE MODE);

特点:

  • 返回按相关性评分降序排列的结果
  • MySQL 自动过滤停用词(Stopwords)
  • 长度低于 innodb_ft_min_token_size 的词被忽略(默认 3)
  • 表中行数少于一定数量(约 50% 行匹配)时可能返回空结果(即50% 阈值,InnoDB 已取消此限制)

2️⃣ 布尔模式(BOOLEAN MODE)

SELECT * FROM article
WHERE MATCH(body) AGAINST('+database -nosql' IN BOOLEAN MODE);

操作符:

操作符含义示例说明
+必须包含+database结果必须包含 database
-必须排除-nosql结果不能包含 nosql
(空)可选,但出现则排名更高database index包含任意词即返回,含多个词排名更高
>提高该词权重>database包含该词会显著提高排名
<降低该词权重<nosql包含该词会降低排名
*通配符(后缀)data*匹配以 data 开头的词(如 database, data)
""精确短语"database index"精确匹配整个短语
()子表达式分组+(database index)括号内操作符优先

组合示例:

-- 必须包含 "MySQL",可以包含 "index"(有则加分),不能包含 "Oracle"
SELECT * FROM article
WHERE MATCH(title, body)
AGAINST('+MySQL index -Oracle' IN BOOLEAN MODE);
 
-- 必须包含 "database",同时匹配以 "index" 开头的词
SELECT * FROM article
WHERE MATCH(title, body)
AGAINST('+database index*' IN BOOLEAN MODE);
 
-- 精确短语匹配
SELECT * FROM article
WHERE MATCH(title, body)
AGAINST('"inverted index"' IN BOOLEAN MODE);

3️⃣ 查询扩展模式(QUERY EXTENSION)

SELECT * FROM article
WHERE MATCH(body) AGAINST('database' WITH QUERY EXPANSION);

执行流程(两阶段搜索):

  1. 第一阶段:使用原始关键词(database)进行全文搜索,找出相关文档
  2. 第二阶段:从第一阶段结果中提取高频且相关的词,组成新查询再搜索一次

谨慎使用查询扩展

查询扩展会显著增加结果数量,可能引入大量不相关内容。适用于”找相关文档”的场景但噪声较大。建议仅在搜索结果太少时使用。


BM25 算法(MySQL 8.0+)

MySQL 8.0 起,InnoDB 全文索引引入 BM25(Best Matching 25) 算法作为相关性评分选项,相比传统的 TF/IDF 更为精准:

-- 查看当前相关性算法(默认或变量)
SHOW VARIABLES LIKE 'innodb_ft_use_bm25';

BM25 引入了两个可调参数:

  • k1(默认 0.5):控制词频饱和程度,越大词频影响越大
  • b(默认 0.6):控制文档长度归一化,b=1 时完全归一化,b=0 时忽略文档长度

BM25 相比 TF/IDF 的优势:

  • 避免长文档天然获得更高词频的不公平
  • 词频非线性增长(饱和函数),防止高频词的过度影响
  • 在长文档上的表现更优

中文全文检索(n-gram 解析器)

MySQL 默认采用空格/标点分词,不适用于中文。MySQL 5.7.6+ 提供了内置的 n-gram 全文解析器(ngram parser) 来支持中、日、韩(CJK)语言。

创建 n-gram 全文索引

-- 使用 ngram 解析器创建全文索引
CREATE TABLE article (
    id INT PRIMARY KEY,
    title VARCHAR(200),
    body TEXT,
    FULLTEXT INDEX idx_body (body) WITH PARSER ngram
) ENGINE=InnoDB;
 
-- 或对已有表
CREATE FULLTEXT INDEX idx_body ON article(body) WITH PARSER ngram;

n-gram 原理

n-gram 将文本按连续 n 个字符为单位进行切分:

-- ngram_token_size = 2(默认值)
-- 原文:"数据库索引"
-- 分词结果:['数据', '据库', '库索', '索引']
 
-- ngram_token_size = 3
-- 原文:"数据库索引"
-- 分词结果:['数据库', '据库索', '库索引']

ngram_token_size 选择

  • ngram_token_size=2 → 更细粒度,召回率更高,但可能产生噪声
  • ngram_token_size=3 → 更精准,但可能漏掉 2 字词
  • 默认值为 2,在大多数场景下表现均衡
  • 可在配置文件或启动参数中设置:ngram_token_size=2

查询 n-gram 全文索引

SELECT * FROM article
WHERE MATCH(body) AGAINST('数据库' IN BOOLEAN MODE);

MySQL 会将查询词 数据库 按同样的 n-gram 规则拆分为 '数据 据库 库索 索引'(n=2)后进行匹配。

n-gram 的限制

  • ngram 解析器不识别语义,纯按字符滑动窗口切分,会产生无意义的 Token(如 库索
  • 对于专业术语或生僻词,可能需要搭配布尔模式精确短语搜索
  • 真正的中文分词需要专业搜索引擎(如 Elasticsearch ES + IK 分词器)

配置参数

InnoDB 全文索引相关参数

参数默认值说明
innodb_ft_min_token_size3索引的最小词长度(InnoDB),可设为 1 支持单字母搜索
innodb_ft_max_token_size84索引的最大词长度
innodb_ft_server_stopword_table内置停用词表自定义停用词表(InnoDB),指向一个包含 value 列的表
innodb_ft_user_stopword_table当前会话级别的停用词表
innodb_ft_enable_stopwordON是否启用停用词过滤
innodb_ft_use_bm25OFF是否使用 BM25 相关性算法(MySQL 8.0+)
ngram_token_size2n-gram 分词窗口大小(使用 ngram parser 时)

MyISAM 全文索引相关参数

参数默认值说明
ft_min_word_len4索引的最小词长度(MyISAM)
ft_max_word_len84索引的最大词长度
ft_stopword_file内置停用词文件路径
ft_boolean_syntax`+ <()~*:""&`

修改参数后需要重建索引

修改 innodb_ft_min_token_size 等参数后,需 REPAIR TABLEOPTIMIZE TABLE 重建 FULLTEXT 索引才会生效。


性能考量与局限

适用场景

  • 博客文章、新闻内容的站内搜索
  • 产品名称/商品描述的关键词匹配
  • 小型 CMS 系统的文本检索
  • 数据量在千万级以下的文本搜索

不适用场景

场景推荐方案原因
高并发实时搜索(毫秒级)Elasticsearch ES / SolrMySQL FULLTEXT 在并发高、数据量大时性能下降明显
全文搜索是核心功能专业搜索引擎分词精度、聚合分析、分布式能力远不如 ES
海量数据 (>1 亿行)Elasticsearch倒排索引在分布式引擎中更高效,支持水平扩展
高级中文分词需求ES + IK 分词器n-gram 纯字符滑动,无语义理解
多维度组合搜索ElasticsearchES 的 DSL 支持结构化 + 全文搜索的组合查询

性能注意事项

-- ✅ 良好实践:使用覆盖索引避免回表
-- 如果 FULLTEXT 在 body 列,查询仅返回 id
-- (id 在主键中,无需回表)
SELECT id FROM article
WHERE MATCH(body) AGAINST('database');
 
-- ⚠️ 回表查询:需要额外数据时
SELECT id, title, body FROM article   -- body 长文本,回表拉取
WHERE MATCH(body) AGAINST('database');
  • 回表查询:FULLTEXT 索引找到匹配行的主键后,如果 SELECT 需要更多列,需回聚簇索引获取完整数据
  • DML 性能:FULLTEXT 索引维护成本高,高频 INSERT/UPDATE 场景写入会变慢
  • 内存占用:倒排索引在内存中的缓存开销较大,需关注 innodb_buffer_pool_size

常见问题

Q1:为什么 MATCH 匹配不到结果?

-- 可能原因排查清单
-- 1️⃣ 检查最小词长限制(默认 3,短词 'is'、'go' 被忽略)
SHOW VARIABLES LIKE 'innodb_ft_min_token_size';
 
-- 2️⃣ 检查是否有停用词
-- 'the'、'a'、'an' 等常见词被过滤
SHOW VARIABLES LIKE 'innodb_ft_enable_stopword';
 
-- 3️⃣ 检查索引是否已建好
SHOW INDEX FROM article;
 
-- 4️⃣ 检查索引是否包含对应列
-- MATCH 列必须与 FULLTEXT 索引定义完全一致

Q2:FULLTEXT 与 LIKE '%keyword%' 能否互相替代?

不能完全替代。LIKE '%keyword%' 的子串匹配能力更强(可以匹配词中的任意部分),但性能极差。FULLTEXT 基于分词匹配,性能好且支持排序,但无法做子串匹配。各有适用场景。

Q3:InnoDB vs MyISAM 哪个 FULLTEXT 更好?

MySQL 5.6+ 优先选择 InnoDB,因为 InnoDB 支持事务、行级锁、外键,FULLTEXT 功能已与 MyISAM 持平(甚至更多)。MyISAM 的 FULLTEXT 只在 5.6 之前的系统中存在遗留使用。

Q4:FULLTEXT 索引占多少空间?

倒排索引通常占文本数据的 50%~100%,比 B+Tree 索引更”重”。如果全文索引列是 TEXT 长字段,索引体积可能超过数据本身,需规划好磁盘空间。


相关笔记