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)- 记录单词的开始结束位置,实现高亮显示
- 倒排索引项(Posting)
- 单词词典(Term Dictionary),记录所有文档的单词,记录单词到倒排列表的关联关系
文档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 写入流程
(1)客户端向NODE1发送写请求。
(2)NODE1使用文档ID来确定文档属于分片0,通过集群状态中的内容路由表信息获知分片0的主分片位于NODE3,因此请求被转发到NODE3上。
(3)NODE3上的主分片执行写操作。如果写入成功,则它将请求并行转发到 NODE1和NODE2的副分片上,等待返回结果。当所有的副分片都报告成功,NODE3将向协调节点报告成功,协调节点再向客户端报告成功。
在客户端收到成功响应时,意味着写操作已经在主分片和所有副分片都执行完成。
1.2.4 查询流程
(1)客户端向NODE1发送读请求。
(2)NODE1使用文档ID来确定文档属于分片0,通过集群状态中的内容路由表信息获知分片0有三个副本数据,位于所有的三个节点中,此时它可以将请求发送到任意节点,这里它将请求转发到NODE2。
(3)NODE2将文档返回给 NODE1,NODE1将文档返回给客户端。
1.2.5 搜索流程
(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的文档,因为这些数据都能够被搜索到。(对象之间没有界限)
Nested Data Type
- Nested数据类型:允许对象数组中的对象被独立索引
- 使用nested和properties关键字,将所有actors索引到多个分隔的文档
- 在内部,Nested文档会被保存在两个Lucene文档中,在查询时做Join处理
如下对对象设置了“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值继续进行查询
关键点:根据提供的排序属性排序后的sort值为依据,向后继续翻页。类似于MySQL中,LIMIT 10000,30。我拿到了第9999条数据的id值,然后SELECT * FROM a WHERE id > 9999 LIMIT 30。
特别注意:SearchAfter这种特性,很显然不支持跳页,但是它也是能够实时向后翻页的,而接下来介绍的Scoll翻页方式就不支持实时。
2.4.1.3 ScollAPI
- 创建一个快照,有新的数据写入以后,无法被查到
- 每次查询后,输入上一次的Scoll Id
我理解和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"
}
}
}
}
}