组合查询
在上面之中我们学习到了 match 和 term 进行查询的时候,只介绍了单个 api 做一个简单的查询效果,没有提供组合查询的效果的效果。
Bool Query,布尔查询效果,可以组合多过滤语句来帮助我们过滤文档。
Boosting Query,通过 positive 块指定匹配文档的评分,同时降低在 negative 块中匹配的文档的得分,提供调整相关性算分的能力
constant_score Query,通过外部包裹过滤器,通过去掉评分的效果提高了我们的查询效率。
dis_max Query,返回匹配了一个或者多个查询语句的文档,但只近最佳匹配的评分作为相关性算法得到的结果进行返回
function_score Query,支持使用函数来修改查询返回的分数
Bool Query 最常用的查询
Bool Query 在es之中,官方提供了一种布尔查询子句帮助我们实现了一种组合一个或者多个查询效果拼接的需求的实现。我们则可以通过Bool Query来实现多个查询的组合。一般来说它包含有以下的几个参数:
- must:相当于 and 查询的内容是组合的他们必须都在文档之中出现,该文档才会被匹配到
- filter:跟 must 类似。没有评分的and 但是filter一旦使用就会忽略评分的效果,因为一般来说它的字句会在 filter context 之中执行,所以它的相关性评分会被忽略掉,并且需要注意的是,他的子句一般来说会被考虑用于缓存。
- should:相当于 or ,minimum_should_num 可以用于设定最小被匹配的条件数量
- must_not:相当于 not 并且需要注意的是 must not 也是不会做相关性评分。
POST ${index}/_search
{
"query": {
"bool": {
"$[must/filter/should/must_not]": [
{
${match type struct / term type struct}
},
{
${match type struct / term type struct}
},
······
],
"$[must/filter/should/must_not]": [
{
${match type struct / term type struct}
},
{
${match type struct / term type struct}
},
······
],
"${minimum_should_match}": n
}
}
}
must
POST books/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"book_id": {
"value": "16"
}
}
},
{
"match": {
"name": "test 114.514"
}
}
]
}
}
}
执行结果
"hits" : [
{
"_index" : "books",
"_type" : "_doc",
"_id" : "J12f8ZEBjKOqwPdUiKBD",
"_score" : 3.453352,
"_source" : {
"book_id" : "16",
"name" : "elasticsearch tb 114.514 est"
}
}
]
should
POST books/_search
{
"query": {
"bool": {
"should": [
{
"term": {
"book_id": {
"value": "16"
}
}
},
{
"match": {
"name": "manba"
}
}
]
// 添加 minimum_should_match 限定匹配最少需要多少条件
/* ,
"minimum_should_match": 2
*/
}
}
}
执行结果
"hits" : [
{
"_index" : "books",
"_type" : "_doc",
"_id" : "6",
"_score" : 2.4883628,
"_source" : {
"book_id" : "6",
"name" : "manba out!"
}
},
{
"_index" : "books",
"_type" : "_doc",
"_id" : "J12f8ZEBjKOqwPdUiKBD",
"_score" : 1.4816045,
"_source" : {
"book_id" : "16",
"name" : "elasticsearch tb 114.514 est"
}
}
]
而如果,我们在使用bool - shoud,也就是or类型的查询的时候,我们可以通过使用参数 minimum_should_match 设定文档要匹配成功最少要匹配到多少个查询条件。在默认的情况下,这个值是1。
must + filter组合
POST books/_search
{
"query": {
"bool": {
"must": [
{
"match_phrase": {
"name": "elasticsearch"
}
},
{
"range": {
"book_id": {
"gte": 0,
"lte": 200
}
}
}
],
"filter": [
{
"term": {
"book_id": {
"value": "16"
}
}
}
]
}
}
}
执行结果
"hits" : [
{
"_index" : "books",
"_type" : "_doc",
"_id" : "J12f8ZEBjKOqwPdUiKBD",
"_score" : 1.3790165,
"_source" : {
"book_id" : "16",
"name" : "elasticsearch tb 114.514 est"
}
}
]
Boosting Query
Boosting Query 可以指定三个参数:positive 和 negative 以及附带的negative_boost,只要使用了 postive 剩下的两个参数也是必填的。三个参数的作用如下:
- positive:使用该参数匹配到的文档,将会提高其文档的相关性评分。
- negative:使用该参数匹配到的文档,将会降低文档的相关性评分。
- negative_boost:配置文档相关性评分降低比,大小可以配置为 0.0 - 1.0 ,如果配置为 0.3 那么就会降低到原先相关性的 30% 。
但是需要注意的是,这里的配置不能配置的过低。如果配置过低,那么就不会再被 es 认为是匹配成功的数据。并展示出来。
模板
POST %{index}/_search
{
"query": {
"boosting": {
"postive": {
${match type strcut / term type struct}
},
"negative": {
${match type struct / term type struct}
},
"negative_boost": n [0.0 - 1.0]
}
}
}
查询示例
POST books/_search
{
"query": {
"boosting": {
"positive": {
"term": {
"name": {
"value": "elasticsearch"
}
}
},
"negative": {
"match_phrase": {
"name": "elasticsearch?还是得学"
}
},
"negative_boost": 1.0
}
}
}
执行结果
"hits" : [
{
"_index" : "books",
"_type" : "_doc",
"_id" : "Jl2f8ZEBjKOqwPdUcqAZ",
"_score" : 0.4783222,
"_source" : {
"book_id" : "15",
"name" : "elasticsearch tfest1"
}
},
{
"_index" : "books",
"_type" : "_doc",
"_id" : "Il2O8ZEBjKOqwPdUM6Cf",
"_score" : 0.4783222,
"_source" : {
"book_id" : "114.514",
"name" : "elasticsearch tbest1"
}
}
······
]
dis_max Query
dis_max Query 其实是针对字段拆分分离成单独的个体,然后再进行统计。因此又被叫做 分离最大化查询 。
- disjunction (分离) 含义是:表示将同一个文档之中的字段都拆分出来,单独进行评分计算。
- max (最大化) 含义是:是将多个字段查询的得分的最大值作为最终评分返回
在所有与任一查询之中匹配成功的文档都会作为结果返回,但是只会将追加匹配的得分作为查询的评分结果进行返回 (也就是最终计算评分的时候,只会取最大的一个字段结果进行返回) 值得一提的是,其他的字段可以通过使用 tie_breaker 参数进行额外统计,这一点跟 multi_match 之中使用的 tie_breaker 统计非 max 字段的评分 tie_breaker * (multi其他字段的值) + 匹配计算值最高的字段
模板
POST books/_search
{
"query": {
"dis_max": {
"queryies": [
${match type struct / term type struct}
······
],
"tie_breaker": n # 取值 [0.1 - 1.0]
}
}
}
查询示例
POST books/_search
{
"query": {
"dis_max": {
"tie_breaker": 0.7,
"boost": 1.2,
"queries": [
{
"term": {
"book_id": "114.514"
}
},
{
"match_phrase_prefix": {
"name": "elasticsearch 学"
}
}
]
}
}
}
执行结果
"hits" : [
{
"_index" : "books",
"_type" : "_doc",
"_id" : "IF2O8ZEBjKOqwPdUEaDZ",
"_score" : 2.276544,
"_source" : {
"book_id" : "114.514",
"name" : "elasticsearch ti 114.514 est"
}
},
{
"_index" : "books",
"_type" : "_doc",
"_id" : "2",
"_score" : 1.8336091,
"_source" : {
"book_id" : "2",
"name" : "elasticsearch 学牛魔,开摆"
}
},
{
"_index" : "books",
"_type" : "_doc",
"_id" : "Il2O8ZEBjKOqwPdUM6Cf",
"_score" : 1.7779255,
"_source" : {
"book_id" : "114.514",
"name" : "elasticsearch tbest1"
}
}
]
function_score Query
function_score Query 在上文之中提供了 dis_max 之中的 tie_breaker 提到了可以通过该参数配置对 非 max 字段评分的统计参与。需要注意的是,function_score Query 只能让 keyword 类型才能实现聚合和排序操作,如果我们在filed之中指定 text 类型的字段的时候,会出现以下的报错信息:
Text fields are not optimised for operations that require per-document field data like aggregations and sorting, so these operations are disabled by default. Please use a keyword field instead. Alternatively, set fielddata=true on [DREDATE] in order to load field data by uninverting the inverted index. Note that this can use significant memory.
显然的从报错之中,就能看出来有两个解决方案:
-
可以通过该字段更改为 keyword 类型,这样就能成功实现聚合排序等操作。
-
可以在字段的mapping上做修改,开启es针对该字段的 fielddata 并且设置为 true
(fielddata的作用:本质上es会针对自身大部分的字段的值先做加载然后构建索引存放到内存之中,但是由于text也就是文本类型的数据自身的特性,这导致了es初始化的时候,不会针对 text 类型的字段构建索引,也就是 fieldata ,所以我们任何设计到 text 字段的操作但凡有类似聚合和排序的工作的时候,就会出现报错)
在除了这种方式elasticsearch之中还提供了 function_score Query 提供了以下多种计算评分的函数:
- script_score:利用自定义脚本完全自定义的控制算分的逻辑
- weight:为每一个文档设置一个简单的并且不会被规范化的权重值
- random_score:为每一个用户提供一个不同的随机评分计算方式,并对结果进行排序
- field_value_factor:使用文档字段的值来影响算分,例如可以将好评数量这个字段用来作为考虑的因数
- decay functitons:衰减函数,可以依靠某个字段作为标准,距离该值约近,那么评分越高
由于function_score Query支持了大量的评分计算函数,这里篇幅所限只写两个比较常见的算分函数。
field_value_factor
field_value_factor 使用某个文档的值来影响相关性的评分机制,一般有以下的几种使用场景:1. 需要优先查询出有优惠的商品 2. 需要优先查询有推荐的商品 3. 需要优先推送买了推广的广告
需要注意 field_value_factor 只支持 integer、long、short、byte、double、float、half_float、scaled_float数值类型 / data、data_nanos 日期类型、 boolean 布尔类型。
对于 keyword、text、嵌套、对象、地理位置的geo_point、geo_shape类型、数组类型都是无法使用的,一旦使用就会报错。
当然field_valie_factor还支持提供了以下的参数允许自由配置
- field:文档的字段
- factor:指定文档的值将会乘以设定好的因子进行统计 (默认的情况下,这个值为1)
- modifier:修改最终值的行数,其值一般采取以下的值: none、log、log1p、log2p、ln、ln1p、ln2p、square、sqrt、reciprocal [默认的情况下是none]
- missing:如果field字段本身不存在,那么就会填写missing的默认值
- boost_mode:按照原先的评分结果 * field_value_factor 直接得到的结果可能会显得太大了,es官方提供了 boost_mode 参数,帮助我们使得乘出来的方法再与_score 结合得到一个更加合理的结果。一般有以下几种取值:
- multiply:_score 再跟行数的结果直接乘 (就是原先默认会很大的值)
- replace:直接采用 函数 的结果,不再保留原先的 _score
- sum: _score 直接加上函数的结果
- avg:取 _score 和 函数结果的平均值
- min:_score + 函数结果较小值
- max:_socre + 函数结果最大值
根据上述的配置,实际上我们的计算方式计算最终的评分结果:新算分 = 匹配过程产生的旧算分 * reciprocal(1.2 * doc['price'].value)
模板:
POST ${index}/_search
{
"query": {
"function_score": {
"query": {
${term type struct}
······
}
},
"field_value_factor": {
"field": "${fieldName}",
"factor": n,
"modifier": "$[none、log、log1p、log2p、ln、ln1p、ln2p、square、sqrt、reciprocal]",
"missing": ${defaultValue}
}
}
}
(因为我的 books 索引本身没有任何一个真正意义上的整型字段,这里非常的尴尬,决定直接用 es 会给所有的文档都会指代的 _seq_no 来测试该函数功能)
POST books/_search
{
"query": {
"function_score": {
"query": { "range": {
"_seq_no": {
"gte": 10,
"lte": 12
}
} },
"field_value_factor": {
"field": "_seq_no",
"factor": 1.2,
"modifier": "reciprocal"
},
"boost_mode": "multiply"
}
}
}
执行结果
"hits" : [
{
"_index" : "books",
"_type" : "_doc",
"_id" : "IV2O8ZEBjKOqwPdUI6Cp",
"_score" : 0.08333333,
"_source" : {
"book_id" : "10",
"name" : "elasticsearch taest1"
}
},
{
"_index" : "books",
"_type" : "_doc",
"_id" : "I12O8ZEBjKOqwPdUQ6Bu",
"_score" : 0.07575757,
"_source" : {
"book_id" : "12",
"name" : "elasticsearch tcest1"
}
}
]
random_score
如果我们需要一些简单的类似于推荐的功能 (推荐部分商品,并要求在一定时间内某个用户推荐的商品是一样的),那random_score就是一种简单的实现方式。
一般来说,在使用 random_score 之中默认就会使用 _seq_no
作为我们的 fieldName
模板
POST ${index}/_search
{
"query": {
"function_score": {
"random_score": {
"seed": n, # 随机数种子,不变结果不变
"field": "${fieldName}" # _seq_np
}
}
}
}
Suggesters API
Suggesters 搜索推荐补全,该功能一般会使用在输入一部分需要自动补全的场景下使用。想要实现上述需求,我们可以通过使用Suggesters API,Suggesters API 会对我们输入的文本本身拆解为多个 token (token 就是根据规则切分文本后一个个的词,es会利用拆分出来的词,然后在索引之中进行查找)
ES常见的四种Suggester:
- Term Suggester:基于单词的纠错补全
- Phrase Suggester:基于短语的纠错补全
- Completion Suggester:自动补全单词,输入词语的前半部分,自动补全单词
- Context Suggester:基于上下文的补全提示,可以实现上下文感知推荐
基本模板
POST $(index)/_search
{
"query": {
$(term type struct / match type struct)
},
"suggest": {
$(userdefinedSuggestName -- 一次一个自己起名) : {
"text": "${queryValue -- 一般等同于 query 之中的 value [可以是 term / match]}",
"$[term / phrase / completion / context]": {
$(term/phrase/completion/context suggest struct)
}
}
}
}
Term Suggester
简单来说其实就是使用倒排索引来实现词项级别的纠错 作用在text上
通过使用该API可以协助我们实现单词的纠错、补全的功能。Term Suggester实现本质是通过 edit distance 是基于编辑距离来运作的,编辑距离的核心思想是 当前用户输入的词跟目前es倒排索引中的词项进行最近匹配。(先假设从改变一个字符开始尝试匹配,并按改变字符次数做排序条件,从少到多进行排序)
Term Suggester是最简单的单词纠错,它的纠错能力受限于当前单词本身,无法根据上下文来针对多个词项进行纠错,也不会根据上下文来纠错。
总结: termSuggester支持断句拆分词项,并针对多个词项给出推荐纠错,但纠错之间相互独立。
参数
- text:指定了需要生成建议的文本,一般是用户的输入内容
- term:指定使用的是 term suggester 纠错API
- field:指代的是建议要从哪一个字段找,换句话说就是我们的搜素本身是在搜索哪一个字段。
- suggest_mode:纠错的模式
- missing:如果索引之中存在,那么不做推荐
- popular:推荐出现频率最高的词
- always:missing对应版本,如果存在也会做推荐
- analyzer:指定分词器进行分词
- character filters:在tokenizer之前对文本进行处理 (删除字符或者替换字符)
- tokenizer:将文本按照一定的规则切割成词条 (term)。例如:keyword,不分词,ik_smart
- toeknizer filter:将tokenizer输出的词条做进一步的处理。比如:大小写转换,同义词处理,拼音处理等
- size:为每个单词都提供的最大建议数量
- sort:设置推荐结果的排序方式
- score:先按照相似性得分进行排序,然后再按照文档出现频率进行排序,最后轮到词项本身按照字母顺序进行排休
- frequency:先按照文档频率排序,然后按相似性得分进行排序,最后再按照词项本身进行排序
模板
POST $(index)/_search {
"suggest": {
$(userdefinedSuggestName -- 一次一个自己起名) : {
"text": "${queryValue -- 一般等同于 query 之中的 value [可以是 term / match]}",
"term": {
"field": $("field"),
"suggest_mode": "missing",
"analyzer": "$[character filter / tokenizer / Token filters]",
"size": n, # 一次最多推荐多少条数据
"sort": "$[score / frequency]"
}
}
}
}
示例
POST books/_search
{
/* 一般来说 query 会和 sugest 搭配使用,当然这两者也都可以单独使用
"query": {
"term": {
"book_id": "1145514"
}
},
*/
"suggest": {
"my_suggest": {
"text": "elasticseaach 1145514",
"term": {
"suggest_mode": "missing",
"field": "name"
}
}
}
}
执行效果
"hits" : {
"total" : {
"value" : 0,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"suggest" : {
"my_suggest" : [
{
"text" : "elasticseaach",
"offset" : 0,
"length" : 13,
"options" : [
{
"text" : "elasticsearch",
"score" : 0.9230769,
"freq" : 14
}
]
},
{
"text" : "1145514",
"offset" : 14,
"length" : 7,
"options" : [
{
"text" : "114.514",
"score" : 0.85714287,
"freq" : 2
}
······
显然的因为我们输入的词项本身就是错的,因此在hits之中查询的结果是空的,没有找到成功匹配的词项,但是在sugest之中,es根据我们的text获得了目标最相匹配词项作为结果进行了返回。
Phrase Sugester
简单来说其实就是使用倒排索引来实现短语级别的纠错 (作用在 text 上)
在上文之中提到的 term suggester 只能提供单单一个词项的纠错,而无法针对整个短语或者语句功能上的做纠错。而 Phrase Sugester 就是解决这个问题的 API。
Phrase Suggester在 Term Suggester 的基础上增加了一些额外的逻辑,并且因为是短语形式的建议,还会考虑到多个 term 之间的关系,比如相邻的程度、词频等。
参数
- text:指定了需要生产建议的文本,一般是用户的输入内容
- separator:如果我们text之中的字符串分隔符不是 ' ' 那么我们使用该配置将其替换为空格
- phrase:指定使用的 API 是 phrase suggester
- field:指代的是建议要从哪一个字段找,换句话说就是我们的搜素本身是在搜索哪一个字段。
- highlight:高亮被修改之后的词语 pre_tag / post_tag
- max_error:指定最多可以拼写错误的词语的个数
- confidence:其主要的作用是用来控制返回结果的数量的,如果输入的短句本身得分是 N ,那么最终返回的结果的得分需要大于 N * confidence。confidence默认的大小是 1.0
- analyzer:分词器配置
模板
POST $(index)/_search
{
"suggest": {
"${userdefinedSuggestName -- 当前这次查询自定义的名字}": {
"text": $("searchValue"),
"phrase": {
"field": "${field}",
"highlight": {
"pre_tag": "high ",
"post_tag":" light"
},
"max_error": n,
"confidence": n
}
}
}
}
示例
POST books/_search
{
"suggest": {
"suggesttype": {
"text": "elasticsaarch tbest",
"phrase": {
"field": "name",
"highlight": {
"pre_tag": "<em>",
"post_tag": "</em>"
}
}
}
}
}
查询结果
"hits" : {
"total" : {
"value" : 0,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"suggest" : {
"suggesttype" : [
{
"text" : "elasticsaarch tbest",
"offset" : 0,
"length" : 19,
"options" : [
{
"text" : "elasticsearch tbest",
"highlighted" : "<em>elasticsearch</em> tbest",
"score" : 0.11466347
},
{
"text" : "elasticsaarch taest1",
"highlighted" : "elasticsaarch <em>taest1</em>",
"score" : 0.04948662
},
······
]
······
从结果上我们就能明显的看出来实际上 phrase suggester 的具体实现是将输入的短语放到es的目标字段之中的值进行匹配 看是否存在类似于目标的值 (单个词项跟term suggester纠错是一样的),并且如果我们设置了 highlight 的时候,会使用pre_tag 和 post_tag 将被成功匹配到的 短语包围起来。
Completion Suggester
相比于 term suggester 又或者是 phrase suggester 的针对单个词项或者多个词项的纠错来说, 固然非常的好用,但问题是大部分时候我们甚至连一个错误的词都未必能够直接打出来,更多的时候是只打出来一个词项的前缀或者是一部分。在面对这种情况的时候,往往就需要使用 es 之中的 completion suggester 。
Completion Suggester 在实现的时候,会通过 分词器 (也就是我们在analyze之中所配置的) 将我们输入的文本进行分词,并去除掉一些类似于 is at 一样的没有实际含义的介词后,会将数据转为一种叫做 FST 的数据格式,并且和索引本身进行存储。而 ES 会在加载的时候,将整个 FST 结构加载带缓存之中。 FST 本身是一种前缀查询特化索引,这使得我们补全查询的效率非常高 ,使用起来很爽,但是需要我们在 mapping 之中添加 completion 字段格式 (需要注意的是 completion 仅仅支持前缀查询)
另外,在分词器不同的情况下,FST 的数据内容会发生改变,所以即使我们搜索的内容是对的,仍然存在不匹配的可能。
参数
-
prefix:用户输入的需要字符串
-
regex:使用正则表达式进行前缀匹配
-
completion:描述 completion Suggester 的配置
-
field (必需):需要提供自动补全功能的字段
-
skip_duplicates:是否跳过重复的建议
-
size:返回的补全数量
-
analyzer:分词器
-
fuzzy:是否开启模糊匹配
- fuzziness:允许的最大可以编辑的距离 (0 , 1, 2, AUTO)
- transpositions:是否考虑字符换位符 (true / false)
- min_length:引用模糊逻辑的最小字符长度
- prefix_length:不进行模糊逻辑查询的前缀长度
- unicode_aware:是否使用 Unicode 感知的编辑距离 (true / false)
-
contexts:基于上下文的过滤
-
category 对应completion字段 mapping 时,所配置的对应项
"context": { "category": ["${inputValue1Part}", "${inputValue2Part}"] }
-
-
模板
POST ${index}/_search
{
"suggest": {
"${userdefinedSuggestName -- 当前这次查询自定义的名字}": {
"prefix": "${searchValue}",
"completion": {
"field": "${completion_field}",
"skip_duplicates": "$[true / false]",
"size": n,
"analyzer": "$[character filter / tokenizer / Token filters]",
"fuzzy": {
"fuzziness": $[0, 1, 2, AUTO],
"transpositions": "$[true / false]",
"min_length": n,
"prefix_length": m,
"unicode_aware": true
},
"context": {
"category": ["${inputValue1Part}, ${inputValue2Part}"]
}
}
}
}
}
(之前的 books 索引这里无法处理该测试,只能简单新增一个)
PUT booksv2
{
"mappings": {
"properties": {
"book_id": {
"type": "keyword"
},
"name": {
"type": "text",
"analyzer": "standard"
},
"name_completion": {
"type": "completion"
},
"author": {
"type": "keyword"
},
"price": {
"type": "double"
},
"date": {
"type": "date"
},
"seq_num": {
"type": "integer"
}
}
},
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
}
}
# 添加数据 这里加了三个
POST booksv2/_doc/3
{
"book_id": "ab114516",
"name": "manba out",
"name_completion": "manba out",
"author": "kobe",
"price": 114.51,
"date": "2020-01-01",
"seq_num": 3
}
示例
POST booksv2/_search
{
"suggest": {
"ufdsuggest": {
"prefix": "what can i",
"completion": {
"field": "name_completion"
}
}
}
}
结果
"suggest" : {
"ufdsuggest" : [
{
"text" : "what can i",
"offset" : 0,
"length" : 10,
"options" : [
{
"text" : "what can i say",
"_index" : "booksv2",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"book_id" : "ab114514",
"name" : "what can i say",
"name_completion" : "what can i say",
"author" : "kobe",
"price" : 114.514,
"date" : "1979-08-23",
"seq_num" : 1
}
}
]
}
]
}
从结果来看就能显然的看到,我们在查询的时候,在 options 之中,会将其他的相关字段也显示出来。
Context Suggester
相比于 Completion Suggester 来说,Context Suggester 的扩展其实更加简单,就是单纯地增强,额外的增加了上下文的感知补全。这一点其实非常常见于 搜索引擎 以及 utools 这种快捷工具上。
而在es之中基础的支持中,主要支持以下两种类型的上下文功能:
-
Category:任一字符串的分类 (在 构建 index 的 mapping 时,需要声明 completion 类型字段)
POST ${index} { "mappings": { "properties": { "${suggest_field}": { "type": "completion", "contexts": [ { "name": "${categoryName}", "type": "category" } ] } } } }
-
Geo:地理位置信息
POST ${index} { "mappings": { "properties": { "suggest_field": { "type": "completion", "contexts": [ { "name": "${locationName}", "type": "geo", "percision": n # 地标位置精确度 } ] } } } }
参数
-
contexts:描述 context suggester 的主体
- category:类别上下文查询的主体
- context:实际上查询内容
- prefix:类别查询的指定前缀查询
- location:地理位置查询的主体
- lat:维度
- lon:经度
- precision:精度 (默认会对应 mappings 之中指定的值)
- neighbours:地理位置上的上下文
- category:类别上下文查询的主体
-
boost:设置权重
模板
POST ${index}/_search
{
"suggest": {
"${userdefinedSuggestName -- 当前这次查询自定义的名字}": {
"prefix": "${searchValue part}",
"completion": {
"field": "${suggester_filed}",
"size": n,
"contexts": {
$[
"location": {
"lat": xx.xxxx,
"lon": xx.xxxx,
"precision": n,
"neighbours": [n, m]
}
/
"category": [
{ "context": "${categoryValue1}", "boost": n },
{ "context": "${categoryValue2}", "boost": n }
······
]
]
}
}
}
}
}
为了测试es之中的 Context Suggester的功能 (根据上下文针对字符串做补全的功能)
PUT booksv3
{
"mappings": {
"properties": {
"book_id": {
"type": "keyword"
},
"name": {
"type": "text",
"analyzer": "standard"
},
"name_completion": {
"type": "completion",
"contexts": [
{
"name": "book_type",
"type": "category"
}
]
},
"author": {
"type": "keyword"
},
"price": {
"type": "double"
},
"date": {
"type": "date"
},
"seq_num": {
"type": "integer"
}
}
},
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
}
}
# 添加数据
POST booksv3/_doc/3
{
"book_id": "ab114516",
"name": "manba out",
"name_completion": {
"input": "manba out",
"contexts": {
"book_type": "manba"
}
},
"author": "kobe",
"price": "114.516",
"date": "2020-01-01",
"seq_num": 3
}
示例
POST booksv3/_search
{
"suggest": {
"usfSuggester": {
"prefix": "manba",
"completion": {
"field": "name_completion",
"contexts": {
"book_type": "kobeLanguage"
}
}
}
}
}
结果
"suggest" : {
"usfSuggester" : [
{
"text" : "manba",
······
"_source" : {
"book_id" : "ab114516",
"name" : "manba out",
"name_completion" : {
"input" : "manba out",
"contexts" : {
"book_type" : "kobeLanguage"
}
},
"author" : "kobe",
"price" : "114.516",
"date" : "2020-01-01",
"seq_num" : 3
},
"contexts" : {
"book_type" : [
"kobeLanguage"
]
}
}
······
拼音分词器 - 纠错 / 补全
在ES之中,广大的国内市场的基础上让资本也在更大的层面上对国内环境做了适配和开发,在这个背景下拼音分词器,配合实现拼音的自动补全的功能出现了。 当然因为使用拼音和汉字分词器势必比原生的要麻烦,那就存在着性能上的考量问题,针对长、数据量大的索引需要慎重考虑性能再决定是否使用索引,避免出现性能问题。
首先需要先下载一个拼音的插件 (当然的,这里也是一定要相同的版本)
https://github.com/medcl/elasticsearch-analysis-pinyin (拼音分词器)
https://github.com/infinilabs/analysis-ik (拼音插件)
如果我们配置完毕,那就可以通过 _analyze 查看实际的分词效果
GET /_analyze
{
"analyzer": "ik_max_word",
"text": "不是哥们"
}
# 可以得到拼音分词结果如下:
"tokens" : [
{
"token" : "不是",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "哥们",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 1
}
]
但如果我们只是用 ik (拼音插件 是不能满足我们的需要的 [并不支持拼音类型的自动补全等功能] ,而如果将analyzer 指定为拼音,又会失去普通的单词的处理能力,因此最好的解决方案其实是自定义一个分词器,以下就提供一个例子)
新增索引
PUT /chinese_index { "settings": { "analysis": { "analyzer": { "ik_pinyin_analyzer": { "type": "custom", "tokenizer": "ik_smart", // 指明分词器是使用 ik_smart 最大支持 "filter": ["lowercase", "pinyin_filter"] // 指定小写转换 + pinyin 分词器 } }, "filter": { "pinyin_filter": { "type": "pinyin", "keep_full_pinyin": true, "keep_joined_full_pinyin": true, "keep_original": true, "limit_first_letter_length": 16, "remove_duplicated_term": true, "none_chinese_pinyin_tokenize": false } } } }, "mappings": { "properties": { "id": { "type": "integer" }, "elementMark": { "type": "keyword" }, "name": { "type": "text", "analyzer": "ik_pinyin_analyzer", "search_analyzer": "ik_smart" }, "name_completion": { "type": "completion", "analyzer": "ik_pinyin_analyzer", "search_analyzer": "ik_smart" } } } # 填充数据待会用作查询 POST chinese_index/_doc/1 { "id": 1, "elementMark": "114519", "name": "北京是中国的首都", "name_completion": "北京是中国的首都" } POST chinese_index/_doc/2 { "id": 2, "elementMark": "114516", "name": "不是,哥们,你诗人握持", "name_completion": "不是,哥们,你诗人握持" }
添加完索引之后,我们可以通过 普通的 match 利用自定义的分词器进行查询
# 测试分词器效果 POST chinese_index/_analyze { "text": "不是,哥们,你诗人握持", "analyzer": "udf_chinese_analyzer" } # 通过分词器查询 POST chinese_index/_search { "query": { "match": { "name": "北京是或者不是中国首都,这是个是不是首都的问题" } } } "hits" : [ { "_index" : "chinese_index", "_type" : "_doc", "_id" : "5", "_score" : 10.422695, "_source" : { "id" : 5, "elementMark" : "114519", "name" : "北京是中国的首都", "name_completion" : "北京是中国的首都" } }, ······ ]
纠错功能
拼音分词器和 ik 插件,在集群脚本安装的版本,也就是7.13版本下,不支持直接包含汉字的直接纠错功能(没看过就是不支持,只要其中发生错误的内容是汉字,比如 不是,哥们两个字,如果替换为不是,歌们,再去匹配检查纠错,是无效的 【只支持拼音的纠错,可以有汉字,但是纠错只纠错拼音】)
单个词语的纠错
POST chinese_index/_search
{
"suggest": {
"new_suggester": {
"text": "beijxng",
"term": {
"field": "name"
}
}
}
}
执行结果
"suggest" : {
"new_suggester" : [
{
"text" : "beijxng",
"offset" : 0,
"length" : 7,
"options" : [
{
"text" : "beijing",
"score" : 0.85714287,
"freq" : 1
}
]
}
]
}
整个短语的纠错
POST chinese_index/_search
{
"suggest": {
"text": "beij shi zhongguo de shoudu",
"udfsuggester": {
"phrase": {
"field": "name",
"size": 1,
"gram_size": 3,
"confidence": 0,
"max_errors": 2,
"highlight": {
"pre_tag": "<em>",
"post_tag": "</em>"
},
"analyzer": "ik_pinyin_analyzer"
}
}
}
}
查询结果
"suggest" : {
"udfsuggester" : [
{
"text" : "beij shi zhongguo de shoudu",
"offset" : 0,
"length" : 27,
"options" : [
{
"text" : "bei shi zhongguo de shoudou",
"highlighted" : "<em>bei</em> shi zhongguo de <em>shoudou</em>",
"score" : 7.098891E-5
}
]
}
]
}
补全功能
利用拼音分词器的提供单个单词的自动补全功能
POST chinese_index/_search
{
"suggest": {
"udfsuggester": {
"text": "bs",
"completion": {
"field": "name_completion"
}
}
}
}
查询结果
···
"suggest" : {
"udfsuggester" : [
{
"text" : "bs",
"offset" : 0,
"length" : 2,
"options" : [
{
"text" : "不是,哥们,你诗人握持",
"_index" : "chinese_index",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"id" : 1,
"elementMark" : "114514",
"name" : "不是,哥们,你诗人握持",
"name_completion" : "不是,哥们,你诗人握持"
}
}
]
}
]
}
······
利用拼音分词器的提供短语的自动补全功能
POST chinese_index/_search
{
"query": {
"match": {
"name.pinyin": {
"query": "bs,歌们,你是忍我吃"
}
}
}
}
查询结果
······
"hits" : [
{
"_index" : "chinese_index",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.8107404,
"_source" : {
"id" : 1,
"elementMark" : "114514",
"name" : "不是,哥们,你诗人握持",
"name_completion" : "不是,哥们,你诗人握持"
}
}
]
······
聚合查询
聚合查询其实就是实现根据某些条件从es数据库之中做数据信息的统计,并最终实现统计的计算需求。这一点在更为常见的关系型数据库之中也经常出现。(首先需要注意的是,AGGS也就是聚合操作行为,在es默认的情况下并不支持 keyword / text类型字段做聚合操作,除非开了 global-ordinals(keyword) / fileddata*(text,使用fileddata es会尝试用global-ordinals来优化聚合)***)
es之中的聚合查询一般有以下几种类型:
- Metric Aggregations:提供对总和 (sum)、求平均数 (average) 计算的功能等数学运算
- Bucket Aggregations:提供针对满足某些特定条件的文档进行分组操作,例如将数据区分为 A 类的数据,将另外一部分的数据区分为 B类的数据。
- Pipleline Aggregations:对其他聚合输出的结果再次进行聚合
聚合查询的问题
在ES之中使用聚合查询的时候,因为数据存储在不同的节点的分片上,这个数据的存储状态就决定了我们使用ES进行计算的时候,在使用Terms进行聚合统计的时候,不同的分片需要提供到协调节点上进行统计这个聚合统计的方式,就会出现聚合不准确的情况。
对于max / min 这种简单的单值返回的聚合来说,显然并不会有聚合结果错误的可能性,每个节点都返回自己的计算值,协调节点针对这些其他的节点返回结果,再做max / min 就能解决,非常简单。但不是所有的聚合都这么简单就能解决的。如果我们需要对目标的数据做 avg 或者 百分比数额计算,按照该方法进行查询,那就会出现聚合不准确的问题。
假设我们有三个主分片,需要针对则三个分片 前95% 进行聚合计算
分片 1: 100ms, 200ms, 300ms, 400ms, 500ms ===> pre-95%: 475ms
分片 2: 150ms, 250ms, 350ms, 450ms, 2000ms ===> pre-95%: 1700ms
分片 3: 180ms, 280ms, 380ms, 480ms, 3000ms ===> pre-95%: 2580ms
如果我们直接计算15个的95%,那么这个值应该是2000ms。
es协调节点
如果不采用任何的措施做修正和维护,那么es实际上的计算方式其实就会变为简单的 avg 计算 475 + 1700 + 2580 / 3 = 1585。实际偏差415ms
如果使用TDigest进行统计计算的话,ES的计算机会变得更好一些,得到的大概结果应该是:2100ms左右。实际偏差会是100ms
# 普通直接查询 GET /website_logs/_search { "size": 0, "aggs": { "load_time_percentiles": { "percentiles": { "field": "page_load_time", "percents": [95] } } } } # 使用TDisgest进行调整 GET /website_logs/_search { "size": 0, "aggs": { "load_time_percentiles": { "percentiles": { "field": "page_load_time", "percents": [95], "tdigest": { "compression": 200 } } } } }
而分片将他们的统计结果交给协调节点的时候,根据他们各自的统计结果进行统计会获得以下的信息:
参数 sum_other_doc_count
sum_other_doc_count
代表着什么,是否寓意着数据会不准确?
因为Bucket下的 Term Suggester本身只返回前面十个 Bucket,也就是只会聚合计算前十个类型,那么就会有多个文档会没有被聚合结果显示,提供该参数的数据本质上是告诉你聚合结果之中还有多少数据没被统计到。
参数 doc_count_error_upper_bound
doc_count_error_upper_bound
代表着什么?
在集群es之中,因为数据的统计往往是通过多个服务来进行提供计算的,而又因为有可能数据会分布在不同的分片上在聚合计算的时候,就有可能会出现错误。因此这个值越大越说明此次的聚合计算偏离也久越大 只要该值大于0,那就代表数据存在偏差,并且实际上每个桶的实际文档数量可能比报告要高,最高会高出 doc_count_error_upper_bound 的值
如何降低偏差?
一般来说,如果想要降低 sum_other_doc_count 之中计算的数据的值大小,有以下的几种方式:
- 分片数量:可以在查询之中要求使用更多的分片 (GET _cat/indices/${index}?v [pri是主分片、rep是副分片)
通过配置shard_size
可以 - size:如果改值请求外部之中,那么就代表是限制返回结果数量
如果改值在 aggs - terms 中的 size 实际效果是限制聚合 bucket 的数量大小 (默认是 10),如
果该值越小,那么查询效果更加精确 collect_mode
:通过配置collect_mode: "breadth_first"
可以进一步提高准确度,但是会影响性能。- breadth_first:优先进行广度遍历计算,计算完上层的聚合结果之后,再进行每个桶的聚合结果计算 ()
- deepth_first:优先进行深度遍历计算,每个分支进行一次深度遍历计算,然后再进行剪切
全体结构模板
POST ${index}/_search
{
/* "query": { //如果有需要可以嵌套查询,针对查询结果聚合
$[term / match struct]
}, */
"aggs": { // 代表聚合查询
"${udf_aggs_name}": { // 自定义聚合查询1的名字
"$[max / min / sum / avg / cardinality / terms / range······]": { // 哪一类聚合
aggs body
},
"aggs": { // 子聚合查询
"${udf_son_aggs_name}": { // 子聚合查询名
"$[max / min / sum / avg / cardinality / terms / range······]": {
son aggs body
},
}
}
},
"${udf_aggs_name2}": { // 自定义聚合查询2的名字
"$[max / min / sum / avg / cardinality / terms / range······]": {
aggs body
},
······
}
},
"size": 0 // 设定返回是 0 那就只返回统计结果,而不包含 _source 等具体信息
}
Metric Aggregations
总的来说,对于Metric主要其实就包含几个常见的聚合函数级别的聚合功能和统计功能。
单值聚合 Single-Value metrics
模板
POST ${index}/_search
{
"aggs": {
"${udf_aggs_name}": {
"$[max / min / avg / sum / cardinality]": {
"field": ${field}
}
},
"${udf_aggs_name}": {
"$[max / min / avg / sum / cardinality]": {
"field": ${field}
}
}
······
},
"size": 0
}
示例
POST booksv2/_search
{
"aggs": {
"sum_aggs_elastic": {
"sum":{
"field": "price"
}
},
"avg_aggs_elastic": {
"avg": {
"field": "price"
}
}
},
"size": 0
}
// 结果 (总共三个文档: price分别为: 114.515、114.51、114.514)
"aggregations" : {
"avg_aggs_elastic" : {
"value" : 114.51299999999999
},
"sum_aggs_elastic" : {
"value" : 343.539
}
}
多值聚合计算 Multi-Value metrics
查询不同取值 cardinality
Metric Aggregations还通过了 cardinality 聚合获得出版社的数量。效果类似等同于 count(distinct(field))
需要介绍一下的是,cardinality实现去重的方式其实是通过哈希运算,也就是通过哈希值的方式(类似于哈希表结构),它并不能保证百分百的去重,但是在大数据量的情况下基本满足条件。
另外,为了满足条件,还可以配置一个额外项:precision_threshold 设置精确度,取值 [0, 40000]超过4w会默认4w。该值越小精确度越高,在官方介绍之中提到,但配置大小在 1000 及其以下的时候,误差基本被压缩在 百万级别下 5% 以内。
POST booksv2/_search
{
"aggs": {
"author_aggerate": {
"cardinality": {
"field": "author"
}
}
},
"size": 0
}
# 查询结果
"aggregations" : {
"author_aggerate" : {
"value" : 2
}
}
当然Metric还支持有 针对数据做方差、标准差、等计算;另外还支持做百分位的统计计算,比如计算 95%的文档的数据字段都大于某个值或者小于等于某个值。
百分比计算
使用percentiles可以实现针对所有数据做百分比统计,比如想要知道在书库之中80%书的价格低于多少钱、前10%的人的工资收入等百分比计算的处理上,我们可以通过使用Percentile。
模板
POST ${index}/_search
{
"aggs": {
"${udf_aggs_name}": {
/* method1:percentile 百分位数统计 */
"percentiles": {
"field": "${fieldName}" // 针对 field 字段做百分比计算
},
"percents": $[percentValue1, percentValue2, ······] // 设置百分比的点
// 默认值是 [ 1, 5, 25, 50, 75, 95, 99 ]
/* method2:percentile_rank 计算某个区间 或者 值落在总范围的百分比之中 */
"percentile_ranks": {
"field": "${fieldName}",
"values":[$(num1), $(num2)······] // 0-num1占数据百分比、0-num2占数据百分比
}
}
}
}
百分比-值区间计算 - percentiles
POST booksv2/_search
{
"aggs": {
"percent_aggs": {
"percentiles": {
"field": "price",
"percents": [50, 95, 99]
}
}
},
"size": 0
}
# 结果
"aggregations" : {
"percent_aggs" : {
"values" : {
"50.0" : 114.5145,
"95.0" : 514.114,
"99.0" : 514.114
······
区间-百分比 percentiles_ranks
POST booksv2/_search
{
"aggs": {
"percent_rank_aggs": {
"percentile_ranks": {
"field": "price",
"values": [ 200, 600 ]
}
}
},
"size": 0
}
# 结果
"aggregations" : {
"percent_rank_aggs" : {
"values" : {
"200.0" : 60.69638388388389,
"600.0" : 100.0
······
Bucket Aggregations
Bucket通过一个或者多个桶,或者多个分组,来讲数据分开成组级别然后再展示。基本完全等同于 Group by 的效果。
Terms:根据某个字段进行分组
Range、Data Range:根据用户指定的范围参数作为分组的依据进行聚合操作
Histogram、Date Histogram:可以指定间隔的区间来进行聚合操作 (例:价格在某个区间的)
Missing Aggregation:查找缺失特定字段的文档
Nested Aggregation:用于嵌套对象字段的聚合
// 嵌套对象如以下结构,在字段内部包含字段 PUT ${index} { "mappings": { "properties": { ······ "${fieldName}": { "type": "$[integer / text / double / boolean ······]", "$[properties / fields]": { "type": $[integer / text / double / boolean ······] } } ······
Filters Aggregation:使用多个过滤器创建桶
Geo Distance Aggregation:基于到指定中心点的距离创建桶
Bucket 模板
POST ${index}/_search
{
"aggs": {
// term aggregation
"${udf_term_aggs}": {
"terms": { "field": "${fieldName}", "size": n }
}
// range aggregation
"${udf_range_aggs}": {
"range": {
"field": "${field}",
"keyed": true,
"ranges": [
{ "key": "${rangeKey1}", "from": ${fromDoubleNum1}, "to": ${toDoubleNum1} },
{ "key": "${rangeKey2}", "from": ${fromDoubleNum2}, "to": ${toDoubleNum2} },
······
]
}
}
// date range
{
"${udf_buck}"
}
// date range / histogram / date histogram
// missing / nested / filters / geo distance
},
"size": 0
}
Term 字段分组
在前文之中有所介绍,因为text类型本身构建索引是倒排索引的缘故,聚合本身无法直接针对text进行处理,往往会通过对text类型的字段内部嵌套字段或者是构建其他字段实现的聚合。Term 和 Nested往往都是处理这方面问题的API
示例
POST booksv2/_search
{
"aggs": {
"my_aggs": {
"terms": {
"field": "author"
}
}
},
"size": 0
}
# 结果
"aggregations" : {
"my_aggs" : {
"doc_count_error_upper_bound" : 0, # 没有统计到聚合之中,但可能存在的潜在聚合结果
"sum_other_doc_count" : 0, # 表示这次聚合中没有统计到的文档数
"buckets" : [
{
"key" : "kobe",
"doc_count" : 3
},
{
"key" : "leticaifeng",
"doc_count" : 1
}
······
分组匹配度排序聚合 top_hits
另外配合 Term Aggregation 可以实现分组匹配度排序聚合的效果 (也可以用于 group by 去重) 另外,es之中还支持 collapse 折叠[通过查询后使用 collapse 指定groupby字段] (相比Term + top_hits性能更高,但自定义程度低) 和 上文提到的 cardinality[通过HyperLogLog++做哈希运算实现去重,并不完全保证去重] (性能最好,但是并不完全保证去重) 统计提供两种去重方式
模板
POST ${index}/_search
{
"aggs": {
"${udf_aggs_name}": {
"terms": {
"field": "${fieldName}",
"size": n # 允许显示的分组的数量
},
"aggs": {
"${son_aggs_name}": {
"top_hits": {
"size": n,
/*
"${sort}": {
"${fieldName}": {
"order": "$[asc / desc]"
}
},
"${_source}": {
"includes": $[fieldName1, filedName2,······]
},
*/
"${size}": n # 设置单个分组大小
}
}
}
}
}
}
示例
POST booksv2/_search
{
"aggs": {
"top_order_tags": {
"terms": {
"field": "author"
},
"aggs": {
"top_author_hits": {
"top_hits": {
"size": 1,
"_source": {
"includes": [ "book_id", "name", "author"]
}
}
}
}
}
},
"size": 0
}
# 结果
"aggregations" : {
"top_order_tags" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "kobe",
"doc_count" : 3,
"top_author_hits" : {
"hits" : {
"total" : {
"value" : 3,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "booksv2",
"_type" : "_doc",
"_id" : "2",
"_score" : 1.0,
"_source" : {
"author" : "kobe",
"name" : "hahahha",
"book_id" : "ab114515"
}
······
},
{
"key" : "leticiafeng",
"doc_count" : 1,
"top_author_hits" : {
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "booksv2",
"_type" : "_doc",
"_id" : "4",
"_score" : 1.0,
"_source" : {
"author" : "leticiafeng",
"name" : "It's my way",
"book_id" : 4
}
······
Range 字段查询
示例
POST booksv2/_search
{
"aggs": {
"my_aggs": {
"terms": {
"field": "price",
"range": [
{ "key": "lowPriceSku", "from": 0.00, "to": 100 },
{ "key": "highPriceSku", "from": 100.00, "to": 1000.00 }
]
}
}
},
"size": 0
}
# 结果
"aggregations" : {
"my_aggs" : {
"buckets" : [
{
"key" : "lowPriceSku",
"from" : 0.0,
"to" : 100.0,
"doc_count" : 0
},
{
"key" : "highPriceSku",
"from" : 100.0,
"to" : 1000.0,
"doc_count" : 4
}
]
}
}
Histogram Aggregation
如果我们的区间间隔大小是一致的,并且存在大量的区间需要进行聚合计算的时候,我们可以通过使用 Histogram Aggregation来帮助我们实现数据的聚合计算。这个又叫做 直方图聚合 (跟直方图一样间隔固定)
Histogram 模板
POST ${index}/_search
{
"aggs": {
"${udf_aggs}": {
"histogram": {
"field": "${fieldName}",
"interval": n // 区间间隔大小 (可以使整数也可以是小数)
}
}
},
"size": 0
}
示例
POST booksv2/_search
{
"aggs": {
"my_aggs": {
"histogram": {
"field": "price",
"interval": 500.00
}
}
},
"size": 0
}
# 结果
"aggregations" : {
"my_aggs" : {
"buckets" : [
{ "key" : 0.0, "doc_count" : 3 },
{ "key" : 500.0, "doc_count" : 1 }
]
}
}
Missing Aggregation
Missing Aggregation的作用几乎等同于 is null 判断,可以通过该API将某个字段结果为NULL的数据做统计。
示例
POST booksv4/_search
{
"aggs": {
"missing_aggs": {
"missing": {
"field": "author"
}
}
},
"size": 0
}
# 结果
"aggregations" : {
"missing_aggs" : {
"doc_count" : 1
}
}
Nested Aggregation
Nested Aggregation的意思其实很简单,就是多重聚合嵌套查询
示例
POST booksv2/_search
{
"aggs": {
"nested_aggs": {
"terms": {
"field": "author" # 先对 author 做聚合
},
"aggs": {
"son_aggs": {
"range": {
"field": "price", # 对 author 聚合结果对 price 做 range 聚合
"ranges": [
{"key": "low","from": 0,"to": 200},
{"key": "high","from": 200,"to": 600}
]
}
}
}
}
},
"size": 0
}
# 结果
"aggregations" : {
"nested_aggs" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "kobe",
"doc_count" : 3,
"son_aggs" : {
"buckets" : [
{
"key" : "low",
"from" : 0.0,
"to" : 200.0,
"doc_count" : 3
},
{
"key" : "high",
"from" : 200.0,
"to" : 600.0,
"doc_count" : 0
}
······
},
{
"key" : "leticiafeng",
"doc_count" : 1,
"son_aggs" : {
"buckets" : [
{
"key" : "low",
"from" : 0.0,
"to" : 200.0,
"doc_count" : 0
},
{
"key" : "high",
"from" : 200.0,
"to" : 600.0,
"doc_count" : 1
}
······
Filters Aggregation
在聚合粉组件结果之中我们往往针对其中一部分分组的结果是不需要的,这个时候我们可以通过使用Filters Aggregation协助我们实现该效果。
示例
# 原结果
"aggregations" : {
"my_aggs" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "kobe",
"doc_count" : 3
},
{
"key" : "leticiafeng",
"doc_count" : 1
}
]
}
}
POST booksv2/_search
{
"aggs": {
"my_aggs": {
"terms": {
"field": "author",
"include": ".*kobe",
"exclude": ".*leticiafeng"
}
}
},
"size": 0
}
# 结果
"aggregations" : {
"my_aggs" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "kobe",
"doc_count" : 3
}
]
}
}
Pipleline Aggregation
Pipeline Aggregation 可以针对其他聚合结果再进行聚合操作。为了方便理解管道聚合的效果和功能和使用场景等,这里直接先介绍几个常见的管道使用的例子。然后再介绍一下他们的使用情况等。
父子管道聚合
示例1
比如Pipleline Aggregation可以应对以下场景,找平均价格最高的作者最早出现的语录。但实际结果来看,我们如果想要将某个字段作为排序条件,就必须构建一个 son_aggs 的结果作为子聚合结果返回给父聚合之中进行处理。
POST booksv2/_search
{
"aggs": {
"publisher": {
"terms": {
"field": "author",
"size": 2,
"order": { "first_date": "asc" }
},
"aggs": {
"avg_price": {
"avg": { "field": "price" }
},
"first_date": {
"min": { "field": "date" }
}
}
}
},
"size": 0
}
# 结果
"aggregations" : {
"publisher" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "kobe",
"doc_count" : 3,
"avg_price" : {
"value" : 114.51299999999999
},
"first_date" : {
"value" : 3.042144E11,
"value_as_string" : "1979-08-23T00:00:00.000Z"
}
},
{
"key" : "leticiafeng",
"doc_count" : 1,
"avg_price" : {
"value" : 514.114
},
"first_date" : {
"value" : 4.133808E12,
"value_as_string" : "2100-12-30T00:00:00.000Z"
}
}
]
}
}
需要注意的是,上面的聚合操作相比之前的es查询或者聚合操作更为复杂,这里为了方便理解,会稍微详细一点的介绍:
- 首先 es 会先对父聚合或者说是主聚合,先对主聚合之中的操作先做聚合处理 (与关系型数据库类似的,这里只是单纯的做Group by还没有针对结果做排序的处理操作)
- 然后es针对父聚合的分组结果分别做子聚合之中的操作,比如示例之中先对他们的 price 做运算,然后对日期找最早的日期信息,并返回给父聚合做处理
- 最后es针对结果做排序处理,并最终返回到请求端。
示例2
示例2的操作是先对 所有文档按照作者做聚合,然后分开作者根据不同的语录再做一次聚合,并通过脚本语言计算 每个作者底下各个书在索引下所有数据的 seq_num (也就是将重复的语录的结果也sum起来)
POST booksv2/_search
{
"aggs": {
"author_aggs": { # 对 author 首先做聚合处理
"terms": {
"field": "author"
},
"aggs": {
"author_book_aggs": { # 子聚合对 父聚合结果做 book_id 的聚合
"terms": {
"field": "book_id"
},
"aggs": {
"sum_seq_num_price": { # 孙聚合 对子聚合的结果做统计
"sum": {
"script": "doc['seq_num'].value * doc['price'].value" # 类似算营收
}
}
}
}
}
}
},
"size": 0
}
# 结果
"aggregations" : {
"author_aggs" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "kobe",
"doc_count" : 3,
"author_book_aggs" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "ab114514",
"doc_count" : 1,
"sum_seq_num_price" : {
"value" : 114.514
}
},
{
"key" : "ab114515",
"doc_count" : 1,
"sum_seq_num_price" : {
"value" : 229.03
}
},
{
"key" : "ab114516",
"doc_count" : 1,
"sum_seq_num_price" : {
"value" : 343.53000000000003
}
}
]
}
},
{
"key" : "leticiafeng",
"doc_count" : 2,
"author_book_aggs" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "4",
"doc_count" : 2,
"sum_seq_num_price" : {
"value" : 12056.456 # 有两条数据 一条1w 一条2056.456
}
}
······
]
}
兄弟管道聚合
示例3
示例3的聚合效果是 想要将不同作者和其各自的作品总营收统计起来。另外通过兄弟管道的方式获得营收最低的作者是谁。
POST booksv2/_search
{
"aggs": {
"publisher": {
"terms": {
"field": "author" # 主聚合先对 author 做聚合处理
},
"aggs": {
"book": {
"terms": {
"field": "book_id" # 子聚合对父聚合的结果首先做 book_id 聚合
}
},
"sum_sale_money": { # 子聚合2 (book的兄弟管道聚合)
"sum": { # sum_sale_money是sum聚合结果
"script": """ if (doc['author'].value == 'leticiafeng') {
return doc['seq_num'].value * doc['price'].value * 2
} else {
return doc['seq_num'].value * doc['price'].value * 1
}""" # 通过脚本实现聚合
}
}
}
},
"min_avg_price": { # publisher的兄弟管道聚合结果,针对bucket做处理,这里用的是min
"min_bucket": {
"buckets_path": "publisher>sum_sale_money" # bucket获取的路径
}
}
},
"size":0
}
示例4
示例4的聚合结果是 根据作者分组,并在其基础上对不同作者的不同语录做聚合处理。计算不同语录的营收情况,并返回。
类似于示例三,也同样通过兄弟管道的方式对聚合计算的结果进行了统计,按照同兄弟级别的作者分组结果,统计出各个作者之中营收最低的一个语录是什么。
POST booksv2/_search
{
"aggs": {
"publisher": {
"terms": {
"field": "author" # 主聚合先对 author 做分组聚合的效果
},
"aggs": {
"book": {
"terms": {
"field": "book_id" # 针对 author 的聚合结果做 book_id 的聚合
},
"aggs": {
"sum_sale_money": {
"sum": { # 经过两次的分组聚合之后,再进行统计
"script": """ if (doc['author'].value == 'leticiafeng') {
return doc['seq_num'].value * doc['price'].value * 2
} else {
return doc['seq_num'].value * doc['price'].value * 1
}""" # 针对不同的作者做不同的聚合计算
}
}
}
},
"min_avg_price": { # 主聚合做兄弟管道的聚合,将 buckets_path 指向内部聚合桶
"min_bucket": {
"buckets_path": "book>sum_sale_money" # publisher 内部桶也能直接指定
}
}
}
}
},
"size": 0
}
兄弟管道常见写法
对于父子管道的实现,看完示例基本就能直接写了,这里更多介绍一下兄弟管道之中类似于 min_bucket 还有哪些。
- max_bucket:指明bucket_path可以直接针对bucket的结果数据做 max 聚合。
- avg_bucket:类似上文,但做的是 avg 聚合效果。
- min_bucket:类似于例子,做的其实就是 min 聚合效果
- sum_bucket:类似于上文,做的其实就是 sum 聚合效果
- bucket_script:通过脚本实现针对已经通过聚合的结果做处理操作。 (不能直接放在顶级的 aggs 之中,也就是必须放在子聚合或者孙聚合上才能生效,另外其中的参数必须是 number 类型的数据)
因为上文之中的 bucket_script 缺少例子,下面补充一个例子。
该示例实际的功能如下:
POST booksv2/_search
{
"aggs": {
"publisher": {
"terms": {
"field": "author" # 首先针对 author 做聚合处理
},
"aggs": {
"book": {
"terms": {
"field": "book_id" # 在 author 聚合的基础上,再针对 book_id 进行聚合
}
},
"sum_sale_money": {
"sum": { # 如果该语录售卖时间是 12 月,那么实际统计值翻倍
"script": """ if (doc['date'].value.monthValue == 12) {
return doc['seq_num'].value * doc['price'].value * 2
} else {
return doc['seq_num'].value * doc['price'].value * 1
}"""
}
},
"bookNum": {
"min": {
"field": "seq_num" # 提供该语录的编制
}
},
"socre_handle": {
"bucket_script": {
"buckets_path": { "bookId": "bookNum", "sum_sale": "sum_sale_money" },
# buckets_path 从兄弟 aggs 之中获得所需的参数
"script": "if( params.bookId < 100 ) { return params.sum_sale * 2 } else { return params.sum_sale_money }"
# 通过脚本进行处理
}
}
}
}
},
"size": 0
}
# 结果
"aggregations" : {
"publisher" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "kobe",
"doc_count" : 3,
"bookNum" : {
"value" : 1.0
},
"book" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "ab114514",
"doc_count" : 1
},
{
"key" : "ab114515",
"doc_count" : 1
},
{
"key" : "ab114516",
"doc_count" : 1
}
]
},
"sum_sale_money" : {
"value" : 916.104
},
"bucket_handle" : {
"value" : 1832.208
}
},
{
"key" : "leticiafeng",
"doc_count" : 2,
"bookNum" : {
"value" : 1.0
},
"book" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "4",
"doc_count" : 2
}
]
},
"sum_sale_money" : {
"value" : 24112.912
},
"bucket_handle" : {
"value" : 48225.824
}
}
]
}
}
Script 聚合
完全自定义的聚合方式,通过Script脚本自定义聚合计算处理。
结合其他aggs_type做脚本聚合
POST ${index}/_search
{
"aggs": {
"${udf_aggs}": {
"${aggs_type [terms / range]}": {
"script": {
"source": "${script (contain params)}",
"lang": "painless"
/*"${params}": {
"${param}": ${paramValue}
}*/
}
}
}
}
}
# 常见的 udf 级别脚本 (对结果处理)
1. doc['${integerField / doubleField}'].value * params.factor
2. if (doc['integerField / doubleField'] > n) {
return doc['integerField / doubleField'].value
} else {
return 0
}
# 针对数据直接做脚本归类聚合 (在什么范围归类为哪一种值)
1. if (doc['${integerValue / doubleValue}'].value < 60) { return 'F' }
else if (doc['${integerValue / doubleValue}'].value < 70) { return 'D' }
else if (doc['${integerValue / doubleValue}'].value < 80) { return 'C' }
else if (doc['${integerValue / doubleValue}'].value < 90) { return 'B' }
else { return 'A' }
分页查询
实际上elasticsearch,本质上的查询行为是通过Query + Fetch实现的,从每一个节点上都查 from + size的数量的数据,然后做全局排序之后再返回。这个流程本质上跟Mysql - Innodb做分页查询的时候,需要注意的fetchSize返回再分页结果返回有一定的相似。但是es的数据复杂度更高,按照实际计算要求,es实际需要处理的数据是 shards_number * (from + size [from之前的数据也会被查出来]) 。
从整个ES的角度来看,分页查询在ES在分页部分之中其实提供了三种分页查询的方式:
- form + size:最普通、简单的分页方式,但如果需要查询的数据本身很深,那么势必会导致性能很差的问题出现
- search after:解决了深度分页的问题,但是只能一页一页的玩下进行翻页查询,并不能直接指定跳转到某个页面上
- scroll API:通过创建数据快照的方式,进行分页查询,但存在一个问题,我们查询到刚刚新插入的文档,因为此时的快照之中并不可能存在刚刚插入的文档
from + size
使用from + size实现分页查询的问题是什么?
我们进行数据的检索的时候,es集群内部节点,会根据自身对数据相关性的评分计算,然后将自身的统计结果交给协调节点做全局排序,并最终提供。显然的,在很多的场景下,我们都需要获取相关性没有那么高的数据的内容,这一点往往在做搜索的时候非常常见。
本质上导致from size会出现深分页失败,或者说是深分页性能爆炸的主要原因其实是 from size 从各个分片查询出来之后,需要到协调节点之中做唯一 _id 的排序以及去重工作
以下提供一个简单的ES通过 from + size 实现分页搜索的简单示例:
示例
GET ${index}/_search
{
"from": n, # 搜索开始位置
"size": m, # 搜索量 n + m
"query": {
"match_all": {} # 相关查询条件
}
}
有一个需要注意的一点是,但我们将 from 设置大于 10000 或者 size 的大小设置为 10001 的时候,查询就会出现类似的以下报错
"type" : "illegal_argument_exception",
"reason" : "Result window is too large, from + size must be less than or equal to: [10000] but was [10001]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting."
按照该逻辑,我们可以通过请求将查询的数据量进行放宽:
POST ${index}/_settings
{
"index": {
"max_result_window": n // 设置一个值,限制返回的数量 (当然修改这个值势必
//造成对性能的影响,如果还是使用from + size进行查询,会导致性能问题)
}
}
search after
es面对类似于网页翻页的处理逻辑上提供了一种可以避免深分页会导致性能爆炸的方式,也就是search after,但是值得一提的是search after不支持跳转到某个具体的页面,只能一页一页的玩下进行处理。这一效果比较常见于往下滚动实现翻页的网页,比如早期手机上的百度图片。(这一点其实等同于SQL之中通过 order by id where id 实现翻页效果的查询方式)
需要注意的是 search afrer 要求sort的字段必须是唯一值 (这个要求是为了保证分页的结果不会出现重复的文档)
Search After的实际执行流程如下:
- 在sort之中指定需要进行排序的字段,然后保证其值本身的唯一性 (可以使用ID)
- 在下一次查询的时候,带上返回结果的最后一个文档进行sort查询
示例
// 首次调用search_after之前需要先通过普通的分页查询获取第一批数据
POST ${index}/_search
{
"size":n,
"query": { "$[term / match]": { term query struct / match query struct } },
"sort": [
{ "${field}": "$[asc / desc]" },
······
{ "_id", "asc" }
]
}
// 开始使用 search_after 帮助查询
POST ${index}/_search
{
"query": {
"$[term / match]": {
term query struct / match query struct
}
},
"size": n,
"sort": [
{ "${field}": "$[asc / desc]" },
······
{ "_id": "asc" }
],
"search_after": [${last_first_query_sortfiled0_value}, ······, "${last_first_query_id_num}"]
}
// 简单示例
POST books/_search
{
"query": {
"match_all": {}
},
"size": 20,
"sort": [
{
"book_id": "desc"
},
{
"_id": "asc"
}
]
}
# 查询结果
"hits" : [
{
"_index" : "books",
"_type" : "_doc",
"_id" : "8",
"_score" : null,
"_source" : {
"book_id" : "8",
"name" : "zzzzz"
},
"sort" : ["8","8"]
},
{
"_index" : "books",
"_type" : "_doc",
"_id" : "H12N8ZEBjKOqwPdU-KA-",
"_score" : null,
"_source" : {
"book_id" : "8",
"name" : "elasticsearch thest1"
},
"sort" : ["8","H12N8ZEBjKOqwPdU-KA-"]
}
]
scroll API
当然只有上面的两个实现分页的方式,明显是不足以满足我们的需求的,实际上在分页查询的时候,还有时候是需要做导出的,但由于一次全部获取很容易会导致内存溢出,所以,往往有存在需要逐批取出做导出处理的需求。而为了实现逐批导出效果的功能。需要使用scroll API来实现该功能。(但是需要注意的是,使用scroll API其实是通过快照的方式来实现导出效果的)
模板
// 首次查询
POST ${index}/_search?scroll=nm // 这里的nm指代的是 快照的有效时间 -- n个分钟
{
"query": {
"$[term / match]": {
term query struct / match query struct
}
},
"sort": { "${field}": "$[asc / desc]" },
"size": n
}
// 第二次查询
POST /_search/scroll
{
"scroll": "nm", // 这里的nm指代的是 快照的有效时间 -- n个分钟
"scroll_id": "${first_query_last_scroll_id}"
}
// 查询示例 (首次查询)
POST books/_search
{
"query": {
"match_all": {}
},
"sort": { "_id": "desc" },
"size": 2
}
// 第二次查询
POST _search/scroll
{
"scroll": "5m",
"scroll_id": "${first_query_last_scroll_id}"
}
// 查询结果
"hits" : [
{
"_index" : "books",
"_type" : "_doc",
"_id" : "Jl2f8ZEBjKOqwPdUcqAZ",
······
"_source" : {
"book_id" : "15",
"name" : "elasticsearch tfest1"
},
"sort" : [
"Jl2f8ZEBjKOqwPdUcqAZ"
]
},
{
"_index" : "books",
"_type" : "_doc",
"_id" : "JV2f8ZEBjKOqwPdUW6Cb",
······
"_source" : {
"book_id" : "14",
"name" : "elasticsearch teest1"
},
"sort" : [
"JV2f8ZEBjKOqwPdUW6Cb"
······
Point In Time
Point In Time跟Scroll API类似的,都是通过构建一个临时的数据存储的方式来记录一段数据的内容,然后通过该存储结果返回的进行查询从而实现分页查询的效果。但是Point In Time和scroll API的功能上也是几乎一致的,因为他们两者都遵守了通过快照来解决问题,那么就会无法避开新插入的文档无法被检索到的问题。
那么使用 Point In Time和scroll API的区别是什么呢?
- 从实现的原理上来看,scroll API的实现方式其实是通过构建快照的方式来实现的分页查询效果,但是 Point In Time 的而是通过构建视图的方式来实现的分页。
- scroll API 支持大量的数据批量处理,但是消耗的资源很多;point In time的实现方式相比于scroll API (甚至需要在一定加锁处理,影响并发) 消耗的资源更少,并且支持排序和过滤一起使用
- scroll API 支持的是单词的消费,而不是重复的消费作用,并且需要注意的是scroll本身也不是可以传递给别人可以使用的;但是 point in time 是视图可以允许其他的请求使用当前创建出来的视图进行使用
不管是PIT还是Scroll对应的视图其实都不是数据的副本,更像是一次的记录对数据的记录。
虽然说他们本质上是有不少的区别的,但是找 Point In Time 在 ES 7 之中出现之后,基本就不推荐使用原先的 scroll API了
在使用Point In Time的时候,还有一件事情是需要注意的,我们需要使用 from + size / search after的方式,到视图之中进行查询得到结果。这里需要注意的是就跟上文提到的一样 from + size 因为在协调节点上进行排序和去重,一样会造成性能的损耗。所以在PIT之中,还是需要使用 search after 帮助排序去重。需要注意的一点是:pit提供的视图会对所有的文档偷偷藏一个关键的字段 _shard_doc
提供了一个隐蔽的全局唯一项,用来保证了分页结果的去重效果。
模板
POST ${index}/_pit?keep_alive=nm // 这里的nm指代的是 视图的有效时间 -- n个分钟
// 结果
{
"id": "${generate view id}"
}
// 使用 pit 视图id进行查询
POST /_search
{
"size": n,
"query": {
"$[ term / match ]": {
term query struct / match query struct
}
},
"pit": {
"id": "${generate pit id}",
"keep_alive": "nm" // 同样的这里也是视图的有效时间 (会重置之前的时间)
},
"sort": [
{ "${field}": "$[asc / desc]" },
······
{ "_id": "asc" }
]
}
// 需要知道的是,如果不再需要pid视图,推荐使用请求直接删除 (占用资源)
DELETE /_pit
{
"id": "${generate view id}"
}
示例
POST books/_pit?keep_alive=10m
// 结果
{
"id" : "85ezAwIFYm9va3MWLUdScy05YmdSeG1Hc1pBX1ZyNFZ3ZwAWd2JTNXkwakRTR3E5TFUwSklkZER1dwAAAAAAAAAV7RZhclZTOExWcFNueXUySy12Y1N6TU1RAAVib29rcxYtR1JzLTliZ1J4bUdzWkFfVnI0VndnARZFbmJWTEhNTlN1dTZuS2o2alp1SnpnAAAAAAAAABtXFllvemxmMHhBUTVtZVdSNkpWUk10a3cAARYtR1JzLTliZ1J4bUdzWkFfVnI0VndnAAA="
}
// 使用pid进行首次查询
POST /_search
{
"query": {
"match_all": {}
},
"size": 2,
"pit": {
"id": "85ezAwIFYm9va3MWLUdScy05YmdSeG1Hc1pBX1ZyNFZ3ZwAWd2JTNXkwakRTR3E5TFUwSklkZER1dwAAAAAAAAAV7RZhclZTOExWcFNueXUySy12Y1N6TU1RAAVib29rcxYtR1JzLTliZ1J4bUdzWkFfVnI0VndnARZFbmJWTEhNTlN1dTZuS2o2alp1SnpnAAAAAAAAABtXFllvemxmMHhBUTVtZVdSNkpWUk10a3cAARYtR1JzLTliZ1J4bUdzWkFfVnI0VndnAAA=",
"keep_alive": "10m"
},
"sort": [
{
"book_id": { "order": "desc" } // 这里不需要有一个field绝对的唯一
}
]
}
// 查询结果
"hits" : [
{
"_index" : "books",
"_type" : "_doc",
"_id" : "H12N8ZEBjKOqwPdU-KA-",
"_score" : null,
"_source" : {
"book_id" : "8",
"name" : "elasticsearch thest1"
},
"sort" : [
"8",
4294967300
]
},
{
"_index" : "books",
"_type" : "_doc",
"_id" : "8",
"_score" : null,
"_source" : {
"book_id" : "8",
"name" : "zzzzz"
},
"sort" : [
"8",
4294967302
]
}
]
// 第二次配合 search after 进行查询
POST /_search
{
"size": 2,
"pit": {
"id": "85ezAwIFYm9va3MWLUdScy05YmdSeG1Hc1pBX1ZyNFZ3ZwAWd2JTNXkwakRTR3E5TFUwSklkZER1dwAAAAAAAAAV7RZhclZTOExWcFNueXUySy12Y1N6TU1RAAVib29rcxYtR1JzLTliZ1J4bUdzWkFfVnI0VndnARZFbmJWTEhNTlN1dTZuS2o2alp1SnpnAAAAAAAAABtXFllvemxmMHhBUTVtZVdSNkpWUk10a3cAARYtR1JzLTliZ1J4bUdzWkFfVnI0VndnAAA=",
"keep_alive": "10m"
},
"search_after": [
"8",
4294967302
],
"sort": [
{
"book_id": { "order": "desc" } // 这里不需要有一个field绝对的唯一
}
]
}
// 第二次查询结果
"hits" : [
{
"_index" : "books",
"_type" : "_doc",
"_id" : "7",
"_score" : null,
"_source" : {
"book_id" : "6",
"name" : "不是,哥们,你认真的? 7 - 6"
},
"sort" : [
"6",
2
]
},
{
"_index" : "books",
"_type" : "_doc",
"_id" : "6",
"_score" : null,
"_source" : {
"book_id" : "6",
"name" : "manba out!"
},
"sort" : [
"6",
6
]
}
]
// 删除PID
DELETE /_pit
{
"id": "85ezAwIFYm9va3MWLUdScy05YmdSeG1Hc1pBX1ZyNFZ3ZwAWd2JTNXkwakRTR3E5TFUwSklkZER1dwAAAAAAAAAV7RZhclZTOExWcFNueXUySy12Y1N6TU1RAAVib29rcxYtR1JzLTliZ1J4bUdzWkFfVnI0VndnARZFbmJWTEhNTlN1dTZuS2o2alp1SnpnAAAAAAAAABtXFllvemxmMHhBUTVtZVdSNkpWUk10a3cAARYtR1JzLTliZ1J4bUdzWkFfVnI0VndnAAA="
}