跳到主要内容

Elasticsearch 整体架构

Elasticsearch 整体架构

场景化例子: 假设你运营一个电商网站,每天有100万个商品需要搜索。你可以这样设计:

  • Master节点:管理集群状态,决定商品索引的分片分配
  • Data节点:存储实际的商品数据(价格、描述、库存等)
  • Coordinate节点:接收用户搜索请求"红色连衣裙",协调各分片查询

数据存储结构关系

Elasticsearch 采用层次化的数据存储结构,从上至下包含以下几个核心概念:

集群(Cluster)

集群是整个 ES 系统的最高层级,由一个或多个物理节点组成。集群负责:

  • 节点管理:自动发现和管理集群中的所有节点
  • 故障转移:当节点宕机时自动重新分配分片
  • 负载均衡:将请求分发到不同节点处理
  • 数据一致性:保证集群间数据的一致性

查询工作机制:客户端请求可以发送到集群中任意节点,该节点作为协调节点(Coordinating Node)负责将查询路由到相关分片并汇总结果。

索引(Index)

索引是 ES 中的核心概念,但与传统数据库索引含义不同:

数据结构特点

  • 文档集合:索引是具有相似特征文档的集合
  • Mapping 定义:每个索引都有 Mapping(映射),定义字段类型和分析器
  • 设置配置:包含分片数、副本数等配置信息

与 MySQL 对比

对比维度Elasticsearch IndexMySQL Database/Table
数据结构无固定 schema,JSON 文档固定表结构,行列数据
字段类型动态映射,支持复杂嵌套预定义列类型
查询方式全文检索 + 结构化查询SQL 结构化查询
存储方式倒排索引 + 列式存储B+ 树索引 + 行式存储

查询工作机制

// ES 查询示例
GET /products/_search
{
"query": {
"bool": {
"must": [
{"match": {"title": "红色连衣裙"}},
{"range": {"price": {"gte": 200, "lte": 500}}}
]
}
}
}

分片(Shard)

分片是索引的水平分割单元,每个分片本质上是一个完整的 Lucene 索引:

底层数据结构

  • 倒排索引:每个词项指向包含该词的文档列表
  • 正排索引:用于聚合和排序的 DocValues
  • 存储字段:原始 JSON 文档的压缩存储

分片策略

shard_num = hash(document_id) % number_of_primary_shards

查询工作机制

  1. 查询阶段(Query Phase):协调节点将查询发送到所有相关分片
  2. 取回阶段(Fetch Phase):从各分片收集文档并合并排序
  3. 结果返回:返回最终的聚合结果

对应 MySQL 的概念: 在 MySQL 中,分片类似于分库分表的概念。它们的主要区别在于:

  • 分片(Shard):Elasticsearch 的分片是自动管理的,用户无需手动指定数据存储位置,分片的分布和副本由集群自动处理。
  • 分库分表:MySQL 的分库分表通常需要开发者手动设计和实现,例如通过分片键将数据分布到不同的数据库或表中。
对比维度Elasticsearch 分片MySQL 分库分表
数据分布自动分片,集群管理手动分片,开发者管理
数据存储Lucene 索引数据库表
查询方式全文检索 + 分布式查询SQL 查询 + 分布式查询
容错性支持副本,自动故障转移需要额外实现容错机制

文档(Document)

文档是 ES 中数据存储的基本单位:

数据结构

{
"_index": "products",
"_id": "1",
"_source": {
"title": "红色连衣裙",
"price": 299,
"category": "服装",
"tags": ["时尚", "优雅"],
"specs": {
"color": "红色",
"size": ["S", "M", "L"]
}
}
}

与 MySQL 行数据对比

  • 灵活性:ES 文档支持嵌套对象和数组,MySQL 行数据结构固定
  • 版本控制:ES 每次更新都是文档重写,MySQL 支持原地更新
  • 关联关系:ES 倾向于反规范化设计,MySQL 通过外键建立关联

字段(Field)

字段是文档内的具体数据项,每个字段都有特定的数据类型:

核心字段类型

  • text:全文检索字段,会进行分词
  • keyword:精确匹配字段,不分词
  • 数值类型:integer, long, float, double
  • 日期类型:date, date_nanos
  • 复合类型:object, nested, geo_point

索引过程

查询时的字段处理

  1. 分析阶段:查询文本使用相同分析器处理
  2. 倒排查找:在倒排索引中查找匹配的词项
  3. 文档评分:基于 TF-IDF 或 BM25 算法计算相关性得分
  4. 结果过滤:应用过滤器进行精确匹配

