es 基於match_phrase/fuzzy的模糊匹配原理及使用


[版權聲明]:本文章由danvid發布於http://danvid.cnblogs.com/,如需轉載或部分使用請注明出處

  

  在業務中經常會遇到類似數據庫的"like"的模糊匹配需求,而es基於分詞的全文檢索也是有類似的功能,這個就是短語匹配match_phrase,但往往業務需求都不是那么簡單,他想要有like的功能,又要允許有一定的容錯(就是我搜索"東方賓館"時,"廣州花園賓館酒店"也要出來,這個就不是單純的"like"),下面就是我需要解析的問題(在此吐槽一下業務就是這么變態。。)

  描述一個問題時首先需要描述業務場景:假設es中有一索引字段name存儲有以下文本信息:

doc[1]:{"name":"廣州東方賓館酒店"}

doc[2]:{"name":"廣州花園賓館酒店"}

doc[3]:{"name":"東方公園賓館"}

需求要求在輸入:"東方賓館"的時候doc[1]排最前面doc[3]排第二doc[2]排第三,對於這個需求從簡單的全文檢索match來說,doc[3]:{"name":"東方公園賓館"}應該是第一位(注意:為了簡化原理分析,分詞我們使用standard即按單個字分詞)

   業務分析:顯然對於上面的業務場景如果單獨使用match的話,顯然是不合適,因為按照standard分詞,doc[3]的詞條長度要比doc[1]的詞條長度短,而詞頻又是都出現了[東][方][賓][館]4個詞,使用match匹配的話就會吧doc[3]排到最前面,顯然業務希望把輸入的文字順序匹配度最高的數據排前面,因為我確實要找的是"廣州東方賓館酒店"而不是"東方公園賓館"你不能把doc[3]給我排前面,OK業務邏輯好像是對的那么怎么解決問題;

  解決問題前介紹一哈match_phrase原理(match的原理我就不說了自己回去看文檔),簡單點說match_phrase就是高級"like"。api如下:

GET test_index/_search
{
    "query": {
        "match_phrase" : {
            "message" : {
                "query" : "東方賓館",
                "slop" : 2
            }
        }
    }
}

es在給文本分詞的時候,除了分詞之外還有一個詞條標記,就是position,例如我按照standard對以上三個doc的name進行分詞會變成這樣:

doc[1]:廣[0],州[1],東[2],方[3],賓[4],館[5],酒[6],店[7];

doc[2]:廣[0],州[1],花[2],園[3],賓[4],館[5],酒[6],店[7];

doc[3]:東[0],方[1],公[2],園[3],賓[4],館[5];

query文本分詞:東[0],方[1],賓[2],館[3];

使用match_phrase時:

1.es會先過濾掉不符合的query條件的doc,即doc[2]中沒有"東方"兩個詞匯,會被過濾掉

2.es會根據分詞的position對分詞進行過濾和評分,這個是就slop參數,默認是0,意思是查詢分詞只需要經過距離為0的轉換就可以變成跟doc一樣的文檔數據,例如:對於doc[1]來說slop就是0了,對於doc[3]slop就是2,即"賓"和"館"最大位移這兩個分詞只需要最多移動2個位置就可以變成"東方賓館"(反過來也一樣,query的文本中的"賓"和"館"只需要移動2個位置就可以變成"東方**賓館"),用數學的理解就是doc[3]的賓[4]-東[0]=4,query文本中的賓[2]-東[0]=2,那么轉換距離slop就是4-2=2,同理doc[3]的館[5]-東[0]=5,query的是3,slop結果也是2,那么"賓"和"館"最大的slop就是2,則query時設置slop等於2就能把doc[3]匹配出來,當設置為0時就是我們數據庫的"like"

  原理解析完了,就知道使用match只能匹配相關度即tf/idf,而分詞之間的位置關系卻無法保證,而match_phrase能保證分詞間的鄰近關系,那么就可以利用兩者優勢,結合搜索進行評分

GET test_index/_search
{
  "query": {
    "bool": {
      "must": {
        "match": { 
          "name": {
            "query": "東方賓館"
          }
        }
      },
      "should": {
        "match_phrase": { 
          "name": {
            "query": "東方賓館",
            "slop":  0
          }
        }
      }
    }
  }
}

這樣就的結果就是相當於match_phrase幫match進行了相關度分數的加分,當然你也可以通過修改slop的參數來進行步控制分數,這個就根據用戶需求來了;

    性能問題:其實使用match_phrase性能是要比單純的全文檢索性能低的,因為他要計算位置嘛,那么想提高性能可以通過先使用match進行過濾數據,然后利用rescore api對已經match的結果進行加分,這樣就減少了部分不必要的非match過濾:

