ElasticSearch 2 (15) - 深刻搜索系列之多字段搜索

ElasticSearch 2 (15) - 深刻搜索系列之多字段搜索

摘要

查詢不多是簡單的一句話匹配(one-clause match)查詢。不少時候,咱們須要用相同或不一樣的字符串查詢1個或多個字段,也就是說,咱們須要對多個查詢語句以及他們相關分數(relevance scores)進行有意義的合併。html

有時候或許咱們正查找一本名爲戰爭與和平(War and Peace)而做者叫Leo Tolstoy的書,或許咱們正用「最少匹配」(「minimum should match」)的方式在文檔中進行查找(多是頁面標題,也多是頁面的內容),或許咱們正搜索全部名字爲 John Smith 的用戶。算法

本篇文章中,咱們會介紹構造多語句搜索的工具以及不一樣場景下不一樣的適合解決方案。app

版本

elasticsearch版本: elasticsearch-2.xdom

內容

多字符串查詢(Multiple Query Strings)

最簡單的多字段(multifield)查詢是能夠將搜索術語與具體字段映射的。若是咱們知道 War and Peace 是標題,Leo Tolstoy 是做者,咱們只須要將兩個條件寫成 match 語句,而後將他們用 bool 查詢組合起來便可:elasticsearch

GET /_search
{
  "query": {
    "bool": {
      "should": [
        { "match": { "title":  "War and Peace" }},
        { "match": { "author": "Leo Tolstoy"   }}
      ]
    }
  }
}

bool 查詢的採用越多匹配越好 (more-matches-is-better)的方式,因此每一個 match 語句的分數結果會加和在一塊兒,爲每一個文檔獲得一個最終的分數,能與兩個語句同時匹配的文檔比只與一個語句匹配的文檔得分要高。ide

固然,咱們並非只能使用 match 語句:能夠用 bool 查詢組合任意其餘類型的查詢,甚至其餘的 bool 查詢。咱們能夠爲上面的例子添加特定譯者版本的偏好:工具

GET /_search
{
  "query": {
    "bool": {
      "should": [
        { "match": { "title":  "War and Peace" }},
        { "match": { "author": "Leo Tolstoy"   }},
        { "bool":  {
          "should": [
            { "match": { "translator": "Constance Garnett" }},
            { "match": { "translator": "Louise Maude"      }}
          ]
        }}
      ]
    }
  }
}

爲何將譯者條件語句放入另外一個獨立的 bool 查詢中呢?全部的4個 match 查詢都是 should 語句,咱們爲何不將translator語句與其餘語句(如 titleauthor)放在同一層呢?post

答案在於分數的計算方式。bool 查詢執行每一個 match 查詢,而後把他們加在一塊兒,而後將結果與全部匹配的語句數量相乘,再除以全部的語句數量。處於同一層的每條語句具備相同的權重。在上面這個例子中,包含translator語句的 bool 查詢,只佔總分數的三分之一,若是咱們將translator語句與 titleauthor 兩個語句放入同一層,那麼 titleauthor 語句只貢獻四分之一。性能

句子的優先級排序

有可能上面這個例子中每一個語句貢獻三分之一的分數並非咱們想要的,咱們極可能對 titleauthor 兩個句子更感興趣,這樣咱們就須要調整查詢,使 titleauthor 語句更重要。測試

在咱們軍械庫中,最容易使用的武器就是 boost 參數。爲了提升 titleauthor 字段的權重,咱們爲他們分配高於1的 boost 值。

GET /_search
{
  "query": {
    "bool": {
      "should": [
        { "match": { #1
            "title":  {
              "query": "War and Peace",
              "boost": 2
        }}},
        { "match": { #2
            "author":  {
              "query": "Leo Tolstoy",
              "boost": 2
        }}},
        { "bool":  { #3
            "should": [
              { "match": { "translator": "Constance Garnett" }},
              { "match": { "translator": "Louise Maude"      }}
            ]
        }}
      ]
    }
  }
}
  • #1 #2 titleauthor 語句的 boost 值爲2。
  • 嵌套的 bool 語句的 boost 值爲1。

獲取 boost 參數最佳值的一個比較簡單的方式就是須要不斷試錯:設定 boost 值,運行測試查詢,如此反覆。boost 值比較合理的一個區間是1到10,固然也有多是15。若是爲 boost 分配比這更高的值將不會對最終的結果產生更大影響,由於分數最後會被規範化(normalized)。

單字符串查詢(Single Query String)

