Elasticsearch 深度解析:从原理到实践
一、为什么选择 Elasticsearch?
数据模型
Elasticsearch 是基于文档的搜索引擎,它使用 JSON 文档来存储数据。 在 Elasticsearch 中,相关的数据通常存储在同一个文档中,而不是分散在多个表中。
**MySQL 是一个关系型数据库管理系统,它使用表、行和列的结构来组织数据。**数据通过外键关系分散在多个表中。
查询语言
Elasticsearch 使用 Query DSL(Domain Specific Language) ,这是一种非常灵活的查询语言,基于 JSON,支持全文搜索、复合查询、过滤以及聚合等。
MySQL 使用 SQL(Structured Query Language),这是一种强类型和非常成熟的语言,专门用于查询和管理关系数据库。
全文搜索
Elasticsearch 的核心功能是全文搜索。 它对数据进行索引时会自动建立全文搜索索引,使其在搜索大量文本数据时表现优异。
MySQL 虽然也提供了基本的全文搜索功能,但其主要设计目标是处理结构化数据的存储和查询,对全文搜索的支持不如 Elasticsearch 那样强大。
事务支持
Elasticsearch 不支持传统的 ACID(原子性、一致性、隔离性、持久性)事务。虽然它确保了单个文档操作的原子性,但不适用于跨多个文档的复杂事务。 虽然 Elasticsearch 不支持传统的事务,但是他是可以确保单个文档的更改(如创建、更新、删除)是原子性的 。这意味着任何文档级的操作都完整地成功或完整地失败,但不保证跨多个文档或操作的一致性。
MySQL 支持 ACID 事务,这使得它非常适合需要严格数据一致性的应用,如金融服务和其他商业数据处理。
支持乐观锁:
Elasticsearch 支持通过使用文档版本控制来实现乐观锁。
6.7之前。 在 ES 中,每个文档存储时都有一个 _version 字段,这个版本号在每次文档更新时自动增加。当我们执行更新、删除或者使用脚本处理文档时,可以指定这个版本号来确保正在操作的文档是预期中的版本。如果操作中的版本号与存储在索引中的文档版本号不一致,说明文档已被其他操作更改,当前操作将会失败。(CAS)
为什么_version在后续版本被抛弃
_version 机制是基于单一递增整数,它主要适用于简单的冲突检测,但在复杂的分布式系统中,仅依靠版本号可能无法准确地反映数据的历史和复制状态。尤其是在发生网络分区或节点故障时,仅凭 _version 可能导致数据丢失或过时的数据被错误地写入。
主要还是 在高并发和高可用 的环境下,单一的-version不足以处理因节点故障 或网络问题导致的多个副本之间的数据不一致问题。
然而if_seq_no 和 if_primary_term这两个参数可以更准确地确定操作是否应当被执行,可以跟踪每个文档变更的序列号 和主分片的期限。 这种机制帮助确保即使在集群状态发生变化(如分片重新分配到新的节点)的情况下,也不会应用基于过时副本的更新。它有效地防止了脑裂问题。
6.7之后,使用 _version 关键字进行乐观锁已经被废弃了,替代方法是使用
if_seq_no ( 是一个递增的序列号,表示文档的每次修改**)和**
if_primary_term( 表示主分片的当前任期,每当主分片发生变化时,这个值会增加。)
来指定版本。
1. 核心优势()
维度 | Elasticsearch | MySQL |
---|---|---|
数据模型 | 文档型(JSON),动态映射 | 关系型(严格Schema) |
搜索能力 | 全文检索、模糊匹配、语义分析 | 仅支持精确查询和简单LIKE |
扩展性 | 分布式架构,自动分片/副本 | 分库分表复杂 |
吞吐量 | 单集群支持每秒10万+查询 | 高并发写入更优 |
一致性 | 最终一致性(近实时) | 强一致性(ACID) |
Elasticsearch是一个开源的分布式搜索和分析引擎,主要适用于以下场景:
1:搜索引擎:用于快速检索文档、商品、新闻等。
2:日志分析:通过分析日志数据,帮助企业了解其业务的性能情况。
3:数据分析:帮助数据科学家和数据分析师进行数据分析,以获取有价值的信息。
4:商业智能:帮助企业制定数据驱动的决策,以实现商业上的成功。
5:实时监控:帮助企业实时监测系统性能、监控数据变化,以保证系统正常运行。
6:安全性:帮助企业保证数据的安全性,保证数据不被非法窃取。
7:应用程序开发:帮助开发人员开发基于搜索的应用程序,以增加用户体验。
Elasticsearch具有以下几个优势:
1:高性能 :Elasticsearch具有高性能的搜索和分析能力,其中涵盖了多种查询语言和数据结构。
2:可扩展性 :Elasticsearch是分布式的,可以通过增加节点数量扩展搜索和分析能力。
3:灵活性 :Elasticsearch支持多种数据类型,支持多种语言,支持动态映射,允许快速地调整模型以适应不同的需求。
4:实时分析 :Elasticsearch支持实时分析,可以对数据进行实时查询,这对于快速检索数据非常有用。
5:可靠性:Elasticsearch具有可靠性和高可用性,支持数据备份和恢复。
ES为什么块:
1:分布式存储 :Elasticsearch使用分布式存储技术,将数据存储在多个节点上,从而减少单个节点的压力,提高整体性能。
2:索引分片 :Elasticsearch把每个索引划分成多个分片,这样可以让查询操作并行化,从而提高查询速度。
3:全文索引 :Elasticsearch使用了高效的全文索引技术,把文档转化成可搜索的结构化数据,使得搜索操作快速高效。
4:倒排索引 :Elasticsearch支持倒排索引这种数据结构,倒排索引将文档中的每个词与该词出现在哪些文档中进行映射,并存储这些信息。当搜索请求发生时,ES可以快速查找包含所有搜索词的文档,从而返回结果。
5:索引优化 :Elasticsearch通过索引优化技术,可以使查询速度更快。例如,它支持索引覆盖、索引下推等优化技术,使得查询速度更快。
6:预存储结果 :Elasticsearch在插入数据时,对数据进行预处理,把结果预存储到索引中,从而在查询时不需要再重新计算,提高查询速度。
7:高效的查询引擎 :Elasticsearch使用了高效的查询引擎,支持各种类型的查询,并对复杂查询提供了优化策略,从而提高查询速度。
8:异步请求处理 :ES使用了异步请求处理机制,能够在请求到达时立即返回,避免长时间的等待,提高用户体验。
9:内存存储:ES使用了内存存储技术,能够在读写数据时大大减少磁盘访问次数,提高数据存储和查询效率。
我觉得最重要就是倒排索引了:它 用于快速搜索文档中的某个词汇。
假设我们有一个原始文档:
文档ID | 文档内容 |
---|---|
Doc1 | "Elasticsearch is fast" |
Doc2 | "Redis is fast too" |
Doc3 | "Elasticsearch and Redis" |
主要分为俩个过程:
-
分词(Tokenization):
-
Doc1 → ["elasticsearch", "is", "fast"]
-
Doc2 → ["redis", "is", "fast", "too"]
-
Doc3 → ["elasticsearch", "and", "redis"]
-
- 生成词项-文档映射:
Term DocIDs (Postings List) ───────────────────────────────────── "and" → [Doc3] "elasticsearch" → [Doc1, Doc3] "fast" → [Doc1, Doc2] "is" → [Doc1, Doc2] "redis" → [Doc2, Doc3] "too" → [Doc2]
3. 倒排索引的核心组件
组件 | 作用 | 示例 |
---|---|---|
词项字典(Term Dictionary) | 存储所有唯一词项,通常用 FST(有限状态转换器) 压缩存储 | "elasticsearch", "redis" |
倒排列表(Postings List) | 记录包含该词项的文档ID及位置信息(支持快速跳表SkipList优化遍历) | Doc1:[pos1,pos2], Doc3:[pos1] |
词频(TF) | 词项在文档中的出现次数(用于相关性评分) | "fast"在Doc1中TF=1 |
文档频率(DF) | 包含该词项的文档数(用于IDF计算) | "elasti |
倒排索引的优化技术
-
压缩存储:
-
FST:压缩词项字典,减少内存占用。
-
Delta Encoding:对文档ID差值编码(如[100, 102, 105]→[100, +2, +3])。
-
-
跳表(SkipList):
- 加速倒排列表的合并操作(如
AND
查询)。
- 加速倒排列表的合并操作(如
-
多级索引:
- 内存中保留热点词项,磁盘存全量数据。
二、Elasticsearch 核心技术细节
1. 分词器(Analyzer)
分词器类型 | 功能说明 | 示例 |
---|---|---|
Standard | 默认分词器,按空格/标点切分 | "Elasticsearch" → ["elasticsearch"] |
IK Analyzer | 中文分词(社区版+扩展词典) IK分词器有几种模式? * ik_smart:智能切分,粗粒度 * ik_max_word:最细切分,细粒度 IK分词器如何拓展词条?如何停用词条? * 利用config目录的IkAnalyzer.cfg.xml文件添加拓展词典和停用词典 * 在词典中添加拓展词条或者停用词条 | "华为手机" → ["华为", "手机"] |
Pinyin | 中文转拼音搜索 | "北京" → ["beijing", "bj"] |
Whitespace | 仅按空格切分 | "hello world" → ["hello", "world"] |
Keyword | 不分词,整体作为Term | "2023-08-15" → ["2023-08-15"] |
自定义分词器:
PUT /my_index { "settings": { "analysis": { "analyzer": { "my_ik": { "type": "custom", "tokenizer": "ik_max_word", "filter": ["lowercase"] } } } } }
2. 底层原理
-
倒排索引(Inverted Index):
-
Term → Document ID 的映射(如"手机" → [Doc1, Doc3])。
-
通过 FST(Finite State Transducer) 压缩存储,减少内存占用。
-
-
分片(Shard)机制:
-
索引自动拆分为多个分片(默认5个),分散到不同节点。
-
每个分片有1个主副本和N个从副本(通过
_settings
调整)。
-
-
近实时(NRT):
- 数据写入后默认1秒(
refresh_interval
)可被搜索,通过 translog 保证持久化。
- 数据写入后默认1秒(
3. 查询高效的原因
-
分布式计算:
- 查询并行发送到所有分片,结果聚合(Scatter-Gather模式)。
-
缓存优化:
-
Query Cache:缓存过滤条件结果。
-
Fielddata:文本字段启用内存缓存(慎用,易OOM)。
-
-
列式存储(Doc Values):
- 对排序、聚合字段预先构建磁盘数据结构,避免实时计算。
4. 增删改操作
-
写入流程:
-
请求发送到协调节点。
-
路由到对应分片的主副本。
-
写入Lucene内存Buffer和translog。
-
定期刷新(Refresh)生成新Segment(可搜索)。
-
后台合并(Merge)Segment优化存储。
-
-
删除:
- 标记文档为
deleted
,Merge时物理删除。
- 标记文档为
-
更新:
- 先删除旧文档,再写入新文档(版本号
_version
递增)。
- 先删除旧文档,再写入新文档(版本号
三、关键问题解决方案
1. 深度分页优化
-
问题 :
from 10000, size 10
需遍历所有分片的10010条记录。在Elasticsearch中进行分页查询通常使用from和size参数。当我们对Elasticsearch发起一个带有分页参数的查询(如使用from和size参数)时,ES需要遍历所有匹配的文档直到达到指定的起始点(from),然后返回从这一点开始的size个文档。跟MySQL的深度分页原因差不多, -
方案:
-
Search After :search_after 是 Elasticsearch 中用于实现深度分页的一种机制。与传统的分页方法(使用 from 和 size 参数)不同,search_after 允许你基于上一次查询的结果来获取下一批数据,这在处理大量数据时特别有效。
在第一次查询时,你需要定义一个排序规则。不需要指定 search_after 参数:
{ "query": { "match_all": {} }, "size": 10, "sort": [{"_id": "asc"}],//在第一次查询时,你需要定义一个排序规则。 "search_after": [last_id] //第一次查询不要定义,在后续的查询中,使用上一次查询结果中最后一条记录的排序值 //search_after包含timestamp和last_id 的值,对应上一次查询结果的最后一条记录。 }
search_after 可以有效解决深度分页问题,原因如下:
避免重复处理数据: 与传统的分页方式不同,search_after 不需要处理每个分页请求中所有先前页面上的数据。这大大减少了处理数据的工作量。提高查询效率: 由于不需要重复计算和跳过大量先前页面上的数据,search_after 方法能显著提高查询效率,尤其是在访问数据集靠后部分的数据时。
但是这个方案有一些局限,一方面需要有一个全局唯一的字段用来排序,另外虽然一次分页查询时不需要处理先前页面中的数据,但实际需要依赖上一个页面中的查询结果。
-
适用于深度分页和大数据集的遍历。
-
-
-
Scroll API:Scroll API在Elasticsearch中的主要目的是为了能够遍历大量的数据,它通常用于数据导出或者进行大规模的数据分析。可以用于处理大量数据的深度分页问题。
-
POST /_search/scroll { "scroll": "1m", "scroll_id": "DXF1ZXJ5QW..." }
//如上方式初始化一个带有scroll参数的搜索请求。这个请求返回一个scroll ID,用于后续的滚动。Scroll参数指定了scroll的有效期,例如1m表示一分钟。
//接下来就可以使用返回的scroll ID来获取下一批数据。每次请求也会更新scroll ID的有效期。
-
业务妥协:限制最大页码(如只展示前100页)。
-
**避免重复排序:**在传统的分页方式中,每次分页请求都需要对所有匹配的数据进行排序,以确定分页的起点。Scroll避免了这种重复排序,因为它保持了一个游标。
-
稳定视图: **Scroll提供了对数据的"稳定视图"。**当你开始一个scroll时,Elasticsearch会保持搜索时刻的数据快照,这意味着即使数据随后被修改,返回的结果仍然是一致的。
-
减少资源消耗:
由于不需要重复排序,Scroll**减少了对CPU和内存的消耗,**特别是对于大数据集。
-
Scroll非常适合于处理需要访问大量数据但不需要快速响应的场景,如数据导出、备份或大规模数据分析。
但是,需要知道,使用Scroll API进行分页并不高效,因为你需要先获取所有前面页的数据。Scroll API主要用于遍历整个索引或大量数据,而不是用于快速访问特定页数的数据。
-
2. 数据一致性
第一种方法:双写一致性
对MySQL和ES进行双写,先更新数据库,再更新ES,放在一个事务里面,需要保证事务的一致性。有数据不一致的风险,比如MySQL写入成功,但是ES发生了写入成功但因为超时抛异常了就会出现数据不一致的情况。
第二种方法:使用消息队列MQ进行处理
在更新数据库时,发送一个消息到MQ中,然后数据库和ES各设置一个监听者,监听消息之后各自去做数据变更,如果失败了就基于消息的重试在重新执行。
好处是进行了异步解耦,性能稍后,但是有延迟
第三种方法:定时扫描MySQL
如果ES中的数据变更的实时性要求不高,可以考虑定时任务扫表来批量更新ES。
这个方案优点是没有侵入性,数据库的写操作处不需要改代码。
缺点是实时性很差,并且轮询可能存在性能问题、效率问题以及给数据库带来压力。
第四种方法:监听binlog日志(基于canal做数据同步的方案)
对业务代码完全没有侵入性,业务也非常解耦,不需要关心这个ES的更新操作。
缺点就是需要基于binlog监听,需要引入第三方框架。存在一定的延迟。
一致性级别 | 实现方案 | 优缺点 |
---|---|---|
强一致性 | 写入后立即refresh (性能差) |
数据最新,吞吐量下降 |
最终一致性 | 默认1秒刷新 + 手动_refresh (平衡选择) |
性能高,短暂延迟 |
与Canal集成:主要的过程
-
MySQL Binlog → Canal Server 解析变更。
-
Canal Client → Kafka 发送变更事件。
-
Logstash/自定义Consumer → 写入ES。
关于Logstash和自定义Consumer
. Logstash 是什么?
-
定位:开源的数据处理管道工具,属于 Elastic Stack(ELK)的一部分。
-
核心功能:
-
数据输入(Input):从 Kafka、MySQL、文件等读取数据。
-
数据过滤(Filter):清洗、转换、丰富数据(如字段重命名、类型转换)。
-
数据输出(Output):将处理后的数据写入 ES、MySQL、文件等。
-
-
特点:
-
配置驱动:通过配置文件定义数据处理流程,无需编码。
-
插件化 :支持 200+ 官方/社区插件(如
kafka
、elasticsearch
、grok
)。
-
Canal → ES 场景中的用途
将 Canal 发送到 Kafka 的 MySQL Binlog 数据转换为 ES 所需的格式,并写入 ES。
2. 自定义 Consumer 是什么?
-
定位:用户自行编写的程序(通常用 Java/Python/Go),用于消费 Kafka 消息并处理。
-
核心功能:
-
从 Kafka 拉取 Canal 生成的 Binlog 消息。
-
解析消息并转换为 ES 支持的格式(如 JSON)。
-
调用 ES 的 API 写入数据。
-
-
特点:
-
灵活可控:可自定义业务逻辑(如特殊字段处理、错误重试)。
-
高性能:通过批量写入、异步请求等优化吞吐量。
-
整体架构流程:

3. ES支持的数据结构
-
核心类型:
-
Text:用于存储全文文本数据。
-
Keyword:用于存储文本值,通常用于索引结构化内容,如邮件地址、标签或任何需要精确匹配的内容。
-
Nested:类似于 Object 类型,但用于存储数组列表,其中列表中的每个元素都是完全独立且可搜索的。
-
Long, Integer, Short, Byte, Double, Float: 这些是数值类型,用于存储各种形式的数字。
-
Date: 存储日期或日期和时间。
**Boolean:**存储 true 或 false 值。
-
Binary: 用于存储二进制数据。
-
**Object:**用于嵌套文档,即文档内部可以包含文档。
-
-
特殊类型:
-
Flattened:避免深层JSON字段爆炸。
-
Join:父子文档(性能较差,慎用)。
-
text 和 keyword 有啥区别?
text 类型被设计用于全文搜索。这意味着当文本被存储为text 类型 时,Elasticsearch 会对其进行分词, 把文本分解成单独的词或短语,便于搜索引擎进行全文搜索。因为 text 字段经过分词,它不适合用于排序或聚合查询。
text适用于存储需要进行全文搜索的内容,比如新闻文章、产品描述等。
keyword 类型用于精确值匹配 ,不进行分词处理。这意味着存储在 keyword 字段的文本会被当作一个完整不可分割的单元进行处理。因为 keyword 类型字段是作为整体存储,它们非常适合用于聚合(如计数、求和、过滤唯一值等)和排序操作。由于不进行分词,keyword 类型字段不支持全文搜索,但可以进行精确匹配查询。
keyword适用于需要进行精确搜索的场景,比如标签、ID 编号、邮箱地址等。
*
- ES 不支持 decimal,如何避免丢失精度?
五种方法:
一:使用字符串类型(比较好用)
完全保留数字的精度。简单易于实现,数据迁移时不需特别处理。但是作为字符串存储的数字不能直接用于数值比较或数学运算,需要在应用层处理转换。但也不是什么大毛病。
二:扩大浮点类型的精度
就是直接使用 double 类型,在理论上可能会有精度损失,但实际上 double 类型提供的精度对于许多业务需求已经足够使用。需要接受减小精度损失。
三:使用scaled_float(推荐)
Elasticsearch 的 scaled_float 类型是一种数值数据类型,专门用于存储浮点数。其特点是通过一个缩放因子(scaling factor)将浮点数转换为整数来存储,从而在一定范围内提高存储和计算的效率。他使用一个缩放因子将浮点数转换为整数存储。例如,如果缩放因子是 100,那么值 123.45 会存储为 12345。这样可以避免浮点数存储和计算中的精度问题。
四:使用多个字段
在某些情况下,可以将 decimal 数值拆分为两个字段存储:一个为整数部分,另一个为小数部分。这样做可以在不丢失精度的情况下,将数值分开处理。可以保持数值精确,同时可进行部分数学运算。但是增加了数据处理的复杂性,需要在应用层重建数值。
五:使用自定义脚本
用 Elasticsearch 的脚本功能(如 Painless 脚本)来处理数值计算,确保在处理过程中控制精度。优点:灵活控制数据处理逻辑。缺点:可能影响查询性能,增加系统复杂性。
四、实际应用场景
1. 电商搜索
-
需求:支持颜色、品牌、价格区间等多维度过滤。
-
实现:
{ "query": { "bool": { "must": [ { "term": { "brand": "华为" } }, { "range": { "price": { "gte": 1000, "lte": 2000 } } } ], "filter": { "term": { "color": "红色" } } } }, "aggs": { "price_stats": { "stats": { "field": "price" } } } }
2. 日志告警
-
需求:实时检测ERROR日志并触发告警。
-
实现:
-
Watcher 插件定时查询:
{ "query": { "match": { "level": "ERROR" } }, "condition": { "compare": { "ctx.hits.total": { "gt": 0 } } }, "actions": { "email": { "to": "[email protected]" } } }
-
3. 数据同步容灾
-
双写方案:
- 应用同时写MySQL和ES,通过本地事务表记录同步状态。
-
补偿机制:
- 定时任务扫描MySQL与ES差异数据,修复不一致。
五、性能调优
集群和硬件优化
负载均衡: 确保查询负载在集群中均衡分配。
硬件资源: 根据需要增加 CPU、内存或改善 I/O 性能(例如使用 SSD)。
配置 JVM: 优化 JVM 设置,如堆大小,以提高性能。
-
硬件:SSD磁盘、32GB+内存(堆内存不超过31GB)。
-
索引设计:
-
选择合适的分词器
冷热分离:hot-warm
架构(热数据用SSD,冷数据用HDD)。- 生命周期管理(ILM):自动滚动到新索引。
-
查询优化:
-
避免
wildcard、
regexp查询(如*test*
)。 -
使用
constant_score
过滤不相关文档。 -
使用过滤器: 对于不需要评分的查询条件,使用 filter 而不是 query,因为 filter 可以被缓存以加快后续相同查询的速度。
-
合理使用聚合:聚合可以用于高效地进行数据分析,但复杂的聚合也可能非常消耗资源。优化聚合查询,如通过限制桶的数量,避免过度复杂的嵌套聚合。
-
查询尽可能少的字段: 只返回查询中需要的字段,减少数据传输和处理时间。
-
避免深度分页: 避免深度分页,对于需要处理大量数据的情况,考虑使用 search_after。
-
总结
-
选型:ES适合搜索/分析,MySQL适合事务/精准查询。
-
核心能力:分词器、倒排索引、分布式计算支撑高效查询。
-
一致性 :通过
refresh
、Canal同步等方案平衡实时性与性能。 -
实践:结合业务场景选择分页策略、数据结构、同步机制。