这种层次化结构既保证了数据的逻辑组织清晰,又通过倒排索引实现了毫秒级的全文检索能力,这是传统关系型数据库难以达到的搜索性能。

为什么 ES 7.x 废弃 Type? 想象 Type 就像数据库中的表,但 ES 发现同一索引下不同 Type 的字段会相互影响。比如:

// 商品Type
{
"title": "红色连衣裙", // text类型
"price": 299
}

// 店铺Type
{
"title": "女装专营店", // 也是text类型,但语义完全不同
"price": 5 // 这里price表示店铺评分,语义冲突!
}

之前的版本中,Type 是用来解决什么问题的? 在早期版本的 Elasticsearch 中,Type 的设计初衷是为了在同一个索引中存储多种类型的文档。每种 Type 类似于关系型数据库中的表,允许用户在一个索引中区分不同类别的数据。

Type 的使用方式

  1. 定义多个 Type:用户可以在一个索引中定义多个 Type,每个 Type 有自己的字段和映射。

    PUT /ecommerce
    {
    "mappings": {
    "product": {
    "properties": {
    "title": {"type": "text"},
    "price": {"type": "float"}
    }
    },
    "store": {
    "properties": {
    "title": {"type": "text"},
    "rating": {"type": "integer"}
    }
    }
    }
    }
  2. 存储不同类型的文档

    // 存储商品文档
    POST /ecommerce/product
    {
    "title": "红色连衣裙",
    "price": 299
    }

    // 存储店铺文档
    POST /ecommerce/store
    {
    "title": "女装专营店",
    "rating": 5
    }
  3. 查询特定 Type 的数据

    GET /ecommerce/product/_search
    {
    "query": {
    "match": {"title": "连衣裙"}
    }
    }

Type 的问题

  • 字段冲突:同一索引下的不同 Type 共享底层存储结构,如果字段名称相同但类型不同,会导致冲突。
  • 复杂性增加:Type 的存在增加了索引管理的复杂性,尤其是在数据量大时。
  • 性能问题:查询时需要额外处理 Type 信息,影响性能。

因此,从 Elasticsearch 7.x 开始,Type 被废弃,推荐的替代方案是使用单一索引,并通过字段(如 type 字段)来区分文档类型。

评分机制详解

在 Elasticsearch 中,评分(Score) 是衡量文档与查询匹配程度的数值指标,用于确定搜索结果的排序。

BM25 算法(默认评分算法)

Elasticsearch 7.0+ 使用 BM25 作为默认评分算法,它是对经典 TF-IDF 的改进:

BM25 = IDF × (f(qi,D) × (k1 + 1)) / (f(qi,D) + k1 × (1 - b + b × |D|/avgdl))

核心组成部分

  1. 词频(TF - Term Frequency)

    TF = 词项在文档中出现次数 / 文档总词数
    • 词在文档中出现越频繁,相关性越高
    • 示例:文档中"连衣裙"出现3次,总词数100,则TF = 0.03
  2. 逆文档频率(IDF - Inverse Document Frequency)

    IDF = log(文档总数 / 包含该词的文档数)
    • 词越稀有,价值越高
    • 示例:1000个商品中只有10个包含"vintage",则IDF较高
  3. BM25 关键参数

    • k1:控制词频饱和度(默认1.2),防止词频过高导致得分无限增长
    • b:控制文档长度归一化(默认0.75),避免长文档获得不公平优势

评分实例

// 搜索商品
GET /products/_search
{
"query": {
"match": {
"title": "红色连衣裙"
}
},
"explain": true // 显示评分详情
}

// 返回结果
{
"hits": [
{
"_score": 2.3,
"_explanation": {
"value": 2.3,
"description": "sum of:",
"details": [
{
"value": 1.5,
"description": "weight(title:红色)",
"details": [
{"value": 0.3, "description": "tf(freq=2.0)"},
{"value": 5.0, "description": "idf(docFreq=10, docCount=1000)"}
]
},
{
"value": 0.8,
"description": "weight(title:连衣裙)"
}
]
}
}
]
}

影响评分的关键因素

  • 词频匹配度:查询词在文档中的出现频率
  • 词项稀有性:罕见词(如品牌名)比常见词(如"商品")权重更高
  • 字段权重:可为标题、描述等字段设置不同权重
  • 文档长度:较短文档中的匹配通常权重更高

自定义评分示例

{
"query": {
"function_score": {
"query": { "match": { "title": "连衣裙" } },
"boost": 1.2,
"functions": [
{
"filter": { "match": { "category": "热销" } },
"weight": 2.0 // 热销商品评分加权
}
],
"score_mode": "multiply"
}
}
}

