第 39 章
全文检索
MySQL 全文检索
MySQL InnoDB 引擎从 5.6 版本开始支持全文索引(FULLTEXT Index)。与 LIKE '%keyword%' 全表扫描不同,全文检索使用倒排索引(Inverted Index)实现高效的文本搜索,并支持相关度排序。MySQL 8.0 还引入了 ngram 分词器,为中文搜索提供了原生支持。
全文索引基础
-- 创建全文索引
CREATE TABLE articles (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
title VARCHAR(200) NOT NULL,
content TEXT,
author VARCHAR(100),
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
FULLTEXT INDEX ft_title_content (title, content) -- 多列全文索引
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 在现有表上添加全文索引
ALTER TABLE articles ADD FULLTEXT INDEX ft_content (content);
CREATE FULLTEXT INDEX ft_title ON articles (title);
-- 全文索引要求:
-- 列类型必须是 CHAR, VARCHAR, TEXT
-- 只支持 InnoDB 和 MyISAM 引擎
-- 字符集必须是 utf8 系列(中文需要 ngram)
InnoDB 全文索引的底层结构
-- 全文索引存储在 6 个辅助表中
SELECT * FROM information_schema.INNODB_FT_INDEX_TABLE LIMIT 5;
-- WORD: 词语
-- FIRST_DOC_ID, LAST_DOC_ID: 文档 ID 范围
-- DOC_COUNT: 包含该词的文档数
-- DOC_ID: 具体文档 ID
-- POSITION: 在文档中的位置
-- 查看全文索引缓存(插入时先进缓存,达到阈值后合并)
SELECT * FROM information_schema.INNODB_FT_INDEX_CACHE LIMIT 5;
-- 手动刷新全文索引缓存到磁盘(查询时自动触发,一般不需要手动)
SET GLOBAL innodb_optimize_fulltext_only = ON;
OPTIMIZE TABLE articles;
SET GLOBAL innodb_optimize_fulltext_only = OFF;
自然语言模式(默认)
自然语言模式(Natural Language Mode)是默认的全文搜索模式,返回相关度得分,自动过滤停用词和过于常见的词(出现在超过 50% 文档中的词)。
-- 基础自然语言搜索
SELECT id, title,
MATCH(title, content) AGAINST ('MySQL performance tuning') AS relevance
FROM articles
WHERE MATCH(title, content) AGAINST ('MySQL performance tuning')
ORDER BY relevance DESC
LIMIT 10;
-- 注意:MATCH...AGAINST 必须同时出现在 WHERE 和 SELECT 中
-- WHERE 中的 MATCH 触发索引扫描,SELECT 中的 MATCH 获取相关度分数
-- 不写 ORDER BY relevance 时,MySQL 通常也会按相关度排序
-- 但显式写出更安全(某些情况下不保证顺序)
-- 限制:
-- 1. 默认最短词长 innodb_ft_min_token_size = 3(少于3字符的词被忽略)
-- 2. 过于常见的词(出现在50%以上文档中)被视为停用词
-- 3. 默认停用词列表包含: a, an, the, is, are... 等英文词
配置参数
-- 修改最小词长(需要重建索引后生效)
SET GLOBAL innodb_ft_min_token_size = 2;
-- 查看当前停用词
SELECT * FROM information_schema.INNODB_FT_DEFAULT_STOPWORD;
-- 自定义停用词表
CREATE TABLE my_stopwords (value VARCHAR(30));
INSERT INTO my_stopwords VALUES ('and'), ('or'), ('the');
SET GLOBAL innodb_ft_server_stopword_table = 'mydb/my_stopwords';
-- 禁用停用词(中文环境通常需要)
SET GLOBAL innodb_ft_enable_stopword = OFF;
布尔模式
布尔模式(Boolean Mode)提供精确的搜索控制,支持 + - * "" ~ 等操作符,不计算相关度得分(除非显式计算)。
-- 布尔模式操作符
-- +word : 必须包含
-- -word : 必须不包含
-- word* : 通配符(以 word 开头)
-- "phrase" : 精确短语
-- ~word : 降低包含该词的相关度
-- (expr) : 分组
-- 必须包含 MySQL,可以包含 performance,不能包含 deprecated
SELECT * FROM articles
WHERE MATCH(title, content) AGAINST
('+MySQL performance -deprecated' IN BOOLEAN MODE)
ORDER BY MATCH(title, content) AGAINST
('+MySQL performance -deprecated' IN BOOLEAN MODE) DESC;
-- 精确短语搜索
SELECT * FROM articles
WHERE MATCH(title, content) AGAINST
('"query optimization"' IN BOOLEAN MODE);
-- 通配符搜索(搜索所有以 optim 开头的词)
SELECT * FROM articles
WHERE MATCH(title, content) AGAINST ('optim*' IN BOOLEAN MODE);
-- 匹配: optimize, optimization, optimizer...
-- 复合布尔查询
SELECT * FROM articles
WHERE MATCH(title, content) AGAINST
('+(MySQL PostgreSQL) +("performance" "tuning") -("NoSQL")' IN BOOLEAN MODE);
-- 必须包含 MySQL 或 PostgreSQL,
-- 必须包含 performance 或 tuning 短语,
-- 不能包含 NoSQL 短语
查询扩展
查询扩展(Query Expansion)分两步:先找相关文档,再从这些文档提取高频词,用这些词再次搜索,找到更多相关结果(用于搜索词较短时扩大召回)。
-- 自动查询扩展
SELECT id, title
FROM articles
WHERE MATCH(title, content) AGAINST ('database' WITH QUERY EXPANSION)
LIMIT 20;
-- 第一步搜索 "database" 找到相关文档
-- 从结果中提取高频词(如 MySQL、SQL、index...)
-- 第二步用扩展词重新搜索,返回更多结果
-- 注意:查询扩展会增加执行时间,不适合实时搜索
-- 适合:搜索词很短、需要提高召回率的场景
中文全文检索(ngram 分词器)
英文通过空格分词,而中文没有自然的词界。MySQL 8.0 内置的 ngram 分词器将文本按 N 个字符滑动切割(默认 N=2),实现中文搜索。
-- 查看 ngram 配置
SHOW VARIABLES LIKE 'ngram_token_size';
-- ngram_token_size = 2 (默认,二字分词)
-- 创建使用 ngram 的全文索引
CREATE TABLE articles_cn (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
title VARCHAR(200) NOT NULL,
content TEXT,
PRIMARY KEY (id),
FULLTEXT INDEX ft_content_ngram (title, content) WITH PARSER ngram
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- "高性能 MySQL 数据库优化" 被 ngram(2) 分词为:
-- 高性 性能 能M My yS SQ QL L数 数据 据库 库优 优化
-- 搜索(ngram 模式不区分分词,直接匹配子串)
SELECT id, title,
MATCH(title, content) AGAINST ('数据库优化' IN NATURAL LANGUAGE MODE) AS score
FROM articles_cn
WHERE MATCH(title, content) AGAINST ('数据库优化' IN NATURAL LANGUAGE MODE)
ORDER BY score DESC;
-- 布尔模式下的中文搜索
SELECT * FROM articles_cn
WHERE MATCH(title, content) AGAINST ('+数据库 +优化 -降级' IN BOOLEAN MODE);
-- ngram 搜索注意事项:
-- 搜索词少于 ngram_token_size 个字符的词会被忽略
-- 解决方案:修改 ngram_token_size=1(单字搜索,索引更大)
-- my.cnf 配置
[mysqld]
ngram_token_size = 2 -- 对中文通常是 2
innodb_ft_min_token_size = 2
中文搜索示例:电商商品搜索
CREATE TABLE products (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(200) NOT NULL,
description TEXT,
tags VARCHAR(500),
PRIMARY KEY (id),
FULLTEXT INDEX ft_product (name, description, tags) WITH PARSER ngram
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 搜索"防晒 夏季"(布尔模式,两个词都要有)
SELECT id, name,
MATCH(name,description,tags) AGAINST ('+防晒 +夏季' IN BOOLEAN MODE) AS score
FROM products
WHERE MATCH(name,description,tags) AGAINST ('+防晒 +夏季' IN BOOLEAN MODE)
ORDER BY score DESC
LIMIT 20;
-- 搜索"防晒霜"(精确短语)
SELECT * FROM products
WHERE MATCH(name,description,tags) AGAINST ('"防晒霜"' IN BOOLEAN MODE);
相关度排序优化
-- 综合相关度:标题权重 > 描述权重
SELECT
id,
name,
-- 标题匹配权重 × 3 + 描述匹配权重
MATCH(name) AGAINST ('MySQL优化' IN NATURAL LANGUAGE MODE) * 3
+ MATCH(description) AGAINST ('MySQL优化' IN NATURAL LANGUAGE MODE) AS weighted_score
FROM products
WHERE MATCH(name, description) AGAINST ('MySQL优化' IN NATURAL LANGUAGE MODE)
ORDER BY weighted_score DESC
LIMIT 20;
-- 混合相关度:全文相关度 + 时间衰减
SELECT
id, title, created_at,
MATCH(title,content) AGAINST (?) AS ft_score,
-- 时间衰减:越新的文章得分更高
EXP(-0.01 * DATEDIFF(NOW(), created_at)) AS time_decay,
MATCH(title,content) AGAINST (?) * EXP(-0.01 * DATEDIFF(NOW(), created_at)) AS final_score
FROM articles
WHERE MATCH(title,content) AGAINST (?)
ORDER BY final_score DESC
LIMIT 20;
优化与限制
| 限制 | 说明 | 解决方案 |
|---|---|---|
| 不支持中文分词 | ngram 是滑动窗口,非真正中文分词 | 使用 Elasticsearch + IK 分词器 |
| 高频词(停用词)被忽略 | "的 了 是"等词被过滤 | 关闭 innodb_ft_enable_stopword |
| 短词被忽略 | 少于 min_token_size 的词 | 减小 innodb_ft_min_token_size |
| 不支持模糊匹配(拼音/错别字) | 输入错误无法匹配 | Elasticsearch 的 fuzzy 查询 |
| 不支持同义词搜索 | "手机"搜不到"手持设备" | Elasticsearch 同义词词库 |
| 大文本表性能 | 全文索引文件可能很大 | 分区、单独全文索引表 |
-- 全文索引性能调优
-- 1. 增大全文索引缓存(减少磁盘 I/O)
SET GLOBAL innodb_ft_cache_size = 16000000; -- 单索引 16MB
SET GLOBAL innodb_ft_total_cache_size = 128000000; -- 总缓存 128MB
-- 2. 批量插入时先禁用全文索引,插入完再重建(快 5-10 倍)
ALTER TABLE articles DISABLE KEYS;
-- ... 大批量 INSERT ...
ALTER TABLE articles ENABLE KEYS;
-- 3. 查询时带其他条件先过滤再全文搜索
SELECT * FROM articles
WHERE created_at > '2024-01-01' -- 先用普通索引过滤
AND MATCH(title,content) AGAINST (?); -- 再全文搜索
MySQL 全文检索 vs Elasticsearch
| 维度 | MySQL 全文索引 | Elasticsearch |
|---|---|---|
| 中文分词 | ngram(滑动窗口) | IK 分词器(智能分词) |
| 模糊匹配 | 不支持 | 支持(fuzzy query) |
| 同义词 | 不支持 | 支持(同义词词库) |
| 搜索性能(大规模) | 千万级文档慢 | 亿级文档仍高效 |
| 数据一致性 | 实时一致(ACID) | 近实时(1秒延迟) |
| 运维成本 | 低(已有 MySQL) | 高(独立集群) |
| 与业务数据结合 | 原生 JOIN | 需要 CDC 同步 |
| 高亮/摘要提取 | 不支持 | 支持 |
| 适用规模 | <1000 万文档 | 亿级以上 |
选型建议:数据量 <500 万、搜索需求简单(关键词匹配即可)、团队不想引入 Elasticsearch 运维成本 → 使用 MySQL 全文索引。数据量 >1000 万、需要中文智能分词/模糊搜索/搜索建议/高亮显示 → Elasticsearch 是更好的选择,通过 Canal/Debezium CDC 实时同步 MySQL 数据到 ES。