Skip to main content
  1. posts/

ElasticSearch核心原理深度剖析

·7709 words·16 mins

1. ElasticSearch是什么?

ElasticSearch(简称 ES)是一个基于 Lucene 的分布式搜索和分析引擎。由 Elastic 公司开发维护,于 2010 年首次发布,目前已成为最流行的企业级搜索引擎。

1.1 核心特点

  • 倒排索引:基于 Lucene 的倒排索引,实现高效的全文搜索
  • 分布式架构:天然支持数据分片和副本,可水平扩展
  • 近实时搜索:写入数据约 1 秒后即可被搜索到
  • RESTful API:通过 HTTP + JSON 进行交互,使用门槛低
  • Schema Free:支持动态映射,灵活的文档结构
  • 丰富的查询 DSL:支持全文搜索、结构化搜索、聚合分析

1.2 核心组件

正在加载图表...

组件说明

组件说明类比
Cluster集群,多个节点的集合数据库集群
Node节点,一个 ES 实例数据库实例
Index索引,文档的集合数据库表
Shard分片,索引的子集分区/分表
Segment段,Lucene 的存储单元数据文件
Document文档,一条数据记录表中的一行

1.3 适用场景

✅ 适用场景:

  • 全文搜索(站内搜索、商品搜索)
  • 日志分析(ELK Stack)
  • 实时数据分析和可视化
  • 应用性能监控(APM)
  • 安全分析(SIEM)
  • 地理位置搜索

❌ 不适用场景:

  • 事务性操作(OLTP)
  • 频繁的更新操作
  • 强一致性要求的场景
  • 复杂的关联查询(Join)
  • 作为主数据存储(建议配合 MySQL 等使用)

1.4 与其他系统对比

特性ElasticSearchMySQLClickHouseMongoDB
存储方式倒排索引+行列混合行式列式文档(BSON)
搜索能力极强(全文搜索)中等中等
分析能力强(聚合)中等极强中等
写入性能中等极高
更新性能
事务支持完整部分
分布式原生支持需中间件原生支持原生支持

2. ElasticSearch解决什么问题?

2.1 传统数据库的搜索局限

在传统关系型数据库中,全文搜索存在以下问题:

  1. LIKE 查询效率低

    • WHERE content LIKE '%关键词%' 无法使用索引
    • 全表扫描,性能随数据量线性下降
    • 百万级数据查询可能需要数秒
  2. 不支持分词

    • 中文 “我爱北京天安门” 无法按词搜索
    • 无法处理同义词、近义词
    • 无法进行相关性排序
  3. 扩展性差

    • 单机性能瓶颈
    • 分库分表后全文搜索更加困难
    • 跨库搜索需要在应用层聚合

2.2 搜索场景的特点

  • 读多写少:搜索请求远多于写入请求
  • 模糊匹配:用户输入不精确,需要容错
  • 相关性排序:返回最匹配的结果,而非全部结果
  • 高并发:需要支持大量并发搜索请求
  • 低延迟:用户期望毫秒级响应

2.3 核心需求

  1. 高效的全文搜索:毫秒级返回搜索结果
  2. 灵活的分词能力:支持中英文分词、同义词等
  3. 相关性评分:返回最相关的文档
  4. 水平扩展:支持 PB 级数据存储和搜索
  5. 高可用性:节点故障不影响服务

3. 怎么解决的?

ElasticSearch 通过以下核心机制解决上述问题:

3.1 倒排索引

核心思想:建立词条(Term)到文档(Document)的映射,而非文档到词条的映射。

正在加载图表...

倒排索引的组成

  • Term Dictionary:词条字典,存储所有词条,支持快速查找
  • Posting List:倒排列表,存储包含该词条的文档 ID 列表
  • Term Frequency:词频,词条在文档中出现的次数
  • Position:位置信息,用于短语查询

3.2 分布式架构

  • 数据分片:将索引拆分为多个 Shard,分布到不同节点
  • 副本机制:每个 Shard 有多个 Replica,保证高可用
  • 自动路由:根据文档 ID 自动路由到对应分片
  • 并行查询:查询时并行搜索所有分片,合并结果

3.3 分段存储(Segment)

  • 数据写入时先进入内存缓冲区(Buffer)
  • 定期刷新为不可变的 Segment 文件
  • Segment 不可修改,删除和更新通过标记实现
  • 后台合并小 Segment 为大 Segment

3.4 近实时搜索

  • Refresh:每秒将内存缓冲区数据刷新到文件系统缓存,生成新 Segment
  • Flush:将文件系统缓存的数据持久化到磁盘
  • Translog:事务日志,保证数据不丢失

4. 核心流程

4.0 ES 整体工作流程

正在加载图表...

流程说明

  1. 写入流程

    • 协调节点接收请求,计算路由确定目标分片
    • 主分片写入数据,同步到副本分片
    • 数据先写入内存 Buffer,定期 Refresh 生成 Segment
  2. 查询流程(Query Then Fetch)

    • Query 阶段:并行查询所有分片,返回匹配文档的 ID 和评分
    • Fetch 阶段:根据排序后的 Top N 文档 ID,获取完整文档内容

