菜鸟工程师-ElasticSearch学习

385 阅读13分钟

ES的用途-搜索

现在常用的Google、Baidu,都是非常强大的搜索工具,那么是如何搜索的呢?肯定不能是在Mysql这种传统的关系型数据库。在全文搜索中,关系型数据库完全派不上用场,因此就有了ES,正常的数据库使用的是正排索引,而ES使用的是倒排索引。这一点很重要,是进行高效的全文搜索的基础。

倒排索引 vs 正排索引

资料:倒排与正排

  • 正排索引:一个未经处理的数据库中,一般是以文档ID作为索引,以文档内容作为记录。
  • 倒排索引:Inverted index,指的是将单词或记录作为索引,将文档ID作为记录,这样便可以方便地通过单词或记录查找到其所在的文档。

正排索引--Mysql

Mysql的一个数据库的一个表中,每一行数据,一般是以ID为索引,后面的数据都是根据这个ID进行查找,通过建立正排索引的方式,能很快根据索引查询到数据。例子如下,其中ID为索引。

IDDoc
1我爱学习
2学习使我快乐

倒排索引--ES

但是在全文搜索中,如果要搜索“学习”这个词,使用正排索引是非常低效的,完全没有索引可走。因此就有倒排索引这个玩意儿。倒排索引中,以内容为索引,文档ID作为数据。当然,倒排索引的建立成本比较高,也比较复杂,这个先不管,本文只是粗浅的了解ES。

DocID
1,2
学习1,2
快乐2

ES数据定义与查询

资料:ES教程

ES与Mysql的类比

ESIndexTypeDocField
MysqlDBTableRowColumn

ES 7之后的版本中,已经没有Type的概念了,一般都是Index直接跟着Doc,Mysql是根据某个Column查询一行数据,而ES则是根据某个Field查询某个Doc,查询的原理都是一样的,只是建立索引的方式不同。

形象的描述一下,在存储数据的时候,需要写一个PUT语句,如下所示,其中dynamic_mapping_test 就是索引名称,_doc是type,1就是你要存储的这个Doc(邮件、文章)的编号ID,json体内数据则是这个Doc具有的fields属性。在es 7的版本中,是不允许用户生成type的,默认都是_doc这个type。ES搜索,就是根据Fields建立索引,去搜索到对应的Doc。

PUT dynamic_mapping_test/_doc/1

{

  "name":"lbh1995",

  "age":"26",

  "sex":"男"

}

Mapping

ES的mapping其实就是对数据的定义,可以在TCE的 在线搜索ES平台 上进入Kibana,在BOE环境下,进行一些测试。

首先先找一个Index,看看该Index的mapping定义,以涡轮平台的标签作为例子,查看标签数据的mapping。

GET turbine_tag_data-2021.06.20

返回了很多数据,主要是三个部分aliases、mappings、settings。aliases是别名,一般用不到,mappings是数据的映射关系和字段定义,settings是该索引的一些设置。

