Elasticsearch之结构化索引。

来源:互联网 发布:美国金融研究生 知乎 编辑:程序博客网 时间:2024/05/16 11:34

结构化搜索是指查询包含内部结构的数据。日期、时间和数字都是结构化的:他们有明确地格式给你执行逻辑操作。一般包含比较数字或日期的范围,或确定两个值哪个大。

文本也可以被结构化。一包蜡笔有不同的颜色:红色、绿色、蓝色。一篇博客可能被打上 分布式 和 搜索 的标签。电子商务产品有商品统一代码(UPCs)或其他有着严格格式的标识。

通过结构化搜索,你的查询结果始终是 是或非;是否应该属于集合。结构化搜索不关心文档的相关性或分数,它只是简单地包含或排除文档。

这必须是有意义的逻辑,一个数字不能比同一个范围中的其他数字更多。它只能包含在一个范围中——或不在其中。类似的,对于结构化文本,一个值必须相等或不等。这里没有更匹配的概念。

查找准确值

对于准确值,你需要使用过滤器。过滤器的重要性在于他们非常的快。他们不计算相关性(避过所有计分阶段)而且很容易被缓存。请记住尽可能多的使用过滤器。

用于数字的term过滤器

我们下面将介绍term过滤器,首先因为你可能经常会用到它,这个过滤器旨在处理数字、布尔值、日期和文本。

我们来看一下例子,一些产品最初用数字来索引,包含两个字段 price 和 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" }

我们的目标是找出特定价格的产品。假如你有关系型数据库背景,可能用SQL来表现这次查询比较熟悉,它看起来像这样:

SELECT document FROM products WHERE price = 20

在Elasticsearch DSL中,我们使用term 过滤器来实现同样的事。 term 过滤器会查找我们设定的准确值。 term 过滤器本身很简单,它接受一个字段名和我们希望查找的

值:

{

"term" : {

"price" : 20

}

}

term 过滤器本身并不能起作用。搜索API需要得到一个查询语句,而不是一个过滤器。为了使用term 过滤器,我们需要将它包含在一个过滤查询语句中:

GET /my_store/products/_search

{

"query" : {

"filtered" : {<1>

"query" : {

"match_all" : {}<2>

},

"filter" : {

"term" : { <3>

"price" : 20

}

}

}

}

}

<1> filtered 查询同时接受 query 与 filter。

<2> match_all 用来匹配所有文档,这是默认行为,所以在以后的例子中我们将省略掉query部分。

<3> 这是我们上面见过的 term 过滤器。注意它在 filter分句中的位置。

执行之后,你将得到预期的搜索结果:只能文档2被返回了(因为只有2 的价格是 20):

用于文本的 term过滤器

像我们在开头提到的,term 过滤器可以像匹配数字一样轻松地匹配字符串。让我们通过特定UPC标识码来找出产品,而不是通过价格。如果用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,我们可以看到UPC被分解成短小

的表征:

GET /my_store/_analyze?field=productID

XHDK-A-1293-#fJ3

这里有一些要点:

  • 我们得到了四个分开的表征,而不是一个完整的表征来表示UPC。
  • 所有的字符都被转为了小写。
  • 我们失去了连字符和 # 符号。
所以当我们用XHDK-A-1293-#fJ3来查找时,得不到任何结果,因为这个表征不在我们的倒排索引中。相反,那里有上面列出的四个表征。
显然,在处理唯一标识码,或其他枚举值时,这不是我们想要的结果。
为了避免这种情况发生,我们需要通过设置这个字段为not_analyzed 来告诉Elasticsearch它包含一个准确值。为了实现目标,我们要先删除旧索引(因为它包含了错误的
映射),并创建一个正确映射的索引:
DELETE /my_store<1>
PUT /my_store<2>
{
"mappings" : {
"products" : {
"properties" : {
"productID" : {
"type" : "string",
"index" : "not_analyzed"<3>
}
}
}
}
}
<1>必须首先删除索引,因为我们不能修改已经存在的映射。
<2>删除后,我们可以用自定义的映射来创建它。
<3>这里我们明确的表示不希望 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" }

现在我们的 term 过滤器将按预期工作。让我们在新索引的数据上再试一次(注意,查询和过滤都没有修改,只是数据被重新映射了)。

GET /my_store/products/_search

{

"query" : {

"filtered" : {

"filter" : {

"term" : {

"productID" : "XHDK-A-1293-#fJ3"

}

}

}

}

}

productID 字段没有经过分析, term 过滤器也没有执行分析,所以这条查询找到了准确匹配的值,如期返回了文档 1.

