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

原文:http://www.cnblogs.com/richaaaard/p/5241461.html

摘要

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

文本也可以是結構化的。比如彩筆可以有紅、綠、藍顏色集合,一個博客可以有關鍵字標籤 分佈式 和 搜索 。 電商網站上的商品都有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運行時,會執行多個操作:

  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 查詢時,filter 在 query 之前執行,所以在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 條件塊裏面的 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,所以兩個文件都會作爲結果返回。

注意:
由於反向索引表自身的特性,整個字段是否相等比較難以計算,如果確定一個文件包含我們想要查找的詞呢?首先我們需要在反向索引表中找到相關的記錄,然後再掃描記錄,看他們是否包含其他的詞,可以想象這樣做的代價是非常高的。正因如此,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文件都會返回,只有兩個文件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 語句就需要過濾整月的文件,而非當天的文件。

參考

elastic.co: Structured Search

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章