1. 程式人生 > >ElasticSearch學習筆記十五 深入檢索

ElasticSearch學習筆記十五 深入檢索

查詢還是過濾

Elasticsearch 的檢索一般來講我們分為2種: 過濾(filtering context)和查詢(query context

過濾

當使用於 過濾情況 時,查詢被設定成一個 “不評分”或者“過濾”查詢。 即,這個查詢只是簡單的問一個問題:“這篇文件是否匹配?”。回答也是非常的簡單,yes 或者 no ,二者必居其一。

  • created 時間是否在 2013 與 2014 這個區間?
  • status 欄位是否包含 published 這個單詞?
  • lat_lon 欄位表示的位置是否在指定點的 10km 範圍內?

查詢

當使用於 查詢情況 時,查詢就變成了一個“評分”的查詢。 和不評分的查詢類似,也要去判斷這個文件是否匹配,同時它還需要判斷這個文件匹配的有 多好(匹配程度如何)。 此查詢的典型用法是用於查詢以下文件:

  • 查詢與 full text search 這個詞語最佳匹配的文件
  • 包含 run 這個詞,也能匹配runs 、 running 、 jog 或者 sprint
  • 包含 quick 、 brownfox 這幾個詞 — 詞之間離的越近,文件相關性越高
  • 標有 lucene 、 search 或者 java 標籤 — 標籤越多,相關性越高

一個評分查詢計算每一個文件與此查詢的 相關程度,同時將這個相關程度分配給表示相關性的欄位 _score,並且按照相關性對匹配到的文件進行排序。這種相關性的概念是非常適合全文搜尋的情況,因為全文搜尋幾乎沒有完全 ``正確’’ 的答案。

自 Elasticsearch 出現以來,查詢與過濾(queries and filters)就獨自成為 Elasticsearch 的元件。但從 Elasticsearch 2.0 開始,過濾(filters)已經從技術上被排除了,同時所有的查詢(queries)擁有變成不評分查詢的能力。

一般來講,我們用 filter 這個詞表示不評分、只過濾情況下的查詢。即過濾查詢。相似的,如果單獨地不加任何修飾詞地使用 query 這個詞,我們指的是評分查詢。

查詢和過濾效能差異

過濾查詢(Filtering queries)只是簡單的檢查包含或者排除,這就使得計算起來非常快。考慮到至少有一個過濾查詢(filtering query)的結果是 “稀少的”(很少匹配的文件),並且經常使用不評分查詢(non-scoring queries),結果會被快取到記憶體中以便快速讀取,所以有各種各樣的手段來優化查詢結果。

相反,評分查詢(scoring queries)不僅僅要找出匹配的文件,還要計算每個匹配文件的相關性,計算相關性使得它們比不評分查詢費力的多。同時,查詢結果並不快取。

多虧倒排索引(inverted index),一個簡單的評分查詢在匹配少量文件時可能與一個涵蓋百萬文件的filter表現的一樣好,甚至會更好。但是在一般情況下,一個filter 會比一個評分的query效能更優異,並且每次都表現的很穩定。

過濾(filtering)的目標是減少那些需要通過評分查詢(scoring queries)進行檢查的文件。

通常的規則是,使用查詢(query)語句來進行 全文 搜尋或者其它任何需要影響 相關性得分 的搜尋。除此以外的情況都使用過濾(filters)。

查詢API

match_all

match_all 查詢簡單的匹配所有文件。在沒有指定查詢方式時,它是預設的查詢:

"query": {
    "match_all": {}
  }

match

無論你在任何欄位上進行的是全文搜尋還是精確查詢,match 查詢是你可用的標準查詢。

如果你在一個全文欄位上使用 match 查詢,在執行查詢前,它將用正確的分析器去分析查詢字串:

{ "match": { "tweet": "About Search" }}

如果在一個精確值的欄位上使用它,例如數字、日期、布林或者一個 not_analyzed 字串欄位,那麼它將會精確匹配給定的值:

{ "match": { "age":    26           }}
{ "match": { "date":   "2014-09-01" }}
{ "match": { "public": true         }}
{ "match": { "tag":    "full_text"  }}

對於精確值的查詢,你可能需要使用 filter 語句來取代 query,因為 filter 將會被快取。

multi_match

multi_match 查詢可以在多個欄位上執行相同的 match 查詢:

{
    "multi_match": {
        "query":    "full text search",
        "fields":   [ "title", "body" ]
    }
}

range

range 查詢找出那些落在指定區間內的數字或者時間:

{
    "range": {
        "age": {
            "gte":  20,
            "lt":   30
        }
    }
}

被允許的操作符如下:

操作 說明
gt 大於
gte 大於等於
lt 小於
lte 小於等於

term

term 查詢被用於精確值匹配,這些精確值可能是數字、時間、布林或者那些 not_analyzed 的字串:

{ "term": { "age":    26           }}
{ "term": { "date":   "2014-09-01" }}
{ "term": { "public": true         }}
{ "term": { "tag":    "full_text"  }}

term 查詢對於輸入的文字不 分析 ,所以它將給定的值進行精確查詢。

terms

terms 查詢和 term 查詢一樣,但它允許你指定多值進行匹配。如果這個欄位包含了指定值中的任何一個值,那麼這個文件滿足條件:

{ "terms": { "tag": [ "search", "full_text", "nosql" ] }}

和 term 查詢一樣,terms 查詢對於輸入的文字不分析。它查詢那些精確匹配的值(包括在大小寫、重音、空格等方面的差異)。

exists missing

exists 查詢和 missing 查詢被用於查詢那些指定欄位中有值 (exists) 或無值 (missing) 的文件。這與SQL中的 IS_NULL (missing) 和 NOT IS_NULL (exists) 在本質上具有共性:

{
    "exists":   {
        "field":    "title"
    }
}

這些查詢經常用於某個欄位有值的情況和某個欄位缺值的情況。

深入分析

此時我們插入一條tweet資訊

PUT tweet/tweet/1
{
    "tweet":    "What is Elasticsearch?",
    "date":     "2014-09-14",
    "name":     "Mary Jones",
    "about":["es","elasticsearch"],
    "topic":"elasticsearch",
    "user_id":  1,
    "comments": [
      {
        "content":  "very good",
        "date":  "2014-09-14",
        "user_id":  1
      },
      {
        "content":  "good question",
        "date":  "2014-09-15",
        "user_id":  2
      }
    ]
}

elasticsearch自動建立的索引對映如下

{
  "tweet": {
    "mappings": {
      "tweet": {
        "properties": {
          "about": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "comments": {
            "properties": {
              "content": {
                "type": "text",
                "fields": {
                  "keyword": {
                    "type": "keyword",
                    "ignore_above": 256
                  }
                }
              },
              "date": {
                "type": "date"
              },
              "user_id": {
                "type": "long"
              }
            }
          },
          "date": {
            "type": "date"
          },
          "name": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "topic": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "tweet": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "user_id": {
            "type": "long"
          }
        }
      }
    }
  }
}

這裡可以看出現,在我們沒有為索引指明對映的時候,elasticsearch會自動建立對映有一下特點:

  • 字串都被索引為text 全文檢索型別並且又自動添加了keyword精確詞第2種類型。
   	 "about": {
           "type": "text",
           "fields": {
             "keyword": {
               "type": "keyword",
               "ignore_above": 256
             }
           }
         }
  • 資料自動識別為首元素型別
  • 物件陣列已經自動巢狀物件處理
	"comments": {
            "properties": {
              "content": {
                "type": "text",
                "fields": {
                  "keyword": {
                    "type": "keyword",
                    "ignore_above": 256
                  }
                }
              }

此時假如我們希望查詢一下Jones發的tweet,此時我們只關心名字包含Jones,所以此時很明顯我們需要match查詢。

GET tweet/tweet/_search
{
  "query": {
    "match": {
      "name": "Jones"
    }
  }
}

我們可以明顯拿到自己的結果:

{
  "took": 0,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 0.2876821,
    "hits": [
      {
        "_index": "tweet",
        "_type": "tweet",
        "_id": "1",
        "_score": 0.2876821,
        "_source": {
          "tweet": "What is Elasticsearch?",
          "date": "2014-09-14",
          "name": "Mary Jones",
          "about": [
            "es",
            "elasticsearch"
          ],
          "topic": "elasticsearch",
          "user_id": 1,
          "comments": [
            {
              "content": "very good",
              "date": "2014-09-14",
              "user_id": 1
            },
            {
              "content": "good question",
              "date": "2014-09-15",
              "user_id": 2
            }
          ]
        }
      }
    ]
  }
}

假設我們嘗試精確過濾呢

GET tweet/tweet/_search
{
  "query": {
    "bool": {
      "filter": {
        "term": {
          "name": "Jones"
        }
      }
    }
  }
}

結果什麼都沒有查詢出來

{
  "took": 0,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 0,
    "max_score": null,
    "hits": []
  }
}

假如我們精確匹配全部名稱呢

GET tweet/tweet/_search
{
  "query": {
    "bool": {
      "filter": {
        "term": {
          "name": "Mary Jones"
        }
      }
    }
  }
}

結果如下:

{
  "took": 0,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 0,
    "max_score": null,
    "hits": []
  }
}

