Elasticsearch内容汇总[持续更新]

一、Elasticsearch技术简介

Elastic本身也是一个分布式存储系统,如同其他分布式系统一样,我们经常关注的一些特性如下。

  • 数据可靠性:通过分片副本和事务日志机制保障数据安全
  • 服务可用性:在可用性和一致性的取舍方面,默认情况下Elastic更倾向于可用性,只要主分片可用即可执行写入操作
  • 一致性:弱一致性。只要主分片写成功,数据就可能被读取。因此读取操作在主分片和副本分片上可能会得到不同的结果
  • 原子性:索引的读写、别名更新是原子操作,不会出现中间状态。但Bulk不是原子操作。不能用来实现事务
  • 扩展性:主副本分片都可以承担读请求,分担系统负载

1.1 Elasticsearch与MySQL的关系

RDBMS Elasticsearch
Table Index(Type)
Row Document
Column Field
Schema Mapping
SQL DSL

1.1.1 Mapping

索引结构

1.1.2 DSL

查询语句

1.1.3 倒排索引

正排索引: 文档ID -> 文档内容
倒排索引:文档内容 -> 文档ID

文档ID 文档内容
1 Mastering Elasticsearch
2 Elasticsearch Server
3 Elasticsearch Essentials
Term Count DocumentId:Position
Elasticsearch 3 1:1,2:0,3:0
Mastering 1 1:0
Server 1 2:1
Essentials 1 3:1

倒排索引的核心组成

  • 倒排索引包含两个部分
    • 单词词典(Term Dictionary),记录所有文档的单词,记录单词到倒排列表的关联关系
      • 单词词典一般比较大,可以通过B+树或哈希拉链法实现,以满足高性能的插入与查询
    • 倒排列表(Posting List)记录了单词对应的文档结合,由倒排索引项组成
      • 倒排索引项(Posting)
        • 文档ID
        • 词频TF - 该单词在文档中出现的次数,用于相关性评分
        • 位置(Position)- 单词在文档中分词的位置。用于语句搜索(phrase query)
        • 便宜(Offset)- 记录单词的开始结束位置,实现高亮显示
文档ID 文档内容
1 Mastering Elasticsearch
2 Elasticsearch Server
3 Elasticsearch Essentials

Posting List

DocId TF Position Offset
1 1 1 <10,23>
2 1 0 <0,13>
3 1 0 <0,13>
  • Elasticsearch的JSON文档中的每个字段,都有自己的倒排索引
  • 可以指定对某些字段不做索引
    • 优点:节省存储空间
    • 缺点:字段无法被搜索

1.1.4 Lucene字典数据结构FST

常见的词典数据结构:

名称 特点
排序列表Array/List 使用二分法查找,不平衡
HashMap/TreeMap 性能高,内存消耗大,几乎是原始数组的三倍
Skip List 跳跃表,可快速查找词语,在Lucene、Redis、HBase等均有实现。相对于TreeMap等结构,特别适合高并发场景
Trie 适合英文词典,如果系统中存在大量字符串且这些字符串基本没有公共前缀,则相应的trie树将非常消耗内存
Double Array Trie 适合做中文词典,内存占用小,很多分词工具均采用此算法
Ternary Search Tree 三叉树,每一个node有3个节点,兼具省空间和查询快的优点
Finite State Transducers(FST) 一种有限状态机,Luncene 4有开源实现,并大量使用

FST数据结构:
插入单词“cat”、”deep“、”do“、”dog“、”dogs“

1.2 Elasticsearch基本概念

高可用高扩展的分布式搜索引擎——Elasticsearch

1.2.1 节点

节点即一个Elasticsearch的实例,本质上就是一个Java进程,一台机器上可以运行多个Elasticsearch进程,但是生产环境一般建议一台机器上只运行一个Elasticsearch实例

Master-eligible节点和Master节点

  • 每个节点启动后默认就是一个Master-eligible节点,该类型节点可以参加选主流程,成为Master节点。
  • 当第一个节点启动的时候,它会将自己选举成Master节点
  • 每个节点都保存了集群的状态,但是只有Master节点可以修改集群的状态信息(如所有节点的信息、所有的索引以及其相关的Mapping、Setting信息、分片路由信息等)

Data节点和Coordinating节点

  • Data节点

    • 可以保存数据的节点,叫做Data Node。负责保存分片数据。在数据扩展上起到了至关重要的作用。
  • Coordinating节点

    • 负责接收Client的请求,将请求分发到合适的节点,最终把结果汇集到一起
    • 每个节点默认都起到了Coordinating Node的职责
  • Ingest 节点

    • 数据前置处理转换节点,支持pipeline管道设置
    • 可以使用ingest节点对数据进行过滤、转换等操作
    • 每个节点默认都起到了该职责,即在文档进入索引前做预处理

Hot节点和Warm节点

不同硬件配置的Data Node,用来实现Hot & Warm 架构,降低集群部署成本

分片

