白日夢的Elasticsearch實戰筆記,32個查詢案例、15個聚合案例、7個查詢優化技巧。



一、導讀

Hi!大家久等了!時隔10天,白日夢的Elasticsearch筆記進階篇終於甘完了!本次更新依然是乾貨滿滿!

下面會和大家分享 32種查詢方法、15中聚合方式、7種優化後的查詢技巧。歡迎大家轉發支持!

如果對ES中的各種概念不太清楚可以去看上一篇文章,白日夢的ES筆記-基礎篇,並且有些概念不理解並不會影響你看懂本文中爲大家介紹的各種查詢方式。

下一篇(白日夢的ES系列筆記第三篇)文章會跟大家一起殺回到基礎部分,系統的做一次概念上的掃盲!

最後一篇(ES系列筆記第四篇)以編程語言實戰爲主,不出意外的話會以視頻的方式和大家見面。

文章公衆號首發! 歡迎關注白日夢!第一時間追更新!

點擊鏈接閱讀原文:json的格式會好看很多!



三、_search api 搜索api

search api也是我們最需要了解和掌握的APi。因爲絕大部分時間你使用ES就是爲了檢索嘛,所以下面一起看一下ES有哪些檢索API,當然最終的目的是大家有擁有選擇出一種適合自己業務的檢索方式的能力。

我又來吹牛了!

如果你不學白日夢跟你介紹的這些查詢方式、技巧。我敢說你八成不懂別人用Java或者Golang寫出來的代碼。

相反如果你看懂了下面的幾十個Case後,我敢說你自己可以分分鐘獨立的用熟悉的編程語言寫出對應的查詢代碼!

3.1、什麼是query string search?

所謂的query string search其實就是ES爲我們提供的一種檢索方式。下面這行請求就是典型的通過 query string search的方式進行檢索。

其實這種檢索方式很少用。直觀上看 query string search 這種檢索方式的特點就是它的請求參數全部寫在URI中。

GET /your_index/your_type/_search?q=*&sort=account_number:asc&pretty

解讀一下上面的 query string search: q=* ,表示匹配index=bank的下的所有doc,sort=account_number:asc表示告訴ES,結果按照account_number字段升序排序,pretty是告訴ES,返回一個漂亮的json格式的數據。

上面的q還可以寫成下面這樣:

GET /your_index/your_type/_search?q=自定義field:期望的值
GET /your_index/your_type/_search?q=+自定義field:期望的值
GET /your_index/your_type/_search?q=-自定義field:期望的值

解讀ES返回的響應如下(包括後面的query dsl的幾十種查詢案例的返回值也長這樣,並且下面不再重複分析這個返回值都有啥字段了,所以推薦你好好看下這個返回值再去瀏覽本文的重頭戲:query dsl 和 查詢優化技巧哈):

{
  "took" : 63,// 耗費的時間
  // 是否超時了,默認情況下不存在time_out,比如你的搜索耗時1分鐘,它就等1分鐘,但是不超時
  // 在發送搜索請求時可以指定超時時間
  // 比如你指定了10ms超時,它就會把這10ms內獲得的數據返回給你
  "timed_out" : false,
  "_shards" : { // 你的搜索請求打到了幾個shard上面去。
    // Primary Shard可以承接讀、寫流量。Replica Shard會承接讀流量。
    // 因爲我是默認配置,有五個primary shard。
    // 所以它的搜索請求會被打到5個分片上去,並且都成功了
    "total" : 5,
    "successful" : 5,
    "skipped" : 0,// 跳過了0個
    "failed" : 0 // 失敗了0個
  },
  "hits" : {//命中的情況
    "total" : 1000,// 命中率 1000個
    // _score 全文檢索時使用,這個相關性得分越高,說明doc和檢索的內容的越相關、越匹配
    // max_score就是最大的 _score
    "max_score" : null,
    // 默認查詢前10條,直接返回每個doc的完整數據
    "hits" : [ {   
      "_index" : "bank",// 索引
      "_type" : "_doc",// type
      "_id" : "0",// id 
      "sort": [0],
      "_score" : null,// 相關性得分
      // _source裏面存放的是doc的具體數據
      "_source" : 		{"account_number":0,
                       "balance":16623,
                       "firstname":"Bradshaw",
                       "lastname":"Mckenzie",
                       "age":29,
                       "gender":"F",
                       "address":"244 Columbus Place",
                       "employer":"Euron",
                       "email":"[email protected]",
                       "city":"Hobucken",
                       "state":"CO"}
    		},
		 {
      "_index" : "bank",
      "_type" : "_doc",
      "_id" : "1",
      "sort": [1],
      "_score" : null,
      "_source" : {"account_number":1,
                   "balance":39225,
                   "firstname":"Amber",
                   "lastname":"Duke",
                   "age":32,
                   "gender":"M",
                   "address":"880 Holmes Lane",
                   "employer":"Pyrami",
                   "email":"[email protected]",
                   "city":"Brogan",
                   "state":"IL"}
    }, ...
    ]
  }
}

指定超時時間: GET /_search?timeout=10ms 在進行優化時,可以考慮使用timeout, 比如: 正常來說我們可以在10s內獲取2000條數據,但是指定了timeout,發生超時後我們可以獲取10ms中獲取到的 100條數據。



3.2、什麼是query dsl?

dsl 全程 domain specified language

不論是query string search 還是這小節的query specified language它們本質上都是在發送Resutful類型的網絡請求。相對於 query string search 的將所有的請求參數都寫在URI中,query dsl 一般長下面這樣:

GET /yourIndex/yourType/_search
{
  // 很多請求參數
}

說的直白一點,query string search 更像是http中的GET請求,因爲它沒有請求體。而本小節的query dsl 更像是http 中的POST請求。



3.3、乾貨!32個查詢案例!

下面一起看一下有哪些query dsl的使用方式。(查詢的返回值和上面我們一起看的那個是一樣的,所以下面的重點是怎麼查,而不是怎麼看返回值哈)

1、查詢指定index下的全部doc

# _search是關鍵字,下文基本每個查詢都會有它,不再贅述了哈
GET /your_index/your_type/_search
{
  "query": { "match_all": {} }
}

2、針對name字段進行全文檢索(match查詢)

