Part II 深入搜索
搜索不仅仅是全文本搜索:数据的很大部分是结构化的值例如日期、数字。这部分开始解释怎样以一种高效地方式结合结构化搜索和全文本搜索。
第十二章 结构化搜索
结构化搜索_ 是指查询包含内部结构的数据。日期,时间,和数字都是结构化的:它们有明确的格式给你执行逻辑操作。一般包括比较数字或日期的范围,或确定两个值哪个大。
文本也可以被结构化。一包蜡笔有不同的颜色:
红色
,绿色
,蓝色
。一篇博客可能被打上分布式
和搜索
的标签。电子商务产品有商品统一代码(UPCs) 或其他有着严格格式的标识。
通过结构化搜索,你的查询结果始终是 是或非;是否应该属于集合。结构化搜索不关心文档的相关性或分数,它只是简单的包含或排除文档。
这必须是有意义的逻辑,一个数字不能比同一个范围中的其他数字更多。它只能包含在一个范围中 —— 或不在其中。类似的,对于结构化文本,一个值必须相等或不等。这里没有 更匹配 的概念。
12.1 查找准确值
对于准确值,你需要使用过滤器。过滤器的重要性在于它们非常的快。它们不计算相关性(避过所有计分阶段)而且很容易被缓存。我们今后再来讨论过滤器的性能优势【过滤器缓存】,现在,请先记住尽可能多的使用过滤器。
用于数字的 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
过滤器本身并不能起作用。像在【查询 DSL】中介绍的一样,搜索 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
):
"hits" : [
{
"_index" : "my_store",
"_type" : "products",
"_id" : "2",
"_score" : 1.0, <1>
"_source" : {
"price" : 20,
"productID" : "KDKE-B-9947-#kL5"
}
}
]
<1> 过滤器不会执行计分和计算相关性。分值由 match_all
查询产生,所有文档一视同仁,所有每个结果的分值都是 1
用于文本的 term
过滤器
像我们在开头提到的,term
过滤器可以像匹配数字一样轻松的匹配字符串。让我们通过特定 UPC 标识码来找出产品,而不是通过价格。如果用 SQL 来实现,我们可能会使用下面的查询:
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 被分解成短小的表征:
curl -XGET '10.10.10.114:9200/my_store_weichao/_analyze?field=productID&pretty' -d'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。
- 所有的字符都被转为了小写。
- 我们失去了连字符和
#
符号。
所以当用 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 在内部会通过一些操作来执行一次过滤:
-
查找匹配文档。
term
过滤器在倒排索引中查找词XHDK-A-1293-#fJ3
,然后返回包含那个词的文档列表。在这个例子中,只有文档 1 有我们想要的词。 -
创建字节集
然后过滤器将创建一个 字节集 —— 一个由 1 和 0 组成的数组 —— 描述哪些文档包含这个词。匹配的文档得到
1
字节,在我们的例子中,字节集将是[1,0,0,0]
-
缓存字节集
最后,字节集被储存在内存中,以使我们能用它来跳过步骤 1 和 2。这大大的提升了性能,让过滤变得非常的快。
-
增加使用次数累积
Elasticsearch能够缓存
non-scoring
查询,从而让查询访问更快,然而它也会将很少再用的查询缓存起来。因此我们想缓存那些未来会再次用到的查询从而来减少资源的浪费。
当执行 filtered
查询时,filter
会比 query
早执行。结果字节集会被传给 query
来跳过已经被排除的文档。这种过滤器提升性能的方式,查询更少的文档意味着更快的速度。
组合过滤
前面的两个例子展示了单个过滤器的使用。现实中,你可能需要过滤多个值或字段,例如,想在 Elasticsearch 中表达这句 SQL
SELECT product
FROM products
WHERE (price = 20 OR productID = "XHDK-A-1293-#fJ3")
AND (price != 30)
这些情况下,需要 bool
过滤器。这是以其他过滤器作为参数的组合过滤器,将它们结合成多种布尔组合。
布尔过滤器
bool
过滤器由三部分组成:
{
"bool" : {
"must" : [],
"should" : [],
"must_not" : [],
}
}
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
过滤器中的不同分句:
"hits" : [
{
"_id" : "1",
"_score" : 1.0,
"_source" : {
"price" : 10,
"productID" : "XHDK-A-1293-#fJ3" <1>
}
},
{
"_id" : "2",
"_score" : 1.0,
"_source" : {
"price" : 20, <2>
"productID" : "KDKE-B-9947-#kL5"
}
}
]
<1> 匹配 term
过滤器 productID = "XHDK-A-1293-#fJ3"
<2> 匹配 term
过滤器 price = 20
嵌套布尔过滤器
虽然 bool
是一个组合过滤器而且接受子过滤器,需明白它自己仍然只是一个过滤器。这意味着你可以在 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" : {
"constant_score" : {
"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
中的bool
过滤器中,因此至少term
和bool
其中一个查询必须被匹配。
<2> 这两个term
过滤器在bool
查询的must
中匹配嵌套,因此必须全部匹配。
结果得到两个文档,分别匹配一个 should
分句:
"hits" : {
"total" : 2,
"max_score" : 1.0,
"hits" : [ {
"_index" : "my_store_weichao",
"_type" : "products",
"_id" : "2",
"_score" : 1.0,
"_source" : {
"price" : 20,
"productID" : "KDKE-B-9947-#kL5"
}
}, {
"_index" : "my_store_weichao",
"_type" : "products",
"_id" : "3",
"_score" : 1.0,
"_source" : {
"price" : 30,
"productID" : "JODL-X-1937-#pV7"
}
} ]
这只是一个简单的例子,但是它展示了该怎样用布尔过滤器来构造复杂的逻辑条件。
查询多个准确值
term
过滤器在查询单个值时很好用,但是你可能经常需要搜索多个值。比如你想寻找 20 或 30 元产品的文档,该怎么做呢?
比起使用多个 term
过滤器,你可以用一个 terms
过滤器。terms
过滤器是 term
过滤器的复数版本。
它用起来和 term
差不多,我们现在来指定一组数值,而不是单一价格:
{
"terms" : {
"price" : [20, 30]
}
}
像 term
过滤器一样,我们将它放在 filtered
查询中:
GET /my_store/products/_search
{
"query" : {
"constant_score" : {
"filter" : {
"terms" : {
"price" : [20, 30]
}
}
}
}
}
<1> 这是前面提到的 terms
过滤器,放置在 包含在constant_score
的filtered
查询中这条查询将返回第二,第三和第四个文档:
包含,而不是相等
理解 term
和 terms
是包含操作,而不是相等操作,这点非常重要。
假如你有一个 term 过滤器 { "term" : { "tags" : "search" } }
,它将匹配下面两个文档:
{ "tags" : ["search"] }
{ "tags" : ["search", "open_source"] } <1>
<1> 虽然这个文档除了 search
还有其他短语,它还是被返回了
回顾一下 term
过滤器是怎么工作的:它检查倒排索引中所有具有短语的文档,然后组成一个字节集。在我们简单的示例中,我们有下面的倒排索引:
Token | DocIDs |
---|---|
open_source |
2 |
search |
1 ,2
|
当执行 term
过滤器来查询 search
时,它直接在倒排索引中匹配值并找出相关的 ID。如你所见,文档 1 和文档 2 都包含 search
,所以他们都作为结果集返回。
提示:
倒排索引的特性让完全匹配一个字段变得非常困难。你将如何确定一个文档只能包含你请求的短语?你将在索引中找出这个短语,解出所有相关文档 ID,然后扫描 索引中每一行来确定文档是否包含其他值。
由此可见,这将变得非常低效和开销巨大。因此,term
和 terms
是 必须包含 操作,而不是 必须相等。
完全匹配
假如你真的需要完全匹配这种行为,最好是通过添加另一个字段来实现。在这个字段中,你索引原字段包含值的个数。引用上面的两个文档,我们现在包含一个字段来记录标签的个数:
{ "tags" : ["search"], "tag_count" : 1 }
{ "tags" : ["search", "open_source"], "tag_count" : 2 }
一旦你索引了标签个数,你可以构造一个 bool
过滤器来限制短语个数:
GET /my_index/my_type/_search
{
"query": {
"constant_score" : {
"filter" : {
"bool" : {
"must" : [
{ "term" : { "tags" : "search" } }, <1>
{ "term" : { "tag_count" : 1 } } <2>
]
}
}
}
}
}
<1> 找出所有包含 search
短语的文档
<2> 但是确保文档只有一个标签
这将匹配只有一个 search
标签的文档,而不是匹配所有包含了 search
标签的文档。
范围
到现在只搜索过准确的数字,现实中,通过范围来过滤更为有用。例如,你可能希望找到所有价格高于 20 元而低于 40 元的产品。
在 SQL 语法中,范围可以如下表示:
SELECT document
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
操作。这比日期或数字的范围操作慢得多。
字符串范围适用于一个基数较小的字段,一个唯一短语个数较少的字段。你的唯一短语数越多,搜索就越慢。
12.2 处理 Null 值
回到我们早期的示例,在文档中有一个多值的字段 tags
,一个文档可能包含一个或多个标签,或根本没有标签。如果一个字段没有值,它是怎么储存在倒排索引中的?
这是一个取巧的问题,因为答案是它根本没有存储。让我们从看一下前几节的倒排索引:
Token | DocIDs |
---|---|
open_source |
2 |
search |
1 ,2
|
你怎么可能储存一个在数据结构不存在的字段呢?倒排索引是标记和包含它们的文档的一个简单列表。假如一个字段不存在,它就没有任何标记,也就意味着它无法被倒排索引的数据结构表达出来。
本质上来说,null
,[]
(空数组)和 [null]
是相等的。它们都不存在于倒排索引中!
显然,这个世界却没有那么简单,数据经常会缺失字段,或包含空值或空数组。为了应对这些情形,Elasticsearch 有一些工具来处理空值或缺失的字段。
exists
过滤器
工具箱中的第一个利器是 exists
过滤器,这个过滤器将返回任何包含这个字段的文档,让我们用标签来举例,索引一些示例文档:
POST /my_index_weichao/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
字段的倒排索引看起来将是这样:
Token | DocIDs |
---|---|
open_source |
2 |
search |
1 ,2 ,5
|
我们的目标是找出所有设置了标签的文档,我们不关心这个标签是什么,只要它存在于文档中就行。在 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" }
}
}
}
}
查询返回三个文档:
"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
过滤器
missing
过滤器本质上是 exists
的反义词:它返回没有特定字段值的文档,像这条 SQL 一样:
SELECT tags
FROM posts
WHERE tags IS NULL
让我们在前面的例子中用 missing
过滤器来取代 exists
:
GET /my_index/posts/_search
{
"query" : {
"filtered" : {
"filter": {
"missing" : { "field" : "tags" }
}
}
}
}
如你所愿,我们得到了两个没有包含标签字段的文档:
"hits" : [
{
"_id" : "3",
"_score" : 1.0,
"_source" : { "other_field" : "some data" }
},
{
"_id" : "4",
"_score" : 1.0,
"_source" : { "tags" : null }
}
]
什么时候 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
的。然而,在【映射】中,我们提到对象在内部被转成扁平化的键值结构,像下面所示:
{
"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
就是不存在的。
12.3 关于缓存
在【内部过滤操作】章节中,提到过过滤器是怎么计算的。它们的核心是一个字节集来表示哪些文档符合这个过滤器。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 过滤器:
定位过滤器(我们会在【geoloc】中更详细的介绍),通常被用于过滤基于特定用户地理位置的结果。因为每个用户都有一个唯一的定位,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> 在这个过滤器上禁用缓存
以后的章节将提供一些例子来说明哪些时候覆盖默认缓存策略是有意义的。
12.4 过滤顺序
在 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
表示最近一毫秒的时间。然而,得益于第一个过滤器,第二个过滤器只需要检测当天的文档就行。
这些条件的排序很重要。上面的实现能正常工作是因为自从昨晚凌晨条件比最近一小时条件位置更前。假如它们用别的方式组合,那么最近一小时条件还是需要检测所有的文档,而不仅仅是昨夜以来的文档。
《读书报告 – Elasticsearch入门 》----Part II 深入搜索(1)的更多相关文章
-
《读书报告 -- Elasticsearch入门 》--简单使用(2)
<读书报告 – Elasticsearch入门 > ' 第四章 分布式文件存储 这章的主要内容是理解数据如何在分布式系统中存储. 4.1 路由文档到分片 创建一个新文档时,它是如何确定应该 ...
-
《读书报告 -- Elasticsearch入门 》-- 安装以及简单使用(1)
<读书报告 – Elasticsearch入门 > 第一章 Elasticsearch入门 Elasticsearch是一个实时的分布式搜索和分析引擎,使得人们可以在一定规模上和一定速度上 ...
-
《读书报告 – Elasticsearch入门 》----Part II 深入搜索(2)
第十三章 全文检索 这一章开始介绍 全文检索 :怎样对全文字段(full-text fields)进行检索以找到相关度最高的文档. 全文检索最重要的两个方面是: 相关度(Relevance) 根据文档 ...
-
ElasticSearch入门-搜索如此简单
搜索引擎我也不是很熟悉,但是数据库还是比较了解.可以把搜索理解为数据库的like功能的替代品.因为like有以下几点不足: 第一.like的效率不行,在使用like时,一般都用不到索引,除非使用前缀匹 ...
-
和我一起打造个简单搜索之ElasticSearch入门
本文简单介绍了使用 Rest 接口,对 es 进行操作,更深入的学习,可以参考文末部分. 环境 本文以及后续 es 系列文章都基于 5.5.3 这个版本的 elasticsearch ,这个版本比较稳 ...
-
ElasticSearch入门-搜索(java api)
ElasticSearch入门-搜索(java api) package com.qlyd.searchhelper; import java.util.Map; import net.sf.json ...
-
Elasticsearch入门教程(五):Elasticsearch查询(一)
原文:Elasticsearch入门教程(五):Elasticsearch查询(一) 版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:h ...
-
Elasticsearch入门教程(一):Elasticsearch及插件安装
原文:Elasticsearch入门教程(一):Elasticsearch及插件安装 版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:h ...
-
ElasticSearch入门知识扫盲
ElasticSearch 入门介绍 tags: 第三方 lucene [toc] 1. what Elastic Search(ES)是什么 全文检索和lucene 全文检索 优点:高效,准确,分词 ...
随机推荐
-
SE Springer小组《Spring音乐播放器》软件需求说明3
3 需求规定 3.1对功能的规定 基本功能与相关的输入输出如下表所示.歌曲播放.停止.暂停等功能调用MCI库,数据在MCI库下如何运作与用户的直观感受无关,就不具体列出. 输入 处理 输出 用户登录信 ...
-
Apache Shiro 学习记录4
今天看了教程的第三章...是关于授权的......和以前一样.....自己也研究了下....我觉得看那篇教程怎么说呢.....总体上是为数不多的精品教程了吧....但是有些地方确实是讲的太少了.... ...
-
App_api设计
2014年,移动APP的热度丝毫没有减退,并没有像桌面软件被WEB网站那样所取代,不但如此,越来越多的传统应用.网站也都开始制作自己的移动APP,也就是我们常说的IOS客户端.android客户端.这 ...
-
dragloader.js帮助你在页面原生滚动下实现Pull Request操作
dragloader.js是一个面向移动Web开发的JavaScript库,帮助开发者在使用页面原生滚动时,模拟上/下拉手势,实现Pull Request操作. 在移动设备上,一般会使用 drag d ...
-
Linux中如何新建用户
对于一般用户来说,主目录(home directory)是硬盘上唯一可以原来写东西的地方.一般的路径名是/home/login_user_name. 主目录用于存储各种用户文件:设置文件,程序配置文件 ...
-
Kafka小记
kafka简介 kafka是由LinkedIn开发,主要是用来处理Linkedin的大面积活跃数据流处理(activity stream). 此类的数据经常用来反映网站的一些有用的信息,比如PV,页 ...
-
UI设计师不可不知的安卓屏幕知识
不少设计师和工程师都被安卓设备纷繁的屏幕搞得晕头转向,我既做UI设计,也做过一点安卓界面布局,刚好对这块内容比较熟悉,也曾在公司内部做过相关的讲座,在此,我将此部分知识重新梳理出来分享给大家! 1.了 ...
-
spring中WebApplicationContextUtils类说明
WebApplicationContextUtils是一个抽象类,其提供了一个很便利的方法来获取spring应用的上下文即WebApplicationContext. 其中的静态方法getWebApp ...
-
Python之数据类型转换
平时我们在处理数据的时候,有些数据类型不是我们想要的,怎么办? 一.数据类型转换:要转换的类型(数据) 要把num01转换为整数:int(num01) 要把num01转换为浮点数:float(num0 ...
-
Angular4.x跨域请求
Angular4.x请求 码云地址: https://gitee.com/ccsoftlucifer/Angular4Study 1. 搭建工程 新建一个工程angulardemo03 ng new ...