使用filters優化查詢
ElasticSearch支持多種不同類型的查詢方式,這一點大家應該都已熟知。但是在選擇哪個文檔應該匹配成功,哪個文檔應該呈現給用戶這一需求上,查詢並不是唯一的選擇。ElasticSearch 查詢DSL允許用戶使用的絕大多數查詢都會有各自的標識,這些查詢也以嵌套到如下的查詢類型中:
constant_score
filterd
custom_filters_score
那么問題來了,為什么要這么麻煩來使用filtering?在什么場景下可以只使用queries? 接下來就試着解決上面的問題。
過濾器(Filters)和緩存
首先,正如讀者所想,filters來做緩存是一個很不錯的選擇,ElasticSearch也提供了這種特殊的緩存,filter cache來存儲filters得到的結果集。此外,緩存filters不需要太多的內存(它只保留一種信息,即哪些文檔與filter相匹配),同時它可以由其它的查詢復用,極大地提升了查詢的性能。設想你正運行如下的查詢命令:
{
"query" : { "bool" : { "must" : [ { "term" : { "name" : "joe" } }, { "term" : { "year" : 1981 } } ] } } }
該命令會查詢到滿足如下條件的文檔:name
域值為joe
同時year
域值為1981
。這是一個很簡單的查詢,但是如果用於查詢足球運動員的相關信息,它可以查詢到所有符合指定人名及指定出生年份的運動員。
如果用上面命令的格式構建查詢,查詢對象會將所有的條件綁定到一起存儲到緩存中;因此如果我們查詢人名相同但是出生年份不同的運動員,ElasticSearch無法重用上面查詢命令中的任何信息。因此,我們來試着優化一下查詢。由於一千個人可能會有一千個人名,所以人名不太適合緩存起來;但是年份比較適合(一般year
域中不會有太多不同的值,對吧?)。因此我們引入一個不同的查詢命令,將一個簡單的query與一個filter結合起來。
{
"query" : { "filtered" : { "query" : { "term" : { "name" : "joe" } }, "filter" : { "term" : { "year" : 1981 } } } } }
我們使用了一個filtered類型的查詢對象,查詢對象將query元素和filter元素都包含進去了。第一次運行該查詢命令后,ElasticSearch就會把filter緩存起來,如果再有查詢用到了一樣的filter,就會直接用到緩存。就這樣,ElasticSearch不必多次加載同樣的信息。
並非所有的filters會被默認緩存起來
緩存很強大,但實際上ElasticSearch在默認情況下並不會緩存所有的filters。這是因為部分filters會用到域數據緩存(field data cache)。該緩存一般用於按域值排序和faceting操作的場景中。默認情況下,如下的filters不會被緩存:
- numeric_range
- script
- geo_bbox
- geo_distance
- geo_distance_range
- geo_polygon
- geo_shape
- and
- or
- not
盡管上面提到的最后三種filters不會用到域緩存,它們主要用於控制其它的filters,因此它不會被緩存,但是它們控制的filters在用到的時候都已經緩存好了。
更改ElasticSearch緩存的行為
ElasticSearch允許用戶通過使用_chache和_cache_key屬性自行開啟或關閉filters的緩存功能。回到前面的例子,假定我們將關鍵詞過濾器的結果緩存起來,並給緩存項的key取名為year_1981_cache
,則查詢命令如下:
{
"query" : { "filtered" : { "query" : { "term" : { "name" : "joe" } }, "filter" : { "term" : { "year" : 1981, "_cache_key" : "year_1981_cache" } } } } }
也可以使用如下的命令關閉該關鍵詞過濾器的緩存:
{
"query" : { "filtered" : { "query" : { "term" : { "name" : "joe" } }, "filter" : { "term" : { "year" : 1981, "_cache" : false } } } } }
為什么要這么麻煩地給緩存項的key取名
上面的問題換個說法就是,我有是否有必要如此麻煩地使用_cache_key屬性,ElasticSearch不能自己實現這個功能嗎?當然它可以自己實現,而且在必要的時候控制緩存,但是有時我們需要更多的控制權。比如,有些查詢復用的機會不多,我們希望定時清除這些查詢的緩存。如果不指定_cache_key,那就只能清除整個過濾器緩存(filter cache);反之,只需要執行如下的命令即可清除特定的緩存:
curl -XPOST 'localhost:9200/users/_cache/clear?filter_keys=year_1981_cache'
什么時候應該改變ElasticSearch 過濾器緩存的行為
當然,有的時候用戶應該更多去了解業務需求,而不是讓ElasticSearch來預測數據分布。比如,假設你想使用geo_distance 過濾器將查詢限制到有限的幾個地理位置,該過濾器在請多查詢請求中都使用着相同的參數值,即同一個腳本會在隨着過濾器一起多次使用。在這個場景中,為過濾器開啟緩存是值得的。任何時候都需要問自己這個問題“過濾器會多次重復使用嗎?”添加數據到緩存是個消耗機器資源的操作,用戶應避免不必要的資源浪費。
關鍵詞查找過濾器
緩存和標准的查詢並不是全部內容。隨着ElasticSearch 0.90版本的發布,我們得到了一個精巧的過濾器,它可以用來將多個從ElasticSearch中得到值作為query的參數(類似於SQL的IN操作)。
讓我們看一個簡單的例子。假定我們有在一個在線書店,存儲了用戶,即書店的顧客購買的書籍信息。books索引很簡單(存儲在books.json文件中):
{
"mappings" : { "book" : { "properties" : { "id" : { "type" : "string", "store" : "yes", "index" : "not_analyzed" }, "title" : { "type" : "string", "store" : "yes", "index" : "analyzed" } } } } }
上面的代碼中,沒有什么是非同尋常的;只有書籍的id和標題。 接下來,我們來看看clients.json文件,該文件中存儲着clients索引的mappings信息:
{
"mappings" : { "client" : { "properties" : { "id" : { "type" : "string", "store" : "yes", "index" : "not_analyzed" }, "name" : { "type" : "string", "store" : "yes", "index" : "analyzed" }, "books" : { "type" : "string", "store" : "yes", "index" : "not_analyzed" } } } } }
索引定義了id信息,名字,用戶購買書籍的id列表。此外,我們還需要一些樣例數據:
curl -XPUT 'localhost:9200/clients/client/1' -d '{
"id":"1", "name":"Joe Doe", "books":["1","3"]
}'
curl -XPUT 'localhost:9200/clients/client/2' -d '{
"id":"2", "name":"Jane Doe", "books":["3"]
}'
curl -XPUT 'localhost:9200/books/book/1' -d '{
"id":"1", "title":"Test book one"
}'
curl -XPUT 'localhost:9200/books/book/2' -d '{
"id":"2", "title":"Test book two"
}'
curl -XPUT 'localhost:9200/books/book/3' -d '{
"id":"3", "title":"Test book three"
}'
接下來想象需求如下,我們希望展示某個用戶購買的所有書籍,以id為1的user為例。當然,我們可以先執行一個請求 curl -XGET 'localhost:9200/clients/client/1'
得到當前顧客的購買記錄,然后把books域中的值取出來,執行第二個查詢:
curl -XGET 'localhost:9200/books/_search' -d '{
"query" : {
"ids" : {
"type" : "book",
"values" : [ "1", "3" ]
}
}
}'
這樣做太麻煩了,ElasticSearch 0.90版本新引入了 關鍵詞查詢過濾器(term lookup filter),該過濾器只需要一個查詢就可以將上面兩個查詢才能完成的事情搞定。使用該過濾器的查詢如下:
curl -XGET 'localhost:9200/books/_search' -d '{ "query" : { "filtered" : { "query" : { "match_all" : {} }, "filter" : { "terms" : { "id" : { "index" : "clients", "type" : "client", "id" : "1", "path" : "books" }, "_cache_key" : "terms_lookup_client_1_books" } } } } }'
請注意_cache_key
參數的值,可以看到其值為terms_lookup_client_1_books
,它里面包含了顧客id信息。請注意,如果給不同的查詢設置了相同的_cache_key
,那么結果就會出現不可預知的錯誤。這是因為ElasticSearch會基於指定的key來存儲查詢結果,然后在不同的查詢中復用。 接下來看看上述查詢的返回值:
{
...
"hits" : { "total" : 2, "max_score" : 1.0, "hits" : [ { "_index" : "books", "_type" : "book", "_id" : "1", "_score" : 1.0, "_source" : {"id":"1", "title":"Test book one"} }, { "_index" : "books", "_type" : "book", "_id" : "3", "_score" : 1.0, "_source" : {"id":"3", "title":"Test book three"} } ] } }
這正是我們希望看到的結果,太棒了!
term filter的工作原理
回顧我們發送到ElasticSearch的查詢命令。可以看到,它只是一個簡單的過濾查詢,包含一個全量查詢和一個terms 過濾器。只是該查詢命令中,terms 過濾器使用了一種不同的技巧——不是明確指定某些term的值,而是從其它的索引中動態加載。
可以看到,我們的過濾器基於id域,這是因為只需要id域就整合其它所有的屬性。接下來就需要關注id域中的新屬性:index,type,id,和path。idex屬性指明了加載terms的索引源(在本例中是clients索引)。type屬性告訴ElasticSearch我們的目標文檔類型(在本例中是client類型)。id屬性指明的我們在指定索引的指文檔類型中的目標文檔。最后,path屬性告訴ElasticSearch應該從哪個域中加載term,在本例中是clients索引的books域。 總結一下,ElasticSearch所做的工作就是從clients索引的client文檔類型中,id為1的文檔里加載books域中的term。這些取得的值將用於terms filter來過濾從books索引(命令執行的目的地是books索引)中查詢到的文檔,過濾條件是文檔id域(本例中terms filter名稱為id)的值在過濾器中存在。