bool 查詢是多語句查詢的主幹。它的適用場景不少,特別是當咱們須要將不一樣查詢字符串與不一樣字段創建映射的時候。

有些用戶指望將全部的搜索術語堆積到一個字段中,而後指望ElasticSearch能理解這些搜索,併爲他們提供正確的結果。有意思的是多字段搜索的表單一般被稱爲 高級查詢 (Advanced Search)- 由於它對用戶而言是高級的,可是,多字段搜索在實現上卻很是簡單。

對於多詞(multiword)、多字段(multifield)查詢沒有簡單的一刀切方案。爲了獲得最佳結果,咱們除了須要瞭解如何使用合適的工具外,還須要瞭解咱們的數據。

瞭解咱們的數據(Know Your Data)

當咱們的用戶輸入了一個單字符串查詢的時候,咱們一般會遇到一下情形:

  • 最佳字段

    當咱們搜索具備具體概念的詞的時候,好比「brown fox」,詞組比它們各自更有意義。像 titlebody 這樣的字段,儘管他們是相關的,可是他們也彼此相互競爭。當文檔在相同字段中具備更多詞的時候,最終的分數來自於最匹配字段(best-matching field)。

  • 多數字段

    爲了對相關度進行微調,一個經常使用的技術是將相同的數據索引到不一樣的字段中,它們各自具備獨立的分析鏈。

    主字段(main field)可能包括他們的詞源、同義詞,以及變音詞或口音詞。用它們來匹配儘量多的文檔。

    相同的文本被索引到其餘字段,以提供更精確的匹配。一個字段能夠包括原詞,其餘詞源、口音,以及能夠提供詞語類似性的 瓦片詞 (shingles)。

    其餘字段是做爲匹配每一個文檔時提升相關度分數的信號詞,越多字段能匹配則越好。

  • 混合字段

    對於某些實體,咱們須要在多個字段中肯定其信息,單個字段都只能做爲總體的一部分:

    • Person: first_namelast_name
    • Book: titleauthordescription
    • Address: streetcitycountrypostcode

    在這種狀況下,咱們但願在全部這些列出的字段中找到儘量多的詞,這有如在一個大的字段中進行搜索,這個大的字段包括全部列出字段。

上述全部的全部都是多詞、多字段查詢(mutiword,multifield queries),可是每一個具體查詢都須要使用不一樣的策略。在後面章節中,咱們會依次介紹這些策略。

最佳字段(Best Fields)

若是咱們有個網站併爲用戶提供博客內容搜索的功能,如下面兩個博客內容文檔爲例:

PUT /my_index/my_type/1
{
    "title": "Quick brown rabbits",
    "body":  "Brown rabbits are commonly seen."
}

PUT /my_index/my_type/2
{
    "title": "Keeping pets healthy",
    "body":  "My quick brown fox eats rabbits on a regular basis."
}

用戶輸入詞組「Brown fox」而後點擊搜索按鈕。事先,咱們並不知道用戶的搜索術語是會在 title 仍是 body 中被找到,可是,用戶頗有多是想對「Brown fox」這個相關詞組進行搜索。以肉眼判斷,文檔2的匹配度更高,由於它同時具備兩個詞:

咱們用bool查詢試試:

{
    "query": {
        "bool": {
            "should": [
                { "match": { "title": "Brown fox" }},
                { "match": { "body":  "Brown fox" }}
            ]
        }
    }
}

可是咱們發現查詢的結果是文檔1具備更高分數:

{
  "hits": [
     {
        "_id":      "1",
        "_score":   0.14809652,
        "_source": {
           "title": "Quick brown rabbits",
           "body":  "Brown rabbits are commonly seen."
        }
     },
     {
        "_id":      "2",
        "_score":   0.09256032,
        "_source": {
           "title": "Keeping pets healthy",
           "body":  "My quick brown fox eats rabbits on a regular basis."
        }
     }
  ]
}

爲了理解這個現象,咱們須要回想一下 bool 是如何計算分數的:

  1. 它會執行 should 語句中的兩個查詢
  2. 將兩個查詢的分數相加
  3. 與總匹配語句的數目相乘
  4. 併除以總語句的數目(這裏爲:2)

文檔1中,兩個字段都包含 brown 這個詞,因此兩個 match 語句都成功匹配且有一個分數。文檔2中,body 字段同時包含 brownfox 這兩個詞,可是 title 字段沒有包含任何詞。這樣,body 查詢結果中的高分,加上 title 查詢中的0分,並乘以二分之一,就獲得了一個比文檔1更低的總體分數。

