查詢很少是對一個字段做 match 查詢,通常都是一個 query 查詢多個字段,比如一個 doc 有 title、content、pagetag 等文本字段,要在這些字段查詢含多個 term 的 query,就要對它們的相關度評分做合理的合並。這被稱為多詞(multiword)、多字段(multifield)查詢。
如果一個 query 可以結構化,如哪些詞是 title,哪些詞是 author,那么就可以直接在相關字段中查詢,使用 bool 查詢即可解決問題,bool 查詢是“匹配越多越好”,如搜“War and Peace Leo Tolstoy”,查詢語句如下:
GET /_search
{
"query": {
"bool": {
"should": [
{ "match": { "title": "War and Peace" }},
{ "match": { "author": "Leo Tolstoy" }}
]
}
}
}
還可以對不同的字段加不同的 boost 權重。
以上被稱為多重查詢字符串,也可算是結構化查詢,不過現實中通常是一個 query 在多個字段中查詢,即單一查詢字符串。畢竟對 query 做結構化需要些 nlp 技術和額外的人力成本,且比起單一查詢字符串的效果提升也有限,所以若不是對召回效果有更高追求,還是不要輕舉妄動,就好好做一個 query 在多個字段的查詢吧。
一個 query 在多個字段中的查詢,有三種策略:best_fields、most_fields、cross_fields。
介紹這三種策略之前,先鋪墊下布爾查詢和 dis_max 查詢。
1. bool 查詢
一個 query 在多個字段中的查詢,同樣可使用 bool 查詢。
{
"query": {
"bool": {
"should": [
{ "match": { "title": "Brown fox" }},
{ "match": { "body": "Brown fox" }}
]
}
}
}
不過由於 bool 查詢評分公式的問題,效果不太好,比如一個文檔 title 和 body 都包含 brown,不包含 fox,另一個文檔在 body 字段包含了 brown 和 fox,顯然后者更符合搜索意圖,但 bool 查詢的評分前者高,為了理解導致這樣的原因,需要看下 bool 查詢是如何計算評分的:
-
它會執行 should 語句中的兩個查詢。
-
加和兩個查詢的評分。
-
乘以匹配字段的總數(這里不知是否理解正確,存疑,待驗證)。
-
除以所有語句總數(這里為:2)。
注意這里的“乘以匹配語句的總數”是關鍵,這會導致匹配字段越多,分值越大。(后面的 most_fields 也是使用這個計算,才使得匹配字段數越多,分值越大)
解決方案是,使用最佳匹配字段的分值作為整個查詢的整體分值,讓包含 query 兩個單詞的字段有更高的權重,而不是在不同的字段中重復出現的相同單詞。dis_max 查詢應運而生。
2. dis_max
dis_max 查詢就是返回匹配了 query 的文檔,分值是最佳匹配字段產生的分值。加上 tie_breaker 可得出很好的搜索效果。
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "Quick pets" }},
{ "match": { "body": "Quick pets" }}
],
"tie_breaker": 0.3
}
}
}
3. best_fields
multi_match 查詢提供了一個簡便的方法對多個字段執行相同的查詢。默認情況下,該查詢以 best_fields 類型執行,它會為每個字段生成一個 match 查詢,然后將這些查詢包含在一個 dis_max 查詢中。
例如:
GET /_search
{
"query": {
"multi_match" : {
"query": "brown fox",
"type": "best_fields",
"fields": [ "subject", "message" ],
"tie_breaker": 0.3
}
}
}
執行時就變成了:
GET /_search
{
"query": {
"dis_max": {
"queries": [
{ "match": { "subject": "brown fox" }},
{ "match": { "message": "brown fox" }}
],
"tie_breaker": 0.3
}
}
}
可通過 caret 語法(^) 對個別字段加權,如:
{
"multi_match": {
"query": "Quick brown fox",
"fields": [ "title", "chapter_title^2" ]
}
}
best_fields 和 most_fields 都是以字段為中心的查詢,參數 operator 和 minimum_should_match 也是針對每個字段生效的,至少有一個字段滿足要求,才會通過篩選並進入下一步計分,計分時也只有符合要求的字段才會參與計分。
operator 默認為 or,如果設置為 and,那么字段必須匹配所有 query 分詞。當 operator 設為默認值 or 時,minimum_should_match 才會生效,設置每個字段應匹配分詞數。
所以有些 query 信息是分布在多個字段上的,這時就不適合設置 operator 為 and,會減少召回量。如果確認 query 信息一定完全在某個字段上,則可設為 and。
為與 cross_fields 做對比,這里舉個實際應用的例子。
搜索詞為“蘋果8plus國行”,文檔有三個字段:cateName、title、content,其中 cateName 和 content 用 ik_smart 分詞,title 用 ik_max_word 分詞(不同字段的分詞方法差異會在 cross_fields 中有所體現)。
看下 best_fields 查詢的實際執行。
ES 語句:
curl -XGET 'ip:port/indexname/_validate/query?explain&pretty' -d' { "query": { "function_score": { "query": { "bool": { "should": [{ "multi_match": { "query": "蘋果8plus國行", "fields": ["cate_name^1.0", "content^1.0", "title^1.0"], "type": "best_fields", "operator": "AND", "tie_breaker": 0.3 } }] } } } } } '
返回解釋:
(
(+cate_name:蘋果8 +cate_name:plus +cate_name:國行) |
(+content:蘋果8 +content:plus +content:國行) |
(+title:蘋果8 +title:蘋果 +title:8plus +title:8 +title:plus +title:國 +title:行)
)~0.3
明顯的以字段為中心的查詢。
tips:字段名可以通過通配符指定,如:
{
"multi_match": {
"query": "Quick brown fox",
"fields": "*_title"
}
}
4. most_fields
有時為了盡可能多地匹配文檔,會將同一文本的不同形式索引到多個字段。
ES語句(注意不要加 operator 或 minimum_should_match,不然就跟 best_fields 一樣了):
curl -XGET 'ip:port/indexname/_validate/query?explain&pretty' -d' { "query": { "function_score": { "query": { "bool": { "should": [{ "multi_match": { "query": "蘋果8plus國行", "fields": ["cate_name^1.0", "content^1.0", "title^1.0"], "type": "most_fields" } }] } } } } } '
返回解釋:
(
(cate_name:蘋果8 cate_name:plus cate_name:國 cate_name:行) |
(content:蘋果8 content:plus content:國 content:行) |
(title:蘋果8 title:蘋果 title:8plus title:8 title:plus title:國 title:行)
)~1.0
根據文檔,most_fields 查詢是用 bool 查詢將兩個字段語句包在里面,而不是像 best_fields 一樣用 dis_max。(不知這個怎么驗證,在自己的 ES 里試了下,看 explain 日志,沒看出跟 best_fields 時 tie_breaker=1 有什么差別)
5. cross_fields
ES語句:
curl -XGET 'ip:port/indexname/_validate/query?explain&pretty' -d' { "query": { "function_score": { "query": { "bool": { "should": [{ "multi_match": { "query": "蘋果8plus國行", "fields": ["cate_name^1.0", "content^1.0", "title^1.0"], "type": "cross_fields", "operator": "AND", "tie_breaker": 0.3 } }] } } } } } '
返回解釋:
(
(+blended(terms:[cate_name:蘋果8, content:蘋果8]) +
blended(terms:[cate_name:plus, content:plus]) +
blended(terms:[cate_name:國行])) |
(+title:蘋果8 +title:蘋果 +title:8plus +title:8 +title:plus +title:國 +title:行)
)~0.3
這里 title 要和 cate_name、content 分開計算的原因,是因為兩部分的分詞方法不同,term 也不同。
根據《Elasticsearch: 權威指南》,關於 蘋果8 的 IDF,會在 cate_name 和 content 中取最小值作為兩個字段的 IDF。(待驗證)
參考資料
- Elasticsearch: 權威指南:https://www.elastic.co/guide/cn/elasticsearch/guide/current/_best_fields.html
- Elasticsearch Reference:https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html