"MySQL搜不动就上ES"?我先在50万行数据上测了它自带的ngram全文索引

今天上午刷到掘金热榜那篇《从 LIKE 到倒排索引:MySQL 搜不动的内容,ES 是怎么接住的》,写得挺清楚:LIKE 从第一个字符开始扫全表,数据量一大就完蛋,ES 用倒排索引把这个事接住了。 道理我都懂。但这个论证让我想起上周组里那个争论:产品要在管理后台加个"按内容搜工单",数据量预估五十万条,组里有人说"这还不简单,上ES",另一个说"为个搜索框再养一套集群?"。争了半天谁也没拿数据出来。

我正好有台1核2G的破机器,索性把这个问题从头到尾量了一遍:LIKE到底多慢、MySQL自带的ngram全文索引能顶到哪、什么时候才真的非ES不可。结果里有两个数据跟我预想的完全相反。

先测LIKE:慢,但没慢在你以为的地方

造数据的脚本很简单,50万条"技术文章",标题加正文,正文一百多字,往里面按比例埋了几个关键词:缓存、数据库连接池、消息队列,还有专门用来捣乱的数据结构和数仓(原因后面说)。脚本贴在文末,可复现。然后用三档数据量(1万、10万、50万)测 body 的 LIKE '%关键词%',每个查询跑3次取中位。为了让结果可比,用 WHERE id <= N 限定扫描范围,近似"数据量只有N的表":

复制代码
   规模 | 查询词     | COUNT耗时 | 命中数
  10000 | 无命中词   |    16ms  |  0
 100000 | 无命中词   |   169ms  |  0
 500000 | 无命中词   |   886ms  |  0
 500000 | 缓存       |   684ms  |  253889

无命中词 是个语料里根本不存在的词,它代表最坏情况:既然一行都搜不到,就得把全表扫完才能死心。886ms,在1核机上,50万行就是这个价。 但接下来这个数据打破了我的预期。同样是50万行,搜缓存这种命中25万条的高频词,取前20条的分页查询,耗时0ms。

复制代码
   规模 | 查询     | COUNT耗时 | LIMIT 20耗时 | 命中数
  500000 | 缓存      |   684ms  |      0ms    | 253889
  500000 | 消息队列  |   785ms  |      0ms    | 159020

原因不复杂:LIMIT 20 凑够行数就提前收工,命中率高的时候扫几百行就够了。慢的是"搜了个稀罕玩意"------命中稀少甚至没有时,每次查询都得扫到最后一行。后台管理系统里,用户输错字、搜一个不存在的单号,恰恰是高频操作。 所以"从第一个字符开始扫"没错,但完整的故事是:扫全表不可怕,可怕的是每次都要扫到底

上全文索引之前,先看看代价

MySQL 内置的 ngram 全文解析器,就是给中日韩这种没空格分词的语言准备的:把文本切成连续两个字一组的片段建倒排。它从 5.7.6 就有,WITH PARSER ngram 一个词缀就能开,写在 MySQL 8.0 手册 14.9.8 节,不是野路子特性。建索引之前我以为就是加个索引的事,代价主要体现在两个地方:最直观的是速度:不是秒级操作。我在一张10万行的表上实测,加 FULLTEXT(body) WITH PARSER ngram 花了 52.8 秒。1核机,50万行表换算下来四五分钟起步,这期间写入还得排队。还有个更隐蔽的:加了索引之后查表体积,data_length 从 213MB 涨到了 311MB。查了资料才明白,InnoDB 会自动给表补一个隐藏的 FTS_DOC_ID 列,等于把聚簇索引整个重建了一遍。生产大表上做这个操作,得当成一次重型变更来评审。

快是真快,坑也是真坑

索引建完先看速度,这部分符合预期:

scss 复制代码
查询(50万行)   | LIKE    | 全文索引-自然语言 | 全文索引-布尔模式
无命中词       |  886ms  |      0ms         |      0ms
缓存(2字)      |  684ms  |    150ms         |    215ms
消息队列(4字)  |  785ms  |    323ms         |   1710ms

无命中词从 886ms 直接归零,2字词快4倍,EXPLAIN 里 type=fulltext 替换了 type=ALL。到这里一切正常,如果文章在这就结束,结论就是"装个全文索引解决了"。 但坑是从这里开始的。

布尔模式搜长词,比LIKE还慢。 上表最后一行,搜4字的消息队列,布尔模式1710ms,是LIKE(785ms)的2.2倍,也是自然语言模式(323ms)的5倍多。一开始我以为测错了,反复跑了几遍,稳定复现。翻文档和源码注释才对上号:布尔模式下的中文查询会被转成ngram短语搜索------拿消息队列举例,引擎先从倒排里捞出含消息、息队、队列这些片段的所有文档,再逐个校验这些片段在原文里是不是连续出现。50万行里16万条命中,每条都要取回位置信息做连续性校验,还是单核。这一套做完,比全表扫一遍文本还贵。

这还没完。我的实验机只有2G内存,跑到一个更长的查询词时,mysqld直接被系统的OOM killer杀了两次,dmesg里赫然写着 Out of memory: Killed process (mysqld)。长词的布尔短语搜索,是在拿内存换精确度。2G内存的机器上,这是个能打死数据库的操作。

自然语言模式的相关性,也会给你惊喜。 MySQL的全文搜索默认是自然语言模式,它对查询词的处理方式是切成ngram片段后取并集------官方文档原话是"the search term is converted to a union of ngram terms"。并集意味着什么,我用数据库连接池实测了一下:

makefile 复制代码
LIKE 精确子串命中:        196892
布尔模式(短语语义)命中:    196892   ← 和LIKE完全一致
自然语言模式(并集)命中:    379058   ← 虚高92%

同一张表,自然语言模式比精确命中多出18万条。我统计了一下这37.9万条里有多少条正文根本不含完整的数据库连接池:182166条,占48% 。数据结构梳理、数仓建模这些埋进去的捣乱词,全被数据、库、结构这些片段捞了进来。

也就是说,用默认模式做搜索框,用户搜"数据库连接池",将近一半的结果是"沾边但不是"的内容。排序上相关文档的分数确实更高(完整命中的文档分数3.2,只有片段命中的在1上下),把结果限制在前几页问题不大。但一旦用户翻页,或者按score做了个不严格的过滤,体验就开始劣化。

顺带一个更冷门的坑:单字查询直接失效 。搜缓一个字:

sql 复制代码
LIKE '%缓%'              命中 253889
MATCH AGAINST('缓')      命中 0

ngram_token_size=2 时索引里根本没有单字token,一个字都搜不到。而ngram_token_size是个只读参数,改它要改配置重启,还会影响库里所有ngram索引。如果业务里有"搜姓"这种需求------比如在通讯录里搜张------这个方案直接出局。

所以,到底什么时候才需要ES

把数据摊开,我的结论是这样的:如果你的场景是后台系统的内容搜索框、几十万行、能接受"整词/多字词"检索,MySQL的ngram全文索引+布尔模式完全够用。记得两条纪律:查询词必须套双引号走布尔模式(它和LIKE的精确结果是严格等价的,我实测19.7万条一条不差),模式别用默认的自然语言模式,除非你能接受48%的误召回。

但要满足下面任何一条,别犹豫,上ES:搜索框是产品主路径、高频长词查询(布尔模式的短语校验成本会反噬你)、需要单字检索、需要真正的中文分词相关性排序、要聚合和高亮。ES解决的问题不只是快,是这套检索能力的完整度。回到开头组里那个争论,现在有了答案:五十万条的工单搜索框,一个全文索引就够了。但我也得承认,这次测试的语料是模板造的,词频分布比真实工单规整,真到生产上,建议照这个思路用自己的一小撮真实数据再量一遍------附录的代码就是按这个路子写的,换个数据源就能跑。

附录:核心实验代码

生成语料(节选,关键词按概率植入):

python 复制代码
IMPLANTS = {  # 关键词 -> 植入概率
    "数据库连接池": 0.05,
    "数据结构梳理": 0.03,   # 干扰:含"数据"
    "数仓建模": 0.02,       # 干扰:含"数"
    "缓存穿透": 0.08,
    "消息队列积压": 0.03,
}
def gen_doc(rng):
    kw = rng.choices(list(IMPLANTS.keys()), weights=IMPLANTS.values())[0]
    # ...模板拼句,每条正文必含一个植入词

建索引与查询:

sql 复制代码
-- 建索引(InnoDB,10万行实测52.8s,注意data_length会涨:聚簇被重建)
ALTER TABLE articles ADD FULLTEXT INDEX ft_body_ng (body) WITH PARSER ngram;

-- 布尔模式:与 LIKE 精确等价(实测 196892 = 196892)
SELECT COUNT(*) FROM articles
WHERE MATCH(body) AGAINST('"数据库连接池"' IN BOOLEAN MODE);

-- 自然语言模式(默认):并集语义,误召回48%,慎用于搜索框
SELECT title, MATCH(body) AGAINST('数据库连接池') AS score
FROM articles WHERE MATCH(body) AGAINST('数据库连接池')
ORDER BY score DESC LIMIT 20;
相关推荐
用户7783366132111 小时前
写个浏览器插件查 SERP:选中关键词右键看前 10(Chrome MV3 实战)
搜索引擎·api
柒和远方1 小时前
混合检索 RAG 全链路:查询增强、双路召回与重排——向量库和搜索引擎联手补齐召回
elasticsearch·langchain·llm
imDwAaY1 小时前
Redo Log 和 Binlog 为什么需要两阶段提交?
后端·mysql
Elasticsearch1 小时前
用自然语言分析跟踪数据,询问 Elastic Agent Builder 为什么运行缓慢
elasticsearch
数据库小学妹1 小时前
MySQL深分页优化:LIMIT大偏移的根因分析与五种解法对比
数据库·mysql·性能优化
Elasticsearch1 小时前
Elastic 中的 Temporal Cloud 可观测性:50+ 个指标,无需采集器
elasticsearch
这个DBA有点耶1 小时前
从异步复制到MGR:MySQL复制机制的三层演进与选型框架
数据库·mysql·代码规范
阿里云大数据AI技术2 小时前
云栖2026|从“找到答案”到“完成任务”:阿里云以 Agentic Search 重塑 AI 搜索
大数据·人工智能·elasticsearch
Elasticsearch2 小时前
遥测策略:在运行时更改 OpenTelemetry 采样率和日志级别,无需重启
elasticsearch