一、es的核心概念
基于lucene
(1)分布式文档存储引擎
(2)分布式搜索引擎和分析引擎
(3)分布式,支持PB级数据
1. Near Realtime(NRT)
近实时
- 从数据写入到数据可以被搜索有一个小延迟,大约1秒钟
- 基于elasticsearch的搜索和分析可以达到秒级
2. CLuCLuster
集群,包含多个节点,每个节点属于哪个集群通过配置来决定,集群名称默认是elasticsearch
3. Node
节点,集群中的一个节点,节点名称默认是随机分配的,默认节点会加入elasticsearch集群
5. Index
索引,相当于数据库中的库
5. Type
类型,相当于数据库中的表,一个index中可以有多个type
6. Document
文档,elasticsearch中的最小数据单元,每个type中都可以有多个document,一个document相当于数据库中的一条数据
7. primary shard
分片,一台机器上无法存大量数据,可以分成多个存在多台机器上,就可以横向扩展,存储更多的数据,提高吞吐量和性能,每个shard都给lucene index
8. replica shard
副本,当服务器宕机时,shard会丢失,所以可以给shard创建多个replica副本,可以在shard故障时备用,保障数据的不丢失,primary shard在创建索引时被设置,默认5个,之后不能修改,replica shard随时都能修改,默认1个,默认每个索引10个shard(5个primary,5个replica),最小的高可以用配置需要两台服务器,一般primary和replica不放在同一台机器上
二、es和数据库对比
elasticsearch | 数据库 |
---|---|
document | 行 |
type | 表 |
index | 库 |
三、安装和验证
1.安装
下载windows包,解压缩,进入bin启动elasticsearch.bat
2.验证
浏览器输入地址:http://localhost:9200/?pretty
返回:
name:node的名称
cluster_name:集群名称,默认是elasticsearch
{
"name" : "DESKTOP-28G220S",
"cluster_name" : "elasticsearch",
"cluster_uuid" : "VXjCxw3PRoKGuvEnxgt91g",
"version" : {
"number" : "7.0.0",
"build_flavor" : "default",
"build_type" : "zip",
"build_hash" : "b7e28a7",
"build_date" : "2019-04-05T22:55:32.697037Z",
"build_snapshot" : false,
"lucene_version" : "8.0.0",
"minimum_wire_compatibility_version" : "6.7.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}
接下来使用kibana的dev_tools来学习操作elasticsearch
3.kibana安装
下载解压缩kibana安装包,启动bin/kibana.bat
4.kibana验证
浏览器输入地址:http://localhost:5601/app/kibana#/dev_tools/console?_g=()
接下来进行操作学习
四、集群的简单管理
1. 查询es的健康状况
es提供了一套cat的api,可以查看es中的各种数据
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
1567915131 03:58:51 elasticsearch green 1 1 2 2 0 0 0 0 - 100.0%
2. 查询集群中索引
GET /_cat/indices?v
索引的状态分为:
- green:每个索引的primary shard和replica shard都是active状态
- yellow:每个索引的primary shard都是active状态,部分replica shard处于不可用状态
- red:不是所有的primary shard都是active状态,有部分数据丢失
3. 创建索引myindex
PUT /myindex?pretty
{
"acknowledged" : true,
"shards_acknowledged" : true,
"index" : "myindex"
}
创建索引的时候也可以设置索引的primary shard和replica shard的数量
PUT /suppermarket
{
"settings": {
"number_of_shards": 5,
"number_of_replicas": 1
}
}
这个表示的是,设置suppermarket这个index的primary shard数量为5,每个primary shard对应的副本replica shard数量为1
4. 删除索引
DELETE /indexname?pretty
{
"acknowledged" : true
}
五、document的增删查改操作
1.新增
PUT /suppermarket/product/1
{
"name":"tongyi fangbianmian",
"desc":"laotansuancai",
"price":15,
"tags":["suancai"]
}
PUT /suppermarket/product/2
{
"name":"kangshifu fangbianmian",
"desc":"hongshaoniurou",
"price":25,
"tags":["hongshao","niurou"]
}
PUT /suppermarket/product/3
{
"name":"tangdaren fangbianmian",
"desc":"malaniurou",
"price":30,
"tags":["mala","niurou"]
}
当新增时有两种指定id的方式
-
手动指定:
一般是用于导入数据时,数据有自己的id,就指定这个id作为document的idPUT /index/type/id
-
自动生成:
20个字符,URL安全,GUID,分布式系统并行时不容易发生冲突POST /index/type
2.查询
-
普通查询
GET /suppermarket/product/1
{ "_index" : "suppermarket", "_type" : "product", "_id" : "1", "_version" : 1, "_seq_no" : 0, "_primary_term" : 1, "found" : true, "_source" : { "name" : "tongyi fangbianmian", "desc" : "laotansuancai", "price" : 15, "tags" : [ "suancai" ] } }
-
定制返回值查询
在查询的时候,_source返回的是所有的数据,如果要返回指定的数据,操作如下GET /supermarket/product/1?_source=name,desc
返回
{ "_index" : "suppermarket", "_type" : "product", "_id" : "1", "_version" : 1, "_seq_no" : 0, "_primary_term" : 1, "found" : true, "_source" : { "name" : "tongyi fangbianmian", "desc" : "laotansuancai" } }
-
批量查询(_mget)
(1)批量查询不同index下数据GET /_mget { "docs":[{ "_index":"supermarket", "_type":"product", "_id":1 },{ "_index":"supermarket", "_type":"product", "_id":2 },{ "_index":"supermarket", "_type":"product", "_id":3 }] }
(2)批量查询不同type下的数据
GET /suppermarket/_mget { "docs":[{ "_type":"product", "_id":1 },{ "_type":"product", "_id":2 },{ "_type":"product", "_id":3 }] }
(3)批量查询同一type下不同id的数据
GET /suppermarket/product/_mget { "ids":[1,2,3] }
3.修改
3.1. 直接覆盖
PUT /suppermarket/product/3
{
"name":"tangdaren fangbianmian",
"desc":"malaniurou",
"price":30,
"tags":["mala","niurou"]
}
3.2. 修改指定值(papartial update)
一般更新会先查询出来再修改写入es
优点
- papartial update直接传入值,在es的一个shard内部进行查询,修改,相比一般的更新,papartial update避免了所有的网络数据传输的开销,提升了性能(一次外部网络查询,一次外部网络修改数据传入es)
- 减少了查询和修改的时间间隔,有效减少并发冲突的问题
POST /suppermarket/product/3/_update
{
"doc": {
"price":35
}
}
返回
{
"_index" : "suppermarket",
"_type" : "product",
"_id" : "3",
"_version" : 3,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 10,
"_primary_term" : 1
}
4.删除
删除的时候并没有真的删除,只是标记成为deleted,当数据越来越多的时候,es会自动物理删除,修改替换的时候原数据也是这样
DELETE /suppermarket/product/3/?pretty
{
"_index" : "suppermarket",
"_type" : "product",
"_id" : "3",
"_version" : 2,
"result" : "deleted",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 5,
"_primary_term" : 1
}
六、es的5种搜索方式
1. query string search
1.1. 查询所有document
GET /suppermarket/product/_search
took:花费了几毫秒
timed_out:是否超时
_shards:数据拆成1个分片,搜索所有的primary shard
hits.total:查询结果个数
hits.max_score:score是document对于搜索的一个匹配值,值越高越匹配
hits.hits:查询document的详细数据
"max_score" : 1.0,
"hits" : [
{
"_index" : "suppermarket",
"_type" : "product",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"name" : "tongyi fangbianmian",
"desc" : "laotansuancai",
"price" : 15,
"tags" : [
"suancai"
]
}
},
{
"_index" : "suppermarket",
"_type" : "product",
"_id" : "2",
"_score" : 1.0,
"_source" : {
"name" : "kangshifu fangbianmian",
"desc" : "hongshaoniurou",
"price" : 25,
"tags" : [
"hongshao",
"niurou"
]
}
},
{
"_index" : "suppermarket",
"_type" : "product",
"_id" : "3",
"_score" : 1.0,
"_source" : {
"name" : "tangdaren fangbianmian",
"desc" : "malaniurou",
"price" : 30,
"tags" : [
"mala",
"niurou"
]
}
}
]
1.2. 查询名称包含方便面的数据,并且price降序排列
GET /suppermarket/product/_search?q=name:fangbianmian&sort=price:desc
报错,需要将price的fielddata设置为true
"caused_by": {
"type": "illegal_argument_exception",
"reason": "Fielddata is disabled on text fields by default. Set fielddata=true on [price] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead.",
"caused_by": {
"type": "illegal_argument_exception",
"reason": "Fielddata is disabled on text fields by default. Set fielddata=true on [price] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead."
}
}
设置fielddata=true
PUT /suppermarket/_mappings
{
"properties": {
"price":{
"type": "text",
"fielddata": true
}
}
}
接下来查询,就能看到价格是降序排列的了
(PS:第一种这种方式不常用,拼接太麻烦)
2. query DSL
2.1. 查询所有商品
GET /suppermarket/product/_search
{
"query": {
"match_all": {}
}
}
2.2. 查询名字包含方便面的数据并按照价格降序排列
GET /suppermarket/product/_search
{
"query": {
"match": {
"name": "fangbianmian"
}
},
"sort": [
{
"price": "desc"
}
]
}
2.3. 分页查询(本示例相是每页1条,查询第二页,就是第二条数据)
GET /suppermarket/product/_search
{
"query": {
"match_all": {}
},
"from": 1,
"size": 1
}
2.4. 指定需要查询出来的值
GET /suppermarket/product/_search
{
"query": {
"match_all": {}
},
"_source":["name","price"]
}
结果的数据中就只返回了name和price两个字段的数据
3. query filter
对数据进行过滤
查找价格大于20的名字包含方便面的数据
GET /suppermarket/product/_search
{
"query": {
"bool": {
"must": [ {
"match": {
"name": "fangbianmian"
}
}
],
"filter": {
"range": {
"price": {
"gte": 20
}
}
}
}
}
}
4. full-text search
全文检索
GET /suppermarket/product/_search
{
"query": {
"match": {
"tags":"mala niurou"
}
}
}
查询的时候会使用倒排索引,分别查询mala和niurou,查找相关数据序号,根据查询出的相关度从大到小排序
查询结果如下,只查询出mala和niurou相关的2条数据,麻辣牛肉面相关度0.99,红烧牛肉面相关度0.33
"max_score" : 0.99185646,
"hits" : [
{
"_index" : "suppermarket",
"_type" : "product",
"_id" : "3",
"_score" : 0.99185646,
"_source" : {
"name" : "tangdaren fangbianmian",
"desc" : "malaniurou",
"price" : 35,
"tags" : [
"mala",
"niurou"
]
}
},
{
"_index" : "suppermarket",
"_type" : "product",
"_id" : "2",
"_score" : 0.33698124,
"_source" : {
"name" : "kangshifu fangbianmian",
"desc" : "hongshaoniurou",
"price" : 25,
"tags" : [
"hongshao",
"niurou"
]
}
}
]
5. phrase search
短语搜索,和全文搜索相反,它不像全文搜索,包含这个字符串就能查询出来,他是搜索必须要包含输入的整个短语才能查询出来
GET /suppermarket/product/_search
{
"query": {
"match_phrase": {
"name":"tangdaren fangbianmian"
}
}
}
只查询出唯一完全相同的数据
"max_score" : 0.7985077,
"hits" : [
{
"_index" : "suppermarket",
"_type" : "product",
"_id" : "3",
"_score" : 0.7985077,
"_source" : {
"name" : "tangdaren fangbianmian",
"desc" : "malaniurou",
"price" : 35,
"tags" : [
"mala",
"niurou"
]
}
}
]
七、聚合函数
1. group by(aggs->terms)
计算数量
GET /suppermarket/product/_search
{
"aggs": {
"group_by_tags": {
"terms": {
"field": "tags"
}
}
}
}
这样会报一个错
需要先设置fieldfielddata属性为true才能查询
PUT /suppermarket/_mapping
{
"properties": {
"tags":{
"type": "text",
"fielddata": true
}
}
}
设置完了查询
结果会展示所有的参于聚合的document
添加一个设置size来限制
GET /suppermarket/product/_search
{
"size": 0,
"aggs": {
"group_by_tags": {
"terms": {
"field": "tags"
}
}
}
}
结果
"aggregations" : {
"group_by_tags" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "niurou",
"doc_count" : 2
},
{
"key" : "hongshao",
"doc_count" : 1
},
{
"key" : "mala",
"doc_count" : 1
},
{
"key" : "suancai",
"doc_count" : 1
}
]
}
}
先查询,再分组
GET /suppermarket/product/_search
{
"size": 0,
"query": {
"match": {
"tags": "niurou"
}
},
"aggs": {
"group_by_tags": {
"terms": {
"field": "tags"
}
}
}
}
结果
"aggregations" : {
"group_by_tags" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "niurou",
"doc_count" : 2
},
{
"key" : "hongshao",
"doc_count" : 1
},
{
"key" : "mala",
"doc_count" : 1
}
]
}
}
avg(aggs->avg)
平均值
先分组再求平均值
GET /indexname/typename/_search
{
"size":0,
"aggs":{
"自定义名字":{
"terms":{"field":"分组的字段名"},
"aggs":{
"自定义名字":{
"avg":{"field":"求平均值的字段名"}
}
}
}
}
}
按照求出来的平均值降序排列
GET /indexname/typename/_search
{
"size":0,
"aggs":{
"自定义分组名字":{
"terms":{
"field":"分组的字段名",
"order":{"下面自定义平均值名字":"desc"}
},
"aggs":{
"自定义平均值名字":{
"avg":{"field":"求平均值的字段名"}
}
}
}
}
}
八、容错
当master node发生故障,状态变成红色
- master选举,自动选举另一个服务器node成为master node
- 将丢失node中的primary shard的一个replica shard提升为primary shard,状态变成黄色
- 重启故障的node,将缺失的副本都copy到该node上,同步一下数据,状态变成绿色
九、解决并发问题
问题
当两个用户同时拿到数据100,同时做减一修改,然后写入数据,最后数据显示是99,和预期结果98不相同
使用的是乐观锁解决(使用数据的版本号version作为锁)
当用户A和用户B同时获取数据,同时对数据做减一操作
然后A操作后的数据99重新写入es,该数据版本号增加version=2
当用户B写入es的时候,es中数据的版本号version=2,和B获取数据时的版本号version=1不相同,所以会重新获取数据99,然后进行减一操作,得到98,写入es
_version的操作
- _version在修改和删除数据的时候会自动加一
- 在删除这条数据以后,又重新put这条数据进来,_version变成了删除数据时的_version加一,侧面也证明了,删除数据的时候,并没有真正的物理删除,而是标记为了deleted,他的版本号这是数据还是被保留下来了的
基于版本号更新数据
PUT /supermarket/product/1?_version=1
只有当es中的数据版本号为1时才能进行修改,不然会报错
使用外部的版本号控制
- 当使用es的_version控制版本号时,必须两个版本号一致才能进行修改
- 当使用外部的版本号控制修改数据时,传入的数据版本号必须要大于es的版本号才能进行修改
语法:
只能是那条数据版本号小于2才能修改成功PUT /supermarket/product/1?_version=2&version_type=external
十、批量操作
1. _mget
批量查询(_mget)
(1)批量查询不同index下数据
GET /_mget
{
"docs":[{
"_index":"supermarket",
"_type":"product",
"_id":1
},{
"_index":"supermarket",
"_type":"product",
"_id":2
},{
"_index":"supermarket",
"_type":"product",
"_id":3
}]
}
(2)批量查询不同type下的数据
GET /suppermarket/_mget
{
"docs":[{
"_type":"product",
"_id":1
},{
"_type":"product",
"_id":2
},{
"_type":"product",
"_id":3
}]
}
(3)批量查询同一type下不同id的数据
GET /suppermarket/product/_mget
{
"ids":[1,2,3]
}
2. _bulk
可以分为index(新增,数据重复覆盖)、create(不覆盖新增,数据重复报异常)、update(partial update)、delete(删除)
可以像_mget一样分为3种方式,下面只演示一种操作方式
PUT /_bulk
{"index":{"_index":"supermarket","_type":"product","_id":"1"}}
{ "name":"index test"}
{"create":{"_index":"supermarket","_type":"product","_id":"4"}}
{"name":"create test"}
{"create":{"_index":"supermarket","_type":"product","_id":"2"}}
{"name":"create2 test"}
{"update":{"_index":"supermarket","_type":"product", "_id":"2"}}
{"doc":{"name" :"update test"}}
{"delete":{"_index":"supermarket","_type":"product","_id":"3"}}
- 第一部分新增,和第二部分不同,index的新增如果数据已经存在是直接覆盖数据,而create的新增,如果存在会报出异常
- 第二部分新增,两条json串,一条指明哪个index、type等,一条操作数据
- 第三部分新增,由于已经存在2,所以执行新增时会报异常,但是bulk一条数据执行不会影响到别的数据执行成功
- 第四部分partial update,两条json串,一条指明哪个index、type等,一条操作数据
- 第五部分,删除,只需要一条json串
十一、路由算法
公式
shard=hash(routing)%number_of_primary_shards
- 每一次增删改查的时候都会带过来一个routing,默认是使用_id(可以是手动指定的,可以是自动生成的),在es中会调用一个hash方法,得到routing的hash值,和primary shard的数量进行求余数,得到数据该存在第几个primary shard中
- 相同routing传进来hash值都是一定相同的,都在同一个shard中
- 也可以在操作的时候手动指定routing传进来
PUT /supermarkdet/product/id?routing=自定义id
十二、timeout
timeout 机制,指定每个shard,就只能在timeout时间范围内将搜索到的数据返回。而不是等到全部数据全部检索出来才返回。
设置超时时间:
GET /_search?timeout=1ms
十三、分页查询
GET /_search?from=3&size=2