4.1 底层存储结构

Lucene 索引结构

正在加载图表...

倒排索引详细结构

倒排索引组成:
├── Term Index (.tip)      # FST 结构,前缀树,指向 Term Dictionary
├── Term Dictionary (.tim) # 词条字典,存储所有 Term
└── Posting List (.doc)    # 倒排列表
    ├── DocId List         # 文档 ID 列表(差值压缩)
    ├── Term Frequency     # 词频(用于评分)
    └── Position (.pos)    # 位置信息(用于短语查询)

实际案例:用户表的倒排索引

假设有一个用户索引 users,包含以下 3 条文档:

_idnamecitybio
1张三北京资深Java开发工程师
2李四上海Java架构师,擅长分布式系统
3王五北京Go语言开发,熟悉Java

ES 会为每个字段建立独立的倒排索引:

正在加载图表...

查询示例

// 查询:city = "北京" AND bio 包含 "Java"
{
  "query": {
    "bool": {
      "must": [
        { "term": { "city": "北京" } },
        { "match": { "bio": "Java" } }
      ]
    }
  }
}

查询执行过程

  1. city 字段查找北京 → [Doc1, Doc3]
  2. bio 字段查找java → [Doc1, Doc2, Doc3]
  3. 求交集[Doc1, Doc3] ∩ [Doc1, Doc2, Doc3] = [Doc1, Doc3]
  4. 返回结果:张三、王五

倒排索引文件存储结构

词条到文档的映射关系通过 三个文件协同工作 完成:

正在加载图表...

查询 “java” 的完整过程

1. 查 .tip 文件:前缀 "j" → 定位到 .tim 文件 offset:1024
2. 查 .tim 文件:offset:1024 找到词条 "java" → postingPtr:100  
3. 查 .doc 文件:postingPtr:100 → DocId 列表 [1, 2, 3] ⭐ 这就是映射!
4. 返回结果:包含 "java" 的文档是 Doc1, Doc2, Doc3

各文件职责总结

文件后缀存储内容作用
Term Index.tip词条前缀的 FST 索引快速定位词条在 .tim 中的位置
Term Dictionary.tim所有词条 + Posting 指针存储词条,指向 .doc
Posting List.docTerm → DocId 列表核心:存储映射关系
Positions.pos词条在文档中的位置支持短语查询
Payloads.pay词频等额外信息用于相关性评分

💡 简单记忆.tip 找词条位置 → .tim 找词条 → .doc 找文档列表

Doc Values(列式存储)

用于 keyword 类型字段的排序和聚合,按列存储:

city 字段 Doc Values:
Doc1 → 北京
Doc2 → 上海  
Doc3 → 北京

# 聚合查询 "按城市统计用户数" 时直接遍历此列
# 结果:北京=2, 上海=1

4.2 数据写入流程

正在加载图表...

写入流程详解

  1. 写入内存 Buffer

    • 文档经过分词器(Analyzer)处理
    • 构建内存中的倒排索引
    • 此时数据不可搜索
  2. 追加 Translog

    • 记录写入操作日志
    • 默认每个请求都 fsync 到磁盘
    • 保证宕机后数据可恢复
  3. Refresh(默认 1 秒)

    • 将内存 Buffer 写入文件系统缓存
    • 生成新的 Segment
    • 数据变为可搜索状态
    • 这是"近实时"的来源
  4. Flush(默认 30 分钟)

    • 将文件系统缓存的 Segment 持久化到磁盘
    • 清空 Translog
    • 触发条件:时间间隔或 Translog 大小超过阈值

4.3 数据查询流程

正在加载图表...

查询流程详解

  1. Query 阶段

    • 协调节点将查询广播到所有相关分片
    • 每个分片在本地执行查询,返回 from + size 个文档的 ID 和评分
    • 协调节点合并所有结果,全局排序,确定最终的 Top N
  2. Fetch 阶段

    • 协调节点根据 Query 阶段的结果,向相关分片请求文档内容
    • 只请求最终需要返回的文档(Top N)
    • 分片返回完整文档,协调节点组装后返回给客户端
  3. 相关性评分

    • 默认使用 BM25 算法(5.x 之前使用 TF-IDF)
    • 考虑词频(TF)、逆文档频率(IDF)、文档长度等因素

4.4 Segment Merge 机制

正在加载图表...

Merge 机制详解

  1. 为什么需要 Merge

    • 每次 Refresh 都会生成新 Segment
    • Segment 数量过多影响查询性能(需要搜索所有 Segment)
    • 删除操作只是标记,不会真正释放空间
  2. Merge 策略

    • Tiered Merge Policy(默认):按大小分层合并
    • 小 Segment 优先合并
    • 控制单次 Merge 的 Segment 数量和大小
  3. Merge 过程

    • 选择多个小 Segment
    • 合并倒排索引和存储的文档
    • 物理删除已标记删除的文档
    • 生成新的大 Segment
    • 原子性替换旧 Segment
  4. Force Merge

    • 手动触发:POST /index/_forcemerge?max_num_segments=1
    • 强制合并为指定数量的 Segment
    • 注意:会消耗大量 I/O,建议在低峰期执行

