面试追问地图

主问题必讲关键点下一层追问
倒排索引词典、倒排表、文档列表分词、跳表/压缩、为何适合全文搜索
Mapping字段类型、分词、doc_values动态字段爆炸、类型错误能否修改
分片副本路由、扩展、容灾分片过多/过大、如何重新规划
写入流程主分片、复制、translog、refresh写成功为何暂时搜不到
查询流程Query Then Fetch、协调合并分片数对查询延迟的影响
text/keyword分词与精确值排序聚合为何通常用 keyword
match/term查询分析与精确词项analyzer 不一致为何搜不到
深分页各分片 Top N、协调节点丢弃search_after、PIT、唯一排序键
聚合bucket、metric、pipeline高基数、内存和准确性
数据同步消息、CDC、补偿乱序更新、全量重建、别名切换
集群与选主健康状态、脑裂、gossipgreen/yellow/red、选主机制
性能优化写入/查询调优、分片规划、段合并forcemerge、堆内存设置、bulk 策略

ES 题要区分“写入确认”“持久化”“可搜索”三个时间点,并说明它通常是派生索引而非事实源。


一、基础概念

Elasticsearch 适合什么场景?

难度 🟢

  • 全文检索。
  • 多条件过滤和聚合分析。
  • 日志和可观测数据检索。
  • 商品、地点等复杂搜索。

它不适合替代所有关系数据库。事务、多表强一致关联和高频随机更新通常仍由数据库承担。

Index、Document、Field 分别是什么?

  • Index:逻辑数据集合。
  • Document:JSON 文档,是基本检索单位。
  • Field:文档字段,由 Mapping 定义类型和索引方式。

现代 Elasticsearch 已弱化 Type 概念,一个 Index 中通常使用统一文档结构。

分片和副本有什么作用?

  • 主分片:将数据水平拆分到多个节点。
  • 副本分片:提供容灾和读扩展。

主分片数量创建后调整成本较高。分片过多会增加内存、文件句柄和集群状态开销,分片过大又会影响迁移和恢复。

ES 路由机制(Routing)是如何工作的?

频次 ★★★★ · 难度 🟡 · 高频:美团/字节

ES 通过路由计算决定文档写入哪个分片:

shard = hash(routing) % number_of_primary_shards

默认 routing 值为文档 _id,也可在请求中显式指定:

PUT /my_index/_doc/1?routing=user123
{
  "title": "ES 教程"
}

为什么用 Routing:

  • 默认行为:按 _id 哈希均匀分布到各分片,绝大部分场景够用
  • 自定义路由:将相关文档路由到同一分片,查询时指定 routing 参数可跳过广播(仅扫描目标分片),显著提升查询性能;典型场景是按用户 ID 路由,查询该用户的所有文档只需扫描一个分片
  • 数据倾斜风险:若 routing 值分布不均匀(如按「大客户 vs 小客户」路由),某些分片数据量远超其他分片,导致集群负载不均和查询延迟波动

面试追问:

  • Routing 写错了能改吗?→ 主分片数创建后不可变,routing 目标是固定的;要重新分布只能 reindex 到新索引
  • 不指定 routing 查询会怎样?→ 广播到索引的全部主分片(或副本分片),协调节点合并结果
  • 自定义 routing 后还想按 _id 查询怎么办?→ 要么在请求中同时指定 routing(需已知 routing 值),要么在文档中存储 routing 值再搜索——没有 routing 时 ES 只能广播

二、倒排索引

倒排索引是什么?

难度 🔴

倒排索引建立“词项到文档”的映射:

高德 → 文档 1、文档 3
地图 → 文档 1、文档 2

全文检索时无需扫描所有文档,而是通过词典找到词项对应的文档列表,再计算相关性。

textkeyword 的区别?

textkeyword
是否分词
场景全文检索精确匹配、排序、聚合
示例标题、正文状态、编码、标签

同一字段可通过 multi-fields 同时建立 textkeyword 两种索引。

分词器由什么组成?

通常包括:

  • Character Filter:字符预处理。
  • Tokenizer:切分词元。
  • Token Filter:小写、停用词、同义词等处理。

索引时和查询时的分析方式需要兼容,否则可能出现“数据存在但搜不到”。

Mapping 为什么重要?

Mapping 决定字段类型、分词和索引能力。错误类型会导致:

  • 数字或日期范围查询异常。
  • 精确匹配字段被分词。
  • 动态字段爆炸,集群状态膨胀。

生产索引应使用显式 Mapping 和模板,谨慎依赖动态映射。


三、写入与查询流程

文档写入流程?

难度 🔴

  1. 请求根据文档 ID 路由到目标主分片。
  2. 主分片执行写入并记录操作。
  3. 操作复制到副本分片。
  4. 满足确认条件后响应客户端。
  5. Refresh 后文档才对搜索可见。

写入确认、持久化和搜索可见不是同一个时间点。

