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 、 brown
和fox
這幾個詞 — 詞之間離的越近,文件相關性越高 - 標有
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" }}
]
}
}
}
}