全文索引 - Full-Text Index详解
定义
全文索引 (Full-Text Index) 是一种专门用于文本内容搜索的索引结构,它通过对文本进行分词(Tokenization),建立从单词(词条)到文档的映射关系(倒排索引),从而实现高效的全文检索。与传统的B+Tree索引不同,全文索引支持模糊匹配、相关性排序、布尔搜索等高级搜索功能。
核心特征
| 特征 | 说明 |
|---|---|
| 索引结构 | 倒排索引(Inverted Index) |
| 分词机制 | 按空格、标点、语言规则分词 |
| 搜索模式 | 自然语言、布尔、查询扩展 |
| 相关性排序 | TF-IDF算法计算相关度 |
| 适用字段 | VARCHAR、TEXT、CHAR |
| 最小词长 | 默认3字符(可配置) |
| 停用词过滤 | 自动忽略常见词(the, is等) |
与传统索引对比
B+Tree索引 (传统索引)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
• 精确匹配或前缀匹配
• WHERE name = 'Alice'
• WHERE name LIKE 'Ali%'
• ✗ 不支持 WHERE name LIKE '%ice'
• ✗ 不支持语义搜索
全文索引 (倒排索引)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
• 分词后建立词条→文档映射
• MATCH(content) AGAINST('database search')
• ✓ 支持模糊匹配
• ✓ 支持相关性排序
• ✓ 支持布尔逻辑(AND/OR/NOT)
• ✓ 支持同义词扩展倒排索引原理
数据结构
原始文档:
Doc1: "MySQL database is fast"
Doc2: "PostgreSQL database is powerful"
Doc3: "MySQL is open source"
正向索引 (Forward Index):
┌──────┬──────────────────────────┐
│ DocID│ Content │
├──────┼──────────────────────────┤
│ 1 │ MySQL database is fast │
│ 2 │ PostgreSQL database... │
│ 3 │ MySQL is open source │
└──────┴──────────────────────────┘
倒排索引 (Inverted Index):
┌───────────┬──────────────────┐
│ Term │ Document List │
├───────────┼──────────────────┤
│ MySQL │ [Doc1, Doc3] │
│ database │ [Doc1, Doc2] │
│ is │ [Doc1, Doc2, Doc3]│
│ fast │ [Doc1] │
│ PostgreSQL│ [Doc2] │
│ powerful │ [Doc2] │
│ open │ [Doc3] │
│ source │ [Doc3] │
└───────────┴──────────────────┘
搜索 "MySQL database":
1. 查找词条: MySQL → [Doc1, Doc3]
2. 查找词条: database → [Doc1, Doc2]
3. 取交集: [Doc1]
4. 返回结果: Doc1 ✓InnoDB全文索引实现
c
/* storage/innobase/fts/fts0fts.h */
/**
* 全文索引数据结构
*/
struct fts_index_t {
dict_index_t* index; /* InnoDB索引对象 */
/* 倒排列表 */
fts_inverted_index_t* inverted_idx;
/* 分词器 */
fts_parser_t* parser;
/* 缓存 */
fts_cache_t* cache;
};
/**
* 倒排列表项
*/
struct fts_doc_list_t {
doc_id_t doc_id; /* 文档ID */
/* 位置信息(用于短语搜索) */
ulint* positions;
ulint n_positions;
/* TF-IDF信息 */
float tf; /* 词频 */
float idf; /* 逆文档频率 */
float score; /* 相关性得分 */
};MySQL全文索引实现
创建全文索引
sql
-- 方式1: 创建表时定义
CREATE TABLE articles (
id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(200),
content TEXT,
author VARCHAR(100),
created_at DATETIME,
-- 创建全文索引
FULLTEXT INDEX ft_idx_title_content (title, content)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 方式2: 对现有表添加
ALTER TABLE articles
ADD FULLTEXT INDEX ft_idx_content (content);
-- 方式3: 单独创建
CREATE FULLTEXT INDEX ft_idx_title ON articles(title);基本搜索
sql
-- 1. 自然语言搜索(默认模式)
SELECT
id,
title,
MATCH(title, content) AGAINST('database optimization') as relevance
FROM articles
WHERE MATCH(title, content) AGAINST('database optimization')
ORDER BY relevance DESC;
-- 输出:
-- +----+----------------------------------+------------+
-- | id | title | relevance |
-- +----+----------------------------------+------------+
-- | 5 | MySQL Database Optimization | 2.345 |
-- | 12 | Database Performance Tuning | 1.892 |
-- | 8 | NoSQL vs SQL Databases | 1.234 |
-- +----+----------------------------------+------------+
-- 2. 布尔搜索(支持逻辑运算符)
SELECT id, title
FROM articles
WHERE MATCH(title, content) AGAINST('+MySQL +database -MongoDB' IN BOOLEAN MODE);
-- 含义: 必须包含MySQL和database,不能包含MongoDB
-- 3. 查询扩展搜索(相关词扩展)
SELECT id, title
FROM articles
WHERE MATCH(title, content) AGAINST('MySQL' WITH QUERY EXPANSION);
-- 会同时搜索: MySQL, database, InnoDB, SQL等相关词布尔搜索操作符
sql
-- 操作符说明
+word 必须包含
-word 必须不包含
word* 通配符(前缀匹配)
"phrase" 精确短语匹配
>word 提高相关性
<word 降低相关性
~word 否定相关性(软排除)
() 分组
-- 示例1: 复杂布尔搜索
SELECT id, title
FROM articles
WHERE MATCH(content) AGAINST(
'+InnoDB +(performance tuning) -beginner >advanced <basic'
IN BOOLEAN MODE
);
-- 含义:
-- ✓ 必须包含 InnoDB
-- ✓ 必须包含 performance 和 tuning(短语)
-- ✗ 不能包含 beginner
-- ↑ 提高 advanced 的权重
-- ↓ 降低 basic 的权重
-- 示例2: 通配符搜索
SELECT id, title
FROM articles
WHERE MATCH(content) AGAINST('optim*' IN BOOLEAN MODE);
-- 匹配: optimize, optimization, optimizer, optimal...
-- 示例3: 短语搜索
SELECT id, title
FROM articles
WHERE MATCH(content) AGAINST('"machine learning"' IN BOOLEAN MODE);
-- 只匹配完整短语 "machine learning"
-- 不匹配单独的 machine 或 learningInnoDB源码分析
分词流程
cpp
/* storage/innobase/fts/fts0parser.cc */
/**
* 全文索引分词器
* @param text 输入文本
* @param tokens 输出的词条数组
*/
void fts_parse_text(
const char* text,
fts_token_list_t* tokens)
{
const char* ptr = text;
const char* word_start;
ulint word_len;
while (*ptr != '\0') {
/* 1. 跳过空白和标点符号 */
while (*ptr && !is_word_char(*ptr)) {
ptr++;
}
if (*ptr == '\0') {
break;
}
/* 2. 记录单词起始位置 */
word_start = ptr;
/* 3. 找到单词结束位置 */
while (*ptr && is_word_char(*ptr)) {
ptr++;
}
word_len = ptr - word_start;
/* 4. 检查最小词长 */
if (word_len < fts_min_word_size) {
continue; /* 跳过太短的词 */
}
/* 5. 检查停用词 */
if (fts_is_stopword(word_start, word_len)) {
continue; /* 跳过停用词 */
}
/* 6. 转换为小写 */
char* lower_word = fts_to_lowercase(word_start, word_len);
/* 7. 添加到词条列表 */
fts_add_token(tokens, lower_word, word_start - text);
}
}
/**
* 判断是否是单词字符
*/
bool is_word_char(char c)
{
return isalnum(c) || c == '_';
}构建倒排索引
cpp
/* storage/innobase/fts/fts0fts.cc */
/**
* 为文档构建全文索引
* @param doc_id 文档ID
* @param text 文档内容
*/
void fts_index_document(doc_id_t doc_id, const char* text)
{
fts_token_list_t tokens;
fts_token_t* token;
/* 1. 分词 */
fts_parse_text(text, &tokens);
/* 2. 遍历每个词条 */
FOR_EACH_TOKEN(&tokens, token) {
const char* word = token->word;
ulint position = token->position;
/* 3. 查找或创建倒排列表 */
fts_inverted_list_t* inv_list =
fts_get_inverted_list(word);
if (inv_list == NULL) {
/* 创建新的倒排列表 */
inv_list = fts_create_inverted_list(word);
}
/* 4. 添加文档引用 */
fts_add_doc_to_list(inv_list, doc_id, position);
/* 5. 更新TF-IDF统计 */
fts_update_tf_idf(inv_list, doc_id);
}
/* 6. 写入磁盘(异步) */
fts_flush_to_disk();
}
/**
* 添加文档到倒排列表
*/
void fts_add_doc_to_list(
fts_inverted_list_t* list,
doc_id_t doc_id,
ulint position)
{
fts_posting_t* posting;
/* 分配posting节点 */
posting = (fts_posting_t*)ut_malloc(sizeof(fts_posting_t));
posting->doc_id = doc_id;
posting->position = position;
/* 插入到 postings 链表(按doc_id排序) */
UT_LIST_INSERT_LAST(list->postings, posting);
/* 更新文档频率 */
list->doc_freq++;
}搜索算法
cpp
/* storage/innobase/fts/fts0search.cc */
/**
* 全文搜索
* @param query 搜索查询
* @param mode 搜索模式(NATURAL/BOOLEAN)
* @return 匹配的文档列表(按相关性排序)
*/
fts_result_t* fts_search(
const char* query,
fts_search_mode_t mode)
{
fts_token_list_t query_tokens;
fts_result_t* result;
/* 1. 解析查询语句 */
fts_parse_query(query, &query_tokens, mode);
/* 2. 初始化结果集 */
result = fts_result_create();
/* 3. 对每个查询词条,获取文档列表 */
fts_token_t* token;
FOR_EACH_TOKEN(&query_tokens, token) {
fts_inverted_list_t* inv_list =
fts_get_inverted_list(token->word);
if (inv_list == NULL) {
continue; /* 词条不存在 */
}
/* 4. 合并结果集 */
switch (mode) {
case FTS_MODE_NATURAL:
/* 自然语言: 并集,计算相关性 */
fts_merge_results_union(result, inv_list);
break;
case FTS_MODE_BOOLEAN:
/* 布尔搜索: 根据操作符合并 */
if (token->operator == FTS_OP_AND) {
fts_merge_results_intersect(result, inv_list);
} else if (token->operator == FTS_OP_OR) {
fts_merge_results_union(result, inv_list);
} else if (token->operator == FTS_OP_NOT) {
fts_merge_results_exclude(result, inv_list);
}
break;
}
}
/* 5. 按相关性排序 */
fts_sort_by_relevance(result);
return result;
}
/**
* 计算TF-IDF相关性得分
*/
float fts_calc_relevance(
const char* term,
doc_id_t doc_id)
{
/* TF: 词频(Term Frequency) */
float tf = get_term_frequency(term, doc_id);
/* IDF: 逆文档频率(Inverse Document Frequency) */
float idf = log(total_docs / get_doc_frequency(term));
/* TF-IDF得分 */
return tf * idf;
}性能优化
配置参数
ini
[mysqld]
# 最小搜索词长(默认3)
# 减小可搜索更短的词,但索引会变大
ft_min_word_len = 2
# 最大搜索词长(默认84)
ft_max_word_len = 84
# 停用词文件路径
# 可自定义停用词列表
ft_stopword_file = /path/to/stopwords.txt
# 布尔模式操作符
ft_boolean_syntax = '+ -><()~*:""&|'
# InnoDB专用: 全文索引缓存大小
innodb_ft_cache_size = 8000000 # 8MB
# InnoDB专用: 总缓存大小(所有事务)
innodb_ft_total_cache_size = 640000000 # 640MB
# InnoDB专用: 优化频率
innodb_ft_num_word_optimize = 2000索引优化
sql
-- 1. 多列全文索引
CREATE FULLTEXT INDEX ft_multi ON articles(title, content, tags);
-- 搜索时可以使用任意组合
MATCH(title, content) AGAINST('keyword')
MATCH(title) AGAINST('keyword')
-- 2. 单独索引不同字段(更灵活)
CREATE FULLTEXT INDEX ft_title ON articles(title);
CREATE FULLTEXT INDEX ft_content ON articles(content);
-- 可以分别设置权重
SELECT id, title,
MATCH(title) AGAINST('MySQL') * 1.5 +
MATCH(content) AGAINST('MySQL') * 0.5 as score
FROM articles
WHERE MATCH(title, content) AGAINST('MySQL')
ORDER BY score DESC;
-- 3. 定期优化索引
OPTIMIZE TABLE articles;
-- 或
ALTER TABLE articles FORCE;查询优化
sql
-- ❌ 糟糕: 搜索太短的词(被忽略)
SELECT * FROM articles
WHERE MATCH(content) AGAINST('is');
-- is是停用词,返回空结果
-- ❌ 糟糕: 高频词导致大量结果
SELECT * FROM articles
WHERE MATCH(content) AGAINST('database');
-- 可能匹配数万条记录
-- ✓ 优秀: 使用布尔模式精确控制
SELECT * FROM articles
WHERE MATCH(content) AGAINST(
'+MySQL +InnoDB +performance'
IN BOOLEAN MODE
)
LIMIT 20;
-- ✓ 优秀: 限制结果集大小
SELECT id, title,
MATCH(content) AGAINST('optimization') as score
FROM articles
WHERE MATCH(content) AGAINST('optimization')
ORDER BY score DESC
LIMIT 10;实际应用案例
案例1: 博客文章搜索
sql
-- 创建博客表
CREATE TABLE blog_posts (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(200) NOT NULL,
summary VARCHAR(500),
content MEDIUMTEXT,
tags VARCHAR(200),
author_id BIGINT,
created_at DATETIME,
view_count INT DEFAULT 0,
FULLTEXT INDEX ft_search (title, content, tags)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 搜索功能实现
CREATE PROCEDURE sp_search_posts(
IN p_keyword VARCHAR(200),
IN p_page INT,
IN p_page_size INT
)
BEGIN
DECLARE v_offset INT;
SET v_offset = (p_page - 1) * p_page_size;
SELECT
p.id,
p.title,
p.summary,
p.view_count,
p.created_at,
MATCH(p.title, p.content, p.tags) AGAINST(p_keyword) as relevance
FROM blog_posts p
WHERE MATCH(p.title, p.content, p.tags) AGAINST(p_keyword IN BOOLEAN MODE)
ORDER BY relevance DESC, p.view_count DESC
LIMIT p_page_size OFFSET v_offset;
END;
-- 调用示例
CALL sp_search_posts('+MySQL +optimization -beginner', 1, 10);案例2: 电商商品搜索
sql
CREATE TABLE products (
id BIGINT PRIMARY KEY,
product_name VARCHAR(200),
brand VARCHAR(100),
category VARCHAR(100),
description TEXT,
specs JSON,
price DECIMAL(10,2),
sales_count INT,
FULLTEXT INDEX ft_product (product_name, brand, description)
);
-- 商品搜索(带权重)
SELECT
p.id,
p.product_name,
p.brand,
p.price,
p.sales_count,
(
MATCH(p.product_name) AGAINST('无线蓝牙耳机') * 3.0 +
MATCH(p.brand) AGAINST('无线蓝牙耳机') * 2.0 +
MATCH(p.description) AGAINST('无线蓝牙耳机') * 1.0
) as relevance_score
FROM products p
WHERE MATCH(p.product_name, p.brand, p.description)
AGAINST('无线蓝牙耳机' IN BOOLEAN MODE)
ORDER BY relevance_score DESC, p.sales_count DESC
LIMIT 20;案例3: 日志检索系统
sql
CREATE TABLE application_logs (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
log_time DATETIME NOT NULL,
level ENUM('DEBUG','INFO','WARN','ERROR'),
module VARCHAR(100),
message TEXT,
stack_trace TEXT,
FULLTEXT INDEX ft_log_message (message),
INDEX idx_time_level (log_time, level)
) ENGINE=InnoDB PARTITION BY RANGE (YEAR(log_time));
-- 错误日志搜索
SELECT
id,
log_time,
level,
module,
LEFT(message, 200) as preview,
MATCH(message) AGAINST('NullPointerException database connection') as relevance
FROM application_logs
WHERE MATCH(message) AGAINST('+NullPointerException +database' IN BOOLEAN MODE)
AND level = 'ERROR'
AND log_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)
ORDER BY log_time DESC
LIMIT 50;局限性
中文分词问题
sql
-- MySQL内置全文索引不支持中文分词!
-- 测试:
INSERT INTO articles (title, content) VALUES ('测试中文搜索', '这是一个测试文档');
SELECT * FROM articles
WHERE MATCH(title, content) AGAINST('中文');
-- ✗ 无法正确分词,可能返回空结果
-- 解决方案1: 使用ngram插件(MySQL 5.7+)
CREATE TABLE articles_cn (
id INT PRIMARY KEY,
content TEXT,
FULLTEXT INDEX ft_cn (content) WITH PARSER ngram
) ENGINE=InnoDB;
-- ngram按字符切分:
-- "你好世界" → "你", "你好", "好世", "世界", "世", "界"
-- 解决方案2: 使用外部搜索引擎
-- Elasticsearch
-- Apache Lucene
-- Sphinx其他限制
1. 只支持InnoDB和MyISAM引擎
2. 不支持前缀通配符(*word)
3. 最小词长限制(默认3)
4. 停用词自动过滤
5. 索引体积大(约为数据的30-50%)
6. 写入性能下降(需维护倒排索引)
7. 不支持中文(需ngram插件)最佳实践
1. 何时使用全文索引
适合:
✓ 大量文本数据(文章、评论、日志)
✓ 需要模糊搜索
✓ 需要相关性排序
✓ 搜索频率高
不适合:
✗ 短文本(< 20字符)
✗ 精确匹配场景
✗ 中文搜索(用Elasticsearch)
✗ 实时性要求极高2. 配置建议
ini
[mysqld]
# 生产环境推荐
ft_min_word_len = 2
innodb_ft_cache_size = 16000000 # 16MB
innodb_ft_total_cache_size = 1GB
innodb_ft_enable_stopword = ON # 启用停用词3. 监控和维护
sql
-- 查看全文索引大小
SELECT
table_name,
index_name,
ROUND(stat_value * @@innodb_page_size / 1024 / 1024, 2) as size_mb
FROM mysql.innodb_index_stats
WHERE stat_name = 'size'
AND index_name LIKE 'ft%';
-- 优化全文索引
OPTIMIZE TABLE articles;
-- 重建索引
ALTER TABLE articles DROP INDEX ft_idx, ADD FULLTEXT INDEX ft_idx (content);参考资料
MySQL官方文档
源码文件
storage/innobase/fts/fts0fts.cc- 全文索引核心storage/innobase/fts/fts0parser.cc- 分词器storage/innobase/fts/fts0search.cc- 搜索算法
相关术语
替代方案
版本历史:
- 2026-04-12: 初始版本,全面讲解全文索引原理与实践