又称为主分片,用以解决数据水平扩展问题。通过主分片,可以将数据分布到集群内的所有节点之上。

  • 一个分片是一个运行Lucene的实例
  • 主分片数在索引创建时指定,后续不允许修改,除非Reindex

副本

用以解决数据高可用问题,是主分片的拷贝。

  • 副本分片数可以动态调整
  • 增加副本数,可以在一定程度上提高服务的可用性(读取的吞吐)

1.2.2 水平扩展

1.2.3 写入流程

write
(1)客户端向NODE1发送写请求。
(2)NODE1使用文档ID来确定文档属于分片0,通过集群状态中的内容路由表信息获知分片0的主分片位于NODE3,因此请求被转发到NODE3上。
(3)NODE3上的主分片执行写操作。如果写入成功,则它将请求并行转发到 NODE1和NODE2的副分片上,等待返回结果。当所有的副分片都报告成功,NODE3将向协调节点报告成功,协调节点再向客户端报告成功。

在客户端收到成功响应时,意味着写操作已经在主分片和所有副分片都执行完成。
写入底层原理
官网translog配置说明

1.2.4 查询流程

get流程

(1)客户端向NODE1发送读请求。
(2)NODE1使用文档ID来确定文档属于分片0,通过集群状态中的内容路由表信息获知分片0有三个副本数据,位于所有的三个节点中,此时它可以将请求发送到任意节点,这里它将请求转发到NODE2。
(3)NODE2将文档返回给 NODE1,NODE1将文档返回给客户端。

1.2.5 搜索流程

search
(1)客户端发送search请求到NODE 3。
(2)NODE 3将查询请求转发到索引的每个主分片或副分片中。
(3)每个分片在本地执行查询,并使用本地的Term/Document Frequency信息进行打分,添加结果到大小为from + size的本地有序优先队列中。
(4)每个分片返回各自优先队列中所有文档的ID和排序值给协调节点,协调节点合并这些值到自己的优先队列中,产生一个全局排序后的列表。

1.2.6 动态索引

动态索引

  • 新增加字段
    • Dynamic 设置为true时,一旦有新增字段的文档写入,Mapping也同时被更新
    • Dynamic 设置为false,Mapping不会被更新,新增字段的数据无法被索引,但是信息会出现在_source中
    • Dynamic 设置为Strict,文档写入失败
  • 对已有字段,一旦已经有数据写入,就不再支持修改字段定义
    • Lucene实现的倒排索引,一旦生成后,就不允许修改
  • 如果希望改变字段类型,使用Reindex API,重建索引
    • 因为如果修改了字段的数据类型,会导致已被索引的数据无法被搜索
    • 如果是增加新的字段,就不会有这样的影响

1.2.7 数据建模

1.2.7.1 Elasticsearch中处理关联关系

  • 对象类型
  • 嵌套对象(Nested Object)
  • 父子关联关系(Parent / Child)
  • 应用端关联
对象类型

因为Elasticsearch会把JSON打平(扁平式键值对结构),所以能够搜索到名称为”John“,年龄为31的文档,因为这些数据都能够被搜索到。(对象之间没有界限)
Object关联关系

Nested Data Type
  • Nested数据类型:允许对象数组中的对象被独立索引
  • 使用nested和properties关键字,将所有actors索引到多个分隔的文档
  • 在内部,Nested文档会被保存在两个Lucene文档中,在查询时做Join处理

如下对对象设置了“nested”类型,则不再能够搜索到”不正确的数据”了。
Nested

父子关联关系
  • 对象和Nested对象的局限性
    • 每次更新,需要重新索引整个对象(包括根对象和嵌套对象)
  • ES提供了类似关系型数据库中Join的实现。使用Join数据类型实现,可以通过维护Parent/Child的关系,从而分离两个对象
    • 父文档和子文档是两个独立的文档
    • 更新父文档无需重新索引子文档。子文档被添加,更新或者删除也不会影响到父文档和其他的子文档
    • 父文档和子文档必须在相同的分片上 -> 确保查询join的性能
    • 当指定子文档的时候,必须指定它的父文档Id -> 使用route参数保证分配到相同分片上

父子文档

Nested Object Parent / Child
优点 文档存储在一起,读取性能高 父子文档可以独立更新
缺点 更新嵌套的子文档时,需要更新整个文档 需要额外的内存维护关系。读取性能相对差
适用场景 子文档偶尔更新,以查询为主 子文档更新频繁

2.4 优化手段

2.4.1 深度分页

2.4.1.1 FROM+SIZE

这种分页方式,当FROM+SIZE > 10000的时候,Elasticsearch会报错,因为这里它有个默认分页窗口设置(当然也可以修改,一般不建议修改)。

  • ES天生就是分布式的。查询信息的时候需要从多个分片(多台机器)上拉取数据,并且ES天生就需要满足排序的需要(按照相关性算分)
  • 当一个查询: From = 990, Size = 10
    • 会在每个分片上都获取1000个文档。然后,通过Coordinating Node聚合所有结果。最后再通过排序选取前1000个文档
    • 页数越深,占用内存越多。为了避免深度分页带来的内存开销。ES有一个设定,默认限定到10000个文档。
      • Index.max.result.window

