《读书报告 – Elasticsearch入门 》----Part II 深入搜索(1)

Part II 深入搜索

搜索不仅仅是全文本搜索:数据的很大部分是结构化的值例如日期、数字。这部分开始解释怎样以一种高效地方式结合结构化搜索和全文本搜索。

第十二章 结构化搜索

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

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

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

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

12.1 查找准确值

对于准确值,你需要使用过滤器。过滤器的重要性在于它们非常的快。它们不计算相关性(避过所有计分阶段)而且很容易被缓存。我们今后再来讨论过滤器的性能优势【过滤器缓存】,现在,请先记住尽可能多的使用过滤器。

用于数字的 term 过滤器

介绍 term 过滤器,经常会用到它,这个过滤器旨在处理数字,布尔值,日期,和文本。

看一下例子,一些产品最初用数字来索引,包含两个字段 priceproductID

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 查询同时接受接受 queryfilter
<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 在内部会通过一些操作来执行一次过滤:

  1. 查找匹配文档

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

  2. 创建字节集

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

  3. 缓存字节集

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

  4. 增加使用次数累积

    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> 因为termboolshould中的bool过滤器中,因此至少termbool其中一个查询必须被匹配。

<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_scorefiltered 查询中这条查询将返回第二,第三和第四个文档:

包含,而不是相等

理解 termterms包含操作,而不是相等操作,这点非常重要。

假如你有一个 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,然后扫描 索引中每一行来确定文档是否包含其他值。

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

完全匹配

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

{ "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

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 就是不存在的。


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 表示最近一毫秒的时间。然而,得益于第一个过滤器,第二个过滤器只需要检测当天的文档就行。

这些条件的排序很重要。上面的实现能正常工作是因为自从昨晚凌晨条件比最近一小时条件位置更前。假如它们用别的方式组合,那么最近一小时条件还是需要检测所有的文档,而不仅仅是昨夜以来的文档。


原文地址:https://www.cnblogs.com/wangyaning/p/7853883.html