Elasticsearch是一种基于Lucene开发的高性能,可扩展的分布式搜索引擎。与常用数据库索引不同,采用倒序索引(全文索引)方式实现,将内容信息进行分词拆装成关键字,然后对每个关键字建立索引。广泛应用在搜索,电商及各类资源型网站中。
ES提供了Restful API及java开发接口,隐藏了原有Lucene的复杂性。实现了近乎实时的搜索功能,延迟控制在秒级内,可以使用集群方式达到大数据量的快速检索。
类似数据库,ES也有与库、表及字段相对应的存储结构,分别为index,type,document。
index:索引库,是具有相同或者大致相同数据的整体存储结构,如商品index,订单index
type:一个index下可以有多个type,但是每个index可以有稍微不同的结构,如电子产品type,服装type,但是在新es版本中,一个index中只能有一个type,因为同一个index下的相同字段会被创建到同一个索引中,如果不同type定义的field类型不同,创建索引就会冲突。索引新版es中创建和检索都不需要指定type了,es缺省使用1来代替了。
document:存放数据的最小单元,一个document表示一条数据
index采用主从和分片的方式存储数据,其中primary shard可以进行读写操作,replica shard从primary shard中同步数据后, 可提供查询操作。创建index时,可指定主从分片个数, 创建后,主分片个数不能修改,副本分片个数可以继续扩展。
生产环境中, 如果ES集群数据负载过大,可进行垂直拓展主副分片,即增加结点内存和磁盘空间。如果查询压力过大,可水平扩展,增加结点至集群中,增加副本分片个数,分散请求压力。
Kibana是ES的可视化web操作界面,类似ZooKeeper和ZKUI一样。
ES及Kibana下载地址 https://www.elastic.co/cn/downloads/past-releases
Windows系统下,解压执行bin目录下bat文件即可使用。
访问 http://localhost:5601/ 打开Kibana系统
Kibana系统下操作ES
进入Dev Tools菜单,输入相关Restful API,即可对ES的数据进行各操作。
PUT(增加)、DELETE(删除)、POST(修改)、GET(查询)
查看集群状况
v表示显示头信息
GET /_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
1619277503 15:18:23 elasticsearch green 1 1 4 4 0 0 0 0 - 100.0%
总共1个节点, 共有4个分片(主), 集群状态为green
集群共有3个状态:
red:部分primary分片不是active状态
yellow:所有的primary分片都是active状态,部分replica分片不是active状态
green:表示primary和replica分片都是active状态
一个很重要的知识点:
同一个分片的primary和replica分片不能在同一个结点上,原因:避免因为结点服务不可用或物理宕机,导致部分数据丢失 。
查看索引
GET /_cat/indices?v
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
green open .apm-custom-link JqNQ1KDESAG2C12Urfgslw 1 0 0 0 208b 208b
green open .kibana_task_manager_1 JpmN6VfUT-yzBB-QrlmOaw 1 0 5 1 95.7kb 95.7kb
green open .apm-agent-configuration irX9cVRsTf-Y7CtpH-rxng 1 0 0 0 208b 208b
green open .kibana_1 MVhb5PfNQgW0I_jk8fkHgg 1 0 19 1 52.2kb 52.2kb
以上索引都是Kibana自动创建的
创建索引
PUT /my_index 默认创建了一个主分片,一个副本分片
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
green open .apm-custom-link JqNQ1KDESAG2C12Urfgslw 1 0 0 0 208b 208b
green open .kibana_task_manager_1 JpmN6VfUT-yzBB-QrlmOaw 1 0 5 3 112.3kb 112.3kb
yellow open my_index aQCskrRLQXOi2QmumtVMwg 1 1 0 0 208b 208b
green open .apm-agent-configuration irX9cVRsTf-Y7CtpH-rxng 1 0 0 0 208b 208b
green open .kibana_1 MVhb5PfNQgW0I_jk8fkHgg 1 0 20 3 65.2kb 65.2kb
可以看到my_index的状态为yellow,因为只有一个primary分片在active状态,另一个replica不是活跃状态,所以是yellow状态。此时,如果启动一个新的ES结点到集群中,ES会自动将分片均匀分布到各节点上,短时间内会将此index达到green状态。
指定主副本分片数量
PUT /my_index2
{
"settings": {
"number_of_shards": 3, ## primary shard个数
"number_of_replicas": 1 ## replica shard个数
}
}
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
green open .apm-custom-link JqNQ1KDESAG2C12Urfgslw 1 0 0 0 208b 208b
green open .kibana_task_manager_1 JpmN6VfUT-yzBB-QrlmOaw 1 0 5 3 53.8kb 53.8kb
yellow open my_index aQCskrRLQXOi2QmumtVMwg 1 1 0 0 208b 208b
green open .apm-agent-configuration irX9cVRsTf-Y7CtpH-rxng 1 0 0 0 208b 208b
yellow open my_index2 Wn5reLWvRTyVh6BKPOxZWA 3 1 0 0 624b 624b
green open .kibana_1 MVhb5PfNQgW0I_jk8fkHgg 1 0 20 5 36.9kb 36.9kb
新增商品
index: goods type:phone document_id:1
PUT /goods/phone/1
{
"sku": "SKU001",
"name": "iPhone 12",
"price": "9999",
"desc": "The newest iPhone",
"tags": ["iPhone", "iPhone12"]
}
{
"_index" : "goods",
"_type" : "phone",
"_id" : "2",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1
}
查看商品
GET /goods/phone/1
{
"_index" : "goods",
"_type" : "phone",
"_id" : "1",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"sku" : "SKU001",
"name" : "iPhone 12",
"price" : "9999",
"desc" : "The newest iPhone",
"tags" : [
"iPhone",
"iPhone12"
]
}
}
修改商品
PUT /goods/phone/1
{
"sku" : "SKU001",
"name" : "iPhone 12"
}
使用PUT修改完成后,查看数据
{
"_index" : "goods",
"_type" : "phone",
"_id" : "1",
"_version" : 2,
"_seq_no" : 2,
"_primary_term" : 1,
"found" : true,
"_source" : {
"sku" : "SKU001",
"name" : "iPhone 12"
}
}
数据其他属性丢失,此操作相当于替换操作,将原document标记删除, 创建新的document,谨慎使用
修改商品部分属性
POST /goods/phone/1/_update
{
"doc": {
"price" : "9988"
}
}
使用POST修改完成后,查看数据
{
"_index" : "goods",
"_type" : "phone",
"_id" : "1",
"_version" : 4,
"_seq_no" : 4,
"_primary_term" : 1,
"found" : true,
"_source" : {
"sku" : "SKU001",
"name" : "iPhone 12",
"price" : "9988",
"desc" : "The newest iPhone",
"tags" : [
"iPhone",
"iPhone12"
]
}
}
这种部分属性修改可以减少并发的异常情况,全量替换可能会覆盖其他操作,部分修改原理与全部修改基本一致,只不过获取原document的时机不同,部分修改在提交给es后,es会先获取document,然后再修改,插入一条新的document,旧document标记删除,影响范围更小,并发性更高。
删除商品
DELETE /goods/phone/1
查询返回
{
"_index" : "goods",
"_type" : "phone",
"_id" : "1",
"found" : false
}
这里简单说一下document删除的原理,删除时只是将原纪录标记删除,并没有立刻物理删除,而是等待数据越来越多时,es会自动检索已经标记删除的数据,将其物理清除掉。
检索所有数据
GET /goods/phone/_search
{
"query": {
"match_all": {}
}
}
GET /goods/phone/_search
#! Deprecation: [types removal] Specifying types in search requests is deprecated.
{
"took" : 664, ## 消耗时间
"timed_out" : false, ## 是否超时
"_shards" : { ## 分片数量
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 2, ## 结果数量
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "goods",
"_type" : "phone",
"_id" : "2",
"_score" : 1.0,
"_source" : {
"sku" : "SKU002",
"name" : "iPhone 10",
"price" : "8888",
"desc" : "The iPhone X",
"tags" : [
"iPhone",
"iPhoneX"
]
}
},
{
"_index" : "goods",
"_type" : "phone",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"sku" : "SKU001",
"name" : "iPhone 12",
"price" : "9999",
"desc" : "The newest iPhone",
"tags" : [
"iPhone",
"iPhone12"
]
}
}
]
}
}
搜索document时,可以指定index和type,也可以不指定,可以指定多个index,多个type
GET /goods/phone/_search
GET /goods,goods1/phone/_search
GET /_search
GET /_all/phone/_search
GET /goods*/phone/_search
另外一种简写方式
GET /goods/phone/_search?q=name:iPhone // name中必须包含iPhone
GET /goods/phone/_search?q=+name:iPhone // 与上面一致
GET /goods/phone/_search?q=name:iPhone // name中必须不包含iPhone
GET /goods/phone/_search?q=iPhone // 会将各field拼接成一个字符串,添加到索引中,用于检索
关键字检索
GET /goods/phone/_search
{
"query": {
"match": {
"name": "xiaomi"
}
}
}
多条件检索
GET /goods/phone/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "iPhone"
}
},
{
"match": {
"tags": "iPhone12"
}
}
]
}
}
}
GET /goods/phone/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "iPhone"
}
}
],
"must_not": [
{
"match": {
"tags": "iPhone12"
}
}
]
}
}
}
多字段匹配
查询name和tags中包含iPhone的document
GET /goods/_search
{
"query": {
"multi_match": {
"query": "iPhone",
"fields": ["name", "tags"]
}
}
}
指定数量检索
GET /goods/phone/_search
{
"query": {
"match": {
"name": "xiaomi"
}
},
"from": 0,
"size": 20
}
批量获取document
GET /_mget
{
"docs": [
{
"_index": "goods",
"_type": "phone",
"_id": 1
},
{
"_index": "goods",
"_type": "phone",
"_id": 2
}
]
}
指定查询字段
GET /goods/phone/_search
{
"query": {
"match": {
"name": "xiaomi"
}
},
"_source": ["sku", "name"],
"from": 0,
"size": 20
}
短语搜索match_phrase
短语表示被筛选内容中,必须连续包含整个子串,不会进行分词
GET /goods/phone/_search
{
"query": {
"match_phrase": {
"name": "mi"
}
}
}
排序
GET /order/_search
{
"query": {
"match": {
"userName": "guan"
}
},
"sort": [
{
"id": {
"order": "desc"
}
}
]
}
filter和query
二者都是用来筛选数据的,不同的区别是:query不仅关心是否满足条件,还会根据相关度进行score计算;filter只判断是否符合条件,不会计算score,并且内置了缓存,所以filter效率更高。
GET /order/_search
{
"query": {
"bool": {
"must": [
{ "match":
{ "userName": "Guan" }
}
],
"filter": [
{ "range": {
"id": {
"gte": 0,
"lte": 3
}
}}
]
}
}
}
查询校验
GET /order/_validate/query?explain
{
"query": {
"match": {
"userName": "Guan"
}
}
}
{
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"valid" : true,
"explanations" : [
{
"index" : "order",
"valid" : true,
"explanation" : "userName:guan"
}
]
}
乐观锁
低版es在对文档创建时,会默认加上一个版本号,与MySQL的乐观锁一致,避免并发时数据异常问题。
新版es添加了seq_no和primary_term,对document进行并发控制,两个参数都一致才允许修改。
参考官方文档示例:https://www.elastic.co/guide/en/elasticsearch/reference/current/optimistic-concurrency-control.html
POST /goods/phone/1?if_seq_no=11&if_primary_term=1
{
"price": "9988"
}
{
"_index" : "goods",
"_type" : "phone",
"_id" : "1",
"_version" : 6,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 12,
"_primary_term" : 1
}
可以看到修改成功后seq_no已经加1,再次发起报错,因为版本号已经不匹配,所以更新失败
{
"error" : {
"root_cause" : [
{
"type" : "version_conflict_engine_exception",
"reason" : "[1]: version conflict, required seqNo [11], primary term [1]. current document has seqNo [12] and primary term [1]",
"index_uuid" : "43hVEeIDRCKyYR6ySDTF1g",
"shard" : "0",
"index" : "goods"
}
],
"type" : "version_conflict_engine_exception",
"reason" : "[1]: version conflict, required seqNo [11], primary term [1]. current document has seqNo [12] and primary term [1]",
"index_uuid" : "43hVEeIDRCKyYR6ySDTF1g",
"shard" : "0",
"index" : "goods"
},
"status" : 409
}
bulk操作
当客户端频繁发起操作,可以采取批量提交请求执行,减少网络开销,但是bulk操作要求严格的顺序,各操作不能换行,创建和更新操作时,doc写在操作命令下一行,不用数组的原因也是不需要一次将其转换成一个对象,因为有可能数据量太大导致json转换cpu和内存消耗过高,这样只要一行行读取就好了,也是为了提升性能。
bulk操作一般控制在一次1000~5000个操作,并不是数量越大性能越高,可以在生成环境中逐步调优。
POST /goods/phone/_bulk
{"update": {"_id": 1}}
{"doc": {"desc": "The iPhone 12"}}
{"create": {"_id": 9}}
{"sku": "SKU0009", "name":"Redmi K30", "price": "1599"}
{"delete": {"_id": 5}}
{
"took" : 8,
"errors" : false,
"items" : [
{
"update" : {
"_index" : "goods",
"_type" : "phone",
"_id" : "1",
"_version" : 8,
"result" : "noop",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 14,
"_primary_term" : 1,
"status" : 200
}
},
{
"create" : {
"_index" : "goods",
"_type" : "phone",
"_id" : "9",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 21,
"_primary_term" : 1,
"status" : 201
}
},
{
"delete" : {
"_index" : "goods",
"_type" : "phone",
"_id" : "5",
"_version" : 5,
"result" : "not_found",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 22,
"_primary_term" : 1,
"status" : 404
}
}
]
}
返回结果中包含每一个操作对应的结果,并且各个操作独立,顺序执行,相互不影响,如果失败会在结果中返回失败原因。
获取index的mapping
一般情况下mapping不需要我们手动创建或指定,es会自动根据document生成对应的mapping,当然也可以指定各field的类型,但是类型一旦指定后就不可以修改,可以新增filed。
GET /order/_mapping
{
"order" : {
"mappings" : {
"properties" : {
"createTime" : {
"type" : "long"
},
"id" : {
"type" : "long"
},
"sn" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"userId" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"userName" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
}
}
}
properties:可以嵌套,比如通常address中包含省市区信息。
text:用来存储字符串类型,text表示使用分词器对文档进行分词,并添加到索引中,并且会截取整个字符串的前256个字符以keyword方式进行存储,keyword表示必须完整匹配。
POST /order/order/1
{
"id": 1,
"sn": "O00001",
"userId": "U00001",
"userName": "Guan Dongsheng",
"createTime": 1621156874000
}
GET /order/_search // 可以检索出数据
{
"query": {
"match": {
"userName": "Guan"
}
}
}
GET /order/_search // 不可以检索出数据
{
"query": {
"match": {
"userName.keyword": "Guan"
}
}
}
GET /order/_search // 可以检索出数据
{
"query": {
"match": {
"userName.keyword": "Guan Dongsheng"
}
}
}
分布式存储
index在创建时可以指定多个主分片和副本分片,主分片用于读写数据,副本分片同步数据后提供读操作,提升集群的并发能力,同一个结点中不能包含同一个主副本分片,避免单个服务结点挂掉导致数据丢失。创建index后,es会将主副分片均匀分散到集群中,主分片个数不能再次修改,但是可以根据并发数量调整副本分片的个数。
document在存放或者获取时,需要根据算法选定一个主分片(比如P0),可以将请求发送到P1上,当收到请求后,获取路由结果,转发到P0,获取结果后返回给客户端。一般采用hash取余算法,当数据量很大时,各节点数据量几乎平均分配,这也是为什么主分片数不能修改的主要原因。主分片保存document后会将数据同步到自己的副本分片上。
客户端可以将请求发送到集群中任一节点,此时此节点会充当协调者的角色,根据路由算法判断请求数据会在哪个节点上,然后将请求转发到对应节点,获取结果后,如果需要处理则会处理完成后返回。如分页请求,第一页10条数据,则每个节点都要获取前10条,协调节点获取到所有节点的前10条后,再次排序筛选返回最终结果,因此会有deep paging问题,当偏移量越来越高,检索和传输数据越来越大,导致性能越来越慢。
Bouncing Results
参考文档 https://elasticsearch.cn/article/334
搜索同一query,结果ES返回的顺序却不尽相同,这就是请求轮询到不同分片,而未设置排序条件,相同相关性评分情况下,是按照所在segment中lucene id来排序的,相同数据的不同备份之间该id是不能保证一致的,故造成结果震荡问题。
如设置该参数,则有一下9种情况
_primary
:发送到集群的相关操作请求只会在主分片上执行。
_primary_first
:指查询会先在主分片中查询,如果主分片找不到(挂了),就会在副本中查询。
_replica
:发送到集群的相关操作请求只会在副本上执行。
_replica_first
:指查询会先在副本中查询,如果副本找不到(挂了),就会在主分片中查询。
_local
: 指查询操作会优先在本地节点有的分片中查询,没有的话再在其它节点查询。
_prefer_nodes:abc,xyz
:在提供的节点上优先执行(在这种情况下为’abc’或’xyz’)
_shards:2,3
:限制操作到指定的分片。 (2
和“3”)。这个偏好可以与其他偏好组合,但必须首先出现:_shards:2,3 | _primary
_only_nodes:node1,node2
:指在指定id的节点里面进行查询,如果该节点只有要查询索引的部分分片,就只在这部分分片中查找,不同节点之间用“,”分隔。
custom(自定义)
:注意自定义的preference参数不能以下划线"_"开头。
当preference为自定义时,即该参数不为空,且开头不以“下划线”开头时,特别注意:如果以用户query作为自定义preference时,一定要处理以下划线开头的情况,这种情况下如果不属于以上8种情况,则会抛出异常。
Scroll批量检索
scroll检索类似于分页,生成一份数据快照,指定时间内分批次查询,快照之后的数据变化不可见,创建一个scroll_id进行标记,可用于系统进行大批量处理,性能比较高。
GET /goods/_search?scroll=1m
{
"query": {
"match_all": {}
},
"size": 2
}
{
"_scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFGNpYm5kSGtCMzNheHFVVFM4c1ktAAAAAAAARSsWTTdIN1RHWkJRTWl4cHVIWVpoRHY1QQ==",
"took" : 5,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 6,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "goods",
"_type" : "phone",
"_id" : "2",
"_score" : 1.0,
"_source" : {
"sku" : "SKU002",
"name" : "iPhone 10",
"price" : "8888",
"desc" : "The iPhone X",
"tags" : [
"iPhone",
"iPhoneX"
]
}
},
{
"_index" : "goods",
"_type" : "phone",
"_id" : "3",
"_score" : 1.0,
"_source" : {
"sku" : "SKU003",
"name" : "xiao mi",
"price" : 1999,
"desc" : "xiao mi k30",
"tags" : [
"xiaomi",
"k30"
]
}
}
]
}
}
后续查询使用
GET /_search/scroll
{
"scroll": "1m",
"scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFFVTYm1kSGtCMzNheHFVVFNvc1lSAAAAAAAARQoWTTdIN1RHWkJRTWl4cHVIWVpoRHY1QQ=="
}
持续更新中。。。