1. 开篇

1.1. 概念

  • **Document**(文档)最小数据单元。
  • **Index**(索引)包含一堆有相似结构的document数据,代表了一类相似或者相同的document。
  • **Type**(分类)表示Index中的一个逻辑数据分类。每个Index可以有多个type,而每个type又可以存储多个document。

    5.X 版本一个 index 可创建多个 type6.X 版本一个 index 只能存在一个 type7.0 版本开始,移除Type概念。主要原因是在同一个索引中,存储仅有小部分字段相同或者全部字段都不相同的document,会导致数据稀疏,影响 Lucene 有效压缩数据的能力,目前在7.x中兼容Type。

  • **Field**(字段)document里面有多个field,每个field就是一个数据字段。

  • **Shard**(分片)单台机器无法存储大量数据,Elasticsearch可以将一个Index(索引)中的数据切分为多个Shard,分布在不同服务器节点上,这样就可以实现水平扩展,提升吞吐量和性能。这其实是一种数据分散集群的架构模式。

    每个Shard都是一个Lucene index,可以看成是一个Lucene实例。

  • **Replica**(副本)Shard中的数据就可能丢失,为每个Shard创建多个备份(Replica副本)。Replica可以在Shard故障时提供备用服务,保证数据不丢失,多Replica提升搜索的吞吐量和性能,主从架构模式。

    每个索引的Primary Shard数量,需要在索引建立时就设置,一旦设置完不能修改,而Replica Shard可以随时修改数量。

Elasticsearch 关系型数据库
Index 数据库实例
Type
Document
Field

1.2. 集群状况

  • **green** 每个索引的primary shard和replica shard为active状态。
  • **yellow** 每个索引的primary shard为active状态,但部分replica shard非active状态(即不可用状态)。
  • **red** 部分索引的primary shard非active状态,部分索引有数据丢失。

    GET /_cat/health?v

1.3. 索引操作

  • 查询

    GET /_cat/indices?v

  • 创建

    PUT /test_index?pretty

  • 删除

    DELETE /test_index?pretty

1.4. CURD操作

index 索引名 / type 类型名 / id document唯一标识。

  • 新增

Elasticsearch会自动建立Index和Type,同时,ES默认会对document的每个field都建立倒排索引,让其可以被搜索。

