Domain Specific Language 领域专用语言
DSL由叶子查询子句和复合查询子句两种子句组成。
无查询条件
无查询条件是查询所有,默认是查询所有的索引库的数据,或者使用match_all表示所有
GET /es_db/_doc/_search
{
"query":{
"match_all":{}
}
}
有查询条件
查询命令GET POST 都可以
模糊匹配主要是针对文本类型的字段,文本类型的字段会对内容进行分词,对查询时,也会对搜索条件进行分词,然后通过倒排索引查找到匹配的数据,模糊匹配主要通过match等参数来实现。
上面已经提及
# 查找address分词后的索引中含有广州的DOC
POST /es_db/_doc/_search
{
# from和size为分页参数,大数据时耗内存带宽,慎用
"from": 0,
"size": 2,
"query": {
"match": {
"address": "广州"
}
}
}
match条件还支持以下参数:
and:条件分词后都要匹配
or:条件分词后有一个匹配即可(默认)
下述搜索中,如果document中的remark字段包含java或developer词组,都符合搜索条件。
GET /es_db/_search
{
"query": {
"match": {
"remark": "java developer"
}
}
}
如果需要搜索的document中的remark字段,需要同时包含java和developer词组,则需要使用下述语法:
GET /es_db/_search
{
"query": {
"match": {
"remark": {
"query": "java developer",
"operator": "and"
}
}
}
}
上述语法中,如果将operator的值改为or。则与第一个案例搜索语法效果一致。默认的ES执行搜索的时候,operator就是or。
如果在搜索的结果document中,需要remark字段中包含多个搜索词条中的一定比例,可以使用下述语法实现搜索。其中minimum_should_match可以使用百分比或固定数字。百分比代表query搜索条件中词条百分比,如果无法整除,向下匹配(如,query条件有3个单词,如果使用百分比提供精准度计算,那么是无法除尽的,如果需要至少匹配两个单词,则需要用67%来进行描述。如果使用66%描述,ES则认为匹配一个单词即可。)。固定数字代表query搜索条件中的词条,至少需要匹配多少个。
GET /es_db/_search
{
"query": {
"match": {
"remark": {
"query": "java architect assistant",
"minimum_should_match": "68%"
}
}
}
}
如果使用should+bool搜索的话,也可以控制搜索条件的匹配度;should+must同理。具体看相应位置的说明。(should相当于match+or;must相当于match+and)
会对输入做分词,但是需要结果中也包含所有的分词,并且是连续的,而且顺序要求一样。以"hello world"为例,要求结果中必须包含hello和world,而且还要求他们是连着的,顺序也是固定的,hello that world不满足,world hello也不满足条件。
ES是如何实现match phrase短语搜索的?其实在ES中,使用match phrase做搜索的时候,也是和match类似,首先对搜索条件进行分词-analyze。将搜索条件拆分成hello和world。既然是分词后再搜索,ES是如何实现短语搜索的?
这里涉及到了倒排索引的建立过程。在倒排索引建立的时候,ES会先对document数据进行分词,如:
GET _analyze
{
"text": "hello world, java spark",
"analyzer": "standard"
}
# 分词结果
{
"tokens": [
{
"token": "hello",
"start_offset": 0,
"end_offset": 5,
"type": "",
"position": 0
},
{
"token": "world",
"start_offset": 6,
"end_offset": 11,
"type": "",
"position": 1
},
{
"token": "java",
"start_offset": 13,
"end_offset": 17,
"type": "",
"position": 2
},
{
"token": "spark",
"start_offset": 18,
"end_offset": 23,
"type": "",
"position": 3
}
]
}
从上述结果中,可以看到。ES在做分词的时候,除了将数据切分外,还会保留一个position。position代表的是这个词在整个数据中的下标。当ES执行match phrase搜索的时候,首先将搜索条件hello world分词为hello和world。然后在倒排索引中检索数据,如果hello和world都在某个document的某个field出现时,那么检查这两个匹配到的单词的position是否是连续的,如果是连续的,代表匹配成功,如果是不连续的,则匹配失败。
在做搜索操作的是,如果搜索参数是hello spark。而ES中存储的数据是hello world, java spark。那么使用match phrase则无法搜索到。在这个时候,可以使用match来解决这个问题。但是,当我们需要在搜索的结果中,做一个特殊的要求:hello和spark两个单词距离越近,document在结果集合中排序越靠前,这个时候再使用match则未必能得到想要的结果。
ES的搜索中,对match phrase提供了参数slop。slop代表match phrase短语搜索的时候,单词最多移动多少次,可以实现数据匹配(即单词分词后的position最多相差多少位),默认是0。在所有匹配结果中,多个单词距离越近,相关度评分越高,排序越靠前。使用slot,按照分析步骤来看,效率肯定是不高的。
这种使用slop参数的match phrase搜索,就称为近似匹配(proximity search) [prɒkˈsɪməti]。
经验分享:使用match和proximity search实现召回率和精准度平衡。
召回率:召回率就是搜索结果比率,如:索引A中有100个document,搜索时返回多少个document,就是召回率(recall)。
精准度:就是搜索结果的准确率,如:搜索条件为hello java,在搜索结果中尽可能让短语匹配和hello java离的近的结果排序靠前,就是精准度(precision)。
如果在搜索的时候,只使用match phrase语法,会导致召回率底下,因为搜索结果中必须包含短语(包括proximity search)。
如果在搜索的时候,只使用match语法,会导致精准度底下,因为搜索结果排序是根据相关度分数算法计算得到。
那么如果需要在结果中兼顾召回率和精准度的时候,就需要将match和proximity search混合使用,来得到搜索结果。
GET /test_a/_search
{
"query":{
"bool":{
"must":[
{"match":{"f":"java spark"}}
],
"should":[
{"match_phrase":{
# f是字段名
"f":{
"query":"java spark",
"slop":50
}
}
}
]
}
}
}
以下是举例:
# 索引库中有三个数据,title分别为:
# 中国是世界上人口最多的国家
# 北京是中国的首都
# 美国是世界上。。。的国家
# 我们如果想搜title中含有”中国“的数据,如果使用match搜索并且索引库用了默认的分词器,
# 则会把“中“和”国“拆开,这样美国的那个数据因为也含有“国”,所以也会被搜出来,这和es的分词插件有关,
# 但这结果这不是我们想要的,这时我们就可以使用match_phrase,把中国看成一个短语来搜。
GET /es_db/_doc/_search
{
"query": {
"match_phrase": {
"title": {
"query": "中国"
}
}
}
}
那么,现在我们要想搜索中国
和世界
相关的文档,但又忘记其余部分了,怎么做呢?用match
也不行,那就继续用match_phrase
试试:我们把查询的title改为:中国世界
返回结果也是空的,因为没有中国世界
这个短语。
我们搜索中国
和世界
这两个指定词组时,但又不清楚两个词组之间有多少别的词间隔。那么在搜的时候就要留有一些余地。这时就要用到了slop
了。相当于正则中的中国.*?世界
。这个间隔默认为0,导致我们刚才没有搜到,现在我们指定一个间隔。
GET /es_db/_doc/_search
{
"query": {
"match_phrase": {
"title": {
"query": "中国世界",
"slop": 2
}
}
}
}
现在,两个词组之间有了2个词的间隔,这个时候,就可以查询到结果了:“中国是世界上人口最多的国家”这条数据。
使用场景1:搜索推荐,比如百度的搜索输入框的提示(prefix前缀搜索也可以实现)
GET /test_a/_search
{
"query": {
"match_phrase_prefix": {
"f": {
"query": "java s",
"slop" : 10,
"max_expansions": 10
}
}
}
}
其原理和match phrase类似,是先使用match匹配term数据(java),然后在指定的slop移动次数范围内,前缀匹配(s),max_expansions是用于指定prefix最多匹配多少个term(单词),超过这个数量就不再匹配了。
这种语法的限制是,只有最后一个term会执行前缀搜索。
执行性能很差,毕竟最后一个term是需要扫描所有符合slop要求的倒排索引的term。
因为效率较低,如果必须使用,则一定要使用参数max_expansions。
使用场景2:
假设我们想查找含有某个单词的数据,但是我们忘了这个单词怎么拼写了,只记得前面几个字母。但这里用match
和match_phrase
都不太合适,输入的不是完整的词。那我们用match_phrase_prefix来实现:
# 查询desc中含有bea开头的单词的数据
POST /es_db/_doc/_search
{
"query": {
"match_phrase_prefix": {
"desc": "bea"
#"max_expansions": 1
}
}
}
使用这种行为进行搜索时,最好通过max_expansions
来设置最大的前缀扩展数量,因为产生的结果会是一个很大的集合,不加限制的话,影响查询性能。
查询多个字段中含有某个关键词,不一定同时都含有
# address或name中含有张三的数据
POST /es_db/_doc/_search
{
"query":{
"multi_match":{
"query":"张三",
"fields":["address","name"]
}
}
}
# 我们也可以结合phrase_prefix和phrase短语查询来使用,指定type即可
# 多字段前缀查询
GET /es_db/_doc/_search
{
"query": {
"multi_match": {
"query": "gi",
"fields": ["title"],
"type": "phrase_prefix"
}
}
}
#多字段短语查询
GET /es_db/_doc/_search
{
"query": {
"multi_match": {
"query": "girl",
"fields": ["title"],
"type": "phrase"
}
}
}
指定字段条件查询 query_string , 含 AND 与 OR 条件,AND和OR必须大写
AND 某个DOC的某个字段分词后必须同时含有AND指定的词
OR 某个DOC的某个字段分词后至少含有一个OR指定的词
POST /es_db/_doc/_search
{
"query":{
"query_string":{
"query":"admin OR 长沙",
"fields":["name","address"]
}
}
}
通常针对keyword类型字段,也就是不分词的字段。前缀搜索效率比较低。前缀搜索不会计算相关度分数。前缀越短,效率越低。如果使用前缀搜索,建议使用长前缀。因为前缀搜索需要扫描完整的索引内容,所以前缀越长,相对效率越高。
因为前缀搜索是用于keyword类型的字段,如果字段不是keyword类型还想做前缀搜索的话,ES给我们提供了这样一种方式[.keyword],供其他类型来使用前缀搜索,不提倡.
应用场景:百度的搜索输入的自动提示效果
GET /test_a/_search
{
"query": {
"prefix": {
# f不是keyword类型,需要声明一下
"f.keyword": {
"value": "J"
}
}
}
}
注意:针对前缀搜索,是对keyword类型字段而言。而keyword类型字段数据大小写敏感。
ES中也有通配符,但是和java还有数据库不太一样。通配符可以在倒排索引中使用,也可以在keyword类型字段中使用。
常用通配符:
? - 一个任意字符
* - 0~n个任意字符
GET /test_a/_search
{
"query": {
"wildcard": {
"f.keyword": {
"value": "?e*o*"
}
}
}
}
性能也很低,也是需要扫描完整的索引。不推荐使用。
ES支持正则表达式。可以在倒排索引或keyword类型字段中使用。
常用符号:
[] - 范围,如: [0-9]是0~9的范围数字
. - 一个字符
+ - 前面的表达式可以出现多次。
GET /test_a/_search
{
"query": {
"regexp" : {
"f.keyword" : "[A-z].+"
}
}
}
搜索的时候,可能搜索条件文本输入错误,如:hello world -> hello word。这种拼写错误还是很常见的。fuzzy技术就是用于解决错误拼写的(在英文中很有效,在中文中几乎无效。)。其中fuzziness代表value的值word可以修改多少个字母来进行拼写错误的纠正(修改字母的数量包含字母变更,增加或减少字母。)。f代表要搜索的字段名称。
GET /test_a/_search
{
"query": {
"fuzzy": {
"f" : {
"value" : "word",
"fuzziness": 2
}
}
}
}
总结:
slop
分词间隔。max_expanions
搭配。其实默认是50.......match_phrase
和match_phrase_prefix
的工作。# name字段中含有admin的数据
POST /es_db/_doc/_search
{
"query": {
"term": {
"name": "admin"
}
}
}
其中一些关键字:
POST /es_db/_doc/_search
{
"query" : {
"range" : {
"age" : {
"gte":25,
"lte":28
}
}
}
}
总结:
1.match会对搜索内容进行分词处理,拿着分词后的内容去查询索引;term不会对搜索的内容进行分词,直接拿着搜索的内容去查询索引。
2.通过term 和 match查询数据时细节点以及数据类型keyword与text区别?
1) term查询keyword字段。
term不会分词。而keyword字段也不分词。需要完全匹配才可。
2) term查询text字段。
因为text字段会分词,而term不分词,所以term查询的内容必须是text字段分词后的某一个。
3)match查询keyword字段
match查询keyword操作时,match不会进行分词,而keyword插入数据时也不会分词,所以等效term,即match的内容需要跟keyword的完全匹配(完全一样)才可以。
注意:对于keyword字段,如果使用?q参数拼接形式查询,并且指定分词器,这个时候就会把查询的内容进行分词,查找索引。如
GET /test_index2/_search?q=address:广州是个好地方&analyzer=ik_max_word
官方文档中说只有在q的时候才能使用指定分词器,但是经过实验,match的时候也能指定,指定后再查询keyword类型的字段时就可以实现分词的效果,因为官方文档没有标明,所以慎用,以官方为准。
GET /test_index2/_search
{
"query": {
"match": {
"address": {
"analyzer": "ik_max_word",
"query": "广州是个好地方"
}
}
}
}
4)match查询text字段
match分词,text也分词,只要match的分词结果和text的分词结果有相同的就匹配。
如果使用should+bool搜索的话,也可以控制搜索条件的匹配度。具体如下:下述案例代表搜索的document中的remark字段中,必须匹配java、developer、assistant三个词条中的至少2个。
GET /es_db/_search
{
"query":{
"bool":{
"should":[
{"match":{"remark":"java"}},
{"match":{"remark":"developer"}},
{"match":{"remark":"assistant"}}
],
"minimum_should_match":2
}
}
}
其实在ES中,执行match搜索的时候,ES底层通常都会对搜索条件进行底层转换,来实现最终的搜索结果(也就是说把match命令传过去,底层最终执行之前还是做一层转换)。
should相当于match+or;must相当于match+and
如:
# match的operator是or就会转成bool+should
GET /es_db/_search
{
"query": {
"match": {
"remark": "java developer"
}
}
}
转换后是:
GET /es_db/_search
{
"query": {
"bool": {
"should": [
{"term": {"remark": "java"}},
{"term": {"remark": {"value": "developer"}}}
]
}
}
}
# match的operator是and就会转成bool+must
GET /es_db/_search
{
"query":{
"match":{
"remark":{
"query":"java developer",
"operator":"and"
}
}
}
}
转换后是:
GET /es_db/_search
{
"query":{
"bool":{
"must":[
{"term":{"remark":"java"}},
{"term":{"remark":{"value":"developer"}}}
]
}
}
}
GET /es_db/_search
{
"query":{
"match":{
"remark":{
"query":"java architect assistant",
"minimum_should_match":"68%"
}
}
}
}
转换后为:
GET /es_db/_search
{
"query":{
"bool":{
"should":[
{"term":{"remark":"java"}},
{"term":{"remark":"architect"}},
{"term":{"remark":"assistant"}}
],
"minimum_should_match":2
}
}
}
must not 代表每个条件都必须不满足
建议,如果不怕麻烦,尽量使用转换后的语法执行搜索,效率更高。
如果开发周期短,工作量大,使用简化的写法。
搜索document中remark字段中包含java的数据,如果remark中包含developer或architect,则包含architect的document优先显示。(就是将architect数据匹配时的相关度分数增加)。
一般用于搜索时相关度排序使用。如:电商中的综合排序。将一个商品的销量、广告投放、评价值、库存、单价比较综合排序。在上述的排序元素中,广告投放权重最高,库存权重最低。
# 效果:remark字段必须包含java关键字;remark中包含architect的document排序靠前(权重大)
GET /es_db/_search
{
"query":{
"bool":{
"must":[
# remark字段必须包含java关键字
{"match":{"remark":"java"}}
],
"should":[
# remark中包含developer的document排序优先级低(权重小)
{"match":{
"remark":{
"query":"developer",
"boost":1
}
}
},
# remark中包含architect的document排序优先级高,优先排在前面(权重大)
{"match":{
"remark":{
"query":"architect",
"boost":3
}
}
}
]
}
}
}
best_fields策略: 搜索的document中的某一个field,尽可能多的匹配搜索条件;与之相反的是,尽可能多的字段匹配到搜索条件(most_fields策略)。如百度搜索使用best_fields这种策略,最匹配的到最前面,但是其他的就没什么区分度了,这种策略的优缺点:
优点:精确匹配的数据可以尽可能的排列在最前端,且可以通过minimum_should_match来去除长尾数据,避免长尾数据字段对排序结果的影响。(长尾数据:比如说我们搜索4个关键词,但很多文档只匹配1个,也显示出来了,这些文档其实不是我们想要的)
缺点:除了那些精准匹配的结果,其他差不多大的结果,排序结果不是太均匀,没有什么区分度了。
dis_max语法: 在多条件搜索中,如果根据某一个query条件查询的数据的相关度分数比较高,就以这个查询条件中相关字段的匹配度做相关度排序。
下述的案例中,就是找name字段中rod匹配相关度分数或remark字段中java developer匹配相关度分数,看根据哪个字段查找出来的数据的相关匹配度高,就使用哪一个字段的相关度分数进行结果排序。假设根据name字段查出来的数据的相关匹配度高,那么就以name这个字段的匹配度做相关排序。
GET /es_db/_search
{
"query":{
"dis_max":{
"queries":[
{"match":{"name":"rod"}},
{"match":{"remark":"java developer"}}
]
}
}
}
dis_max是将多个搜索query条件中相关度分数最高的某个条件中的字段的匹配度用于结果排序,忽略其他query分数,在某些情况下,可能还需要其他query条件中的相关度介入最终的结果排序,这个时候可以使用tie_breaker参数来优化dis_max搜索。
tie_breaker参数代表的含义是:将其他query搜索条件的相关度分数乘以参数值,再参与到结果排序中。如果不定义此参数,相当于参数值为0。所以其他query条件的相关度分数被忽略。
如下:最终影响数据排序的相关度的计算是:匹配度最高的字段的分值+其他的低匹配度的字段分值*0.5,这样其他字段也能影响最终的排序了。
GET /es_db/_search
{
"query":{
"dis_max":{
"queries":[
{"match":{"name":"rod"}},
{"match":{"remark":"java developer"}}
],
"tie_breaker":0.5
}
}
}
cross_fields:Crossfields搜索策略,是从多个字段中搜索条件数据。默认情况下,和most_fields搜索的逻辑是一致的,计算相关度分数是和bestfields策略一致的。一般来说,如果使用crossfields搜索策略,那么都会携带一个额外的参数operator,用来标记搜索条件如何在多个字段中匹配(and 或or)。(如果operator指定为or,则查询语义就和most_field一样了,都是命中一个和多个,但是most_field可以去除长尾数据)
具体语法如下:
# 下面语法代表的是,搜索条件中的java必须在name或remark字段中匹配
# developer也必须在name或remark字段中匹配
GET /es_db/_search
{
"query": {
"multi_match": {
"query": "java developer",
"fields": [
"name",
"remark"
],
"type": "cross_fields",
"operator": "and"
}
}
}
Multi-match query 的目的是多字段匹配,但 Multi-match query 中的 best_fields, most_fields, cross_fields这三种策略的含义是什么呢,下面通过举例解读:
(1)best_fields
为默认值,如果不指定,默认best_fields 匹配。
POST blogs/_search
{
"query": {
"multi_match": {
# 指定策略类型
"type": "best_fields",
"query": "hello world",
"fields": [
"title",
"body"
],
"tie_breaker": 0.2
}
}
}
# 与上述best_fields等价
POST blogs/_search
{
"query": {
"dis_max": {
"queries": [
{
"match": {
"title": "hello world"
}
},
{
"match": {
"body": "hello world"
}
}
],
"tie_breaker": 0.2
}
}
}
(2)most_fields
含义:主要是说尽可能返回更多field匹配到某个关键词的doc,优先返回回来。综合多个field一起进行搜索,尽可能多地让所有field的query参与到总分数的计算中来,此时就会是个大杂烩,出现类似best_fields案例最开始的那个结果,结果不一定精准,某一个document的一个field包含更多的关键字,但是因为其他document有更多field匹配到了,所以排在了前面。
优点:将尽可能匹配更多field的结果推送到最前面,整个排序结果是比较均匀的,可以去除长尾数据。
缺点:可能那些精准匹配的结果(比如一个doc的某一个字段高度匹配所有关键字,但是因为只命中了一个字段,导致命中多个字段的doc排在了前面),无法推送到最前面。
类似:bool + 多字段匹配。
等价举例:(两个例子一起看,容易理解)
most_fields 与下面的 bool 查询等价。
GET /titles/_search
{
"query": {
"multi_match": {
"query": "hello world",
# 指定策略
"type": "most_fields",
"fields": [
# 代表权重10
"title^10",
"title.std"
]
}
}
}
# 与上面的most_fields等价
GET titles/_search
{
"query": {
"bool": {
"should": [
{
"match": {
"title": {
"query": "hello world",
"boost": 10
}
}
},
{
"match": {
"title.std": "hello world"
}
}
]
}
}
}
(3)cross_fields
含义:跨字段匹配——待查询内容在多个字段中都显示。
类似:bool + dis_max 组合。
举例:
GET test003/_validate/query?explain=true
{
"query": {
"multi_match": {
"query": "Bruce Li",
"type": "cross_fields",
"fields": [
"first_name",
"last_name"
],
# and代表first_name和last_name都必须命中关键字
"operator": "and"
}
}
}
copy_to 可以替代cross_fields
总结:
mostfield策略问题:mostfields策略是尽可能匹配更多的字段,所以会导致精确搜索结果排序问题。又因为crossfields搜索,不能使用minimum_should_match来去除长尾数据。所以在使用mostfields和crossfields策略搜索数据的时候,都有不同的缺陷。所以商业项目开发中,都推荐使用bestfields策略实现搜索。
就是将多个字段,复制到一个字段中,实现一个多字段组合。copy_to可以解决cross fields搜索问题,在商业项目中,也用于解决搜索条件默认字段问题。
下面的mapping定义中,是新增了4个字段,分别是provice、city、street、address,其中provice、city、street三个字段的值,会自动复制到address字段中,实现一个字段的组合。那么在搜索地址的时候,就可以在address字段中做条件匹配,从而避免most fields策略导致的问题。在维护数据的时候,不需对address字段特殊的维护。因为address字段是一个组合字段,是由ES自动维护的。在存储的时候,未必存在,但是在逻辑上是一定存在的,因为address是由3个物理存在的属性province、city、street组成的。
如果我们添加了数据(省、市、街道),当我们查看数据的时候,address是没有数据的,但是你根据address字段执行条件查询的时候,会匹配到相应的doc。
如果需要使用copy_to语法,则需要在定义index的时候,手动指定mapping映射策略:
PUT /es_db
PUT /es_db/_mapping
{
"properties": {
"provice": {
"type": "text",
"analyzer": "standard",
# 指向address
"copy_to": "address"
},
"city": {
"type": "text",
"analyzer": "standard",
"copy_to": "address"
},
"street": {
"type": "text",
"analyzer": "standard",
"copy_to": "address"
},
"address": {
"type": "text",
"analyzer": "standard"
}
}
}
用一个大家容易理解的SQL语法来解释,如:
select count(*) from table group by column
那么group by column分组后的每组数据就是bucket。对每个分组执行的count(*)就是metric。
语法理解:
最外层肯定是aggs,如果其中平行定义了多个聚合名称,则这些名称就代表不同的聚合(不同的分组),这些分组之间互不影响,各自分各自的组;如果在一个聚合分组中嵌套了aggs,则就代表在当前分组中,进行再次聚合分组操作(terms、histogram等)或者统计分析metric操作。(注意做不同聚合间的隔离分析,这样分析语法就会清晰一些)
准备案例数据:
PUT /cars
{
"mappings": {
"properties": {
"price": {
"type": "long"
},
"color": {
"type": "keyword"
},
"brand": {
"type": "keyword"
},
"model": {
"type": "keyword"
},
"sold_date": {
"type": "date"
},
"remark": {
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
POST /cars/_bulk
{ "index": {}}
{ "price" : 258000, "color" : "金色", "brand":"大众", "model" : "大众迈腾", "sold_date" : "2021-10-28","remark" : "大众中档车" }
{ "index": {}}
{ "price" : 123000, "color" : "金色", "brand":"大众", "model" : "大众速腾", "sold_date" : "2021-11-05","remark" : "大众神车" }
{ "index": {}}
{ "price" : 239800, "color" : "白色", "brand":"标志", "model" : "标志508", "sold_date" : "2021-05-18","remark" : "标志品牌全球上市车型" }
{ "index": {}}
{ "price" : 148800, "color" : "白色", "brand":"标志", "model" : "标志408", "sold_date" : "2021-07-02","remark" : "比较大的紧凑型车" }
{ "index": {}}
{ "price" : 1998000, "color" : "黑色", "brand":"大众", "model" : "大众辉腾", "sold_date" : "2021-08-19","remark" : "大众最让人肝疼的车" }
{ "index": {}}
{ "price" : 218000, "color" : "红色", "brand":"奥迪", "model" : "奥迪A4", "sold_date" : "2021-11-05","remark" : "小资车型" }
{ "index": {}}
{ "price" : 489000, "color" : "黑色", "brand":"奥迪", "model" : "奥迪A6", "sold_date" : "2022-01-01","remark" : "政府专用?" }
{ "index": {}}
{ "price" : 1899000, "color" : "黑色", "brand":"奥迪", "model" : "奥迪A 8", "sold_date" : "2022-02-12","remark" : "很贵的大A6。。。" }
只执行聚合分组,不做复杂的聚合统计。在ES中最基础的聚合为terms,相当于SQL中的count。在ES中默认为分组数据做排序,使用的是doc_count数据执行降序排列。可以根据分组后的字段数据(sum、avg等)执行不同的排序方案;也可以根据_count元数据(就是doc_count),根据分组后的统计值执行不同的排序方案。
GET /cars/_search
{
# size指定为0代表不返回原始数据,只返回聚合后的数据
"size":0,
"aggs": {
"group_by_color": {
"terms": {
"field": "color",
"order": {
# 如果为desc,则效果和去掉order属性一样,默认就是按照doc_count倒序排列
"_count": "asc"
}
}
}
}
}
# 执行结果
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 8,
"relation" : "eq"
},
"max_score" : null,
# 因为上面指定size为0,所以原始数据为空
"hits" : [ ]
},
"aggregations" : {
"group_by_color" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "红色",
# 代表color为红色的doc数量为1
"doc_count" : 1
},
{
"key" : "白色",
"doc_count" : 2
},
{
"key" : "金色",
"doc_count" : 2
},
{
"key" : "黑色",
"doc_count" : 3
}
]
}
}
}
size可以设置为0,表示不返回ES中的文档,只返回ES聚合之后的数据,提高查询速度,当然如果你需要这些文档的话,也可以按照实际情况进行设置。
本案例先根据color执行聚合分组,在此分组的基础上,对组内数据执行聚合统计,这个组内数据的聚合统计就是metric。同样可以执行排序,因为组内有聚合统计,且对统计数据给予了命名avg_by_price,所以可以根据这个聚合统计数据字段名执行排序逻辑。
GET /cars/_search
{
"size": 0,
"aggs": {
"group_by_color": {
"terms": {
"field": "color",
"order": {
# 根据组内数据的price的平均值进行升序排列
"avg_by_price": "asc"
}
},
# 再次嵌套了一层aggs,代表分组后,组内统计
"aggs": {
"avg_by_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
先根据color聚合分组,在组内根据brand再次聚合分组,这种操作可以称为下钻分析。Aggs如果定义比较多,则会感觉语法格式混乱,aggs语法格式,有一个相对固定的结构。
简单定义:aggs可以嵌套定义,可以水平定义。
嵌套定义称为下钻分析。水平定义就是平铺多个分组方式。
GET /cars/_search
{
"size": 0,
"aggs": {
# 根据颜色分组
"group_by_color": {
"terms": {
"field": "color",
"order": {
# 每个颜色分组的价格升序排列
"avg_by_price_color": "asc"
}
},
# 嵌套
"aggs": {
# 计算每个颜色的平均价格
"avg_by_price_color": {
"avg": {
"field": "price"
}
},
# 在每个颜色的分组中再根据品牌分组
"group_by_brand": {
"terms": {
"field": "brand",
"order": {
"avg_by_price_brand": "desc"
}
},
# 嵌套
"aggs": {
# 计算每个颜色中每个品牌的平均价格
"avg_by_price_brand": {
"avg": {
"field": "price"
}
}
}
}
}
}
}
}
# 执行结果
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 8,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"group_by_color" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
# 根据颜色分组-金色
"key" : "金色",
# 金色的文档数量
"doc_count" : 2,
# 金色分组中的平均价格
"avg_by_price_color" : {
"value" : 190500.0
},
# 在金色分组中再根据品牌分组
"group_by_brand" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
# 金色分组中的大众品牌
"key" : "大众",
# 金色分组中的大众品牌数量
"doc_count" : 2,
# 金色分组中的大众品品牌的平均价格
"avg_by_price_brand" : {
"value" : 190500.0
}
}
]
}
},
{
"key" : "白色",
"doc_count" : 2,
"avg_by_price_color" : {
"value" : 194300.0
},
"group_by_brand" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "标志",
"doc_count" : 2,
"avg_by_price_brand" : {
"value" : 194300.0
}
}
]
}
},
{
"key" : "红色",
"doc_count" : 1,
"avg_by_price_color" : {
"value" : 218000.0
},
"group_by_brand" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "奥迪",
"doc_count" : 1,
"avg_by_price_brand" : {
"value" : 218000.0
}
}
]
}
},
{
"key" : "黑色",
"doc_count" : 3,
"avg_by_price_color" : {
"value" : 1462000.0
},
"group_by_brand" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "大众",
"doc_count" : 1,
"avg_by_price_brand" : {
"value" : 1998000.0
}
},
{
"key" : "奥迪",
"doc_count" : 2,
"avg_by_price_brand" : {
"value" : 1194000.0
}
}
]
}
}
]
}
}
}
GET /cars/_search
{
"size": 0,
"aggs": {
"group_by_color": {
"terms": {
"field": "color"
},
"aggs": {
"max_price": {
"max": {
"field": "price"
}
},
"min_price": {
"min": {
"field": "price"
}
},
"sum_price": {
"sum": {
"field": "price"
}
}
}
}
}
}
执行结果:
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 8,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"group_by_color" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "黑色",
"doc_count" : 3,
"max_price" : {
"value" : 1998000.0
},
"min_price" : {
"value" : 489000.0
},
"sum_price" : {
"value" : 4386000.0
}
},
{
"key" : "白色",
"doc_count" : 2,
"max_price" : {
"value" : 239800.0
},
"min_price" : {
"value" : 148800.0
},
"sum_price" : {
"value" : 388600.0
}
},
{
"key" : "金色",
"doc_count" : 2,
"max_price" : {
"value" : 258000.0
},
"min_price" : {
"value" : 123000.0
},
"sum_price" : {
"value" : 381000.0
}
},
{
"key" : "红色",
"doc_count" : 1,
"max_price" : {
"value" : 218000.0
},
"min_price" : {
"value" : 218000.0
},
"sum_price" : {
"value" : 218000.0
}
}
]
}
}
}
简述:聚合后,每一个聚合Bucket里面仅返回指定顺序的前N条数据。
在分组后,可能需要对组内的数据进行排序,并选择(展示)其中排名高的数据。top_hits中的属性size代表取组内多少条数据(展示多少数据,默认为10);sort代表组内使用什么字段什么规则排序(默认使用_doc的asc规则排序);_source代表结果中包含document中的那些字段(默认包含全部字段)。
GET cars/_search
{
"size": 0,
"aggs": {
# 根据brand聚合
"group_by_brand": {
"terms": {
"field": "brand"
},
"aggs": {
# 名称随便起
"top_car": {
"top_hits": {
# 取排名后前1条数据
"size": 1,
"sort": [
{
# 根据价格排序
"price": {
# 倒序
"order": "desc"
}
}
],
# 要展示的字段
"_source": {
"includes": [
"model",
"price"
]
}
}
}
}
}
}
}
执行结果:
{
"took" : 2,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 8,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"group_by_brand" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
# 大众品牌
"key" : "大众",
# 大众品牌的数量
"doc_count" : 3,
# 我们定义的名称
"top_car" : {
"hits" : {
"total" : {
"value" : 3,
"relation" : "eq"
},
"max_score" : null,
"hits" : [
# 排名后的前1条数据
{
"_index" : "cars",
"_type" : "_doc",
"_id" : "RNX6338BwiD60xe0-_x0",
"_score" : null,
# 之前指定的字段
"_source" : {
"price" : 1998000,
"model" : "大众辉腾"
},
# 因为我们指定了只根据价格排序,这里代表辉腾的价格
"sort" : [
1998000
]
}
]
}
}
},
{
"key" : "奥迪",
"doc_count" : 3,
"top_car" : {
"hits" : {
"total" : {
"value" : 3,
"relation" : "eq"
},
"max_score" : null,
"hits" : [
{
"_index" : "cars",
"_type" : "_doc",
"_id" : "R9X6338BwiD60xe0-_x0",
"_score" : null,
"_source" : {
"price" : 1899000,
"model" : "奥迪A 8"
},
"sort" : [
1899000
]
}
]
}
}
},
{
"key" : "标志",
"doc_count" : 2,
"top_car" : {
"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : null,
"hits" : [
{
"_index" : "cars",
"_type" : "_doc",
"_id" : "QtX6338BwiD60xe0-_x0",
"_score" : null,
"_source" : {
"price" : 239800,
"model" : "标志508"
},
"sort" : [
239800
]
}
]
}
}
}
]
}
}
}
histogram类似terms,也是进行bucket分组操作的,是根据一个field,实现数据区间分组。
如:以100万为一个范围,统计不同范围内车辆的销售量和平均价格。那么使用histogram的聚合的时候,field指定价格字段price。区间范围是100万-interval : 1000000。这个时候ES会将price价格区间划分为: [0, 1000000), [1000000, 2000000), [2000000, 3000000)等,依次类推。在划分区间的同时,histogram会类似terms进行数据数量的统计(count),可以通过嵌套aggs对聚合分组后的组内数据做再次聚合分析。
GET /cars/_search
{
"size": 0,
"aggs": {
# 名称随便起
"histogram_by_price": {
# histogram根据price聚合,每个组间隔1000000
"histogram": {
"field": "price",
"interval": 1000000
},
# 嵌套
"aggs": {
# 计算每个分组中的平均价格
"avg_by_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
执行结果:
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 8,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"histogram_by_price" : {
"buckets" : [
{
# 前区间的值[0.0,1000000.0)
"key" : 0.0,
"doc_count" : 6,
"avg_by_price" : {
"value" : 246100.0
}
},
{
"key" : 1000000.0,
"doc_count" : 2,
"avg_by_price" : {
"value" : 1948500.0
}
}
]
}
}
}
date_histogram可以对date类型的field执行区间聚合分组,如每月销量,每年销量等。
如:以月为单位,统计不同月份汽车的销售数量及销售总金额。这个时候可以使用date_histogram实现聚合分组,其中field来指定用于聚合分组的字段;
interval指定区间范围(可选值有:year、quarter、month、week、day、hour、minute、second);
format指定日期格式化;
min_doc_count指定每个区间的最少document,即每个区间文档数量最少有多少个才会返回对应区间的相关聚合数据,否则不会显示目标区间的信息(如果不指定,默认为0,当所有区间范围内都没有document,或都没有达到最少数量的doc时,也会显示bucket分组,只不过是个空分组),比如下面的例子,根据结果来看最多的doc数量为2,如果这个时候指定min_doc_count为3的话,那么查询结果就是个空的bucket分组,因为没有一个区间的doc的数量大于等于3;extended_bounds指定起始时间和结束时间(如果不指定,默认使用字段中日期最小值所在范围和最大值所在范围为起始和结束时间)。
(interval :7.x之前使用,7.x之后使用calender_interval)
GET /cars/_search
{
"size": 0,
"aggs": {
# 名称随便起
"histogram_by_date": {
# 日期聚合
"date_histogram": {
# 按照那个字段进行聚合
"field": "sold_date",
# 按月聚合,每个月一组
"calendar_interval": "month",
# 展示日期格式
"format": "yyyy-MM-dd",
"min_doc_count": 1,
# 要统计的目标数据的起始时间和结束时间
"extended_bounds": {
"min": "2021-01-01",
"max": "2022-12-31"
}
},
# 每个月的平均价格
"aggs": {
"sum_by_price": {
"sum": {
"field": "price"
}
}
}
}
}
}
执行结果:
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 8,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"histogram_by_date" : {
"buckets" : [
{
# 因为是按月聚合,这里就是每个月的起始时期
"key_as_string" : "2021-05-01",
# 对应上面的毫秒时间戳
"key" : 1619827200000,
"doc_count" : 1,
"sum_by_price" : {
"value" : 239800.0
}
},
{
"key_as_string" : "2021-07-01",
"key" : 1625097600000,
"doc_count" : 1,
"sum_by_price" : {
"value" : 148800.0
}
},
{
"key_as_string" : "2021-08-01",
"key" : 1627776000000,
"doc_count" : 1,
"sum_by_price" : {
"value" : 1998000.0
}
},
{
"key_as_string" : "2021-10-01",
"key" : 1633046400000,
"doc_count" : 1,
"sum_by_price" : {
"value" : 258000.0
}
},
{
"key_as_string" : "2021-11-01",
"key" : 1635724800000,
"doc_count" : 2,
"sum_by_price" : {
"value" : 341000.0
}
},
{
"key_as_string" : "2022-01-01",
"key" : 1640995200000,
"doc_count" : 1,
"sum_by_price" : {
"value" : 489000.0
}
},
{
"key_as_string" : "2022-02-01",
"key" : 1643673600000,
"doc_count" : 1,
"sum_by_price" : {
"value" : 1899000.0
}
}
]
}
}
}
在聚合统计数据的时候,有些时候需要对比部分数据和总体数据。
如:统计某品牌车辆平均价格和所有车辆平均价格。global是用于定义一个全局bucket,这个bucket会忽略query的条件,检索所有document进行对应的聚合统计。
GET /cars/_search
{
"size": 0,
"query": {
"match": {
"brand": "大众"
}
},
"aggs": {
"volkswagen_of_avg_price": {
"avg": {
"field": "price"
}
},
"all_avg_price": {
"global": {},
"aggs": {
"all_of_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
执行结果:
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 3,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
# 所有数据的平均价格
"all_avg_price" : {
"doc_count" : 8,
"all_of_price" : {
"value" : 671700.0
}
},
# 根据条件查询的部分数据的平局价格
"volkswagen_of_avg_price" : {
"value" : 793000.0
}
}
}
聚合类似SQL中的group by子句,search类似SQL中的where子句。在ES中是完全可以将search和aggregations整合起来,执行相对更复杂的搜索统计。
# 如:统计某品牌车辆每个季度的销量和销售额
GET /cars/_search
{
"size": 0,
"query": {
# 查询某个品牌
"match": {
"brand": "大众"
}
},
"aggs": {
"histogram_by_date": {
# 日期聚合
"date_histogram": {
# 聚合字段
"field": "sold_date",
# 按季度聚合
"calendar_interval": "quarter",
"min_doc_count": 1
},
"aggs": {
# 每个季度的价格总和
"sum_by_price": {
"sum": {
"field": "price"
}
}
}
}
}
}
在ES中,filter也可以和aggs组合使用,实现相对复杂的过滤聚合分析。
# 如:统计10万~50万之间的车辆的平均价格。
GET /cars/_search
{
"size": 0,
"query": {
"constant_score": {
"filter": {
"range": {
"price": {
"gte": 100000,
"lte": 500000
}
}
}
}
},
"aggs": {
"avg_by_price": {
"avg": {
"field": "price"
}
}
}
}
介绍下各种filter的作用:
1)setQuery(或者说filter):对搜索结果和aggregation都起作用,过滤的是agg之前的数据。
2)filter_agg: 只影响agg,过滤的是agg之前的数据(在进行agg之前过滤,这个时候查询结果结果hits已经确定了),不影响搜索。
3)post_filter: 只对搜索结果起作用,不影响agg结果;
注意:基于性能考虑,慎用post_filter
post_filter只有在对搜索结果和聚合结果做不同的过滤时才使用!因为post_filter的特性是查询之后执行的(根据命名也能看出),因为它是在查询之后执行的,对查询范围没有影响,所以才会对聚合也不会有影响。
将filter放在aggs内部(filter_agg),代表这个过滤器只对执行聚合的数据执行filter过滤,搜索结果hits该怎么展示就怎么展示。如果filter放在aggs外部(setQuery),过滤器则会过滤所有的数据(搜索结果和聚合数据)。
# 我想要某品牌汽车的所有数据以及统计某品牌汽车最近一年的销售总额
GET /cars/_search
{
"query": {
"match": {
"brand": "大众"
}
},
"aggs": {
"count_last_year": {
# filter_agg-聚合之前过滤数据,不影响展示返回的搜索结果hits
"filter": {
"range": {
"sold_date": {
"gte": "now-12M"
}
}
},
"aggs": {
"sum_of_price_last_year": {
"sum": {
"field": "price"
}
}
}
}
}
}
存在弊端:性能问题,大数据量不建议使用
POST /es_db/_doc/_search
{
"from": 0,
"size": 2,
"query": {
"match": {
"address": "广州天河"
}
}
}
滚动、游标查询,也叫深分页。针对海量数据的分页,如果不是海量数据,就没有太大的优势。
前面使用from和size方式,查询在1W条数据以内都是OK的,但如果数据比较多的时候,会出现性能问题。Elasticsearch做了一个限制,不允许查询的是10000条以后的数据。如果要查询1W条以后的数据,需要使用Elasticsearch中提供的scroll游标来查询。
在进行大量分页时,每次分页都需要将要查询的数据进行重新排序,这样非常浪费性能。使用scroll是将要用的数据一次性排序好,然后分批取出。性能要比from + size好得多。使用scroll查询后,排序后的数据会保持一定的时间,后续的分页查询都从该快照取数据即可。
# 快照数据保存1分钟,每次查询100条
GET /es_db/_search?scroll=1m
{
"query": {
"multi_match": {
"query": "广州长沙张三",
"fields": [
"address",
"name"
]
}
},
"size": 100
}
会返回scoll_id,后续,我们需要根据这个_scroll_id来进行查询
{
"_scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFDJ4Y0tHSUFCd2lENjB4ZTAzTEVsAAAAAAAQTmQWZjVHelU4TlVSUmFjM1BONFFvY3llUQ==",
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : 1.3862942,
"hits" : [
{
"_index" : "es_db",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.3862942,
"_source" : {
"address" : "广州"
}
}
]
}
}
GET _search/scroll?scroll=1m
{
"scroll_id":"FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFDJ4Y0tHSUFCd2lENjB4ZTAzTEVsAAAAAAAQTmQWZjVHelU4TlVSUmFjM1BONFFvY3llUQ=="
}
scroll_id:就相当于查询的钥匙,你下次查询的时候带着,他就知道你下一批要查的数据是哪一批了。但是你要在缓存的时间内去查,如果过期了,那就会出异常。非第一次请求中每次请求返回的scroll_id是不会变的。
PS:随着学习还会继续补充,仅供学习使用,不对的地方请指教,勿喷。