内部过滤操作

Elasticsearch在内部会通过一些操作来执行一次过滤:

1、查找匹配文档。

term 过滤器在倒排索引中查找词 XHDK-A-1293-#fJ3,然后返回包含那个词的文档列表。在这个列子中,只有文档1有我们想要的词。

2、创建字节集

然后过滤器将创建一个字节集——一个由1和0组成的数组——描述哪些文档包含这个词。匹配的文档得到1字节,在我们的例子中,字节集将会是[1 , 0 , 0 , 0]。

3、缓存字节集

最后,字节集被储存在内存中以使我们能用它来跳过步骤1和2。这大大的提升了性能,让过滤变得非常的快。

当执行filtered 查询时, filter 会比 query 早执行。结果字节集会被传给 query 来跳过已经被排除的文档。这种过滤器提升性能的方式,查询更少的文档意味着更快的速度。

组合过滤

前面的两个例子展示了单个过滤器的使用。现实中,你可能需要多虑多个值或字段,例如,想在Elasticsearch中表示这句SQL吗?

SELECT product FROM products WHERE (price = 20 OR productID = "XHDK-A-1293-#fJ3") AND (price != 30)

这些情况下,你需要bool 过滤器。这是以其他过滤器作为参数的组合过滤器,将他们结合成多种布尔组合。

布尔过滤器

bool 过滤器由三部分组成:

must:所有分句都必须匹配,与 AND 相同。

must_not:所有分句都必须不匹配,与 NOT 相同。

should:至少有一个分句匹配,与 OR 相同。

这样就行了!假如你需要多个过滤器,将他们放入 bool 过滤器就行。

提示: bool 过滤器的每个部分都是可选的(例如,你可以只保留一个must 分句),而且每个部分可以包含一到多个过滤器。

为了复制上面的SQL示例,我们将两个term 过滤器放在bool过滤器的should分句下,然后用另一个分句来处理 NOT 条件:

GET /my_store/products/_search

{

"query" : {

"filtered" : {<1>

"filter" : {

"bool" : {

"should" : [

{ "term" : { "price" : 20 } },<2>

{ "term" : { "productID" : "XHDK-A-1293-#fJ3" } }<2>

],

"must_not" : {

"term" : { "price" : 30 }<3>

}

}

}

}

}

}

<1>注意我们仍然需要用 filtered 查询来包裹所有条件。

<2>这两个 term 过滤器是 bool 过滤器的子节点,因为它们被放在 should 分句下,所以至少它们要有一个条件符合。

<3>如果一个产品价值 30,它就会被自动排除掉,因为它匹配了 must_not 分句。

我们的搜索结果返回了两个结果,分别满足了 bool 过滤器中的不同分句:

嵌套布尔过滤器

虽然 bool 是一个组合过滤器而且接收子过滤器,需明白它自己仍然只是一个过滤器。这意味着你可以在 bool 过滤器中嵌套 bool 过滤器,让你实现更复杂的布尔逻辑。

下面先给出SQL语句:

SELECT doucment 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" : { <1>

"must" : [

{ "term" : { "productID" : "JODL-X-1937-#pV7" } },<2>

{ "term" : { "price" : 30 } }<2>

]

}}

]

}

}

}

}

}

<1>因为 term 和 bool 在第一个should 分句中是平级的,至少需要匹配其中的一个过滤器。

<2>must 分句中有两个平级的 term 分句,所以他们两都需要匹配。

结果得到两个文档,分别匹配一个 should 分句:

这只是一个简单的例子,但是他展示了该怎样用布尔过滤器来构造复杂的逻辑条件。

查询多个准确值

term 过滤器在查询单个值时很好用,但是你经常需要搜索多个值。比如你想寻找20或30元的文档,该怎么做?

比起使用多个 term 过滤器,你可以用一个 terms 过滤器。 terms 过滤器是 term 过滤器的复数版本。

它用起来和 term 差不多,我们现在来指定一组数值,而不是单一价格:

{

"terms" : {

"price" : [20 , 30]

}

}

像 term 过滤器一样,我们将它放在 filtered查询中:

GET /my_store/products/_search

{

"query" : {

"filtered" : {

"filter" : {

"terms" : {<1>

"price" : [20 , 30]

}

}

}

}

}

<1>这是前面提到的 terms 过滤器,放置在 filtered查询中

这条查询将返回第二、第三和第四个文档:

包含,而不是相等

理解 term 和 terms 是包含操作,而不是相等操作,这点非常重要。这意味着什么?

假如你有一个term过滤器 { "term" : { "tags" : "search" } } , 它将匹配下面两个文档:

