Elasticsearch 原理深度剖析 🔍

理解 ES 内部机制,掌握搜索引擎技术精髓

ES 架构概览 #

初识 Elasticsearch

Elasticsearch 是一个基于 Lucene 的分布式搜索和分析引擎,被设计用于处理大规模数据的全文搜索、结构化搜索和分析。了解其核心架构是掌握 ES 原理的第一步。

核心概念 #

概念 说明
文档 (Document) ES 中的基本数据单元,类似于数据库中的一行记录,以 JSON 格式存储
类型 (Type) ES 7.0 之前的概念,类似数据库表,现已废弃
索引 (Index) 文档的集合,类似于数据库中的数据库概念
分片 (Shard) 索引被分割成的片段,每个分片是一个完整的 Lucene 实例
副本 (Replica) 分片的备份,提供高可用性和搜索性能
节点 (Node) 单个 ES 实例,存储数据并参与集群的索引和搜索功能
集群 (Cluster) 一个或多个节点的集合,共同持有数据并提供联合索引和搜索能力

分布式架构 #

graph TB Client[客户端请求] --> CN[协调节点] CN --> DP{分发请求} DP --> N1[数据节点 1] DP --> N2[数据节点 2] DP --> N3[数据节点 3] N1 --> R[结果收集与合并] N2 --> R N3 --> R R --> Client2[返回客户端] subgraph "Elasticsearch 集群" CN DP N1 N2 N3 R end

ES 的分布式特性主要体现在以下几个方面:

  • 水平扩展:通过添加节点可以线性扩展系统容量
  • 分片机制:将索引分成多个分片,分布在不同节点上
  • 复制机制:每个分片可以有多个副本,提供高可用性
  • 自动均衡:系统会自动均衡分片在节点间的分布

集群组件 #

节点类型

  • 主节点(Master Node):负责轻量级集群管理操作
  • 数据节点(Data Node):存储数据并执行数据相关操作
  • 客户端节点(Client Node):转发集群请求,不存储数据
  • 协调节点(Coordinating Node):分发请求并合并结果
  • 摄取节点(Ingest Node):预处理文档,执行转换

集群状态

  • 绿色:所有主分片和副本分片都正常运行
  • 黄色:所有主分片正常运行,但至少有一个副本分片不正常
  • 红色:至少有一个主分片(及其对应的所有副本)不正常

集群状态反映了数据的完整性和可用性,是监控 ES 健康状况的重要指标。

ES 内部结构 #

理解底层原理

深入了解 ES 的内部结构和底层 Lucene 架构,对于优化查询性能和解决复杂问题至关重要。

索引结构 #

graph TD Index[索引 Index] --> S1[主分片 Primary Shard] Index --> S2[主分片 Primary Shard] S1 --> R1[副本分片 Replica Shard] S1 --> R2[副本分片 Replica Shard] S2 --> R3[副本分片 Replica Shard] S2 --> R4[副本分片 Replica Shard] S1 --> Seg1[段 Segment] S1 --> Seg2[段 Segment] S1 --> Seg3[段 Segment] Seg1 --> SST[Segment Search Tree] Seg1 --> Docs[文档存储] Seg1 --> Field[字段数据] Seg1 --> DV[DocValues] Seg1 --> NI[标准化信息]

Elasticsearch 索引由以下关键部分组成:

分片 (Shards)

索引被分散到多个分片上,每个分片是一个独立的 Lucene 索引,分片分为主分片和副本分片

段 (Segments)

分片由多个不可变的段组成,新文档被索引到新段中,段定期合并以优化性能

提交点 (Commit Point)

提交点是索引状态的快照,记录所有已知段的文件

Lucene 架构 #

Elasticsearch 底层依赖 Lucene 作为核心搜索库。理解 Lucene 的基本架构有助于深入了解 ES 的工作原理:

Lucene 核心组件

  • IndexWriter:负责创建和维护索引
  • IndexReader:提供对索引的只读访问
  • IndexSearcher:利用 IndexReader 执行搜索
  • Query:表示用户的搜索条件
  • Analyzer:处理文本,执行分词、过滤等操作

段的结构 #