GET test_index/_search
{
  "query": {
    "match": {
      "name":"東方賓館"
    }
  },
"rescore": { "window_size": 30, "query": { "rescore_query": { "match_phrase": { "name": { "query": "東方賓館", "slop": 0 } } } } } }
#window_size 是每一分片進行重新評分的頂部文檔數量這個只要大於你可能分頁的總數*每頁的數量即可(pageNumber*pageSize)實際上不需要這么多因為翻頁不可能很深,這個根據業務調整即可。

總結及注意點:

1.rescore其實跟bool結合一樣是評分的相加,評分不在這里細說了;

2.雖然可以提高相關度評分,但是還是存在可能match很低+一個很低的match_phrase結果沒有單獨只匹配了一個match的分數高的情況,但是這是很極限了,也是符合相關度評分原理的;

3.由於match_phrase是在搜索階段進行的計算,會影響搜索效率,據說比term查詢慢20倍,所以不要進行大文本量的字段搜索,盡量進行標題,名字這種類型的搜索才使用這個;

4.本文章沒有討論在文本數據重復時的情況,即文本中有多個"[東][方][賓][館]"和query文本中有多個"[東][方][賓][館]"分詞的情況,但是原理是一樣的還是取距離轉換的最小值;

5.文中使用了standard分詞,實際上可能會用不同的分詞器,但是建議使用match_phrase時使用標准的一個個分詞,這樣是方便進行鄰近搜索的控制的,如果使用ik等分詞,執行match_phrase時分詞是不可控的,所以結果也是不可控。match使用ik,match_phrase用standard結合一起使用也是可以的;

6.鄰近搜索效率較低,其實可以通過增加詞庫的方式進行單純使用match匹配效率是最高的,前提是你知道客戶會搜索什么,這又是另一個研究話題了

 

更新[2019-05-22]:

補充:短語匹配match_phrase必須要滿足下面的要求才能認定和["東方賓館"]這個詞條匹配(以standard分析器為例)

1.搜索的詞必須有且僅有["東","方","賓","館"]這幾個詞(對於中文是字)的一個或者多個,如果有其他的詞(對於中文是字)是不會匹配到的,slop不是完全等同於萊文斯坦距離,可以理解成字符的偏移

2.查詢詞中偏移量應該跟文檔中詞的偏移量一樣,或者在slop的偏差范圍內,就是上文解析的意思。

 

這里講解一下fuzzy和match_phrase的區別

1.fuzzy是詞/項級別的模糊匹配,match_phrase是基於短語級別的

例如對於英文(standard分析器)來說"dog cat bird"來說"dog"就是一個詞/詞項,而"dog cat"就是一個短語,因此作用范圍不一樣

2.fuzzy是基於萊文斯坦距離的,所以fuzzy是可以容錯的例如你輸入"dcg" 你也可以匹配到"dog cat bird",但是這里注意的是你的查詢只能是單詞條的查詢,不能"dcg cat",如果你需要查詢短語里面的拼寫錯誤,可以使用match的fuzziness參數,match_phrase是不允許出現不存在的詞條的。

下面是對於fuzzy和match_phrase和match 添加fuzziness參數進行對比

文檔內容是{"name":"dog cat bird"} 分析器是standard
--------------------------------------------------------------------------------------------------
1.使用拼寫錯誤的"cot"可以使用fuzzy匹配但是,如果是下面這種,短語是不可以的,輸入應當是詞條,而不是短語

GET test_save/_search { "query": { "fuzzy": { "name":{ "value": "bird cot", "fuzziness": 1 } } } }
--------------------------------------------------------------------------------------------------
2.這里可以匹配到因為match先分解成bird 和 cot 其中bird可以匹配到,同時cot也是可以匹配到,只不過分數要比輸入"bird cat"要低
GET test_save
/_search { "query": { "match": { "name":{ "query": "bird cot", "fuzziness":1 } } } }
-----------------------------------------------------------------------------------------------
3.這里由於cot和文本中的cat不是同一個詞,所以是無法匹配到的
GET test_save
/_search { "query": { "match_phrase": { "name": { "query": "bird cot", "slop": 1 } } } }

以上是對於英文的單詞的解析,對於中文其實也是一樣,只是由於中文如果使用standard一個詞項就是一個字,因此使用因此分詞后的數據對於fuzzy模糊匹配來說意義不大,但是可以使用keyword進行

GET test_save/_search
{
  "query": {
    "fuzzy": {
      "name.keyword":{
        "value": "東日賓館",
        "fuzziness": 1
      }
    }
  }
}
這樣是可以匹配到"東方賓館"的數據的,但是無法匹配"廣州東方賓館"因為萊文斯坦距離已經不止1了

其實短語匹配應該叫臨近查詢更適合些

 

以上就是對模糊查詢和短語匹配的解析和補充~

 

[說明]:elasticsearch版本5.6.4


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM