ElasticSearch 进阶

ES使用场景
全文搜索
日志检索,配合Kibana可视化
商品搜索
实时监控
个性化推荐,根据用户搜索历史、浏览行为找出相似内容
地理位置搜索(外卖系统)
智能提示/提示词
🔵 MySQL不能替代ES,因为:
MySQL做全文检索很弱(like %xxx% 查询超级慢)
MySQL不擅长复杂聚合分析(特别是大数据量)
MySQL无法做到灵活的多字段模糊搜索、地理位置搜索、同义词处理
🔵 ES不能替代MySQL,因为:
ES写一致性弱,不支持多表事务(比如扣库存和扣余额一起回滚)
ES不保证强一致性(默认最终一致性,适合搜索,不适合资金类应用)
ES的数据模型松散,复杂关系建模很痛苦(比如订单拆分、子表操作)
什么是ElasticSearch?
简单来说,ES是一个分布式的、基于文档的 搜索引擎+数据库系统
分布式: 天然支持分布式/集群、扩展节点
文档存储: 数据并不是以行或者列进行存储,而是一份份JSON文档
- 聚合分析(数值型字段)会单独存储到Doc values中,列式存储(加速聚合分析,且无冗余字段,比MySQL快太多了
搜索引擎: 天然支持全文检索、模糊匹配、权重排行
近实时: 数据写进去后,需要等待几秒才能查询到
专业术语(import)
集群: 一堆ES结点组成的整体
结点:单个ES实例
索引: 类似于MySQL的数据库表
文档: 存储的一条数据(JSON) 类似于MySQL的一行数据
字段(Field): 文档里面的key , 类似于MySQL的列字段
Mapping: 字段结构定义 , 类似于MySQL的表结构
分片(shard):把索引数据切片到不同结点上 (分库分表)
副本(Replica):分片的备份
索引
ES的索引一旦建好,很多东西无法随意更改哦!
settings中的参数能够动态修改(副本数)但是分片数无法修改
只能追加新字段,无法修改或者删除字段
为什么ES无法随便修改字段?
倒排索引,修改字段= 破坏索引结构,代价极大
ES底层时不可变文件(Segment)
- 类似于LSM树的SSTable
- 只写不改
- 旧的数据标记为无效,合并时清除
Reindex
创建一个新索引(新的mapping)
将老数据reindex过去
删除老索引
ES 中 就有这个API -> reindex
POST _reindex |
底层结构
- 倒排索引
极为核心的一点
不同于正排索引那样: 这个文档包含哪些字段
倒排索引描述: 这些字段 存储在哪些文档中
- 文档存储
ES中,文档时存储数据的基本单位,每个文档实际上就是一个JSON对象,包含一个唯一的ID
文档数据存储在 一个 名为 Segment 的数据结构中
- Segment(段)
ES会把文档写入到磁盘中的Segment中
每个段是不可变的(存储了倒排索引、文档存储、元数据等内容)
和LSM树的SSTable极为相似
- 倒排表
倒排表是倒排索引的存储形式,用来快速检索词和文档之间的关系
分词 | 文档id | |
---|---|---|
az | 1,2,3 | |
阿巴阿巴 | 3 |
每个词都会有一个倒排表,用于存储哪些文档包含整个词,以及这个词在文档中的位置(postings
倒排表会结合Posting List 来存储具体的位置信息
- FST(Finite State Transducer)
FST是ES中用来优化倒排索引的一种数据结构,用来存储词典(Term Dictionary),提高查找效率
Term Dictionary 存储所有的term(词条)FST通过有限状态机压缩这些词条
由于字典中的词条数量巨大,通过压缩减少存储空间
- Posting List
用于存储某个词出现的文档ID以及其他相关信息(词频、位置等)
结构: Posting List 中每一项都包含了文档ID和该词在文档中的出现位置
索引: 每个都会有一个对应的Posting List
CRUD操作过程
关于近实时问题:https://blog.csdn.net/hugo_lei/article/details/106519069
Elasticsearch搜索引擎的索引构建过程?
解析文档,通过分词器将文档中的字段进行分词,存储到term dictionary中
ES会遍历term dictionary,查看这个分词是否已经被记录过。
为每一个term(分词)创建一个Posting List,并将该文档id以及该词存储进去
如果已经为该 term 创建了一个PostingList,直接合并该文档id进去
输入一个单词去查询会发生什么?
由一个协调节点分析 该索引 是存储在哪个 主分片上的
确定好后,将该单词进行分词
从目标分片的Segment中使用FST 进行前缀遍历 定位 分词在倒排索引表中的偏移量
通过偏移量查询到该 分词所在posting list位置,然后拿到文档id集合
将多个segment的id集合进行合并
通过相关算法计算相关性评分然后进行结果排序
根据from和size进行分页与截断
聚合分片结果(文档 ID 和评分,生成全局排序列表)获取原始文档
执行高亮/聚合操作 返回 JSON格式搜索结果
如何通过文档id查询到对应索引下的文档?
通过hash 计算_id 是存储在哪个分片下的
在分片内定位到Segment
Segment元信息:维护一个全局文档ID 到 Segment的映射表
使用跳表 快速定位文档所在Segment
在Segment内部查找文档
通过内部的 文档ID 到内部DocID的映射表
Lucene每个segment内部是按docID顺序存的,非常快
假设查询 GET /orders/_doc/123: |
如何实现范围查询?
数值/日期范围:BKD树
字符串范围:字典序+倒排索引
日期范围转化为数值
写入一个新文档过程?
https://blog.csdn.net/hugo_lei/article/details/106519069
新写入的文档会首先暂存到in-memory-buffer中(不可被搜索到)
同时写入事务日志(Translog)(实时fsync写入到磁盘中,顺序写
Refresh:默认每1秒,将内存缓冲区的数据生成新的segment并放入到文件系统缓存(可搜索)
Page Cache中? yep!
- Flush: 定期将文件系统中的段持久化到磁盘中
接下来是困难版
客户端发送写入请求
协调结点处理请求
解析索引名称,确定文档应该写入到哪个主分片
根据routing 计算目标分片位置(通过_id 进行hash计算)
转发请求到该分片所在主分片结点
主分片处理写入
文档先写入到in-memory-buffer中(不可被搜索、未持久化
写入事务日志(Translog
同步写入
顺序写入磁盘
返回客户端ACK
Refresh(数据可被搜索
Flush(30分钟/translog写满
Segment Merge(段合并
后台自动执行
不影响搜索(旧segment在合并完成后才被替换
副本分片同步
执行主分片相同操作
wait_for_active_shards:可配置必须有多少分片可用才返回成功
删除一个文档过程
删除请求到达
不会物理删除,而是打上一个标记,同时写入到translog中(查询时会过滤
刷新(flush)和合并(merge)时真正删除
更新操作
本质时删除 + 新增
先打上删除标记
将新文档内容写入到in-memory-buffer和translog中
更新之后,文档的_version字段会+1,通过乐观锁保证并发一致性
后续通过merge清理旧数据
什么时候进行Segment合并
后台自动合并
Flush时触发
手动触发
基于segment数量和大小(数量/存储大小达到阈值
Lucene
倒排索引
FST
https://juejin.cn/post/7226710109585424440
FST(有限状态机)用于快速检索、前缀匹配、范围查询
ES中主要用于存储 词典
为什么要使用FST?
倒排索引中词典十分庞大,需要通过关键字快速定位Posting List
兼顾 小空间占用 + 快速查询
可以理解为压缩版的有向图(共享前缀和后缀)
核心元素
节点: 一个状态,代表某个字符读到这里了
边: 从一个节点到另一个节点的跳转,携带一个字符/字符范围
输出: 走到某个路径时累加的值
- 所有以该前缀开头的词项的公共信息(如内存地址)-》前缀优化共享
- 权重(如概率、分数),通过累加得到路径总权重。
- 该词在倒排索引中的 文件偏移量(File Pointer)。
前缀共享
后缀合并
通过前缀遍历快速找到关键词,然后拿到Posting List然后拿到文档ID
思考: 前缀遍历?那 apple 构建状态机后, 使用 pl 如何搜索呢?
害,pl 和apple已经是两个单词了,且没有共用一个前缀,所以无法搜到,除非把apple五个字母都进行拆分
实在要搞的话,可以建立反向索引或者使用高级搜索,遍历Segment
比如使用appld 可以搜到apple
开始遍历时,记录当前位置和当前误差次数。
遇到不同字符时,不是直接失败,而是:
- 如果误差没超上限(比如1次),尝试走插入/删除/替换的可能路径!
找到匹配路径后,返回最接近的词
Trie只能共享前缀,内存消耗较大,每个节点存储一个字符串
通过前缀遍历得到的 词组 + 输出值便能得到 该词在倒排索引表中的偏移量,无需是使用Hash定位(甚至不需要解决hash冲突)
查找效率更高,无需注意哈希函数计算以及冲突问题
Hash不关心顺序
设计不当时,Hash内存开销更大
通过前缀遍历进行匹配
若设置了模糊查询可以忍受一定程度的不同(比如app-》ape)
走完整个词(比如app 走到 p时,发现该边指向节点接受状态(此时累计的输出值就是posting list的起始位置
积累值会变化
小demo体验一下创建过程
番外?
FST结构(有序!): |
Posting List
用于记录每个分词 出现在哪些文档的id集合
Segment(Import!)
核心组成部分
- 倒排索引(FST+Posting List)
- 词项字典 (存储所有的 词项 term ,使用 FST 压缩
- 倒排列表(记录 词项 对应的 文档ID 以及位置信息(词频、偏移量)
- 存储词项位置和负载(用于短语查询和高亮
加速全文搜索
- 正排索引(Doc Values)
- 存储字段的列式数据(numeric、keyword等)
- 存储元数据(最大值、最小值、压缩方式
加速排序、聚合、脚本计算,按照 列式 存储字段
文档存储
用于存储原始文档内容(_source)用于返回搜索结果存储原始JSON文档
存储文档的元信息(偏移量、压缩方式
文档ID映射 (DocID -> _id)
内部简历DocID 和用户 _id的映射关系Segment的元信息,DocID 范围
Lucene内部通过 DocIDMap维护 _id 到DocID的关联
BKD树
K-D 树 + B+树混合

适用场景:数值过滤、地理围栏、时间区间分析等
优势:
- 高效磁盘存储:数据按块(Block)组织,减少随机 I/O。
- 查询优化:剪枝策略快速跳过不匹配的数据块。
- 压缩存储:对数值类型(如整数、浮点数)采用增量编码压缩。
LSM树
https://zhuanlan.zhihu.com/p/181498475
https://zhuanlan.zhihu.com/p/415799237
Memtable
存储在内存中,用于保存最近更新的数据
通常使用红黑树/跳表数据结构实现,按照Key有序地组织这些数据
写入的时候通常会追加一份WAL(Write Ahead log)预写日志,保证宕机恢复
当memtable达到一定大小时,会flush到磁盘中
Immutable memtable
memtable到sstable的一个中间状态
当memtable满了之后,不会直接删除,会变为一个只读的immutable memetable
同时创建一个新的memtable用来接收写请求
这个immutable memtable会通过后台线程刷盘,变为sstable
为什么这么设计?
写操作不中断
异步刷盘
SSTable
当Immutable memeTable刷盘到磁盘中,形成一个SSTable文件
由于是顺序写到磁盘,写性能极高
SSTable是不可变的,只能增加或者合并产生新的SSTable
主要包含内容
Key/value数据(有序
稀疏索引(快速定位
布隆过滤器(快速判断key是否存在
元数据(校验数据,时间戳等–》因为可能存在重复key,更新时间最新的才是有效key
Compact策略
- Size-tiered 策略

- Leveled 策略

关键点:
读放大
写放大
空间放大
Level 0: 刚flush出来的小sstable文件,不要求有序,且key之间范围可以重叠
Leve 1 及以上: 每一层文件的key范围不重叠,数据量更大
增删查改操作
由于LSM树初衷就是应对写操作极为频繁的场景
插入操作只需要无脑写入memtable就行了
采用逻辑删除
写一条墓碑标记到 WAL 和 memtable中
真正删除该数据是在compaction时完成
也就是说物理数据在memtable中还是磁盘中,还是不存在,无需关系
compaction过程会进行删除
先查memtable(因为是最新的操作)
再查immutable memtable
还没有找到则按照level 0 - n依次查找
查询时要处理墓碑标记(如果找到墓碑,就算有旧数据,也算被删除)
随着level越高,文件越少、范围越大(减少查找代价)
写入WAL
写入memtable
待修改数据在memtable中,直接覆盖
带修改数据在磁盘中 直接写在memtable中,后续合并时新数据覆盖旧
该数据不存在,update操作变为insert
其实update和insert操作在这里貌似就是相同的
有墓碑标记的数据什么时候被清除呢?会不会永远无法清除?
SSTable合并时删除带有墓碑标记的数据
由于LSM树的机制,不同层可能包含相同的key
直接删除可能会漏删老版本,或者删除代价极大
定期强制压缩
设置墓碑最大存活时间
- 创建一个新SSTable,然后进行拷贝
ElasticSearch(Lucene) 与LSM的异同
LSM树 | Lucene(ES) | |
---|---|---|
写入 | 先写WAL文件再写入MemTable | 先写Translog,再写入in-memroy-buffer |
刷盘 | MemTable满了刷入immutable 然后刷入sstable | in-mm-buffer满了(或1秒后)刷入Page Cache然后刷入磁盘 |
查询 | 先mt,再imt,再sst(读放大严重) | 先内存中的segment、再磁盘中的segment 加入Caching优化机制,优先读最新段 |
删除 | 墓碑、懒回收、合并清除 | 墓碑,懒回收,合并清除 |
合并 | Compaction | Segment Merge |
数据 |
LSM
|
倒排索引(FST + Posting List)
文档存储(JSON) |
查询优化 | 布隆过滤器、二分查找 | FST词典、跳表、缓存 |
数据不可修改 | 是 | 是 |
空间回收 | 合并时清理墓碑、去重 | 合并时清理已删除文档 |
关注点 | 写多读少(SSTable有序) | 写少读多 |
核心相似点
- 追加写入
两者都是先将数据写入到内存中,然后刷盘不可变的文件中
通过顺序写入减少随机IO
- 分层合并
LSM树通过后台合并,将多个SSTable合并为更大更有序的SSTable,更新文件,删除冗余数据(重复key)
Lucene通过后台合并,将多个小segment合并为一个大Segment,优化查询性能,并回收空间
- 不可变性
SSTable和Segment写入磁盘后无法更改,删除/更新操作通过追加 + 合并进行实现
简化并发控制,避免锁竞争
关键差异
设计目标
数据结构
合并策略
删除处理
核心思想
从集群来看ES
集群
Cluster= 一群Node + 一个Master Node(协调)+ 数据分散存储
结点
Mater Node:复杂管理Cluster状态(建索引、删除、分片分片等)
Data Node: 负责存储数据、处理读写请求
Coordinating Node: 负责接收请求、拆分请求、分发到各个结点,最后聚合结果
Ingest Node: 做预处理
一个结点可以有多个身份
索引
Index相当于数据库中的表,逻辑上的数据集合
每个Index物理上会被分成多个 Shard(分片,存储到不同结点上
索引创建时就需要指定主分片数+副本分片数
分片
主分片(Primary Shard):真正存储数据的分片。
副本分片(Replica Shard):主分片的拷贝,用于容灾、高可用,同时也能分担查询负载。
每个分片其实内部就是一个 Lucene 引擎实例(独立维护倒排索引、Segment、Document等)。
🔥 举个例子
假设:
我创建了一个 products 索引
设置为 3个主分片,每个主分片 1个副本
那就是一共:
3 个 Primary Shard(主)
3 个 Replica Shard(副本)
总计 6 个 Shard
如果你的集群有 3 个 Node,那么可能的分配:
⚡ 注意:Elasticsearch 调度器会尽量让 主副本和副副本不在同一台机器上,以提高容错性!
番外
. 用 geo_shape 类型(而不是 geo_point)
geo_point:经纬度,单个点(只能做简单的附近搜索)
geo_shape:支持复杂的几何图形,比如:Polygon(多边形)、MultiPolygon(多个多边形)、Line、Envelope 等等。
示例:建索引 Mapping:
PUT /stores |
- 存储门店配送范围(多边形Polygon)
每家店上传一个或多个 Polygon:
POST /stores/_doc |
这里的 coordinates 是经纬度数组(WGS84坐标系)。
每个 Polygon 一定要首尾闭合。
- 检索用户位置是否在配送区内
使用 geo_shape 的 intersects 查询:
GET /stores/_search |
意思是:找到所有 配送区域覆盖这个点的门店。
当然也可以用 within、contains 等其他空间关系。
场景设计
外卖系统怎么用ES聚合附近商家的评分?
比如,用户打开饿了么,美团,想找附近评价高的商家。
- 商家文档结构(在ES里)
{ |
- 用户搜索流程
根据用户位置(比如上海市中心)用 geo_distance 查询,查出 3km 内的商家。
然后用 聚合(Aggregation):
- 按评分(rating)做 平均值、排序,挑评分最高的商家。
- 按销量(monthly_sales)做 销量榜单。
具体的 ES 查询
{ |
🛒 电商系统怎么用ES算实时销售榜?
比如淘宝、京东首页上,「实时热销榜」就是这么搞的!
- 商品文档结构(在ES里)
{ |
- 查询实时热销榜流程
用字段 last_30min_sales 按照销量做 降序排序。
只取 Top 100 热销商品。
如果按品类分类(比如”手机”、”家电”),再做个 terms aggregation 按 category 分组!
具体的 ES 查询
{ |
如果要按分类出榜单,再加一个聚合:
"aggs": { |
👉 效果:比如“手机”品类下热销前5名,“家电”下热销前5名。
对比Redis的Zset有什么优势?
支持复杂筛选:比如「上海地区,手机品类,价格5000以内,近一周销量榜」。
支持复杂聚合分析:比如「按品牌分组统计销量」「统计每天销量走势」。
支持冷热数据分层:老数据转冷,不影响新数据查询。
适合超大规模数据:TB级,PB级也能撑住。