注:近期的开发涉及了不少与es相关的逻辑处理,上线后进行复盘的同时也对es做了一个简单的整理
什么是es?
es,Elasticsearch的简称,由 Elastic 公司开发,基于 Apache Lucene(开源的全文检索库,核心是倒排索引技术)构建,是一个分布式、开源的搜索引擎和数据分析引擎,主要用于高效的全文搜索、数据存储和实时数据分析
来看官网截图:
核心特点
- 高效的全文搜索(性能和效率) :es 可以处理复杂的全文检索请求,支持模糊匹配、分词等功能
- 分布式架构:支持分布式系统,数据可以分片(主分片+副分片)并分布在多个节点上,具有高可用性和水平扩展能力
- 实时数据分析:可以快速分析大量实时数据,适用于日志分析、业务数据分析等场景
- RESTful API:基于 HTTP 协议进行交互,用户可以轻松向 es 发起请求
- JSON 数据格式:数据在 es 中的存储为 JSON 文档,方便灵活查询
- 强大的搜索功能(灵活性和功能丰富性) :支持倒排索引、聚合查询、过滤查询、排序、分词等功能
倒排索引
倒排索引是搜索引擎中一种常见的数据结构,它将单词(Term)映射到包含该单词的文档列表, 这一机制与书籍的索引类似,可以快速定位包含某些关键词的文档
假设有以下三个文档:
- 文档1: "猫 喜欢 鱼"
- 文档2: "狗 喜欢 骨头"
- 文档3: "猫 和 狗 是 好朋友"
则倒排索引的结构如下:
Term | 文档列表 |
---|---|
猫 | 1, 3 |
喜欢 | 1, 2 |
鱼 | 1 |
狗 | 2, 3 |
骨头 | 2 |
和 | 3 |
是 | 3 |
好朋友 | 3 |
通过这个索引,查找包含“狗”的文档,只需要访问与“狗”相关的条目即可(文档2和文档3),不需要扫描所有文档
倒排索引的组成部分
一个完整的倒排索引通常包含以下三部分:词项字典、倒排列表、存储优化
词项字典, 存储所有的词项,即文档中提取出的每个唯一词语,词项字典有以下功能:
- 提供词项到倒排列表的映射
- 支持快速查询,通过使用排序或树形结构优化查找效率
倒排列表, 记录每个词项对应的文档ID列表,以及其他可能的位置信息或频率信息:
- 文档ID: 表示包含该词项的文档
- 词频: 某词在文档中出现的次数
- 位置信息: 词项在文档中的具体位置,用于短语查询或邻近查询
存储优化, 为减少存储占用,ES使用多种压缩算法(如帕特森编码、布尔编码)优化倒排列表的存储
倒排索引的构建过程
在将文档索引到 Elasticsearch 中时,会经历以下步骤:分词、建立词项字典、生成倒排列表
分词, 将文档内容拆分为词语,分词器会移除停用词(如“的”、“是”)、处理大小写、词干化等,例如:
"猫喜欢吃鱼" -> ["猫", "喜欢", "吃", "鱼"]
建立词项字典, 将所有的词语存储为词项字典,并对其排序
生成倒排列表, 记录每个词项在哪些文档中出现,通常包括:
- 文档ID
- 出现频率
- 位置等
倒排索引的优势
- 快速查询:通过直接访问倒排列表,可迅速找到相关文档,无需逐一遍历
- 支持复杂查询:倒排索引结合布尔逻辑(must、should、filter)能够实现复杂的多条件查询
- 优化排名:结合 TF-IDF、BM25 等算法,可以根据相关性排序搜索结果
es倒排索引的特点
基于 Lucene 的倒排索引: es 构建在 Lucene 之上,继承了 Lucene 的倒排索引结构与优化策略
分片与倒排索引: 每个分片是独立的 Lucene 索引,包含自己的倒排索引。ES 通过分片的方式并行查询,从而提高效率
实时性与刷新机制: ES 使用分段(Segment)机制管理倒排索引:
- 数据写入时,先写入内存中的缓冲区
- 通过
refresh
将缓冲区内容刷入新的倒排索引分段 - 查询时,访问最新的倒排索引分段
优化机制:
- 压缩存储: 减少倒排列表占用的空间
- 跳表(Skip List): 用于快速跳过不相关的文档,提升查询效率
- 布尔查询: 倒排索引天然适合布尔操作,支持
must
、should
等条件
倒排索引的局限性:
- 更新代价高:倒排索引是只追加的,更新操作通常意味着先删除旧数据,再添加新数据
- 不适合数值范围查询:例如,查询
price > 100
的数据,倒排索引表现不如树形结构或列式存储 - 存储需求高:对于大规模文本数据,倒排索引可能占用较多存储空间
Elasticsearch 的改进方案: 为了弥补倒排索引的不足,ES 提供了以下补充技术
- 列式存储: 用于数值、日期等字段的高效排序和聚合
- 正向索引: 用于需要快速访问文档完整内容的场景
- 内存缓存: 提高查询的实时性
倒排索引是 Elasticsearch 全文搜索的核心,它的高效性源于词项字典与倒排列表的设计。结合 Lucene 的优化机制,ES 不仅实现了快速的全文检索,还扩展了对多样化查询需求的支持
核心组件
- Cluster(集群) :由多个节点组成的 Elasticsearch 系统
- Node(节点) :集群中的一个运行实例,每个节点可以存储数据和响应查询
- Index(索引) :相当于数据库中的表,数据存储的地方
- Shard(分片) :索引数据可以被分为多个分片存储,每个分片可以分布在不同节点上
- Document(文档) :索引中的最小存储单元,类似于数据库中的一条记录
- Replica(副本/副分片) :分片的复制,用于容错和提高查询性能
Cluster(集群)
├── Node(节点)【多个节点组成一个集群】
│ ├── Shard(分片)【每个节点存储索引的分片】
│ │ ├── Primary Shard(主分片)【索引的主数据】
│ │ └── Replica Shard(副本分片)【主分片的备份】
│ └── ...
├── Index(索引)【相当于数据库中的表】
│ ├── Shards(分片集合)【一个索引由多个分片组成】
│ │ ├── Documents(文档集合)【分片内存储文档】
│ │ └── ...
│ └── ...
└── ...
层级结构
- 集群:由多个节点组成,负责协调所有节点的工作
- 节点:集群中的单个实例,存储索引的分片,负责存储数据和处理搜索与索引请求
- 索引:数据的逻辑单元,由多个分片组成,每个索引可以有独立的分片和副本配置
- 分片:数据的物理存储单元,可以分布在不同节点上以实现分布式存储和并行查询
- 文档:数据的最小单位,JSON 格式,存储在分片中
- 副本:主分片的备份,用于增强容错能力和提高查询性能
节点和索引
节点和索引的关系可以理解为物理存储与逻辑单位的映射关系( 没有直接关系,两者的联系通过分片建立 ):索引是数据的逻辑组织单位,一个索引由多个分片组成 <=> 分片是实际存储数据的物理单位,分片会分布在节点上 <=> 每个节点负责存储分片,并对分片执行相关的读写操作
Node 与 Index 映射特点 | 描述 |
---|---|
一个 Node 可以存储多个 Index | 每个节点可以存储多个索引的数据(即存储多个分片) |
一个 Index 可以跨多个 Node | 一个索引的数据通过分片分布在多个节点上,从而实现分布式存储和高可用性 |
分片是连接桥梁 | Node 和 Index 的关系通过 Shard 建立:索引的数据被拆分为分片,分片再分布到各个节点 |
副本在不同节点上 | 副本分片和主分片会分布在不同的节点上,增强容错能力。例如,如果 Node1 宕机,Node3 上的副本分片可以提升为主分片继续服务 |
动态负载均衡 | Elasticsearch 自动管理分片的分布。例如当新的节点加入集群时,Elasticsearch 会将部分分片重新分配到新节点,优化负载和性能 |
es中的主从架构
es 中的主从架构,在实现上更贴近于分布式系统的主副本机制,而不是传统数据库的主从模式
比较项 | Elasticsearch(ES) | 传统数据库主从模式(如 MySQL) |
---|---|---|
主从架构角色 | 每个分片(主分片和副本分片)独立存在,主分片和副本分片角色动态切换,且分布在多个节点上 | 主库负责写操作,从库只负责读操作,主从角色固定不变,通常一个主库多个从库 |
数据分布方式 | 数据被分片,索引中的每个主分片和对应的副本分片分布到多个节点,实现负载均衡和分布式存储 | 数据以完整的表或库为单位复制到从库,所有从库存储的内容与主库一致 |
扩展性 | 原生分布式架构,节点可横向扩展,分片数量可调整,适合处理大规模数据。 | 需要依赖手动分库分表或主从复制扩展,扩展能力有限,可能需要中间件支持 |
故障处理 | 主分片故障时,自动选举副本分片为主分片,无需手动干预,系统高可用 | 主库故障后需手动或借助工具切换到从库,切换时间较长,系统可用性受影响 |
一致性模型 | 最终一致性:写入操作先写主分片,再同步到副本分片,短时间内查询可能存在数据延迟 | 强一致性:事务写入主库后,主从同步通常较快,但主从延迟可能存在 |
读写模式 | 支持负载均衡,读请求可在主分片和副本分片之间分配,写请求只针对主分片 | 读写分离:读请求发送到从库,写请求发送到主库,主库写性能压力较大 |
使用场景 | 适合实时全文搜索、大数据分析、分布式存储等需要高可用和高扩展性的场景 | 适合结构化数据的事务处理(如订单管理、财务系统等),强一致性需求较高 |
总结:Elasticsearch 的主副本机制与传统数据库的主从模式不同,它更适合分布式场景,支持自动容错和高可用性,但由于采用最终一致性模型,可能不适合高一致性要求的业务场景(如金融系统)
应用场景
- 全文搜索: 用于实现电商、博客、文档管理等系统的快速搜索功能,支持模糊查询、分词和相关性排序。如电商网站商品搜索、知识库的文档检索
- 日志分析: 配合 Logstash 和 Kibana,用于实时日志数据的采集、存储和可视化分析。如服务器日志、应用性能监控(APM)
- 实时数据分析: 用于大规模数据的实时聚合和统计分析,如用户行为分析、流量监控
- 推荐系统: 基于用户行为和商品数据的搜索与分析,构建个性化推荐系统。如内容推荐、电商推荐
- 安全监控: 分析安全事件日志,检测异常行为。如入侵检测系统、银行交易异常监控
使用es的缺点有哪些?
- 数据一致性问题: 由于分布式架构(主副分片类似于数据库的主从架构),数据在不同节点间同步存在延迟,可能导致短时间内查询结果不一致
- 写入性能较差: 数据写入时需要重建索引和分片,耗费较多资源,不适合高频写入场景
- 资源消耗高: 对内存和磁盘的需求较高,尤其在处理大规模数据或复杂查询时
- 缺乏事务支持: 不支持 ACID 事务,难以应用于需要强一致性保障的场景(如银行系统)
- 学习成本较高: 配置、索引设计和性能优化需要较多学习和实践
- 运维复杂: 分布式系统的架构使得运维和故障排查复杂,例如分片不均衡或节点故障时的处理
总结:Elasticsearch 非常适合实时搜索、数据分析和非结构化数据处理,但不适合事务性强、高一致性要求的场景。此外,资源消耗和运维复杂性是它的主要弱点,需要在项目中合理评估是否适用
查询es和查询MySQL有什么区别
es 和 MySQL 是两种不同的技术,分别用于搜索引擎和关系型数据库,查询方式和用途也存在较大区别
特性 | MySQL | Elasticsearch |
---|---|---|
数据类型 | 结构化数据(行和列) | 非结构化/半结构化数据(JSON 文档) |
查询特点 | 精确查询、关系型操作 | 全文搜索、模糊查询、多维聚合 |
事务支持 | 支持事务(ACID) | 不支持事务,最终一致性 |
数据规模 | 中小规模数据,性能随数据量增加可能降低 | 海量数据处理性能优越 |
适用场景 | 金融、库存管理、用户管理等 | 搜索引擎、日志分析、实时监控等 |
数据存储模型
MySQL:
- 基于关系型数据库模型,数据以表的形式存储,遵循行和列的结构化数据模型
- 数据具有严格的模式,字段类型和约束需要事先定义
- 查询语言为 SQL, 主要用于结构化数据查询
Elasticsearch:
- 基于文档(Document)存储,数据以 JSON 的形式存储,文档被组织在索引中
- 模式相对灵活,字段可以动态映射,支持多种数据类型和嵌套结构
- 查询使用的是 DSL, 特别适合全文搜索和复杂分析
查询方式
特性 | SQL | DSL |
---|---|---|
查询格式 | 类似自然语言(关键字如 SELECT、WHERE) | JSON 格式,结构化且灵活 |
查询能力 | 精确查询和关系型操作 | 全文搜索、模糊查询和多维聚合分析 |
聚合方式 | GROUP BY 和聚合函数 | 聚合(aggregations),支持嵌套和多维分析 |
适用场景 | 结构化数据,事务管理 | 非结构化/半结构化数据,搜索和分析场景 |
MySQL:
- 查询通过 SQL, 强调精确查询和结构化数据操作,比如 SELECT、JOIN、GROUP BY、ORDER BY 等
- 支持事务,查询时能保证数据的一致性(ACID)
- 适合简单的键值查询和表间关系查询
SELECT name, age FROM users WHERE age > 25 AND city = 'beijing';
Elasticsearch:
- 查询通过 JSON 查询 DSL(类似于 RESTful 风格),支持复杂的全文搜索(如模糊匹配、分词查询、相关性评分等)和聚合分析
- 支持实时搜索,但可能会有数据最终一致性延迟
- 更适合非结构化或半结构化数据,尤其是需要模糊匹配或分词的场景
示例:
GET /users/_search
{
"query": {
"bool": {
"must": [
{ "match": { "city": "beijing" } },
{ "range": { "age": { "gt": 25 } } }
]
}
}
}
查询性能和适用场景
MySQL:
- 查询速度取决于表的大小、索引的设计和优化
- 对于简单的主键查询或小规模的数据操作性能高
- 适合事务处理、数据一致性要求高的业务场景
Elasticsearch:
- 查询速度对全文搜索场景非常高效,特别是海量数据中进行模糊查询
- 倒排索引(Inverted Index)使其在关键词搜索、相关性计算和聚合分析方面更有优势
- 更适合搜索和分析密集型场景
聚合和分析
MySQL:聚合操作主要通过 SQL 中的 GROUP BY 和聚合函数(如 COUNT、SUM、AVG)实现,数据分析能力相对有限,适用于简单统计和查询。
SELECT city, COUNT(*) FROM users GROUP BY city;
Elasticsearch:提供强大的聚合功能(如 Terms Aggregation、Date Histogram 等),可以实现复杂的数据分析和分组统计,同时支持嵌套聚合、层级聚合,非常适合实时数据分析。
GET /users/_search
{
"aggs": {
"group_by_city": {
"terms": { "field": "city.keyword" }
}
}
}
数据一致性
MySQL:
- 保证强一致性(ACID 特性),事务的提交和回滚确保数据可靠性。
- 数据操作在多表之间可以保证一致性。
Elasticsearch:
- 默认采用 最终一致性
- 在分布式集群中,数据的写入和查询可能存在短暂延迟,但最终会达到一致状态
实践中的es相关
从理论回到实践,来看看项目中与es相关的部分内容
什么样的需求需要走es而非数据库
以标签字段 Tags 为例,该字段在数据库中以 JSON 格式存储,且没有索引,直接对数据库进行查询,相当于全表扫描,查询效率很低,所以走 es
es查询基础
Bool 查询
BoolQuery
是构建复杂查询的核心,通过以下子句组合条件:
must
:必须满足的条件,相当于 SQL 的AND
must_not
:必须不满足的条件,相当于 SQL 的NOT
should
:可以满足的条件,相当于 SQL 的OR
,满足多个条件时相关性得分更高filter
:必须满足的条件,但不影响相关性得分
常用查询类型
term
查询:用于精确匹配,如查询某字段的具体值。
{ "term": { "status": "active" } }
match
查询:用于全文搜索,支持分词匹配
{ "match": { "description": "elastic search" } }
range
查询:用于范围过滤
{ "range": { "date": { "gte": "2024-01-01", "lte": "2024-12-31" } } }
查询结果控制
from
和size
:控制分页,类似 SQL 的LIMIT
和OFFSET
sort
:控制排序规则_source
:指定返回字段
查询语法示例
以下是一个简单的示例,查找满足多个条件的用户数据:
{
"query": {
"bool": {
"must": [
{ "term": { "status": "active" } },
{ "range": { "age": { "gte": 20, "lte": 30 } } }
],
"must_not": [
{ "term": { "is_deleted": true } }
],
"should": [
{ "match": { "name": "John" } },
{ "match": { "name": "Doe" } }
]
}
},
"from": 0,
"size": 10,
"sort": [
{ "last_login": { "order": "desc" } }
]
}
es封装方式
在了解了es查询基础概念后,可以更顺畅的熟悉封装方式
- 通用封装:如何基于
BoolQuery
动态生成must
、must_not
、should
等条件 - 功能封装:如何将重复逻辑(如
term
查询和range
查询)抽象为独立模块,提高代码复用性
通用封装 - 布尔查询 ( BoolQuery
)
通用封装使用 BoolQuery
构建查询条件,通过 Must
、MustNot
、Should
等组合来精细控制查询逻辑
使用方法特点:
- 输入多条件查询:对数据进行组合过滤,比如部门、用户 ID、绑定状态等
- 支持分页和排序:通过
From
和Size
分页,以及Sort
指定排序字段
//相关代码已进行脱敏处理
func AccountList(ctx context.Context, req iapi.UserListReq) (total int64, list []dao.WorkUser, err error) {
var (
es = esclient.Client()
query = elastic.NewBoolQuery()
)
query.Must(elastic.NewTermQuery("corp_id", req.CorpId))
query.Must(elastic.NewTermQuery(`origin_dept`, req.DeptId))
query.MustNot(elastic.NewTermQuery(`status`, types.StatusDelete))
if req.StartTime > 0 && req.EndTime > 0 {
query.Must(elastic.NewRangeQuery("modify_time").Gte(req.StartTime).Lte(req.EndTime))
}
esResp, err := es.Search("index_name").
Query(query).
From((req.Page - 1) * req.PageSize).
Size(req.PageSize).
Sort("work_pk_id", false).
Do(ctx)
}
适用场景:
- 需要构建复杂的多条件查询。
- 查询条件是动态变化的,根据用户输入或逻辑动态生成
BoolQuery
功能封装 - 专用条件构建器
这种方式通过特定的函数封装子查询条件(比如部门条件、用户条件),简化主查询逻辑
使用方法特点:
- 条件生成器封装:独立封装子条件生成器(如部门条件或用户条件),主查询通过调用这些生成器完成条件构建
//相关代码已进行脱敏处理
func buildShouldDept(ctx context.Context, corpId, deptId int64) *elastic.BoolQuery {
boolQuery := elastic.NewBoolQuery()
all := department.AllDescendants(ctx, corpId, deptId)
for _, qId := range append(all, deptId) {
boolQuery.Should(elastic.NewTermQuery("origin_dept", qId))
}
return boolQuery
}
- 主查询调用:
func AccountTotal(ctx context.Context, corpId, deptId int64) (total int64) {
query := elastic.NewBoolQuery()
query.Must(elastic.NewTermQuery("corp_id", corpId))
query.Must(buildShouldDept(ctx, corpId, deptId))
query.MustNot(elastic.NewTermQuery("status", types.StatusDelete))
countRes, err := es.Search("index_name").
Query(query).
Size(0).
Do(ctx)
total = countRes.TotalHits()
}
适用场景:
- 某些查询逻辑需要复用,适合单独抽离为功能模块
- 提高代码可读性,减少重复
总结:这两种封装方式在你的代码中广泛使用,通用封装适用于复杂的多条件查询,功能封装用于提高可复用性,将子条件独立封装,可根据实际需求结合两种方式使用,复杂场景可以借助子条件构建器简化主查询逻辑
问题一:界面刷新问题
弹窗筛选列表接口走的es查询,当前端通过标签增加/删除接口修改es有关数据后,会在一定的时间内再次调用弹窗筛选列表接口,这个时间最初设置60毫秒,发现不行,设置80毫秒,还是不行,最后增加到了500毫秒,发现也只是概率性成功
问题分析
写入延迟?否,基础架构部门的设置,会直接从缓冲区中读取
异步数据复制机制?是,在主分片写入完成后,副本分片尚未同步最新数据
解决方案
实时性要求较高的话,可以结合 Redis 缓存优化整体查询架构
es进阶文章推荐
进阶文章一
写在最后
对你有一定帮助的话可以点赞支持