ES會將用戶將輸入的字符串通過分詞器拆解開,然後去倒排索引中掃描匹配(下一篇文章白日夢的筆記會重新殺回ES涉及的核心概念,包括這個倒排索引)。在倒排索引中哪怕匹配上了一個也會將結果返回。

GET /yourIndex/yourType/_search
{
   "query": { 
     # match表示全文檢索,所以白日夢會被分詞成 白日、夢、白日夢
     # 也就是說當前的match會匹配出name中有“白日” 或者“夢” 或者“白日夢”的doc
     "match": {
       "name":"白日夢"
     } 
   }
}
# 實際上,match query底層會被轉換成下面的格式進行檢索
#
# {
#    "bool":{
#        "should":[
#         {"term":{"title":"白日"}},
#					{"term":{"title":"白日夢"}},
#         {"term":{"title":"夢"}}
#     ]
#  }
# }
#

3、全文檢索:手動控制全文檢索的精度

GET /your_index/your_type/_search
{
   "query": { 
     "match": {
        "name":{
            "query":"bairi meng",
          	# and表示,只有同時出現bairi meng兩個詞的doc纔會被命中
            # 如果不加and限制,則bairi和meng之間是或的關係,只要出現一個就行
            "operator":"and",  
        }
     }
    }
}
# 添加上operator 操作會被ES轉換成下面的格式,將上面的should轉換成must
#
# {
#    "bool":{
#        "must":[
#         {"term":{"title":"bairi"}},
#         {"term":{"title":"meng"}}
#     ]
#  }
# }

4、去掉全文檢索的長尾

# 去長尾
GET /your_index/your_type/_search
{
   "query": { 
     "match": {
        "name":{
            "query":"歡迎關注白日夢!",
            "operator":"and",  
            # 上面的query可能被分詞成: 歡迎、關注、白日夢、歡迎關注、關注白日夢這五個詞。
            # 默認來說只要命中其中的一個詞,那個doc就會被返回,所以有長尾現象。
            # 去長尾:控制至少命中3/4個詞的doc纔算是真正命中。
            "minimum_should_match":"75%" 
        }
     }
    }
}
# 添加上 minimum_should_match 操作會被ES轉換成下面的格式 
#
# {
#    "bool":{
#        "should":[
#         {"term":{"title":"白日"}},
#         {"term":{"title":"夢"}}
#     ],
#       "minimum_should_match":3
#  }
# }
#  

5、全文檢索:通過boost控制權重。

如下Case:要求doc的name字段必須包含:“關注”,於此同時,如果doc的name字段中包含:“白日夢”,則將這個doc的權重提高爲3,如果name字段中包含了“公衆號” 再提高它的權重2。經過這樣的處理,name字段中包含:“關注白日夢公衆號” 的doc的權重就最高,它在搜索結果中的排名就越靠前。

GET /your_index/your_type/_search
{
   "query": { 
     "bool":{
     		"must":{
     			"match": {
       			 "name":{
       			 		# 默認情況下,所有字段的權重都是樣的,都是1
            		"query":"關注",
        			}
     				}
     		 },
     		 "should":[
     		 		{
     		 		"match": {
       			 "name":{
            		"query":"白日夢",
            		# 將name字段的權重提升成3
            		"boost":3 
        			}
     				}
     		 		},
     		 		{
     		 		"match": {
       			 "name":{
            		"query":"公衆號",
            		# 將name字段的權重提升成3
            		# 默認情況下,所有字段的權重都是樣的,都是1
            		"boost":2  
        			}
     				}
     		 	}
     		]
      }
   }
}   

6、稍微複雜一點的多條件查詢:bool查詢

GET /your_index/your_type/_search
{ 
  "query": {
  	# 比如你的查詢比較複雜,涉及到很多的子查詢,那你可以考慮通過bool查詢包裹這些子查詢
    # 每一個子查詢都會計算出這個doc針對於它這種查詢得到的相關性得分。
    # 最終由bool查詢將這些得分合併爲一個最終的得分
    "bool": {
      # 必須匹配到XXX, 並且會得出相關性得分
      # address中必須包含mill 
      "must": [ {"match": { "address": "mill" } }, 
      ],
      # 在滿足must的基礎上,should條件不滿足也可以,但是如果也匹配上了,相關性得分會增加
      # 如果沒有must的話,should中的條件必須滿足一個
      "should": [{ "match": { "address": "lane" } }],
      "must_not": [ # 一定不包含誰
        { "match": { "address": "mill" } },
      ]
		}
	}
}

7、bool查詢+去長尾。

# bool查詢+去長尾
GET /your_index/your_type/_search
{ 
  "query": {
    "bool":{
      "should":[
        "match":{"name":"白日夢1"},
     		"match":{"name":"白日夢2"},
    		"match":{"name":"白日夢3"},
      ],
    	"minimum_should_match":3
    }
  }
}

8、best fields策略:取多個query中得分最高的得分作爲doc的最終得分。

一個query中是存在多個match的(我們稱它爲多字段查詢),而且每個match都會貢獻自己的相關性得分,也就是說doc最終的相關性得分是通過這多個match貢獻的相關性得分通過一定的機制計算出來的。而且相關性得分越高,文檔在搜索結果中就越靠前。

這時,如果你不希望讓doc的最終得分是通過綜合所有的match計算得出的,可以使用dis_max查詢。它會取所有match中得分最高的match當作doc的最終得分。

GET /your_index/your_type/_search
{
   "query": { 
     # 這種用法不容忽略
     # 直接取下面多個query中得分最高的query當成最終得分
     "dis_max": {
        "queries":[
           {"match":{"name":"白日夢"}},
           {"match":{"content":"關注白日夢!"}}
        ]
     }
   }
}

9、基於 tie_breaker 優化dis_max

上面的Case中有提到這個dis_max查詢,這個dis_max也是實現best field的關鍵,即:它會取所有match中得分最高的match當作doc的最終得分。

而這個例子中的tie_breaker會重新讓dis_max考慮到其他field的得分影響,比如下面的0.4,表示最終的doc得分會考慮其他match的影響,但是它的影響會被弱化成原來的0.4。

