Elasticsearch核心技术与实战
01 Nov 2022在极客时间上学习elasticsearch课程,主要关注点在query的DSL语句以及集群的管理,在本地基于es 7.1来构建集群服务,启动脚本如下,同时在conf/elasticsearch.yml中添加xpack.ml.enabled: false、http.host: 0.0.0.0的配置(禁用ml及启用host):
bash> bin/elasticsearch -E node.name=node0 -E cluster.name=geektime -E path.data=node0_data -d
bash> bin/elasticsearch -E node.name=node1 -E cluster.name=geektime -E path.data=node1_data -d
bash> bin/elasticsearch -E node.name=node2 -E cluster.name=geektime -E path.data=node2_data -d
bash> bin/elasticsearch -E node.name=node3 -E cluster.name=geektime -E path.data=node3_data -d
在docker容器中启动cerebro服务,用于监控elasticsearch集群的状态,docker启动命令如下:
bash> docker run -d --name cerebro -p 9100:9000 lmenezes/cerebro:latest
文档index基础操作
1) elasticsearch中创建新文档,用post请求方式,url内容为index/_doc/id。当未指定{id}时,会自动生成随机的id。put方式用于更新文档,当PUT users/_doc/1?op_type=create或PUT users/_create/1指定文档id存在时,就会报错。
POST users/_doc
{
"user": "mike",
"post_date": "2019-04-15T14:12:12",
"message": "trying out kibana"
}
2) elasticsearch的分词器analysis,分词是指把全文本转换为一些列的单词(term/token)的过程,其通常由Character Filters、Tokenizer、Token Filters这三部分组成。具体url示例如下,analyzer的类型可以有:standard、stop、simple等。
GET _analyze
{
"analyzer": "stop",
"text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening."
}
3) url中query string的语法,指定字段v.s.泛查询,其中df为默认字段,当不指定df只按q查询时,则是泛查询,从_doc的所有字段检索:
GET /movies/_search?q=2012&df=title&sort=year:desc&from=0&size=10&timeout=1s
URl Search、Request Body查询及文档Mapping
1)在elasticsearch中查询可以分为url search和request body查询,其中url search用GET方式,相关参数放在url中。df指定默认查询字段,q为查询字符串。当未指定df时,称为泛查询,会拿数值与doc中所有字段进行匹配:
# es中查询的dsl,df指定默认字段,q为查询数值,TermQuery
GET kibana_sample_data_ecommerce/_search?q=Eddie&df=customer_first_name
{
"profile": "true"
}
# 若不用df的话,可以用q=field:value来进行替换
GET kibana_sample_data_ecommerce/_search?q=customer_first_name:Eddie
{
"profile": "true"
}
2)Phrase query与Term query的区别,PhraseQuery会按整个字符串进行匹配,而TermQuery则会对字符串进行分词。对于term来说,只要Field value中包含任意一个单词就可以。
# phrase query,相当于不会做分词,匹配完整字符串(1条)
GET kibana_sample_data_ecommerce/_search?q=customer_full_name:"Eddie Underwood"
{
"profile": "true"
}
# term query,对字符串进行了分词,好像也有keyword概念,任意匹配Eddie或Underwood就可以
GET kibana_sample_data_ecommerce/_search?q=customer_full_name:Eddie Underwood
{
"profile": "true"
}
此外,在url query中还支持分组的概念,也就是Bool Query。当查询条件为customer_full_name:(Eddie Underwood)时,会分别按Eddie和Underwood进行匹配,其是任意的满足关系。若想在字段中同时满足要求,则可在分组中添加AND操作符。此外,url query还支持range查询及通配符查询。
# bool query,full_name中包括Eddie或Underwood才可以,实现同时包含,则需添加AND关键字
GET kibana_sample_data_ecommerce/_search?q=customer_full_name:(Eddie AND Underwood)
{
"profile": "true"
}
# 数值范围查询,(订单总额)taxful_total_price大于50
GET kibana_sample_data_ecommerce/_search?q=taxful_total_price:>=50
{
"profile": "true"
}
# 通配符查询,只要email字段中含"gwen"就会被匹配
GET kibana_sample_data_ecommerce/_search?q=email:gwen*
{
"profile": "true"
}
3)Request body查询的详细解释,这其实是一种更通用的写法,使用POST请求方式。在body中使用_source指定要获取的字段列表,同时sort可指定按哪个字段进行排序。query部分指定了具体的查询条件,operator为and最终效果类似于phrase query。elasticsearch的painless脚本用于特定计算,返回计算后的新字段(如金额转换等)。
# es request body的写法,按订单总金额排序desc,_source过滤doc中的字段
POST kibana_sample_data_ecommerce/_search
{
"_source": ["taxful_total_price", "total_quantity", "customer_full_name", "manufacturer"],
"sort": [{"taxful_total_price": "desc"}],
"query": {
"match": {
"customer_full_name": {
"query": "Eddie Lambert",
"operator": "and"
}
}
},
"script_fields": {
"addtional_field": {
"script": {
"lang": "painless",
"source": "doc['taxful_total_price'].value + '_hello'"
}
}
}
}
此外,对于match_phrase则不会进行分词,对_doc会直接进行查询。body中的slop参数可用于近似度查询,提升数据检索的容错性。
# match_phrase查询,不会进行分词,直接匹配total字符串,slop指定term结果
POST kibana_sample_data_ecommerce/_search
{
"query": {
"match_phrase": {
"customer_full_name": {
"query": "Eddie Lambert",
"slop": 1
}
}
}
}
4)query_string与simple_query_string的区别,query_string与url query类似,也需指定default_field。同时,其也支持多字段fields及多分组query的查询,simple_query_string#query也需指定查询条件。
# query_string和url query比较类似,也支持分组,如下的query_string#fields
POST /users/_search
{
"query": {
"query_string": {
"default_field": "name",
"query": "Ruan AND YiMing"
}
}
}
POST /users/_search
{
"query": {
"query_string": {
"fields": ["name", "about"],
"query": "(Ruan And YiMing) OR (Java AND Elasticsearch)"
}
}
}
POST /users/_search
{
"query": {
"simple_query_string": {
"query": "Ruan AND YiMing",
"fields": ["name"]
}
}
}
5)对于文档mapping这一部分,类似比喻的话,相当于是数据表的schema,规定了字段的约束信息。对于dynamic mapping,elasticsearch支持三种模式:true、false和strict。其默认值为true,当设置mapping为false时,新添加的字段不能检索,但会在_source部分展示,当为strict时,索引文档新增字段时,会进行报错。
GET mapping_test/_mapping
# 修改dynamic为false,新加的字段不能被索引
PUT dynamic_mapping_test/_mapping
{
"dynamic": false
}
PUT dynamic_mapping_test/_doc/10
{
"anotherField": "otherValue"
}
# dynamic为false时,新增的字段无法被检索,strict模式下,新添加字段会报错
POST dynamic_mapping_test/_search
{
"query": {
"match": {
"anotherField": "otherValue"
}
}
}
深入ElasticSearch搜索机制
1)深入理解分词的逻辑,在使用_bulk api批量写入一批文档后,查询文档时,通过原有的字段是检索不到的,必须将其转换为小些。向products索引写入3条数据,分别为Apple的产品。
# _bulk api批量写入数据,一次写入3条数据
POST /products/_bulk
{"index": {"_id": 1}}
{"productID": "XHDK-1902-#fj3", "desc": "iPhone", "price": 30}
{"index": {"_id": 2}}
{"productID": "XHDK-1003-#446", "desc": "iPad", "price": 35}
{"index": {"_id": 3}}
{"productID": "XHDK-6902-#521", "desc": "MBP", "price": 40}
通过term query按iPhone进行检索时,是查不到数据的。原因是在存储文档时,elasticsearch对字段值进行了分词,数据字段按小写形式进行存储,当用iphone检索时是可以的。此外,elasticsearch中每个字段都有keyword属性,在用field.keyword查询时则可以进行完整的匹配。
# 直接用iPhone在desc#value查询,搜不到记录。但用desc.keyword可以,因为在保存文档时,iPhone在索引中已进行了小写
POST /products/_search
{
"query": {
"term": {
"desc.keyword": {
"value": "iPhone"
}
}
}
}
# 将query改为filter的方式,忽略TF-IDF算分问题,避免相关性算分的开销,提升查询性能
POST /products/_search
{
"explain": true,
"query": {
"constant_score": {
"filter": {
"term": {
"productID.keyword": "XHDK-1902-#fj3"
}
}
}
}
}
为了提升查询效率,可以用constant_score#filter来替换term query,因为其不进行算分,所以效率能高一些。同时,其也支持range query和exists操作符。
# 用range方式进行范围查询,通过doc.price进行过滤
GET /products/_search
{
"query": {
"constant_score": {
"filter": {
"range": {
"price": {
"gte": 20, "lte": 30
}
}
}
}
}
}
# 用exists来查找一些field值非空的文档,并将其进行返回
POST /products/_search
{
"query": {
"constant_score": {
"filter": {
"exists": {
"field": "desc"
}
}
}
}
}
2)query context与filter context影响算分的问题,默认情况下elasticsearch会按照匹配度问题给文档进行打分,在文档每部分可使用boost来影响其分数,当文档中两个字段都含关键词时,可通过boost设置权重,进而影响文档的排名。
# query context与filter context影响算分问题
POST /blogs/_bulk
{"index": {"_id": 1}}
{"title": "Apple iPad", "content": "Apple iPad,Apple iPad"}
{"index": {"_id": 2}}
{"title": "Apple iPad,Apple iPad", "content": "Apple iPad"}
# 通过boost指定每部分字段的权重,进而影响文档的算分排序
POST blogs/_search
{
"query": {
"bool": {
"should": [
{"match": {
"title": {
"query": "apple,ipad",
"boost": 1
}
}
},
{"match": {
"content": {
"query": "apple,ipad",
"boost": 2
}
}}
]
}
}
}
在bool查询中,must和should是算分的,而must_not则不计入算分,在检索示例中可通过must及must_not来过滤文档。默认情况下,用term query查询时,只要doc中包含关键字的频率高,则其相应的算分也会高。在具有相同数量关键词的字段中,doc长度越小的文档相关性越高。
# 批量写入关于apple的新闻数据,批量写入文档记录
POST news/_bulk
{"index": {"_id": 1}}
{"content": "Apple Mac"}
{"index": {"_id": 2}}
{"content": "Apple iPad"}
{"index": {"_id": 3}}
{"content": "Apple employee like Apple Pie and Apple Juice"}
# 然而并不是所期望的,返回了apple食品记录
POST news/_search
{
"query": {
"bool": {
"must": {
"match": {"content": "apple"}
}
}
}
}
可通过must_not对不符合条件的文档进行剔除,若只是想将不相关的文档分数减小,则可以通过boosting#positive或boosting#negative使得对文档进行重新的计分,这样不相关的文档也会进行展示,但其排名比较靠后。
# 用must_not排除pie字符串,只剩余电子产品
POST news/_search
{
"query": {
"bool": {
"must": {"match": {"content": "apple"}},
"must_not": {"match": {"content": "pie"}}
}
}
}
# 当不想删除时,可使用boosting#positive、negative方式排序
POST news/_search
{
"query": {
"boosting": {
"positive": {
"match": {"content": "apple"}
},
"negative": {
"match": {"content": "pie"}
},
"negative_boost": 0.5
}
}
}
3)disjunction query也是关于文档相关性的,若文档中有两部分都匹配,若想按文档匹配度高的那一部分排序的话(不按累加求和),则应使用此查询。同时,还可按tie_breaker对文档分数进行扰乱,进而影响文档的排名。
PUT /blogs/_bulk
{"index": {"_id": 1}}
{"title": "Quick brown rabbits", "body": "Brown rabbits are commonly seen"}
{"index": {"_id": 2}}
{"title": "Keeping pets happy", "body": "My quick brown fox eats rabbits on a regular basis."}
# 用dis_max#queries找两部分,各自评分最高的内容,此外还可通过tie_breaker进行调整
POST /blogs/_search
{
"query": {
"dis_max": {
"queries": [
{"match": {"title": "Brown fox"}},
{"match": {"body": "Brown fox"}}
],
"tie_breaker": 0.2
}
}
}
多字段查询的搜索语法,most_fields会累计多个字段的分数之和,cross_fields也就是当query在多个字段中存在时,就会返回结果,也就是所谓的跨字段查询。
PUT address/_doc/1
{
"street": "5 Poland Street",
"city": "London",
"country": "United Kingdom",
"postcode": "W1V 3DG"
}
# 使用most_fields是可以的,但增加operator:and就不可以了。可将type改为cross_fields,表示将query string在多个字段中进行检索
POST address/_search
{
"query": {
"multi_match": {
"query": "Poland Street W1V",
"fields": ["street", "city", "country", "postcode"],
"type": "cross_fields",
"operator": "and"
}
}
}
可以使用alias语法对索引进行重命名,应用场景多为elasticsearch索引数据备份,为避免应用服务端开发时修改配置,可做到无感数据源切换。
# index的alias操作,用于对address进行重命名
POST _aliases
{
"actions": [
{
"add": {
"index": "address",
"alias": "address_latest"
}
}
]
}
深入ElasticSearch聚合分析
elasticsearch聚合分metric和bucket两类,metric类似于一些指标(count、avg、sum等),而bucket相当于sql语句中的group by操作。
select count(brand)=>[metric] from cars group by brand=>[bucket];
一个简单的例子,通过elasticsearch请求分别统计max、min和avg的平均工资,size设置为0表示不返回原始文档。aggs表示聚合语法开始,其中max、min为聚合类型,里面的field值salary表示要聚合的字段。其实,简化语法可直接用stats替换max,其在一次执行中会统计出相关指标。
# Metrics聚合,找最低、最高及平均工资
POST employees/_search
{
"size": 0,
"aggs": {
"max_salary": {
"max": {
"field": "salary"
}
},
"min_salary": {
"min": {
"field": "salary"
}
},
"avg_salary": {
"avg": {
"field": "salary"
}
}
}
}
elasticsearch通过jobs#terms进行分桶操作,首先一点elasticsearch不能对text类型字段进行分桶(keyword是可以的),需打开fielddata的配置。aggs还可以嵌套,如下是对员工按age进行排序,并取前2位进行展示。
# 对keyword进行聚合,必须要用.keyword,避免分词,直接用job会报错,还可指定terms#size参数
POST employees/_search
{
"size": 0,
"aggs": {
"jobs": {
"terms": {
"field": "job.keyword"
},
"aggs": {
"old_employee": {
"top_hits": {
"size": 2,
"sort": [
{
"age": {
"order": "desc"
}
}
]
}
}
}
}
}
}
# 对text字段打开fielddata,支持terms aggregation
PUT employees/_mapping
{
"properties": {
"job": {
"type": "text",
"fielddata": "true"
}
}
}
cardinate操作相当于sql中的distinct count操作,可用于去重后的计数。salary还支持按range进行数量查询,其中key的值可以进行自定义。
# 对job.keyword进行聚合分析,cardinate操作,相当于做distinct count操作
POST employees/_search
{
"size": 0,
"aggs": {
"cardinate": {
"cardinality": {
"field": "job.keyword"
}
}
}
}
# salary range分桶,可以自定义桶#key,并按range进行查询
POST employees/_search
{
"size": 0,
"aggs": {
"salary_range": {
"range": {
"field": "salary",
"ranges": [
{
"to": 10000
},
{
"from": 10000,
"to": 20000
},
{
"key": ">20000",
"from": 20000
}
]
}
}
}
}
histogram用于展示员工薪资的直方图,field表示按哪个字段展示,interval为直方图每格的间隔大小。此外,elasticsearch还支持pipeline操作,其会将aggs后的结果再进行分析,常见的有min_bucket、max_bucket、avg_bucket等操作。
# salary Histogram,工资分布的直方图
POST /employees/_search
{
"size": 0,
"aggs": {
"salary_histogram": {
"histogram": {
"field": "salary",
"interval": 20000,
"extended_bounds": {
"min": 0,
"max": 100000
}
}
}
}
}
# elasticsearch pipeline操作, min_bucket最终选出最低平均工资,max_bucket则求最大的工作类型,avg_bucket只是所有类型工作的平均值,percentiles_bucket为百分位数的统计
POST /employees/_search
{
"size": 0,
"aggs": {
"jobs": {
"terms": {
"field": "job.keyword",
"size": 10
},
"aggs": {
"avg_salary": {
"avg": {
"field": "salary"
}
}
}
},
"min_salary_by_jobs": {
"percentiles_bucket": {
"buckets_path": "jobs>avg_salary"
}
}
}
}
Aggs Query聚合的filter这块,共分为Filter、Post_Filter和global这3种类型,第一个在aggs#old_person#filter中,其行为属于前置filter(也即先过滤再agg)。第二个属于post_aggs,先进行aggs然后只展示Dev Manager的bucket桶。而all#global{}会排除query#filter的作用,而对所有doc进行计算。
# Filter,先按age#from 从35岁开始filter
POST employees/_search
{
"size": 0,
"aggs": {
"old_person": {
"filter": {
"range": {
"age": {
"from": 35
}
}
},
"aggs": {
"jobs": {
"terms": {
"field": "job.keyword"
}
}
}
}
}
}
#post filter,相当于先做bucket分桶操作,然后再进行filter过滤
POST /employees/_search
{
"aggs": {
"jobs": {
"terms": {
"field": "job.keyword"
}
}
},
"post_filter": {
"match": {
"job.keyword": "Dev Manager"
}
}
}
ElasticSearch数据建模
数据建模-对象及Nested对象,例如blog文档中含User对象,结构类似于json。在用Rest接口进行查询时,可通过user.username进行嵌套式查询。
# 插入一条blog信息, user为嵌套的对象,包含3个字段
PUT nested_blog/_doc/1
{
"content": "I like elasticsearch",
"time": "2022-11-06T00:00:00",
"user": {
"userid": 1,
"username": "Jack",
"city": "ShangHai"
}
}
# 查询blog的信息,对text做了分词,不区分大小写了
POST nested_blog/_search
{
"query": {
"bool": {
"must": [
{"match": {"content": "elasticsearch"}},
{"match": {"user.username": "Jack"}}
]
}
}
}
当嵌套字段类型为数组时,通过bool查询其返回的结果会存在异常。此时,index的mapping和查询的dsl也必须改为nested query。
# 电影的mapping信息,对于数组类型字段,需将`type`改为`nested`
PUT my_movies
{
"mappings": {
"properties": {
"actors": {
"type": "nested",
"properties": {"first_name": {"type": "keyword"},
"last_name": {"type": "keyword"}}
},
"title": {
"type": "text",
"fields": {"keyword": {"type": "keyword", "ignore_above": 256}}
}
}
}
}
# 写入一条电影信息, actors部分为一个数组
PUT my_movies/_doc/1
{
"title": "Speed",
"actors": [{"first_name": "Keanu", "last_name": "Reeves"},
{"first_name": "Dennis", "last_name": "Hopper"}]
}
在进行数据检索时,bool类型的query,在json结构中也需指明nested.path,这样检索数据时,才会按同一个对象的first_name、last_name一起检索。此外,对于普通嵌套对象,Agg操作是不生效的。
# 查询电影信息,但是检索到了结果,需调整为Nested Query, 再根据条件筛选就正确
POST my_movies/_search
{
"query": {
"bool": {
"must": [
{"match": {"title": "Speed"}},
{"nested": {
"path": "actors",
"query": {
"bool": {
"must": [
{"match": {"actors.first_name": "Keanu"}},
{"match": {"actors.last_name": "Reeves"}}
]
}
}
}}
]
}
}
}
# 嵌套对象的Agg聚合操作,也需指定类型为Nested Query,普通Agg是不生效的
POST my_movies/_search
{
"size": 0,
"aggs": {
"actors": {
"nested": {
"path": "actors"
},
"aggs": {
"actor_name": {
"terms": {
"field": "actors.first_name",
"size": 10
}
}
}
}
}
}
elasticsearch中的父子文档,索引的mapping如下所示,blog_comments_relation#type为join,在relations中定义了blog和comment的对应关系。在写入blog文档时,blog_comments_relation#name的值为blog。
# Es中的父/子文档,blog_comments_relation#此part未看懂
PUT my_blogs
{
"settings": {
"number_of_shards": 2
},
"mappings": {
"properties": {
"blog_comments_relation": {
"type": "join",
"relations": {
"blog": "comment"
}
},
"content": {
"type": "text"
},
"title": {
"type": "keyword"
}
}
}
}
# 索引父文档,分别写入两个文档
PUT my_blogs/_doc/blog1
{
"title": "Learning Elasticsearch",
"content": "Learning ELK @ geektime",
"blog_comments_relation": {
"name": "blog"
}
}
PUT my_blogs/_doc/blog2
{
"title": "Learning Hadoop",
"content": "Learning Hadoop",
"blog_comments_relation": {
"name": "blog"
}
}
索引comment子文档,需在json结构中指定id为comment1和routing信息,其中index name值为comment,对应的parent值为blog1。通过my_blogs/_search可以查到所有文档列表:
# 索引子文档,需指定routing路由字段值
PUT my_blogs/_doc/comment1?routing=blog1
{
"comment": "I am learning ELk",
"username": "Jack",
"blog_comments_relation": {
"name": "comment",
"parent": "blog1"
}
}
PUT my_blogs/_doc/comment2?routing=blog2
{
"comment": "I like Hadoop !!!",
"username": "Jack",
"blog_comments_relation": {
"name": "comment",
"parent": "blog2"
}
}
# 查询所有文档,包含blog和comment两种类型
POST my_blogs/_search
{}
父子文档间的查询,通过父文档id查询,若查看blog#comment,则可以通过parent_id来查询,其中type值为comment。若想根据comment查询对应的blog,则可使用has_child注解。此外,可通过comment2和routing查看blog2下所有的评论数据。
# 根据父文档id查询
GET my_blogs/_doc/blog2
# parentId查询,依据blog2查到其下所有comment
POST my_blogs/_search
{
"query": {
"parent_id": {
"type": "comment",
"id": "blog2"
}
}
}
# has child查询返回父文档, has parent查询会返回子文档
POST my_blogs/_search
{
"query": {
"has_child": {
"type": "comment",
"query": {
"match": {
"username": "Jack"
}
}
}
}
}
# 通过id和routing来访问子文档
GET my_blogs/_doc/comment2?routing=blog2
对于elasticsearch中已有的index,要修改其某个字段类型时,只能对当前索引进行reindex操作。直接更新索引mapping文件,会抛出remote_transport_exception的异常。
# reindex api,类似于导数据
POST _reindex
{
"source": {
"index": "reindex_blogs"
},
"dest": {
"index": "blogs_fix"
}
}
elasticsearch中pipeline和painless脚本,可通过PUT请求直接注册一个blog_pipeline,processors可以有多种类型,像split会对指定字段进行切分,并且指定切分字符串为,。在索引文档时,可以指定blog_pipeline,这样存入文档的字段会被切分开。
# 为ES增加一个pipeline, 对index的文档进行计算
PUT _ingest/pipeline/blog_pipeline
{
"description": "a blog pipeline",
"processors": [
{
"split": {
"field": "tags",
"separator": ","
}
},
{
"set": {
"field": "views",
"value": 0
}
}
]
}
# 测试pipeline,确实tags字段被切分了,同时增加了views字段
POST _ingest/pipeline/blog_pipeline/_simulate
{
"docs": [
{
"_source": {
"title": "Introducing big data....",
"tags": "openstask,k8s",
"content": "you known, for cloud"
}
}
]
}
PUT tech_blogs/_doc/2?pipeline=blog_pipeline
{
"title": "Introducing big data....",
"tags": "openstask,k8s",
"content": "you known, for cloud"
}
painless脚本内容如下,在script语法中指定执行脚本,其中ctx可取上下文中定义的对象。
POST tech_blogs/_update/1
{
"script": {
"source": "ctx._source.views += params.views",
"params": {
"views": 100
}
}
}