Refresh、Flush、Translog 的区别?

  • Refresh:生成新的可搜索 Segment,使近期写入可见。
  • Translog:记录尚未安全落盘到 Lucene 的操作,用于恢复。
  • Flush:提交 Lucene Commit,开启新的 Translog generation。

频繁手动 Refresh 会产生大量小 Segment,降低写入性能。

搜索请求的 Query Then Fetch 流程?

  1. 协调节点将查询发送到相关分片。
  2. 各分片本地计算并返回候选文档 ID 和排序值。
  3. 协调节点合并全局 Top N。
  4. 再到对应分片获取完整文档。

分片越多,协调和合并成本越高。

Filter 和 Query 有什么区别?

  • Query Context:计算匹配程度和相关性分数。
  • Filter Context:只判断是否匹配,适合状态、范围等精确条件,也更容易缓存。

业务查询应把不需要算分的条件放入 filter。

ES 如何实现并发控制(版本控制/乐观锁)?

频次 ★★★ · 难度 🟡 · 高频:阿里/腾讯

ES 使用基于版本号的乐观锁来避免并发更新冲突:

内部版本控制(_version):

  • 每个文档有递增的 _version 号,更新/删除时版本号随之递增
  • 写操作可指定 version 参数,要求当前版本必须匹配才执行:
PUT /my_index/_doc/1?version=5
{
  "title": "new title"
}
// 若当前 _version ≠ 5,返回 409 Conflict

外部版本控制(version_type=external):

  • 使用业务系统自己的版本号(如数据库行记录的版本),ES 只做「新版本号 > 旧版本号」的判断
  • 适合业务侧已有乐观锁的场景(如 MySQL 行版本 + CDC 同步到 ES):
PUT /my_index/_doc/1?version=10&version_type=external
{
  "title": "new title"
}

if_seq_no + if_primary_term(7.x+ 推荐方式):

  • _seq_no 是分片级的递增序号,_primary_term 是主分片的任期号
  • 先 GET 获取文档拿到当前 _seq_no_primary_term,更新时带回去做条件:
PUT /my_index/_doc/1?if_seq_no=5&if_primary_term=1
{
  "title": "new title"
}

对比:

方式粒度适用场景
_version文档全局简单并发控制
version_type=external文档全局业务已有版本号
if_seq_no + if_primary_term分片级精确 CAS(推荐)

常见追问:

  • 高并发写入场景版本冲突怎么办?→ 冲突概率不高时直接重试;冲突频繁应检讨并发模型(是否不必同时写同一文档),或用 retry_on_conflict 参数(自动重试,需幂等)

四、相关性与查询

matchterm 的区别?

  • match:会分析查询文本,适合 text 全文检索。
  • term:不分词,按精确词项匹配,适合 keyword、数字和状态字段。

text 字段直接使用 term 是常见的“查不到数据”原因。

BM25 是什么?

BM25 是常用相关性算法,主要考虑:

  • 词项在当前文档中的频率。
  • 词项在全部文档中的稀有程度。
  • 文档长度归一化。

业务排序通常还会结合时间、热度、距离和业务权重。

mustshouldfiltermust_not 如何理解?

  • must:必须匹配,参与算分。
  • should:可选匹配,可提升得分。
  • filter:必须匹配,不参与算分。
  • must_not:必须不匹配。

五、分页与聚合

Elasticsearch 深分页为什么慢?

难度 🔴

使用 from + size 时,每个分片都要取出足够多的候选结果,协调节点再全局排序并丢弃前面的数据。

页数越深,CPU、内存和网络开销越大。

深分页如何优化?

  • 普通页面限制最大页数。
  • 连续翻页使用 search_after
  • 需要稳定视图时结合 Point in Time。
  • 离线全量遍历使用适合的批处理方式。

search_after 需要稳定且唯一的排序字段,通常增加 _id 或业务唯一字段作为最后排序条件。

Aggregation 有哪些常见类型?

  • Bucket:按条件分组,如 terms、date histogram。
  • Metric:计算指标,如 avg、sum、cardinality。
  • Pipeline:基于其他聚合结果继续计算。

高基数字段 terms 聚合可能消耗大量内存,应控制桶数量和字段设计。


六、性能优化

写入性能如何优化?

  • 使用 Bulk 批量写入。
  • 控制批次大小和并发,避免压垮节点。
  • 批量导入期间适当增大 Refresh Interval。
  • 减少不必要字段和副本,导入后恢复配置。
  • 避免频繁更新同一文档。
  • 使用稳定且分布均匀的路由。

查询性能如何优化?

  • 精确条件使用 filter。
  • 避免通配符前缀和高开销脚本。
  • 控制返回字段和结果数量。
  • 合理设计 Mapping、分片和路由。
  • 对慢查询使用 Profile API 和慢日志分析。
  • 预计算高频复杂业务字段。

分片数量如何规划?

没有固定公式,应结合:

  • 总数据量和增长速度。
  • 单分片大小。
  • 查询并发和聚合复杂度。
  • 节点数量、堆内存和恢复时间。

可使用 rollover 按大小或时间滚动索引,避免单个索引无限增长。

为什么不建议大量使用脚本查询?

脚本通常无法充分利用索引,需要逐文档计算,CPU 成本高。高频查询应把可计算字段在写入时预处理。

段合并与 forcemerge 对写入性能的影响

频次 ★★ · 难度 🟡 · 高频:阿里

ES 的段(Segment)机制:

  • 写入 → 生成新的不可变段文件(类似 LSM-Tree)
  • 段过多 → 查询需要合并多个段的倒排表 → 性能下降
  • 后台线程定期合并段:小段合并成大段

段合并的过程:

  1. 后台线程选择合适大小的段(按大小分层)
  2. 读取段中的文档,合并排序,写入新段
  3. 旧段标记删除,查询切换到新段
  4. 释放旧段占用的文件句柄和内存

合并对写入的影响:

影响原因缓解措施
IO 压力飙升合并读取多个段再写入,产生大量磁盘 IO限制合并线程数、调大 indices.store.throttle.max_bytes_per_sec
写入延迟波动合并期间占用 IO 资源,写入请求排队错峰写入、使用 SSD
大段反复合并达到阈值后大段继续合并,越合越大合理设置 segments.merge.policy,对大索引用 forcemerge

forcemerge 用于索引只读场景(如日志索引归档):

POST /my_index/_forcemerge?max_num_segments=1
  • 强制合并到指定段数(通常 1),减少段数量
  • 期间 IO 压力巨大,必须在业务低峰期手动执行
  • 只读索引的 segment 不再变化,forcemerge 后查询性能显著提升

关联概念: 段合并本质是空间换时间——更多段 → 写入更快(不需要合并)、查询更慢(更多文件需扫描);更少段 → 写入变慢(合并开销)、查询更快。和 LSM-Tree(LevelDB/RocksDB)的 compaction 机制同源。


七、集群与故障

集群健康状态 green、yellow、red 表示什么?

  • Green:主分片和副本分片全部已分配。
  • Yellow:主分片可用,但部分副本未分配。
  • Red:至少一个主分片不可用,部分数据无法访问。

为什么分片会 unassigned?

  • 节点故障或磁盘不足。
  • 分配过滤和 awareness 约束。
  • 副本数超过可用节点数。
  • 分片恢复失败。
  • 集群磁盘水位限制。

应查看 Allocation Explain,而不是盲目重启节点。

脑裂如何避免?

现代集群通过选主和法定人数机制避免多个 Master 同时提交集群状态。部署时仍应:

  • 使用至少三个符合条件的主节点。
  • 跨故障域部署。
  • 保持稳定网络。
  • 不随意修改发现和选举配置。

Elasticsearch 选主机制

频次 ★★★ · 难度 🟡 · 高频:阿里

ES 使用 Zen Discovery 模块实现选主(7.x 后默认用基于集群状态版本的选主)。

7.x 前的选主(Zen Discovery):

  1. 节点发现其他节点后发起选举
  2. 每个节点投票给比自己集群状态版本高的节点,版本相同则比 nodeId(最小优先)
  3. 候选节点得票数 ≥ minimum_master_nodes 时成为 Master
  • 该参数推荐公式:quorum = N/2 + 1(N = 有资格成为 Master 的节点数)
  • 设置不当是脑裂的核心原因:设置得太小(如 1)→ 分区后两个子集都选出 Master

7.x 后的选主(改良版):

  • 内置法定人数检查(不再依赖手动配置 minimum_master_nodes
  • 基于集群状态版本 + 节点加入顺序,更稳定
  • Master 选举完成后定期续期 lease,旧 Master 重新加入时自动降级

选主触发条件:

  • 集群启动时
  • 当前 Master 失联(超时 discovery.zen.ping_timeout,默认 3s)
  • 节点无法连接 Master,发起新一轮选举

关联知识点: ZooKeeper 的 ZAB 协议也是多数派选主,但 ZK 用「事务 ID(zxid)」比较新旧,ES 用「集群状态版本」。

数据库与 Elasticsearch 如何保持一致?

常见方案:

  • 数据库写入后发送可靠消息,异步更新 ES。
  • 使用 CDC 订阅数据库变更。
  • 定时校验和补偿。
  • 查询不到或异常时降级回源。

ES 通常作为派生索引,不应成为核心业务唯一事实来源。


八、场景题

如何设计商品搜索?

  • 标题、描述使用 text
  • 品牌、类目、状态使用 keyword
  • 价格、销量和时间使用数值或日期类型。
  • 使用 bool query 组合全文检索与过滤。
  • 使用 function score 融合文本相关性、销量和新鲜度。
  • 使用聚合生成品牌、类目和价格筛选项。

如何设计地理位置搜索?

  • 使用 geo_point 存储经纬度。
  • 使用 geo distance 查询附近数据。
  • 按距离排序或衰减打分。
  • 先用业务条件过滤,再执行地理查询。
  • 对高频固定区域可考虑预计算区域编码。

参见 LBS 地理信息系统