Skip to content

全文索引 - 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 或 learning

InnoDB源码分析 ​

分词流程 ​

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: 初始版本,全面讲解全文索引原理与实践

Released under MIT License.