段是 Lucene 索引的基本构建块,每个段包含多个文件,存储不同类型的数据:

文件后缀 用途
.nvd, .nvm 标准化因子,用于词项权重计算
.tim, .tip 词条字典,存储词元到倒排表的映射
.doc 存储文档、词频信息
.pos 位置信息,用于短语查询
.pay 有效载荷数据,存储偏移量等
.fdx, .fdt 存储原始文档字段
.dvd, .dvm DocValues,用于聚合和排序

索引的倒排结构

Lucene 使用倒排索引结构,这是信息检索系统的核心:

graph LR A["Term(词元)"] -->|映射到| B["Posting List(倒排表)"] B --> C["DocID, TF, Position..."] subgraph "倒排索引" A B C end

倒排索引中的每个词元对应一个倒排表,包含:

  • 包含该词元的文档 ID 列表
  • 词频(TF)信息
  • 位置信息(用于短语查询)
  • 偏移量信息(用于高亮显示)

索引文档流程详解 #

写入过程的理解

ES 的文档索引过程是一个复杂的分布式流程,涉及多个节点协作和多层内存/磁盘操作,了解这个过程有助于优化写入性能。

写入流程概述 #

sequenceDiagram participant Client as 客户端 participant CN as 协调节点 participant PN as 主分片节点 participant RN as 副本分片节点 participant Lucene as Lucene 索引 Client->>CN: 索引请求 CN->>CN: 验证请求 CN->>PN: 路由到主分片 PN->>PN: 验证&预处理 PN->>Lucene: 写入内存缓冲区 Lucene-->>PN: 确认 PN->>RN: 复制操作 RN->>Lucene: 写入副本 Lucene-->>RN: 确认 RN-->>PN: 确认完成 PN-->>CN: 报告成功 CN-->>Client: 返回响应

协调节点处理 #

索引请求首先到达协调节点,协调节点执行以下操作:

  1. 验证请求:检查请求的有效性,包括索引名称、映射兼容性等
  2. 生成文档 ID:如果请求中没有指定 ID,则生成一个唯一的文档 ID
  3. 确定路由目标:根据路由算法确定文档应该存储在哪个分片
    • 默认使用文档 ID 的哈希值:shard_num = hash(_routing) % num_primary_shards
    • 也可以通过指定 _routing 参数来自定义路由
  4. 转发请求:将请求转发到包含目标分片的节点

主分片索引过程 #

主分片节点接收到索引请求后,执行以下步骤:

  1. 验证操作:检查版本冲突、映射兼容性等
  2. 执行前置操作
    • 应用动态映射更新(如果需要)
    • 执行用户定义的 Ingest Pipeline
  3. 文档解析与分析
    • 解析文档字段
    • 对文本字段执行分析过程(分词、标准化等)
  4. 写入内存缓冲区
    • 文档首先写入 Lucene 的内存缓冲区
    • 同时写入事务日志(Translog)
  5. 标记操作完成:操作在主分片上完成,准备同步到副本

事务日志(Translog)

Translog 是 ES 的预写日志(WAL),提供持久性保证:

  • 每个分片维护自己的 Translog
  • 记录尚未刷盘的所有操作
  • 在节点重启后用于恢复尚未持久化的操作
  • 默认每个请求都会提交(fsync)到磁盘

版本控制机制

ES 使用版本号处理并发写入:

  • 内部版本控制:每次更新自动递增版本号
  • 外部版本控制:用户指定版本号,ES 确保递增
  • 使用 _version 字段存储当前版本
  • 支持乐观并发控制(OCC)模式

副本同步 #

主分片完成索引操作后,需要将变更同步到副本分片:

  1. 并行复制:主分片将操作并行转发给所有副本分片
  2. 副本执行:副本分片执行与主分片相同的索引操作
  3. 确认机制:所有副本分片确认操作完成后,主分片向协调节点报告成功
  4. 响应客户端:协调节点收到主分片的成功响应后,向客户端返回成功

写一致性

ES 支持不同级别的写一致性要求,通过 wait_for_active_shards 参数控制:

  • one(默认):只需主分片确认
  • all:所有主分片和副本分片都必须确认
  • 指定数字:要求特定数量的分片确认

