ElasticSearch 2 (13) - 深入搜索系列之結構化搜索
摘要
結構化查詢指的是查詢那些具有內在結構的數據,比如日期、時間、數字都是結構化的。它們都有精確的格式,我們可以對這些數據進行邏輯操作,比較常見的操作包括比較時間區間,或者獲取兩個數字間的較大值。
文本也可以是結構化的。比如彩筆可以有紅、綠、藍顏色集合,一個博客可以有關鍵字標簽 分布式 和 搜索 。 電商網站上的商品都有UPC(Universal Product Codes)或者其他需要嚴格結構化格式的唯一標識。
在結構化查詢中,我們得到的結果通常是 是 或 非 ,要么是處於集合中的,要么是集合之外的。結構化查詢通常不需要操心文件之間的相關性或者准確的相關程度,對於結果來說,要么 包含 ,要么 排除 。
這在邏輯上是說得通的,對於一個數字來說,我們不能說它比其他數字更適合在某一集合中。確切的結果只能是:要么在范圍中 ,要么反之。同理,對於一個結構化文本,一個值要么相等,要么不等。在結構化查詢中,沒有更相似 這種概念
版本
elasticsearch版本: elasticsearch-2.x
內容
精確查詢
當進行精確查詢時,過濾器filter是十分重要的,因為它們效率非常高,過濾器不計算相關性(直接跳過了整個記分階段)而且很容易進行緩存。我們會在本片文章的后面介紹緩存為filter帶來的性能優勢。現在需要記住的只是:盡可能多的使用filter。
過濾數字
我們首先看 term filter,它最常用,可以用來處理數字,布爾值,日期和文本。
例如我們有一些產品:
POST /my_store/products/_bulk
{ "index": { "_id": 1 }}
{ "price" : 10, "productID" : "XHDK-A-1293-#fJ3" }
{ "index": { "_id": 2 }}
{ "price" : 20, "productID" : "KDKE-B-9947-#kL5" }
{ "index": { "_id": 3 }}
{ "price" : 30, "productID" : "JODL-X-1937-#pV7" }
{ "index": { "_id": 4 }}
{ "price" : 30, "productID" : "QQPX-R-3956-#aD8" }
我們想要做的是要查詢具有某個價格的所有產品,如果對於SQL熟悉,那么它的表達式是:
SELECT document
FROM products
WHERE price = 20
在ElasticSearch查詢DSL里,我們使用 term 達到相同的目的:
{
"term" : {
"price" : 20
}
}
但是在ElasticSearch里,term 不能單獨使用,search API期望的是一個 query 而不是 filter,所以,我們需要把 term 放在一個filter query里進行使用:
GET /my_store/products/_search
{
"query" : {
"filtered" : { #1
"query" : {
"match_all" : {} #2
},
"filter" : {
"term" : { #3
"price" : 20
}
}
}
}
}
- #1 filtered 查詢同時接受一個 query 和 filter
- #2 match_all 會返回所有匹配的文件,這是個默認行為
- #3 term 過濾我們之前說到的,需要注意的是這里 term塊 是處於 filter 之內的
執行結果正如我們期望一樣,它只會返回文檔2,這里我們稱為命中hit。
"hits" : [
{
"_index" : "my_store",
"_type" : "products",
"_id" : "2",
"_score" : 1.0, #1
"_source" : {
"price" : 20,
"productID" : "KDKE-B-9947-#kL5"
}
}
]
- \1 之前我們說到filter不會進行記分或相關性計算,這里的分數來自於我們查詢時使用的關鍵字 match_all ,它會同等對待所有的文件,並對所有的結果都給以1的記分。
過濾文本
term 同樣可以用來過濾文本,如果我們想要查詢某個具體UPC id的產品,SQL語句會是下面這樣:
SELECT product
FROM products
WHERE productID = "XHDK-A-1293-#fJ3"
轉換成DSL,同樣使用 term 來查詢:
GET /my_store/products/_search
{
"query" : {
"filtered" : {
"filter" : {
"term" : {
"productID" : "XHDK-A-1293-#fJ3"
}
}
}
}
}
但這里有個小問題,我們沒有如預期得到想要的結果!為什么呢?問題並不出在 term 查詢上,問題出在數據索引的方式。如果使用 analyze API(Test Analyzers),我們可以看到這里的UPC碼以及被拆分成多個小的token:
GET /my_store/_analyze?field=productID
XHDK-A-1293-#fJ3
結果
{
"tokens" : [ {
"token" : "xhdk",
"start_offset" : 0,
"end_offset" : 4,
"type" : "<ALPHANUM>",
"position" : 1
}, {
"token" : "a",
"start_offset" : 5,
"end_offset" : 6,
"type" : "<ALPHANUM>",
"position" : 2
}, {
"token" : "1293",
"start_offset" : 7,
"end_offset" : 11,
"type" : "<NUM>",
"position" : 3
}, {
"token" : "fj3",
"start_offset" : 13,
"end_offset" : 16,
"type" : "<ALPHANUM>",
"position" : 4
} ]
}
這里有些幾點需要注意的:
- 這個UPC我們有4個不同的token而不是1個
- 所有token的字母都變成了小寫
- 我們都掉了短橫線(-)和哈希符(#)
所以,當我們用 term 去過濾值 XHDK-A-1293-#fJ3 的時候,找不到任何文件,因為這個token不在我們的反向索引(inverted index)之中,正如上面呈現的,索引里面有4個token。
顯然,這種對於id碼或其他任何精確值的處理方式不是我們想要的。
為了避免這種問題,我們需要告訴ElasticSearch這個字段具有精確值,需要被設置成 not_analyzed 。 我們可以在定制化字段mapping中找到相關內容。為了修正這個問題,我們需要首先刪除老的index,然后再創建一個新的
DELETE /my_store #1
PUT /my_store #2
{
"mappings" : {
"products" : {
"properties" : {
"productID" : {
"type" : "string",
"index" : "not_analyzed" #3
}
}
}
}
}
- #1 刪除索引是必須的,因為我們不能更新已存在的mapping(Immutable)。
- #2 在索引被刪除后,我們可以創建自定義的mapping。
- #3 我們在這里告訴ElasticSearch,我們不想對 productID 做任何分析。
然后我們就可以對文件重索引了:
POST /my_store/products/_bulk
{ "index": { "_id": 1 }}
{ "price" : 10, "productID" : "XHDK-A-1293-#fJ3" }
{ "index": { "_id": 2 }}
{ "price" : 20, "productID" : "KDKE-B-9947-#kL5" }
{ "index": { "_id": 3 }}
{ "price" : 30, "productID" : "JODL-X-1937-#pV7" }
{ "index": { "_id": 4 }}
{ "price" : 30, "productID" : "QQPX-R-3956-#aD8" }
此時,如果我們再次搜索就會得到我們想要的結果。
過濾器的內部操作
在內部,當ElasticSearch運行時,會執行多個操作:
-
找到匹配的文件
term 過濾器在反向索引表中查找 XHDK-A-1293-#fJ3 然后返回有這個 term 的所有文件,這個例子中,只有1個文件滿足。
-
創建位集合(bitset)
filter會創建一個包含有0和1的 bitset ,這個數組描述了哪個文檔有這個 term 。對於匹配的文件標志位為1,在我們的這個例子中,位集合的值為 [1,0,0,0]。
-
緩存位集合
最后,bitset會存在於內存之中,因為我們可以用這個值來直接跳過步驟1和2,這使得filter處理更快,性能更好。
當執行 filtered 查詢時,filter 在 query 之前執行,所以在filter產生的bitset會傳給 query,query 會依據bitset的內容,直接排除掉已被filter過濾掉的文件,這是提高處理性能的一種方式,更少的文檔意味着更小的相應時間。
組合過濾器
上面的兩個例子都是單個filter的使用方式,在實際中,我們很多情況下會同時會對多個值或字段使用filter。例如,在ElasticSearch中,如何標識下面這個SQL?
SELECT product
FROM products
WHERE (price = 20 OR productID = "XHDK-A-1293-#fJ3")
AND (price != 30)
在這種情況下,我們需要 bool filter。這是一個復合過濾器(compound filter)可以接收多個參數,然后將他們組合成布爾組合(Boolean combination)。
布爾過濾器(Bool Filter)
bool filter包括三部分:
{
"bool" : {
"must" : [],
"should" : [],
"must_not" : [],
}
}
-
must
所有的語句必須匹配,與 AND 等價。
-
must_not
所有的語句都不能匹配,與 NOT 等價。
-
should
至少有一個語句匹配,與 OR 等價。
需要注意的是:bool filter的每個部分都是可選的(例如,我們可以只有一個 must 語句),而且每個部分內部可以只有一個filter,或者一組(array)filter。
用ElasticSearch的DSL實現我們上面SQL里的查詢:
GET /my_store/products/_search
{
"query" : {
"filtered" : { #1
"filter" : {
"bool" : {
"should" : [
{ "term" : {"price" : 20}}, #2
{ "term" : {"productID" : "XHDK-A-1293-#fJ3"}} #3
],
"must_not" : {
"term" : {"price" : 30} #4
}
}
}
}
}
}
- #1 注意,我們仍然需要一個 filtered 查詢將所有的東西包裹起來。
- #2 這兩個在 should 條件塊里面的 term 是 bool filter的子過濾器,
- #3 should 條件塊里面,其一需要滿足
- #4 如果一個產品的價格是 30,那么它會自動被排除,因為它處於 must_not 條件塊里面。
我們搜索的結果返回了2個hits,兩個文件各滿足其中一個條件:
"hits" : [
{
"_id" : "1",
"_score" : 1.0,
"_source" : {
"price" : 10,
"productID" : "XHDK-A-1293-#fJ3"
}
},
{
"_id" : "2",
"_score" : 1.0,
"_source" : {
"price" : 20,
"productID" : "KDKE-B-9947-#kL5"
}
}
]
嵌套布爾過濾器(Nesting Boolean Filters)
盡管 bool 是一個復合的過濾器,可以接受多個子過濾器,需要注意的是 bool 過濾器本身仍然是一個過濾器(filter)。這意味着我們可以將一個bool過濾器置於另外一個bool過濾器內部,這為我們提供了復雜布爾邏輯的處理能力:
對於一個SQL語句:
SELECT document
FROM products
WHERE productID = "KDKE-B-9947-#kL5"
OR ( productID = "JODL-X-1937-#pV7"
AND price = 30 )
我們將其轉換成一個嵌套的 bool 過濾器:
GET /my_store/products/_search
{
"query" : {
"filtered" : {
"filter" : {
"bool" : {
"should" : [
{ "term" : {"productID" : "KDKE-B-9947-#kL5"}}, #1
{ "bool" : { #2
"must" : [
{ "term" : {"productID" : "JODL-X-1937-#pV7"}}, #3
{ "term" : {"price" : 30}} #4
]
}}
]
}
}
}
}
}
- #1 因為 term 和 bool 過濾器是兄弟關系,他們都處於 should 過濾器內部,
- #2 命中返回的文件中,需要至少滿足其中一個filter的條件。
- #3 這兩個 term 兄弟關系的條件同時處於 must 語句之中,所以
- #4 命中返回的文件,必須同時滿足這兩個條件。
得到的結果有兩個文件,他們各滿足 should 中的一個條件:
"hits" : [
{
"_id" : "2",
"_score" : 1.0,
"_source" : {
"price" : 20,
"productID" : "KDKE-B-9947-#kL5" #1
}
},
{
"_id" : "3",
"_score" : 1.0,
"_source" : {
"price" : 30, #2
"productID" : "JODL-X-1937-#pV7" #3
}
}
]
- #1 這個 productID 匹配 bool 過濾器 should 里的第一個 term
- #2 這兩個字段匹配 bool 過濾器 should 里嵌套的 bool 過濾器
這只是一個簡單的例子,但足以呈現 Boolean filter 可以用來構建復雜邏輯條件的能力。
多值精確查詢
term 過濾器對於查找單個值非常有用,但是在很多時候我們想要進行多值查詢。如果我們想要找到價格為 $20 或 $30 的產品文件該怎么辦呢?
不需要使用多個 term 過濾器,我們只需要為 term 加上 s 告訴ElasticSearch就行,terms 只是 term 過濾器的復數形式(以英語單詞做比)。
我們要做的只是要將 price 的值改為數組:
{
"terms" : {
"price" : [20, 30]
}
}
完整的形式和 term 過濾器一樣,我們只需要將其置入 filtered 查詢塊中:
GET /my_store/products/_search
{
"query" : {
"filtered" : {
"filter" : {
"terms" : {
"price" : [20, 30]
}
}
}
}
}
運行結果返回第二、三、四個文檔
"hits" : [
{
"_id" : "2",
"_score" : 1.0,
"_source" : {
"price" : 20,
"productID" : "KDKE-B-9947-#kL5"
}
},
{
"_id" : "3",
"_score" : 1.0,
"_source" : {
"price" : 30,
"productID" : "JODL-X-1937-#pV7"
}
},
{
"_id": "4",
"_score": 1.0,
"_source": {
"price": 30,
"productID": "QQPX-R-3956-#aD8"
}
}
]
包含,但不是相等
需要了解的是 term 和 terms 是包含操作,而非等值判斷,如何理解這句話呢?
如果我們有一個term過濾器
{ "term" : { "tags" : "search" } }
它會與以下兩個文件匹配:
{ "tags" : ["search"] }
{ "tags" : ["search", "open_source"] } #1
- #1 盡管第二個文件包含除 search 之外的其他詞,它也會被匹配到。
回想 term 過濾器是如何工作的?ElasticSearch會在反向索引表中查找相應的term,然后創建一個bitset。在我們的例子中,反向索引表如下:
-------------------------------------------
Token | DocIDs
-------------------------------------------
open_source | 2
-------------------------------------------
search | 1,2
-------------------------------------------
這里 term 過濾器直接在反向索引表中找到 search 相關的文檔ID,這里即為文件1、文件2,所以兩個文件都會作為結果返回。
注意:
由於反向索引表自身的特性,整個字段是否相等比較難以計算,如果確定一個文件只包含我們想要查找的詞呢?首先我們需要在反向索引表中找到相關的記錄,然后再掃描記錄,看他們是否包含其他的詞,可以想象這樣做的代價是非常高的。正因如此,term和terms是must contain 操作,而非 must equal。
Equals Exactly
如果一定期望得到我們上面說的那種行為 must equal,最好的方式是添加另一個字段,這個字段用來存儲比較字段詞個數,同樣以上面提到的兩個文件為例:
{ "tags" : ["search"], "tag_count" : 1 }
{ "tags" : ["search", "open_source"], "tag_count" : 2 }
我們增加了tag_count以滿足我們的要求,這個我們可以通過 bool 來確保查詢滿足我們的要求:
GET /my_index/my_type/_search
{
"query": {
"filtered" : {
"filter" : {
"bool" : {
"must" : [
{ "term" : { "tags" : "search" } },
{ "term" : { "tag_count" : 1 } }
]
}
}
}
}
}
范圍查詢
到目前為止,我們只講到了數字的精確查詢。在實際中,按照數字的范圍進行查找也非常普遍,例如,我們想要找到價格大於 $20 而且小於 $40 的產品。
在SQL語句中,這句話可以表示成:
SELECT document
FROM products
WHERE price BETWEEN 20 AND 40
在ElasticSearch中,我們對應有:
"range" : {
"price" : {
"gt" : 20,
"lt" : 40
}
}
range 過濾器同時提供包含和排除兩種范圍表達式,可以組合使用一下選項:
- gt: > greater than
- lt: < less than
- gte: >= greater than or equal to
- lte: <= less than or equal to
這里有一個完整的例子
GET /my_store/products/_search
{
"query" : {
"filtered" : {
"filter" : {
"range" : {
"price" : {
"gte" : 20,
"lt" : 40
}
}
}
}
}
}
如果需要一邊無界(例如>20),將lt部分刪除即可:
"range" : {
"price" : {
"gt" : 20
}
}
時間范圍
range同樣可以應用到時間字段上:
"range" : {
"timestamp" : {
"gt" : "2014-01-01 00:00:00",
"lt" : "2014-01-07 00:00:00"
}
}
當使用range處理時間字段時,range 過濾器支持時間計算(date math)操作,例如,我們可以查找時間戳在過去一小時內的所有文件:
"range" : {
"timestamp" : {
"gt" : "now-1h"
}
}
這個過濾器會時刻查找過去一個小時內的所有文件,這樣我們也實現了通過移動的時間窗過濾文件的功能。
時間計算還可以指定某一具體時間,只要在某一時間后面加上一個pipe (||)就能實現
"range" : {
"timestamp" : {
"gt" : "2014-01-01 00:00:00",
"lt" : "2014-01-01 00:00:00||+1M"
}
}
上面所要查找的是2014年1月1日加上1月的時間。
Date本身是日歷相關的,所以它自己知道每個月具體的日期,也知道一年有多少天(閏年),具體的內容可以在時間格式相關的文檔中找到。
字符串的范圍
range同樣可以應用到字符串字段,字符串范圍可以按照 lexicographically 來,也可以根據alphabetically來,例如下面一串字符串是根據lexicographically來排序的:
5, 50, 6, B, C, a, ab, abb, abc, b
在反向索引表中的詞就是根據lexicographically的順序來排列的,這也是為什么字符串可以使用這個順序來確定范圍。
查找自 a 開始,以 b (不包括)結束的所有詞:
"range" : {
"title" : {
"gte" : "a",
"lt" : "b"
}
}
注意Cardinality:
數字和日期的索引方式使他們可以高效的進行range查詢,但是對於字符串來說,ElasticSearch只是簡單的比較每個反向索引表中的每個詞,看他們是否處於范圍之中,但是這比時間和數字的范圍查找要慢許多。
字符串范圍查找在 low cardinality (即具有少數唯一值)的時候可以正常使用,但是唯一值越多,對於字符串的范圍查詢會越慢。
處理Null
回想我們之前的一個例子,有字段名為 tags 的一組文件,這個字段有多個值,一個文件可能有一個tag(標簽),多個tag,也有可能沒有tag,如果一個字段沒有任何值,那么它在反向索引中是如何存儲的呢?
這是個具有欺騙性的問題,因為答案是,什么都不存。讓我們回頭看看之前那個反向索引表:
-------------------------------------------
Token | DocIDs
-------------------------------------------
open_source | 2
-------------------------------------------
search | 1,2
-------------------------------------------
那么如何存儲一個數據結構中不存在的字段呢?這樣可似乎我們做不到,一個反向索引表只是一個簡單的token以及包含它的文件列表,如果一個字段不存在,那么它也不會有任何token,也就是說它不會在反向索引表中存在。
這就意味着,null,[](空數組)和 [null] 是等價的。它們都不在反向索引表中。
但是世界並不簡單,有很多情況字段沒有數據,或者有顯式的 null 或者空數組。為了解決這個問題,ElasticSearch提供了一些工具。
存在過濾器(exists Filter)
第一個武器是 exists 過濾器,讓我們以下面這些文檔為例:
POST /my_index/posts/_bulk
{ "index": { "_id": "1" }}
{ "tags" : ["search"] } #1
{ "index": { "_id": "2" }}
{ "tags" : ["search", "open_source"] } #2
{ "index": { "_id": "3" }}
{ "other_field" : "some data" } #3
{ "index": { "_id": "4" }}
{ "tags" : null } #4
{ "index": { "_id": "5" }}
{ "tags" : ["search", null] } #5
- #1 tags字段有1個值
- #2 tags字段有2個值
- #3 The tags field is missing altogether.
- #4 The tags field is set to null.
- #5 The tags field has one value and a null.
上面的文件集合對應的反向索引表是這樣:
-------------------------------------------
Token | DocIDs
-------------------------------------------
open_source | 2
-------------------------------------------
search | 1,2,5
-------------------------------------------
我們的目的是找到那些設置過tag的文件,並不關心tag具體是什么,只要它存在於文檔中即可,在SQL里,我們會使用 IS NOT NULL 進行查詢。
SELECT tags
FROM posts
WHERE tags IS NOT NULL
在ElasticSearch中,我們使用 exists 過濾器:
GET /my_index/posts/_search
{
"query" : {
"filtered" : {
"filter" : {
"exists" : { "field" : "tags" }
}
}
}
}
這個查詢返回3個文件
"hits" : [
{
"_id" : "1",
"_score" : 1.0,
"_source" : { "tags" : ["search"] }
},
{
"_id" : "5",
"_score" : 1.0,
"_source" : { "tags" : ["search", null] } #1
},
{
"_id" : "2",
"_score" : 1.0,
"_source" : { "tags" : ["search", "open source"] }
}
]
- #1 盡管文件5有null,但它也會被返回。字段因為有真實值而存在,null對過濾不會產生任何影響。
結果顯而易見,只要含有字段tags文件都會返回,只有兩個文件3、4被排除在外。
缺失過濾器(missing Filter)
missing 過濾器本質上與 exists 相反,它返回某個字段沒有值的文件,如果用類似SQL表示
SELECT tags
FROM posts
WHERE tags IS NULL
我們將前面例子里面的 exists 換成 missing
GET /my_index/posts/_search
{
"query" : {
"filtered" : {
"filter": {
"missing" : { "field" : "tags" }
}
}
}
}
按照我們期望的那樣,3、4兩個文件會返回
"hits" : [
{
"_id" : "3",
"_score" : 1.0,
"_source" : { "other_field" : "some data" }
},
{
"_id" : "4",
"_score" : 1.0,
"_source" : { "tags" : null }
}
]
當null是null
有時候我們需要區分一個字段是沒有值,還是一個字段被顯式的設置成了null。我們看到之前的系統默認行為是無法做到的;數據丟失了。不過幸運的是,我們可以選擇將顯試的 null 替換成一個我們定義的占位符。
同樣,在字符串,數字,布爾值或時間為 null 的時候,我們可以為之設置 null_value,對於沒有任何值的字段還是會被排除在反向索引表之外。
當我們選擇合適的 null_value 的時候,我們需要保證以下幾點:
- 它會匹配字段類型,我們不能為一個時間字段設置一個字符串類型的 null_value。
- 它必須與一般平常的值不一樣,這樣可以避免把真實值當成 null 的情況。
對象上的存在或缺失(exists/missing on Objects)
exists 和 missing 除了過濾核心類型外,還可以過濾一個對象的內部字段。下面這個文件:
{
"name" : {
"first" : "John",
"last" : "Smith"
}
}
我們可以直接檢查 name.first 和 name.last 的存在性,也可以只檢查 name 的存在性,正如在類型與映射中說的,上面這個對象的結構在內部會扁平化存儲,類似下面這樣:
{
"name.first" : "John",
"name.last" : "Smith"
}
那我們如何去用 exists 和 missing 過濾 name 字段呢?它並不在反向索引表中真實存在,
原因是當我們執行下面這個過濾的時候:
{
"exists" : { "field" : "name" }
}
實際上執行的是:
{
"bool": {
"should": [
{ "exists": { "field": { "name.first" }}},
{ "exists": { "field": { "name.last" }}}
]
}
}
這也就意味着,如果 first 和 last 都是空的情況下,name 的命名空間也不存在
關於緩存
在前面 過濾器的內部操作 中我們以及簡單介紹過濾器是如何計算的。他們的內部實際上是用一個bitset記錄與過濾器匹配的文件。ElasticSearch把這些內容緩存起來,以備將來使用。一旦緩存成功,如果重復使用相同的過濾器,這些bitset可以被復用,而不需要重新計算整個過濾器。
這些bitset緩存是非常智能的,他們可以做到增量更新,當我們索引新文件時,只需要將新文檔的計算結果加入到現有的bitset中,而不是對整個緩存一遍又一遍的重新計算。過濾器是實時的,我們不需要擔心緩存失效的問題。
獨立的過濾器緩存
每個過濾器是獨立計算並獨立緩存的,與他們具體的使用場景無關,如果兩個完全不同的查詢使用了相同的過濾器,相同的緩存bitset會被復用。同樣,如果一個查詢在多個地方使用到了相同的過濾器,bitset只會計算一次然后被重復使用。
讓我們看看下面這個例子,它查詢了需要滿足以下條件的email:
- 在收件箱中而且沒有被讀過
- 不在收件箱中但是被標注了重要
示例:
"bool": {
"should": [
{ "bool": {
"must": [
{ "term": { "folder": "inbox" }}, #1
{ "term": { "read": false }}
]
}},
{ "bool": {
"must_not": {
"term": { "folder": "inbox" } #2
},
"must": {
"term": { "important": true }
}
}}
]
}
- #1 #2兩個過濾器是相同的,所以也會使用同一bitset。
盡管一個inbox語句是 must,另一個是 must_not ,但是他們兩個是一樣的,這意味着第一個語句執行之后,這個過濾器的bitset會被緩存起來,供第二個使用。當這個查詢再次執行時,這個過濾器已經被緩存,所以兩個語句都會使用已緩存的bitset。
這點與DSL查詢結合得很好。它可以被移動到任何地方,也可以在同一查詢中的多個位置反復使用。這不僅僅能方便開發者,而且對性能有直接的好處。
緩存控制
多數葉子過濾器(leaf filters)是被緩存的。葉子過濾器是指那些直接處理字段的term過濾器,但是不會緩存復合過濾器,如bool過濾器。
注意:
葉子過濾器會要訪問磁盤上的反向索引表,所以我們有理由將他們緩存起來,但是組合過濾器運用快速的位邏輯將內部語句的bitset合並起來,所以即使每次計算效率也很高。
對於某些葉子過濾器,默認狀態下不會緩存,因為緩存它們沒有任何意義,比如:
-
腳本過濾(Script filters)
因為對於ElasticSearch來說腳本的含義是含糊的。
-
地理位置過濾(GEO filters)
因為地理位置的信息通常是和用戶相關的,所以每次過濾的結果都會不太一樣,對它做緩存意義不大。
-
日期范圍(Date ranges)
時間范圍用了 now 的。每次過濾的時候 now 都會返回一個最新的時間,所以舊的過濾器不會被復用,所以也不需要緩存。但是,當如果我們將 now 與rounding一起使用表示最近的一天時(now/d),它也會緩存。
有時默認的緩存策略並不正確。可能我們需要反復使用一個非常復雜的bool查詢,或者我們對時間字段有一個過濾器但永遠不會復用。默認的緩存策略可以幾乎在所有filter上進行覆蓋重寫,只要設置標志位 _cache 就行:
{
"range" : {
"timestamp" : {
"gt" : "2014-01-02 16:15:14" #1
},
"_cache": false #2
}
}
- #1 我們通常情況下不會再次使用這個時間戳
- #2 關閉這個過濾器的緩存功能
過濾順序
在 bool 過濾器中,過濾器的順序對性能是非常重要的,更具體的過濾器需要放在次具體的過濾器前面,這樣可以幫更早的排除更多的文件。
如果語句A可以匹配10,000,000個文件,語句B只能匹配100個,那么語句B需要放在語句A的前面。
緩存的過濾器非常快,他們需要放在不能緩存的過濾器前,如果我們對1小時內的數據非常感興趣:
GET /logs/2014-01/_search
{
"query" : {
"filtered" : {
"filter" : {
"range" : {
"timestamp" : {
"gt" : "now-1h"
}
}
}
}
}
}
因為使用了now,ElasticSearch不會緩存這個過濾器,這意味着我們每次查詢時都需要檢查一個月的日志數據。
我們可以將這個查詢與一個緩存過濾器結合,讓它變得更高效,我們可以通過增加昨天凌晨的時間點,將大量日志排除:
"bool": {
"must": [
{ "range" : {
"timestamp" : {
"gt" : "now-1h/d" #1
}
}},
{ "range" : {
"timestamp" : {
"gt" : "now-1h" #2
}
}}
]
}
- #1 這個過濾器會被緩存,因為它用到了now字段,並將其截斷到凌晨
- #2 這個過濾器不會被緩存,因為它沒有用到now時間截取
now-1h/d 這個句子將時間置為凌晨,把今日之前所有的文件都排除掉了,這意味着bitset每天只會被執行一次,這次發生在 昨日凌晨(midnight-last-night) 這個時間發生變化的時候。由於第一個過濾器可以幫我們過濾掉之前的大量文件,第二個過濾器只會從剩下的文件中過濾出最近一小時的文件。
語句的順序非常重要,這個方法只在 since-midnight 語句置於 last-hour 之前有效。如果順序相反,那么 last-hour 語句就需要過濾整月的文件,而非當天的文件。