2. Elasticsearch基本核心概念
Elasticsearch中具备一些基本的概念,做如下描述:
-
Near Realtime(NRT)
:ES是几乎是一个实时查询平台,从索引文档到可搜索文档有一点延迟,通常是1秒; -
Cluster
:集群是一个或多个节点(服务器)的集合,它们共同保存整个数据,并提供跨所有节点的联合索引和搜索功能,一般一个集群唯一的名字作标识符(默认是"elatsicsearch"),这个名称很重要,因为如果节点设置为按名称加入群集,则该节点只能是群集的一部分,千万不要在不同的环境中用相同的集群名字,否则很容易导致节点加入错误的集群; -
Node
:节点是组成集群的一个单独服务器,用于存储数据、提供集群索引和搜索功能,和集群一样,节点由名称标识,默认情况下,该名称是在启动时分配给节点的随机通用唯一标识符(UUID),当然这个名字是可以自定义的,节点名称对于管理目的非常重要,可以在其中识别网络中哪些服务器与Elasticsearch集群中的哪些节点相对应。可以通过集群的名字将节点配置进去,默认情况下,每个节点都是被设置加入到一个名字为"elatsicsearch"的集群,这就表示在网络上启动一些节点(假设它们之间可以相互发现),那么它们将会自动形成/加入到名字为elasticsearch
的单个集群。在单个群集中,可以拥有任意数量的节点。此外,如果网络上当前没有其他Elasticsearch节点正在运行,则默认情况下,启动单个节点将形成名为elasticsearch的新单节点集群。 -
Index
:索引是具有某些类似特征的文档集合,比如拥有客户数据的索引,产品目录的索引以及订单数据的索引。索引由名称标识(必须全部小写),此名称用于在对其中的文档执行索引、搜索、更新和删除操作时引用索引。在单个群集中,可以根据需要定义任意数量的索引。 -
Document
:文档(在Elasticsearch中使用JSON的形式表示)是可以编制索引的基本信息单元。例如,可以为单个客户提供文档,为单个产品提供另一个文档,为单个订单再提供另一个文档。在索引Index
中,可以根据需要存储任意数量的文档document
。请注意,尽管文档实际上驻留在索引中,但实际上必须将文档编入索引/分配给索引中的类型。 -
Shards
:索引Index
可能存储了超过单个节点的硬件限制的数据。例如,占用1TB磁盘空间的十亿个文档的单个索引可能不适合单个节点的磁盘,或者可能太慢而无法单独从单个节点提供搜索请求。为了解决这样的问题,Elasticsearch提供了将索引细分为多个称为碎片(Shards
)的功能,创建索引的时候就可以自定义碎片shards
的数量,这些碎片shards
本身都是功能齐全、可以托管到集群中任何节点的独立索引(index
)。碎片的分布方式和如何将其文档聚合回搜索请求都是完全由Elasticsearch管理的并且这些都是对用户透明的,分片(将索引分为一些碎片)最主要的原因有两个:- 允许平行拆分/模块化内容容量;
- 允许跨分片(这些碎片可能在多个节点上)分布和并行化操作,从而提高性能/吞吐量;
-
Replicas
:在网络/云端环境中,故障可能随时发生,为了避免某些碎片shards
或者节点node
由于某些原因脱机或者消失,强烈推荐使用故障转移机制。为此,Elasticsearch允许将索引的分片shards
的一个或多个副本制作成所谓的副本分片replica shards
或简称副本replicas
。副本replicas
重要的原因有两个:- 在碎片或节点挂掉时可以提供高度可用性。注意,碎片副本从来都不会被分配在源碎片所在的节点上(要挂一起挂,我还要副本干嘛( ఠൠఠ )ノ);
- 由于搜索可以在所有副本上并行执行,所以它扩展了搜索性能和吞吐量。
描述完,对ES的存储作简单说明,ES中的索引实际上是指向一个或者多个物理分片(即后续中的“碎片”
shards
)的逻辑命名空间,分片是底层的工作单元,每个分片存储了全部数据中的一部分,所以从某种意义上说,分片其实就是数据的容器,然后ES利用分片将数据分发到集群内各个地方,当集群规模扩大或者缩小时,ES会自动在各节点中迁移分片,使得数据仍然均匀分布在集群里,其实每个分片都是一个Lucene的实例,而且每个分片本身就是一个完整的搜索引擎。文档被存储和索引到分片内,但是应用程序是直接与索引交互而不是与分片进行交互的(数据被存储、索引在分片中,而索引仅仅是逻辑上的命名空间,这个命名空间由1个或多个分片组成,但应用程序只需要知道文档在哪个索引中即可)。索引内任意一个文档都归属于一个主分片,所以主分片的数目决定着索引能够保存的最大数据量,一个副本分片只是一个主分片的拷贝。副本分片作为硬件故障时保护数据不丢失的冗余备份,并为搜索和返回文档等读操作提供服务。默认情况下,索引会被分为5个主分片和1个副本分片,但是具体数据量我们可以自己配,如(也可以通过配置更改默认情况):
PUT /blogs
{
// 动态配置索引的分片数量和分片副本数量
"settings" : {
"number_of_shards" : 3,
"number_of_replicas" : 1
}
}
各个节点上的硬件资源是被分片所共享的,在一定程度上来讲,相同条件下,节点上分派的分片越少,各个分片的性能就越高,主分片的数量(排除硬件、使用场景,这个数量决定了这个索引能够存储的最大数据量,)在索引创建的时候就已经确定下来了,ES中的读操作(即搜索和返回数据)可以同时被主分片或副本分片所处理,所以当拥有越多的副本分片时,也将拥有越高的吞吐量,比如3个节点的集群上,blogs
索引将分片副本的数量调节为2,那就拥有9个分片:3个主分片和6个副本分片,这意味着可以将集群扩容到9个节点,每个节点上一个分片,相比原来3个节点时,集群搜索性能可以提升3倍!当然如果只是在相同节点数目的集群上增加更多副本并不能提高ES的性能,因为每个分片从节点上获取的资源会变少,反而会变慢,这时需要添加更多的资源提升吞吐量。
【总结】
每个索引index
可以分割为许多碎片shards
,当然这些索引可以有0个或多个副本,一旦索引有了副本,那就有了主碎片shards
和副本碎片replica shards
了。这些碎片shards
和副本replicas
的数量在索引被创建时就可以被定义出来,当然副本数量可以在索引创建后动态进行更改。虽然可以通过_shrink
和_split
接口对索引index
的碎片shards
数量进行更改,但是提前计划好碎片shards
的数量是最好的方式,也是非常重要的。默认情况下,Elasticsearch中的每个索引index
都被分了5个主碎片shards
和1个副本replica
,那就表示如果群集中至少有两个节点,则索引将包含5个主分片和另外5个副本分片(1个完整副本),每个索引总共有10个分片。
【注意】 每个Elasticsearch碎片shards
都是Lucene中的一个索引index
,对于一个Lucene索引index
所含的文档documents
数量通常都是有一个最大值,比如LUCENE-5843
,极限值是2,147,483,519 (= Integer.MAX_VALUE - 128)
,另外,可以通过_cat/shards
接口监控碎片shards
的体积。Type
属性在Elasticsearch 6.x.x
已经被弃用,所以这里不提了。
3. 基本操作
Elasticsearch提供强大支持Rest风格的API(顺便熟悉下),可以完成下面的一些操作(仅是部分):
- 检查集群
cluster
、节点node
和索引index
的运行状态和统计信息; - 管理集群、节点和索引数据以及元数据;
- 执行CRUD以及针对索引的搜索操作;
- 执行高级搜索操作,例如分页,排序,过滤,脚本编写,聚合等等;
3.1 Cluster Health
健康检查可以让给我们看到集群当前正在做的事情,正常可以通过curl
来干这事儿,当然可以使用任意工具(只要能执行HTTP/REST
请求即可),命令为curl -X GET "localhost:9200/_cat/health?v"
。使用_cat
API接口进行健康检查,比如:
C:\Users\liuwg-a\Desktop\curl-7.61.0_4-win64-mingw\curl-7.61.0-win64-mingw\bin>curl -X GET "localhost:9200/_cat/health?v"
epoch timestamp cluster status node.total node.data shards pri relo init unassign pending_tasks max_task_wait_time active_shards_percent
1535783031 14:23:51 elasticsearch green 1 1 0 0 0 0 0 0 - 100.0%
【注意】这里记录一个坑,win10的powershell自带的curl应该是阉割版的,只能进行很简单的访问,没法带参,所以还是自己去下载一个win平台下的curl,配置教程这里就不写了,注意curl
命令是对大小写敏感的!
上面可以看到,“elasticsearch”集群目前是绿色状态,节点node
总共有1个,碎片shards
0个。注意,由于我们使用的是默认群集名称elasticsearch
,并且由于Elasticsearch默认使用单播网络发现来查找同一台计算机上的其他节点,因此可能会意外启动计算机上的多个节点并拥有它们所有都加入一个集群。使用curl -X GET "localhost:9200/_cat/nodes?v"
可以查看所有节点的详细信息,我这里只有一个V8JnETy
节点信息,本地结果为:
C:\Users\liuwg-a>curl -X GET http://localhost:9200/_cat/nodes?v
ip heap.percent ram.percent cpu load_1m load_5m load_15m node.role master name
127.0.0.1 30 36 8 mdi * V8JnETy
关于health查询时的颜色说明如下:
- Green:集群
cluster
功能齐全,一切都很好; - Yellow:所有数据都可用,但尚未分配一些副本(群集功能齐全,但节点没有分配到具体合法节点上,单节点集群肯定是黄色,因为默认的副本分片没有节点分配);
- Red:某些数据由于某种原因不可用(群集只有部分功能);
【注意】当群集cluster
为红色时,它将继续提供来自可用分片的搜索请求,但您可能需要尽快修复它,因为存在未分配的分片。
3.2 索引index
3.2.1 查询所有索引indices
利用curl -X GET "localhost:9200/_cat/indices?v"
可以列出所有的索引(indexes和indices都是index的复数),本次操作结果:
C:\Users\liuwg-a>curl -X GET "localhost:9200/_cat/indices?v"
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
这表示集群中还没索引。
3.2.2 创建索引
注:从这儿开始,发现win中使用curl还是很蛋疼,直接用kibana(端口号为5601)或者postman去作下面所有的测试。
由于Elasticsearch所有操作都是搞了个REST,所以不难想到创建索引肯定是用PUT
请求,下面是创建一个名为customer
值为pretty
的索引过程(返回一个JSON结果):
C:\Users\liuwg-a>curl -X PUT http://localhost:9200/customer?pretty
// 返回的结果为
{
"acknowledged" : true,
"shards_acknowledged" : true,
"index" : "customer"
}
然后验证索引创建成功,查看一下:
C:\Users\liuwg-a>curl -X GET localhost:9200/_cat/indices?v
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
yellow open customer 3M7rfq18SeCEy8qLI3UVyQ 5 1 0 0 1.1kb 1.1kb
这时就不像刚刚空索引时查询到的返回值了,可以看到索引名为customer
,它有5个主碎片primary shards
和1个副本replica
,内部的文档数document
为0,最后健康状态居然是yellow
(所有数据都可用,但尚未分配一些副本),这是因为Elasticsearch默认在创建索引时会帮我们搞出5个主碎片和1个副本,虽然搞了1个副本,但是这个副本并没有分配到某个节点上(replica
和源index
不能在同一个节点上),但现在我只有一个V8JnETy
节点,只能等稍后另一个节点加入elasticsearch
群集才能将这个replica
分配过去,然后再看状态就会变成green
。
3.2.3 删除索引
同样依据REST的尿性,删除用Delete
,指令如下(删除名字为customer
的索引):
curl -X DELETE "localhost:9200/customer?pretty"
返回结果为:
{
"acknowledged": true
}
再次查询所有索引:
curl -X GET localhost:9200/_cat/indices?v
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
此时已经不存在索引了,说明删除成功。
3.3 文档document
3.3.1 创建文档
前面说过,index
是由无数的文档document
组成,所以完整的索引还要加一些document
,指令如下(在customer
索引中创建id
为1的文档,customer
是索引index
,_doc
是类型type
,内容是个json):
curl -X PUT "localhost:9200/customer/_doc/1?pretty" -H 'Content-Type: application/json' -d'
{
"name": "John Doe"
}
'
返回的结果:
{
"_index": "customer",
"_type": "_doc",
"_id": "1",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 0,
"_primary_term": 1
}
当然如果再次执行上面的语句,也指定id
为1,如下:
curl -X PUT "localhost:9200/customer/_doc/1?pretty" -H 'Content-Type: application/json' -d'
{
"name": "John Doe2"
}
'
那么此时John Doe2
文档对象将会取代之前的John Doe
的对象,当然在创建时id
属性是非必需的,如果不显式指定id
,那么elasticsearch将会帮我们指定一个随机之前没有用过id
:
curl -X POST "localhost:9200/customer/_doc?pretty" -H 'Content-Type: application/json' -d'
{
"name": "Jane Doe"
}
'
请注意,在上述情况下,我们使用POST动词而不是PUT,因为我们没有指定ID
值得注意的是,Elasticsearch在将文档编入索引之前不需要硬性先显式创建索引。在前面的示例中,如果客户索引事先尚未存在,则Elasticsearch将自动创建客户索引。
3.3.2 查询文档
查询customer
索引中id
为1的文档(注意REST的写法):
curl -X GET "localhost:9200/customer/_doc/1?pretty"
结果:
{
"_index": "customer",
"_type": "_doc",
"_id": "1",
"_version": 1,
"found": true,
"_source": {
"name": "John Doe"
}
}
_source
就是document
文档中的所有内容。
3.3.3 更新文档属性
当前的customer
索引下的id
为1的文档document
为:
curl -X GET http://localhost:9200/customer/_doc/1?pretty
{
"_index": "customer",
"_type": "_doc",
"_id": "1",
"_version": 6,
"found": true,
"_source": {
"name": "Jane Doe",
"age": 21
}
}
更新(将原来的age
改为22):
curl -X POST "localhost:9200/customer/_doc/1/_update?pretty" -H 'Content-Type: application/json' -d'
{
"doc": {
"age": 22
}
}
'
在更新时,必须像上面那样用doc
包起来(虽然PUT时没有包,但是elasticsearch默认是加上这个东西的),否则报400,不能将内容直接简单的写为:
{"age": 22}
再次查询:
{
"_index": "customer",
"_type": "_doc",
"_id": "1",
"_version": 8,
"found": true,
"_source": {
"name": "John Doe",
"age": 22
}
}
此外,还可以用脚本语言去更新,如:
curl -X POST "localhost:9200/customer/_doc/1/_update?pretty" -H 'Content-Type:application/json' -d'
{
"script": "ctx._source.age += 5"
}
这里应该多少有点眼熟,ctx
可以理解为当前的上下文,_source
是document
的完整内容,然后通过.age
的形式获取内部的age
属性,然后执行增加5岁的操作,再次查看结果:
{
"_index": "customer",
"_type": "_doc",
"_id": "1",
"_version": 5,
"found": true,
"_source": {
"name": "John Doe",
"age": 27
}
}
上述操作全部成功,但是都是单个操作,Elasticsearch也提供了通过一个条件更新多个document
文件的能力,暂先搁置,详情见docs-update-by-query API。
【注意】
如果使用脚本方式更新类似于name
这样的字符串变量,必须加上单引号'
,否则不能更新,必须像下面这样:
curl -X POST "localhost:9200/customer/_doc/1/_update?pretty" -H 'Content-Type:application/json' -d'
{
"script": "ctx._source.name = 'jack'"
}
3.3.4 删除文档
删除document
文档可能不去看教程现在已经可以大概猜出来,下面是删除对应索引下id
为2的document
的案例:
curl -X DELETE http://localhost:9200/customer/_doc/2?pretty
当然也可以删除指定条件的所有document
,详见_delete_by_query API,但是如果想删除某个索引下的所有文档,还不如直接将对应的索引删除。
3.3.5 验证文档是否存在
如果只是想检验一个文档是否存在,不关心文档内部的内容,可以直接使用HEAD请求方法去请求ES,比如:
curl -I HEAD "localhost:9200/customer/_doc/1"
ES不会返回具体文档内容,如果文档存在,那就返回一个200 OK的状态码,否则返回404 Not Found状态码。
4. 批处理和数据导入
上一章节基本都是围绕单个的索引或者文档进行操作,在ES中也支持批处理主要是依靠_bulk
,它提供了一种非常有效的机制,可以尽可能快地执行多个操作,并尽可能少地进行网络往返。下面是索引两个文档的命令:
curl -X POST "localhost:9200/customer/_doc/_bulk?pretty" -H 'Content-Type: application/json' -d'
{"index":{"_id":"1"}}
{"name": "John Doe" }
{"index":{"_id":"2"}}
{"name": "Jane Doe" }
'
更新id
为1的document,然后同时删除id
为2 的document:
curl -X POST "localhost:9200/customer/_doc/_bulk?pretty" -H 'Content-Type: application/json' -d'
{"update":{"_id":"1"}}
{"doc": { "name": "John Doe becomes Jane Doe" } }
{"delete":{"_id":"2"}}
'
【注意】上述包含了更新和删除2个动作,统一使用POST
方法,动作直接在json中表述,更新动作后面接了更新的内容doc
,但删除动作只需要一个_id
删除即可,不需要doc
内容,返回结果为:
{
"took": 61,
"errors": false,
"items": [
{
"update": {
"_index": "customer",
"_type": "_doc",
"_id": "1",
"_version": 7,
"result": "noop",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"status": 200
}
},
{
"delete": {
"_index": "customer",
"_type": "_doc",
"_id": "2",
"_version": 3,
"result": "deleted",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 7,
"_primary_term": 2,
"status": 200
}
}
]
}
_bulk
API在执行批量操作时不会因为某个方法执行失败而失败,而是继续执行剩下的其他方法,在返回json时,它会提供批操作中每个方法的执行结果。
官方提供了一个简单的银行账户json数据集,可以下载保存到某个文件夹,然后dos先进入到该文件目录下,夹下导入下载的数据:
curl -H "Content-Type: application/json" -XPOST "localhost:9200/bank/_doc/_bulk?pretty&refresh" --data-binary "@accounts.json"
然后一直在导入,完成后查看是否导入成功:
D:\ElasticSearch\elasticsearch-6.4.0\testdateset>curl "localhost:9200/_cat/indices?v"
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
yellow open bank oFrvmRD5TyOjRIWeb-89jg 5 1 1000 0 475.1kb 475.1kb
green open .monitoring-kibana-6-2018.09.05 IjOR5IMKSO-8q6P8gqeqhg 1 0 336 0 118.5kb 118.5kb
green open .monitoring-es-6-2018.09.05 ao1DOXomTBmutbDI-juldw 1 0 2862 115 1.3mb 1.3mb
green open .kibana y-h6y-sMTASw_hx_Ve6WNA 1 0 1 0 4kb 4kb
yellow open customer lFQka4g1QmiRUlbeicYtIA 5 1 3 0 10.9kb 10.9kb
看到bank
索引下document
的数量为1000,说明导入数据成功,这里注意如果不进入json数据集的所在目录进行导入操作将出现400错误:
Warning: Couldn't read data from file "accounts.json", this makes an empty
Warning: POST.
{
"error" : {
"root_cause" : [
{
"type" : "parse_exception",
"reason" : "request body is required"
}
],
"type" : "parse_exception",
"reason" : "request body is required"
},
"status" : 400
}
【问题】
批量导入json数据时,没有显式的制定索引名字,怎么会自动创建bank
索引的?
5. 结构化(域映射mapping)
ES在每个索引中都有一个类型的映射,通过下面的请求可以查看数据映射:
GET /shardstest/_mapping/_doc
// 结果
{
"shardstest": {
"mappings": {
"_doc": {
"properties": {
"likes": {
"type": "long"
},
"message": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"post_date": {
"type": "long"
},
"price": {
"type": "float"
},
"product": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"user": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}
}
}
错误的映射将会导致查询出现奇怪的结果,比如将likes
映射为string
类型,虽然之前往索引中存入文档都没有指定数据映射的方式,ES会根据索引中文档,为域(就是文档中各个字段或属性)动态生成映射。当然各个域的数据映射我们可以自定义(为了避免歧义),在自定义数据映射时mapping
,可以执行如下操作(仅是一部分):
- 全文字符串域和精确值字符串域的区别;
- 使用特定语言分析器(使用
analyzer
指定); - 优化域以适应部分匹配;
- 指定自定义数据格式;
每个域作数据映射时最重要的属性为type
,对于不是string
的域一般只需要设置type
即可,比如:
"likes": {
"type": "integer"
}
默认,string
类型域会被认为包含全文,它们的值在索引前,会通过一个分析器,针对于这个域的查询在搜索前也会经过一个分析器,string
域映射的两个最重要属性是index
和analyzer
。下面是创建结构化索引的栗子:
GET localhost:9200/mapping_index
{
"mappings": {
"_doc": {
"properties": {
"id": {
"type": "long"
},
"name": {
"type": "string",
"analyzer": "standard"
},
"age": {
"type": "integer"
},
"date": {
"type": "date"
}
}
}
}
}
注:在 ES 5.X+ 版本已经取消了string
类型,取而代之的是keyword
和text
两种类型。
对已经做过数据类型映射后的索引还可以根据后来的需求添加结构化映射,比如:
PUT localhost:9200/mapping_index/_mapping/_doc
{
"properties" : {
"tag" : {
"type" : "text"
}
}
}
在mapping_index
原有的域上又添加了一个tag
域。