刷新、冲刷与合并 #

刷新(Refresh)

将内存缓冲区的内容转换为段,使其可被搜索,但不刷盘

  • 默认每 1 秒自动刷新
  • 产生新的 Lucene 段
  • 通过 refresh_interval 设置

冲刷(Flush)

完整持久化操作,将段刷盘并清除 translog

  • 定期自动执行(30 分钟或 translog 过大)
  • 触发 Lucene commit
  • 创建提交点(checkpoint)

合并(Merge)

将多个小段合并为较大的段,优化搜索性能

  • 后台自动执行
  • 减少段数量,提高搜索效率
  • 删除已标记删除的文档
flowchart TB A[内存缓冲区] -->|刷新 refresh| B[不可变段] A -->|写入| C[Translog] B -->|合并 merge| D[更大的段] B -->|冲刷 flush| E[磁盘段] C -->|冲刷 flush| F[清空 Translog] D -->|冲刷 flush| E

读取文档流程详解 #

理解 ES 的读取机制

Elasticsearch 提供了两种主要的读取操作:基于 ID 的文档检索(GET)和基于查询的搜索(SEARCH)。两者的流程和性能特征有明显差异。

读取流程概述 #

GET 操作

根据 ID 检索单个文档:

  1. 协调节点根据文档 ID 计算目标分片
  2. 请求被转发到拥有该分片的节点
  3. 节点从本地分片检索文档
  4. 返回文档给协调节点
  5. 协调节点返回文档给客户端

SEARCH 操作

基于查询条件搜索文档:

  1. 协调节点广播查询请求到相关分片
  2. 每个分片执行本地搜索
  3. 分片返回文档 ID 和排序值(query 阶段)
  4. 协调节点合并结果并确定最终集合
  5. 协调节点请求完整文档(fetch 阶段)
  6. 返回结果集给客户端

Get 操作 #

sequenceDiagram participant Client as 客户端 participant CN as 协调节点 participant Shard as 目标分片 Client->>CN: GET 请求 (索引, ID) CN->>CN: 确定分片位置 CN->>Shard: 转发请求 Shard->>Shard: 查找文档 Note over Shard: 1. 检查实时性时间
2. 检查事务日志
3. 检查段缓存
4. 检查Lucene索引 Shard-->>CN: 返回文档 CN-->>Client: 返回响应

Get 操作的详细流程:

  1. 路由确定:协调节点根据文档 ID 和路由值确定文档所在的分片
  2. 分片选择:协调节点从可用的主分片或副本分片中选择一个进行请求,支持负载均衡
  3. 文档查找:目标节点按以下顺序查找文档:
    • 版本映射(Version Map):内存中的最近更新缓存
    • 事务日志(Translog):包含尚未刷新到 Lucene 的最新操作
    • 段缓存(Segment Memory):最近刷新的段
    • Lucene 索引:持久化的段文件
  4. 实时性:默认 Get 操作是实时的,会考虑尚未刷新的更新
  5. 结果返回:找到文档后,返回给协调节点,再返回给客户端

Search 操作 #

Search 操作是 ES 最复杂也是最强大的功能,它分为查询阶段和获取阶段两个主要步骤:

sequenceDiagram participant Client as 客户端 participant CN as 协调节点 participant S1 as 分片 1 participant S2 as 分片 2 Client->>CN: 搜索请求 Note over CN,S2: 查询阶段 (Query Phase) CN->>S1: 发送查询请求 CN->>S2: 发送查询请求 S1->>S1: 构建本地优先队列 S2->>S2: 构建本地优先队列 S1-->>CN: 返回文档IDs和排序值 S2-->>CN: 返回文档IDs和排序值 CN->>CN: 合并结果,构建全局排序 Note over CN,S2: 获取阶段 (Fetch Phase) CN->>S1: 获取完整文档 CN->>S2: 获取完整文档 S1-->>CN: 返回文档内容 S2-->>CN: 返回文档内容 CN->>CN: 组装最终结果 CN-->>Client: 返回搜索结果

查询阶段详解 #