注:

以公式表示文檔1的分數爲:

(score_of_doc_1_title_match + score_of_doc_1_body_match) * total_number_of_match_clause / total_number_of_clause

其中:
score_of_doc_1_body_match = 0
total_number_of_match_clause = 1
total_number_of_clause = 2

在這個例子中,titlebody 兩個字段處於競爭地位,因此咱們就須要找到單個最佳匹配(best-matching)字段。

若是咱們不是簡單將每一個字段的分數結果加在一塊兒,而是將最佳匹配(best-matching)字段的分數做爲總體查詢的分數,會有怎樣的結果?這樣返回的結果多是:同時包含兩個詞的單個字段 比 相同詞語反覆出現的多個不一樣字段 相關度更高。

dis_max 查詢

咱們可使用 dis_max分離最大化查詢(Disjunction Max Query)。分離(Disjunction)的意思是或(or),這與能夠把結合(conjunction)理解成與(and)對應。分離最大化查詢(Disjunction Max Query)指的是:將任何與任一查詢匹配的文檔做爲結果返回,可是隻將最佳匹配的分數做爲查詢的結果分數。

{
    "query": {
        "dis_max": {
            "queries": [
                { "match": { "title": "Brown fox" }},
                { "match": { "body":  "Brown fox" }}
            ]
        }
    }
}

這個查詢的結果爲:

{
  "hits": [
     {
        "_id":      "2",
        "_score":   0.21509302,
        "_source": {
           "title": "Keeping pets healthy",
           "body":  "My quick brown fox eats rabbits on a regular basis."
        }
     },
     {
        "_id":      "1",
        "_score":   0.12713557,
        "_source": {
           "title": "Quick brown rabbits",
           "body":  "Brown rabbits are commonly seen."
        }
     }
  ]
}

最佳字段查詢調優(Tuning Best Fields Queries)

當用戶搜索「quick pets」時會發生什麼呢?使用以前的例子,兩個文檔都包含詞 quick,可是隻有文檔2包含詞 pets,兩個文檔中都不具備同時包含兩個詞的字段。

以下,一個簡單的 dis_max 查詢會採用單個最佳匹配(best matching)字段,而後忽略其餘的匹配:

{
    "query": {
        "dis_max": {
            "queries": [
                { "match": { "title": "Quick pets" }},
                { "match": { "body":  "Quick pets" }}
            ]
        }
    }
}

結果是:

{
  "hits": [
     {
        "_id": "1",
        "_score": 0.12713557, #1
        "_source": {
           "title": "Quick brown rabbits",
           "body": "Brown rabbits are commonly seen."
        }
     },
     {
        "_id": "2",
        "_score": 0.12713557, #2
        "_source": {
           "title": "Keeping pets healthy",
           "body": "My quick brown fox eats rabbits on a regular basis."
        }
     }
   ]
}
  • #1 #2 注意這兩個分數是相同的。

咱們可能指望在這個例子中,可以同時匹配 titlebody 字段的文檔比只與一個字段匹配的文檔的相關度更高,但事實並不是如此,由於 dis_max 查詢只會使用單個最佳匹配語句的分數(*_score*)做爲總體分數。

打破平衡(tie_breaker)

咱們可使用 tie_breaker 這個參數將其餘匹配語句的分數也考慮其中:

{
    "query": {
        "dis_max": {
            "queries": [
                { "match": { "title": "Quick pets" }},
                { "match": { "body":  "Quick pets" }}
            ],
            "tie_breaker": 0.3
        }
    }
}

這個查詢的結果以下:

{
  "hits": [
     {
        "_id": "2",
        "_score": 0.14757764, #1
        "_source": {
           "title": "Keeping pets healthy",
           "body": "My quick brown fox eats rabbits on a regular basis."
        }
     },
     {
        "_id": "1",
        "_score": 0.124275915, #2
        "_source": {
           "title": "Quick brown rabbits",
           "body": "Brown rabbits are commonly seen."
        }
     }
   ]
}
  • #1 #2 能夠看到,文檔2比文檔1在相關度上有微弱優點。

tie_breaker 參數的出現其實是提供了一種處於 dis_maxbool 中間狀態的查詢,它分數計算的方式以下:

  1. 得到最佳匹配(best-matching)語句的分數 *_score*
  2. 將其餘匹配語句的得分與 tie_breaker 相乘
  3. 將以上分數求和並規範化(normalize)