很神奇,我們還是沒有精確匹配。仔細想想就會明白其中的端倪,這裡的問題在於我們的name是text域文字,elasticsearch預設會對它進行分析索引,而我們的term 查詢將給定的值進行精確查詢對於輸入的文字不 分析 ,所以它會和name分析後的結果不匹配。那麼這個問題應該怎麼解決呢?

我們前面說過的name還又一個keyword對映

GET tweet/tweet/_search
{
  "query": {
    "bool": {
      "filter": {
        "term": {
          "name.keyword": "Mary Jones"
        }
      }
    }
  }
}

結果如下:

{
  "took": 0,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 0,
    "hits": [
      {
        "_index": "tweet",
        "_type": "tweet",
        "_id": "1",
        "_score": 0,
        "_source": {
          "tweet": "What is Elasticsearch?",
          "date": "2014-09-14",
          "name": "Mary Jones",
          "about": [
            "es",
            "elasticsearch"
          ],
          "topic": "elasticsearch",
          "user_id": 1,
          "comments": [
            {
              "content": "very good",
              "date": "2014-09-14",
              "user_id": 1
            },
            {
              "content": "good question",
              "date": "2014-09-15",
              "user_id": 2
            }
          ]
        }
      }
    ]
  }
}

但是是不是隻有term精確匹配可以呢?我們知道如果在一個精確值的欄位上使用match,那麼它將會精確匹配給定的值:

