ElasticSearch学习笔记

一、简介(什么是ES)

1.1 普通数据库搜索的缺点

  • 每条记录的字段的文本,可能会很长。比如说“商品描述”字段的长度,有长达数千个,甚至数万个字符,这个时候,每次都要对每条记录的所有文本进行扫描,性能很差。
  • 不能将搜索词拆分开来,尽可能去搜索更多的符合你的期望的结果。比如输入“生化机”,就搜索不出来“生化危机”

1.2 lucene和elasticsearch对比

lucene,最先进、功能最强大的搜索库,直接基于lucene开发,非常复杂,api复杂(实现一些简单的功能,写大量的java代码),需要深入理解原理(各种索引结构)。

elasticsearch,基于lucene,隐藏复杂性,提供简单易用的restful api接口,分布式集群管理,数据监控。

1.3 全文检索(倒排索引)

Elaticsearch,有全文检索的功能(倒排索引):把一句话分成各个词,查询的时候根据关键字找到相应的数据。

  • Term Index:在 term dictionary 的基础上添加了 term index 来加速检索,term index 以FST树(非常节省内存)的形式缓存在内存中。
  • Term Dictionary:词项字典,存储term->[docid]的指针在磁盘上是以分block的方式保存的,一个 block 内部利用公共前缀压缩,比如都是 Ab 开头的单词就可以把 Ab 省去。这样 term dictionary 可以比 b-tree 更节约磁盘空间。
  • Posting List:倒排列表,存储term对应的docid列表

lucence 在 term dictionary 的基础上添加了 term index 来加速检索,term index 以FST树(非常节省内存)的形式缓存在内存中。从 term index 查到对应的 term dictionary 的 block 位置之后,再去磁盘上找 term,大大减少了磁盘的 random access 次数。

二、ES核心知识点

2.1 ES的功能

(1)分布式的搜索引擎和数据分析引擎

搜索引擎:百度,网站的站内搜索,IT系统的检索
数据分析:电商网站,最近7天牙膏这种商品销量排名前10的商家有哪些;新闻网站,最近1个月访问量排名前3的新闻版块是哪些

(2)全文检索,结构化检索,数据分析

全文检索:我想搜索商品名称包含牙膏的商品,select * from products where product_name like “%牙膏%”
结构化检索:我想搜索商品分类为日化用品的商品都有哪些,select * from products where category_id=‘日化用品’
部分匹配、自动完成、搜索纠错、搜索推荐
数据分析:我们分析每一个商品分类下有多少个商品,select category_id,count(*) from products group by category_id

(3)对海量数据进行近实时的处理

分布式:ES自动可以将海量数据分散到多台服务器上去存储和检索
海量数据的处理:分布式以后,就可以采用大量的服务器去存储和检索数据,自然而然就可以实现海量数据的处理
近实时:在秒级别对数据进行搜索和分析

2.2 ES的核心概念

(1)Near Realtime(NRT):近实时,两个意思,从写入数据到数据可以被搜索到有一个小延迟(大概1秒);基于es执行搜索和分析可以达到秒级

(2)Cluster:集群,包含多个节点,每个节点属于哪个集群是通过一个配置(集群名称,默认是elasticsearch)来决定的
(3)Node:节点,集群中的一个节点,节点也有一个名称(默认是随机分配的),节点名称很重要(在执行运维管理操作的时候),默认节点会去加入一个名称为“elasticsearch”的集群,如果直接启动一堆节点,那么它们会自动组成一个elasticsearch集群,当然一个节点也可以组成一个elasticsearch集群

(4)Document:文档,es中的最小数据单元,一个document可以是一条客户数据,一条商品分类数据,一条订单数据,通常用JSON数据结构表示,每个index下的type中,都可以去存储多个document。一个document里面有多个field,每个field就是一个数据字段。

{
  "product_id": "1",
  "product_name": "高露洁牙膏",
  "category_name": "日化用品"
}

(5)Index:索引,包含一堆有相似结构的文档数据,比如可以有一个客户索引,商品分类索引,订单索引,索引有一个名称。一个index包含很多document,一个index就代表了一类类似的或者相同的document。比如说建立一个product index,商品索引,里面可能就存放了所有的商品数据,所有的商品document。
(6)Type:类型,每个索引里都可以有一个或多个type,type是index中的一个逻辑数据分类,一个type下的document,都有相同的field。

商品index,里面存放了所有的商品document。但是商品分很多种类,每个种类的document的field可能不太一样。

比如说电器商品,可能还包含一些诸如售后时间范围这样的特殊field;生鲜商品,还包含一些诸如生鲜保质期之类的特殊field。

type,日化商品type,电器商品type,生鲜商品type

日化商品type:product_id,product_name,product_desc,category_id,category_name
电器商品type:product_id,product_name,product_desc,category_id,category_name,service_period
生鲜商品type:product_id,product_name,product_desc,category_id,category_name,eat_period

ES5.x下,一个index可以创建多个type
ES6.x下,一个index只能创建一个type
ES7.x下,直接舍弃了type

(7)shard:单台机器无法存储大量数据,es可以将一个索引中的数据切分为多个shard,分布在多台服务器上存储。有了shard就可以横向扩展,存储更多数据,让搜索和分析等操作分布到多台服务器上去执行,提升吞吐量和性能。每个shard都是一个lucene index。
(8)replica:任何一个服务器随时可能故障或宕机,此时shard可能就会丢失,因此可以为每个shard创建多个replica副本。replica可以在shard故障时提供备用服务,保证数据不丢失,多个replica还可以提升搜索操作的吞吐量和性能。primary shard(建立索引时一次设置,不能修改,默认5个),replica shard(随时修改数量,默认1个),默认每个索引10个shard,5个primary shard,5个replica shard,最小的高可用配置,是2台服务器。同一个shard的primary和replica不能存在于同一个节点上。

2.3 容错机制x

(1)9 shard,3 node
(2)master选举:master node宕机,自动master选举,red
(3)replica容错:新master将replica提升为primary shard,yellow(有replica宕机)
(4)数据恢复:重启宕机node,master copy replica到该node,使用原有的shard并同步宕机后的修改,green

2.4 并发冲突问题x

冲突问题:购买商品减少商品数目的操作:读取商品document(100件) -> 缩减库存(-1) -> 更新商品document(99件)。

如果多个请求同时操作,则可能会基于旧数据来缩减,导致并发问题。

解决方案

  • 悲观锁:对单个document加锁
  • 乐观锁:先查询数据,得到version为1的数据,对数据进行操作;更新数据时,先比对数据库中的数据的version是否是最开始的version,若是,则更新数据并将version加一,若不是,则重试或失败。

ES内部基于_version进行乐观锁并发控制

ES内部会有主副本之间的同步行为,期间可能会产生顺序错乱的情况,可能先修改的后到,后修改的先到,导致先修改的数据覆盖正确数据。

因此,ES内部会首先判断该更新请求的version是否>=旧数据的version,若是,则更新;若不是,则丢弃该请求。(ES中删除数据并不是真正地删除数据,而是将其标记为只会将其标记为deleted;更新数据并不是在原document上修改,而是新创建一个document,并将version加一)

2.5 写一致性原理x

在发送任何一个增删改操作的时候,比如说put /index/type/id,都可以带上一个consistency参数,指明我们想要的写一致性是什么?

put /index/type/id?consistency=quorum
  • one:只要有一个primary shard是active活跃可用的,就可以执行
  • all:必须所有的primary shard和replica shard都是活跃的,才可以执行
  • quorum:默认值,要求所有的shard中,必须是大部分的shard都是可用的,才可以执行

quorum机制

必须确保大多数shard都可用,(primary + number_of_replicas) / 2 + 1,当number_of_replicas>1时才生效。

例:1个primary,3个replica,需要(1 + 3) / 2 + 1 = 3个shard的可用,若只有两个节点,最多只能有1个priamry和1个replica可用,导致请求无法执行。

number_of_replicas>1时才生效

es提供了一种特殊的处理场景,当number_of_replicas>1时才生效。如果只有一个primary shard,replica=1,此时就2个shard (1 + 1 / 2) + 1 = 2,要求必须有2个shard是活跃的,但是可能只有1个node,此时就1个shard是活跃的,如果不处理的话,会导致单节点集群无法工作。

quorum不齐全时,会进行等待,默认1分钟

在写操作的时候,可用设置timeout参数,put /index/type/id?timeout=30 (30表示30ms)

2.6 document查询流程

1、客户端发送请求到任意一个node,成为coordinate node
2、coordinate node对document进行路由,将请求转发到对应的node,此时会使用round-robin随机轮询算法,在primary shard以及其所有replica中随机选择一个,让读请求负载均衡
3、接收请求的node返回document给coordinate node
4、coordinate node返回document给客户端
5、特殊情况:document如果还在建立索引过程中,可能只有primary shard有数据,replica shard没有数据,此时可能会导致无法读取到document

2.7 _search查询timeout机制x

  • 返回结果 各个字段含义

    GET /_search
    
    {
      "took": 9,   # 本次搜索耗费9ms
      "timed_out": false,
      "_shards": {
        "total": 6,
        "successful": 6,
        "failed": 0
      },
      "hits": {
        "total": 10,  # 返回了几条结果
        "max_score": 1,  # 本次搜索的所有结果中,最大的相关度分数是多少
        "hits": [  # 默认查询前10条数据,_score降序排序
          {
            "_index": ".kibana",
            "_type": "config",
            "_id": "5.2.0",
            "_score": 1,
            "_source": {
              "buildNum": 14695
            }
          }
        ]
      }
    }
    
  • timeout机制

    GET /_search?timeout=1s

    例:假设有3个shard,每个shard都要耗费5s才能返回全部数据,指定timeout为1s后,会直接返回1s内查询的部分数据(每个shard都只查询1s)。

2.8 分页搜索&scroll搜索

  • 分页搜索

    size,from
    
    GET /_search?size=10
    GET /_search?size=10&from=0
    GET /_search?size=10&from=20
    
  • deep paging的弊端

    从每个shard读取数据到coordinate node,整合排序后得到一堆docid

    再向shard中读取完整的document,返回至client

    其中包含大量数据的传输,耗费性能。

  • scroll搜索

    使用scoll滚动搜索,可以先搜索一批数据,然后下次再搜索一批数据,以此类推,直到搜索出全部的数据。

    scoll搜索会在第一次搜索的时候,保存一个当时的视图快照,如果这个期间数据变更,用户无法看到

    每次发送scroll请求,我们还需要指定一个scoll参数,指定一个时间窗口,每次搜索请求只要在这个时间窗口内能完成就可以了

    # 第一次
    GET /test_index/test_type/_search?scroll=1m
    {
      "query": {
        "match_all": {}
      },
      "sort": [ "_doc" ],
      "size": 3
    }
    ----
    {
      "_scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAACx",
      "took": 5,
      "timed_out": false,
      "hits": {}
    }
    
    # 之后的搜索需要指定第一次结果中的scroll_id
    GET /_search/scroll
    {
        "scroll": "1m", 
        "scroll_id" : "DnF1ZXJ5VGhlbkZldGNoBQAAA"
    }
    

2.9 分词器

什么是分词器?

将一段句子拆分成单个的单词,同时对每个单词进行normalization(时态转换,单复数转换),提高recall(召回率,搜索的时候,增加能够搜索到的结果的数量)

  1. character filter:在一段文本进行分词之前,先进行预处理,比如说最常见的就是,过滤html标签(hello --> hello),& --> and(I&you --> I and you)
  2. tokenizer:分词,hello you and me --> hello, you, and, me
  3. token filter:lowercase,stop word,synonymom,dogs --> dog,liked --> like,Tom --> tom,a/the/an --> 扔掉,mother --> mom,small --> little

一个分词器,很重要,将一段文本进行各种处理,最后处理好的结果才会拿去建立倒排索引