{ "tags" : [ "search" ] }

{ "tags" : [ "search" , "open_source" ] }<1>

<1> 虽然这个文档除了 search 还有其他短语,他还是被返回了

回顾一下 term 过滤器是怎么工作的:它检查倒排索引中所有具有短语的文档,然后组成一个字节集。在我们简单的示例中,我们有下面的倒排索引:

执行 term 过滤器来查询 search 时,它直接在倒排索引中匹配值并找到相关的ID。如你所见,文档1和文档2都包含 search ,所以他们都作为结果集返回。

提示:倒排索引的特性让完全匹配一个字段变得非常困难。你将如何确定一个文档只能包含你请求的短语?你将在索引中找出这个短语,解出所有相关文档ID,然后扫描

索引中每一行来确定文档是否包含其他值。

由此可见,这将变得非常低效和开销巨大。因此,term 和 terms 是必须包含操作,而不是必须相等。

完全匹配

假如你真的需要完全匹配这种行为,最好是通过添加另一个字段来实现。在这个字段中,你索引原字段包含值的个数。引用上面的两个文档,我们现在包含一个字段来记录

标签的个数:

{ "tags" : ["search"] , "tag_count" : 1 }

{ "tags" : ["search"] , "open_source" , "tag_count" : 2 }

一旦你索引了标签个数,你可以构造一个bool 过滤器来限制短语个数:

GET /my_idnex/my_type/_search

{

"query" : {

"filtered" : {

"filter" : {

"bool" : {

"must" : [

{ "term" : { "tags" : "search" } },<1>

{ "term" : { "tag_count" : 1 } }<2>

]

}

}

}

}

}

<1>找出所有包含 search 短语的文档

<2>但是确保文档只有一个标签

这将匹配只有一个 search 标签的文档,而不是匹配所有包含了 search 标签的文档。

范围

我们到现在只搜索过准确的数字,现实中,通过范围来过滤更为有用。例如,你可能希望找到所有价格高于20元而低于40元的产品。

在SQL语法中,范围可以如下表示:

SELECT doucment FROM products WHERE price BETWEEN 20 AND 40

Elasticsearch 有一个 range 过滤器,让你可以根据范围过滤:

"range" : {

"price" : {

"gt" : 20,

"lt" : 40

}

}

range 过滤器既能包含也能排除范围,通过下面的选项:

  • gt:> 大于
  • lt: < 小于
  • gte:>= 大于或等于
  • lte: <= 小于或等于
下面是范围过滤器的一个示例:
GET /my_store/products/_search
{
"query" : {
"filtered" : {
"filter" : {
"range" : {
"price" : {
"gte" : 20,
"lt" : 40
}
}
}
}
}
}
假如你需要不设限的范围,去掉一边的限制就可以了:
"range" : {
"price" : {
"gt" : 20
}
}

日期范围

range过滤器也可以用于日期字段:
"range" : {
"timestamp" : {
"gt" : "2014-01-01 00:00:00,
"lt" : "2014-01-07 00:00:00"
}
}
当用于日期字段时, range 过滤器支持日期数字操作。例如,我们想找到所有最近一个小时的文档:
"range" : {
"timestamp" : {
"gt" : "now - 1h"
}
}
这个过滤器将始终能找到所有时间戳大于大于当前时间减1小时的文档,让这个过滤器像移窗一样通过你的文档。
日期计算也能用于实际的时间,而不是仅仅是一个像now 一样的占位符。只要在日期后加上双竖线 ||,就能使用日期数学表达式了。
"range" : {
"timestamp" : {
"gt" : "2014-01-01 00:00:00",
"lt" : "2014-01-01 00:00:00||+1M"<1>
}
}
<1> 早于2014年1月1号加一个月
日期计算是与日历相关的,所以它知道每个月的天数,每年的天数,等等。

字符串范围

range 过滤器也可以用于字符串。字符串范围根据字典或字母顺序来计算。例如,这些值按照字典顺序排序:
5,50,6,B,C,a,ab,abb,abc,b
提示:倒排索引中的短语按照字典顺序排序,也是为什么字符串范围使用这个顺序。
假如我们想让范围从 a 开始而不包含 b,我们可以用类似的 range 过滤器语法:
"range" : {
"title" : {
"gte" : "a",
"lt" : "b"
}
}
当心基数:
数字和日期字段的索引方式让他们在计算范围时十分搞笑。但对于字符串来说却不是这样。为了在字符串上执行范围操作,Elasticsearch会在这个范围内的每个短语执行
term操作。这比日期或数字的范围操作慢得多。
字符串范围适用于一个基数较小的字段,一个唯一短语个数较小的字段。你的唯一短语数越多,搜索就越慢。