GET /your_index/your_type/_search
{   
    # 基於 tie_breaker 優化dis_max
    # tie_breaker可以使dis_max考慮其它field的得分影響
    "query": { 
     # 直接取下面多個query中得分最高的query當成最終得分
     # 這也是best field策略
     "dis_max": { 
        "queries":[
           {"match":{"name":"關注"}},
           {"match":{"content":"白日夢"}}
        ],
        "tie_breaker":0.4
     }
    }
}   

10、同時在你指定的多個字段中進行檢索:multi_match

GET /your_index/your_type/_search
{    
  # 查詢多個,在下面指定的兩個字段中檢索含有 “this is a test“ 的doc
  "query": { 
    "multi_match" : {
      "query":    "this is a test", 
      "fields": [ "subject", "message" ] 
    }
  }
}

11、使用multi_match query簡化dis_max

# 還是這個dis_max query,如下:
GET /your_index/your_type/_search
{   
    # 基於 tie_breaker 優化dis_max
    # tie_breaker可以使dis_max考慮其它field的得分影響
    "query": { 
     # 直接取下面多個query中得分最高的query當成最終得分
     # 這也是best field策略
     "dis_max": { 
        "queries":[
           {"match":{"name":"關注"}},
           {"match":{"content":"白日夢"}}
        ],
        "tie_breaker":0.4
     }
    }
} 

# 使用multi_match query簡化寫法如下:
GET /your_index/your_type/_search
{    
    "query": { 
       "multi_match":{
           "query":"關注 白日夢",
 					  # 指定檢索的策略 best_fields(因爲dis_max就是best field策略)
           "type":"best_fields",
  					# content^2 表示增加權重,相當於:boost2
           "fields":["name","content^2"],
					 "tie_breaker":0.4,
					 "minimum_should_match":3
       }
    }
}

12、most field策略和上面說的best field策略是不同的,因爲best field策略說的是:優先返回某個field匹配到更多關鍵字的doc。

優先返回有更多的field匹配到你給定的關鍵字的doc。而不是優先返回某個field完全匹配你給定關鍵字的doc

另外most_fields不支持使用minimum_should_match去長尾。

GET /your_index/your_type/_search
{    
    # most_fields策略、優先返回命中更多關鍵詞的doc
    # 如下從title、name、content中搜索包含“賜我白日夢”的doc
    "query": { 
       "multi_match":{
           "query":"賜我白日夢",
 					  # 指定檢索的策略most_fields
           "type":"most_fields",
           "fields":["title","name","content"]
       }
    }
}

13、cross_fields策略:如下Case

GET /your_index/your_type/_search
{    
    "query": { 
       "multi_match":{
           "query":"golang java",
         		# cross_fields 要求golang:必須在title或者在content中出現
            # cross_fields 要求java:必須在title或者在content中出現
           "type":"cross_fields",
           "fields":["title","content"]
       }
    }
}

14、查詢空

GET /your_index/your_type/_search
{   
  "query": { 
    "match_none": {}
  }
}

15、精確匹配

# 使用trem指定單個字段進行精確匹配
GET /your_index/your_type/_search
{   
  # 精確匹配name字段爲白日夢的doc
  "query": { 
  	"constant_score":{
  			"filter":{
					"term": {
  					"name":"白日夢"
				 } 
			}
		}
	} 
}

# 使用terms指定在多個字段中進行精確匹配
# 下面的例子相當於SQL: where name in ('tom','jerry')
GET /your_index/your_type/_search
{
   # 精確匹配
  "query": { 
  	"constant_score":{
  			"filter":{
					"terms": {
  					"想搜索的字段名":[
  								"tom",
  						    "jerry"
  					]
				 } 
			}
		}
	} 
} 

16、短語檢索:要求doc的該字段的值和你給定的值完全相同,順序也不能變,所以它的精確度很高,但是召回率低。

GET /your_index/your_type/_search
{   
  # 短語檢索 
  # 順序的保證是通過 term position來保證的
  # 精準度很高,但是召回率低
  "query": {  
  				# 只有name字段中包含了完整的 白日夢 這個doc纔算命中
  			  # 不能是單個 ”白日“,也不能是單個的 “夢”,也不能是“白日xxx夢”
  			  # 要求 短語相連,且順序也不能變
         "match_phrase": { 
             "name": "白日夢"
					}
		}
}

17、提高短語檢索的召回率

如果使用match_phase進行短語檢索,本質上就是要求doc中的字段值和給定的值完全相同,即使是順序不同也不行。但是爲了提高召回率如你又想容忍短語匹配可以存在一定的誤差,比如你希望搜索 “i love world” 時,能夠搜索出''world love i"

這時可以通過slop來實現這個功能,slop可以幫你讓指定短語中的詞最多經過slop次移動後如果能匹配某個doc,也把這個doc當作結果返回給用戶。

GET /your_index/your_type/_search
{    
   # 短語檢索
   "query": {
    	   # 指定了slop就不再要求搜索term之間必須相鄰,而是可以最多間隔slop距離。
         # 在指定了slop參數的情況下,離關鍵詞越近,移動的次數越少, relevance score 越高。
         # match_phrase +  slop 和 proximity match 近似匹配作用類似。
         # 平衡精準度和召回率。
         "match_phrase": { 
             "address": "mill lane",
 						 # 指定搜索文本中的幾個term經過幾次移動後可以匹配到一個doc
             "slop":2
          } 
  }
}

18、混合使用match和match_phrase 平衡精準度和召回率