内置分词器的介绍

例句:Set the shape to semi-transparent by calling set_trans(5)

  • standard analyzer:set, the, shape, to, semi, transparent, by, calling, set_trans, 5(默认的是standard)
  • simple analyzer:set, the, shape, to, semi, transparent, by, calling, set, trans
  • whitespace analyzer:Set, the, shape, to, semi-transparent, by, calling, set_trans(5)
  • language analyzer(特定的语言的分词器,比如说,english,英语分词器):set, shape, semi, transpar, call, set_tran, 5

2.10 mapping

mapping,就是index的type的元数据,每个type都有一个自己的mapping,决定了数据类型,建立倒排索引的行为,以及搜索时的行为。

1)直接插入数据后,es会自动建立索引,同时建立type以及对应的mapping

2)不同的数据类型(比如说text和date),可能有的是exact value,有的是full text

  • exact value

    建立倒排索引时,将整个值作为一个关键词建立到倒排索引中。

    2017-01-01,exact value,搜索的时候,必须输入2017-01-01,才能搜索出来;如果只输入01,是搜索不出来的

  • full text

    不是单纯匹配完整值,而是对数据分词normaliztion(时态转换,同义词转换,大小写转换)后,再建立到倒排索引中。

3)同时exact value和full text类型的field决定了搜索的行为,会跟建立倒排索引的行为保持一致;比如说exact value搜索的时候,就是直接按照整个值进行匹配,full text query string,也会进行分词和normalization再去倒排索引中去搜索

4)可以使用es的dynamic mapping,让其自动建立mapping,包括自动设置数据类型;也可以提前手动创建index和type的mapping,自己对各个field进行设置,包括数据类型、索引行为、分词器等

5)常见数据类型

text(可以指定analyzer)
keyword(extra value)
byte,short,integer,long
float,double
boolean
date

2.12 相关度评分TF&IDF算法

计算索引中的文本与搜索文本之间的关联匹配程度的算法

  • Term frequency:搜索文本中的各个词条在field文本中出现了多少次,出现次数越多,就越相关

    搜索请求:hello world
    
    doc1:hello you, and world is very good
    doc2:hello, how are you
    
    doc1出现两次词条,更相关
    
  • Inverse document frequency:搜索文本中的各个词条在整个索引的所有文档中出现了多少次,出现的次数越多,就越不相关

    搜索请求:hello world
    
    doc1:hello, today is very good
    doc2:hi world, how are you
    
    比如说,在index中有1万条document,hello这个单词在所有的document中,一共出现了1000次;world这个单词在所有的document中,一共出现了100次
    
    doc2更相关
    
  • Field-length norm:field长度,field越长,相关度越弱

    搜索请求:hello world
    
    doc1:{ "title": "hello article", "content": "babaaba 1万个单词" }
    doc2:{ "title": "my article", "content": "blablabala 1万个单词,hi world" }
    
    假设hello world在整个index中出现的次数是一样多的
    
    doc1更相关,title field更短
    

2.13 doc values正排索引

在建立索引的时候,一方面会建立倒排索引,以供搜索用;一方面会为每个field建立正排索引(除了text类型),也就是doc values,以供排序,聚合,过滤等操作使用。

doc values是被保存在磁盘上的,此时如果内存足够,os会自动将其缓存在内存中,性能还是会很高;如果内存不足够,os会将其写入磁盘上。

PUT my_index
{
  "mappings": {
      "properties": {
        "title": {
          "type": "keyword",
          "doc_values": false //若没有排序、聚合等操作可以禁用
        }
    }
  }
}

2.14 type底层数据结构

type,是一个index中用来区分相似数据的

lucene是没有type的概念,在底层的lucene中建立索引时,field的value全部是bytes类型,不区分类型

在document中,会将type作为一个document的field来存储,即_type,es通过_type来进行type的过滤和筛选
一个index中的多个type,在物理上是放在一起存储的,其他type的字段会被设为空

