Elasticsearch浅析

111 阅读18分钟

注:近期的开发涉及了不少与es相关的逻辑处理,上线后进行复盘的同时也对es做了一个简单的整理

什么是es?

es,Elasticsearch的简称,由 Elastic 公司开发,基于 Apache Lucene(开源的全文检索库,核心是倒排索引技术)构建,是一个分布式、开源的搜索引擎和数据分析引擎,主要用于高效的全文搜索、数据存储和实时数据分析

来看官网截图:

image.png

核心特点

  • 高效的全文搜索(性能和效率) :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): 用于快速跳过不相关的文档,提升查询效率
  • 布尔查询: 倒排索引天然适合布尔操作,支持 mustshould 等条件

倒排索引的局限性:

  • 更新代价高:倒排索引是只追加的,更新操作通常意味着先删除旧数据,再添加新数据
  • 不适合数值范围查询:例如,查询 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有什么区别

esMySQL 是两种不同的技术,分别用于搜索引擎和关系型数据库,查询方式和用途也存在较大区别

特性MySQLElasticsearch
数据类型结构化数据(行和列)非结构化/半结构化数据(JSON 文档)
查询特点精确查询、关系型操作全文搜索、模糊查询、多维聚合
事务支持支持事务(ACID)不支持事务,最终一致性
数据规模中小规模数据,性能随数据量增加可能降低海量数据处理性能优越
适用场景金融、库存管理、用户管理等搜索引擎、日志分析、实时监控等

数据存储模型

MySQL

  • 基于关系型数据库模型,数据以的形式存储,遵循行和列的结构化数据模型
  • 数据具有严格的模式,字段类型和约束需要事先定义
  • 查询语言为 SQL, 主要用于结构化数据查询

Elasticsearch

  • 基于文档(Document)存储,数据以 JSON 的形式存储,文档被组织在索引
  • 模式相对灵活,字段可以动态映射,支持多种数据类型和嵌套结构
  • 查询使用的是 DSL, 特别适合全文搜索和复杂分析

查询方式

特性SQLDSL
查询格式类似自然语言(关键字如 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 的 LIMITOFFSET
  • 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 动态生成 mustmust_notshould 等条件
  • 功能封装:如何将重复逻辑(如 term 查询和 range 查询)抽象为独立模块,提高代码复用性

通用封装 - 布尔查询 ( BoolQuery )

通用封装使用 BoolQuery 构建查询条件,通过 MustMustNotShould 等组合来精细控制查询逻辑

使用方法特点:

  • 输入多条件查询:对数据进行组合过滤,比如部门、用户 ID、绑定状态等
  • 支持分页和排序:通过 FromSize 分页,以及 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进阶文章推荐

进阶文章一

ES+Redis+MySQL高可用架构设计

写在最后

对你有一定帮助的话可以点赞支持