处理Null值

回到我们早期的实例,在文档中有一个多值的字段 tags,一个文档可能包含一个或多个标签,或根本没有标签。如果一个字段没有值,它是怎么储存在倒排索引中的?
这是一个取巧的问题,因为答案是它根本没有存储。让我们从看一下前几节的倒排索引:
你怎么可能存储一个在数据结构不存在的字段呢?倒排索引是表征和包含他们的文档的一个简单列表。假如一个字段不存在,他就没有任何表征,也就意味着它无法被倒排
索引的数据结构表达出来。
本质上来说,null, [] 空数组 和 [null] 是相等的。他们都不存在于倒排索引中!
显然,这个世界却没有那么简单,数据经常缺失字段,或包含空值或空数组。为了应对这些情形,Elasticsearch有一些工具来处理空值或缺失的字段。
exists 过滤器
工具箱中的第一个利器是 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 字段有一个值
<2>tags 字段有两个值
<3>tags 字段不存在
<4>tags 字段被设为 null
<5> tags 字段有一个值和一个 null
结果我们 tags 字段的倒排索引看起来将是这样:
我们的目标是找出所有设置了标签的文档,我们不关心这个标签是什么,只要它存在于文档中就行。在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" }
}
}
}
}
查询返回三个文档:
结果很容易理解,所以在tags 字段中有值的文档都被返回了,只排除了文档3和4。

missing过滤器

missing 过滤器本质上 exists 的反义词:它返回没有特定字段值的文档,像这条SQL一样:
SELECT tags FROM posts WHERE tags IS NULL
让我们在前面的例子中用 missing 过滤器来取代exits :
GET /my_index/posts/_search
{
"query" : {
"filtered" : {
"filter" : {
"missing" : { "field" : "tags" }
}
}
}
}
如你所愿,我们得到了两个没有包含标签字段的文档:

什么时候 null 才表示 null

有时你需要能区分一个字段是没有值,还是被设置为 null。用上面见到的默认行为无法区分这一点,数据都不存在了。幸运的是,我们可以将明确地 null 值用我们选择的
占位符来代替。
当指定字符串、数字、布尔值或日期字段的映射时,你可以设置一个 null_value来处理明确地 null 值。没有值的字段仍将被排除在倒排索引外。
当选定一个合适的 null_value 时,确保以下几点:
  • 它与字段的类型匹配,他不能在date类型的字段中使用字符串 null_value
  • 它需要能与这个字段可能包含的正常值区分开来,以避免真实值和null 值混淆

对象的 exists/missing

exists 和 missing 过滤器同样能在内联对象上工作,而不仅仅是核心类型。例如下面的文档:
{
"name" : {
"first" : "John" ,
"last" : "Smith"
}
}
你可以检查 name.first 和 name.last的存在性,也可以检查 name的。然而,对象在内部被转成扁平化的键值结构,像下面所示:
所以我们是怎么使用 exists或 missing来检测 name 字段的呢,这个字段并没有真正存在于倒排索引中。
原因是像这样的一个过滤器
{
"exists" : { "field" : "name" }
}
实际是这样执行的
{
"bool" : {
"should" : [
{ "exists" : { "field" : { "name.first" } } },
{ "exists" : { "field" : { "name.last" } } }
]
}
}
同样这意味着假如 first 和 last 都为空,那么name就是不存在的。

关于缓存

过滤器是如何计算的?他们的核心是一个字节集来表示哪些文档符合这个过滤器。Elasticsearch主动缓存了这些字节集留作以后使用。一旦缓存后,当遇到相同的过滤时,
这些字节集就可以被重用,而不需要重新运算整个过滤。
缓存字节集很“聪明” :他们会增量跟新。你索引中添加了新的文档,只有这些新文档需要被添加到已存的字节集中,而不是一遍遍重新计算整个缓存的过滤器。过滤器和
整个系统的其他部分一样是实时的,你不需要关心缓存的过期时间。

独立的过滤缓存

每个过滤器都被独立计算和缓存,而不管他们在哪里使用。如果两个不同的查询使用相同的过滤器,则会使用相同的字节集。同样,如果一个查询在多处使用同样地过滤
器,只有一个字节集会被计算和重用。
让我们看一下示例,查找符合下列条件的邮箱:
  • 在收件箱而且没有被读取过
  • 不在收件箱但是被标记为重要