因爲tie_breaker的做用,全部匹配語句都會被考慮其中,可是最佳匹配語句仍然佔最終結果的大頭。

注意:
tie_breaker 能夠是 0 到 1 之間的浮點數,其中,若是數值爲0,即表明使用dis_max最佳匹配語句的普通邏輯,若是數值爲1,即表示全部匹配語句同等重要。最佳的準確值須要根據數據與查詢進行調試得出,可是合理的值一般與零接近(處於 0.1 - 0.4 之間),這樣的合理值不會改變 dis_max 使用最佳匹配的本質。

多配查詢(multi_match查詢)

multi_match 查詢爲反覆執行在多個字段上的查詢提供了一種簡便的方式。

注意:
multi_match 查詢的類型有多種,其中的三種恰巧與 瞭解咱們的數據(Know YOur Data) 中介紹的三個場景對應,即:best_fields,most_fields,cross_fields。

默認狀況下,下面這個查詢的類型是 best_fields,這表示它會爲每一個字段生成一個查詢,而後將他們組合到dis_max 查詢的內部:

{
  "dis_max": {
    "queries":  [
      {
        "match": {
          "title": {
            "query": "Quick brown fox",
            "minimum_should_match": "30%"
          }
        }
      },
      {
        "match": {
          "body": {
            "query": "Quick brown fox",
            "minimum_should_match": "30%"
          }
        }
      },
    ],
    "tie_breaker": 0.3
  }
}

上面這個查詢以 multi_match 重寫更爲簡潔:

{
    "multi_match": {
        "query":                "Quick brown fox",
        "type":                 "best_fields", #1
        "fields":               [ "title", "body" ],
        "tie_breaker":          0.3,
        "minimum_should_match": "30%" #2
    }
}
  • #1 這個 best_fields 類型是默認值,能夠不指定。
  • #2 如 minimum_should_matchoperator 這樣的參數會被傳遞到生成的 match 查詢中。

查詢字段名稱的模糊匹配(Using Wildcards in Field Names)

字段名稱能夠用模糊匹配的方式給出:任何與模糊匹配(wildcard)正則匹配的字段都會被包括在搜索中,好比,咱們可使用一下方式同時匹配 book_titlechapter_titlesection_title 這三個字段:

{
    "multi_match": {
        "query":  "Quick brown fox",
        "fields": "*_title"
    }
}

增長單個字段的權重(Boosting Individual Fields)

可使用脫字號(caret ^ )的語法爲單個字段增長權重:只須要在字段末尾添加 ^boost,其中 boost 是一個浮點數:

{
    "multi_match": {
        "query":  "Quick brown fox",
        "fields": [ "*_title", "chapter_title^2" ] #1
    }
}
  • #1 chapter_title 這個字段的boost值爲2,而其餘兩個字段 book_titlesection_title 字段具備默認的boost值爲1。

多數字段(Most Fields)

全文搜索被稱做是召回(Recall)與準確(Precision)的戰場:召回(Recall)指的是返回結果中的全部文檔都是相關的;準確(Precision)指的是返回結果中沒有不相關的文檔。目的是,在結果的第一頁中,爲用戶呈現最相關的文檔。

爲了提升召回(Recall)的效果,咱們在全網中進行搜索,不只返回與用戶搜索術語精確匹配的文檔,還會返回咱們認爲與查詢相關的全部文檔。若是一個用戶搜索「quick brown box」,一個包含詞語fast foxes的文檔出如今結果集中會被認爲是很是合理的。

固然,若是包含詞語fast foxes的文檔是咱們找到的惟一相關文檔,那麼它會出如今結果集的頂端,可是,若是有100個文檔都出現了詞語「quick brown fox」,那麼這個包含詞語fast foxes的文檔會被認爲是次相關的,它可能處於返回結果列表的下面某個地方。當包含了不少潛在匹配以後,咱們須要將最匹配的幾個放在結果集的頂端。

對全文相關度提升精度的一個經常使用方式是爲同一文本創建不一樣方式的索引,每種方式都提供了一個不一樣的相關度信號(signal)。主字段(main field)會包括最寬匹配(broadest-matching)形式的術語去儘量的匹配更多的文檔。舉個例子,咱們能夠進行一下操做:

  • 使用詞jump做爲根(root)來索引 jumps、jumping和jumped樣的詞。這樣,不管用戶使用 jumped 仍是 jumping 進行搜索,都能找到匹配的文檔。
  • 將同義詞包括其中,如jump、leap和hop。
  • 移除變音或口音詞:如ésta、 está和esta都會以無變音形式創建索引。

