Elasticsearch面试题
倒排索引原理
一句话核心定义 🔍
倒排索引是 ES 实现全文检索的核心数据结构,本质是 "通过词条 (Term) 找文档 (Document)",与传统数据库 "通过文档找内容" 的正排索引正好相反。
举个例子 🌰
我们可以用一个最简单的例子切入。假设有两条文档:
- 文档1:
Elasticsearch is fast - 文档2:
Elasticsearch is powerful
📖 正排索引是文档ID→词,就像从上到下读文章。
🔎 倒排索引则是词→文档ID列表,就像查字典。构建后长这样:
| 词项 (Term) | 倒排列表 (Postings List) |
|---|---|
| elasticsearch | [1,2] |
| is | [1,2] |
| fast | [1] |
| powerful | [2] |
所以用户搜 “elasticsearch”,无需扫描所有文档,直接拿到文档1、2,这就是 O(1) 级的查询提速。🚀
正排 vs 倒排 直观对比 📊
面试加分点:
正排索引适合按 ID 查内容,倒排索引适合按关键词查所有包含该词的文档,这就是 ES 做全文检索比 MySQL 快几个数量级的根本原因。
ES 倒排索引的三大核心结构 ⚙️
这是面试必问的关键点,也是区分初级和中级工程师的分水岭!
倒排索引在 ES 里不只是一张表,它是一个多层次数据结构的组合,用一张图来说明:
Term Dictionary(词条字典)📚
- 存储所有文档分词后得到的不重复词条
- 按字典序排序,天然支持前缀匹配、模糊查询
- 本质是一个排序好的字符串数组
2. Posting List(倒排列表)📋
- 存储包含某个词条的所有文档 ID
- 额外存储词频 (TF)、位置 (Position)、偏移量 (Offset) 元数据
- 这些元数据是实现相关性评分、高亮显示、短语精确查询的基础
3. Term Index(词条索引)🚀
- ES 独有的优化结构,90% 的面试者会漏掉这一点!
- 是 Term Dictionary 的 "索引的索引",存储词条前缀和对应的磁盘位置
- 采用FST (有限状态转换器) 数据结构,内存占用仅为哈希表的 1/10
- 核心作用:将查询复杂度从 O (n) 降到 O (log n),避免遍历整个词条字典
倒排索引的构建与查询流程 🔄
构建流程(文档写入时)
- 📝 文档写入 ES,经过分词器 (Analyzer) 拆分成多个词条
- 🔄 对每个词条,在 Term Dictionary 中查找,不存在则新增
- 📌 在对应的 Posting List 中添加当前文档 ID 和元数据
- 💾 定期将内存中的索引刷写到磁盘,生成不可变的 Segment 文件
查询流程(用户搜索时)
- 🔍 用户输入查询词,经过相同的分词器拆分成词条
- 🚀 通过 Term Index 快速定位到 Term Dictionary 中词条的位置
- 📋 获取该词条对应的 Posting List
- ⚖️ 根据 BM25 算法对文档进行相关性评分
- 📤 返回评分最高的 Top N 文档给用户
📊 一个真实的 ES 倒排存储示意
用 JSON 思维来看,一个 text 字段的倒排就像:
"fast": {
"postings": [
{"docId": 1, "tf": 1, "positions": [2]}
]
}这也就是我们常说的 “空间换时间”,牺牲一些磁盘和内存,换取查询时无需全表扫描。
⚙️ 为啥这么设计?(接地气的解释)
- Segment 不可变:ES 底层是 Lucene,每收一批文档写成一个 Segment,Segment 内的倒排索引一旦生成就不再修改。这样不用加锁,多线程并发查询超快。
- 近实时搜索:默认 1s refresh,把内存的文档刷成新 Segment,让新数据可见。
- 排序/聚合不怕:倒排索引擅长找文档,但不擅长“按价格排序”。所以 ES 额外维护 Doc Values(正排列存),作为排序和聚合的数据源。
进阶优化点(面试加分项)✨
- 跳表 (Skip List):Posting List 内部使用跳表结构,大幅提升多个 Posting List 合并(AND/OR 查询)的效率
- FST 压缩:Term Index 采用 FST 压缩,百万级词条仅占用几 MB 内存
- 段合并 (Merge):ES 定期将小 Segment 合并成大 Segment,减少磁盘 IO,提升查询性能
一句话总结:📌 查什么词,直接翻词典,找到倒排列表取文档,需要排序再查列存。 这条链路让 ES 在 PB 级数据下仍能毫秒响应。
ES 集群架构:Node 角色、分片与副本
面试官您好,我来回答一下 ES 集群架构的核心组成部分,主要从节点角色、分片和副本三个维度展开,这三者共同构成了 ES 高可用、高扩展的基础。
先上一张图,直观感受集群结构
🟢 这张图里有四个核心角色,下面逐一拆解。
Node 节点角色 🎭
ES 集群由多个 Node(节点)组成,每个节点是一个独立的 JVM 进程,通过不同的角色分工协作。
核心节点角色表
| 角色名称 | 核心职责 | 配置方式 | 生产建议 |
|---|---|---|---|
| Master 节点 | 集群元数据管理(索引创建 / 删除、分片分配)、集群状态维护 | node.roles: [master] | 单独部署 3 个组成脑裂防护组,奇数个 |
| Data 节点 | 存储索引数据、执行 CRUD / 搜索 / 聚合操作 | node.roles: [data] | 水平扩展主力,根据数据量调整数量 |
| Coordinating 节点 | 接收客户端请求、路由到对应节点、汇总结果返回 | node.roles: [] | 大流量场景单独部署,分担负载 |
| Ingest 节点 | 数据写入前的预处理(管道、转换、过滤) | node.roles: [ingest] | 数据清洗复杂时单独部署 |
👑 Master 节点(主节点)
- 职责:不是干读写活儿的,它是集群的“大脑”,负责:
- 索引的创建/删除
- 分片的分配与再平衡
- 节点加入/摘除
- 注意:生产环境 必须部署奇数个专用 Master 节点(如3个),防止脑裂。配置是:
node.master: true
node.data: false📦 Data 节点(数据节点)
- 职责:扛数据的,执行文档的增删改查、聚合、排序。
- 配置:
node.master: false
node.data: true- 我的经验:一定要用 SSD + 大内存,而且按冷热数据分层(Hot-Warm),不然 GC 时间一长,节点就掉线了 🔥。
🧭 Coordinating 节点(协调节点)
- 每个节点默认都是协调角色,但可以 专用的协调节点。
- 职责:接收客户端请求,路由到目标分片,汇总结果返回。
- 关键点:它不存数据、不当 Master,只做“收发室+聚合”,能有效分担 Data 节点的聚合内存压力。
node.master: false
node.data: false
node.ingest: false🧪 Ingest 节点(预处理节点)
- 职责:在索引文档之前,执行管道(Pipeline)加工,比如字段改名、增加时间戳、Grok 解析日志。
- 用好了能剥离应用程序里的数据清洗逻辑,但注意管道里别放太重的正则。
⚠️ 默认情况下,elasticsearch.yml 不配角色,一个节点啥都干,开发环境随便,生产上务必 角色分离。
节点角色关系图
关键点:生产环境强烈建议角色分离,不要让一个节点同时承担 Master 和 Data 角色,避免数据节点压力过大导致 Master 节点不稳定,引发集群脑裂。
分片(Shard) 🧩
分片是 ES 数据存储的最小单元,本质上是一个 Lucene 索引。ES 将一个大索引拆分成多个分片,分布在不同的数据节点上。
分片核心特性
- 水平扩展能力:单节点磁盘 / CPU 有限,分片可以分散到多个节点,突破单节点性能瓶颈
- 并行计算能力:搜索 / 聚合请求可以在多个分片上同时执行,最后汇总结果
- 主分片(Primary Shard):数据写入的唯一入口,每个文档只属于一个主分片
- 分片数量一旦创建不可修改(ES7 + 默认 1 个主分片,ES6 及以前默认 5 个)
分片分配示意图
生产建议:单个分片大小控制在10GB-50GB之间,过小会导致分片过多增加元数据负担,过大则会影响数据恢复和迁移速度。
副本(Replica) 🛡️
副本是主分片的完整拷贝,与主分片存储完全相同的数据,是 ES 高可用的核心保障。
副本核心作用
- 高可用性:主分片所在节点宕机时,副本会自动提升为新的主分片,保证服务不中断
- 负载均衡:读请求可以路由到主分片或任意副本分片,提升查询吞吐量
- 数据可靠性:多副本存储避免单点故障导致数据丢失
分片与副本完整架构图
关键点:
- 主分片和它的副本绝对不能在同一个节点上,否则失去高可用意义
- 副本数量可以动态修改,生产环境通常设置1-2 个副本(3 副本及以上性价比低)
- 写入操作只在主分片执行,成功后同步到所有副本,副本同步完成才返回客户端成功
我举个场景:一个 3 节点集群,创建索引 order,设置:
- 3 个主分片 (Primary Shard)
- 1 个副本 (Replica,每个主分片有一个副本)
实际分布就像这样:
Node 1 Node 2 Node 3
┌──────────┐ ┌──────────┐ ┌──────────┐
│ P0 │ │ P1 │ │ P2 │
│ R1 │ │ R2 │ │ R0 │
└──────────┘ └──────────┘ └──────────┘- P0/P1/P2:3 个主分片,每个文档根据
_routing哈希落位。 - R0/R1/R2:对应的副本,绝对不允许和主分片在同一节点(否则没容错意义)。
🔑 核心机制要搞透
1)路由公式
shard_num = hash(_routing) % num_primary_shards这也是为啥 主分片数一旦设定就不能改——mod 的分母变了,所有文档的路由都会乱掉。要改只能重建索引 + Reindex ⚠️。
2)读写流程
- 写:先写到主分片,再同步到副本。默认
wait_for_active_shards=1,生产可以设 all 保证多数派写入。 - 读:协调节点会轮询主/副分片,分摊查询压力,所以加副本能 线性提升读并发。
3)副本的“双刃剑”
✅ 好处:数据高可用 + 读性能提升
❌ 代价:降低写入速度(多了同步开销),占用更多磁盘和内存。
所以:不是副本越多越好,一般 1~2 个副本,结合集群规模平衡。
4)故障转移验证
我特意做过演练:kill 掉 Node1,集群秒级将 P0 的副本 R0 提升为主分片,服务无感知 🎯。这就是为什么 P0 和 R0 必须在不同节点上。
我踩过的一个坑 🩸
曾经为了省机器,3节点集群没分离角色,所有节点 master:true + data:true。结果一个数据节点 GC 超时,导致 Master 心跳丢失,触发重选,整个集群短暂不可用。
后来老老实实拆成:3 个专用 Master + 若干 Data + 2 个 Coordinating,集群稳如老狗。
面试加分总结 ✨
- ES 集群通过角色分离实现职责清晰,Master 节点负责管理,Data 节点负责存储,Coordinating 节点负责路由
- 分片解决了数据水平扩展和并行计算问题,副本解决了高可用和读负载均衡问题
- 生产环境核心原则:Master 节点奇数个、角色分离、单分片 10-50GB、1-2 个副本
- 常见坑点:分片数量设置不合理(过多或过少)、主副分片同节点、混合角色节点压力过大
“ES 集群的精髓就是 角色分离让控制面和数据面解耦,通过 主分片散列数据、副本提供容错和读扩展,再配合合理的节点角色配置,才能扛住大厂级的高并发搜索与分析。主分片数量提前规划,副本按需动态调整,这是线上运维的铁律。”
写入流程:refresh、flush、translog
面试官您好,我来给您梳理一下 Elasticsearch 的写入流程,以及 refresh、flush、translog 这三个核心机制的作用、区别和联系:
整体写入流程(全局视角 🗺️)
核心逻辑:写入先到内存 + 日志,再通过 refresh 实现可搜索,最后通过 flush 完成磁盘持久化。
先上一张全景图(灵魂画手版)
三大核心机制深度解析(重点 🔥)
| 机制 | 触发时机 | 核心作用 | 数据持久化 | 性能影响 | 默认配置 |
|---|---|---|---|---|---|
| refresh | 定时触发 / 手动调用 | 让数据可搜索(近实时) | ❌ 仅生成内存 Segment | 中(生成倒排索引消耗 CPU) | 1 秒 |
| translog | 每次写入 / 定时刷盘 | 防止节点宕机数据丢失 | ✅ 先写内存,默认每次请求 fsync | 低(顺序写,性能极高) | 每 5 秒刷盘,每次请求同步刷盘 |
| flush | translog 满 / 定时 / 手动调用 | 真正持久化到磁盘 | ✅ 内存 Segment+translog 全量刷盘 | 高(磁盘 IO 密集型操作) | 30 分钟或 translog 达到 512MB |
2.1 refresh:近实时搜索的核心 ⏱️
- ES 不是实时搜索,是近实时(NRT),根源就是 refresh 机制
- 内存缓冲区的原始数据不能直接搜索,必须生成倒排索引结构的 Segment 文件
- refresh 只是把数据从 JVM 堆内存移到操作系统文件缓存(OS Cache),并没有刷到物理磁盘
- 频繁 refresh 会产生大量小 Segment,导致后续合并压力暴增,严重拖慢集群性能
2.2 translog:数据可靠性的保障 🛡️
- 解决内存数据易失性问题:节点宕机时,内存缓冲区的数据会全部丢失,translog 可以用来恢复
- 写入流程是先写 translog,再写内存缓冲区,默认每次写入请求都会等待 translog fsync 到磁盘后才返回成功
- translog 是顺序写,性能远高于随机写,所以对整体写入性能影响极小
- translog 本身也有刷盘机制,可配置为异步刷盘来进一步提升写入性能(牺牲少量可靠性)
2.3 flush:真正的磁盘持久化 💾
- flush 操作会做两件核心事:
- ① 将内存中的所有 Segment 刷到物理磁盘;
- ② 清空对应的 translog 日志
- flush 是比较重的操作,会产生大量磁盘 IO,ES 会自动控制 flush 频率,避免影响业务
- 当 translog 达到最大大小(默认 512MB)或者超过 30 分钟没有 flush 时,会自动触发 flush
- 手动 flush 一般只在重启节点、关闭索引、备份数据时使用,避免数据丢失
三者关系与常见面试误区 ❌
核心关系
写入数据 → 内存缓冲区 + translog → refresh → 生成内存Segment(可搜索) → flush → Segment刷盘 + 清空translog- refresh 不影响 translog,只有 flush 才会清空 translog
- 节点宕机重启时,会先加载磁盘上已有的 Segment,再重放 translog 中未被 flush 的操作,恢复完整数据
高频误区
- ❌ 误区 1:refresh 会把数据刷到磁盘 → 错,refresh 只是到 OS Cache
- ❌ 误区 2:translog 是在 flush 之后才写入的 → 错,translog 是写入时就同步写了
- ❌ 误区 3:flush 和 refresh 是一回事 → 错,flush 负责持久化,refresh 负责可搜索
实战优化建议 💡
- 非实时搜索场景(如日志分析):调大 refresh 间隔到 30 秒甚至 1 分钟,写入性能可提升数倍
- 写入量大、可靠性要求稍低的场景:将 translog 刷盘策略改为异步(
index.translog.durability: async) - 避免手动频繁执行 flush 操作,会严重阻塞集群写入
- 合理设置
index.translog.flush_threshold_size,避免 translog 过大导致恢复时间过长
面试加分总结 ✅
写入就像往银行存钱:
translog是给你的小票凭证,掉了也能查;refresh是柜员把钞票放进柜台透明抽屉,你能看到但拿不走flush是下班后统一押运进金库,彻底安全 🔐
面试官,总结一下:ES 的写入流程通过内存缓冲区提升写入速度,通过 refresh 实现近实时搜索,通过 translog 保证数据可靠性,通过 flush 实现最终的磁盘持久化。这三个机制是 ES 在写入性能、搜索实时性、数据可靠性三者之间做的精妙权衡,也是 ES 能够支撑海量数据读写的核心设计之一。
查询流程:Query Then Fetch
ES 默认的搜索类型就是 query_then_fetch,它把一次完整的搜索拆成了先筛选、后拉取两个独立阶段,核心目的是用最小的网络开销完成分布式排序与分页。
核心定义
Query Then Fetch 是一种两阶段分布式查询流程:先在所有分片上执行查询获取文档 ID 和评分,再根据全局排序结果拉取完整文档。它平衡了性能和网络开销,适用于 90% 以上的常规查询场景。
完整流程拆解(带时序图)
阶段一:Query(查询 / 海选)
这个阶段只传 “轻量级” 元数据,不传 _source 大字段。
- 协调节点把请求广播到索引的所有相关分片(主分片或副本)。
- 每个分片在本地执行搜索,生成一个优先级队列,只返回
from + size个文档的 ID 和 排序所需字段(如 _score)。 - 协调节点收集所有分片的结果,在内存里做全局排序,截断出最终要返回给客户端的文档 ID 列表。
- ✨ 关键理解:如果查第 1000 页(深度分页),每个分片要乖乖返回
1000 * pageSize条数据给协调节点,这就是性能杀手。
阶段二:Fetch(拉取 / 决赛圈)
- 协调节点根据文档 ID,向所在的分片发起 Multi-Get 请求。
- 各分片返回完整的 _
source、高亮信息、存储字段等。 - 协调节点拼装响应,返回客户端。
🧩 灵魂比喻:超市买年货 🛒
| 流程阶段 | 超市场景 | 技术映射 |
|---|---|---|
| Query 阶段 | 你列好购物清单,派 3 个店员分别去零食区、生鲜区、饮料区,让他们只拿价签(文档ID + 得分)回来 | 协调节点让分片只回传排序用的元数据,数据量极小 |
| 合并排序 | 你把 3 个店员递来的价签摊在桌上,比价、排序、挑出最想要的 10 件 | 协调节点在内存中全局排序,选出最终 Top N |
| Fetch 阶段 | 你拿着挑好的 10 个价签,再让对应店员去把实物搬来(文档全文) | 按需拉取完整 _source,避免无用传输 |
👉 如果一开始就让店员把整箱货物搬出来再挑,网络和体力直接就崩了。 这正是 query_then_fetch 的设计哲学。
面试官必问的核心关键点 ✅
- Query 阶段的核心优化:只返回
doc_id + 相关性评分,不返回完整文档,大幅减少网络传输量 - 分布式并行计算:每个分片独立执行查询,充分利用集群的 CPU 和内存资源
- 分片选择策略:默认优先选择副分片执行查询,分担主分片的写入压力
- 深度分页问题的根源:当
from=10000, size=10时,每个分片要返回 10010 条结果,协调节点需要合并分片数 × 10010条数据,内存压力呈指数级增长
追问彩蛋:那 “深度分页” 咋办?
- 为什么慢:from 越大,Query 阶段每个分片要回传的数据越多,协调节点排序压力爆炸。
- 解法:
- 🔄 改用
search_after(游标实时滚动,避开 from)。 - 📜 历史数据/不关心实时性用 Scroll(保留快照遍历,但已不推荐用于深度分页)。
- ⚡ 业务上直接限制翻页深度,或用异步导出。
- 🔄 改用
⚠️一个常见误区
“Query 阶段分片是不是只给协调节点传 10 条数据?”
不是。假设你要 from=90, size=10,每个分片必须回传 100 条。协调节点从 N×100 条里排序,再砍掉前 90 条,剩下 10 条去 Fetch。这就是深度分页资源消耗大的根本原因。
优缺点分析
| 优点 ⚡ | 缺点 ⚠️ |
|---|---|
| 实现简单,性能稳定,ES 默认查询方式 | 存在深度分页问题,默认最多返回 10000 条结果 |
| 网络传输量小,适合大多数常规查询 | 查询过程中文档更新可能导致轻微结果不一致 |
| 支持复杂查询条件和全文检索 | 不适合超大结果集的全量导出 |
易混淆点对比 💡
和Query And Fetch的区别:
- Query And Fetch 是单阶段查询:每个分片直接返回完整的 top N 文档,协调节点只做排序
- 优势:速度更快(少一次网络往返)
- 劣势:网络传输量极大,只适合
size非常小(如 < 10)的场景
面试官高频追问延伸 🔍
问:怎么解决 Query Then Fetch 的深度分页问题?
答:
- 滚动分页:使用
search_after(适合无限滚动加载,推荐) - 全量导出:使用
scroll API(适合一次性导出全量数据) - 不推荐:修改
index.max_result_window参数,会大幅增加集群内存风险
分词器与 IK 分词器配置
我打个比方,数据库 like '%手机%' 就像你拿把尺子量整篇文章,必须一个字一个字对齐才能命中。而分词器是把‘我想买华为手机’先切成 ‘我’、‘想’、‘买’、‘华为’、‘手机’,再建倒排索引,搜索‘手机’直接命中,效率高得多,还能支持相关性算分。
ES 分词器核心概念与工作原理 🧠
ES 分词器(Analyzer)是全文检索的灵魂,负责将输入的文本切分成一个个独立的、可被搜索的词项(Term)。
一个完整的分词器由三部分组成,执行顺序严格如下:
| 组件 | 组件 | 作用 | 常见例子 |
|---|---|---|---|
| 字符过滤器 | Character Filter | 预处理原始文本,过滤或替换字符 | HTML 标签过滤、表情符号替换、大小写转换 |
| 分词器 | Tokenizer | 将文本切分成独立的词项 | 标准分词器、空格分词器、IK 分词器 |
| 词项过滤器 | Token Filter | 对切分后的词项进行二次处理 | 小写转换、停用词过滤、同义词扩展、词干提取 |
用个流程图表示就是:
这三种组件打包起来就叫一个 Analyzer(分析器)。
关键知识点:
- ES 内置了多种分词器,但对中文支持极差(标准分词器会把中文拆成单个汉字)
- 分词发生在两个阶段:索引时(写入文档)和搜索时(查询文本),两者必须使用相同的分词器,否则会出现搜不到的情况!
IK 分词器详解与核心配置 ⚙️
IK 分词器是目前最流行的中文分词器,提供两种分词模式:
ik_smart:智能切分,粗粒度,适合搜索时使用ik_max_word:最细粒度切分,穷尽可能的组合,适合索引时使用
举个例子:文本 "我爱北京天安门"
- ik_smart:
["我", "爱", "北京", "天安门"] - ik_max_word:
["我", "爱", "北京", "天安门", "天安", "门"]
1. 基础配置(elasticsearch.yml)
# 配置IK分词器的扩展词典和停用词词典
ik:
analyzer:
ik_smart:
use_smart: true
ext_dict: custom.dic # 自定义扩展词典
ext_stopwords: stopwords.dic # 自定义停用词词典2. 索引级别配置(创建索引时指定)
PUT /my_index
{
"settings": {
"analysis": {
"analyzer": {
"my_ik_analyzer": {
"type": "custom",
"tokenizer": "ik_max_word",
"filter": ["lowercase", "my_stop_filter"]
}
},
"filter": {
"my_stop_filter": {
"type": "stop",
"stopwords_path": "stopwords.dic"
}
}
}
},
"mappings": {
"properties": {
"content": {
"type": "text",
"analyzer": "my_ik_analyzer", // 索引时使用
"search_analyzer": "ik_smart" // 搜索时使用
}
}
}
}索引时用 ik_max_word 切得更全,搜索时用 ik_smart 避免过度匹配,这样查准率和查全率能平衡。
3. 热更新配置(生产环境必备)
IK 分词器支持不重启 ES的情况下更新词典:
- 在
config/analysis-ik/目录下创建IKAnalyzer.cfg.xml - 配置远程词典地址:
<entry key="remote_ext_dict">http://your-server/dict/custom.dic</entry>
<entry key="remote_ext_stopwords">http://your-server/dict/stopwords.dic</entry>IK 会每分钟自动检测远程词典的Last-Modified头,有变化就自动重新加载
插件命令安装
# 1. 进到 ES 的 bin 目录
cd /usr/share/elasticsearch/bin
# 2. 执行安装(版本号要跟 ES 一致)
./elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.17.10/elasticsearch-analysis-ik-7.17.10.zip
# 3. 重启该节点(滚动重启)
systemctl restart elasticsearch装完后,config/analysis-ik/ 目录下就会多出 IKAnalyzer.cfg.xml 和词典文件。验证一下:
POST _analyze
{
"analyzer": "ik_max_word",
"text": "程序员面试必备宝典"
}如果能返回 ‘程序员’,‘面试’,‘必备’,‘宝典’,就成功了 ✅。”
实际项目中踩过的坑 💣
- 索引和搜索分词器不一致:导致明明文档里有内容却搜不到,这是最常见的坑
- 没有使用自定义词典:专业术语、品牌名、人名被错误切分(比如 "小米手机" 被拆成 "小"、"米"、"手机")
- 停用词配置不当:把重要的词过滤掉了,或者该过滤的没过滤(比如 "的"、"了"、"是")
- 热更新不生效:远程词典没有正确设置
Content-Type: text/plain和Last-Modified头 - 分词器版本不匹配:IK 分词器版本必须和 ES 版本完全一致,否则会启动失败
常见问题
👨💻 面试官:“那业务上肯定有专属词,比如你们内部代号、产品名,词典怎么搞?”
🧑💻 候选人:
“词典是 IK 的灵魂。我们改 IKAnalyzer.cfg.xml,配上自己的扩展词典和停用词典:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!-- 扩展词典(新词) -->
<entry key="ext_dict">custom/mydict.dic</entry>
<!-- 停用词典(忽略词) -->
<entry key="ext_stopwords">custom/ext_stopword.dic</entry>
</properties>custom/mydict.dic 里一行一个词,比如:
鸿蒙
双十一爆款
低代码平台配好重启一下,这些词就能被正确切出来,不会拆开。搜索‘鸿蒙’就能精准命中 💪。”
👨💻 面试官:“每次加词都重启,这肯定不行吧?有没有办法 热更新?”
🧑💻 候选人:
“这就是关键进阶点 🚀。官方 IK 自带的只能改文件重启,我们做了 字典热更新,方案是:IK + MySQL 远程词典。
利用 IK 的 Remote Dictionary 机制,配一个 HTTP 接口,定时拉取词库。具体配置改 IKAnalyzer.cfg.xml:
<!-- 远程扩展词典 -->
<entry key="remote_ext_dict">http://192.168.1.100:8080/ik/words</entry>
<entry key="remote_ext_stopwords">http://192.168.1.100:8080/ik/stopwords</entry>IK 内部会起一个线程,默认 每 60 秒 去请求这个接口,返回纯文本词表。如果响应头的 Last-Modify 有变化(或者 MD5),它会自动重载词典,完全不用重启 ES。
我们的接口背后连 MySQL,运营在后台加新词后,接口数据变了,IK 一分钟内自动感知。对于大部分非实时场景足够了。如果需要秒级,就得改 IK 源码的轮询间隔,或者换成文件监听的方案。
这就是我们生产落地的完整词库管理方案 😎。”
👨💻 面试官:“很好,这个方案兼顾了稳定性和灵活性。那最后问一个开放性的:如果用 IK 分词,怎么排查“明明有数据但搜不出来”的问题?”
🧑💻 候选人:
“这类问题九成跟分词结果有关。我第一板斧就是 _analyze API 看切词:
GET _analyze
{
"analyzer": "ik_max_word",
"text": "我想买最新款华为手机"
}看切出来的词项是不是我索引里的词。如果关键术语被切开,那就是词典没覆盖。再看 mapping 确认字段的 analyzer 和 search_analyzer 是否配错。最后查 _termvectors 看一眼文档的真正词项,两头对一下就能定位。
好,这套下来,从原理到落地再到排障,基本就闭环了 🔚。”
深度分页问题:from+size 限制、scroll、search_after
为什么会有深度分页问题?⚠️
ES 是分布式搜索引擎,数据分散存储在多个分片上。分页查询时:
- 协调节点向所有分片发送查询请求
- 每个分片返回
from + size条数据 - 协调节点合并所有分片的数据,全局排序后取
size条返回
深度分页的致命问题:当from很大时(比如from=9990, size=10),每个分片要返回 10000 条,10 个分片就是 10 万条,协调节点要在内存中排序 10 万条数据再取最后 10 条,内存和 CPU 开销呈指数级增长,极易导致集群 OOM。
三种分页方案核心解析
1. from+size:最常用但有硬限制 ❌
- 原理:最基础的分页方式,直接指定起始位置和每页条数
- 默认限制:最多只能查询前 10000 条数据(由
index.max_result_window参数控制) - 为什么限制 10000 条?:这是 ES 的安全机制,防止恶意深度分页拖垮整个集群
- 优缺点:
- ✅ 支持跳页,使用简单
- ❌ 深度分页性能极差,有 OOM 风险
- 适用场景:前 100 页的浅分页(比如网站的普通分页)
- 协调节点需要从 每个分片 拉取
from+size条数据(这里是 10010 条) - 在内存里做全局排序,再丢弃前面 10000 条,只保留最后 10 条
- 分片越多、翻页越深,网络、CPU、内存开销越恐怖 💥
- 所以 ES 强制
index.max_result_window默认 10000,一刀切保护集群
结论:from+size 只适合浅分页,比如前几十页。
2. scroll:滚动查询,适合全量导出 📦
- 原理:第一次查询时生成一个数据快照,保存查询上下文,后续通过scroll_id分批从快照中获取数据
- 优缺点:
- ✅ 无 10000 条限制,支持查询全量数据
- ❌ 不支持跳页,只能顺序滚动
- ❌ 快照是查询时刻的静态数据,看不到后续更新
- ❌ 会占用大量内存保存上下文,过期时间需合理设置
- 适用场景:全量数据导出、离线数据分析(绝对不能用于用户实时分页)
3. search_after:官方推荐的深度分页方案 ✅
- 原理:利用上一页最后一条数据的排序值作为下一页的查询条件,每次只查询size条数据
- 关键要求:必须使用唯一且不可变的排序字段(通常用_id作为兜底排序字段)
- 优缺点:
- ✅ 无 10000 条限制,性能始终稳定
- ✅ 支持实时数据,每次查询都是最新的
- ✅ 内存占用极低,不会拖垮集群
- ❌ 不支持跳页,只能一页一页往后翻
- 适用场景:大数据量的实时分页(比如 APP 的无限滚动、下拉加载更多)
- 为什么它能解决深度分页?
- 每个分片只需取
size条order_id > 1024的数据,不用再拉取并丢弃大量无效数据 - 协调节点合并的数据量始终是
shards * size,不会随翻页深度膨胀 - 支持实时写入可见,适合 移动端下拉刷新、无限滚动 🚀
- 每个分片只需取
GET /order/_search
{
"size": 10,
"sort": [
{ "order_id": "asc" } // 必须有唯一排序字段
],
"search_after": [ 1024 ] // 上一页最后一条的 order_id
}核心指标对比表 📊
| 分页方式 | 最大查询量 | 跳页支持 | 实时性 | 性能表现 | 内存占用 | 典型适用场景 |
|---|---|---|---|---|---|---|
| from+size | 默认 10000 条 | ✅ 支持 | ✅ 实时 | 浅分页快,深分页极差 | 深分页极高 | 网站普通分页(前 100 页) |
| scroll | 无限制 | ❌ 不支持 | ❌ 快照级 | 全量查询快 | 高 | 全量数据导出、离线分析 |
| search_after | 无限制 | ❌ 不支持 | ✅ 实时 | 始终稳定 | 极低 | APP 无限滚动、大数据实时分页 |
执行流程对比图 🗺️
面试加分点 💡
1. 为什么 search_after 比 scroll 性能好?
scroll 需要保存整个查询上下文的快照,而 search_after 每次都是独立的查询,不需要保存任何上下文,内存占用小,而且支持实时数据。
2. search_after 为什么必须用唯一排序字段?
如果排序字段不唯一(比如只按时间排序),同一时间有多条数据,下一页查询时会出现数据丢失或重复,所以必须加上_id作为第二个排序字段保证唯一性。
3. 业务必须支持跳页怎么办?
- 限制用户跳页范围(比如最多只能跳转到第 100 页)
- 前端缓存前几页的排序值,实现有限范围的跳页
- 对于超大数据量,建议使用滚动查询预先生成分页数据
总结:ES 的深度分页问题本质上是分布式系统的共性问题,没有完美的解决方案。浅分页用 from+size,全量导出用 scroll,实时大数据分页用 search_after,这是行业内的最佳实践。
Elasticsearch的索引优化问题
面试官您好,关于 Elasticsearch 的索引优化,我会从设计、写入、查询、运维四个核心维度展开,每个维度都是生产环境可落地的关键优化点,整体优化流程如下:
设计阶段(最关键,决定 80% 的性能)✅
1.分片数规划:每个分片建议 20-40GB,分片数 = 节点数 ×1.5~3
- ❌ 误区:分片数越多越好。过多分片会增加 Master 元数据压力,降低查询性能
- 示例:3 节点集群,单索引数据量 100GB → 分片数设为 6(3×2)
2.副本数设置:
- 写入密集型:临时设为 0 副本,写入完成后再恢复
- 查询密集型:设为 1-2 副本,生产环境至少 1 副本保证高可用
3.字段映射优化:
- 禁用
_all字段(ES7 + 默认禁用) - 不需要分词的字段用
keyword,不需要索引的字段设index: false - 大文本用
text+keyword双类型,兼顾分词查询和精确匹配
4.索引生命周期管理 ILM:
按时间分索引(如日志按天),实现冷热分离,过期数据自动删除 / 归档
5.Mapping 建模优化
| 优化点 | 错误做法 | 正确做法 | 收益 |
|---|---|---|---|
| 字段类型 | 字符串全用 text | 精确匹配用 keyword,全文搜索才用 text | 省掉不需要的倒排索引和分词开销 📉 |
| 动态映射 | 全开 dynamic:true | 关闭或严格化 dynamic:strict,显式定义核心字段 | 避免字段爆炸和类型冲突 |
| 数值类型 | 全部用 long | 够用就选 byte/short/integer,必要时用 scaled_float 存金额 | 磁盘和缓存友好 |
| 不需要的字段 | 默认 _source 全存 | 大字段(如日志原文)可设置 enabled:false 或干脆不存 _source(谨慎) | 磁盘 IO 锐减 |
⚠️ 一个经典坑:把订单状态这种枚举值存成 text,查询时还用 term 查,发现永远查不到,因为 text 会分词成小写字母,term 查的是精确未分词 token。改成 keyword 立马解决。
写入阶段优化(高并发写入核心)⚡
- 批量写入:使用 Bulk API,每次批量大小 5-15MB,不超过 1000 条 / 次,避免 OOM
- 刷新间隔调整:默认 1s,写入密集型调大到 30s 甚至 60s,减少段合并压力
- 段合并策略:使用
LogByteSizeMergePolicy,设置max_merge_at_once=10、segments_per_tier=10 - Translog 优化:将
index.translog.durability设为async,sync_interval设为 30s,牺牲少量可靠性换取写入性能
查询阶段优化(直接影响用户体验)🔍
- 避免全表扫描:必须带过滤条件,禁止用
match_all查询大量数据 - 合理使用路由:将相同路由值的数据分到同一个分片,查询时只查一个分片,减少跨分片开销
- 示例:电商商品索引按商家ID路由,查询商家商品时只查一个分片
- 分页优化:
- 浅分页:
from+size(最大 10000 条) - 深分页:用
search_after(推荐) - 离线导出:用
scroll
- 浅分页:
- 过滤查询优先:用
filter代替query,filter不计算评分且会缓存结果,性能提升 3-10 倍
运维阶段优化(保障长期稳定)🛠️
- 节点角色分离:Master、Data、Ingest、Coordinating 节点分离,避免单点故障
- 堆内存优化:堆内存设为物理内存的一半,绝对不超过 31GB(超过 32GB JVM 压缩指针失效,内存利用率骤降)
- 磁盘优化:必须使用 SSD,磁盘使用率不超过 85%,超过后 ES 会自动禁止写入
- 监控告警:重点监控分片状态、段合并次数、GC 频率、磁盘使用率、查询延迟等指标
💡 面试加分点
总结 —— 一张优化清单收尾 ✅
| 场景 | 优化方向 | 关键参数/方法 |
|---|---|---|
| 海量日志写入 | 牺牲实时可见性、批量写入 | refresh_interval=30s, async translog, bulk size 10MB |
| 电商搜索查询 | 精确路由、预缓存、filter 缓存 | 按用户/商品 routing,filter bool,禁用脚本评分 |
| 磁盘紧张 | 压缩、减少 copy、清理旧数据 | codec: best_compression, force merge, 滚动删除索引 |
| GC 频繁 | 控制分片大小、堆比例、段数量 | 分片<50GB,堆<30GB,段数<500 |
如果能结合实际业务场景展开,比如:
- 日志系统:按天分索引 + ILM 冷热分离 + forcemerge 只读索引
- 电商系统:商品索引用路由优化 + 索引别名无缝切换
- 大数据场景:跨集群搜索 + 分片级别的负载均衡
