技术深度解析

全文搜索 Full-Text Search

从倒排索引原理到 MySQL / Elasticsearch / SQLite 的完整实现机制,彻底讲清楚

🔍 倒排索引 📊 TF-IDF / BM25 🐬 MySQL InnoDB FTS 🟡 Elasticsearch Lucene 🪶 SQLite FTS5
1什么是全文搜索
Full-Text Search — 不同于精确匹配的语义级文本检索

🔎 精确匹配 vs 全文搜索

传统数据库用 LIKE '%keyword%' 做模糊查询,本质是全表扫描 + 字符串遍历,不走索引,性能极差。

  • LIKE查询:逐行扫描每个字段,时间复杂度 O(n·m)
  • 全文搜索:预先建立倒排索引,查询时间接近 O(1)
  • 全文搜索还支持相关性排序,不只是"有没有"

✨ 全文搜索的核心能力

  • 🏃 高性能:倒排索引,毫秒级检索
  • 🎯 相关性排序:按权重返回最相关结果
  • ✂️ 分词理解:理解词语边界,中英文分词
  • 🌀 词形变换:支持词干还原(running→run)
  • 🚫 停用词过滤:过滤 "a/the/的/了" 等无意义词
  • 🔄 同义词扩展:搜"手机"也能找到"移动电话"
💡
核心本质:全文搜索 = 文本预处理(分析管道) + 倒排索引存储 + 相关性评分算法。三者缺一不可,后面会逐一深入讲解。
2倒排索引 — 全文搜索的核心数据结构
Inverted Index — 以词为键、文档列表为值的索引结构

📐 正向索引 vs 倒排索引

正向索引(Forward Index)
-- 文档 → 词列表
Doc1 → [MySQL, 数据库, 索引, 查询]
Doc2 → [MySQL, 全文, 搜索, InnoDB]
Doc3 → [Elasticsearch, 全文, Lucene]

-- 查"全文"? 要扫所有文档 ❌
倒排索引(Inverted Index)
-- 词 → 文档列表
MySQL  → [Doc1, Doc2]
全文   → [Doc2, Doc3]
索引   → [Doc1]
Lucene → [Doc3]

-- 查"全文"? 直接查 ✅ O(1)
🔬 倒排索引构建可视化示例
Doc 1
MySQL 支持全文搜索索引
Doc 2
Elasticsearch 全文搜索很强大
Doc 3
MySQL 和 Elasticsearch 对比分析
↓ 分词 + 构建倒排索引
Token(词项) Posting List(倒排列表) 词频 TF 文档频率 DF
MySQL
Doc1(pos:0)Doc3(pos:0)
1,1 2
全文搜索
Doc1(pos:2)Doc2(pos:1)
1,1 2
Elasticsearch
Doc2(pos:0)Doc3(pos:2)
1,1 2
索引
Doc1(pos:3)
1 1
对比分析
Doc3(pos:3)
1 1

📦 Posting List 结构

倒排列表中每条记录(Posting)通常包含:


  • 文档ID:指向哪篇文档
  • 词频 TF:该词在文档中出现几次
  • 位置信息 pos:词在文档第几个位置(用于短语查询)
  • 偏移量 offset:字符级起止位置(用于高亮)

⚡ 查询过程

  • 1️⃣ 对查询词同样做分析(分词/小写/词干)
  • 2️⃣ 在倒排索引中 O(1) 定位各词的 Posting List
  • 3️⃣ 对多个词的列表做 AND(交集)OR(并集)操作
  • 4️⃣ 按相关性评分 排序返回
3文本分析管道 — 文本如何变成索引词
Analysis Pipeline — Analyzer = CharFilter + Tokenizer + TokenFilter
📄
原始文本
🔤
字符过滤
CharFilter
✂️
分词器
Tokenizer
🔧
词项过滤
TokenFilter
📑
Token列表
写入索引

🔤 CharFilter(字符过滤)

在分词前对原始字符做清洗:


  • HTML标签去除:<b>搜索</b> → 搜索
  • 特殊字符替换:&amp; → &
  • 全角转半角:ABC → ABC

✂️ Tokenizer(分词)

把文本切割成词项(Token):


  • 英文:按空格/标点切割
  • 中文:需要专用分词器(IK/结巴/HanLP)
  • N-gram:切割成 n 个字符的滑动窗口
  • Whitespace:仅按空格切割

