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

賜我白日夢發表於2021-01-27



一、導讀

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":"bradshawmckenzie@euron.com",
                       "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":"amberduke@pyrami.com",
                   "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種檔案(已釋出)

相關文章