GET /your_index/your_type/_search
{    
   # 混合使用match和match_phrase 平衡精準度和召回率
   "query": { 
      "bool": {  
      	"must":  {
          	# 全文檢索雖然可以匹配到大量的文檔,但是它不能控制詞條之間的距離
          	# 可能i love world在doc1中距離很近,但是它卻被ES排在結果集的後面
          	# 它的性能比match_phrase和proximity高
         		"match": {
            	"title": "i love world" 
          	} 
     		 },
      	"should": {
            # 因爲slop有個特性:詞條之間間隔的越近,移動的次數越少 最終的得分就越高
          	# 於是可以藉助match_phrase+slop感知term position的功能
          	# 實現爲距離相近的doc貢獻分數,讓它們靠前排列
          	"match_phrase":{
              	"title":{
                  	"query":"i love world",
                  	"slop":15
              	}
          	}
      	}
  	}
}

19、使用rescore_query重打分。提高精準度和召回率。

GET /your_index/your_type/_search
{    
   # 重打分機制
   "query": { 
       "match":{
           "title":{
               "query":"i love world",
               "minimum_should_match":"50%"
           }
       },
       # 對全文檢索的結果進行重新打分
       "rescore":{
       		 # 對全文檢索的前50條進行重新打分
           "window_size":50,  
           "query": { 
               # 關鍵字
               "rescore_query":{ 
               		  # match_phrase + slop 感知 term persition,貢獻分數
                    "match_phrase":{ 
                       "title":{
                           "query":"i love world",
                           "slop":50
                     }
                }
          }
       } 
   }
}

20、前綴匹配:搜索 user字段以"白日夢"開頭的 doc

GET /your_index/your_type/_search
{    
  # 前綴匹配,相對於全文檢索,前綴匹配是不會對前綴進行分詞的。
  # 而且每次匹配都會掃描整個倒排索引,直到掃描完一遍纔會停下來
  # 前綴搜索不會計算相關性得分所有的doc的得分都是1
  # 前綴越短能匹配到的doc就越多,性能越不好
  "query": { 
    "prefix" : { "user" : "白日夢" }
  }
}

21、前綴搜索 + 添加權重

GET /your_index/your_type/_search
{    
  # 前綴搜索 + 添加權重
  "query": { 
    "prefix" : { 
  		"name" :  { 
  			"value" : "白日夢", 
  			"boost" : 2.0 
			}
		}
  }
}

22、通配符搜索

GET /your_index/your_type/_search
{    
  # 通配符搜索
  "query": {
        "wildcard" : { 
  					"title" : "白日夢的*筆記"
				}
   }
}


GET /your_index/your_type/_search
{    
  # 通配符搜索
  "query": {
        "wildcard" : {
  				"title" : { 
  					"value" : "白日夢的*筆記", 
  					"boost" : 2.0 
					} 
			}
   }
}

23、正則搜索

GET /your_index/your_type/_search
{    
   # 正則搜索  
   "query": {
        "regexp":{
            "name.first":{
                "value":"s.*y",
                "boost":1.2
            }
        }
    }
}

24、搜索推薦:match_phrase_prefix,最終實現的效果類似於百度搜索,當用戶輸入一個詞條後,將其它符合條件的詞條的選項推送出來。

match_phrase_prefix和match_phrase相似,但是區別是它會將最後一個term當作前綴,發起一次搜索。因此它也叫search time 搜索推薦,因爲它是在你搜索的時候又發起了一次新的請求來拿到推薦的內容,它的效率整體也是比較低的。

GET /your_index/your_type/_search
{    
   "query": {
      # 前綴匹配(關鍵字)
      "match_phrase_prefix" : {
        "message" : {
     						# 比如你搜索關注白日夢,經過分詞器處理後會得到最後一個詞是:“白日夢”
     					  # 然後他會拿着白日夢再發起一次搜索,於是你就可能搜到下面的內容:
                # “關注白日夢的微信公衆號”
     						# ”關注白日夢的圈子“
                "query" : "關注白日夢",
                # 指定前綴最多匹配多少個term,超過這個數量就不在倒排索引中檢索了,提升性能
                "max_expansions" : 10,
                # 提高召回率,使用slop調整term persition,貢獻得分
                "slop":10
            }
       } 
  }
}

25、Function Score Query

Function Score Query 實際上是一種讓用戶可以自定義實現一種對doc得分進行增強的手段。比如:用戶可以自定義一個function_secore 函數,然後指定將這個field的值和ES計算出來的分數相乘,作爲doc的最終得分。

# Case1
GET /your_index/your_type/_search
{    
  "query": {
        "function_score": {
          	# 正常寫一個query
            "query": { 
          		"match": {
          			"query":"es"
        	    } 
  					},
  				  # 自定義增強策略
  					“field_value_factor”:{
  						# 對檢索出的doc的最終得分都要multiply上star字段的值
              "field":"star",
            }
            "boost_mode":"multiply",
						# 限制最大的得分不能超過maxboost指定的值。
						"maxboost":3
        }
    }
}

# Case2
GET /your_index/your_type/_search
{    
  "query": {
        "function_score": {
            "query": { 
          		"match": {
          			"query":"es"
        	    } 
  					},
  					“field_value_factor”:{
  						# 對檢索出的doc的最終得分都要multiply上star字段的值
  					 	# 這時有個問題,假如說star字段的值爲0,那最終結果豈不是都爲0?
              "field":"star",
              # 所以考慮使用modifier優化一下
              # newScore = oldScore + log(1+star)
              "modifier":"log1p",
            }
            "boost_mode":"multiply",
						"maxboost":3
        }
    }
}

# Case3
GET /your_index/your_type/_search
{    
  "query": {
        "function_score": {
            "query": { 
          		"match": {
          			"query":"es"
        	    } 
  					},
  					“field_value_factor”:{
              "field":"star",
              "modifier":"log1p",
              # 使用factor將star字段對權重的影響降低成1/10
              # newScore = oldScore + log( 1 + star*factor )
  						"factor":0.1
            }
            "boost_mode":"multiply",
						"maxboost":3
        }
    }
}

# 補充boost_mode有哪些中選項
multiply、sum、min、max、replace

26、Fuzzy Query 模糊查詢會提供容錯的處理

GET /your_index/your_type/_search
{    
   # Fuzzy Query 模糊查詢會提供容錯的處理
   "query": {
        "fuzzy" : {
            "user" : {
                "value": "白日夢",
                "boost": 1.0,
                # 最大的糾錯次數,一般設爲之AUTO
                "fuzziness": 2,
                # 不會被“模糊化”的初始字符數。這有助於減少必須檢查的術語的數量。默認值爲0。
                "prefix_length": 0,
                # 模糊查詢將擴展到的最大項數。默認值爲50
                "max_expansions": 100 
                # 是否支持模糊變換(ab→ba)。默認的是false
                transpositions:true 
            }
        }
    }
}

27、解讀一個實用的案例

GET /your_index/your_type/_search
{ 
  "query": {
  	# 比如你的查詢比較複雜,涉及到很多的子查詢,那你可以考慮通過bool查詢包裹這些子查詢
    # 每一個子查詢都會計算出這個doc針對於它這種查詢得到的相關性得分。
    # 最終由bool查詢將這些得分合併爲一個最終的得分
    "bool": {
      # 必須匹配到XXX, 並且會得出相關性得分
      # address中必須包含mill 
      "must": [ {
        	"match": {
          "address": "mill" 
           } 
        }, 
      ],
      # 在滿足must的基礎上,should條件不滿足也可以,但是如果也匹配上了,相關性得分會增加
      # 如果沒有must的話,should中的條件必須滿足一個
      "should": [
        { "match": { "address": "lane" } }
      ],
      "must_not": [ # 一定不包含誰
        { "match": { "address": "mill" } },
      ],
			# filter中的表達式僅僅對數據進行過濾,但是不會影響搜索結果的相關度得分。
			# 所以你如果不希望添加的過濾條件影響最終的doc排序的話,可以將條件放在filter中。
			# query是會計算doc的相關度得分的,得分越高,越靠前。
      "filter": { 
        "range": { # 按照範圍過濾
          "balance": { # 指定過濾的字段
            "gte": 20000s # 高於20000
            "lte": 30000  # 低於30000
          }
        }
      }
    }
  }

默認的排序規則是按照_score降序排序,但像上面說的那樣,如果全部都是filter的話它就不會計算得分,也就是說所有的得分全是1,這時候就需要定製排序規則,定義的語法我在上面寫了

28、查詢名稱中包含“白日夢”的doc,並且按照star排序

高亮、排序、分頁以及_source 指定需要的字段都可以進一步作用在query的結果上。

# ES默認的排序規則是按照 _score 字段降序排序的

# 但是ES允許你像下面這樣定製排序規則
GET /your_index/your_type/_search
{
   "query": { 
     "match": {"name":"白日 夢"}
   },
  # 指定排序條件
  "sort":[
    # 指定排序字段爲 star
    {"star":"desc"}
  ]
}   

29、分頁查詢

如:從第一條doc開啓查,查10條。(如果你不使用from、to搜索的話,默認就搜索前10條)

GET /your_index/your_type/_search
{
   "query": { "match_all": {} },
 	  "from": 0, # 0:是第一個doc
    "size": 10
}   

# 還可以像這樣發起分頁請求
GET /your_index/your_type/_search?size=10
GET /your_index/your_type/_search?size=10&from=20

# deep paging 問題
比如系統中只有3個primary shard,1個replica shard,共有6W條數據。
用戶希望查詢第1000頁,每頁10條數據。也就是1000*10 = 10001 ~ 10010 條數據
假如說用戶將這個分頁請求會打向ES集羣中的replica shard,接下來會發生什麼?
回答:
接收到請求的shard 我們稱它爲coordinate node(協調節點),它會將請求轉發到三個primary,
每個primary shard都會取出它們的第1~10010條數據id,返回給coordinate node,
也就是說coordinate node總共會接收到30030個id,然後coordinate node再拿着這些id發起mget請求獲取數據
對獲取到的結果30030排序處理,最後取相關性得分最高的10條返回給用戶。

所以當分頁過深的時候是非常消耗內存、網絡帶寬、CPU的。

30、指定要查詢出來的doc的某幾個字段。如下:

# 假設白日夢對應的json長下面這樣:
{
  "name":"白日夢",
  “address”:"beijing",
  "gender":"man"
}

# 然後我只想檢索出name字段,其他的不想知道,可以像下面這樣通過_sorce限制
GET /your_index/your_type/_search
{
   "query": { "match_all": {} },
   # ES會返回全文JSON,通過_source可以指定返回的字段
 	 "_source": ["name"],
}  

31、filter過濾,查詢name中包含白日夢,且star大於100的doc。

GET /your_index/your_type/_search
{
   "query": { 
     # 可以使用bool封裝包括多個查詢條件
     “bool":{
      	"must":{"match": {"name":"白日 夢"}}
				# 指定按照star的範圍進行filter
 			  "filter":{
          	# range既能放在query中,也能放在filter中。
            # 如果放在filter中,range過濾的動作不會影響最終的得分。
            # 但是放在query中,range動作會影響最終的得分。
          	"range":{
							“star”:{"gt":100}
        		 }
         }
  	  }
   }
}  

# 拓展:
# 關於range還可以像這樣過濾時間
"range":{
  # 指定birthday範圍爲最近一個月的doc
  "birthday":{
    "gt":"2021-01-20||-30d"
  }
}

# 或者使用now語法
  # 指定birthday範圍爲最近一個月的doc
  "birthday":{
    "gt":"now-30d"
  }
}

32、指定對返回的doc中指定字段中的指定單詞高亮顯示。

GET /your_index/your_type/_search
{
   "query": { 
    	"match": {"name":"白日 夢"}	
  	},
  	"highlight":{ # 高亮顯示
   		 "fields":{  # 指定高亮的字段爲 firstname
     		 "firstname":{}
  	 }
} 

# 最終得到的返回值類似下面這樣
  ... 
  "hits" : {
    "total" : 1000,# 1000個
    "max_score" : null,
    "hits" : [ {   
      "_index" : "bank",
      "_type" : "_doc",
      "_id" : "0",
      "sort": [0],
      "_score" : 0.777777,
      "_source" : 		{"account_number":0,
                       "balance":16623,
                       "firstname":"我是白",
                       "lastname":"日夢",
                       "state":"CO"}
    	}],
			"highlight":{
   		 "firstname":[
         "我是<em>白</em>"
       ]
 }
 ...

參考:https://www.elastic.co/guide/en/elasticsearch/reference/6.2/query-dsl.html



四、聚合分析



4.1、什麼是聚合分析?

聚合分析有點類似於SQL語句中的那種group by、where age > 20 and age < 30、這種操作。常見的聚合分析就是根據某一個字段進行分組分析,要求這個字段是不能被分詞的,如果被聚合的字段被分詞,按照倒排索引的方式去索引的話,就不得不去掃描整個倒排索引(纔可能將被聚合的字段找全,效率很低)。

聚合分析是基於doc value的數據結果集進行操作的,這個doc value 其實就是正排索引(現在瞭解就好,下一篇文章統一掃盲),

關於聚合分析有三個重要的概念:

  • bucket

    特別是你去使用一下java、golang中的es相關的api,就會看到這個bucket關鍵字,bucket就是聚合操作得到的結果集。

  • metric

    metric就是對bucket進行分析,比如取最大值、最小值、平均值。

  • 下鑽

    下鑽就是在現有的分好組的bucket繼續分組,比如可以先按性別分組、下鑽再按年齡分組。



4.2、乾貨!15個聚合分析案例

1、比如我們公司人很多,其中不泛有很多重名的人,現在我的需求是:我想知道我們公司中有多個人叫tom、多少個人叫jerry,也就是說,我想知道:重名的人分別有多少個。於是我們需要像下面這樣根據名字聚合。

聚合的結果中天然存在一個metric,它就是當前bucket的count,也就是我們想要的結果:

GET /your_index/your_type/_search
{	
  # 表示只要聚合的結果,而不要參與聚合的原始數據
  “size”:0,
  # 使用聚合時,天然存在一個metric,就是當前bucket的count
  "aggs": {
    "group_by_name": { # 自定義的名字
      "term": {
        "field": "name" # 指定聚合的字段, 意思是 group by name
      }
    }
  }
} 

GET /your_index/your_type/_search
{	
  “size”:0,
   # 使用聚合時,天然存在一個metric,就是當前bucket的count
  "aggs": {
    "group_by_xxx": { # 自定義的名字
  	 # 除了使用term還可以使用terms
     # trems允許你指定多個字段
     "terms": {
         # 指定聚合的字段, 意思是 group by v1、v2、v3
        "field": {"value1","value2","value3"} 
      }
    }
  }
} 

2、先搜索,再對搜索結果聚合。比如我想知道在所有的男生中的重名情況

GET /your_index/your_type/_search
{	
  # 先查詢
  “query”:{
    	"term":{
        "gender":"man"
      }
  },
   # 再聚合
  "aggs": {
    "group_by_name": { 
      "term": {
        "field": "name" # 指定聚合的字段, 意思是 group by name
      }
    }
  }
} 

3、我想把重名的人分成一組,然後我想了解每組人的平均年齡。可以像下面這樣幹

GET /your_index/your_type/_search
{	
  "size":0,
	 # 聚合中嵌套聚合,意思是先 group by avg age,再 group by field1。
  "aggs": {
    "group_by_name": {
      "terms": {
        "field": "name"
      },
		 	 # 在上面的name分組的結果之上再按照age聚合
      "aggs": { 
        "average_age": {
          # 指定聚合函數爲avg
          "avg": {
            "field": "age"
          }
        }
      }
    }
  }
} 

4、我想了解我們公司不同年齡段:20歲~25歲有多少人、25歲~30歲有多少人、30歲~35歲、35歲~40歲有多少人,以及每個年齡段有多少女生,多少男生。

GET /your_index/your_type/_search
{	
   "size":0,
   # 先按照年齡分組,在按照性別分組
   "aggs": {
    "group_by_age": {
      "range": {
        "field": "age",
        "ranges": [
          {
            "from": 20,
            "to": 25
          },{
            "from": 25,
            "to": 30
          },{
            "from": 30,
            "to": 35
          },{
            "from": 35,
            "to": 40
          }
        ]
      },
      "aggs": {
        "group_by_gender": {
          "terms": {
            # gender.keyword一般是ES自動爲我們創建的類型
            # keyword類型的field不會分詞、默認長度256字符
            # 這裏大家初步瞭解有這個東西,知道怎麼回事就行,下一篇文章掃盲
            "field": "gender.keyword"
          }
        }
    	 }
		}
} 

5、我想知道我們公司每個年齡段,每個性別的平均賬戶餘額。

GET /your_index/your_type/_search
{		
  "size":0,
   # 先按照年齡分組,在按照性別分組,再按照平均工資聚合
   # 最終的結果就得到了每個年齡段,每個性別的平均賬戶餘額
   "aggs": {
    "group_by_age": {
      "range": {
        "field": "age",
        "ranges": [
          {
            "from": 20,
            "to": 30
          }
        ]
      },
      "aggs": {
        "group_by_gender": {
          "term": {
            "field": "gender.keyword"
          },
          # 在上一層根據gender聚合的基礎上再基於avg balance聚合
          "aggs": {
            "average_balance": {
              "avg": {
                "field": "balance"
              }
            }
          }
        }
      }
		}
} 

6、嵌套聚合,並且使用內部聚合的結果集

GET /your_index/your_type/_search
{		
  "size":0,    
   # 嵌套聚合,並且使用內部聚合的結果集
   "aggs": { 
    "group_by_state": {
      "term": {
        "field": "state.keyword",
        "order": {
          # average_balance是下面內部聚合的結果集合,在此基礎上做desc
          "average_balance": "desc" 
        }
      },
			# 如下的agg會產出多個bucket如:
      # bucket1 => {state=1,acg=xxx、min=xxx、max=xxx、sum=xxx}
	    # bucket2 => {state=2,acg=xxx、min=xxx、max=xxx、sum=xxx}
      "aggs": {
        "average_balance": {
          "avg": {  # avg 求平均值  metric
            "field": "balance"
          }
        },
         "min_price": {
          "min": {  # metric 求最小值
            "field": "price"
          }
        },
         "max_price": {
          "max": {  # metric 求最大值
            "field": "price"
          }
        },
         "sum_price": {
          "sum": {  # metric 計算總和
            "field": "price"
          }
        },
      }
    }
  }
}

8、除了前面說的按照值分組聚合,比如男、女,還可以使用histogram按區間聚合分析。

GET /your_index/your_type/_search
{
  "size":0,   
  # histogram,類似於terms,同樣會進行bucket分組操作。
  # 使用histogram需要執行一個field,比如下例中的age,表示按照age的範圍進行分組聚合
  "aggs": { # 聚合中嵌套聚合
      "group_by_price": {
            "histogram": {
                 "field": "age",
   							  # interval爲10,它會劃分成這樣 0-10  10-20  20-30 ...
  								# 那age爲21的記錄就會被分進20-30的區間中
                 "interval":10
             },
       "aggs": { # 聚合中嵌套聚合
            "average_price": {
               "avg": {
                  "field": "price"
               }
            }
        }
     }
  }
}

9、根據日期進行聚合

GET /your_index/your_type/_search
{		
  "size":0, 
  "aggs": {
     "agg_by_time" : { 
       		# 關鍵字
          "date_histogram" : {
                "field" : "age",
       					# 間隔,一個月爲一個跨度
                "interval" : "1M",
                "format" : "yyyy-MM-dd",
       					# 即使這個區間中一條數據都沒有,這個區間也要返回
                "min_doc_count":0 
       					# 指定區間
       					“extended_bounds”:{
       						"min":"2021-01-01",
       					  "max":"2021-01-01",
     						}
            } 
        }
    }
}

# 補充
"interval":“quarter”按照季度劃分

10、filter aggregate 過濾、聚合。

# Case1
# 如下例子:我想先過濾出年齡大於20的人,然後聚合他們的平均工資
GET /your_index/your_type/_search
{
  "size":0,
  "query":{
    "consitant_score":{
      # 這個filter會針對ES中全局的數據進行filter
      "filter":{
        "range":{"age":{"gte":20}}
      }
    }
  },
  "aggs":{
    "avg_salary":{
      "avg":{
        "field":"salary"
      }
    }
  }
}

# Case2
# bucket filter
POST /sales/_search
{
    "aggs" : {
      	# T恤bucket的agg
        "agg_t_shirts" : {
            "filter" : { 
              "term": {
                "type": "t-shirt" 
              }
            },
            "aggs" : {
                "avg_price" : { "avg" : { "field" : "price" } }
            }
        },
			# 毛衣bucket的agg
      "agg_sweater" : {
            "filter" : { 
              "term": {
                "type": "sweater" 
              }
            },
            "aggs" : {
                "avg_price" : { "avg" : { "field" : "price" } }
            }
        }
    }
}

11、嵌套聚合-廣度優先

說一個應用於場景: 我們檢索電影的評論, 但是我們先按照演員分組聚合,再按照評論的數量進行聚合。且我們假設每個演員都出演了10部電影。

分析: 如果我們選擇深度優先的話, ES在構建演員電影相關信息時,會順道計算出電影下面評論數的信息,假如說有10萬個演員的filter aggregate話, 10萬*10=100萬個電影 每個電影下又有很多影評,接着處理影評, 就這樣內存中可能會存在幾百萬條數據,但是我們最終就需要50條,這種開銷是很大的。

廣度優先的話,是我們先處理電影數,而不管電影的評論數的聚合情況,先從10萬演員中幹掉99990條數據,剩下10個演員再聚合。

		"aggs":{
            "target_actors":{
                "terms":{
                    "field":"actors",
                    "size":10,
                    "collect_mode":"breadth_first" # 廣度優先
                }
            }
		}

12、global aggregation

全局聚合,下面先使用query進行全文檢索,然後進行聚合, 下面的聚合實際上是針對兩個不同的結果進行聚合。

  • 第一個聚合添加了global關鍵字,意思是ES中存在的所有doc進行聚合計算得出t-shirt的平均價格

  • 第二個聚合針對全文檢索的結果進行聚合

POST /sales/_search?size=0
{
    "query" : {
        # 全文檢索 type = t-shirt的商品
        "match" : { "type" : "t-shirt" }
    },
    "aggs" : {
        "all_products" : {
            "global" : {}, # 表示讓 all_products 對ES中所有數據進行聚合
            "aggs" : {
                # 沒有global關鍵字,表示針對全文檢索的結果進行聚合
                "avg_price" : { "avg" : { "field" : "price" } }
            }
        },
        "t_shirts": { "avg" : { "field" : "price" } }
    }
}

13、Cardinality Aggregate 基數聚合

在ES中聚合時去重一般選用cardinality metric,它可以實現對每一個bucket中指定的field進行去重,最終得到去重後的count值。

雖然她會存在5%左右的錯誤率,但是性能特別好

POST /sales/_search?size=0
{
    "aggs" : {
      	# 先按照月份聚合得到不同月的bucket
        "agg_by_month" : {
          	"date_histogram":{
              "field" : "my_month",
              "internal":"month"
            }
        },
				# 在上一步得到的以月份爲維護劃分的bucket基礎上,再按照品牌求基數去重。
			  # 於是最終我們就得到了每個月、每種品牌的銷售量。
      	"aggs" : {
        	"dis_by_brand" : {
            	"cardinality" : { 
               	 "field" : "brand"
            }
        }
    }
}

對Cardinality Aggregate的性能優化, 添加 precision_threshold 優化準確率和內存的開銷。

還是下面的例子,如果將precision_threshold的值調整到100意思是:當品牌的總數量小於100時,去重的精準度爲100%, 此時內存的佔用情況爲 100*8=800字節。

加入我們將這個值調整爲1000,意思是當品臺的種類在1000個以內時,去重的精準度100%,內存的佔用率爲1000*8=80KB。

官方給出的指標是:將precision_threshold設置爲5時,錯誤率會被控制在5%以內。

POST /sales/_search?size=0
{
    "aggs" : {
        "type_count" : {
            "cardinality" : { # 關鍵字
                "field" : "brand"
                "precision_threshold":100
            }
        }
    }
}

進一步優化,Cardinality底層使用的算法是 HyperLogLog++。

因爲這個算法的底層會對所有的 unique value取hash值,利用這個hash值去近似的求distcint count, 因此我們可以在創建mapping時,將這個hash的求法設置好,添加doc時,一併計算出這個hash值,這樣 HyperLogLog++ 就無需再計算hash值,而是直接使用。從而達到優化速度的效果。

PUT /index/
{
    "mappings":{
        "my_type":{
            "properties":{
                "my_field":{
                    "type":"text",
                    "fields":{
                        "hash":{
                            "type":"murmu3"
                        }
                    }
                }
            }
        }
    }
}

14、控制聚合的升降序

比如我想知道每種顏色item的平均價格,並且我希望按照價格的從小到大升序展示給我看。

於是就像下面這樣,先按照顏色聚合可以將相同顏色的item聚合成1組,在聚合的結果上再根據價格進行聚合。期望在最終的結果中,通過order控制按照價格聚合的分組中升序排序, 這算是個在下鑽分析時的排序技巧。

GET /index/type/_search
{
     "size":0,
     "aggs":{
         "group_by_color":{
             "term":{
                 "field":"color",
                 "order":{ #
                     "avg_price":"asc"
                 }
             }
         },
         "aggs":{
             # 在上一層按color聚合的基礎上,再針對price進行聚合
             "avg_price":{
                 "avg":{
                     "field":"price"
                 }
             }
         }
     }
}

15、Percentiles Aggregation

計算百分比, 常用它計算:在200ms內成功訪問網站的比率、在500ms內成功訪問網站的比例、在1000ms內成功訪問網站的比例,或者是銷售價爲1000元的商品佔總銷售量的比例、銷售價爲2000元的商品佔總銷售量的比例等等。

示例: 針對doc中的 load_time字段, 計算出在不同百分比下面的 load_time_outliner情況。

GET latency/_search
{
    "size": 0,
    "aggs" : {
        "load_time_outlier" : {
        		# 關鍵字
            "percentiles" : {
                "field" : "load_time" 
            }
        }
    }
}

響應解讀:在百分之50的加載請求中,平均load_time的時間是在445.0。 在99%的請求中,平均加載時間980.1。

{
    ...

   "aggregations": {
      "load_time_outlier": {
         "values" : {
            "1.0": 9.9,
            "5.0": 29.500000000000004,
            "25.0": 167.5,
            "50.0": 445.0,
            "75.0": 722.5,
            "95.0": 940.5,
            "99.0": 980.1000000000001
         }
      }
   }
}

還可以自己指定百分比跨度間隔。

GET latency/_search
{
    "size": 0,
    "aggs" : {
        "load_time_outlier" : {
            "percentiles" : {
                "field" : "load_time",
                "percents" : [95,99,99.9] 
            }
        }
    }
}

優化: percentile底層使用的是 TDigest算法。用很多個節點執行百分比計算,近似估計,有誤差,節點越多,越精準。

可以設置compression的值, 默認是100 , ES限制節點的最多是 compression*20 =2000個node去計算 , 因爲節點越多,性能就越差。

一個節點佔用 32字節, 1002032 = 64KB。

GET latency/_search
{
    "size": 0,
    "aggs" : {
        "load_time_outlier" : {
            "percentiles" : {
                "field" : "load_time",
                "percents" : [95,99,99.9],
                "compression":100 # 默認值100
            }
        }
    }
}

參考:https://www.elastic.co/guide/en/elasticsearch/reference/6.2/search-aggregations.html



五、7個查詢優化技巧



  • 第一種:多字段檢索,巧妙控制權重
  • 第一種: 更換寫法,改變佔用的權重比例。
  • 第三種: 如果不希望使用相關性得分,使用下面的語法。
  • 第四種: 靈活的查詢
  • 第五種: 比如我對title字段進行檢索,我希望檢索結果中包含"java",並且我允許檢索結果中包含:”golang“ ,但是!如果檢索結果中包含”golang“,我希望這個title中包含”golang“的doc的排名能靠後一些。
  • 第六種: 重打分機制
  • 第七種: 提高召回率和精準度的技巧:混用match和match_phrase+slop提高召回率。注意下面的嵌套查詢層級 bool、must、should


上面的七種優化相關性得分的方式的具體實現代碼,在公衆號原文中可以查看到,推薦閱讀原文,json的格式會好看很多,ES專題依然在連載中~,歡迎關注。

點擊閱讀原文,查看7種優化方式的具體實現代碼、還有神祕大禮相送
點擊閱讀原文,查看7種優化方式的具體實現代碼、還有神祕大禮相送
點擊閱讀原文,查看7種優化方式的具體實現代碼、還有神祕大禮相送


參考:

官方文檔:https://www.elastic.co/guide/en/elasticsearch/reference/6.0

query dsl:https://www.elastic.co/guide/en/elasticsearch/reference/6.2/query-dsl.html

聚合分析:https://www.elastic.co/guide/en/elasticsearch/reference/6.2/search-aggregations.html



推薦閱讀


  1. MySQL的修仙之路,圖文談談如何學MySQL、如何進階!(已發佈)
  2. 面前突擊!33道數據庫高頻面試題,你值得擁有!(已發佈)
  3. 大家常說的基數是什麼?(已發佈)
  4. 講講什麼是慢查!如何監控?如何排查?(已發佈)
  5. 對NotNull字段插入Null值有啥現象?(已發佈)
  6. 能談談 date、datetime、time、timestamp、year的區別嗎?(已發佈)
  7. 瞭解數據庫的查詢緩存和BufferPool嗎?談談看!(已發佈)
  8. 你知道數據庫緩衝池中的LRU-List嗎?(已發佈)
  9. 談談數據庫緩衝池中的Free-List?(已發佈)
  10. 談談數據庫緩衝池中的Flush-List?(已發佈)
  11. 瞭解髒頁刷回磁盤的時機嗎?(已發佈)
  12. 用十一張圖講清楚,當你CRUD時BufferPool中發生了什麼!以及BufferPool的優化!(已發佈)
  13. 聽說過表空間沒?什麼是表空間?什麼是數據表?(已發佈)
  14. 談談MySQL的:數據區、數據段、數據頁、數據頁究竟長什麼樣?瞭解數據頁分裂嗎?談談看!(已發佈)
  15. 談談MySQL的行記錄是什麼?長啥樣?(已發佈)
  16. 瞭解MySQL的行溢出機制嗎?(已發佈)
  17. 說說fsync這個系統調用吧! (已發佈)
  18. 簡述undo log、truncate、以及undo log如何幫你回滾事物! (已發佈)
  19. 我勸!這位年輕人不講MVCC,耗子尾汁! (已發佈)
  20. MySQL的崩潰恢復到底是怎麼回事? (已發佈)
  21. MySQL的binlog有啥用?誰寫的?在哪裏?怎麼配置 (已發佈)
  22. MySQL的bin log的寫入機制 (已發佈)
  23. 刪庫後!除了跑路還能幹什麼?(已發佈)
  24. 自導自演的面試現場,趣學數據庫的10種文件(已發佈)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章