特别注意:如果你的查询没有指定from,size的话ES默认会限制为from,size=0,10。
提示:from是指偏移量,不是第几页,与MySQL的limit后的两个参数一样。(我就脑瓜子疼了很久,刚开始一直把from当页码。。)

2.4.1.2 SearchAfter

  • 避免深度分页的性能问题,可以实时获取下一页文档信息
    • 不支持指定偏移量
    • 只能继续向后偏移翻页
  • 第一步搜索需要指定sort,并且保证值是唯一的(可以通过加入_id保证唯一性)
  • 然后使用上一次查询的结果集中,最后一个文档的sort值继续进行查询
    SearchAfter
    关键点:根据提供的排序属性排序后的sort值为依据,向后继续翻页。类似于MySQL中,LIMIT 10000,30。我拿到了第9999条数据的id值,然后SELECT * FROM a WHERE id > 9999 LIMIT 30。
    特别注意:SearchAfter这种特性,很显然不支持跳页,但是它也是能够实时向后翻页的,而接下来介绍的Scoll翻页方式就不支持实时。

2.4.1.3 ScollAPI

  • 创建一个快照,有新的数据写入以后,无法被查到
  • 每次查询后,输入上一次的Scoll Id

ScollAPI
我理解和SearchAfter类似,一个是通过传递上一次的排序值,一个是通过传递上一次的Scoll值。不同的是,SearchAfter是实时的,而Scoll方式对翻页过程中有数据变更是无感知的。

分页总结

一般不建议深度分页,尽可能让业务增加时间范围,减少搜索范围,或者说直接使用另外两种,滚动分页即可。需要注意的是后两者对数据变化的感知是不一样的,具体需要根据场景来选择分页方式。
思考:Scoll是快照,即那一瞬间的快照,所以翻页是在快照中自己玩,对数据的变化无感知了,那么如果数据量很大,会不会把内存玩脱。

代码片段

PUT nested_index
{
  "mappings": {
    "properties": {
      "actors" : {
        "type" : "nested",
        "properties": {
          "first_name" : {"type" : "keyword"},
          "last_name" : {"type" : "keyword"}
        }
      },
      "title": {
        "type" : "text",
        "fields" : {"keyword": {"type" : "keyword","ignore_above":256}}
      }
    }
  }
}
PUT nested_index/_doc/1
{
  "title": "Speed",
  "actors": [
    {"first_name": "Keanu","last_name": "Reeves"},
    {"first_name": "Dennis","last_name": "Hopper"}
  ]
}
POST nested_index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "title": "Speed"
          }
        },
        {
          "nested": {
            "path": "actors",
            "query": {
              "bool": {
                "must": [
                  {"match": {"actors.first_name": "Keanu"}},
                  {"match": {"actors.last_name": "Hopper"}}
                ]
              }
            }
          }
        }
      ]
    }
  }
}


#指定父子关系,父亲为博客“blog”,子为评论“comment”
PUT parent_child_index
{
  "mappings": {
    "properties": {
      "blog_comments_relation": {
        "type" : "join",
        "relations": { "blog": "comment"}
      },
      "content": {"type":"text"},
      "title":{"type": "keyword"}
    }
  }
}
#索引父文档,确认自身身份为“blog”->父文档
PUT parent_child_index/_doc/blog1
{
  "title" : "Learning Elasticsearch",
  "content": "hello Elasticsearch",
  "blog_comments_relation": {"name":"blog"}
}
#索引子文档,引用父文档。
PUT parent_child_index/_doc/comment1?routing=blog1
{
  "comment": "I am learning ELK",
  "username": "Jack",
  "blog_comments_relation": {"name":"comment","parent":"blog1"}
}

PUT parent_child_index/_doc/blog2
{
  "title" : "Learning Elasticsearch",
  "content": "hello Elasticsearch",
  "blog_comments_relation": {"name":"blog"}
}
PUT parent_child_index/_doc/comment2?routing=blog2
{
  "comment": "I am learning ELK too",
  "username": "Bob",
  "blog_comments_relation": {"name":"comment","parent":"blog2"}
}

#查询所有文档
POST parent_child_index/_search
{}

#根据Parent Id查询
POST parent_child_index/_search
{
  "query": {
    "parent_id":{
      "type": "comment",
      "id": "blog2"
    }
  }
}
# Has Child查询,返回父文档
POST parent_child_index/_search
{
  "query": {
    "has_child": {
      "type": "comment",
      "query": {
        "match": {
          "username": "Jack"
        }
      }
    }
  }
}
# Has Parent查询,返回相关子文档
POST parent_child_index/_search
{
  "query": {
    "has_parent": {
      "parent_type": "blog",
      "query": {
        "match": {
          "title": "Learning Elasticsearch"
        }
      }
    }
  }
}
posted @ 2021-07-08 10:00  程序员deepz  阅读(163)  评论(0编辑  收藏  举报