儘管如此,若是咱們有兩個文檔,其中一個包含詞jumped,另外一個包含詞jumping,若是咱們使用jumped進行搜索的時候,當前指望前者能有更高的排名。

爲了解決這個問題,咱們能夠將相同的文本索引到其餘字段中去以提供更精確的匹配。一個字段多是爲非詞根的版本,另外一個字段多是變音過的原始詞,還有一個字段可能使用瓦片詞(shingles)以提供詞語類似性的信息。這些其餘的字段做爲提升每一個文檔的相關度分數的信號(signals),匹配的字段越多越好。

一個文檔若是與寬匹配的主字段匹配,那麼它會出如今結果列表中,若是它同時與信號(signal)字段匹配,它會獲得加分,系統會上提它在結果列表中的位置。

咱們會稍後討論同義詞、詞類似性、半匹配以及其餘潛在的信號,這裏咱們只使用詞幹(stemmed)和非詞幹(unstemmed)字段做爲簡單例子來講明這種技術。

多字段映射(Multifield Mapping)

第一件要作的事情就是要對咱們的字段索引兩次:一次詞幹模式和一次非詞幹模式。咱們使用 multifields 來實現(multifields 在String Sorting and Multifields中介紹過)。

DELETE /my_index

PUT /my_index
{
    "settings": { "number_of_shards": 1 }, #1
    "mappings": {
        "my_type": {
            "properties": {
                "title": { #2
                    "type":     "string",
                    "analyzer": "english",
                    "fields": {
                        "std":   { #3
                            "type":     "string",
                            "analyzer": "standard"
                        }
                    }
                }
            }
        }
    }
}
  • #1 參考被破壞的相關度
  • #2 title 字段使用 english 分析器進行詞幹分析。
  • #3 title.std 字段使用 standard 標準分析器進行非詞幹分析。

接着咱們索引一些文檔:

PUT /my_index/my_type/1
{ "title": "My rabbit jumps" }

PUT /my_index/my_type/2
{ "title": "Jumping jack rabbits" }

這裏用一個簡單 match 查詢 title 字段是否包含 jumping rabbits

GET /my_index/_search
{
   "query": {
        "match": {
            "title": "jumping rabbits"
        }
    }
}

因爲使用 english 分析器,這個查詢是在查找以 jumprabbit 這兩個詞幹做爲術語的文檔。兩個文檔的 title 字段都同時包括兩個術語,因此兩個文檔獲得的分數相同:

{
  "hits": [
     {
        "_id": "1",
        "_score": 0.42039964,
        "_source": {
           "title": "My rabbit jumps"
        }
     },
     {
        "_id": "2",
        "_score": 0.42039964,
        "_source": {
           "title": "Jumping jack rabbits"
        }
     }
  ]
}

若是咱們只是對 title.std 字段進行查詢,那麼只有文檔2是匹配的。儘管如此,若是咱們對兩個字段同時查詢,而後使用 bool 查詢將分數結果合併,則兩個文檔都是匹配的(title 字段的做用),並且文檔2的相關度分數更高(title.std 字段的做用):

GET /my_index/_search
{
   "query": {
        "multi_match": {
            "query":  "jumping rabbits",
            "type":   "most_fields", #1
            "fields": [ "title", "title.std" ]
        }
    }
}
  • #1 咱們但願將全部匹配字段的分數合併起來,因此咱們使用 most_fields 類型。這使 multi_match 查詢用 bool 查詢將兩個字段語句包在裏面,而非 dis_max 查詢。

    {
    "hits": [
    {
    "_id": "2",
    "_score": 0.8226396, #1
    "_source": {
    "title": "Jumping jack rabbits"
    }
    },
    {
    "_id": "1",
    "_score": 0.10741998, #2
    "_source": {
    "title": "My rabbit jumps"
    }
    }
    ]
    }

  • #1 #2 文檔2如今的分數要比文檔1高。

咱們用寬匹配字段 title 包括儘量多的文檔——以增長召回(Recall)——同時又使用字段 title.std 做爲信號(signal)將相關度更高的文檔置入結果集的頂部。

每一個字段對於最終分數的貢獻能夠經過自定義值 boost 來控制。好比,咱們使 title 字段更爲重要,這樣同時也下降了其餘信號字段的做用:

GET /my_index/_search
{
   "query": {
        "multi_match": {
            "query":       "jumping rabbits",
            "type":        "most_fields",
            "fields":      [ "title^10", "title.std" ] #1
        }
    }
}
  • #1 title 字段的 boost 的值爲10使它比 title.std 更重要。