{
   "ecommerce": {
      "mappings": {
        "_type": {
          "type": "string",
          "index": "not_analyzed"
        },
        "name": {
          "type": "string"
        }
        "price": {
          "type": "double"
        }
        "service_period": {
          "type": "string"
        }
        "eat_period": {
          "type": "string"
        }
      }
   }
}

{
  "_type": "elactronic_goods",
  "name": "geli kongtiao",
  "price": 1999.0,
  "service_period": "one year",
  "eat_period": ""
}

{
  "_type": "fresh_goods",
  "name": "aozhou dalongxia",
  "price": 199.0,
  "service_period": "",
  "eat_period": "one week"
}

2.15 常见元字段x

文档元字段

  • _index:文档所属的索引

  • _id:document的唯一标识,与_index_type一起, 唯一标识和定位一个document

  • _type:标注document属于哪个类型。在6.0之前的版本中, 一个index可能会被划分为多个type;但在6.0之后, 一个index只能包含一个type, 否则将出现错误。

  • _source:文档原始JSON内容。该字段本身不建立索引,无法用于搜索, 但是会被存储

    该功能默认开启, 会产生额外的存储开销, 可以关闭

    PUT website/_mapping/blog
    {
        "_source": {"enabled": false}	
    }
    

    _source功能被禁止, 将造成大量功能无法使用:

    partial update 功能基于_source实现;
    hilight 高亮显示功能基于_source实现;
    reindex 重建索引功能基于_source实现, 不需要从其他外部存储中获取数据, 再index;
    基于_source定制返回field;
    调试query时更容易, 因为可以很直观地看到_source内容……

  • store:某些特殊场景下,如果你只想检索单个字段或几个字段的值,而不是整个_source的值,则可以使用源过滤来实现。

    DELETE news-000001
    PUT news-000001
    {
      "mappings": {
        "_source": {
          "enabled": false
        },
        "properties": {
          "title": {
            "type": "text",
            "store": true
          },
          "date": {
            "type": "date",
            "store": true
          },
          "content": {
            "type": "text"
          }
        }
      }
    }
    
    PUT news-000001/_doc/1
    {
      "title":   "Some short title",
      "date":    "2021-01-01",
      "content": "A very long content field..."
    }
    
    GET news-000001/_search
    {
      "stored_fields": [ "title", "date" ] 
    }
    

2.16 索引原理

2.16.1 动态更新索引

参考https://www.elastic.co/guide/cn/elasticsearch/guide/current/dynamic-indices.html

倒排索引是不可变的。

不可变有重要价值:

  • 不需要锁。不需要担心多进程同时修改数据的问题。
  • 一旦索引被读入内核的文件系统缓存,由于其不变性,大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升

但是也有不好的地方。

如果添加新数据,需要重建整个索引。这对一个索引所能包含的数据量或可被更新的频率造成了很大的限制。

**用更多的索引。**通过增加新的补充索引来保存新的修改,而不是直接重写整个倒排索引。

lucene可以按段搜索,每个索引可以分为多个段(segment),每个段都可以被当做一个小的倒排索引。

通过提交点来保存可被搜索的段。

2.16.2 近实时搜索

因为fsync的操作代价极大(舍弃),所以当段在os cache时,即可被搜索到。

refresh:每秒刷新一次,将缓冲区的数据转到一个新的段中,此时该新段位于os cache,可被搜索。

2.16.3 持久化变更

如果没有用 fsync 把数据从文件系统缓存刷(flush)到硬盘,我们不能保证数据在断电甚至是程序正常退出之后依然存在。

Elasticsearch 增加了一个 translog ,在每一次对 Elasticsearch 进行操作时均进行了日志记录。

新数据必须要首先追加写入translog,保证数据不丢失,在宕机重启后,会根据已有check point对应的段,再重新将translog中的数据读取至缓冲区。