🔧 TokenFilter(词项过滤)

对分出的 Token 做后处理:


  • Lowercase:全小写化
  • Stop Filter:删除停用词(the/a/了)
  • Stemmer:词干还原(running→run)
  • Synonym:同义词扩展

📌 中文分词示例(IK分词器)

输入文本
我在北京学习机器学习技术
分词结果(ik_max_word)
北京 学习 机器学习 技术
4相关性评分算法
Relevance Scoring — TF-IDF 与 BM25 的核心原理

📐 TF-IDF(经典算法)

两个核心直觉:

  • 词在这篇文档出现越多 → 越相关(TF)
  • 词在所有文档中越罕见 → 越有区分度(IDF)
TF-IDF 公式
Score(t,d) = TF(t,d) × IDF(t)
TF(t,d) = count(t in d) / total_tokens(d)
IDF(t) = log( N / df(t) )
N=文档总数, df(t)=包含词t的文档数

🚀 BM25(现代算法,ES默认)

BM25 是 TF-IDF 的改进版,解决了两个问题:

  • TF 过大时相关性不再线性增长(词频饱和)
  • 长文档包含更多词是正常的(文档长度归一化)
BM25 公式
Score = IDF × TF·(k₁+1) / (TF + k₁·(1-b+b·|d|/avgdl))
k₁ ∈ [1.2, 2.0]:词频饱和参数(默认1.2)
b ∈ [0,1]:长度归一化参数(默认0.75)
|d|:文档长度, avgdl:平均文档长度
🎯
关键理解:BM25 的 k₁ 参数控制词频的"收益递减"效应——当同一个词出现 100 次和 1 次的文档,不应该有 100 倍的得分差距。b 参数控制文档长度的惩罚力度,避免长文章天然占优势。
MySQL 用 TF-IDF,Elasticsearch 默认用 BM25,SQLite FTS5 用 BM25。

🐬
MySQL 全文搜索
InnoDB 原生 FTS — 基于辅助索引 + TF-IDF 评分
5MySQL 底层架构

🏗️ InnoDB FTS 内部结构

  • FTS 辅助索引表(6张):按词第一个字符的16进制散列分片存储
  • FTS_DOC_ID 列:隐含的文档标识符(BIGINT UNSIGNED)
  • FTS 删除表:被删除文档先标记,异步清理
  • FTS 配置表:记录同步LSN等元数据
  • 内存缓存(FTS Cache):写入先进缓存,批量同步磁盘

🔑 三种查询模式

  • IN NATURAL LANGUAGE MODE(默认):自然语言,TF-IDF相关性排序
  • IN BOOLEAN MODE:支持 +必须 -排除 * 通配符,不排序
  • WITH QUERY EXPANSION:两轮搜索,第二轮用第一轮结果扩展关键词