GET tweet/tweet/_search
{
  "query": {
    "match": {
      "name.keyword": "Mary Jones"
    }
  }
}

結果如下:

{
  "took": 0,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 0.2876821,
    "hits": [
      {
        "_index": "tweet",
        "_type": "tweet",
        "_id": "1",
        "_score": 0.2876821,
        "_source": {
          "tweet": "What is Elasticsearch?",
          "date": "2014-09-14",
          "name": "Mary Jones",
          "about": [
            "es",
            "elasticsearch"
          ],
          "topic": "elasticsearch",
          "user_id": 1,
          "comments": [
            {
              "content": "very good",
              "date": "2014-09-14",
              "user_id": 1
            },
            {
              "content": "good question",
              "date": "2014-09-15",
              "user_id": 2
            }
          ]
        }
      }
    ]
  }
}

組合多查詢

現實的查詢需求從來都沒有那麼簡單;它們需要在多個欄位上查詢多種多樣的文字,並且根據一系列的標準來過濾。為了構建類似的高階查詢,你需要一種能夠將多查詢組合成單一查詢的查詢方法。

你可以用 bool 查詢來實現你的需求。這種查詢將多查詢組合在一起,成為使用者自己想要的布林查詢。它接收以下引數:

引數 說明
must 文件 必須 匹配這些條件才能被包含進來。
must_not 文件 必須不 匹配這些條件才能被包含進來。
should 如果滿足這些語句中的任意語句,將增加 _score ,否則,無任何影響。它們主要用於修正每個文件的相關性得分。
filter 必須 匹配,但它以不評分、過濾模式來進行。這些語句對評分沒有貢獻,只是根據過濾標準來排除或包含文件。

由於這是我們看到的第一個包含多個查詢的查詢,所以有必要討論一下相關性得分是如何組合的。每一個子查詢都獨自地計算文件的相關性得分。一旦他們的得分被計算出來, bool 查詢就將這些得分進行合併並且返回一個代表整個布林操作的得分。

下面的查詢用於查詢 title 欄位匹配 how to make millions 並且不被標識為 spam 的文件。那些被標識為 starred 或在2014之後的文件,將比另外那些文件擁有更高的排名。如果 兩者 都滿足,那麼它排名將更高:

{
    "bool": {
        "must":     { "match": { "title": "how to make millions" }},
        "must_not": { "match": { "tag":   "spam" }},
        "should": [
            { "match": { "tag": "starred" }},
            { "range": { "date": { "gte": "2014-01-01" }}}
        ]
    }
}

如果沒有 must 語句,那麼至少需要能夠匹配其中的一條 should 語句。但,如果存在至少一條 must 語句,則對 should 語句的匹配沒有要求。

增加帶過濾器(filtering)的查詢 如果我們不想因為文件的時間而影響得分,可以用 filter 語句來重寫前面的例子:

{
    "bool": {
        "must":     { "match": { "title": "how to make millions" }},
        "must_not": { "match": { "tag":   "spam" }},
        "should": [
            { "match": { "tag": "starred" }}
        ],
        "filter": {
          "range": { "date": { "gte": "2014-01-01" }}
        }
    }
}

通過將 range 查詢移到 filter 語句中,我們將它轉成不評分的查詢,將不再影響文件的相關性排名。由於它現在是一個不評分的查詢,可以使用各種對 filter 查詢有效的優化手段來提升效能。

所有查詢都可以借鑑這種方式。將查詢移到 bool 查詢的 filter 語句中,這樣它就自動的轉成一個不評分的 filter 了

如果你需要通過多個不同的標準來過濾你的文件,bool 查詢本身也可以被用做不評分的查詢。簡單地將它放置到 filter 語句中並在內部構建布林邏輯:

{
    "bool": {
        "must":     { "match": { "title": "how to make millions" }},
        "must_not": { "match": { "tag":   "spam" }},
        "should": [
            { "match": { "tag": "starred" }}
        ],
        "filter": {
          "bool": {
              "must": [
                  { "range": { "date": { "gte": "2014-01-01" }}},
                  { "range": { "price": { "lte": 29.99 }}}
              ],
              "must_not": [
                  { "term": { "category": "ebooks" }}
              ]
          }
        }
    }
}