2.16.4 数据流程

  1. 新文档首先会被添加到内存缓冲区,并且追加到translog

  1. 每秒**刷新(refresh)**一次:
  • 这些在内存缓冲区的文档被写入到一个新的段中,且没有进行 fsync 操作。
  • 这个段被打开,使其可被搜索。
  • 内存缓冲区被清空。

  1. 这个进程继续工作,更多的文档被添加到内存缓冲区和追加到事务日志

  1. 全量提交(flush): 分片每30分钟被flush,或者在 translog 太大的时候也会flush。
  • 所有在内存缓冲区的文档都被写入一个新的段。
  • 缓冲区被清空。
  • 一个提交点被写入硬盘。
  • 文件系统缓存通过 fsync 被刷新(flush)。
  • 老的 translog 被删除。

2.16.5 段合并

由于自动刷新流程每秒会创建一个新的段 ,这样会导致短时间内的段数量暴增。

Elasticsearch通过在后台进行段合并来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段。段合并的时候会将那些旧的已删除文档从文件系统中清除。被删除的文档(或被更新文档的旧版本)不会被拷贝到新的大段中。

2.17 posting list存储&查询优化

2.17.1 FOR(Frame of Reference)

posting list是有序的整型数组,这样就带来了一个很好的好处,可以通过增量编码(delta-encode)这种方式进行压缩。

比如现在有 id 列表 [73, 300, 302, 332, 343, 372],转化成每一个 id 相对于前一个 id 的增量值列表是 [73, 227, 2, 30, 11, 29]。在这个新的列表里面,所有的 id 都是小于 255 的,所以每个 id 只需要一个字节存储。

把所有的文档id分成很多个block,每个 block 正好包含 256 个文档,然后单独对每个文档进行增量编码。计算出存储这个 block 里面所有文档最多需要多少位来保存每个 id,并且把这个位数作为头信息(header)放在每个 block 的前面。

2.17.2 Roaring Bitmaps (for filter cache)

在ES中,可以使用 filters 来优化查询,filter查询只处理文档是否匹配与否,不涉及文档评分操作,查询的结果可以被缓存(filter cache)。

bitmap缺点是不管业务中实际的元素基数有多少,它占用的内存空间都恒定不变。对于稀疏数组不合适。

对于稀疏位图也有很多成熟的压缩方案,lucene 采用的就是roaring bitmaps

将 doc id 拆成高 16 位,低 16 位。对高位进行聚合 (以高位做 key,value 为有相同高位的所有低位数组),根据低位的数据量 (不同高位聚合出的低位数组长度不相同),使用不同的 container(数据结构) 存储。

  • len<4096 ArrayContainer 直接存值
  • len>=4096 BitmapContainer 使用 bitmap 存储

分界线的来源:value 的最大总数是为2^16=65536. 假设以 bitmap 方式存储需要 65536bit=8kb,而直接存值的方式,一个值 2 byte,4K 个总共需要2byte*4K=8kb。

所以当 value 总量 <4k 时,使用直接存值的方式更节省空间。

2.17.3 联合查询

1、如果查询有filter cache,那就是直接拿 filter cache 来做计算,也就是说位图来做 AND 或者 OR 的计算。

2、如果查询的 filter 没有缓存,那么就用 skip list 的方式去遍历磁盘上的 postings list。

以上是三个 posting list。我们现在需要把它们用 AND 的关系合并,得出 posting list 的交集。

首先选择最短的 posting list,逐个在另外两个 posting list 中查找看是否存在,最后得到交集的结果。

遍历的过程可以跳过一些元素,比如我们遍历到绿色的 13 的时候,就可以跳过蓝色的 3 了,因为 3 比 13 要小。

用 skip list 还会带来一个好处,还记得前面说的吗,postings list 在磁盘里面是采用 FOR 的编码方式存储的。

会把所有的文档分成很多个 block,每个 block 正好包含 256 个文档,然后单独对每个文档进行增量编码,计算出存储这个 block 里面所有文档最多需要多少位来保存每个 id,并且把这个位数作为头信息(header)放在每个 block 的前面。

因为这个 FOR 的编码是有解压缩成本的。利用 skip list,除了跳过了遍历的成本,也跳过了解压缩这些压缩过的 block 的过程,从而节省了 cpu。


hhhhh