1. 程式人生 > >Elasticsearch系列---多欄位搜尋

Elasticsearch系列---多欄位搜尋

### 概要 本篇介紹一下multi_match的best_fields、most_fields和cross_fields三種語法的場景和簡單示例。 ### 最佳欄位 bool查詢採取"more-matches-is-better"匹配越多分越高的方式,所以每條match語句的評分結果會被加在一起,從而為每個文件提供最終的分數_score。能與兩條語句同時匹配的文件會比只與一條語句匹配的文件得分要高,但有時這樣也會帶來一些與期望不符合的情況,我們舉個例子: 我們以英文兒歌為案例背景,我們這樣搜尋: ```java GET /music/children/_search { "query": { "bool": { "should": [ { "match": { "name": "brush mouth" }}, { "match": { "content": "you sunshine" }} ] } } } ``` 結果響應(有刪減) ```java { "hits": { "total": 2, "max_score": 1.7672573, "hits": [ { "_id": "4", "_score": 1.7672573, "_source": { "name": "brush your teeth", "content": "When you wake up in the morning it's a quarter to one, and you want to have a little fun You brush your teeth" } }, { "_id": "3", "_score": 0.7911257, "_source": { "name": "you are my sunshine", "content": "you are my sunshine, my only sunshine, you make me happy, when skies are gray" } } ] } } ``` 預期的結果是"you are my sunshine"要排在"brush you teeth"前面,實際結果卻相反,為什麼呢? 我們按照匹配的方式復原一下_score的評分過程:每個query的分數,乘以匹配的query的數量,除以總query的數量。 我們來看一下匹配情況: 文件4的name欄位包含brush,content欄位包含you,所以兩個match都能得到評分。 文件3的name欄位不匹配,但是content欄位包含you和sunshine,命中一個match,只能得一項的分。 結果文件4的得分會高一些。 但我們仔細想一想,文件4雖然兩個match都匹配了,但每個match只匹配了其中一個關鍵詞,文件3只匹配了一個match,卻是同時匹配了兩個連續的關鍵詞,按我們的預期,一個field上匹配了兩個連續關鍵詞的相關性應該高一些,簡單的把多個match的得分加起來,雖然分高一些,但不是我們期望的首位。 我們探尋的是最佳欄位匹配,某一個欄位匹配到了儘可能多的關鍵詞,讓它排在前面;而不是更多的field匹配了關鍵詞,就讓它在前面。 我們使用dis_max語法查詢,優先將最佳匹配的評分作為查詢的評分結果返回,請求如下: ```java GET /music/children/_search { "query": { "dis_max": { "queries": [ { "match": { "name": "brush mouth" }}, { "match": { "content": "you sunshine" }} ] } } } ``` 結果響應(有刪減) ```java { "hits": { "total": 2, "max_score": 1.0310873, "hits": [ { "_id": "4", "_score": 1.0310873, "_source": { "name": "brush your teeth", "content": "When you wake up in the morning it's a quarter to one, and you want to have a little fun You brush your teeth" } }, { "_id": "3", "_score": 0.7911257, "_source": { "name": "you are my sunshine", "content": "you are my sunshine, my only sunshine, you make me happy, when skies are gray" } } ] } } ``` 呃,結果排序還是不理想,不過可以看到_id為4的評分由之前的1.7672573降為1.0310873,說明dis_max操作後,能夠影響評分,只是案例取得不好,_id為3的記錄評分實在太低了,只有0.7911257,仍然不能改變次序。 ### 最佳欄位查詢調優 上一節的dis_max查詢會採用單個最佳匹配欄位,而忽略其他的匹配項,這對精準化搜尋還是不夠合理,我們需要其他匹配項的匹配結果按一定權重參與最後的評分,權重可以自己設定。 我們可以加一個tie_breaker引數,這樣就可以把其他匹配項的結果也考慮進去,它的使用規則如下: 1. tie_breaker的值介於0-1之間,是個小數,建議此值範圍0.1-0.4. 2. dis_max負責獲取最佳匹配語句的分數_score,其他匹配語句的_score與tie_breaker相乘。 3. 對評分求和並歸一化處理。 所以說,加上了tie_breaker,會考慮所有的匹配條件,但最佳匹配語句仍然佔大頭。 請求示例: ```java GET /music/children/_search { "query": { "dis_max": { "queries": [ { "match": { "name": "brush mouth" }}, { "match": { "content": "you sunshine" }} ], "tie_breaker": 0.3 } } } ``` ### multi_match查詢 #### best_fields best-fields策略:將某一個field匹配了儘可能多關鍵詞的文件優先返回回來。 如果我們在多個欄位上使用相同的搜尋字串進行搜尋,請求語法可以冗長一些: ```java GET /music/children/_search { "query": { "dis_max": { "queries": [ { "match": { "name": { "query": "you sunshine", "boost": 2, "minimum_should_match": "50%" } } }, { "match": { "content": "you sunshine" } } ], "tie_breaker": 0.3 } } } ``` 可以用multi_match將搜尋請求簡化,multi_match支援boost、minimum_should_match、tie_breaker引數的設定: ```java GET /music/children/_search { "query": { "multi_match": { "query": "you sunshine", "type": "best_fields", "fields": ["name^2","content"], "minimum_should_match": "50%", "tie_breaker": 0.3 } } } ``` 而boost、minimum_should_match、tie_breaker引數的一個顯著作用就是去長尾,長尾資料比如說我們搜尋4個關鍵詞,但很多文件只匹配1個,也顯示出來了,這些文件其實不是我們想要的,可以通過這幾個引數的設定,將門檻提高,過濾掉長尾資料。 #### most_fields most-fields策略:儘可能返回更多field匹配到某個關鍵詞的doc,優先返回回來。 常用方式是我們為同一文字欄位,建立多種方式的索引,詞幹提取分析處理的和原文儲存的都做一份,這樣能提高匹配的精準度。 我們拿music索引舉個例子(摘抄mapping片斷資訊)。我們做一點小修改: ```java PUT /music { "mappings": { "children": { "properties": { "name": { "type": "text", "analyzer": "english" "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "content": { "type": "text", "analyzer": "english" "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } } } } } } ``` 比如name和content欄位,我們除了有text型別的欄位,還有keyword型別的子欄位,text會做分詞、英文詞幹處理,keywork則保持原樣,搜尋內容的時候,我們可以使用name或name.keyword兩個欄位同時進行搜尋,示例: ```java GET /music/children/_search { "query": { "multi_match": { "query": "brushed", "type": "most_fields", "fields": ["name","name.keyword"] } } } ``` 我們搜尋name及name.keyword兩個欄位,由於name欄位的分詞器是english,搜尋字串brushed經過提取詞幹後變成brush,是能匹配到結果的,name.keyword則無法匹配,最終還是有文件結果返回。如果只對name.keyword欄位搜尋,則不會有結果返回。 這個就是most_fields的策略,希望對同一個文字進行多種索引,搜尋時各種索引的結果都參與,這樣就能儘可能地多返回結果。 #### 與best_fields區別 1. best_fields,是對多個field進行搜尋,挑選某個field匹配度最高的那個分數,同時在多個query最高分相同的情況下,在一定程度上考慮其他query的分數。簡單來說,你對多個field進行搜尋,就想搜尋到某一個field儘可能包含更多關鍵字的資料 - 優點:通過best_fields策略,以及綜合考慮其他field,還有minimum_should_match支援,可以儘可能精準地將匹配的結果推送到最前面 - 缺點:除了那些精準匹配的結果,其他差不多大的結果,排序結果不是太均勻,沒有什麼區分度了 實際的例子:百度之類的搜尋引擎,最匹配的到最前面,但是其他的就沒什麼區分度了 2. most_fields,綜合多個field一起進行搜尋,儘可能多地讓所有field的query參與到總分數的計算中來,此時就會是個大雜燴,出現類似best_fields案例最開始的那個結果,結果不一定精準,某一個document的一個field包含更多的關鍵字,但是因為其他document有更多field匹配到了,所以排在了前面;所以需要建立更多類似name.keyword,name.std這樣的field,儘可能讓某一個field精準匹配query string,貢獻更高的分數,將更精準匹配的資料排到前面 - 優點:將盡可能匹配更多field的結果推送到最前面,整個排序結果是比較均勻的 - 缺點:可能那些精準匹配的結果,無法推送到最前面 實際的例子:wiki,明顯的most_fields策略,搜尋結果比較均勻,但是的確要翻好幾頁才能找到最匹配的結果 ### cross_fields 有些實體物件在設計中,可能會使用多個欄位來標識一個資訊,如地址,常見儲存方案可以是省、市、區、街道四個欄位,分別儲存,合起來才是完整的地址資訊。再如人名,國外有first name和last name之分。 遇到針對這種欄位的搜尋,我們叫做跨欄位實體搜尋,我們要注意哪些問題呢? 我們回顧music索引的author欄位,就是設計成了author_first_name和author_last_name的結構,我們試著對它來演示一下跨欄位實體搜尋。 #### 使用most_fields查詢 ```java GET /music/children/_search { "query": { "multi_match": { "query": "Peter Raffi", "type": "most_fields", "fields": [ "author_first_name", "author_last_name" ] } } } ``` 響應的結果: ```java { "hits": { "total": 2, "max_score": 1.3862944, "hits": [ { "_id": "4", "_score": 1.3862944, "_source": { "id": "55fa74f7-35f3-4313-a678-18c19c918a78", "author_first_name": "Peter", "author_last_name": "Raffi", "author": "Peter Raffi", "name": "brush your teeth", "content": "When you wake up in the morning it's a quarter to one, and you want to have a little fun You brush your teeth" } }, { "_id": "1", "_score": 0.2876821, "_source": { "author_first_name": "Peter", "author_last_name": "Gymbo", "author": "Peter Gymbo", "name": "gymbo", "content": "I hava a friend who loves smile, gymbo is his name" } } ] } } ``` 看起來結果是對的,"Peter Raffi"按預期排在首位,但Peter Gymbo也出來的,這不是我們想要的結果,只是由於資料量太少的原因,長尾資料沒有顯示出來,most_fields查詢引出的問題有如下3個: 1. 只是找到儘可能多的field匹配的doc,而不是某個field完全匹配的doc 2. most_fields,沒辦法用minimum_should_match去掉長尾資料,就是匹配的特別少的結果 3. TF/IDF演算法,比如Peter Raffi和Peter Gymbo,搜尋Peter Raffi的時候,由於first_name中很少有Raffi的,所以query在所有document中的頻率很低,得到的分數很高,可能會出現非預期的次序。 #### 使用copy_to合併欄位 copy_to語法可以將多個欄位合併在一起,這樣就可以解決跨實體欄位的問題,帶來的副面影響就是佔用更多的儲存空間,copy_to的示例如下: ```java PUT /music/_mapping/children { "properties": { "author_first_name": { "type": "text", "copy_to": "author_full_name" }, "author_last_name": { "type": "text", "copy_to": "author_full_name" }, "author_full_name": { "type": "text" } } } ``` 注意這個請求需要在建立索引時執行,侷限性比較大。 所以案例設計時,專門有一個author欄位,儲存完整的名稱的。 ```java GET /music/children/_search { "query": { "match": { "author_full_name": { "query": "Peter Raffi", "operator": "and" } } } } ``` 單欄位的查詢,就可以隨心所欲的指定operator或minimum_should_match來控制精度了。 我們看一下前面提到的3個問題能否解決 1. 匹配問題 解決,最匹配的資料優先返回。 2. 長尾問題 解決,可以指定operator或minimum_should_match來控制精度。 3. 評分不準的問題 解決,所有資訊在一個欄位裡,IDF計算時次數是均勻的,不會有極端的誤差。 缺點: 需要前期設計時冗餘欄位,佔用的儲存會多一些。 copy_to拼接欄位時,會遇到順序問題,如英文名稱名前姓後,而地址順序則不固定,有的從省到街道由大到小,有的是反的,這也是侷限性之一。 #### 原生cross_fields語法 multi_match有原生的cross_fields語法解決跨欄位實體搜尋問題,請求如下: ```java GET /music/children/_search { "query": { "multi_match": { "query": "Peter Raffi", "type": "cross_fields", "operator": "and", "fields": ["author_first_name", "author_last_name"] } } } ``` 這次cross_fields的含義是要求: - Peter必須在author_first_name或author_last_name中出現 - Raffi必須在author_first_name或author_last_name中出現 看看上面提及的3個問題解決情況: 1. 匹配問題 解決,cross_fields要求每個term都必須在任何一個field中出現 2. 長尾問題 解決,參見上一條,每個term都必須匹配,長尾問題自然迎刃而解。 3. 評分不準的問題 解決,cross_fields通過混合不同欄位逆向索引文件頻率的方式解決詞頻的問題,具體來說,Peter在first_name中頻率會高一些,在last_name中頻率會低一些,在兩個欄位得到的IDF值,會取小的那個,Raffi也是同樣處理,這樣得到的IDF值就比較正常,不會偏高。 ### 小結 我們可以花一點時間瞭解一下多欄位搜尋的場景,和要注意的細節點,精準搜尋是一個非常大的話題,優化的空間沒有上限,可以先從最基礎的場景和調整語法開始嘗試。 專注Java高併發、分散式架構,更多技術乾貨分享與心得,請關注公眾號:Java架構社群 可以掃左邊二維碼新增好友,邀請你加入Java架構社群微信群共同探討技術 ![Java架構社群](https://img2020.cnblogs.com/blog/1834889/202003/1834889-20200303074927076-1724862