查询阶段 (Query Phase)

  1. 查询分发:协调节点将查询请求发送到每个相关分片(主分片或其副本)
  2. 本地执行:每个分片在本地执行查询,步骤包括:
    • 解析查询条件(Query 和 Filter)
    • 创建查询执行计划
    • 在段级别执行查询
    • 收集匹配文档并计算相关性分数
    • 对结果排序并建立本地优先队列(默认大小为 10)
  3. 结果返回:各分片仅返回文档 ID、排序值和相关性分数,不返回完整文档
  4. 结果合并:协调节点合并所有分片的结果,进行全局排序

获取阶段 (Fetch Phase)

  1. 多 ID 获取:协调节点向相关分片发送多文档获取请求,请求最终结果集中的文档
  2. 文档加载:分片从存储中加载完整的文档内容
  3. 后处理:执行文档转换和过滤,包括:
    • 源过滤(_source filtering)
    • 字段包含/排除处理
    • 高亮处理(如果请求)
    • 脚本字段计算
  4. 最终组装:协调节点将所有文档组装为最终的搜索结果
  5. 响应返回:完整的搜索结果返回给客户端

检索优化 #

ES 提供了多种机制优化检索性能:

查询优化

  • 提前终止(Early Termination):当满足特定条件时提前停止搜索
  • 跳过(Skipping):跳过不可能匹配的段
  • 布尔查询优化:先执行成本低的过滤条件
  • 缓存利用:利用查询缓存、过滤器缓存等提高性能

分页优化

  • 浅分页:对于 from + size < 10000 的请求,使用标准分页
  • 深分页问题:避免 deep paging 导致的性能问题
  • search_after:基于游标的分页,适用于大结果集的遍历
  • scroll API:适用于一次性检索大量文档,创建时间点快照

性能提示

优化搜索性能的关键策略:

  • 尽可能使用 Filter 上下文,可以缓存结果
  • 对于高频搜索模式,使用索引排序优化
  • 适当使用 preference 参数控制路由,提高缓存利用率
  • 使用 _source 过滤减少网络传输
  • 对于复杂聚合,考虑使用预计算或近似聚合

性能要点 #

缓存机制 #

ES 使用多种缓存提高性能:

缓存类型 用途 特点
Node Query Cache 缓存查询结果(仅限于 Filter 上下文) LRU 策略,基于段级别,段变更时自动失效
Shard Request Cache 缓存聚合、建议器等结果 默认启用,分片刷新时失效
Field Data Cache 缓存字段数据,用于聚合和排序 基于堆内存,占用空间较大
Index Buffer 用于索引操作的缓冲区 提高写入性能,默认占 JVM 堆的 10%

相关性评分 #

ES 中的相关性评分是搜索引擎的核心功能之一:

评分计算

ES 7.0 开始默认使用 BM25 算法计算相关性,主要考虑以下因素:

  • 词频(TF):词在文档中出现的频率,出现越多分数越高(非线性增长)
  • 逆文档频率(IDF):衡量词的稀有程度,越稀有权重越大
  • 字段长度归一化:较短字段中的匹配通常比长字段中的匹配更相关
  • 协调因子:匹配查询中更多词条的文档分数更高

相关性评分可能出现的问题及解决方案:

分片评分问题

在多分片环境下,由于每个分片只知道自己的文档统计信息,可能导致评分不统一。

解决方案:

  • 使用 search_type=dfs_query_then_fetch 进行全局评分
  • 确保索引有合理数量的分片
  • 在单个分片上测试评分逻辑

评分调优

自定义和调整相关性评分的常用方法:

  • 使用 boost 参数提升特定字段或词的权重
  • 使用函数评分查询(function_score)添加自定义因子
  • 使用字段加权(field boosting)调整多字段查询权重
  • 通过 script_score 实现完全自定义的评分逻辑

总结

通过深入理解 Elasticsearch 的内部原理,我们能够:

  • 构建更高效的索引和查询策略
  • 更好地诊断和解决性能问题
  • 合理规划资源分配和集群扩展
  • 在架构设计时做出更明智的决策

ES 的设计结合了搜索引擎的复杂性和分布式系统的挑战,理解其工作原理是充分利用这一强大工具的关键。