"bool" : {
"should" : [
{ "bool" : {
"must" : [
{ "term" : { "folder" : "inbox" } }, <1>
{ "term" : { "read" : false } }
]
}} ,
{ "bool" : {
"must_not" : {
"term" : { "folder" : "inbox" }<1>
},
"must" : {
"term" : { "important" : true }
}
}}
]
}
<1>这两个过滤器相同,而且会使用同一个字节集。
虽然一个收件箱条件是 must 而另一个是 must_not ,这两个条件本身时相等的。这意味着字节集会在第一个条件执行时计算一次,然后作为缓存被另一个条件使用。而第
二次执行这条查询时,收件箱的过滤已经被缓存了,所以两个条件都能使用缓存的字节集。
这与查询DSL的组合型紧密相关。移动过滤器或在相同查询中多处重用相同的过滤器非常简单。这不仅仅是方便了开发者——对于性能也有很大的提升。

控制缓存

大部分直接处理字段的的枝叶过滤器(例如 term)会被缓存,而像 bool 这类的组合过滤器则不会被缓存。
【提示】
枝叶过滤器需要在硬盘中检查倒排索引,所以缓存他们是有意义的。另一方面来说,组合过滤器使用快捷的字节逻辑来组合他们内部条件生成的字节集结果,所以每次重
新计算他们也是很高效的。
然而,有部分枝叶过滤器,默认不会被缓存,因为他们这样做没有意义:
脚本过滤器:
脚本过滤器的结果不能被缓存因为脚本的意义对于Elasticsearch来说是不透明的。
Geo过滤器:
定位过滤器,通常被用于过滤基于特定用户地理位置的结果。因为每个用户都有一个唯一的定位,geo过滤器看起来不太会重用,所以缓存他们也没有意义。
日期范围:
使用 now 方法的日期范围(例如 “now - 1h”),结果值精确到毫秒。每次这个过滤器执行时,now 返回一个新的值。老的过滤器将不再被使用,所以默认缓存是被禁用
的。然而,当now被取整时(例如,now/d取最近一天),缓存默认是被启用的。
有时候磨人的缓存测试并不正确。可能你希望一个复杂的bool表达式可以在相同的查询中重复使用,或你想要禁用一个date字段过滤器缓存。你可以通过_cache 标记来
覆盖几乎所有过滤器的默认缓存策略。
{
"range" : {
"timestamp" : {
"gt" : "2014-01-02 16:15:14"<1>
},
"_cache" : false <2>
}
}
<1>看起来我们不会再使用这个精确时间戳
<2> 在这个过滤器上禁用缓存

过滤顺序

在 bool条件中过滤器的顺序对性能有很大的影响。更详细的过滤条件应该被防止在其他过滤器之前,以便在更早的排除更多的文档。
假如条件A匹配1000万个文档,而B只匹配100个文档,那么需要将B放在A前面。
缓存的过滤器非常快,所以他们需要被放在不能缓存的过滤器之前。想象一下我们有一个索引包含了一个月的日志事件,然而,我们只对近一个小时的事件感兴趣:
GET /logs/2014-01/_search
{
"query" : {
"filtered" : {
"filter" : {
"range" : {
"timestamp" : {
'gt" : "now - 1h"
}
}
}
}
}
}
这个过滤条件没有被缓存,因为它使用了now 方法,这个值每毫秒都在变化。这意味着我们需要每次执行这条查询时都检测一整个月的日志事件。
我们可以通过组合一个缓存的过滤器来让这变得更有效率:我们可以添加一个含固定时间的过滤器来排除掉这个月的大部分数据,例如昨晚凌晨:
"bool" : {
"must" : [
{ "range" : {
"timestamp" : {
"gt" : "now - 1h/d"<1>
}
}} ,
{ "range" : {
"timestamp" : {
"gt" : "now - 1h"<2>
}
}}
]
}
<1>这个过滤器被缓存了,因为它使用了取整到昨夜凌晨 now 条件。
<2>这个过滤器没有被缓存,因为它没有对now 取整。
now - 1h/d 条件取整到昨夜凌晨,所以所有今天之前的文档都被排除掉了。这个结果的字节集被缓存了,因为 now 被取整了,意味着它只需要每天当昨夜凌晨的值改变时
被执行一次。now - 1h条件没有被缓存,因为now表示最近一毫秒的时间。
然而,得益于第一个过滤器,第二个过滤器只需要检测当天的文档就行。
这些条件的排序很重要。上面的实现能正常工作是因为自从昨晚凌晨条件比最近一小时条件位置更前。假如他们用别方式组合,那么最近一小时条件还是需要检测所有的
文档,而不仅仅是昨夜以来的文档。

原创粉丝点击