Elasticsearch系列---多字段搜索

概要

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

最佳字段

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的數量。app

咱們來看一下匹配狀況:
文檔4的name字段包含brush,content字段包含you,因此兩個match都能獲得評分。
文檔3的name字段不匹配,可是content字段包含you和sunshine,命中一個match,只能得一項的分。
結果文檔4的得分會高一些。分佈式

但咱們仔細想想,文檔4雖然兩個match都匹配了,但每一個match只匹配了其中一個關鍵詞,文檔3只匹配了一個match,倒是同時匹配了兩個連續的關鍵詞,按咱們的預期,一個field上匹配了兩個連續關鍵詞的相關性應該高一些,簡單的把多個match的得分加起來,雖然分高一些,但不是咱們指望的首位。ide

咱們探尋的是最佳字段匹配,某一個字段匹配到了儘量多的關鍵詞,讓它排在前面;而不是更多的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架構社區

相關文章
相關標籤/搜索