文章目录
- 前言
- [一、LIKE + 索引 的局限](#一、LIKE + 索引 的局限)
- 二、倒排索引:把组织数据的方向反过来
-
- [2.1 从"文档找词"到"词找文档"](#2.1 从"文档找词"到"词找文档")
- [2.2 光有倒排不够,快在这三个设计](#2.2 光有倒排不够,快在这三个设计)
- 三、Elasticsearch:开箱即用的倒排索引
- [四、上手:30 行命令建一个能用的搜索](#四、上手:30 行命令建一个能用的搜索)
-
- [4.1 建索引:最容易踩坑的一步](#4.1 建索引:最容易踩坑的一步)
- [4.2 写入和查询](#4.2 写入和查询)
- [4.3 那个坑,现场演示一遍](#4.3 那个坑,现场演示一遍)
- 五、一条数据写进去,到底发生了什么
- 六、高频面试题精讲
-
- [6.1 倒排索引由什么组成?](#6.1 倒排索引由什么组成?)
- [6.2 为什么说 ES 是"近实时",而不是实时?](#6.2 为什么说 ES 是"近实时",而不是实时?)
- [6.3 深分页怎么处理?](#6.3 深分页怎么处理?)
- [6.4 `text` 和 `keyword` 怎么选?](#6.4
text和keyword怎么选?) - [6.5 主分片数为什么创建后不能改?](#6.5 主分片数为什么创建后不能改?)
- [6.6 MySQL 的数据怎么同步到 ES?](#6.6 MySQL 的数据怎么同步到 ES?)
- [6.7 集群脑裂怎么防?](#6.7 集群脑裂怎么防?)
- 写在最后
前言
在图书馆找一本书,有两种办法。
一种是挨个书架、逐本翻页,看哪本书里出现过"倒排索引"这四个字------这是 LIKE '%倒排索引%'。另一种是走到检索台,抽出一张卡片,上面写着"倒排索引 → 3 楼 A 区 12 架第 4 本"。前者靠体力,后者靠一张提前做好的卡片。
很多团队的全文搜索,至今还停在第一种办法上:给字段加上索引,然后写 LIKE '%关键词%',以为加了索引就万事大吉。但是当数据量变得庞大、搜索场景变得复杂时,单纯的数据库操作就无法满足需求。
接下来让我们一起看一看:LIKE + 索引有什么局限?倒排索引凭什么快?ES 怎么用?面试的时候都有哪些常见的高频题?
一、LIKE + 索引 的局限
先看一张 book 表,title 上有普通的 B+ 树索引。
-- 能走上索引:范围扫描 SELECT * FROM book WHERE title LIKE 'Java%'; -- 走不上索引:全表扫描 SELECT * FROM book WHERE title LIKE '%Java%'; -- 走不上索引:全表扫描 SELECT * FROM book WHERE title LIKE '%Java';
区别只有一个符号的位置,结果天差地别。
| 写法 | 能否用上 B+ 树索引 | 原因 |
|---|---|---|
title = 'Java' |
能,精确定位 | 按完整值二分查找 |
title LIKE 'Java%' |
能,范围扫描 | 前缀确定,能找到起点 |
title LIKE '%Java%' |
不能,全表扫描 | 开头不确定,没有起点 |
title LIKE '%Java' |
不能,全表扫描 | 同上 |
原因藏在 B+ 树本身:它按完整值排好序,只能从左往右比。 以某个前缀开头的值一定连成一段------能找到起点,就能范围扫描;起点一旦不确定,二分查找就无从下手,只能把叶子节点挨个读一遍------索引还在,但一点忙都帮不上,退化成了全表扫描。
更麻烦的是,就算全表扫描能扫出结果,它也回答不了全文搜索真正要回答的问题:
-
- 分词:搜"Elasticsearch 教程",内容是"Elasticsearch 入门指南"算不算命中?
-
- 相关度:1000 条结果里,哪条最该排第一?
-
- 多词匹配:搜"Java 并发",要的是同时包含这两个词,而不是包含任意一个。
这三个需求,B+ 树一个都接不住。不是 MySQL 不行,是它压根没打算干这件事。
说句公道话
MySQL 8.0 也有全文索引(
FULLTEXT),InnoDB 从 5.6 起就支持了,底层同样是倒排思想。但它的中文分词、相关度排序、横向扩展能力都远不及专用搜索引擎。小规模、纯英文、对相关度没要求的场景可以凑合;一旦要中文分词,或者文档量上千万,就该换工具了。
二、倒排索引:把组织数据的方向反过来
知道了 LIKE 慢在哪,就能看懂 ES 快在哪------它没有让"搜索"这件事变快,而是把搜索时要做的遍历,提前搬到了写入时。
2.1 从"文档找词"到"词找文档"
正排索引是"文档 → 词",也是我们最习惯的方向:
| 文档 ID | 内容 |
|---|---|
| 1 | Java 并发编程 |
| 2 | Java 虚拟机 |
| 3 | 并发与性能 |
要找含"并发"的文档,只能把三条内容都读一遍------这正是 LIKE '%并发%' 在做的事。
倒排索引把它翻过来,变成"词 → 文档":
| 词项(Term) | 文档 ID 列表(Posting List) |
|---|---|
| Java | 1, 2 |
| 并发 | 1, 3 |
| 虚拟机 | 2 |
| 编程 | 1 |
现在搜"并发",直接查表拿到 1, 3,一次定位,不用碰任何一个文档的原文。

2.2 光有倒排不够,快在这三个设计
词典和倒排表加起来,就是一份"词项到文档"的巨型字典。但一个上千万文档的索引,词项可能有几百万个,倒排表可能上百 GB------凭什么还能毫秒级返回?
1. FST 让词典索引常驻内存
如果整个词典都放内存,放不下;全放磁盘,每次查询都要多次随机读。ES 的做法是折中:把词典的前缀压缩成一份索引,用 FST(有限状态转换器)存储并常驻内存,完整的词典和倒排表留在磁盘上。查询时先在内存的 FST 里定位到词项,再精准地去磁盘读一次。
2. 压缩让倒排表变小
倒排表里存的是递增的文档 ID。ES 用差值编码(Frame of Reference)只存相邻 ID 的差,再按块做位打包,常见词项的倒排表能压掉一个数量级;越稀疏的词项收益越小------块内的位宽由最大的那个间隔决定。数据小到一定程度,磁盘读就不再是瓶颈。
3. 跳表让多条件求交变快
搜"Java 并发",拿到 1, 2 和 1, 3,需要求交集。逐条比对是 O(n),ES 在倒排表上建了跳表(Skip List):每隔一段插一个跳转指针,比对时可以一次跳过一大片。

倒排索引不是魔法
它只是把代价从"每次搜索"转移到了"每次写入"------索引要建、要维护、要占空间,更新还要付出额外成本。这笔账怎么还,第五节会讲清楚。
三、Elasticsearch:开箱即用的倒排索引
Elasticsearch 是基于 Lucene 构建的分布式搜索引擎,对外只暴露 REST 接口和 JSON 文档模型,不依赖任何特定语言。前面说的倒排索引、分词、相关度排序,它都打包成了一个开箱即用的服务------数据以 JSON 文档写进去,全文检索、聚合分析、高亮结果直接查出来,单机扛不住就加节点横向扩展。
它要解决的,正是 LIKE 答不了的那三件事:中文分词、相关度排序、多条件组合。
常见的落地场景有两类:站内搜索 (商品、文章、用户)和日志分析(也就是常说的 ELK 技术栈,ES 负责存和查,Logstash 负责采集,Kibana 负责可视化)。
一句话定位
ES 不是 MySQL 的替代品,它是"搜索"这件事的专用工具------事务、关联查询、强一致这些活,还是交给数据库。
四、上手:30 行命令建一个能用的搜索
原理明白,接下来动手。先启动一个单机实例。
docker
docker network create elastic
docker run --name es01 --net elastic -p 9200:9200 \
-e "discovery.type=single-node" \
docker.elastic.co/elasticsearch/elasticsearch:8.18.0
9300 是节点间通信端口,单机实验用不到,集群部署时在内网放开即可。
ES 8.0 起安全功能默认开启 ,这一步和早期版本不一样:启动时会自动生成 TLS 证书、设置 elastic 超级用户的密码、生成 Kibana 的注册 token。密码只在终端打印一次,先存下来。
docker
# 取出自动生成的 CA 证书,之后用 HTTPS + 密码访问
docker cp es01:/usr/share/elasticsearch/config/certs/http_ca.crt .
curl --cacert http_ca.crt -u elastic:<密码> https://localhost:9200
关掉认证的 ES,是数据泄露的经典入口
本地实验常加
-e "xpack.security.enabled=false"关掉认证图省事,但绝不能用在生产环境。历史上被勒索、被拖库的案例,大多不是被攻破,而是压根没设防。
4.1 建索引:最容易踩坑的一步
sql
PUT /book
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"title": { "type": "text", "analyzer": "ik_max_word" },
"author": { "type": "keyword" },
"description": { "type": "text", "analyzer": "ik_max_word" },
"price": { "type": "double" },
"publishDate": { "type": "date" }
}
}
}
单机实验的一个小提醒
上面
number_of_replicas写 1 是为了讲清概念,但单节点实例里副本分片无处分配,_cluster/health会一直显示yellow------这不是故障,单机练手时改成 0 即可。
ik_max_word 是 IK 中文分词器的细粒度模式(需要单独装插件),会把"Java 并发编程实战"尽量多切几个词出来。
text 和 keyword 的区别,是新手第一个大坑:
| 类型 | 是否分词 | 用于 | 典型坑 |
|---|---|---|---|
text |
是,切成词项后入索引 | 全文检索 | 拿整句话去精确查,查不到 |
keyword |
否,整串原样保留 | 精确匹配、聚合、排序 | 拿它做全文检索,只有完全相同才命中 |
Tip
一句话记法:要"搜得到"用 text,要"对得上"用 keyword。
4.2 写入和查询
sql
POST /book/_doc/1
{
"title": "Java 并发编程实战",
"author": "张三",
"description": "从内存模型讲到线程池",
"price": 89.0,
"publishDate": "2024-06-01"
}
sql
GET /book/_search
{
"query": {
"bool": {
"must": [ { "match": { "title": "Java 并发" } } ],
"filter": [ { "term": { "author": "张三" } } ]
}
}
}
must 里的 match 会参与相关度打分,filter 里的 term 不打分、还能走缓存。
查询优化里性价比最高的一条
能用 filter 就别用 must。 缓存的实现正是 Roaring Bitmap:把命中的文档 ID 集合按块存成数组或位图,求交并只做位运算,不必再去碰倒排表的原文。
Java 侧接入(8.x 起推荐官方 elasticsearch-java 客户端,旧的 RestHighLevelClient 从 7.15 起已标记弃用):
java
ElasticsearchClient client = new ElasticsearchClient(transport);
SearchResponse<Book> resp = client.search(s -> s
.index("book")
.query(q -> q.bool(b -> b
.must(m -> m.match(mm -> mm.field("title").query("Java 并发")))
.filter(f -> f.term(t -> t.field("author").value("张三"))))),
Book.class);
4.3 那个坑,现场演示一遍
数据刚写进去,换一种查法:
sql
GET /book/_search
{ "query": { "term": { "title": "Java 并发编程实战" } } }
没有结果。文档明明在,为什么查不到?
因为 title 是 text 类型,写入时已经被切成 java, 并发, 编程, 实战 这些词项存进倒排表(这里只列了主干词,ik_max_word 是细粒度模式,实际还会切出重叠词项,下一段的 _analyze 一看便知)------倒排表里根本没有"Java 并发编程实战"这整个串 。而 term 查询不会对查询词做分词,它拿整句话原文去词典里找,自然找不到。
这就是 term 和 match 的分工:
-
match:会走分析器,把查询词也分词,用于text字段 -
term:不做任何处理,拿原文精确匹配,用于keyword字段
顺带一提,分析器除了分词还会做归一化处理(比如把英文转成小写),所以同一个词的大小写写法不同,也可能出现"能查到"和"查不到"两种结果。
排查这类问题,第一步永远是
用
_analyze看看你的词到底被切成了什么。
sql
GET /book/_analyze
{ "analyzer": "ik_max_word", "text": "Java 并发编程实战" }
五、一条数据写进去,到底发生了什么
会用之后,问题就变成:为什么刚写完查不到?为什么删了磁盘还不释放?答案在同一条链路里。

1. 先进缓冲区和事务日志。 文档先写进内存缓冲区(Memory Buffer),同时追加到 translog。此刻数据在内存里,搜不到。
2. refresh 让它可见。 默认每 1 秒执行一次 refresh:把缓冲区里的数据生成一个新的 segment。segment 一旦生成,文档就可被搜索了------这就是"近实时"(NRT)的来源。
Note
ES 不是实时的,它默认有大约 1 秒的可见延迟。
3. translog 是保命符。 数据还在缓冲区时断电,靠 translog 恢复。默认 index.translog.durability: request,每个写请求都会 fsync 一次------这条是"写入性能换数据安全"的默认选择,批量导入时可以调,但要知道自己在放弃什么。
4. segment 不可变。 这是 ES 的基石,也是理解一切"怪脾气"的钥匙。不可变意味着不用加锁、可以放心缓存、方便压缩。代价是:"更新"一条文档,实际是把旧文档标记删除、再写一条新的;"删除"也只是打标记,文档依然占着空间。 一条文档改十次,索引里就躺着 10 份被标记删除的旧版本,直到段合并才真正释放。
5. flush 和段合并。 flush 把 segment 落盘 fsync 并清空 translog;段合并(merge)把一堆小 segment 合成大 segment,这时才真正物理删除那些被标记的文档,磁盘空间才释放。
批量导入的标准操作
先关掉自动 refresh,导完再打开。导入期间新写入的数据不会生成新的小 segment,段合并的压力和查询抖动都会小很多。
sql
PUT /book/_settings
{ "index.refresh_interval": "-1" }
导入完成后再改回来:
sql
PUT /book/_settings
{ "index.refresh_interval": "1s" }
六、高频面试题精讲
原理捋顺了,面试问的就都在射程内。下面七道是 ES 面试里出现频率最高的,每题标注考点在哪,并给出可以直接说出口的话术。
Tip
下面每个绿色框里的内容都是口语回答模板,面试被问到可以直接照着说。
6.1 倒排索引由什么组成?
考点 :不是考你记不记得"倒排索引"这四个字,而是考你知不知道它由词典 + 倒排表两部分组成,以及为什么这么设计。
Tip
倒排索引把"文档 → 词"反过来组织成"词 → 文档 ID 列表",由词典(Term Dictionary)和倒排表(Posting List)两部分组成。词典存所有词项,倒排表存每个词项对应的文档 ID 列表。为了性能,词典的词项索引用 FST 压缩后常驻内存,完整词典和倒排表放在磁盘;倒排表用差值编码加位打包压缩,多个词项求交并时用跳表加速。而 filter 查询命中的文档集合会缓存成 Roaring Bitmap,位运算求交并很快,这也是 filter 比 must 高效的原因之一。
6.2 为什么说 ES 是"近实时",而不是实时?
考点:refresh 机制。
Tip
文档写进 ES 后先落在内存缓冲区和 translog 里,此时不可搜索。默认每 1 秒执行一次 refresh,把缓冲区数据生成新的 segment,文档才可见,所以默认有约 1 秒延迟,叫近实时(NRT)。需要立刻可见可以手动 refresh,但频繁 refresh 会产生大量小 segment,反而拖慢查询,批量导入时通常是先关掉 refresh、导完再打开。
6.3 深分页怎么处理?
考点 :from + size 的执行方式------协调节点要向每个分片 都要 from + size 条数据,再汇总排序。翻到第 100 页时,每个分片都得吐 1000 条出来,协调节点的内存和 CPU 都是灾难。默认上限 10000,超了直接报 Result window is too large。
Tip
三种方案按场景选。
from + size只适合浅分页,默认上限 10000;search_after用上一页最后一条的排序值当游标,每次只取 size 条,适合深分页和滚动加载,缺点是不能跳页;导出全量数据用 PIT +search_after,PIT 提供一致性快照,是官方现在推荐的方案,旧的 scroll 已经不再推荐。生产上一般翻页用 search_after,导出用 PIT。
6.4 text 和 keyword 怎么选?
考点:会不会在聚合和排序上踩坑。
Tip
text 会分词,用于全文检索;keyword 不分词、整串保留,用于精确匹配、聚合和排序。用 text 字段做聚合会直接报
Fielddata is disabled。常见做法是一个字段同时建两套:"title": {"type": "text", "fields": {"keyword": {"type": "keyword"}}},检索走 title,聚合排序走 title.keyword。
6.5 主分片数为什么创建后不能改?
考点:路由公式。
Tip
文档落在哪个分片由
shard = hash(_routing) % number_of_primary_shards决定,_routing默认取文档 ID。一旦改了主分片数,取模结果全变,已有数据就找不到了。所以主分片数必须在建索引时确定,要改只能新建索引再_reindex。副本分片数可以随时调整。ES 7.0 起默认主分片数是 1(之前是 5),现在官方更推荐先设少量分片、不够再 reindex,而不是一上来就开一堆。
6.6 MySQL 的数据怎么同步到 ES?
考点:分布式场景下的数据一致性意识。
Tip
三种主流方案。同步双写最简单,但业务代码耦合,两次写入不在一个事务里,失败就不一致;异步 MQ 解耦、可重试、能削峰,是主流做法;binlog 订阅(Canal、Debezium)对业务零侵入,存量加增量一次搞定,适合已有系统改造。无论哪种,ES 和 MySQL 之间都只能是最终一致,必须配补偿和对账,不能指望强一致。
6.7 集群脑裂怎么防?
考点:候选 master 节点的投票机制。
Tip
脑裂是集群被网络分区切成两半,两边各自选出 master,同时写数据导致冲突。ES 7.0 之前靠
discovery.zen.minimum_master_nodes参数防------设为「候选 master 节点数 ÷ 2 + 1」,候选节点必须拿到过半票数才能当选。7.0 起换成了新的集群协调层(Zen2),投票逻辑内置,这个参数已经取消 ,只需要正确配置cluster.initial_master_nodes(仅首次引导集群时使用)和discovery.seed_hosts。节点数上,三个专用候选 master 是最常见的配置------Zen2 靠多数派投票,奇数是个好习惯,但不是硬性要求。
写在最后
回到开头那张检索卡片。全文搜索还靠 LIKE 硬扛,问题从来不是"索引加没加",而是拿 B+ 树去解一道它不擅长的题。
倒排索引把方向反了过来:写入时建好"词 → 文档"的映射,搜索时一次定位。代价是写入链路更长、数据有约 1 秒才可见、segment 不可变导致删除只是打标记------你在生产上遇到的每一个"ES 的怪脾气",几乎都能追溯到这条写入链路。
面试也是如此:问倒排索引,其实是在问你知不知道它的时间和空间代价;问深分页,其实是在问你对分片模型的理解。把这条链路捋顺,这些面试题不过是换个说法再问一遍。
本文的引用仅限自我学习如有侵权,请联系作者删除。
参考知识
全文搜索还靠 LIKE 硬扛?Elasticsearch 从入门到精通与高频面试题精讲