如今咱們討論一種廣泛的搜索模式:跨字段實體搜索(cross-fields entity search)。在如人 (person)產品(product)地址(address) 這樣的實體中,咱們須要使用多個字段來惟一辨認它的信息。一個 人(person) 實體多是這樣索引的:

{
    "firstname":  "Peter",
    "lastname":   "Smith"
}

而一個 地址(address) 多是這樣

{
    "street":   "5 Poland Street",
    "city":     "London",
    "country":  "United Kingdom",
    "postcode": "W1V 3DG"
}

這與咱們以前說的多字符串查詢很像,可是這裏有一個巨大的區別。在多字符串查詢中,咱們爲每一個字段使用不一樣的字符串,在這個例子中,咱們想使用單個字符串在多個字段中進行搜索。

咱們的用戶可能想要查找 「Peter Smith」這我的 或 「Poland Street W1V」這個地址,這些詞都出如今不一樣的字段中,因此若是使用 dis_max / best_fields 查詢去查找單個最佳匹配字段顯然是錯誤的。

一種弱弱的方式(A Naive Approach)

咱們依次查詢每一個字段,而後每一個字段的匹配結果相加,這看起來像個 bool 查詢:

{
  "query": {
    "bool": {
      "should": [
        { "match": { "street":    "Poland Street W1V" }},
        { "match": { "city":      "Poland Street W1V" }},
        { "match": { "country":   "Poland Street W1V" }},
        { "match": { "postcode":  "Poland Street W1V" }}
      ]
    }
  }
}

爲每一個字段重複查詢字符串會使查詢變得冗長,咱們可使用 multi_match 查詢,將類型設置成 most_fields 而後告訴ELasticSearch合併全部匹配字段的分數:

{
  "query": {
    "multi_match": {
      "query":       "Poland Street W1V",
      "type":        "most_fields",
      "fields":      [ "street", "city", "country", "postcode" ]
    }
  }
}

most_fields 方式的問題

most_fields 這種方式搜索會有一些問題,這些問題不會立刻顯現出來:

  • 它是爲多數字段匹配任意詞而設計的,而不是在全部字段中找到最匹配的。
  • 它不能使用參數 operatorminimum_should_match 來減小此相關結果中的長尾。
  • 詞頻對於每一個字段是不同的,並且它們之間相互影響可能會生成一個很差的排序結果。

以字段爲中心的查詢(Field-Centric Queries)

上面這三個來自於 most_fields 的問題都是由於它是 字段爲中心的(field-centric)而不是 術語爲中心的(term-centric)。當咱們對術語(terms)匹配真正感興趣時,它爲咱們查找的是最匹配的字段(fields)。

注意:

best_fields 類型也是字段爲中心的(field-centric)有着相似的問題。

首先咱們來看看這些問題存在的緣由,而後再來解決它們。

問題 1:多個字段從匹配相同詞(Matching the Same Word in Multiple Fields)

回想一下 most_fields 查詢是如何執行的:ElasticSearch爲每一個字段生成一個查詢,而後用 bool 查詢將他們包裹起來。

咱們能夠經過 validate-query API來查看:

GET /_validate/query?explain
{
  "query": {
    "multi_match": {
      "query":   "Poland Street W1V",
      "type":    "most_fields",
      "fields":  [ "street", "city", "country", "postcode" ]
    }
  }
}

生成的解釋(explanation)爲:

(street:poland   street:street   street:w1v)
(city:poland     city:street     city:w1v)
(country:poland  country:street  country:w1v)
(postcode:poland postcode:street postcode:w1v)

獲得的結果中,一個兩個字段與 poland 匹配的文檔比一個字段內同時匹配 polandstreet 的文檔分數要高。

問題 2:剪掉長尾(Trimming the Long Tail)

精度控制(Controlling Precision)中,咱們討論了使用 and 操做符 或者 設置minimum_should_match 參數來消除結果中不相關的長尾:

{
    "query": {
        "multi_match": {
            "query":       "Poland Street W1V",
            "type":        "most_fields",
            "operator":    "and", #1
            "fields":      [ "street", "city", "country", "postcode" ]
        }
    }
}
  • #1 全部詞必須呈現。

可是對於 best_fieldsmost_fields 這樣的參數會在 match 查詢生成時被傳入,這個查詢的 explaination 以下:

(+street:poland   +street:street   +street:w1v)
(+city:poland     +city:street     +city:w1v)
(+country:poland  +country:street  +country:w1v)
(+postcode:poland +postcode:street +postcode:w1v)

換句話說,使用 and 操做符要求全部詞都必須存在於相同字段,顯然這樣是不對的!可能沒有任何文檔能與這個查詢匹配。

問題 3:詞頻(Term Frequencies)

什麼是相關(What is Relevance)中,咱們解釋了每一個術語使用默認類似度算法是 TF/IDF:

  • 詞頻(Term frequency)

    一個詞在單個文檔的某個字段中出現的頻率越高,這個文檔的相關度越高。

  • 逆向文件頻率(Inverse document frequency)

    一個詞在全部文檔索引中出現的頻率越高,這個詞的相關度越低。

當咱們在多個字段中進行搜索時,TF/IDF能夠爲咱們帶來某些意外的結果。

考慮咱們用字段 first_namelast_name 字段查詢 「Peter Smith」的例子,Peter是一個普通的名(first name)同時Smith也是個一個很是普通的姓(last name),他們都具備較低的IDF值。可是當咱們索引中有另一我的的名字是 「Smith Williams」時,Smith做爲姓(first name)就變得很是不普通以至它有一個較高的IDF值。

下面這個簡單的查詢可能會在結果中將 「Smith Williams」 置於 「Peter Smith」之上,儘管事實上第二我的比第一我的更匹配。

{
    "query": {
        "multi_match": {
            "query":       "Peter Smith",
            "type":        "most_fields",
            "fields":      [ "*_name" ]
        }
    }
}

這裏的問題是 Smith 在名字段中有着高IDF,它會削弱 「Peter」做爲名和「Smith」做爲姓時較低的IDF的做用。

解決方案(Solution)

這些問題存在的緣由在於它處理着多個字段,若是咱們將全部這些字段組合成單個字段,這個問題就會不復存在。咱們能夠爲Person文檔添加一個 full_name 字段:

{
    "first_name":  "Peter",
    "last_name":   "Smith",
    "full_name":   "Peter Smith"
}

當對 full_name 進行查詢時:

  • 具備更多匹配詞的文檔會比只有一個重複匹配詞的文檔更重要。
  • 參數 minimum_should_matchoperator 會如咱們指望那樣工做。
  • 姓和名的逆向文件頻率被合併,因此 Smith 究竟是做爲姓出現,仍是做爲名出現?這個問題會變得可有可無。

這麼作固然是可行的,可是咱們不太喜歡存儲冗餘的數據。ElasticSearch爲咱們提供了兩個解決方案——一個是索引時的,另外一個是搜索時的——咱們會在稍後討論這兩個方案。

自定義_all字段(Custom *_all* Fields)

在元數據_all 字段中(**Metadata:_all Field**),咱們解釋過:*_all* 字段的索引方式是將全部其餘字段的值做爲一個巨大的字符串進行索引的。儘管這麼作並非十分靈活,可是咱們能夠爲人的姓名添加一個自定義 *_all* 字段,而後再爲地址添加另外一個 *_all* 字段。

ElasticSearch在字段映射中,爲咱們提供了一個 copy_to 參數來實現這個功能。

PUT /my_index
{
    "mappings": {
        "person": {
            "properties": {
                "first_name": {
                    "type":     "string",
                    "copy_to":  "full_name" #1
                },
                "last_name": {
                    "type":     "string",
                    "copy_to":  "full_name" #2
                },
                "full_name": {
                    "type":     "string"
                }
            }
        }
    }
}
  • #1 #2 first_namelast_name 字段中的值會被複制到 full_name 字段中。

有了這個映射,咱們可使用 first_name 查詢名,使用 last_name 查詢名,或者直接使用 full_name 查詢姓名。

注意:

映射中 first_name 和 last_name 並不知道 full_name是如何被索引的,full_name將兩個字段的內容複製到本地,而後自行索引。

跨字段查詢(cross_fields Queries)

自定義 _all 的方式是一個好的解決方案,咱們只須要在索引文件以前爲其設置好映射便可。不過,ElasticSearch還在搜索時(search-time)提供了相應的解決方案:使用類型 cross_fields 進行multi_match 查詢。cross_fields 使用以術語爲中心(term-centric)的查詢方式,這與 best_fieldsmost_fields 使用的字段爲中心(field-centric)的查詢方式很是不一樣。它將全部字段當作一個大的字段,而後在裏面查找每一個術語(each term)。

爲了說明這兩個查詢方式(field-centric和term-centric)的不一樣,咱們先看看下面這個以字段爲中心的 most_fields 查詢的 explanation

GET /_validate/query?explain
{
    "query": {
        "multi_match": {
            "query":       "peter smith",
            "type":        "most_fields",
            "operator":    "and", #1
            "fields":      [ "first_name", "last_name" ]
        }
    }
}
  • #1 全部術語都是必須的。

對於一個匹配的文檔,petersmith 都必須同時出如今同一字段中,要麼是 first_name字段中,要麼是 last_name 字段中:

(+first_name:peter +first_name:smith)
(+last_name:peter  +last_name:smith)

可是以術語爲中心的方式會是下面這樣:

+(first_name:peter last_name:peter)
+(first_name:smith last_name:smith)

換句話說,術語 petersmith 都必須出現,可是能夠出如今任意字段中。

cross_fields 類型首先分析查詢字符串並生成一個術語列表,而後它在全部字段從依次搜索每一個術語。這種不一樣的搜索方式很天然的解決了字段中心式查詢(Field-Centric Queries)三個問題中的二個。留給咱們的問題只是:逆向文件頻率不一樣。

幸運的是,cross_fields 一樣能夠解決這個問題,經過 validate-query 查看:

GET /_validate/query?explain
{
    "query": {
        "multi_match": {
            "query":       "peter smith",
            "type":        "cross_fields", #1
            "operator":    "and",
            "fields":      [ "first_name", "last_name" ]
        }
    }
}
  • #1 用cross_fields 術語中心式查詢。

它經過將不一樣字段的逆向索引文件頻率(inverse document frequency)混合的方式解決詞頻(term-frequency)的問題:

+blended("peter", fields: [first_name, last_name])
+blended("smith", fields: [first_name, last_name])

換句話說,它會同時在 first_namelast_name 兩個字段中查找 smith 的IDF,而後用二者的最小值做爲兩個字段的IDF。結果實際上就是:smith 會被認爲既是一個普通的姓,同時也是一個普通的名。

注意:

爲了讓 cross_fields 查詢以最優方式工做,全部的字段都須要使用相同的分析器,具備相同分析器的字段會被分組在一塊兒做爲混合字段使用。

若是包括了不一樣分析鏈的字段,它們會以 best_fields 的相同方式加到查詢結果中。例如:咱們將 title 字段加到以前的查詢中(假設他們使用的是不一樣的分析器),explaination 的結果以下:

(+title:peter +title:smith)
  (

      +blended("peter", fields: [first_name, last_name])
      +blended("smith", fields: [first_name, last_name])
  )

提升字段權重(Per-Field Boosting)

cross_fields 查詢與 自定義_all 相比的一個優點就是它能夠在搜索時,爲單個字段提高權重。

咱們不須要爲像 first_namelast_name這樣具備相同值的字段這麼作,可是若是要用 titledescription 字段搜索圖書,咱們可能但願爲 title 分配更多的權重,這一樣可使用前面介紹過的 脫字號(caret ^)語法來實現:

GET /books/_search
{
    "query": {
        "multi_match": {
            "query":       "peter smith",
            "type":        "cross_fields",
            "fields":      [ "title^2", "description" ] #1
        }
    }
}
  • \1 title 字段的 boost 爲 2,description 字段的boost 爲默認值 1。

可以爲單個字段指定boost值所帶來的好處須要權衡多字段查詢與單字段自定義_all之間的代價,即那種方案會給咱們帶來更大的(性能)壓力。

準確值字段(Exact-Value Fields)

在結束多字段查詢這個話題以前,咱們最後須要討論的是準確值 not_analyzed 字段。將 not_analyzed 字段與 multi_matchanalyzed 字段混在一塊兒沒有多大用處。

緣由能夠經過查看查詢的explaination獲得,假設咱們將 title 字段設置成 not_analyzed

GET /_validate/query?explain
{
    "query": {
        "multi_match": {
            "query":       "peter smith",
            "type":        "cross_fields",
            "fields":      [ "title", "first_name", "last_name" ]
        }
    }
}

由於 title 字段是未分析過的,ElasticSearch會將「peter smith」這個完整的字符串做爲查詢術語進行搜索:

title:peter smith
(
    blended("peter", fields: [first_name, last_name])
    blended("smith", fields: [first_name, last_name])
)

顯然這個術語不在title的反向索引中,因此須要在 multi_match 查詢中避免使用 not_analyzed 字段。

參考

elastic.co: Multifield Search

相關文章
相關標籤/搜索