ElasticSearch第三篇,關於搜索、過濾、排序簡單講解。 ...
目錄
發表日期:2019年9月20日
上節回顧
1.講瞭如何對索引CRUD
2.重新解釋了type,只是元數據的效果
3.講瞭如何對文檔CRUD
本節前言
1.ElasticSearch的主要功能是搜索,這節也將會主要講搜索,將會涉及到如何使用關鍵字進行全文搜索
2.除了講搜索,也會講到搜索相關的“分頁”、“排序”、“聚合分析”等內容。
3.還會補充一些與搜索相關的知識。
文檔的搜索
測試數據:請先插入以下數據,以便練習搜索功能
【突然看了一下之前的博文,發現我後面去準備數據的時候寫錯格式了。所以導致id為1,2,3的文檔和後面的文檔的欄位不一樣。你可以僅僅基於以下的數據來測試】
PUT /douban/book/5
{
"book_id":5,
"book_name":"A Boy's Own Story",
"book_author":"Edmund White",
"book_pages":217,
"book_express":"Vintage",
"publish_date":"1994-02-01",
"book_summary":"""
An instant classic upon its original publication, A Boy's Own Story is the first of Edmund White's highly acclaimed trilogy of autobiographical novels that brilliantly evoke a young man's coming of age and document American gay life through the last forty years.
The nameless narrator in this deeply affecting work reminisces about growing up in the 1950s with emotionally aloof, divorced parents, an unrelenting sister, and the schoolmates who taunt him. He finds consolation in literature and his fantastic imagination. Eager to cultivate intimate, enduring friendships, he becomes aware of his yearning to be loved by men, and struggles with the guilt and shame of accepting who he is. Written with lyrical delicacy and extraordinary power, A Boy's Own Story is a triumph."""
}
PUT /douban/book/6
{
"book_id":6,
"book_name":"The Lost Language of Cranes",
"book_author":"David Leavitt",
"book_pages":352,
"book_express":"Bloomsbury Publishing PLC",
"publish_date":"2005-05-02",
"book_summary":"""David Leavitt's extraordinary first novel, now reissued in paperback, is a seminal work about family, sexual identity, home, and loss. Set in the 1980s against the backdrop of a swiftly gentrifying Manhattan, The Lost Language of Cranes tells the story of twenty-five-year-old Philip, who realizes he must come out to his parents after falling in love for the first time with a man. Philip's parents are facing their own crisis: pressure from developers and the loss of their longtime home. But the real threat to this family is Philip's father's own struggle with his latent homosexuality, realized only in his Sunday afternoon visits to gay porn theaters. Philip's admission to his parents and his father's hidden life provoke changes that forever alter the landscape of their worlds."""
}
PUT /douban/book/7
{
"book_id":7,
"book_name":"Immortality",
"book_author":"Milan Kundera",
"book_pages":400,
"book_express":"Faber and Faber",
"publish_date":"2000-08-21",
"book_summary":"""Milan Kundera's sixth novel springs from a casual gesture of a woman to her swimming instructor, a gesture that creates a character in the mind of a writer named Kundera. Like Flaubert's Emma or Tolstoy's Anna, Kundera's Agnes becomes an object of fascination, of indefinable longing. From that character springs a novel, a gesture of the imagination that both embodies and articulates Milan Kundera's supreme mastery of the novel and its purpose: to explore thoroughly the great themes of existence."""
}
搜索的方式主要有兩種,URL搜索和請求體搜索,一個是將搜索的條件寫在URL中,一個是將請求寫在請求體中。
URL參數條件搜索
語法:GET /index/type/_search?參數
參數解析:
- q:使用某個欄位來進行查詢,例如q:book_name=book,就是根據book_name中是否有book來進行搜索。
- sort:使用某個欄位來進行排序,例如sort=cost:desc,就是根據cost欄位來進行降序desc排序。
- 其他:fileds,timeout,analyzer【這些參數留在請求體搜索中講】
- 不帶參數時,為“全搜索”
- 多個參數使用&&拼接
示例:
GET /douban/book/_search?q=book_summary:character
GET /douban/book/_search?q=book_author:Milan
GET /douban/book/_search?q=book_summary:a
GET /douban/book/_search?q=book_summary:a&&sort=book_pages:desc
GET /douban/book/_search?q=book_summary:a&&q=book_author:Milan
【值得註意的是,請先不要對text類型的數據進行排序,這會影響搜索,對整數排序即可,後面會再細講】
查詢結果解析:
【考慮到數據太長的問題,所以我給了另一個搜索結果的返回截圖】
補充:把搜索條件寫在url中的搜索方式比較少用,因為查詢參數拼接到URL中會比較麻煩。
請求體條件搜索
語法與示例:
//全搜索
GET /index/type/_search
GET /douban/book/_search
//全搜索
GET /index/type/_search
{
"query": {
"match_all": {}
}
}
GET /douban/book/_search
{
"query": {
"match_all": {}
}
}
// 查詢指定欄位的數據(全文搜索,如果搜索值有多個詞,僅匹配一個詞的結果也可以查詢出來):
GET /index/type/_search
{
"query": {
"match": {
"欄位名": "搜索值"
}
}
}
GET /douban/book/_search
{
"query": {
"match": {
"book_name": "A The"
}
}
}
// 使用同一搜索值搜索多個欄位:
GET /index/type/_search
{
"query": {
"multi_match": {
"query": "搜索值",
"fields": [
"搜索的欄位1","搜索的欄位2"]
}
}
}
GET /douban/book/_search
{
"query": {
"multi_match": {
"query": "A",
"fields": [
"book_name","book_summary"]
}
}
}
// 短語查詢:【搜索值必須完全匹配,不會把搜索值拆分來搜索】
GET /index/type/_search
{
"query": {
"match_phrase": {
"欄位": "搜索值"
}
}
}
GET /douban/book/_search
{
"query": {
"match_phrase": {
"book_summary": "a character"
}
}
}
// 欄位過濾,查詢的結果只顯示指定欄位
GET /product/book/_search
{
"query": {
"查詢條件"
},
"_source": [
"顯示的欄位1",
"顯示的欄位2"
]
}
GET /douban/book/_search
{
"query": {
"match": {
"book_name": "Story"
}
},
"_source": [
"book_name",
"book_id"
]
}
// 高亮查詢:【根據查詢的關鍵字來進行高亮,高亮的結果會顯示在返回結果的會自動在返回結果中的highlight中,關鍵字會被加上<em>標簽】
// 如果想要多欄位高亮,也需要進行多欄位搜索
GET /index/book/_search
{
"query": {
"查詢條件"
},
"highlight": {
"fields": {
"高亮的欄位名1": {}
}
}
}
GET /douban/book/_search
{
"query": {
"match": {
"book_summary": "Story"
}
},
"highlight": {
"fields": {
"book_summary":{}
}
}
}
GET /douban/book/_search
{
"query": {
"multi_match": {
"query": "Story",
"fields": [
"book_name","book_summary"]
}
},
"highlight": {
"fields": {
"book_summary":{},
"book_name":{}
}
}
}
上面展示了關於全搜索
、單欄位值全文搜索
、多欄位單一搜索值全文搜索
、短語搜索
、欄位過濾
、高亮搜索
的代碼。
由於對多個欄位使用不同搜索值涉及條件拼接,所以單獨講。
前置知識講解:對於條件拼接,在SQL中有and,or,not,在ElasticSearch不太一樣,下麵逐一講解:
bool
:用來表明裡面的語句是多條件的組合,用來包裹多個條件。should
:裡面可以有多個條件,查詢結果必須符合查詢條件中的一個或多個。must
:裡面的多個條件都必須成立must_not
:裡面的多個條件必須不成立
示例:
// 書名必須包含Story的
GET /douban/book/_search
{
"query": {
"bool": {
"must": [
{
"match":{
"book_name":"Story"
}
}
]
}
}
}
// 書名必須不包含Story的
GET /douban/book/_search
{
"query": {
"bool": {
"must_not": [
{
"match":{
"book_name":"Story"
}
}
]
}
}
}
// 書名必須不包含Story,書名包含Adventures或Immortality的
GET /douban/book/_search
{
"query": {
"bool": {
"must_not": [
{
"match":{
"book_name":"Story"
}
}
],
"should": [
{
"match": {
"book_name": "Adventures"
}
},
{
"match": {
"book_name": "Immortality"
}
}
]
}
}
}
// 在should、must、must_not這些裡面都可以放多個條件
GET /douban/book/_search
{
"query": {
"bool": {
"must_not": [
{
"match":{
"book_name":"Story"
}
},
{
"match": {
"book_name": "Adventures"
}
}
]
}
}
}
// 如果是單個條件的時候,還可以這樣寫,省去[]:
GET /douban/book/_search
{
"query": {
"bool": {
"must_not": {
"match":{
"book_name":"Story"
}
}
}
}
}
// 還可以條件嵌套,也就是再嵌套一層bool,不過要註意邏輯,例如:
// 查詢出(書名有story)或者(書名有The而且作者名有David)的,第二個是可成立可不成立的。
GET /douban/book/_search
{
"query": {
"bool": {
"should": [
{
"match": {
"book_name": "Story"
}
},
{
"bool": {
"must": [
{
"match": {
"book_name": "The"
}
},
{
"match": {
"book_author": "David"
}
}
]
}
}
]
}
}
}
補充:
- 上面講了URL參數條件搜索和請求體條件搜索,講了
全搜索
、單欄位值全文搜索
、多欄位單一搜索值全文搜索
、短語搜索
、欄位過濾
、高亮搜索
的使用方法,還講了基於bool、should、must、must_not的多條件搜索,上面的知識已經能基礎地實現一些搜索功能了。但還是有一些知識由於比較晦澀,所以留到後面章節講,比如給搜索指定分詞器、給多條件指定匹配數量、滾動查詢等等。
小節總結:
上面講了URL參數條件搜索和請求體條件搜索。URL參數條件寫在URL裡面,用?來附帶參數,q用來指定搜索欄位。請求體參數把條件寫在請求體中,query是最外層的包裹,match_all用於查詢所有,match用來使用指定搜索值搜索某一欄位,match_phrase用來搜索連續的搜索,_source用來欄位過濾(與query同級,[]裡面是欄位名),highlight用來高亮搜索(與query同級,裡面是{field:{欄位名1:{},欄位名2:{}}}),bool、should、must、must_not用來多條件搜索。
文檔的過濾filter
過濾的效果其實有點像條件搜索,不過條件搜索會考慮相關度分數和考慮分詞,而過濾是不考慮這些的,過濾對相關度沒有影響。過濾一般用於結構化的數據上,也就是通常不用於使用了分詞的數據上,通常都會用在數值類型和日期類型的數據上。
在搜索的時候,如果你不希望要搜索的條件會影響到相關度,那麼就把它放在過濾中,如果希望影響相關度,那麼就放在條件搜索中。
使用過濾時,由於不考慮相關度,所以score固定為1。
文檔的過濾filter裡面主要有五種欄位,range,term,terms,exist,missing。range用於欄位數據比較大小;term主要用於比較字元類型的和數值類型的數據是否相等;terms是term的複數版,裡面可以有多個用於比較相等的值;exist和missing用於判斷文檔中是否包含指定欄位或沒有某個欄位(僅適用於2.0+版本,目前已經移除)
語法與舉例:
// range,gte是不小於,lte是不大於,eq是等於,gt是大於,lt是小於
GET / index/type/_search
{
"query": {
"range": {
"欄位名": {
"gte": 比較值
[,"lte": 比較值]
}
}
}
}
GET /douban/book/_search
{
"query": {
"range": {
"book_pages": {
"gte": 352,
"lt":400
}
}
}
}
// term用於匹配字元串和數值型類型的數據(解決了range中沒有eq的問題),但不能直接用於分詞的欄位。
//【這個並沒有那麼簡單,會後續再講,直接匹配一些會分詞的欄位時,會匹配失敗,
//因為這時候這個欄位拿來匹配的都是散亂的值,不是完整的原本的欄位數據,所以下麵用了不分詞的數值型的欄位來演示】
GET /douban/book/_search
{
"query": {
"term": {
"欄位": "搜索值"
}
}
}
GET /douban/book/_search
{
"query": {
"term": {
"book_pages": 352
}
}
}
//terms
GET /douban/book/_search
{
"query": {
"terms": {
"欄位": ["搜索值1","搜索值2"]
}
}
}
GET /douban/book/_search
{
"query": {
"terms": {
"book_pages": [
"352",
"400"
]
}
}
}
term的問題:
- 首先,提一下的是,在搜索的時候,你並不直接面向原始文檔數據,而是面向倒排索引,這意思是什麼呢?比如你要進行全文搜索,那麼你的搜索值並不是與數據文件比對的,而是與倒排索引匹配的,也就是在我們與數據文件之間有一個專門用於搜索的層次。
- 對於match和match_all,這些都是全文搜索,就不說了,直接就是通過索引詞在索引文件中找到對應的文檔;比較不同的是match_phrase這個會匹配一段詞的搜索,他是怎麼查詢的呢?他實際上也會去查索引文件中包括了搜索值中所有詞並且詞的在文檔中的位置順序也一致的記錄,所以這個短語匹配其實也是通過倒排索引來搜索的。
- 而倒排索引中其實包含了所有欄位的標識,對於分詞的欄位,會存儲索引詞;對於不分詞的,會存儲整個數據。【對於分詞的欄位可以加一個keyword來保留完整的數據,這個後面再講。】
- 而term的搜索主要面向不分詞的數據,所以無法直接用於分詞的欄位,除非加keyword。
官方文檔中關於term
filter與bool
filter也可以用於多條件拼接。例如:
GET /douban/book/_search
{
"query": {
"bool": {
"must": [
{
"match":{
"book_name":"Story"
}
},
{
"range": {
"book_pages": {
"lte":300
}
}
}
]
}
}
}
GET /douban/book/_search
{
"query": {
"bool": {
"must": [
{
"match":{
"book_name":"Story"
}
},
{
"range": {
"book_pages": {
"lte":300
}
}
},
{
"term": {
"publish_date": "1994-02-01"
}
}
]
}
}
}
在這樣條件搜索和過濾一起用的情況下,要註意filter過濾是不計算相關度的,在上面中,假設只有match,那麼某個文檔相關度為0.2,加上filter後,會變成1.2。因為filter預設提供的相關度為1。
constant_score
過濾還可以這樣寫:
GET /douban/book/_search
{
"query": {
"constant_score": {
"filter": {
"range": {
"book_pages": {
"gte": 352,
"lt": 400
}
}
}
}
}
}
// boost設置filter提供的相關度score值
GET /douban/book/_search
{
"query": {
"constant_score": {
"filter": {
"range": {
"book_pages": {
"gte": 352,
"lt": 400
}
}
},
"boost": 1.2
}
}
}
cache
對於過濾,elasticsearch會臨時緩存它的結果,以便可能下次仍需使用它。因為過濾是不關心相關度的。
官方文檔--過濾緩存
小節總結:
這節介紹了不影響相關度的搜索--過濾,過濾通常用於過濾結構化數據,也就是那些不分詞的數據,其中range用於數值範圍過濾,term用於字元類型的數據或數值類型的數據的值是否相等,terms是term的複數版。過濾也支持bool拼接多個條件。過濾提供的相關度分數是一個常數,預設是1。
文檔的聚合分析
準備數據
先準備一批測試數據:
PUT /people/test/1
{
"name":"lilei1",
"age":18,
"gender":1
}
PUT /people/test/2
{
"name":"lilei2",
"age":17,
"gender":0
}
PUT /people/test/3
{
"name":"lilei4",
"age":21,
"gender":1
}
PUT /people/test/4
{
"name":"lilei4",
"age":15,
"gender":0
}
PUT /people/test/5
{
"name":"lilei1 2",
"age":15,
"gender":0
}
像在SQL中會需要SUM(),MAX().AVG()函數。ElasticSearch也提供了關於聚合分析的函數。
ElasticSearch中常見的聚合分析函數有terms(分組函數)
、avg(平均數)
、range(區間分組)
、max(求最大值)
、min(求最小值)
、cardinality(獲取唯一值的數量)
、value_count(獲取值的數量,不去重,可以得出多少個值參與了聚合)
。
語法與舉例:
語法:
GET /index/type/_search
{
"aggs": {
"自定義聚合名稱": {
"聚合函數": {
聚合參數
}
}
}
}
舉例:
// 按性別分組
GET /douban/book/_search
{
"aggs": {
"groud_by_express": {
"terms": {
"field": "book_id",
"size": 10
}
}
}
}
//求年齡的平均數
GET /people/test/_search
{
"aggs": {
"avg_of_age": {
"avg": {
"field": "age"
}
}
}
}
// 求年齡的最大值:
GET /people/test/_search
{
"aggs": {
"max_of_age": {
"max": {
"field": "age"
}
}
}
}
// 把年齡[15,17]的分成一組,把年齡[18,25]的分成一組
GET /people/test/_search
{
"aggs": {
"range_by_age": {
"range": {
"field": "age",
"ranges": [
{
"from": 15,
"to": 17
},
{
"from": 18,
"to": 25
}
]
}
}
}
}
// 獲取不同的年齡數:,比如有年齡[1,2,3,3,4,5],得到的結果是5,因為3只算一次
GET /people/test/_search
{
"aggs": {
"get_diff_age_count": {
"cardinality": {
"field": "age"
}
}
}
}
返回結果解析:
其他語法:
先查詢後聚合:
GET /people/test/_search
{
"query": {
"match": {
"name": "lilei1"
}
},
"aggs": {
"avg_of_age": {
"avg": {
"field": "age"
}
}
}
}
先過濾後聚合:
// 先獲取年齡大於15的,再求平均值
GET /people/test/_search
{
"query": {
"range": {
"age": {
"gt":15
}
}
},
"aggs": {
"avg_of_age": {
"avg": {
"field": "age"
}
}
}
}
聚合函數嵌套:
// 先按性別分組,再獲取年齡平均值
GET /people/test/_search
{
"aggs": {
"groud_by_express": {
"terms": {
"field": "gender"
},
"aggs": {
"avg_of_age": {
"avg": {
"field": "age"
}
}
}
}
}
}
聚合+排序:
// 先按性別分組,再按分組的年齡平均值降序排序,order中的avg_of_age就是下麵的聚合函數的自定義名稱
GET /people/test/_search
{
"aggs": {
"groud_by_express": {
"terms": {
"field": "gender",
"order": {
"avg_of_age": "desc"
}
},
"aggs": {
"avg_of_age": {
"avg": {
"field": "age"
}
}
}
}
}
}
補充:
上面只講了一些基礎的聚合,聚合分析是一個比較重要的內容,會在後面的再講。
小節總結:
本節主要講了ElasticSearch中關於數據聚合的使用方法,aggs是與query同級的,使用聚合函數需要自己定義一個外層的聚合函數名稱,avg用於求平均值,max用於求最大值,range用於範圍分組,term用於數據分組。分組可以與條件搜索和過濾一起使用,aggs是與query同級的,聚合函數也可以嵌套使用。
文檔的分頁、排序
【使用一下上一節準備的數據】
分頁
// 從第一條開始,獲取兩條數據
GET /people/test/_search
{
"from": 0,
"size": 2
}
// 可以先查詢,再分頁
GET /people/test/_search
{
"query": {
"match": {
"name": "lilei1"
}
},
"from": 0,
"size": 1
}
排序
【請註意,下麵的結果中你可以看到score為null,因為這時候你使用了age欄位來排序,而不是相關性,所以此時相關性意義不大,則不計算。】
排序處理:【sort與query同級別,是一個數組,裡面可以有多個排序參數,參數以{"FIELD":{"order":"desc/asc"}}為格式】
GET /people/test/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"age": {
"order": "desc"
}
}
]
}
deep paging
對於分頁和排序,需要共同面對一個問題:
首先,你要想到一個索引中的數據是散落在多個分片上的,你如何確定另一個分片上的數據與其他分片上的順序問題?,比如可能A分片上的有數值為1和數值為3的數據,而B分片上有數值為2和數值為4的數據,所以B分片的部分數據與A分片數據的大小是不確定的。那麼排序的時候怎麼處理這些散落的數據呢?(就算是依據相對度來排序,這個時候散落的數據的相關度也是不太好確定的)
分頁需要面對的問題也同樣是因為數據散落的而不好排序的問題,因為分頁也是要排序的,預設是按相關度排序。因為散落的數據的值的大小不確定,所以就需要把所有可能的數據取出來排完序再分頁,這就會導致需要取出遠遠超出“頁數”的數據來計算。
- 有兩個primary shard(命名為A和B),現在要取第1000頁的數據,假設每一頁10條記錄,那麼理論上是只需要取第10000到第10010條記錄出來即可。
- 但這時候我們並不知道A和B中的_score的大小如何,可能A中的最小的_score要比B中的最大的_score都要大,反過來也有可能,(所以我們並不能說僅僅從A和B中分別取10000到10010出來進行比較即可,我們需要對前面的數據都進行比較,以避免最小的_score都比另一個shard上的_score大的情況),為了確保數據的正確性,我們需要從A和B中都取出1到10010的數據來進行排序比較,然後再取出裡面的10000到10010條。
- 所以,你看到了,我們只是為了拿十條數據,竟然要查10010數據出來。這就是deep paging了。
補充:
- 除了上述的內容,一個沒講的而且比較重要的內容應該是滾動查詢,滾動查詢有點類似分頁查詢,但它會提前準備好數據,我暫時沒想好放在哪裡講合適,可能會在後面寫,也有可能某一天補充在這裡。
小節總結:
上面講了怎麼進行數據的分頁獲取和數據的排序,使用from和size分頁,使用sort排序;還講了一個如果查詢時頁數太深而可能導致的deep paging問題,問題的原因是多個分片上的數據大小不確定,不方便排序。