5. ElasticSearch 存在的问题与挑战

5.1 近实时搜索的延迟

问题描述

  • 数据写入后默认需要 1 秒才能被搜索到
  • 某些场景下 1 秒的延迟无法接受

影响场景

  • 实时监控告警
  • 即时消息搜索
  • 交易完成后立即查询

可视化展示

正在加载图表...

缓解措施

  • 写入后手动 Refresh:POST /index/_refresh(影响性能,慎用)
  • 使用 ?refresh=wait_for 参数等待 Refresh 完成
  • 调整 index.refresh_interval 参数(权衡实时性和性能)
  • 对于需要立即可见的场景,使用 GET /index/_doc/{id} 直接读取

5.2 深度分页问题

问题描述

  • from + size 分页方式在深度分页时性能急剧下降
  • 例如 from=10000, size=10,每个分片需要返回 10010 条数据

影响场景

  • 需要跳页访问的场景
  • 导出大量数据
  • 翻页到很后面的页码

问题示意

正在加载图表...

缓解措施

  • Scroll API:适合大量数据导出,维护快照
  • Search After:基于上一页最后一条的排序值,只支持向后翻页
  • 避免深度分页:业务层限制最大页码
  • 使用 index.max_result_window 调整限制(治标不治本)

5.3 更新和删除代价高

问题描述

  • Segment 不可变,更新 = 删除旧文档 + 写入新文档
  • 删除只是标记(.liv 文件),不会立即释放空间
  • 需要等待 Merge 才能真正删除

影响场景

  • 频繁更新的数据
  • 需要立即删除的合规场景
  • 空间敏感的场景

缓解措施

  • 减少更新频率,批量更新
  • 定期执行 Force Merge
  • 设计时避免频繁更新的字段
  • 使用版本号或时间戳做逻辑删除

5.4 集群脑裂风险

问题描述

  • 网络分区可能导致集群分裂为多个独立集群
  • 每个子集群选举出自己的 Master
  • 数据写入不同子集群,导致数据不一致

影响场景

  • 跨机房部署
  • 网络不稳定环境
  • Master 节点不足

缓解措施

  • 7.x+ 版本已大幅改进选举机制,风险降低
  • 设置合理的 Master 节点数量(至少 3 个)
  • 使用 discovery.zen.minimum_master_nodes(旧版本)
  • 网络规划避免分区

5.5 内存压力

问题描述

  • 倒排索引的 Term Dictionary 需要加载到内存
  • 聚合查询需要大量内存
  • Field Data 可能导致 OOM

影响场景

  • 高基数字段聚合(如用户 ID)
  • 大量 text 字段
  • 复杂的嵌套聚合

缓解措施

  • 使用 Doc Values 代替 Field Data
  • 控制聚合的基数
  • 合理设置 JVM 堆内存(不超过 32GB)
  • 使用 Circuit Breaker 防止 OOM

6. 总结

6.1 ElasticSearch 的优势

  1. 高效的全文搜索:基于倒排索引,毫秒级搜索响应
  2. 灵活的分词:支持多种分词器,可定制化
  3. 分布式架构:原生支持分片和副本,易于扩展
  4. 近实时搜索:写入约 1 秒后可搜索
  5. 丰富的查询能力:全文搜索 + 结构化查询 + 聚合分析

6.2 需要注意的问题

  1. 近实时延迟:写入后不能立即搜索到
  2. 深度分页:避免 from + size 深度分页
  3. 更新代价高:减少不必要的更新操作
  4. 不适合 OLTP:不要用于事务场景
  5. 内存管理:注意 JVM 和系统内存分配

6.3 最佳实践建议

  • 索引设计

    • 合理设置分片数(建议每个分片 10-50GB)
    • 避免过多字段(控制在 1000 以内)
    • 使用合适的字段类型(keyword vs text)
    • 禁用不需要搜索的字段的索引
  • 写入优化

    • 批量写入(Bulk API,每批 5-15MB)
    • 写入时可临时增大 refresh_interval
    • 使用自动生成的 ID 或避免版本冲突
  • 查询优化

    • 使用 Filter 代替 Query(可缓存)
    • 避免 * 通配符开头的查询
    • 使用 Search After 替代深度分页
    • 合理使用 _source 过滤返回字段
  • 集群规划

    • 分离 Master、Data、Coordinating 节点角色
    • JVM 堆内存不超过 32GB,预留一半给文件系统缓存
    • 使用 SSD 存储
    • 监控关键指标:索引延迟、搜索延迟、GC 时间
  • 运维管理

    • 定期监控分片大小和数量
    • 低峰期执行 Force Merge
    • 使用 ILM(Index Lifecycle Management)管理索引生命周期
    • 定期备份(Snapshot)

ElasticSearch 在全文搜索和日志分析场景下具有显著优势,但需要理解其工作原理和限制,才能充分发挥其潜力。通过合理的索引设计、查询优化和集群规划,可以构建高效、稳定的搜索服务。