ElasticSearch 2 (13) - 深刻搜索系列之結構化搜索

ElasticSearch 2 (13) - 深刻搜索系列之結構化搜索

摘要

結構化查詢指的是查詢那些具備內在結構的數據,好比日期、時間、數字都是結構化的。它們都有精確的格式,咱們能夠對這些數據進行邏輯操做,比較常見的操做包括比較時間區間,或者獲取兩個數字間的較大值。html

文本也能夠是結構化的。好比彩筆能夠有紅、綠、藍顏色集合,一個博客能夠有關鍵字標籤 分佈式搜索 。 電商網站上的商品都有UPC(Universal Product Codes)或者其餘須要嚴格結構化格式的惟一標識。數組

在結構化查詢中,咱們獲得的結果一般是 ,要麼是處於集合中的,要麼是集合以外的。結構化查詢一般不須要操心文件之間的相關性或者準確的相關程度,對於結果來講,要麼 包含 ,要麼 排除緩存

這在邏輯上是說得通的,對於一個數字來講,咱們不能說它比其餘數字更適合在某一集合中。確切的結果只能是:要麼在範圍中 ,要麼反之。同理,對於一個結構化文本,一個值要麼相等,要麼不等。在結構化查詢中,沒有更類似 這種概念數據結構

版本

elasticsearch版本: elasticsearch-2.xapp

內容

精確查詢

當進行精確查詢時,過濾器filter是十分重要的,由於它們效率很是高,過濾器不計算相關性(直接跳過了整個記分階段)並且很容易進行緩存。咱們會在本片文章的後面介紹緩存爲filter帶來的性能優點。如今須要記住的只是:儘量多的使用filter。less

過濾數字

咱們首先看 term filter,它最經常使用,能夠用來處理數字,布爾值,日期和文本。elasticsearch

例如咱們有一些產品:分佈式

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熟悉,那麼它的表達式是:ide

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 查詢同時接受一個 queryfilter
  • #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運行時,會執行多個操做:

  1. 找到匹配的文件

    term 過濾器在反向索引表中查找 XHDK-A-1293-#fJ3 而後返回有這個 term 的全部文件,這個例子中,只有1個文件知足。

  2. 建立位集合(bitset

    filter會建立一個包含有0和1的 bitset ,這個數組描述了哪一個文檔有這個 term 。對於匹配的文件標誌位爲1,在咱們的這個例子中,位集合的值爲 [1,0,0,0]。

  3. 緩存位集合

    最後,bitset會存在於內存之中,由於咱們能夠用這個值來直接跳過步驟1和2,這使得filter處理更快,性能更好。

當執行 filtered 查詢時,filterquery 以前執行,因此在filter產生的bitset會傳給 queryquery 會依據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 條件塊裏面的 termbool 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 由於 termbool 過濾器是兄弟關係,他們都處於 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]
                }
            }
        }
    }
}

運行結果返回第2、3、四個文檔

"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"
        }
     }
]

包含,但不是相等

須要瞭解的是 termterms 是包含操做,而非等值判斷,如何理解這句話呢?

若是咱們有一個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,因此兩個文件都會做爲結果返回。

注意:
因爲反向索引表自身的特性,整個字段是否相等比較難以計算,若是肯定一個文件包含咱們想要查找的詞呢?首先咱們須要在反向索引表中找到相關的記錄,而後再掃描記錄,看他們是否包含其餘的詞,能夠想象這樣作的代價是很是高的。正因如此,termtermsmust 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文件都會返回,只有兩個文件三、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" }
            }
        }
    }
}

按照咱們指望的那樣,三、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)

existsmissing 除了過濾核心類型外,還能夠過濾一個對象的內部字段。下面這個文件:

{
   "name" : {
      "first" : "John",
      "last" :  "Smith"
   }
}

咱們能夠直接檢查 name.firstname.last 的存在性,也能夠只檢查 name 的存在性,正如在類型與映射中說的,上面這個對象的結構在內部會扁平化存儲,相似下面這樣:

{
   "name.first" : "John",
   "name.last"  : "Smith"
}

那咱們如何去用 existsmissing 過濾 name 字段呢?它並不在反向索引表中真實存在,

緣由是當咱們執行下面這個過濾的時候:

{
    "exists" : { "field" : "name" }
}

實際上執行的是:

{
    "bool": {
        "should": [
            { "exists": { "field": { "name.first" }}},
            { "exists": { "field": { "name.last"  }}}
        ]
    }
}

這也就意味着,若是 firstlast 都是空的狀況下,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 語句就須要過濾整月的文件,而非當天的文件。

參考

elastic.co: Structured Search

相關文章
相關標籤/搜索