Elasticsearch
一、Elasticsearch概述
1.1 Elasticsearch的概念
The Elastic Stack
, 包括Elasticsearch
、Kibana
、Beats
和Logstash
(也称为ELK Stack
)。- 能够安全可靠地获取任何来源、任何格式的数据,然后实时地对数据进行搜索、分析和可视化。
Elaticsearch
,简称为ES
,ES
是一个开源的高扩展的分布式全文搜索引擎,是整个Elastic Stack
技术栈的核心。- 它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理
PB
级别的数据。
1.2 全文搜索引擎
- 搜索的数据对象是大量的非结构化的文本数据。
- 文件记录量达到数十万或数百万个甚至更多。
- 支持大量基于交互式文本的查询。
- 需求非常灵活的全文搜索查询。
- 对高度相关的搜索结果的有特殊需求,但是没有可用的关系数据库可以满足。
- 对不同记录类型、非文本数据操作或安全事务处理的需求相对较少的情况。
1.3 Elasticsearch And Solr
Lucene
是Apache
软件基金会Jakarta
项目组的一个子项目,提供了一个简单却强大的
应用程式接口,能够做全文索引和搜寻。在Java
开发环境里Lucene
是一个成熟的免费开源
工具。就其本身而言,Lucene
是当前以及最近几年最受欢迎的免费Java
信息检索程序库。
但Lucene
只是一个提供全文搜索功能类库的核心工具包,而真正使用它还需要一个完善的
服务框架搭建起来进行应用。- 目前市面上流行的搜索引擎软件,主流的就两款:
Elasticsearch
和Solr
,这两款都是基
于Lucene
搭建的,可以独立部署启动的搜索引擎服务软件。由于内核相同,所以两者除了
服务器安装、部署、管理、集群以外,对于数据的操作 修改、添加、保存、查询等等都十
分类似。 - 在使用过程中,一般都会将
Elasticsearch
和Solr
这两个软件对比,然后进行选型。这两
个搜索引擎都是流行的,先进的的开源搜索引擎。它们都是围绕核心底层搜索库 -Lucene
构建的 - 但它们又是不同的。
1.4 Elasticsearch Or Solr
Google
搜索趋势结果表明,与Solr
相比,Elasticsearch
具有很大的吸引力,但这并不
意味着Apache Solr
已经死亡。虽然有些人可能不这么认为,但Solr
仍然是最受欢迎的
搜索引擎之一,拥有强大的社区和开源支持。- 与
Solr
相比,Elasticsearch
易于安装且非常轻巧。此外,你可以在几分钟内安装并运行
Elasticsearch
。但是,如果Elasticsearch
管理不当,这种易于部署和使用可能会成为一个
问题。基于JSON
的配置很简单,但如果要为文件中的每个配置指定注释,那么它不适
合您。总的来说,如果你的应用使用的是JSON
,那么Elasticsearch
是一个更好的选择。
否则,请使用Solr
,因为它的schema.xml
和solrconfig.xml
都有很好的文档记录。 Solr
拥有更大,更成熟的用户,开发者和贡献者社区。ES
虽拥有的规模较小但活跃的
用户社区以及不断增长的贡献者社区。Solr
贡献者和提交者来自许多不同的组织,而Elasticsearch
提交者来自单个公司。Solr
更成熟,但ES
增长迅速,更稳定。Solr
是一个非常有据可查的产品,具有清晰的示例和API
用例场景。Elasticsearch
的 文档组织良好,但它缺乏好的示例和清晰的配置说明。
二、Elasticsearch入门
2.1 Elasticsearch安装
-
项目 含义 bin
可执行脚本目录 config
配置目录 jdk
内置 JDK
目录lib
类库 logs
日志目录 modules
模块目录 plugins
插件目录 -
解压后,进入
bin
文件目录,点击elasticsearch.bat
文件启动ES
服务. -
注意:9300 端口为
Elasticsearch
集群间组件的通信端口,9200 端口为浏览器访问的http
协议RESTful
端口. -
打开浏览器,输入地址:
http://localhost:9200
.
-
问题解决:
-
Elasticsearch
是使用java
开发的,且7.8 版本的ES
需要JDK
版本 1.8 以上,默认安装
包带有jdk
环境,如果系统配置JAVA_HOME
,那么使用系统默认的JDK
,如果没有配
置使用自带的JDK
,一般建议使用系统配置的JDK
。 -
双击启动窗口闪退,通过路径访问追踪错误,如果是“空间不足”,请修改
config/jvm.options
配置文件。-Xms1g -Xmx1g
-
2.2 Elasticsearch基本操作
2.2.1 RESTful
2.2.2 客户端安装
2.2.3 数据格式
-
Elasticsearch
是面向文档型数据库,一条数据在这里就是一个文档。为了方便大家理解,
我们将Elasticsearch
里存储文档数据和关系型数据库MySQL
存储数据的概念进行一个类比.
-
ES
里的Index
可以看做一个库,而Types
相当于表,Documents
则相当于表的行。这里Types
的概念已经被逐渐弱化,Elasticsearch 6.X
中,一个index
下已经只能包含一个type
,Elasticsearch 7.X
中,Type
的概念已经被删除了. -
用
JSON
作为文档序列化的格式,比如一条用户信息:{ "name" : "John", "sex" : "Male", "age" : 25, "birthDate": "1990/05/01", "about" : "I love to go rock climbing", "interests": [ "sports", "music" ] }
2.2.4 HTTP 操作
2.2.4.1 索引操作
-
创建索引:
- 对比关系型数据库,创建索引就等同于创建数据库.
- 向
ES
服务器发PUT
请求 :http://127.0.0.1:9200/shopping
{ "acknowledged"【响应结果】: true, # true 操作成功 "shards_acknowledged"【分片结果】: true, # 分片操作成功 "index"【索引名称】: "shopping" } # 注意:创建索引库的分片数默认 1 片,在 7.0.0 之前的 Elasticsearch 版本中,默认 5 片
- 如果重复添加索引,会返回错误信息:
{ "error": { "root_cause": [ { "type": "resource_already_exists_exception", "reason": "index [shopping/qkWzguBIRWaHfmt7onvGBw] already exists", "index_uuid": "qkWzguBIRWaHfmt7onvGBw", "index": "shopping" } ], "type": "resource_already_exists_exception", "reason": "index [shopping/qkWzguBIRWaHfmt7onvGBw] already exists", "index_uuid": "qkWzguBIRWaHfmt7onvGBw", "index": "shopping" }, "status": 400 }
-
查看所有索引:
- 向
ES
服务器发GET
请求 :http://127.0.0.1:9200/_cat/indices?v
- 这里请求路径中的
_cat
表示查看的意思,indices
表示索引,所以整体含义就是查看当前ES
服务器中的所有索引,就好像MySQL
中的show tables
的感觉.health status index uuid pri rep docs.count docs.deleted store.size pri.store.size yellow open shopping qkWzguBIRWaHfmt7onvGBw 1 1 0 0 208b 208b
表头 含义 health
当前服务器健康状态: green
(集群完整)yellow
(单点正常、集群不完整)red
(单点不正常)status
索引打开、关闭状态 index
索引名 uuid
索引统一编号 pri
主分片数量 rep
副本数量 docs.count
可用文档数量 docs.deleted
文档删除状态(逻辑删除) store.size
主分片和副分片整体占空间大小 pri.store.size
主分片占空间大小 status
索引打开、关闭状态 - 向
-
查看单个索引:
- 向
ES
服务器发GET
请求 :http://127.0.0.1:9200/shopping
{ "shopping"【索引名】: { "aliases"【别名】: {}, "mappings"【映射】: {}, "settings"【设置】: { "index"【设置 - 索引】: { "creation_date" 【设置 - 索引 - 创建时间】: "1621614032041", "number_of_shards" 【设置 - 索引 - 主分片数量】: "1", "number_of_replicas"【设置 - 索引 - 副分片数量】: "1", "uuid" 【设置 - 索引 - 唯一标识】: "qkWzguBIRWaHfmt7onvGBw", "version" 【设置 - 索引 - 版本】: { "created": "7080099" }, "provided_name" 【设置 - 索引 - 名称】: "shopping" } } } }
- 向
-
删除索引:
- 向
ES
服务器发DELETE
请求 :http://127.0.0.1:9200/shopping
{ "acknowledged": true }
- 重新访问索引时,服务器返回响应:索引不存在:
{ "error": { "root_cause": [ { "type": "index_not_found_exception", "reason": "no such index [shopping]", "resource.type": "index_or_alias", "resource.id": "shopping", "index_uuid": "_na_", "index": "shopping" } ], "type": "index_not_found_exception", "reason": "no such index [shopping]", "resource.type": "index_or_alias", "resource.id": "shopping", "index_uuid": "_na_", "index": "shopping" }, "status": 404 }
- 向
2.2.4.2 文档操作
-
创建文档:
-
文档可以类比为关系型数据库中的表数据,添加的数据格式为
JSON
格式. -
向
ES
服务器发POST
请求 :http://127.0.0.1:9200/shopping/_doc
. -
请求体内容:
{ "title":"小米手机", "category":"小米", "images":"http://www.canying.com/xm.jpg", "price":3999.00 }
-
此处发送请求的方式必须为
POST
,不能是PUT
,否则会发生错误.{ "_index" 【索引】: "shopping", "_type"【类型-文档】: "_doc", "_id"【唯一标识】: "fuYDknkBsRtOxSyWV-RE",#可以类比为MySQL中的主键,随机生成 "_version"【版本】: 1, "result"【结果】: "created", #这里的 create 表示创建成功 "_shards"【分片】: { "total"【分片 - 总数】: 2, "successful"【分片 - 成功】: 1, "failed"【分片 - 失败】: 0 }, "_seq_no": 0, "_primary_term": 1 }
-
上面的数据创建后,由于没有指定数据唯一性标识(
ID
),默认情况下,ES
服务器会随机
生成一个. -
如果想要自定义唯一性标识,需要在创建时指定:
http://127.0.0.1:9200/shopping/_doc/1
{ "_index": "shopping", "_type": "_doc", "_id": "1", "_version": 1, "result": "created", "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 1, "_primary_term": 1 }
-
如果增加数据时明确数据主键,那么请求方式也可以为
PUT
.
-
-
查看文档:
- 查看文档时,需要指明文档的唯一性标识,类似于
MySQL
中数据的主键查询. - 向
ES
服务器发GET
请求 :http://127.0.0.1:9200/shopping/_doc/1
{ "_index" 【索引】: "shopping", "_type"【文档类型】: "_doc", "_id": "1", "_version": 2, "_seq_no": 2, "_primary_term": 1, "found"【查询结果】: true, # true 表示查找到,false 表示未查找到 "_source"【文档源信息】: { "title": "小米手机", "category": "小米", "images": "http://www.canying.com/xm.jpg", "price": 3999.00 } }
- 查看文档时,需要指明文档的唯一性标识,类似于
-
修改文档:
- 和新增文档一样,输入相同的
URL
地址请求,如果请求体变化,会将原有的数据内容覆盖. - 向
ES
服务器发POST
请求 :http://127.0.0.1:9200/shopping/_doc/1
- 请求体内容:
{ "title":"小米手机", "category":"小米", "images":"http://www.canying.com/xm.jpg", "price":4999.00 }
- 修改成功后,服务器响应结果:
{ "_index": "shopping", "_type": "_doc", "_id": "1", "_version"【版本】: 3, "result"【结果】: "updated", # updated 表示数据被更新 "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 3, "_primary_term": 1 }
- 和新增文档一样,输入相同的
-
修改字段:
-
修改数据时,也可以只修改某一给条数据的局部信息.
-
向
ES
服务器发POST
请求 :http://127.0.0.1:9200/shopping/_update/1
-
请求体内容:
{ "doc": { "price":3000.00 } }
-
修改成功后,服务器响应结果:
{ "_index": "shopping", "_type": "_doc", "_id": "1", "_version": 4, "result": "updated", "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 4, "_primary_term": 1 }
-
根据唯一性标识,查询文档数据,文档数据已经更新:
{ "_index": "shopping", "_type": "_doc", "_id": "1", "_version": 4, "_seq_no": 4, "_primary_term": 1, "found": true, "_source": { "title": "小米手机", "category": "小米", "images": "http://www.canying.com/xm.jpg", "price": 3000.0 } }
-
-
删除文档:
-
删除一个文档不会立即从磁盘上移除,它只是被标记成已删除(逻辑删除).
-
向
ES
服务器发DELETE
请求 :http://127.0.0.1:9200/shopping/_doc/1
-
删除成功,服务器响应结果:
{ "_index": "shopping", "_type": "_doc", "_id": "1", "_version"【版本】: 5, #对数据的操作,都会更新版本 "result"【结果】: "deleted", # deleted 表示数据被标记为删除 "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 5, "_primary_term": 1 }
-
删除后再查询当前文档信息:
{ "_index": "shopping", "_type": "_doc", "_id": "1", "found": false }
-
如果删除一个并不存在的文档:
{ "_index": "shopping", "_type": "_doc", "_id": "2", "_version": 1, "result"【结果】: "not_found", # not_found 表示未查找到 "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 6, "_primary_term": 1 }
-
-
条件删除文档:
- 一般删除数据都是根据文档的唯一性标识进行删除,实际操作时,也可以根据条件对多条数
据进行删除. - 分别增加多条数据:
{ "title":"小米手机", "category":"小米", "images":"http://www.canying.com/xm.jpg", "price":4000.00 } { "title":"华为手机", "category":"华为", "images":"http://www.canying.com/hw.jpg", "price":4000.00 }
- 向
ES
服务器发POST
请求 :http://127.0.0.1:9200/shopping/_delete_by_query
- 请求体内容:
{ "query":{ "match":{ "price":4000.00 } } }
- 删除成功后,服务器响应结果:
{ "took"【耗时】: 1019, "timed_out"【是否超时】: false, "total""【总数】: 2, "deleted"【删除数量】: 2, "batches": 1, "version_conflicts": 0, "noops": 0, "retries": { "bulk": 0, "search": 0 }, "throttled_millis": 0, "requests_per_second": -1.0, "throttled_until_millis": 0, "failures": [] }
- 一般删除数据都是根据文档的唯一性标识进行删除,实际操作时,也可以根据条件对多条数
2.2.4.3 映射操作
-
创建映射:
- 向
ES
服务器发PUT
请求 :http://127.0.0.1:9200/student/_mapping
- 请求体内容:
{ "properties": { "name":{ "type": "text", "index": true }, "sex":{ "type": "text", "index": false }, "age":{ "type": "long", "index": false } } }
- 服务器响应结果:
{ "acknowledged": true }
- 映射数据:
- 字段名:任意填写,下面指定许多属性,例如:
title
、subtitle
、images
、price
. type
:类型,Elasticsearch
中支持的数据类型非常丰富:-
String
类型:text
:可分词.keyword
:不可分词,数据会作为完整字段进行匹配.
-
Numerical
:数值类型,分两类:- 基本数据类型:
long
、integer
、short
、byte
、double
、float
、half_float
. - 浮点数的高精度类型:
scaled_float
.
- 基本数据类型:
-
Date
:日期类型. -
Array
:数组类型. -
Object
:对象.
-
index
:是否索引,默认为true
,也就是说你不进行任何配置,所有字段都会被索引:true
:字段会被索引,则可以用来进行搜索.false
:字段不会被索引,不能用来搜索.
store
:是否将数据进行独立存储,默认为false
:- 原始的文本会存储在
_source
里面,默认情况下其他提取出来的字段都不是独立存储的,是从_source
里面提取出来的。当然你也可以独立的存储某个字段,只要设置"store": true
即可,获取独立存储的字段要比从_source
中解析快得多,但是也会占用更多的空间,所以要根据实际业务需求来设置.
- 原始的文本会存储在
analyzer
:分词器,这里的ik_max_word
即使用ik
分词器.
- 字段名:任意填写,下面指定许多属性,例如:
- 向
-
查看映射:
- 向
ES
服务器发GET
请求 :http://127.0.0.1:9200/student/_mapping
- 服务器响应结果:
{ "student": { "mappings": { "properties": { "age": { "type": "long", "index": false }, "name": { "type": "text" }, "sex": { "type": "text", "index": false } } } } }
- 向
-
索引映射关联:
-
向
ES
服务器发PUT
请求 :http://127.0.0.1:9200/student1
{ "settings": {}, "mappings": { "properties": { "name":{ "type": "text", "index": true }, "sex":{ "type": "text", "index": false }, "age":{ "type": "long", "index": false } } } }
-
结果:
{ "acknowledged": true, "shards_acknowledged": true, "index": "student1" }
-
2.2.4.4 高级查询
-
定义数据:
# http://127.0.0.1:9200/student/_doc/1001 { "name":"zhangsan", "nickname":"zhangsan", "sex":"男", "age":30 } # http://127.0.0.1:9200/student/_doc/1002 { "name":"lisi", "nickname":"lisi", "sex":"男", "age":20 } # http://127.0.0.1:9200/student/_doc/1003 { "name":"wangwu", "nickname":"wangwu", "sex":"女", "age":40 } # http://127.0.0.1:9200/student/_doc/1004 { "name":"zhangsan1", "nickname":"zhangsan1", "sex":"女", "age":50 } # http://127.0.0.1:9200/student/_doc/1005 { "name":"zhangsan2", "nickname":"zhangsan2", "sex":"女", "age":30 }
-
查询所有文档:
- 向
ES
服务器发GET
请求 :http://127.0.0.1:9200/student/_search
{ "query": { "match_all": {} } } # "query":这里的 query 代表一个查询对象,里面可以有不同的查询属性 # "match_all":查询类型,例如:match_all(代表查询所有), match,term , range 等等 # {查询条件}:查询条件会根据类型的不同,写法也有差异
- 服务器响应结果:
{ "took" 【查询花费时间,单位毫秒】: 5, "timed_out"【是否超时】: false, "_shards": { "total"【总数】: 1, "successful"【成功】: 1, "skipped"【忽略】: 0, "failed"【失败】: 0 }, "hits"【搜索命中结果】: { "total"【搜索条件匹配的文档总数】: { "value"【总命中计数的值】: 6, "relation"【计数规则】: "eq" # eq 表示计数准确, gte 表示计数不准确 }, "max_score"【匹配度分值】: 1.0, "hits": [ ... ... ] } }
- 向
-
匹配查询:
-
match
匹配类型查询,会把查询条件进行分词,然后进行查询,多个词条之间是or
的关系. -
向
ES
服务器发GET
请求 :http://127.0.0.1:9200/student/_search
{ "query": { "match": { "name":"zhangsan" } } }
-
服务器响应结果:
{ "took": 4, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 1, "relation": "eq" }, "max_score": 1.0296195, "hits": [ { "_index": "student", "_type": "_doc", "_id": "1001", "_score": 1.0296195, "_source": { "name": "zhangsan", "nickname": "zhangsan", "sex": "男", "age": 30 } } ] } }
-
-
字段匹配查询:
multi_match
与match
类似,不同的是它可以在多个字段中查询.- 向
ES
服务器发GET
请求 :http://127.0.0.1:9200/student/_search
{ "query": { "multi_match": { "query": "zhangsan", "fields": ["name","nickname"] } } }
- 服务器响应结果:
{ "took": 29, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 1, "relation": "eq" }, "max_score": 1.0296195, "hits": [ { "_index": "student", "_type": "_doc", "_id": "1001", "_score": 1.0296195, "_source": { "name": "zhangsan", "nickname": "zhangsan", "sex": "男", "age": 30 } } ] } }
-
关键字精确查询:
term
查询,精确的关键词匹配查询,不对查询条件进行分词.- 向
ES
服务器发GET
请求 :http://127.0.0.1:9200/student/_search
{ "query": { "term": { "name": { "value": "zhangsan" } } } }
-
多关键字精确查询:
terms
查询和term
查询一样,但它允许你指定多值进行匹配.- 如果这个字段包含了指定值中的任何一个值,那么这个文档满足条件,类似于
mysql
的in
. - 向
ES
服务器发GET
请求 :http://127.0.0.1:9200/student/_search
{ "query": { "terms": { "name": ["zhangsan","lisi"] } } }
-
指定查询字段:
- 默认情况下,
Elasticsearch
在搜索的结果中,会把文档中保存在_source
的所有字段都返回. - 如果我们只想获取其中的部分字段,我们可以添加
_source
的过滤. - 向
ES
服务器发GET
请求 :http://127.0.0.1:9200/student/_search
{ "_source": ["name","nickname"], "query": { "terms": { "nickname": ["zhangsan"] } } }
- 默认情况下,
-
过滤字段:
includes
:来指定想要显示的字段.excludes
:来指定不想要显示的字段.- 向
ES
服务器发GET
请求 :http://127.0.0.1:9200/student/_search
{ "_source": { "includes": ["name","nickname"] }, "query": { "terms": { "nickname": ["zhangsan"] } } }
-
组合查询:
bool
把各种其它查询通过must
(必须 )、must_not
(必须不)、should
(应该)的方
式进行组合.- 向
ES
服务器发GET
请求 :http://127.0.0.1:9200/student/_search
.{ "query": { "bool": { "must": [ { "match": { "name": "zhangsan" } } ], "must_not": [ { "match": { "age": "40" } } ], "should": [ { "match": { "sex": "男" } } ] } } }
-
范围查询:
-
range
查询找出那些落在指定区间内的数字或者时间.
-
向
ES
服务器发GET
请求 :http://127.0.0.1:9200/student/_search
{ "query": { "range": { "age": { "gte": 30, "lte": 35 } } } }
-
-
模糊查询:
- 返回包含与搜索字词相似的字词的文档.
- 编辑距离是将一个术语转换为另一个术语所需的一个字符更改的次数.
- 更改字符(
box
→fox
) - 删除字符(
black
→lack
) - 插入字符(
sic
→sick
) - 转置两个相邻字符(
act
→cat
)
- 更改字符(
fuzzy
查询会在指定的编辑距离内创建一组搜索词的所有可能的变体或扩展。然后查询返回每个扩展的完全匹配.- 通过
fuzziness
修改编辑距离。一般使用默认值AUTO
,根据术语的长度生成编辑距离. - 向
ES
服务器发GET
请求 :`http://127.0.0.1:9200/student/_search{ "query": { "fuzzy": { "title": { "value": "zhangsan" } } } }
{ "query": { "fuzzy": { "title": { "value": "zhangsan", "fuzziness": 2 } } } }
-
单字段排序:
sort
可以让我们按照不同的字段进行排序,并且通过order
指定排序的方式。desc
降序,asc
升序.- 向
ES
服务器发GET
请求 :http://127.0.0.1:9200/student/_search
bash { "query":{ "match":{ "name":"zhangsan" } }, "sort":[ { "age":{ "order":"desc" } } ] }
-
多字段排序:
- 使用
age
和_score
进行查询,并且匹配的结果首先按照年龄排序,然后
按照相关性得分排序. - 向
ES
服务器发GET
请求 :http://127.0.0.1:9200/student/_search
{ "query": { "match_all": {} }, "sort": [ { "age": { "order": "desc" } }, { "_score":{ "order": "desc" } } ] }
- 使用
-
高亮查询:
- 在进行关键字搜索时,搜索出的内容中的关键字会显示不同的颜色,称之为高亮.
Elasticsearch
可以对查询内容中的关键字部分,进行标签和样式(高亮)的设置.- 在使用
match
查询的同时,加上一个highlight
属性:pre_tags
:前置标签.post_tags
:后置标签.fields
:需要高亮的字段.title
:这里声明title
字段需要高亮,后面可以为这个字段设置特有配置,也可以空.
- 向
ES
服务器发GET
请求 :http://127.0.0.1:9200/student/_search
{ "query": { "match": { "name": "zhangsan" } }, "highlight": { "pre_tags": "<font color='red'>", "post_tags": "</font>", "fields": { "name": {} } } }
-
分页查询:
from
:当前页的起始索引,默认从 0 开始。from = (pageNum - 1) * size
size
:每页显示多少条.- 向
ES
服务器发GET
请求 :http://127.0.0.1:9200/student/_search
{ "query": { "match_all": {} }, "sort": [ { "age": { "order": "desc" } } ], "from": 0, "size": 2 }
-
聚合查询:
- 聚合允许使用者对
es
文档进行统计分析,类似与关系型数据库中的group by
,当然还有很
多其他的聚合,例如取最大值、平均值等等. - 对某个字段取最大值
max
:- 向
ES
服务器发GET
请求 :http://127.0.0.1:9200/student/_search
{ "aggs":{ "max_age":{ "max":{"field":"age"} } }, "size":0 }
- 向
- 对某个字段取最小值
min
:- 向
ES
服务器发GET
请求 :http://127.0.0.1:9200/student/_search
{ "aggs":{ "min_age":{ "min":{"field":"age"} } }, "size":0 }
- 向
- 对某个字段求和
sum
:- 向
ES
服务器发GET
请求 :http://127.0.0.1:9200/student/_search
{ "aggs":{ "sum_age":{ "sum":{"field":"age"} } }, "size":0 }
- 向
- 对某个字段取平均值
avg
:- 向
ES
服务器发GET
请求 :http://127.0.0.1:9200/student/_search
{ "aggs":{ "avg_age":{ "avg":{"field":"age"} } }, "size":0 }
- 向
- 对某个字段的值进行去重之后再取总数:
- 向
ES
服务器发GET
请求 :http://127.0.0.1:9200/student/_search
{ "aggs":{ "distinct_age":{ "cardinality":{"field":"age"} } }, "size":0 }
- 向
State
聚合:stats
聚合,对某个字段一次性返回count
,max
,min
,avg
和sum
五个指标.- 向
ES
服务器发GET
请求 :http://127.0.0.1:9200/student/_search
{ "aggs":{ "stats_age":{ "stats":{"field":"age"} } }, "size":0 }
- 聚合允许使用者对
-
桶聚合查询
-
桶聚和相当于
sql
中的group by
语句. -
terms
聚合,分组统计:- 向
ES
服务器发GET
请求 :http://127.0.0.1:9200/student/_search
{ "aggs":{ "age_groupby":{ "terms":{"field":"age"} } }, "size":0 }
- 向
-
在
terms
分组下再进行聚合:- 向
ES
服务器发GET
请求 :http://127.0.0.1:9200/student/_search
{ "aggs":{ "age_groupby":{ "terms":{"field":"age"}, "aggs":{ "sum_age":{ "sum":{"field":"age"} } }, } }, "size":0 }
- 向
-
三、Elasticsearch 环境
3.1 相关概念
3.1.1 单机 & 集群
- 单台
Elasticsearch
服务器提供服务,往往都有最大的负载能力,超过这个阈值,服务器
性能就会大大降低甚至不可用,所以生产环境中,一般都是运行在指定服务器集群中. - 单点服务器也存在其他问题:
- 单台机器存储容量有限.
- 单服务器容易出现单点故障,无法实现高可用.
- 单服务的并发处理能力有限.
- 配置服务器集群时,集群中节点数量没有限制,大于等于 2 个节点就可以看做是集群了.
- 一般出于高性能及高可用方面来考虑集群中节点数量都是 3 个以上。
3.1.2 集群 Cluster
- 一个集群就是由一个或多个服务器节点组织在一起,共同持有整个的数据,并一起提供索引和搜索功能.
- 一个
Elasticsearch
集群有一个唯一的名字标识,这个名字默认就是”elasticsearch”
. - 这个名字是重要的,因为一个节点只能通过指定某个集群的名字,来加入这个集群。
3.1.3 节点 Node
- 集群中包含很多服务器,一个节点就是其中的一个服务器。作为集群的一部分,它存储
数据,参与集群的索引和搜索功能。 - 一个节点也是由一个名字来标识的,默认情况下,这个名字是一个随机的漫威漫画角色的名字,这个名字会在启动的时候赋予节点。这个名字对于管理工作来说挺重要的,因为在这个管理过程中,你会去确定网络中的哪些服务器对应于
Elasticsearch
集群中的哪些节点。 - 一个节点可以通过配置集群名称的方式来加入一个指定的集群。默认情况下,每个节点都会被安排加入到一个叫做“elasticsearch”的集群中,这意味着,如果你在你的网络中启动了若干个节点,并假定它们能够相互发现彼此,它们将会自动地形成并加入到一个叫做
“elasticsearch”
的集群中。 - 在一个集群里,只要你想,可以拥有任意多个节点。而且,如果当前你的网络中没有运行任何
Elasticsearch
节点,这时启动一个节点,会默认创建并加入一个叫做“elasticsearch”
的集群。
3.2 Windows 集群
3.2.1 部署集群
- 创建
elasticsearch-cluster
文件夹,在内部复制三个elasticsearch
服务.
- 修改集群文件目录中每个节点的
config/elasticsearch.yml
配置文件:-
node-1001
节点:#节点 1 的配置信息: #集群名称,节点之间要保持一致 cluster.name: my-elasticsearch #节点名称,集群内要唯一 node.name: node-1001 node.master: true node.data: true #ip 地址 network.host: localhost #http 端口 http.port: 1001 #tcp 监听端口 transport.tcp.port: 9301 #discovery.seed_hosts: ["localhost:9301", "localhost:9302","localhost:9303"] #discovery.zen.fd.ping_timeout: 1m #discovery.zen.fd.ping_retries: 5 #集群内的可以被选为主节点的节点列表 #cluster.initial_master_nodes: ["node-1", "node-2","node-3"] #跨域配置 #action.destructive_requires_name: true http.cors.enabled: true http.cors.allow-origin: "*"
-
node-1002
节点:#节点 2 的配置信息: #集群名称,节点之间要保持一致 cluster.name: my-elasticsearch #节点名称,集群内要唯一 node.name: node-1002 node.master: true node.data: true #ip 地址 network.host: localhost #http 端口 http.port: 1002 #tcp 监听端口 transport.tcp.port: 9302 discovery.seed_hosts: ["localhost:9301"] discovery.zen.fd.ping_timeout: 1m discovery.zen.fd.ping_retries: 5 #集群内的可以被选为主节点的节点列表 #cluster.initial_master_nodes: ["node-1", "node-2","node-3"] #跨域配置 #action.destructive_requires_name: true http.cors.enabled: true http.cors.allow-origin: "*"
-
node-1003
节点:#节点 3 的配置信息: #集群名称,节点之间要保持一致 cluster.name: my-elasticsearch #节点名称,集群内要唯一 node.name: node-1003 node.master: true node.data: true #ip 地址 network.host: localhost #http 端口 http.port: 1003 #tcp 监听端口 transport.tcp.port: 9303 #候选主节点的地址,在开启服务后可以被选为主节点 discovery.seed_hosts: ["localhost:9301", "localhost:9302"] discovery.zen.fd.ping_timeout: 1m discovery.zen.fd.ping_retries: 5 #集群内的可以被选为主节点的节点列表 #cluster.initial_master_nodes: ["node-1", "node-2","node-3"] #跨域配置 #action.destructive_requires_name: true http.cors.enabled: true http.cors.allow-origin: "*"
-
3.2.2 启动集群
- 启动前先删除每个节点中的
data
目录中所有内容(如果存在) - 分别双击执行
bin/elasticsearch.bat
, 启动节点服务器,启动后,会自动加入指定名称的
集群.
3.2.3 测试集群
- 查看集群状态
http://127.0.0.1:1001/_cluster/health
3.3 Linux 单机
-
[root@iz2vc3otfvqqqhbzad3955z es]# wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.8.0-linux-x86_64.tar.gz
-
解压:
[root@iz2vc3otfvqqqhbzad3955z es]# tar -zxvf elasticsearch-7.8.0-linux-x86_64.tar.gz [root@iz2vc3otfvqqqhbzad3955z es]# mv elasticsearch-7.8.0 es-1001
-
创建用户:
- 因为安全问题,
Elasticsearch
不允许root
用户直接运行,所以要创建新用户,在root
用户中创建新用户:[root@iz2vc3otfvqqqhbzad3955z es-1001]# useradd es #新增 es 用户 [root@iz2vc3otfvqqqhbzad3955z es-1001]# passwd es #为es用户设置密码 userdel -r es #如果错了,可以删除再加 [root@iz2vc3otfvqqqhbzad3955z es-1001]# chown -R es:es /usr/tool/es #文件夹所有者
- 因为安全问题,
-
修改配置文件:
[root@iz2vc3otfvqqqhbzad3955z config]# vim /usr/tool/es/es-1001/config/elasticsearch.yml # 加入如下配置 cluster.name: elasticsearch node.name: node-1 network.host: 0.0.0.0 http.port: 9200 cluster.initial_master_nodes: ["node-1"] [root@iz2vc3otfvqqqhbzad3955z config]# vim /etc/security/limits.conf # 在文件末尾中增加下面内容 # 每个进程可以打开的文件数的限制 es soft nofile 65536 es hard nofile 65536 [root@iz2vc3otfvqqqhbzad3955z config]# vim /etc/security/limits.d/20-nproc.conf # 在文件末尾中增加下面内容 # 每个进程可以打开的文件数的限制 es soft nofile 65536 es hard nofile 65536 # 操作系统级别对每个用户创建的进程数的限制 * hard nproc 4096 # 注:* 带表 Linux 所有用户名称 [root@iz2vc3otfvqqqhbzad3955z config]# vim /etc/sysctl.conf # 在文件中增加下面内容 # 一个进程可以拥有的 VMA(虚拟内存区域)的数量,默认值为 65536 vm.max_map_count=655360 #重新加载 [root@iz2vc3otfvqqqhbzad3955z config]# sysctl -p
-
启动:
#进入bin目录 [es@iz2vc3otfvqqqhbzad3955z bin]$ ./elasticsearch # 启动时,会动态生成文件,如果文件所属用户不匹配,会发生错误,需要重新进行修改用户和用户组 # 本人解决是 chown -R es:es /usr/tool/es Exception in thread "main" java.nio.file.AccessDeniedException: /usr/tool/es/es-1001/config/elasticsearch.keystore at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:90) at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:111) at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:116) at java.base/sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:219) #后台启动 [es@iz2vc3otfvqqqhbzad3955z bin]$ ./elasticsearch -d
3.4 Linux 集群
-
下载软件
-
解压软件
-
创建用户
-
修改配置文件
#集群名称 cluster.name: cluster-es ##节点名称,每个节点的名称不能重复 node.name: node-1 ##ip 地址,每个节点的地址不能重复 network.host: 0.0.0.0 network.publish_host: 47.18.223.96 transport.tcp.port: 9300 ##是不是有资格主节点 node.master: true node.data: true http.port: 9200 ### head 插件需要这打开这两个配置 http.cors.allow-origin: "*" http.cors.enabled: true http.max_content_length: 200mb ##es7.x 之后新增的配置,初始化一个新的集群时需要此配置来选举 master cluster.initial_master_nodes: ["node-1","node-2"] ##es7.x 之后新增的配置,节点发现 discovery.seed_hosts: ["127.0.0.1:9300","127.0.0.1:9310"] gateway.recover_after_nodes: 2 network.tcp.keep_alive: true network.tcp.no_delay: true transport.tcp.compress: true ###集群内同时启动的数据任务个数,默认是 2 个 cluster.routing.allocation.cluster_concurrent_rebalance: 16 ###添加或删除节点及负载均衡时并发恢复的线程个数,默认 4 个 cluster.routing.allocation.node_concurrent_recoveries: 16 ###初始化数据恢复时,并发恢复线程的个数,默认 4 个 cluster.routing.allocation.node_initial_primaries_recoveries: 16
四、Elasticsearch 进阶
4.1 核心概念
-
索引(
Index
)- 一个索引就是一个拥有几分相似特征的文档的集合。
- 比如说,你可以有一个客户数据的索引,另一个产品目录的索引,还有一个订单数据的索引。
- 一个索引由一个名字来标识(必须全部是小写字母),并且当我们要对这个索引中的文档进行索引、搜索、更新和删除的时候,都要使用到这个名字。
- 在一个集群中,可以定义任意多的索引。
- 能搜索的数据必须索引,这样的好处是可以提高查询速度,比如:新华字典前面的目录就是索引的意思,目录可以提高查询速度。
Elasticsearch
索引的精髓:一切设计都是为了提高搜索的性能。
-
类型(
Type
)- 在一个索引中,可以定义一种或多种类型。
- 一个类型是你的索引的一个逻辑上的分类/分区,其语义完全由你来定。通常,会为具有一组共同字段的文档定义一个类型。不同的版本,类型发生了不同的变化。
-
文档(
Document
)- 一个文档是一个可被索引的基础信息单元,也就是一条数据.
- 比如:你可以拥有某一个客户的文档,某一个产品的一个文档,当然,也可以拥有某个订单的一个文档。文档以
JSON(Javascript Object Notation)
格式来表示,而JSON
是一个到处存在的互联网数据交互格式。 - 在一个
index/type
里面,你可以存储任意多的文档。
-
字段(
Field
)- 相当于是数据表的字段,对文档数据根据不同属性进行的分类标识。
-
映射(
Mapping
)mapping
是处理数据的方式和规则方面做一些限制,如:某个字段的数据类型、默认值、
分析器、是否被索引等等。- 这些都是映射里面可以设置的,其它就是处理 ES 里面数据的一些使用规则设置也叫做映射,按着最优规则处理数据对性~~ 能提高很大,因此才需要建立映射,并且需要思考如何建立映射才能对性能更好。
-
分片(
Shards
)- 一个索引可以存储超出单个节点硬件限制的大量数据。比如,一个具有 10 亿文档数据的索引占据
1TB
的磁盘空间,而任一节点都可能没有这样大的磁盘空间。或者单个节点处理搜索请求,响应太慢。 - 为了解决这个问题,
Elasticsearch
提供了将索引划分成多份的能力,每一份就称之为分片。当你创建一个索引的时候,你可以指定你想要的分片的数量。每个分片本身也是一个功能完善并且独立的“索引”,这个“索引”可以被放置到集群中的任何节点上。 - 分片很重要,主要有两方面的原因:
- 允许水平分割 / 扩展内容容量。
- 允许在分片之上进行分布式的、并行的操作,进而提高性能/吞吐量。
- 至于一个分片怎样分布,它的文档怎样聚合和搜索请求,是完全由
Elasticsearch
管理的,对于作为用户的你来说,这些都是透明的,无需过分关心。 - 被混淆的概念是,一个
Lucene
索引我们在Elasticsearch
称作分片 。 一个Elasticsearch
索引 是分片的集合。 当Elasticsearch
在索引中搜索的时候, 他发送查询到每一个属于索引的分片(Lucene
索引),然后合并每个分片的结果到一个全局的结果集。
- 一个索引可以存储超出单个节点硬件限制的大量数据。比如,一个具有 10 亿文档数据的索引占据
-
副本(Replicas)
- 在一个网络 / 云的环境里,失败随时都可能发生,在某个分片/节点不知怎么的就处于离线状态,或者由于任何原因消失了,这种情况下,有一个故障转移机制是非常有用并且是强烈推荐的。为此目的,
Elasticsearch
允许你创建分片的一份或多份拷贝,这些拷贝叫做复制分片(副本)。 - 复制分片之所以重要,有两个主要原因:
- 在分片/节点失败的情况下,提供了高可用性。因为这个原因,注意到复制分片从不与
原/主要(original/primary
)分片置于同一节点上是非常重要的。 - 扩展你的搜索量/吞吐量,因为搜索可以在所有的副本上并行运行。
- 在分片/节点失败的情况下,提供了高可用性。因为这个原因,注意到复制分片从不与
- 总之,每个索引可以被分成多个分片。一个索引也可以被复制 0 次(意思是没有复制)或多次。一旦复制了,每个索引就有了主分片(作为复制源的原来的分片)和复制分片(主分片的拷贝)之别。分片和复制的数量可以在索引创建的时候指定。在索引创建之后,你可以在任何时候动态地改变复制的数量,但是你事后不能改变分片的数量。默认情况下,
Elasticsearch
中的每个索引被分片 1 个主分片和 1 个复制,这意味着,如果你的集群中至少有两个节点,你的索引将会有 1 个主分片和另外 1 个复制分片(1 个完全拷贝),这样的话每个索引总共就有 2 个分片,我们需要根据索引需要确定分片个数。
- 在一个网络 / 云的环境里,失败随时都可能发生,在某个分片/节点不知怎么的就处于离线状态,或者由于任何原因消失了,这种情况下,有一个故障转移机制是非常有用并且是强烈推荐的。为此目的,
-
分配(
Allocation
)- 将分片分配给某个节点的过程,包括分配主分片或者副本。如果是副本,还包含从主分片复制数据的过程。这个过程是由
master
节点完成的。
- 将分片分配给某个节点的过程,包括分配主分片或者副本。如果是副本,还包含从主分片复制数据的过程。这个过程是由
4.2 系统架构
- 一个运行中的
Elasticsearch
实例称为一个节点,而集群是由一个或者多个拥有相同cluster.name
配置的节点组成, 它们共同承担数据和负载的压力。当有节点加入集群中或者从集群中移除节点时,集群将会重新平均分布所有的数据。 - 当一个节点被选举成为主节点时, 它将负责管理集群范围内的所有变更,例如增加、删除索引,或者增加、删除节点等。 而主节点并不需要涉及到文档级别的变更和搜索等操作,所以当集群只拥有一个主节点的情况下,即使流量的增加它也不会成为瓶颈。 任何节点都可以成为主节点。我们的示例集群就只有一个节点,所以它同时也成为了主节点。
- 作为用户,我们可以将请求发送到集群中的任何节点 ,包括主节点。 每个节点都知道任意文档所处的位置,并且能够将我们的请求直接转发到存储我们所需文档的节点。 无论我们将请求发送到哪个节点,它都能负责从各个包含我们所需文档的节点收集回数据,并将最终结果返回給客户端。
Elasticsearch
对这一切的管理都是透明的。
4.3 分布式集群
4.3.1 单节点集群
-
我们在包含一个空节点的集群内创建名为
users
的索引,为了演示目的,我们将分配 3个主分片和一份副本(每个主分片拥有一个副本分片){ "settings" : { "number_of_shards" : 3, "number_of_replicas" : 1 } }
-
通过
elasticsearch-head
插件查看集群情况:
- 集群健康值
yellow( 3 of 6 )
: 表示当前集群的全部主分片都正常运行,但是副本分片没有全部处在正常状态. DESKTOP-6NEENCN
: 3 个主分片正常.Unassigned
: 3 个副本分片都是Unassigned
—— 它们都没有被分配到任何节点。 在同一个节点上既保存原始数据又保存副本是没有意义的,因为一旦失去了那个节点,我们也将丢失该节点上的所有副本数据。
- 集群健康值
-
当前我们的集群是正常运行的,但是在硬件故障时有丢失数据的风险。
4.3.2 故障转移
- 当集群中只有一个节点在运行时,意味着会有一个单点故障问题——没有冗余。 幸运的是,我们只需再启动一个节点即可防止数据丢失。当你在同一台机器上启动了第二个节点时,只要它和第一个节点有同样的
cluster.name
配置,它就会自动发现集群并加入到其中。但是在不同机器上启动节点的时候,为了加入到同一集群,你需要配置一个可连接到的单播主机列表。之所以配置为使用单播发现,以防止节点无意中加入集群。只有在同一台机器上运行的节点才会自动组成集群。 - 如果启动了第二个节点,我们的集群将会拥有两个节点的集群 : 所有主分片和副本分片都已被分配.
- 通过
elasticsearch-head
插件查看集群情况:
- 集群健康值
green( 6 of 6 )
: 表示所有 6 个分片(包括 3 个主分片和 3 个副本分片)都在正常运行. node-2
:3 个主分片正常.node-1
:当第二个节点加入到集群后,3 个副本分片将会分配到这个节点上——每个主分片对应一个副本分片。这意味着当集群内任何一个节点出现问题时,我们的数据都完好无损。所有新近被索引的文档都将会保存在主分片上,然后被并行的复制到对应的副本分片上。这就保证了我们既可以从主分片又可以从副本分片上获得文档。
- 集群健康值
4.3.3 水平扩容
-
当启动了第三个节点,我们的集群将会拥有三个节点的集群 : 为了分散负载而对分片进行重新分配.
-
通过
elasticsearch-head
插件查看集群情况:
- 集群健康值
green( 6 of 6 )
: 表示所有 6 个分片(包括 3 个主分片和 3 个副本分片)都在正常运行. Node 1
和Node 2
上各有一个分片被迁移到了新的Node 3
节点,现在每个节点上都拥有 2 个分片,而不是之前的 3 个。 这表示每个节点的硬件资源(CPU, RAM, I/O
)将被更少的分片所共享,每个分片的性能将会得到提升。- 分片是一个功能完整的搜索引擎,它拥有使用一个节点上的所有资源的能力。 我们这个拥有 6 个分片(3 个主分片和 3 个副本分片)的索引可以最大扩容到 6 个节点,每个节点上存在一个分片,并且每个分片拥有所在节点的全部资源。
- 集群健康值
-
但是如果我们想要扩容超过 6 个节点怎么办呢?
-
主分片的数目在索引创建时就已经确定了下来。实际上,这个数目定义了这个索引能够存储 的最大数据量。(实际大小取决于你的数据、硬件和使用场景。) 但是,读操作——搜索和返回数据——可以同时被主分片 或 副本分片所处理,所以当你拥有越多的副本分片时,也将拥有越高的吞吐量。
-
在运行中的集群上是可以动态调整副本分片数目的,我们可以按需伸缩集群。让我们把
副本数从默认的 1 增加到 2: -
向
es
发送put
请求http://127.0.0.1:9200/users/_settings
{ "number_of_replicas" : 2 }
-
users
索引现在拥有 9 个分片:3 个主分片和 6 个副本分片。 这意味着我们可以将集群扩容到 9 个节点,每个节点上一个分片。相比原来 3 个节点时,集群搜索性能可以提升 3 倍. -
通过
elasticsearch-head
插件查看集群情况:
-
当然,如果只是在相同节点数目的集群上增加更多的副本分片并不能提高性能,因为每个分片从节点上获得的资源会变少。 你需要增加更多的硬件资源来提升吞吐量。但是更多的副本分片数提高了数据冗余量:按照上面的节点配置,我们可以在失去 2 个节点的情况下不丢失任何数据。
-
4.3.4 应对故障
- 我们关闭第一个节点,这时集群的状态为:关闭了一个节点后的集群.
- 我们关闭的节点是一个主节点。而集群必须拥有一个主节点来保证正常工作,所以发生
的第一件事情就是选举一个新的主节点:Node 2
。在我们关闭Node 1
的同时也失去了主
分片 1 和 2 ,并且在缺失主分片的时候索引也不能正常工作。 如果此时来检查集群的状况,我们看到的状态将会为red
:不是所有主分片都在正常工作。
- 幸运的是,在其它节点上存在着这两个主分片的完整副本, 所以新的主节点立即将这些分片在
Node 2
和Node 3
上对应的副本分片提升为主分片, 此时集群的状态将会为yellow
。这个提升主分片的过程是瞬间发生的,如同按下一个开关一般. - 为什么我们集群状态是
yellow
而不是green
呢:- 虽然我们拥有所有的三个主分片,但是同时设置了每个主分片需要对应 2 份副本分片,而此
时只存在一份副本分片。 所以集群不能为green
的状态,不过我们不必过于担心:如果我们同样关闭了Node 2
,我们的程序 依然 可以保持在不丢任何数据的情况下运行,因为Node 3
为每一个分片都保留着一份副本。 - 如果我们重新启动
Node 1
,集群可以将缺失的副本分片再次进行分配,那么集群的状态也将恢复成之前的状态。 如果Node 1
依然拥有着之前的分片,它将尝试去重用它们,同时仅从主分片复制发生了修改的数据文件。和之前的集群相比,只是Master
节点切换了。
- 虽然我们拥有所有的三个主分片,但是同时设置了每个主分片需要对应 2 份副本分片,而此
4.4 路由计算
- 当索引一个文档的时候,文档会被存储到一个主分片中。
Elasticsearch
如何知道一个文档应该存放到哪个分片中呢?当我们创建文档时,它如何决定这个文档应当被存储在分片1 还是分片 2 中呢?首先这肯定不会是随机的,否则将来要获取文档的时候我们就不知道从何处寻找了。实际上,这个过程是根据下面这个公式决定的:
routing
是一个可变值,默认是文档的_id
,也可以设置成一个自定义的值.routing
通过hash
函数生成一个数字,然后这个数字再除以number_of_primary_shards
(主分片的数量)后得到余数.- 这个分布在 0 到
number_of_primary_shards-1
之间的余数,就是我们所寻求的文档所在分片的位置.
- 这就解释了为什么我们要在创建索引的时候就确定好主分片的数量 并且永远不会改变这个数量:因为如果数量变化了,那么所有之前路由的值都会无效,文档也再也找不到了.
- 所有的文档
API
(get 、 index 、 delete 、 bulk 、 update
以及mget
)都接受一个叫做routing
的路由参数 ,通过这个参数我们可以自定义文档到分片的映射。一个自定义的路由参数可以用来确保所有相关的文档——例如所有属于同一个用户的文档——都被存储到同一个分片中。
4.5 分片控制
-
我们假设有一个集群由三个节点组成。 它包含一个叫
emps
的索引,有两个主分片,每个主分片有两个副本分片。相同分片的副本不会放在同一节点。 -
向
es
服务器发送put
请求http://127.0.0.1:9200/emps
{ "settings" : { "number_of_shards" : 2, "number_of_replicas" : 2 } }
-
通过
elasticsearch-head
插件查看集群情况,所以我们的集群是一个有三个节点和一个索引的集群:
- 我们可以发送请求到集群中的任一节点。 每个节点都有能力处理任意请求。 每个节点都知道集群中任一文档位置,所以可以直接将请求转发到需要的节点上。 在下面的例子中,将所有的请求发送到
Node 1
,我们将其称为 协调节点(coordinating node
) - 当发送请求的时候, 为了扩展负载,更好的做法是轮询集群中所有的节点。
- 我们可以发送请求到集群中的任一节点。 每个节点都有能力处理任意请求。 每个节点都知道集群中任一文档位置,所以可以直接将请求转发到需要的节点上。 在下面的例子中,将所有的请求发送到
4.5.1 写流程
-
新建、索引和删除 请求都是写操作, 必须在主分片上面完成之后才能被复制到相关的副本分片.
-
新建,索引和删除文档所需要的步骤顺序:
- 客户端向
Node 1
发送新建、索引或者删除请求. - 节点使用文档的
_id
确定文档属于分片 0 。请求会被转发到Node 3
,因为分片 0 的主分片目前被分配在Node 3
上. Node 3
在主分片上面执行请求。如果成功了,它将请求并行转发到Node 1
和Node 2
的副本分片上。一旦所有的副本分片都报告成功,Node 3
将向协调节点报告成功,协调节点向客户端报告成功。
- 客户端向
-
在客户端收到成功响应时,文档变更已经在主分片和所有副本分片执行完成,变更是安全的。有一些可选的请求参数允许您影响这个过程,可能以数据安全为代价提升性能。
参数 含义 consistency
consistency
,即一致性。在默认设置下,即使仅仅是在试图执行一个写操作之前,主分片都会要求 必须要有 规定数量(quorum
)(或者换种说法,也即必须要有大多数)的分片副本处于活跃可用状态,才会去执行写操作(其中分片副本可以是主分片或者副本分片)。这是为了避免在发生网络分区故障(network partition
)的时候进行写操作,进而导致数据不一致。规定数量即:int( (primary + number_of_replicas) / 2 ) + 1
,consistency
参数的值可以设为one
(只要主分片状态ok
就允许执行写操作),all
(必须要主分片和所有副本分片的状态没问题才允许执行写操作), 或quorum
。默认值为quorum
, 即大多数的分片副本状态没问题就允许执行写操作。注意,规定数量 的计算公式中number_of_replicas
指的是在索引设置中的设定副本分片数,而不是指当前处理活动状态的副本分片数。如果你的索引设置中指定了当前索引拥有三个副本分片,那规定数量的计算结果即:int( (primary + 3 replicas) / 2 ) + 1 = 3
如果此时你只启动两个节点,那么处于活跃状态的分片副本数量就达不到规定数量,也因此您将无法索引和删除任何文档。timeout
如果没有足够的副本分片会发生什么? Elasticsearch
会等待,希望更多的分片出现。默认情况下,它最多等待 1 分钟。 如果你需要,你可以使用timeout
参数使它更早终止: 100 毫秒,30s 是 30 秒。 -
新索引默认有 1 个副本分片,这意味着为满足规定数量应该需要两个活动的分片副本。 但是,这些默认的设置会阻止我们在单一节点上做任何事情。为了避免这个问题,要求只有当
number_of_replicas
大 于 1 的时候,规定数量才会执行。
4.5.2 读流程
-
我们可以从主分片或者从其它任意副本分片检索文档:
-
从主分片或者副本分片检索文档的步骤顺序:
- 客户端向
Node 1
发送获取请求. - 节点使用文档的
_id
来确定文档属于分片 0 。分片 0 的副本分片存在于所有的三个节点上。 在这种情况下,它将请求转发到Node 2
。 Node 2
将文档返回给Node 1
,然后将文档返回给客户端。
- 客户端向
-
在处理读取请求时,协调结点在每次请求的时候都会通过轮询所有的副本分片来达到负载均衡。在文档被检索时,已经被索引的文档可能已经存在于主分片上但是还没有复制到副本分片。 在这种情况下,副本分片可能会报告文档不存在,但是主分片可能成功返回文档。 一旦索引请求成功返回给用户,文档在主分片和副本分片都是可用的。
4.5.3 更新流程
-
部分更新一个文档结合了先前说明的读取和写入流程:
-
部分更新一个文档的步骤:
- 客户端向
Node 1
发送更新请求. - 它将请求转发到主分片所在的
Node 3
. Node 3
从主分片检索文档,修改_source
字段中的JSON
,并且尝试重新索引主分片的文档。如果文档已经被另一个进程修改,它会重试步骤 3 ,超过retry_on_conflict
次后放弃.- 如果
Node 3
成功地更新文档,它将新版本的文档并行转发到Node 1
和Node 2
上的副本分片,重新建立索引。一旦所有副本分片都返回成功,Node 3
向协调节点也返回成功,协调节点向客户端返回成功。
- 客户端向
-
当主分片把更改转发到副本分片时, 它不会转发更新请求。 相反,它转发完整文档的新版本。请记住,这些更改将会异步转发到副本分片,并且不能保证它们以发送它们相同的顺序到达。 如果
Elasticsearch
仅转发更改请求,则可能以错误的顺序应用更改,导致得到损坏的文档。
4.5.4 多文档操作流程
-
mget
和bulk API
的模式类似于单文档模式。区别在于协调节点知道每个文档存在于哪个分片中。它将整个多文档请求分解成 每个分片 的多文档请求,并且将这些请求并行转发到每个参与节点。 -
协调节点一旦收到来自每个节点的应答,就将每个节点的响应收集整理成单个响应,返回给客户端:
- 用单个
mget
请求取回多个文档所需的步骤顺序:- 客户端向
Node 1
发送mget
请求. Node 1
为每个分片构建多文档获取请求,然后并行转发这些请求到托管在每个所需的主分片或者副本分片的节点上。一旦收到所有答复,Node 1
构建响应并将其返回给客户端。
- 客户端向
- 用单个
-
可以对
docs
数组中每个文档设置routing
参数. -
bulk API
, 允许在单个批量请求中执行多个创建、索引、删除和更新请求.
bulk API
按如下步骤顺序执行:- 客户端向
Node 1
发送bulk
请求. Node 1
为每个节点创建一个批量请求,并将这些请求并行转发到每个包含主分片的节
点主机.- 主分片一个接一个按顺序执行每个操作。当每个操作成功时,主分片并行转发新文档(或删除)到副本分片,然后执行下一个操作。 一旦所有的副本分片报告所有操作成功,该节点将向协调节点报告成功,协调节点将这些响应收集整理并返回给客户端。
- 客户端向
4.6 分片原理
- 分片是
Elasticsearch
最小的工作单元。但是究竟什么是一个分片,它是如何工作的? - 传统的数据库每个字段存储单个值,但这对全文检索并不够。文本字段中的每个单词需要被搜索,对数据库意味着需要单个字段有索引多值的能力。最好的支持是一个字段多个值需求的数据结构是倒排索引。
4.6.1 倒排索引
-
Elasticsearch
使用一种称为倒排索引的结构,它适用于快速的全文搜索。 -
见其名,知其意,有倒排索引,肯定会对应有正向索引。正向索引(
forward index
),反向索引(inverted index
)更熟悉的名字是倒排索引。 -
所谓的正向索引,就是搜索引擎会将待搜索的文件都对应一个文件
ID
,搜索时将这个ID
和搜索关键字进行对应,形成K-V
对,然后对关键字进行统计计数.但是互联网上收录在搜索引擎中的文档的数目是个天文数字,这样的索引结构根本无法满足实时返回排名结果的要求。所以,搜索引擎会将正向索引重新构建为倒排索引,即把文件ID
对应到关键词的映射转换为关键词到文件ID
的映射,每个关键词都对应着一系列的文件,这些文件中都出现这个关键词。 -
一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表。例如,假设我们有两个文档,每个文档的
content
域包含如下内容:The quick brown fox jumped over the lazy dog.
Quick brown foxes leap over lazy dogs in summer.
-
为了创建倒排索引,我们首先将每个文档的 content 域拆分成单独的 词(我们称它为 词条或
tokens
),创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档:
-
现在,如果我们想搜索
quick brown
,我们只需要查找包含每个词条的文档:
- 两个文档都匹配,但是第一个文档比第二个匹配度更高。如果我们使用仅计算匹配词条数量的简单相似性算法,那么我们可以说,对于我们查询的相关性来讲,第一个文档比第二个文档更佳。
-
但是,我们目前的倒排索引有一些问题:
Quick
和quick
以独立的词条出现,然而用户可能认为它们是相同的词。fox
和foxes
非常相似, 就像dog
和dogs
;他们有相同的词根。jumped
和leap
, 尽管没有相同的词根,但他们的意思很相近。他们是同义词。
-
使用前面的索引搜索
+Quick
+fox
不会得到任何匹配文档。(记住,+ 前缀表明这个词必须存在。)只有同时出现Quick
和fox
的文档才满足这个查询条件,但是第一个文档包含quick fox
,第二个文档包含Quick foxes
。 -
我们的用户可以合理的期望两个文档与查询匹配。我们可以做的更好。
-
如果我们将词条规范为标准模式,那么我们可以找到与用户搜索的词条不完全一致,但具有足够相关性的文档。例如:
Quick
可以小写化为quick
.foxes
可以 词干提取 --变为词根的格式-- 为fox
。类似的,dogs
可以为提取为dog
.jumped
和leap
是同义词,可以索引为相同的单词jump
.
- 这还远远不够。我们搜索
+Quick +fox
仍然 会失败,因为在我们的索引中,已经没有Quick
了。但是,如果我们对搜索的字符串使用与content
域相同的标准化规则,会变成查询+quick +fox
,这样两个文档都会匹配!分词和标准化的过程称为分析,这非常重要。你只能搜索在索引中出现的词条,所以索引文本和查询字符串必须标准化为相同的格式。
4.6.2 文档搜索
- 早期的全文检索会为整个文档集合建立一个很大的倒排索引并将其写入到磁盘。 一旦新的索引就绪,旧的就会被其替换,这样最近的变化便可以被检索到。
- 倒排索引被写入磁盘后是 不可改变 的:它永远不会修改。
- 不变性有重要的价值:
- 不需要锁。如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题。
- 一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。
- 其它缓存(像
filter
缓存),在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建,因为数据不会变化。 - 写入单个大的倒排索引允许数据被压缩,减少磁盘
I/O
和 需要被缓存到内存的索引的使用量。
- 当然,一个不变的索引也有不好的地方。主要事实是它是不可变的! 你不能修改它。如果你需要让一个新的文档 可被搜索,你需要重建整个索引。这要么对一个索引所能包含的数据量造成了很大的限制,要么对索引可被更新的频率造成了很大的限制。
4.6.3 动态更新索引
- 如何在保留不变性的前提下实现倒排索引的更新?
- 用更多的索引,通过增加新的补充索引来反映新近的修改,而不是直接重写整个倒排索引。每一个倒排索引都会被轮流查询到,从最早的开始查询完后再对结果进行合并。
Elasticsearch
基于Lucene
, 这个java
库引入了按段搜索的概念。 每一 段 本身都是一个倒排索引, 但索引在Lucene
中除表示所有段的集合外, 还增加了提交点的概念 — 一个列出了所有已知段的文件.- 按段搜索会以如下流程执行:
-
新文档被收集到内存索引缓存.
-
不时地, 缓存被提交:
- 一个新的段—一个追加的倒排索引—被写入磁盘.
- 一个新的包含新段名字的 提交点 被写入磁盘.
- 磁盘进行 同步 — 所有在文件系统缓存中等待的写入都刷新到磁盘,以确保它们被写入物理文件.
-
新的段被开启,让它包含的文档可见以被搜索.
-
内存缓存被清空,等待接收新的文档.
-
当一个查询被触发,所有已知的段按顺序被查询。词项统计会对所有段的结果进行聚合,以保证每个词和每个文档的关联都被准确计算。 这种方式可以用相对较低的成本将新文档添加到索引。
-
段是不可改变的,所以既不能从把文档从旧的段中移除,也不能修改旧的段来进行反映文档的更新。 取而代之的是,每个提交点会包含一个
.del
文件,文件中会列出这些被删除文档的段信息。 -
当一个文档被 “删除” 时,它实际上只是在
.del
文件中被 标记 删除。一个被标记删除的文档仍然可以被查询匹配到, 但它会在最终结果被返回前从结果集中移除。 -
文档更新也是类似的操作方式:当一个文档被更新时,旧版本文档被标记删除,文档的新版本被索引到一个新的段中。 可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就已经被移除。
-
4.6.4 近实时搜索
-
随着按段(
per-segment
)搜索的发展,一个新的文档从索引到可被搜索的延迟显著降低了。新文档在几分钟之内即可被检索,但这样还是不够快。磁盘在这里成为了瓶颈。提交(Commiting
)一个新的段到磁盘需要一个fsync
来确保段被物理性地写入磁盘,这样在断电的时候就不会丢失数据。 但是fsync
操作代价很大; 如果每次索引一个文档都去执行一次的话会造成很大的性能问题。 -
我们需要的是一个更轻量的方式来使一个文档可被搜索,这意味着
fsync
要从整个过程中被移除。在Elasticsearch
和磁盘之间是文件系统缓存。 像之前描述的一样, 在内存索引缓冲区中的文档会被写入到一个新的段中。 但是这里新段会被先写入到文件系统缓存—这一步代价会比较低,稍后再被刷新到磁盘—这一步代价比较高。不过只要文件已经在缓存中,就可以像其它文件一样被打开和读取了。
-
Lucene
允许新段被写入和打开—使其包含的文档在未进行一次完整提交时便对搜索可见。这种方式比进行一次提交代价要小得多,并且在不影响性能的前提下可以被频繁地执行。
-
在
Elasticsearch
中,写入和打开一个新段的轻量的过程叫做refresh
。 默认情况下每个分片会每秒自动刷新一次。这就是为什么我们说Elasticsearch
是 近 实时搜索: 文档的变化并不是立即对搜索可见,但会在一秒之内变为可见。 -
这些行为可能会对新用户造成困惑: 他们索引了一个文档然后尝试搜索它,但却没有搜到。这个问题的解决办法是用
refresh API
执行一次手动刷新:/users/_refresh
-
尽管刷新是比提交轻量很多的操作,它还是会有性能开销。当写测试的时候, 手动刷新很有用,但是不要在生产环境下每次索引一个文档都去手动刷新。 相反,你的应用需要意识到
Elasticsearch
的近实时的性质,并接受它的不足。 -
并不是所有的情况都需要每秒刷新。可能你正在使用
Elasticsearch
索引大量的日志文件,你可能想优化索引速度而不是近实时搜索, 可以通过设置refresh_interval
, 降低每个索引的刷新频率.{ "settings": { "refresh_interval": "30s" } }
-
refresh_interval 可以在既存索引上进行动态更新。 在生产环境中,当你正在建立一个大的新索引时,可以先关闭自动刷新,待开始使用该索引时,再把它们调回来.
# 关闭自动刷新 PUT /users/_settings { "refresh_interval": -1 } # 每一秒刷新 PUT /users/_settings { "refresh_interval": "1s" }
4.6.5 持久化变更
4.6.6 段合并
4.7 文档分析
- 分析包含下面的过程:
- 将一块文本分成适合于倒排索引的独立的 词条.
- 将这些词条统一化为标准格式以提高它们的“可搜索性”,或者
recall
.
- 分析器执行上面的工作。分析器实际上是将三个功能封装到了一个包里:
- 字符过滤器:首先,字符串按顺序通过每个 字符过滤器 。他们的任务是在分词前整理字符串。一个字符过滤器可以用来去掉
HTML
,或者将&
转化成and
. - 分词器:其次,字符串被 分词器 分为单个的词条。一个简单的分词器遇到空格和标点的时候,可能会将文本拆分成词条。
Token
过滤器:最后,词条按顺序通过每个token
过滤器 。这个过程可能会改变词条(例如,小写化Quick
),删除词条(例如, 像a
,and
,the
等无用词),或者增加词条(例如,像jump
和leap
这种同义词)。
- 字符过滤器:首先,字符串按顺序通过每个 字符过滤器 。他们的任务是在分词前整理字符串。一个字符过滤器可以用来去掉
4.7.1 内置分析器
Elasticsearch
还附带了可以直接使用的预包装的分析器。接下来我们会列出最重要的分析器。为了证明它们的差异,我们看看每个分析器会从下面的字符串得到哪些词条:"Set the shape to semi-transparent by calling set_trans(5)"
- 标准分析器:
- 标准分析器是
Elasticsearch
默认使用的分析器。它是分析各种语言文本最常用的选择。它根据Unicode
联盟定义的单词边界划分文本。删除绝大部分标点。最后,将词条小写. - 分词结果:
set, the, shape, to, semi, transparent, by, calling, set_trans, 5
- 标准分析器是
- 简单分析器:
- 简单分析器在任何不是字母的地方分隔文本,将词条小写.
- 分词结果:
set, the, shape, to, semi, transparent, by, calling, set, trans
- 空格分析器:
- 空格分析器在空格的地方划分文本.
- 分词结果:
Set, the, shape, to, semi-transparent, by, calling, set_trans(5)
- 语言分析器:
- 特定语言分析器可用于 很多语言。它们可以考虑指定语言的特点。例如, 英语 分析器附带了一组英语无用词(常用单词,例如
and
或者the
,它们对相关性没有多少影响),它们会被删除。 由于理解英语语法的规则,这个分词器可以提取英语单词的 词干 。 - 分词结果:
set, shape, semi, transpar, call, set_tran, 5
- 注意看
transparent
、calling
和set_trans
已经变为词根格式.
- 特定语言分析器可用于 很多语言。它们可以考虑指定语言的特点。例如, 英语 分析器附带了一组英语无用词(常用单词,例如
4.7.2 分析器使用场景
- 当我们 索引 一个文档,它的全文域被分析成词条以用来创建倒排索引。 但是,当我们在全文域 搜索 的时候,我们需要将查询字符串通过 相同的分析过程 ,以保证我们搜索的词条格式与索引中的词条格式一致。
- 全文查询,理解每个域是如何定义的,因此它们可以做正确的事:
- 当你查询一个 全文 域时, 会对查询字符串应用相同的分析器,以产生正确的搜索词条列表。
- 当你查询一个 精确值 域时,不会分析查询字符串,而是搜索你指定的精确值。
4.7.3 测试分析器
-
有些时候很难理解分词的过程和实际被存储到索引中的词条,特别是你刚接触
Elasticsearch
。 -
使用
analyze API
来看文本是如何被分析的,在消息体里,指定分析器和要分析的文本: -
发送
GET
请求http://localhost:9200/_analyze
{ "analyzer": "standard", "text": "Text to analyze" }
-
结果中每个元素代表一个单独的词条:
{ "tokens": [ { "token": "text", "start_offset": 0, "end_offset": 4, "type": "<ALPHANUM>", "position": 0 }, { "token": "to", "start_offset": 5, "end_offset": 7, "type": "<ALPHANUM>", "position": 1 }, { "token": "analyze", "start_offset": 8, "end_offset": 15, "type": "<ALPHANUM>", "position": 2 } ] } # token 是实际存储到索引中的词条。 # position 指明词条在原始文本中出现的位置。 # start_offset 和 end_offset 指明字符在原始字符串中的位置。
4.7.4 指定分析器
- 当
Elasticsearch
在你的文档中检测到一个新的字符串域,它会自动设置其为一个全文 字符串 域,使用 标准 分析器对它进行分析。 - 你不希望总是这样。可能你想使用一个不同的分析器,适用于你的数据使用的语言。有时候你想要一个字符串域就是一个字符串域—不使用分析,直接索引你传入的精确值,例如用户
ID
或者一个内部的状态域或标签。要做到这一点,我们必须手动指定这些域的映射。
4.7.5 IK 分词器
-
发送
GET
请求http://localhost:9200/_analyze
{ "text":"测试单词" }
-
ES
的默认分词器无法识别中文中测试、单词这样的词汇,而是简单的将每个字拆完分为一个词:{ "tokens": [ { "token": "测", "start_offset": 0, "end_offset": 1, "type": "<IDEOGRAPHIC>", "position": 0 }, { "token": "试", "start_offset": 1, "end_offset": 2, "type": "<IDEOGRAPHIC>", "position": 1 }, { "token": "单", "start_offset": 2, "end_offset": 3, "type": "<IDEOGRAPHIC>", "position": 2 }, { "token": "词", "start_offset": 3, "end_offset": 4, "type": "<IDEOGRAPHIC>", "position": 3 } ] }
-
这样的结果显然不符合我们的使用要求,所以我们需要下载
ES
对应版本的中文分词器. -
安装
IK
中文分词器.# 下载IK分词器 wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.8.0/elasticsearch-analysis-ik-7.8.0.zip # 在plugins中新建文件夹ik,解压到ik文件夹中,重启es
-
发送GET请求 http://localhost:9200/_analyze
{ "text":"测试单词", "analyzer":"ik_max_word" } # ik_max_word:会将文本做最细粒度的拆分 # ik_smart:会将文本做最粗粒度的拆分
-
使用中文分词后的结果:
{ "tokens": [ { "token": "测试", "start_offset": 0, "end_offset": 2, "type": "CN_WORD", "position": 0 }, { "token": "单词", "start_offset": 2, "end_offset": 4, "type": "CN_WORD", "position": 1 } ] }
-
ES
中也可以进行扩展词汇,首先查询:# GET http://localhost:9200/_analyze { "text":"弗雷尔卓德", "analyzer":"ik_max_word" }
-
仅仅可以得到每个字的分词结果,我们需要做的就是使分词器识别到弗雷尔卓德也是一个词语:
{ "tokens": [ { "token": "弗", "start_offset": 0, "end_offset": 1, "type": "CN_CHAR", "position": 0 }, { "token": "雷", "start_offset": 1, "end_offset": 2, "type": "CN_CHAR", "position": 1 }, { "token": "尔", "start_offset": 2, "end_offset": 3, "type": "CN_CHAR", "position": 2 }, { "token": "卓", "start_offset": 3, "end_offset": 4, "type": "CN_CHAR", "position": 3 }, { "token": "德", "start_offset": 4, "end_offset": 5, "type": "CN_CHAR", "position": 4 } ] }
- 首先进入
ES
根目录中的plugins
文件夹下的ik
文件夹,进入config
目录,创建custom.dic
文件,写入弗雷尔卓德. - 同时打开
IKAnalyzer.cfg.xml
文件,将新建的custom.dic
配置其中,重启ES
服务器.<properties> <comment>IK Analyzer 扩展配置</comment> <!--用户可以在这里配置自己的扩展字典 --> <entry key="ext_dict">custom.dic</entry> <!--用户可以在这里配置自己的扩展停止词字典--> <entry key="ext_stopwords"></entry> <!--用户可以在这里配置远程扩展字典 --> <!-- <entry key="remote_ext_dict">words_location</entry> --> <!--用户可以在这里配置远程扩展停止词字典--> <!-- <entry key="remote_ext_stopwords">words_location</entry> --> </properties>
{ "tokens": [ { "token": "弗雷尔卓德", "start_offset": 0, "end_offset": 5, "type": "CN_WORD", "position": 0 } ] }
- 首先进入
4.7.6 自定义分析器
4.8 文档处理
4.8.1 文档冲突
- 当我们使用
index API
更新文档 ,可以一次性读取原始文档,做我们的修改,然后重新索引 整个文档 。 最近的索引请求将获胜:无论最后哪一个文档被索引,都将被唯一存储在Elasticsearch
中。如果其他人同时更改这个文档,他们的更改将丢失。 - 很多时候这是没有问题的。也许我们的主数据存储是一个关系型数据库,我们只是将数据复制到
Elasticsearch
中并使其可被搜索。 也许两个人同时更改相同的文档的几率很小。或者对于我们的业务来说偶尔丢失更改并不是很严重的问题。 - 但有时丢失了一个变更就是非常严重的 。试想我们使用
Elasticsearch
存储我们网上商城商品库存的数量, 每次我们卖一个商品的时候,我们在Elasticsearch
中将库存数量减少。有一天,管理层决定做一次促销。突然地,我们一秒要卖好几个商品。 假设有两个web
程序并行运行,每一个都同时处理所有商品的销售web_1
对stock_count
所做的更改已经丢失,因为web_2
不知道它的stock_count
的拷贝已经过期。 结果我们会认为有超过商品的实际数量的库存,因为卖给顾客的库存商品并不存在,我们将让他们非常失望。 - 变更越频繁,读数据和更新数据的间隙越长,也就越可能丢失变更。
- 在数据库领域中,有两种方法通常被用来确保并发更新时变更不会丢失:
- 悲观并发控制:
- 这种方法被关系型数据库广泛使用,它假定有变更冲突可能发生,因此阻塞访问资源以防止冲突。
- 一个典型的例子是读取一行数据之前先将其锁住,确保只有放置锁的线程能够对这行数据进行修改。
- 乐观并发控制:
Elasticsearch
中使用的这种方法假定冲突是不可能发生的,并且不会阻塞正在尝试的操作。 然而,如果源数据在读写当中被修改,更新将会失败。- 应用程序接下来将决定该如何解决冲突。 例如,可以重试更新、使用新的数据、或者将相关情况报告给用户。
- 悲观并发控制:
4.8.2 乐观并发控制
Elasticsearch
是分布式的。当文档创建、更新或删除时, 新版本的文档必须复制到集群中的其他节点。Elasticsearch
也是异步和并发的,这意味着这些复制请求被并行发送,并且到达目的地时也许 顺序是乱的 。Elasticsearch
需要一种方法确保文档的旧版本不会覆盖新的版本。- 当我们之前讨论
index
,GET
和delete
请求时,我们指出每个文档都有一个 _version (版本)号,当文档被修改时版本号递增。Elasticsearch
使用这个version
号来确保变更以正确顺序得到执行。如果旧版本的文档在新版本之后到达,它可以被简单的忽略。 - 我们可以利用
version
号来确保 应用中相互冲突的变更不会导致数据丢失。我们通过指定想要修改文档的version
号来达到这个目的。 如果该版本不是当前版本号,我们的请求将会失败。 - 老的版本
es
使用version
,但是新版本不支持了,会报下面的错误,提示我们用if_seq_no
和if_primary_term
。
4.8.3 外部系统版本控制
- 一个常见的设置是使用其它数据库作为主要的数据存储,使用
Elasticsearch
做数据检索, 这意味着主数据库的所有更改发生时都需要被复制到Elasticsearch
,如果多个进程负责这一数据同步,你可能遇到类似于之前描述的并发问题。 - 如果你的主数据库已经有了版本号 — 或一个能作为版本号的字段值比如
timestamp
—那么你就可以在Elasticsearch
中通过增加version_type=external
到查询字符串的方式重用这些相同的版本号, 版本号必须是大于零的整数, 且小于9.2E+18
— 一个Java
中long
类型的正值。 - 外部版本号的处理方式和我们之前讨论的内部版本号的处理方式有些不同,
Elasticsearch
不是检查当前_version
和请求中指定的版本号是否相同, 而是检查当前_version
是否 小于 指定的版本号。 如果请求成功,外部的版本号作为文档的新_version
进行存储。 - 外部版本号不仅在索引和删除请求是可以指定,而且在 创建 新文档时也可以指定。
4.5 Kibana
-
Kibana
是一个免费且开放的用户界面,能够让你对Elasticsearch
数据进行可视化,并让你在Elastic Stack
中进行导航。 -
你可以进行各种操作,从跟踪查询负载,到理解请求如何流经你的整个应用,都能轻松完成。
-
# 下载 [root@iz2vc3otfvqqqhbzad3955z kibana]# wget https://artifacts.elastic.co/downloads/kibana/kibana-7.8.0-linux-x86_64.tar.gz # 解压 [root@iz2vc3otfvqqqhbzad3955z kibana]# tar -zxvf kibana-7.8.0-linux-x86_64.tar.gz # 修改 config/kibana.yml 文件 # 默认端口 server.port: 5601 server.host: "0.0.0.0" # ES 服务器的地址 elasticsearch.hosts: ["http://localhost:9200"] # 索引名 kibana.index: ".kibana" # 支持中文 i18n.locale: "zh-CN" # 后台启动 [es@iz2vc3otfvqqqhbzad3955z bin]$ nohup ./kibana & # 关闭 [es@iz2vc3otfvqqqhbzad3955z config]$ fuser -n tcp 5601 5601/tcp: 28499 [es@iz2vc3otfvqqqhbzad3955z config]$ kill -9 28499