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

相关文章
相关标签/搜索