{

  "turbine_tag_data-2021.06.20" : {

    "aliases" : { },

    "mappings" : {

      "dynamic" : "false",

      "properties" : {

        "object_id" : {

          "type" : "long"

        },

        "tag_id_count" : {

          "type" : "integer"

        },

        "tag_id_list" : {

          "type" : "long"

        },

        "tag_value" : {

          "type" : "keyword"

        }

      }

    },

    "settings" : {

      "index" : {

        "lifecycle" : {

          "name" : "ilm_all_2months"

        },

      ...

  }

}

Mapping的dynamic属性

一般的,mapping可以分为动态映射(dynamic mapping)、静态映射(explicit mapping)、精确映射(strict mappings),具体由dynamic属性控制。默认为动态映射,即:

"dynamic" : "true",

动态映射

动态映射,顾名思义,根据PUT进去的数据,会动态增加映射属性,如下所示。

// 建立初始索引

PUT dynamic_mapping_test

{

  "mappings": {

    "dynamic" : "true",

    "properties": {

        "name": {

          "type": "text"

        },

        "age": {

          "type": "long"

        }

      }

  }

}



// 存储数据

PUT dynamic_mapping_test/_doc/1

{

  "name":"lbh1995",

  "age":"26",

  "sex":"男"

}



// 再次查看索引

GET dynamic_mapping_test/_mapping



// mapping返回结果

{

  "dynamic_mapping_test" : {

    "mappings" : {

      "dynamic" : "true",

      "properties" : {

        "age" : {

          "type" : "long"

        },

        "name" : {

          "type" : "text"

        },

        "sex" : {

          "type" : "text",

          "fields" : {

            "keyword" : {

              "type" : "keyword",

              "ignore_above" : 256

            }

          }

        }

      }

    }

  }

}



## 发现Mapping中多了sex字段的mapping,这就是动态映射

静态映射

与动态映射不同,根据PUT进去的数据,并不会动态增加映射属性,但是数据仍然会进行存储,不过这样一来就无法根据新的字段进行搜索,即不建立索引,如下所示。

// 建立初始索引

PUT non_dynamic_mapping_test

{

  "mappings": {

    "dynamic" : "false",

    "properties": {

        "name": {

          "type": "text"

        },

        "age": {

          "type": "long"

        }

      }

  }

}



// 存储数据

PUT non_dynamic_mapping_test/_doc/1

{

  "name":"lbh1995",

  "age":"26",

  "sex":"男"

}



// 再次查看索引

GET non_dynamic_mapping_test/_mapping



// mapping返回结果

{

  "non_dynamic_mapping_test" : {

    "mappings" : {

      "dynamic" : "false",

      "properties" : {

        "age" : {

          "type" : "long"

        },

        "name" : {

          "type" : "text"

        }

      }

    }

  }

}

精确映射

精确映射是指将dynamic的值设置为strict,当存入的数据包含不存在的字段时,会报错,这边就不做具体的演示了。

Note: 在指定唯一ID的时候(_/doc/1,doc后面的这个字符串),可以在_doc中进行PUT。如果不指定,会报错,得改用POST进行存储,并随机分配一个唯一ID。

ES的字段类型

资料:ES字段类型

ES的字段类型分类非常多,我们目前最常用的就是数值和字符串,更多类型的详细信息可以在本小节的博客进行补充了解。

数值

数值类型可以分为:long、integer、short、byte、double、float、half_float、scaled_float。

  • long 取值范围:-2^63~2^63-1(与java取值范围一致)
  • integer 取值范围:-2^31~2^31-1.(与java取值范围一致)
  • short 取值范围:-32,768~32,767(与java取值范围一致)
  • byte 取值范围:-128~127(与java取值范围一致)
  • double 与java取值范围一致
  • float 与java取值范围一致

字符串

字符串由原本的string类型分出了text类型和keyword类型。为什么要分出这两种类型呢?首先需要知道,由于es主要用于全文检索,那么就有自己的分词规则。为了满足不同情况下的文本类型数据,就划分出了这二者,顾名思义,text是全文本搜索,而keyword则是关键词搜索。keyword 类型不进行分词处理。

  • text:如果是邮件信息内容等文字比较长的,则应该使用text类型
  • keyword:如果是文字内容较短的,比如地址、姓名等内容,则推荐使用keyword类型

keyword类型中有个特殊的属性,ignore_above,这是为了限制keyword建立索引的长度,如果keyword的数量超过ignore_above的上限,那么就不会建立索引。仔细思考一下,如果keyword过长,那么还能成为keyword吗?

默认的字符串类型

在新的ES版本中,默认的字符串类型是一种嵌套的类型,例如:

       "msg" : {

          "type" : "text",

          "fields" : {

            "keyword" : {

              "type" : "keyword",

              "ignore_above" : 256

            }

          }

        },

这一默认类型的意思是,这个数据,es既进行了分词处理,也保留了原始数据留在msg.keyword属性中。在使用term进行查询的时候,就得需要进行msg.keyword进行查询。但是当存储的字符串太长的时候,使用keyword查询也是查询不到的,因为超过了ignore_above的上限,只存储数据,而无法建立索引查询。同样,如果在keyword字段,使用match查询,也查询不到。

ES的查询方式

资料:ES查询

ES的查询方式花样很多,就不一一赘述,查看相关博客或者文档即可。


至此,ES的入门了解就差不多了,主要是记录了遇到的一些困惑,仅供各位参考,后续就是深入ES。


ES的性能保障

Google和Baidu存储那么多数据,如果一个类型的话题建立一个index,那也早就超出了一台机器所能存储的极限,那么,es是怎么处理的呢?首先,分布式的集群ES肯定是支持的,集群中有多个节点,节点上存储实际的ES数据。这些都是基础操作,那么ES还有什么具体的高性能高可用处理方案呢?

Settings

在查询某个索引的Mapping时,可以看到返回了三部分信息,setting是最后一部分,我们还没有详细了解,让我们分析一下之前查询得到的setting。其中有两个很重要的属性,number_of_shards和number_of_replicas。

{

  "turbine_tag_data-2021.06.20" : {

    "aliases" : { },

    "mappings" : {

      ...

    },

    "settings" : {

      "index" : {

        "lifecycle" : {

          "name" : "ilm_all_2months"

        },

        "refresh_interval" : "5s",

        "number_of_shards" : "1",

        "provided_name" : "dynamic_mapping_test",

        "creation_date" : "1624714807008",

        "unassigned" : {

          "node_left" : {

            "delayed_timeout" : "10m"

          }

        },

        "priority" : "100",

        "number_of_replicas" : "1",

        "uuid" : "KWV4Dnr5T76Y9572S30wGA",

        "version" : {

          "created" : "7010199"

        }

      }

    }

  }

}

Shards

shards是分片,前文未解决的一个问题就是,数据量太大,一个机器装不下怎么办?分片是ES给出的答案,在建立索引时,可以指定分片的数量,指定后,以后就无法进行更改了,这也好理解,分片数量改变,已经存好的数据怎么去进行分配,数据量太大,非常难处理,这个与Redis是不太一样,数量级相差太大。

当创建一个索引的时候,可以指定想要的分片的数量。每个分片本身也是一个功能完善并且独立的“索引”,这个“索引”可以被放置到集群中的任何节点上。分片很重要,主要有两方面的原因:

  • 允许水平分割 / 扩展内容容量。
  • 允许在分片之上进行分布式的、并行的操作,进而提高性能/吞吐量。

当 Elasticsearch 在索引中搜索的时候, es会发送查询到每一个属于索引的分片,然后合并每个分片的结果到一个全局的结果集。(这个听起来好像就是MapReduce的操作)

Replicas

在一个网络环境里,失败随时都可能发生,在某个分片/节点不知怎么的就处于离线状态,或者由于任何原因宕机,这种情况下,有一个故障转移机制是必须的。为此, Elasticsearch 允许用户创建分片的一份或多份拷贝,这些拷贝叫做复制分片(replicas)。注意,副本是分片的副本。不同的副本一定要在不同的机器/虚拟机,不然副本完全没有意义。

在索引创建之后,你可以在任何时候动态地改变number_of_replicas,这一点与分片不同。默认情况下,Elasticsearch 中的每个索引被分片 1 个主分片和 1 个复制分片,这意味着,集群中至少需要两个节点,这样的话每个索引总共就有 2 个分片。至于那个分片在哪个机器上,就是ES自身的算法实现的了。

总结:shards决定了一个索引最大的存储上限,replicas决定了一个索引的吞吐量(需要相应的增加硬件数量,不然单纯更多的副本并不能提高性能)和可靠性。

故障处理

其实ES出现故障时的处理方式与现有的各种分布式中间件大同小异。

如果此时有三个节点,node-1、node-2、node-3,存在一个索引,该索引具备两个分片index-s1、index-s2,每个分片有三个副本,-m代表该分片为主分片。写数据时,先写主分片,然后并行同步到其余分片。

image.png

若node-1挂了,这时候node-2或3上的分片会晋升为主分片,如果,node-1只是宕机而没有丢数据,那么node-1上的分片会变为副本,并从其余分片更新变更的数据。

image.png

ES的数据更新

数据读写--Rooting

由于每个索引进行了分片,那么es就需要为每个doc确定该存储在哪个分片上。es的默认做法是根据文档的ID值(也可以使用用户自定义值)进行hash取模进行存储,由于分片数量是固定的,因此该算法已经能够充分地将数据均分到各个分片上。

shard = hash(routing) % number_of_primary_shards

每次请求ES服务器的时候,每个节点都可以处理请求,并且根据请求的内容,转发到实际需要去完成操作的节点上,这样的一个中转节点称为协调节点。

数据读写流程

image.png

动态索引更新

资料:段合并

在了解ES如何进行动态的索引更新前,我们需要先了解一下几个概念:

  • 分片(shards):索引的全部数据
  • 段(segment):一个分片包含多个段,每个段都是一个索引
  • 提交点(commit point):一个分片包含的段信息数据
  • 删除文件(.del):对某个文档进行标记删除

数据的更新无非是增删改,ES默认每一秒都会在内存启动一个新段,并将旧的段刷盘存储,所有增删改操作都在段中完成。段被写入磁盘时,同样会增加一个提交点写入磁盘,分片根据提交点来识别属于它的段。

  • 增:新文档写入段中
  • 删:找到旧文档所在的段,在del文件中对该文档进行标记删除,暂时不进行物理删除
  • 改:同时进行增删

在搜索时,对于全部的段,都会进行查询,包括被标记删除的数据,然后汇总。但是在返回给用户之前,会删除掉已经被标记删除的数据,因此用户更新了文档之后,拿到的还是最新的数据。

但是这样有个问题,什么时候进行数据的删除呢?答案是:段合并

由于ES每过1s就新建一个段,那么这个段的数量级也会非常大,遍历每个段进行查询的开销也很大,因此ES会自动进行段合并,段合并时会将除了被标记删除外的数据进行物理拷贝,那么新的段中就不再留存旧数据,合并完后旧的段会被物理删除。

近实时更新

在ES的PUT测试时,我发现PUT完的数据马上查是查不到的,可见ES并不是实时更新的,但是在刷两次请求后,数据就有了,间隔大约是1s左右。1s的刷新频率可以称为近实时刷新了。

那么,ES是如何做到近实时刷新呢?如果每秒直接从内存刷数据到硬盘,那这个IO开销也太大了,因此,ES每秒会先将数据写入到文件系统缓存中,这样新数据对于用户已经是可见的了,后续间隔30分钟,会将数据刷盘。

Translog

由于数据只是每秒被刷到文件系统缓存中,而不是硬盘,因此是仍然有大量数据丢失的风险,translog就是为了保证在数据刷到硬盘之前,给所有的操作留个日志好进行数据恢复。translog每5秒刷一次磁盘。索引每次刷盘,translog就会进行一次清空,进入下次的迭代。

ES如何进行分词


ES了解的差不多,接下来就需要了解我们实际写代码该怎么使用ES。


ES查询SDK--Java

Scroll

资料:scroll使用

scroll顾名思义就是滚动查询,每次查询一部分数据,到java内存中进行数据处理。这里就不重复造轮子了,主要就是代码咋写,理解起来也不费劲,直接看资料吧。