PUT /{index}/{type}/{id} { #document数据 }

  • 查询

    GET /{index}/{type}/{id}

  • 替换:使用替换方式去修改商品信息时,document的所有field都需要带上

    PUT /{index}/{type}/{id} { #document数据 }

  • 更新

    POST /{index}/{type}/{id}/_update {

    1. "doc":{
    2. #document的指定field
    3. }

    }

  • 删除

    DELEET /{index}/{type}/{id}

1.5. 数据检索

1.5.1. query string search

搜索词一般都是跟在search参数后面,以query string的http请求形式发起检索。

GET /{index}/{type}/_search Example GET /ecommerce/product/_search?q=name:yagao&sort=price:desc

# 返回结果
{
  "took" : 530,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [
      {
        ...
      }
    ]
  }
}

字段含义说明:

  • took 耗费时长(毫秒)。
  • timed_out 是否超时。
  • _shards 数据拆成的分片数,所以对于搜索请求,会打到所有的primary shard(或者是primary shard对应的某个replica shard)。
  • hits.total 查询结果的数量,3个document。
  • hits.max_score score的含义,就是document对于一个search的相关度的匹配分数,越相关,就越匹配,分数也高。
  • hits.hits 包含了匹配搜索的document的详细数据。

    query string search适用于一些临时、快速的检索请求,如果查询请求很复杂,那么query string search是不太适用的,所以在生产环境中,几乎很少使用query string search。

1.5.2. query DSL

Domain Specified Language Elasticsearch中很常用的一种检索方式。例如:查询商品名中包含yagao关键子的所有商品,并将结果按照售价排序:

GET /ecommerce/product/_search
{
    "query" : {
        "match" : {
            "name" : "yagao"
        }
    },
    "sort": [
        { "price": "desc" }
    ]
}

所有查询请求参数全部放到了一个http requstbody里面,所以可以构建复杂的查询,适合生产环境使用。

1.5.3. query filter

query filter主要用于对数据进行过滤。例如:我们要搜索商品名称包含“yagao”,且售价大于25元的商品:

GET /ecommerce/product/_search
{
    "query" : {
        "bool" : {
            "must" : {
                "match" : {
                    "name" : "yagao" 
                }
            },
            "filter" : {
                "range" : {
                    "price" : { "gt" : 25 } 
                }
            }
        }
    }
}

上述bool里面可以封装多个查询条件。

1.5.4. full-text search

**全文检索** 例如:查询producer字段中包含 producerjiajieshi

GET /ecommerce/product/_search
{
    "query" : {
        "match" : {
            "producer" : "producer jiajieshi"
        }
    }
}

match 中空格分隔 producer jiajieshi ,只要producer字段中包含了上述任意一个字符,就都会被检索到。采用全文检索时,返回的每一项数据中有一个 _score 字段,这个就是 相关度分数 的意思,分数越高,则越接近检索词。

1.5.5. phrase search

**短语搜索** 与全文检索相反。输入的关键字必须在出现指定的文本中,一模一样才匹配。例如:查询producer字段中包含 jiajieshi producer

GET /ecommerce/product/_search
{
    "query" : {
        "match_phrase" : {
            "producer" : "jiajieshi producer"
        }
    }
}

1.5.6. highlight search

**高亮搜索** 举个例子,我们希望检索name字段包含 yaogao 的所有商品,然后对检索结果中的 yaogao 文本进行高亮:

GET /ecommerce/product/_search
{
    "query" : {
        "match" : {
            "name" : "yagao"
        }
    },
    "highlight": {
        "fields" : {
            "name" : {}
        }
    }
}

匹配项的结果里多了个“highlight”字段,里面用高亮了匹配的文本:

{
  "took" : 97,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.13353139,
    "hits" : [
      {
        "_index" : "ecommerce",
        "_type" : "product",
        "_id" : "1",
        "_score" : 0.13353139,
        "_source" : {
          "name" : "gaolujie yagao",
          "desc" : "gaoxiao meibai",
          "price" : 30,
          "producer" : "gaolujie producer",
          "tags" : [
            "meibai",
            "fangzhu"
          ]
        },
       "highlight" : {
          "name" : [
            "gaolujie <em>yagao</em>"
          ]
        }
      }
    ]
  }
}

2. 架构

2.1. 负载均衡

3节点集群创建索引,共有3个primary shard,每个primary shard都有2个replica shard,那总共就是9个shard:

PUT /test_index
{
   "settings" : {
      "number_of_shards" : 3,
      "number_of_replicas" : 2
   }
}

Elasticsearch(上) - 图1

  • P1 / P2 / P3:test_index索引的数据会被均匀分布到3个primary shard。
  • R-:每个primary shard都有一个replica shard,replica shard其实就是个数据备份,类似于msater/slave模式中的slave节点。6个replica shard也会被负载均衡到各个ES进程节点上。

    Elasticsearch有一个原则:primary shard和它对应的replica shard不能全部署在同一个节点上,否则节点挂了的话,数据和拷贝都会丢失。

2.2. 高可用

Elasticsearch保证集群高可用的方式采用主从模式+选举的方式。
Elasticsearch(上) - 图2Elasticsearch(上) - 图3Elasticsearch(上) - 图4
节点1宕机了,对于P1这个primary shard为非active状态,此时集群状态转为red。但是节点2和节点3上仍然保存着完整的数据,P1的两个副本R1-1和R1-2开始一轮选举,胜出者为primary shard,假设R1-1胜出。
当宕机的节点1恢复后,shard又会重新加入到集群中,原来P1发现节点2上有新的primary shard,自身变成repilca shard,并进行数据同步。