这种层次化结构既保证了数据的逻辑组织清晰,又通过倒排索引和智能评分算法实现了毫秒级的全文检索能力,这是传统关系型数据库难以达到的搜索性能。

倒排索引原理

搜索过程: 当用户搜索"红色连衣裙"时:

  1. 分词得到:"红色"、"连衣裙"
  2. 查找倒排索引:红色→[1,3],连衣裙→[1,2]
  3. 求交集:文档1同时包含两个词
  4. 计算相关性评分返回结果

写入流程详解

真实场景: 电商秒杀时,用户抢购iPhone:

# 1. 用户下单,库存-1
POST /products/_doc/iphone13
{
"stock": 99, # 从100减到99
"last_update": "2024-01-01T10:00:01"
}

# 2. 1秒后用户搜索才能看到最新库存
GET /products/_search
{
"query": {"term": {"name": "iPhone13"}}
}

三大核心操作对比

实际影响

# 场景:电商促销,大量商品更新价格

# 问题:频繁写入导致性能下降
# 解决:调整refresh间隔
PUT /products/_settings
{
"refresh_interval": "30s" # 从1秒改为30秒,提升写入性能
}

# 问题:segment文件过多,查询变慢
# 解决:手动merge
POST /products/_forcemerge?max_num_segments=1

查询性能优化策略

场景化优化示例

慢查询问题

# 问题:商品搜索很慢(5秒+)
GET /products/_search
{
"query": {
"bool": {
"must": [
{"range": {"price": {"gte": 100, "lte": 500}}},
{"match": {"description": "手机"}}
]
}
},
"from": 9000, # 深度分页问题!
"size": 20
}

# 优化方案:
# 1. 用filter替代must(可缓存)
# 2. 用search_after替代深度分页
# 3. 只返回必要字段
GET /products/_search
{
"query": {
"bool": {
"must": [{"match": {"description": "手机"}}],
"filter": [{"range": {"price": {"gte": 100, "lte": 500}}}]
}
},
"search_after": [1234567890], # 替代from/size
"size": 20,
"_source": ["name", "price", "image"] # 只返回需要的字段
}

数据倾斜处理

# 问题:双11当天数据都写入同一分片
# 原因:按日期路由导致热点

# 解决方案1:自定义路由
POST /orders/_doc/order123?routing=user_123
{
"user_id": "user_123",
"product_id": "iphone13",
"order_date": "2024-11-11"
}

# 解决方案2:索引模板 + 别名轮转
PUT /_template/orders_template
{
"index_patterns": ["orders-*"],
"settings": {
"number_of_shards": 10, # 增加分片数
"routing.allocation.total_shards_per_node": 2
}
}

大数据量优化策略

场景:电商平台商品数据从100GB增长到10TB

实施方案

# 1. 设置索引生命周期策略
PUT /_ilm/policy/products_policy
{
"policy": {
"phases": {
"hot": {
"actions": {
"rollover": {
"max_size": "50GB",
"max_age": "30d"
}
}
},
"warm": {
"min_age": "30d",
"actions": {
"allocate": {
"number_of_replicas": 1,
"include": {"box_type": "warm"}
}
}
},
"cold": {
"min_age": "90d",
"actions": {
"allocate": {
"number_of_replicas": 0,
"include": {"box_type": "cold"}
}
}
}
}
}
}

# 2. 优化JVM配置(每个节点)
# -Xms16g -Xmx16g # 不超过31GB,不超过物理内存50%
# -XX:+UseG1GC # 使用G1垃圾收集器

# 3. 集群扩容策略
# 原来:3个节点,每个32GB内存
# 现在:15个节点,每个64GB内存,专门的主节点

缓存机制详解

实际应用

# 场景:商品分类筛选,filter条件经常重复
GET /products/_search
{
"query": {
"bool": {
"filter": [
{"term": {"category": "手机"}}, # 这个filter会被缓存
{"range": {"price": {"gte": 1000}}} # 这个filter也会被缓存
],
"must": [
{"match": {"title": "iPhone"}} # 这部分不会被缓存,因为是query context
]
}
}
}

# 监控缓存效果
GET /_cat/nodes?v&h=name,query_cache.hit_count,query_cache.miss_count

这些优化策略的核心思想是:

  1. 写入优化:批量写入、调整refresh间隔、合理分片
  2. 查询优化:使用filter、避免深度分页、合理使用缓存
  3. 存储优化:冷热分离、生命周期管理、硬件配置
  4. 架构优化:专用节点、负载均衡、监控告警

记住:性能优化永远要基于实际的业务场景和监控数据,不要过度优化!