Elasticsearch系列---多字段搜索

概要

本篇介紹一下multi_match的best_fields、most_fields和cross_fields三種語法的場景和簡單示例。

最佳字段

bool查詢採取"more-matches-is-better"匹配越多分越高的方式,所以每條match語句的評分結果會被加在一起,從而爲每個文檔提供最終的分數_score。能與兩條語句同時匹配的文檔會比只與一條語句匹配的文檔得分要高,但有時這樣也會帶來一些與期望不符合的情況,我們舉個例子:

我們以英文兒歌爲案例背景,我們這樣搜索:

GET /music/children/_search
{
  "query": {
    "bool": {
      "should": [
        { "match": { "name":  "brush mouth" }},
        { "match": { "content": "you sunshine"   }}
      ]
    }
  }
}

結果響應(有刪減)

{
  "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語法查詢,優先將最佳匹配的評分作爲查詢的評分結果返回,請求如下:

GET /music/children/_search
{
  "query": {
    "dis_max": {
      "queries": [
        { "match": { "name":  "brush mouth" }},
        { "match": { "content": "you sunshine"   }}
      ]
    }
  }
}

結果響應(有刪減)

{
  "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,會考慮所有的匹配條件,但最佳匹配語句仍然佔大頭。

請求示例:

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匹配了儘可能多關鍵詞的文檔優先返回回來。

如果我們在多個字段上使用相同的搜索字符串進行搜索,請求語法可以冗長一些:

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參數的設置:

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片斷信息)。我們做一點小修改:

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兩個字段同時進行搜索,示例:

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支持,可以儘可能精準地將匹配的結果推送到最前面
  • 缺點:除了那些精準匹配的結果,其他差不多大的結果,排序結果不是太均勻,沒有什麼區分度了

實際的例子:百度之類的搜索引擎,最匹配的到最前面,但是其他的就沒什麼區分度了

  1. 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查詢

GET /music/children/_search
{
  "query": {
    "multi_match": {
      "query":       "Peter Raffi",
      "type":        "most_fields",
      "fields":      [ "author_first_name", "author_last_name" ]
    }
  }
}

響應的結果:

{
  "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的示例如下:

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字段,存儲完整的名稱的。

GET /music/children/_search
{
  "query": {
    "match": {
      "author_full_name": {
        "query": "Peter Raffi",
        "operator": "and"
      }
    }
  }
}

單字段的查詢,就可以隨心所欲的指定operator或minimum_should_match來控制精度了。

我們看一下前面提到的3個問題能否解決

  1. 匹配問題

解決,最匹配的數據優先返回。

  1. 長尾問題

解決,可以指定operator或minimum_should_match來控制精度。

  1. 評分不準的問題

解決,所有信息在一個字段裏,IDF計算時次數是均勻的,不會有極端的誤差。

缺點:
需要前期設計時冗餘字段,佔用的存儲會多一些。
copy_to拼接字段時,會遇到順序問題,如英文名稱名前姓後,而地址順序則不固定,有的從省到街道由大到小,有的是反的,這也是侷限性之一。

原生cross_fields語法

multi_match有原生的cross_fields語法解決跨字段實體搜索問題,請求如下:

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中出現

  1. 長尾問題

解決,參見上一條,每個term都必須匹配,長尾問題自然迎刃而解。

  1. 評分不準的問題

解決,cross_fields通過混合不同字段逆向索引文檔頻率的方式解決詞頻的問題,具體來說,Peter在first_name中頻率會高一些,在last_name中頻率會低一些,在兩個字段得到的IDF值,會取小的那個,Raffi也是同樣處理,這樣得到的IDF值就比較正常,不會偏高。

小結

我們可以花一點時間瞭解一下多字段搜索的場景,和要注意的細節點,精準搜索是一個非常大的話題,優化的空間沒有上限,可以先從最基礎的場景和調整語法開始嘗試。

專注Java高併發、分佈式架構,更多技術乾貨分享與心得,請關注公衆號:Java架構社區
可以掃左邊二維碼添加好友,邀請你加入Java架構社區微信羣共同探討技術
Java架構社區

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章