2.3. 水平扩展

如上图为原始集群,空间占满后2倍/3倍扩容效果如下:
Elasticsearch(上) - 图5Elasticsearch(上) - 图6

2.4. 并发控制

2.4.1. _version

Elasticsearch内部采用了版本号机制对document的并发修改进行控制。所谓版本号,本质是一种乐观锁。Elasticsearch写入document数据时,ES会自动为document生成一个版本号_version。首次创建document时,_version版本号为1,每次对这个document修改或删除时,_version版本号会自动加1。
执行流程:

  1. 假设我们现在有两个线程A和B,同时读取到了商品信息,此时获取到的document的_version版本号是相同的,假设都是1。
  2. A线程在本地将库存减1后,发送PUT更新请求,请求里带上了版本号PUT /product/storage/1?version=1。
  3. ES收到请求后,对数据进行更新,然后将版本号+1。
  4. B线程在本地将库存减1后,发送PUT更新请求,请求里也带上了版本号1,PUT /product/storage/1?version=1。
  5. ES收到请求后,发现id为1的document的版本号已经是2了,而B线程的更新请求里带的版本号还是1,两者不一样,于是拒绝更新,返回报错。

    2.4.2. external version

    基于自身维护的一个版本号来进行并发控制。
  • 内部版本语法

    ?version=1

  • 自定义版本号语法

    ?version=1&version_type=external

当version_type=external的时,只有当提供的version比Elasticsearch中的_version大的时候,才能完成修改。

2.5. 路由原理

Elasticsearch就是采用了hash路由算法,对document记录的id标识进行计算,产生了一个shard序号,通过这个shard序号就可以立即确认写到哪个shard里面,路由算法shard = hash(routing_key) % number_of_primary_shards。也可以手动指定document的routing_key值,那么routing_key相同的document就会路由到同一个shard中。

2.5.1. 写入

Example:初始3个primary shard,每个primary shard都有一个副本。初始时,客户端(集成了Elasticsearch Client SDK)发起了一条document的写入请求,请求可能hit到任意某个ES节点上,hit到的这个节点也叫做coordinate node(协调节点)
Elasticsearch(上) - 图7

每个ES节点其实都知道集群中的其它节点的信息,包括集群中一共有多少primary/replica shard,每个节点上分配着哪些primary/replica shard。

  1. 假设ES进程2节点(协调节点)接受到了请求,于是根据document的id进行hash计算,发现结果是3,也就是应该由P3这个primary shard处理这个请求,所以就会把请求转发给ES进程3节点上的P3。

Elasticsearch(上) - 图8

  1. primary shard 3处理完请求后,会将数据同步到自己的replica shard(R3-1),同步完后响应ES进程2,ES进程2(协调节点)收到响应后,返回给ES client结果。

Elasticsearch(上) - 图9

Elasticsearch对于写请求,最终都是转交给primary shard去处理的。

2.5.2. 读取

document数据查询的原理基本和写入类似,查询请求既可以由primary shard处理,也可以由replica shard处理,提升系统的吞吐量和性能。coordinate node(协调节点)在接受到查询请求后,会采用round-robin算法,在对应的primary shard及其所有replica中随机选择一个发送请求,以达到读请求负载均衡的目的。
Example:客户端发起查询某个document的请求,假设命中到ES进程2,ES进程2根据document Id计算出应该由primary shard 3来处理,primary shard 3有一个replica,所以协调节点会采用round-robin算法选取其中一个转发请求,比如选择了R3-1,然后将请求转发给它,R3-1查询得到结果后返回,最终ES进程2将结果返回给客户端。
Elasticsearch(上) - 图10

2.6. 数据一致性

Elasticsearch其实提供了三种数据同步机制:one,all,quorum,在请求时带上consistency参数,默认是quorum。

PUT /index/type/id?consistency=quorum

  • one:对于document的写操作(增删改),只要有一个primary shard是active活跃可用的,操作就可以执行。
  • all:对于document的写操作(增删改),要求必须所有的primary shard和replica shard都是活跃的,才可以执行这个写操作。
  • quorum:对于document的写操作(增删改),写之前必须确保大多数shard都可用,当不满足“大多数”这个条件时,请求就会默认等待1分钟,超过时间就会报timeout错误。我们可以在写操作的时候,加一个timeout参数。

    PUT /index/type/id?timeout=30

Elasticsearch是通过一个公式去计算“大多数”shard。

(primary + number_of_replicas) / 2 + 1

2.7. 数据持久化

**Segment File**
在Elasticsearch中创建一个index索引时,需要指定shard分片,一个shard分片在底层其实是一个Lucene索引,它由若干个segment文件和对应的commit point(提交点文件)构成。
Elasticsearch(上) - 图11
Segment文件可以理解成底层存储document数据的文件,ES进行检索时最终是从它里面检索出数据,而commit point文件可以理解成一个保存了若干segment信息的列表,它标识着这个commit point前所有旧的segment file文件。
Example:当Elasticsearch创建commit point文件时,会已有一些segment文件是已经存在的,那commit point就保存着这些旧的segment文件的信息。
Elasticsearch(上) - 图12

写入流程:一条document数据被写入磁盘会经历以下几个过程。

1.write > 2.refresh > 3.flush > 4.merge

**Write**
document会被写入in-memory buffer中,所谓in-memory buffer其实就是应用内存。同时,document数据会被写入translog日志文件。
Elasticsearch(上) - 图13
**Refresh**
每隔1秒Elasticsearch会执行一次refresh操作,将buffer中的数据refresh到filesystem cache中的一个新segment file中。filesystem cache等价os cache。注意:此时的segment file仅仅是存在于os cache中的缓存数据,并不存在于磁盘上。
Elasticsearch(上) - 图14
refresh操作完成后,buffer会被清空。如果buffer满了也会执行refresh。当数据进入filesystem cache后可以被检索到。

Elasticsearch是准实时(NRT,near real-time),默认每隔1秒refresh一次,写入的数据1秒之后才能被检索到。通过index的index.refresh_interval参数配置refresh的时间间隔。

**Flush**
segment file一直存在于os cache中,如果发生宕机cache数据会丢失,提供一种机制将缓存数据写入磁盘文件。在refresh的过程中,os cache中的segment file会越来越多(每次refresh都会创建一个新的segment file)。当translog到达阈值时触发flush操作。

  1. flush的第一步,将buffer中的现有数据refresh到os cache中,清空buffer。
  2. 在磁盘上写入一个commit point文件,里面标识着这个commit point前os cache中的所有旧segment file文件数据。
  3. 强行将os cache中的所有数据fsync到磁盘文件中去。
  4. 最后将translog清空,因为此时里面记录的数据此时都已经被fsync到磁盘了。

Elasticsearch(上) - 图15

默认每隔30分钟会自动执行一次flush操作,如果translog过大会提前触发。

在执行flush操作之前,数据要么是停留在buffer中,要么是停留在os cache中,此时一旦这台机器宕机,数据就全丢了。所以,需要将数据写入到一个专门的日志文件中,此时即使机器宕机了,再次重启时,Elasticsearch会自动读取translog日志文件中的数据,恢复到内存buffer和os cache中去。translog中的数据,本身也是先写入os cache,然后默认每隔5秒刷一次到磁盘中。所以默认情况下,即使有translog保证可用性,也可能丢失5s的数据。此时数据仅仅停留在buffer或者translog文件的os cache中,如果此时机器挂了,会丢失这5秒钟的数据。
Elasticsearch(上) - 图16

保证数据不丢失设置index的index.translog.durability参数,每次写入一条数据都写入buffer,同时fsync写入磁盘上translog文件,写性能、吞吐量严重下降。

**Merge**
每refresh一次会在os cache中产生一个新的segment file,随着segment file增多,搜索的性能会降低。Elasticsearch会定期执行merge操作,每次merge时,ES会选择一些相似大小的segment进行合并,同时会将那些标识为deleted的document物理删除掉。合并后新的segment file会被写入磁盘,同时会新建commit point文件,里面标识着所有合并后新的segment file。

2.8. 分词器

对文本内容进行一些特定处理,根据处理后的结果再建立倒排索引,主要的处理过程一般如下:

  1. character filter 符号过滤,比如hello过滤成hello,I&you过滤成I and you ;
  2. tokenizer 分词,比如,将hello you and me切分成hello、you、and、me;
  3. token filter 比如,dogs替换为dog,liked替换为like,Tom 替换为 tom,small 替换为 little等等。

内置了以下几种分词器 standard analyzer simple analyzer whitespace analyzer language analyzer
我们可以通过以下命令,看下分词器的分词效果:

GET /{index}/_analyze
{
  "analyzer": "standard", 
  "text": "a dog is in the house"
}
# 采用standard分词器对text进行分词

分词器的各种替换行为,也叫做normalization,本质是为了提升命中率,官方叫做recall召回率。

对于document中的不同字段类型,采用不同的分词器进行处理,比如 date 类型压根就不会分词,检索时完全匹配,而对于 text 类型会进行分词处理。

2.8.1. 定制分词器

修改分词器的默认行为。比如,我们修改my_index索引的分词器,启用english停用词:

PUT /my_index
{
  "settings": {
    "analysis": {
      "analyzer": {
        "es_std": {
          "type": "standard",
          "stopwords": "_english_"
        }
      }
    }
  }
}

查看分词效果:

GET /my_index/_analyze
{
  "analyzer": "es_std", 
  "text": "a dog is in the house"
}

2.8.2. IK分词器

安装:GitHub上下载预编译好的IK包,与ES版本一致。解压缩放置到 YOUR_ES_ROOT/plugins/ik/ 目录下,重启Elasticsearch。 https://github.com/medcl/elasticsearch-analysis-ik/releases。

IK和Elasticsearch主要的版本对照:

IK version ES version
master 7.x -> master
6.x 6.x
5.x 5.x

IK分词器有两种analyzer ik_max_word ik_smart 但是一般是选用 ik_max_word
ik_max_word 会将文本做最细粒度的拆分,比如会将“中华人民共和国国歌”拆分为“ 中华人民共和国 中华人民 中华 华人 人民共和国 人民 共和国 共和 国国 国歌 ”等等,会穷尽各种可能的组合。
ik_smart 只做最粗粒度的拆分,比如会将“中华人民共和国国歌”拆分为“ 中华人民共和国 国歌 ”。
IK分词器的分词效果,先将改变指定字段的mapping:

PUT /my_index 
{
  "mappings": {
      "properties": {
        "text": {
          "type": "text",
          "analyzer": "ik_max_word"
        }
      }
  }
}

分词效果:

GET /my_index/_analyze
{
  "text": "美专家称疫情在美国还未达到顶峰",
  "analyzer": "ik_max_word"
}

**配置文件**
IK的配置文件存在于 YOUR_ES_ROOT/plugins/ik/config 目录下,目录下各个文件作用:
main.dic IK原生内置的中文词库,总共有27万多条,只要是这些单词,都会被分在一起;
quantifier.dic 放了一些单位相关的词;
suffix.dic 放了一些后缀;
surname.dic 中国的姓氏;
stopword.dic 英文停用词。
如果自定义词库,可以修改 IKAnalyzer.cfg.xmlext_dict ,配置我们扩展的词库,重启 ES 生效。
**热更新词库**
目前有两种方案,业界一般采用第一种:

  1. 修改IK分词器源码,然后每隔一定时间,自动从MySQL中加载新的词库。
  2. 基于IK分词器原生支持的热更新方案:部署一个web服务器,提供一个http接口,通过 modified 和 tag 两个 http 响应头,来提供词语的热更新。