SQL — MySQL 全文搜索完整示例
-- 1. 建表时创建全文索引
CREATE TABLE articles (
  id     INT PRIMARY KEY AUTO_INCREMENT,
  title  VARCHAR(200),
  body   TEXT,
  -- 创建全文索引(可以多列联合)
  FULLTEXT INDEX idx_fts (title, body)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 2. 自然语言模式(默认,按相关性排序)
SELECT id, title,
  MATCH(title, body) AGAINST ('MySQL 全文搜索') AS relevance
FROM articles
WHERE MATCH(title, body) AGAINST ('MySQL 全文搜索')
ORDER BY relevance DESC;

-- 3. 布尔模式(精确控制)
SELECT * FROM articles
WHERE MATCH(title, body)
  AGAINST ('+MySQL +索引 -删除' IN BOOLEAN MODE);
-- + 必须包含, - 必须排除, * 前缀通配, "" 短语
-- "全文搜索" 短语匹配, ~负权重, > < 调整权重

-- 4. 查询扩展模式(两轮搜索)
SELECT * FROM articles
WHERE MATCH(title, body)
  AGAINST ('数据库' WITH QUERY EXPANSION);

-- 5. 事后创建索引
ALTER TABLE articles
  ADD FULLTEXT INDEX idx_fts2 (title, body)
  WITH PARSER ngram;  -- 中文用 ngram 分词

🔬 MySQL FTS 内部存储结构(6张辅助表)

-- 当你创建 FULLTEXT INDEX 后,MySQL 会在 .ibd 文件中
-- 自动创建 6 张辅助索引段,命名规则:
FTS_{table_id}_BEING_DELETED        -- 待删除文档ID列表
FTS_{table_id}_BEING_DELETED_CACHE  -- 内存缓存的待删列表
FTS_{table_id}_CONFIG               -- FTS配置(同步LSN等)
FTS_{table_id}_DELETED              -- 已标记删除的文档ID
FTS_{table_id}_DELETED_CACHE        -- 删除缓存
FTS_{table_id}_0000000000000161_INDEX_1  -- 实际倒排索引分片
-- INDEX_1 ~ INDEX_6:按首字母哈希值分6个分片

-- 辅助索引表每行结构:
-- (word VARCHAR, first_doc_id BIGINT, last_doc_id BIGINT,
--  doc_count INT, ilist BLOB)
-- ilist:压缩存储 doc_id + word_pos 列表

⚙️ 中文支持:ngram 分词

  • MySQL 内置 ngram parser 支持中文
  • 按滑动窗口切割:ngram_token_size=2
  • "中文搜索" → "中文"、"文搜"、"搜索"
  • 配置参数 innodb_ft_min_token_size(默认3,中文要改2)

⚠️ MySQL FTS 局限

  • 停用词过滤较粗糙,无同义词支持
  • 中文分词只有 ngram,精度不如专业分词器
  • 评分算法固定为 TF-IDF,不可调
  • 不支持分布式,大数据量性能有限
  • 自然语言模式:结果超50%文档出现的词会被忽略
🟡
Elasticsearch 全文搜索
基于 Apache Lucene — 专为全文搜索设计的分布式搜索引擎
6Lucene 核心架构

🏗️ Elasticsearch → Lucene 分层架构

Elasticsearch 层 REST API / 分布式路由 / Shard 管理 / 集群协调
Lucene Index(Shard) 每个 Shard 是一个完整的 Lucene 索引实例
Segment(段) Lucene 最小存储单元,不可变,多个 Segment 合并成大 Segment(Segment Merge)
倒排索引文件 .tim/.tip 文档存储 .fdt 正向索引 .dvd 字段信息 .fnm 删除标记 .del
JSON — Elasticsearch 全文搜索完整示例
// 1. 创建索引,配置 Analyzer(中文IK分词)
PUT /articles
{
  "settings": {
    "analysis": {
      "analyzer": {
        "ik_smart_analyzer": {
          "type": "custom",
          "tokenizer": "ik_smart",       // IK粗粒度分词
          "filter": ["lowercase", "stop"]   // 小写 + 停用词
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "ik_smart_analyzer",  // 写入时分析器
        "search_analyzer": "ik_smart"       // 搜索时分析器
      },
      "body": { "type": "text", "analyzer": "ik_max_word" }
    }
  }
}

// 2. match 查询(全文搜索最常用)
GET /articles/_search
{
  "query": {
    "match": {
      "title": {
        "query": "MySQL 全文搜索",
        "operator": "or",        // or/and
        "minimum_should_match": "75%"  // 至少75%词匹配
      }
    }
  }
}

// 3. multi_match(多字段搜索 + 字段权重)
GET /articles/_search
{
  "query": {
    "multi_match": {
      "query": "全文搜索原理",
      "fields": ["title^3", "body^1"],  // title 权重3倍
      "type": "best_fields"  // cross_fields/most_fields
    }
  }
}

// 4. match_phrase(短语匹配,词序和位置)
GET /articles/_search
{
  "query": {
    "match_phrase": {
      "body": {
        "query": "倒排索引",
        "slop": 1  // 允许词间距1个词的误差
      }
    }
  }
}

// 5. bool 复合查询(最强大的查询方式)
GET /articles/_search
{
  "query": {
    "bool": {
      "must":    [{ "match": { "title": "全文搜索" }}],
      "should":  [{ "match": { "body": "BM25" }}],
      "must_not":[{ "match": { "title": "广告" }}],
      "filter":  [{ "term": { "status": "published" }}]
    }
  },
  "highlight": {
    "fields": { "title": {}, "body": {} }  // 高亮
  }
}

📂 Segment 不可变原理

  • Lucene 写入的 Segment 不可修改
  • 删除 = 标记 .del 文件(仍占磁盘)
  • 更新 = 删除旧文档 + 写新 Segment
  • Segment Merge:后台定期合并小 Segment,清除已删文档,提升查询性能

⚡ 近实时搜索(NRT)

  • 写入先到 内存 Buffer(不可搜索)
  • 每1秒 Refresh:Buffer→内存 Segment(可搜索)
  • 每30秒 Flush:Segment 持久化到磁盘
  • Translog:类似 WAL,保证数据不丢

📊 FST 词典压缩

  • Lucene 词典用 FST(有限状态转换器)存储
  • 比 B-Tree 更省内存(可放进内存)
  • 支持前缀/后缀共享,压缩率极高
  • Posting List 用 Frame of Reference 压缩算法
🪶
SQLite FTS5
内嵌式轻量级全文搜索 — 无服务器,单文件,BM25评分
7SQLite FTS5 架构

🏗️ FTS5 内部存储结构

  • 虚拟表(Virtual Table):通过 SQLite 虚拟表机制实现
  • %_data 表:B-Tree 存储所有索引数据(倒排+文档)
  • %_idx 表:词典索引,存储词→数据段映射
  • %_content 表:文档原始内容(可选 content="")
  • %_config 表:FTS5 配置参数
  • 倒排索引以 B-Tree leaf page 形式存储,键=词+docid

🔑 FTS4 vs FTS5

特性 FTS4 FTS5
评分算法 TF-IDF BM25
列过滤 不支持 支持
短语查询 支持 支持
自定义tokenizer 有限 完整API
SQL — SQLite FTS5 完整示例
-- 1. 创建 FTS5 虚拟表
CREATE VIRTUAL TABLE docs USING fts5(
  title,
  body,
  tokenize = 'unicode61',    -- 默认分词器(英文友好)
  -- tokenize = 'porter ascii', -- 词干还原
  -- tokenize = 'trigram',      -- trigram模式(支持中文LIKE模糊)
  content='articles',        -- 关联真实表(content table)
  content_rowid='id'
);

-- 2. 同步真实表数据到 FTS 索引
INSERT INTO docs(docs)
  VALUES('rebuild');          -- 全量重建索引

-- 3. 基本全文搜索
SELECT rowid, title,
  bm25(docs) AS score       -- BM25 评分(负值,越小越相关)
FROM docs
WHERE docs MATCH '全文搜索'
ORDER BY bm25(docs);        -- 升序(负值越小=得分越高)

-- 4. 布尔查询语法
SELECT * FROM docs
WHERE docs MATCH 'MySQL AND 索引 NOT 删除';

-- 5. 列过滤(仅搜 title 列)
SELECT * FROM docs
WHERE docs MATCH 'title:全文搜索';

-- 6. 短语搜索
SELECT * FROM docs
WHERE docs MATCH '"倒排 索引"';  -- 精确短语

-- 7. 前缀查询
SELECT * FROM docs
WHERE docs MATCH 'index*';        -- 前缀匹配

-- 8. highlight + snippet 辅助函数
SELECT
  highlight(docs, 0, '<b>', '</b>') AS title_hl,
  snippet(docs, 1, '<em>', '</em>', '...', 30) AS body_snippet
FROM docs
WHERE docs MATCH '索引';

-- 9. trigram 模式(SQLite 3.38+,支持中文/LIKE类搜索)
CREATE VIRTUAL TABLE docs_cn USING fts5(
  content,
  tokenize='trigram'  -- 3字符滑动窗口,无需外部分词
);
-- 适合中文但索引较大,存储开销约3x

-- 10. bm25 权重调整(各列权重)
SELECT *, bm25(docs, 10.0, 1.0) score
FROM docs
WHERE docs MATCH '搜索'
ORDER BY score;  -- title权重10, body权重1

🔬 FTS5 B-Tree 存储原理

  • 所有数据存在 %_data 表(隐藏虚拟表)
  • 每个 B-Tree 叶页存一段倒排列表:
    key = (level, segment_id, page_no)
  • 叶页内紧凑存储:词典项 + docID列表(变长整型压缩)
  • 写入时先放 内存哈希,到一定量后合并写盘(类似 LSM-tree)
  • 查询:先在 %_idx 定位词,再顺序读 %_data 中的 Posting 页

🌍 中文处理方案对比

  • trigram tokenizer(内置):无需外部依赖,SQLite 3.38+,索引膨胀约3倍
  • 自定义 tokenizer(C API):可接入结巴、jieba-wasm等分词,精度高,实现复杂
  • 应用层分词:应用中分好词再存入 FTS,最灵活,维护成本高
8三者深度对比
从架构、性能、分词、适用场景多维度横向对比
对比维度 🐬 MySQL InnoDB FTS 🟡 Elasticsearch 🪶 SQLite FTS5
底层引擎 InnoDB 辅助索引段(6张隐藏表) Apache Lucene(Segment + FST) B-Tree 虚拟表(%_data)
索引结构 哈希分片倒排索引,BLOB存Posting FST词典 + 压缩Posting List(FOR算法) B-Tree叶页存Posting,变长整型压缩
评分算法 TF-IDF(固定,不可调) BM25(默认,可自定义) BM25(内置 bm25() 函数)
中文分词 ngram(内置,精度一般) IK/HanLP等专业分词(插件) trigram(内置)/ 自定义tokenizer
部署方式 内嵌 MySQL,无需额外服务 独立服务,需单独维护 无服务,单文件嵌入
分布式支持 不支持 原生支持(Shard/Replica) 不支持
写入性能 中等(FTS Cache 批量落盘) 高(内存Buffer+近实时Refresh) 高(内存哈希+LSM风格合并)
查询性能 中等(千万级可用) 极高(亿级,分布式并行) 高(百万级以内)
数据一致性 与主表强一致(事务支持) 近实时(默认1秒延迟) 与SQLite事务一致
高亮/摘要 不支持 highlight API,支持多种策略 highlight() / snippet() 内置函数
聚合分析 不支持 强大的 Aggregation API 不支持
运维复杂度 低(MySQL自带) 高(独立集群,JVM调优) 极低(嵌入式)
适用数据量 百万~千万级 亿级以上 百万级以内
典型场景 已有MySQL系统,简单搜索需求 专业搜索引擎,日志分析,大数据 移动端/桌面App,小型系统
9选型建议
根据场景选择最适合的全文搜索方案

🏢 已有 MySQL,需要简单搜索

MySQL FTS 直接在现有 MySQL 中建 FULLTEXT 索引,无需引入新服务。适合博客、CMS、内部工具等中小规模业务,数据量千万级以内,不追求极致搜索体验。

🚀 专业搜索引擎,大数据量

Elasticsearch 电商搜索、知识库、日志平台、新闻推荐等搜索是核心功能的场景。支持同义词、拼音、分面过滤、相关性调优,亿级文档毫秒响应。

📱 移动端 / 桌面 App / 小型服务

SQLite FTS5 iOS/Android App内置搜索、Electron桌面应用、个人工具、CLI程序。零运维,单文件,BM25评分,百万文档轻松驾驭。

经典架构模式:MySQL + Elasticsearch 双写
业务数据写入 MySQL 保证事务一致性,同步写(或异步 Canal/Binlog 同步)到 ES 提供搜索服务。ES 作为只读搜索层,MySQL 作为主存储。这是互联网公司最常见的搜索架构。

🌱 入门推荐路径

  • 1

    SQLite FTS5 入门,理解倒排索引和BM25

  • 2

    MySQL 项目中实践全文索引,理解生产限制

  • 3

    学习 Elasticsearch,掌握 Lucene Segment 架构

🔑 核心知识点回顾

  • 倒排索引 = 词→文档列表
  • Analyzer = CharFilter + Tokenizer + Filter
  • BM25 解决 TF 饱和 + 长文档问题
  • ES Segment 不可变 + Merge 机制
  • MySQL FTS 6张辅助表 + Cache
  • SQLite FTS5 B-Tree leaf page 存储

⚠️ 常见坑

  • MySQL FTS:默认最小词长3字符,中文要改2
  • MySQL FTS:50%以上文档含某词则被忽略(自然语言模式)
  • ES:写后立即查可能查不到(1秒refresh)
  • SQLite FTS5:bm25() 返回负值,ORDER BY 要升序
  • 中文一定要用专业分词,不能靠空格切割