失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > Elasticsearch顶尖高手系列:高手进阶篇(一)

Elasticsearch顶尖高手系列:高手进阶篇(一)

时间:2021-04-01 02:19:30

相关推荐

Elasticsearch顶尖高手系列:高手进阶篇(一)

Elasticsearch顶尖高手系列:高手进阶篇(一)

第1-29节01_搜索_IT技术论坛案例背景介绍02_搜索_term filter03_搜索_filter原理(bitset机制与caching机制)04_搜索_bool组合多个filter条件05_搜索_terms搜索06_搜索_filter range范围过滤07_搜索_operator、minimum_should_match控制精度08_搜索_term+bool实现的multiword09_搜索_boost权重10_搜索_多shard下relevance score不准确11_搜索_dis_max实现best fields多字段搜索12_搜索_tie_breaker优化dis_max搜索13_搜索_multi_match+best-fields策略14_搜索_multi_match+most fiels策略15_搜索_most_fields策略进行cross-fields搜索16_搜索_copy_to定制组合field解决cross-fields搜索弊端17_搜索_cross-fiels解决搜索弊端18_搜索_match_phrase搜索19_搜索_match_phrase的slop参数实现近似匹配20_搜索_match_phrase和match召回率与精准度的平衡21_搜索_match_phrase的rescore近似匹配优化22_搜索_prefix前缀、wildcard通配符、regexp正则搜索等技术23_搜索_match_phrase_prefix搜索推荐24_搜索_ngram分词机制实现index-time搜索推荐25_搜索_深入揭秘TF&IDF算法以及向量空间模型算法26_搜索_深入揭秘lucene的相关度分数算法27_搜索_四种常见的相关度分数优化方法28_搜索_function_score自定义相关度分数29_搜索_fuzzy误拼写模糊搜索、_delete_by_query第30-58节30_IK中文分词31_IK中文分词_配置文件以及自定义词库32_IK中文分词_修改IK分词器源码来基于mysql热更新词库33_聚合分析_bucket与metric两个核心概念的讲解34_聚合分析_aggs+terms35_聚合分析_aggs+terms+avg统计每种颜色电视平均价格36_聚合分析_bucket嵌套实现颜色+品牌的多层下钻分析37_聚合分析_统计每种颜色电视最大最小价格38_聚合分析_hitogram按价格区间统计电视销量和销售额39_聚合分析_date hitogram之统计每月电视销量40_聚合分析_date_histogram统计每季度每个品牌的销售额41_聚合分析_query+filter:统计指定品牌下每个颜色的销量42_聚合分析_global bucket:单个品牌与所有品牌销量对比43_聚合分析_filter+aggs:统计价格大于1200的电视平均价格44_聚合分析_bucket filter:统计牌品最近一个月的平均价格45_聚合分析_排序:按每种颜色的平均销售额降序排序46_聚合分析_颜色+品牌下钻分析时按最深层metric进行排序47_聚合分析_易并行聚合算法,三角选择原则,近似聚合算法48_聚合分析_cardinality去重每月销售品牌数量统计==49_聚合分析_cardinality算法之优化内存开销以及HLL算法==50_聚合分析_percentiles百分比算法以及网站访问时延统计51_聚合分析_percentiles rank以及网站访问时延SLA统计52_聚合分析_doc value正排索引的聚合53_聚合分析_doc value机制内核级原理深入探秘54_聚合分析_string field聚合实验以及fielddata55_聚合分析_fielddata内存控制以及circuit breaker断路器56_聚合分析_fielddata filter的细粒度内存加载控制57_聚合分析_fielddata预加载机制以及序号标记预加载==58_聚合分析_海量bucket优化机制:从深度优先到广度优先==

第1-29节

01_搜索_IT技术论坛案例背景介绍

课程大纲

IT技术论坛,案例背景

IT技术论坛中相关的数据,会在es中建立数据的索引

深度讲解搜索,数据分析,数据建模

特色:纯手工画图剖析各种原理,纯实战驱动讲解各种知识点,知识体系的细致和完整

怎么实战驱动?

核心知识篇,上半季,我们也是纯实战驱动,但是之前没有一个统一的案例背景

IT技术论坛,发各种IT技术的帖子:一种是自己研究了一个技术,就发出来一些研究心得;自己遇到了问题,发个帖子问一问。帖子会有人回复,还会有人浏览。当然了,还有一些论坛会提供简单社交的一个功能,互相加好友,互相关注,互相点赞,之类的。

在IT技术论坛的背景下,去开发一些跟案例背景相关的搜索或者数据分析,或者数据建模的需求,用每一讲学到的知识点,去接解决一些问题

既可以学到知识和技术,也可以在真实的案例背景中练习一下学到的东西

02_搜索_term filter

课程大纲

1、根据用户ID、是否隐藏、帖子ID、发帖日期来搜索帖子

(1)插入一些测试帖子数据

POST /forum/article/_bulk{"index": {"_id": 1 }}{"articleID" : "XHDK-A-1293-#fJ3", "userID" : 1, "hidden": false, "postDate": "-01-01" }{"index": {"_id": 2 }}{"articleID" : "KDKE-B-9947-#kL5", "userID" : 1, "hidden": false, "postDate": "-01-02" }{"index": {"_id": 3 }}{"articleID" : "JODL-X-1937-#pV7", "userID" : 2, "hidden": false, "postDate": "-01-01" }{"index": {"_id": 4 }}{"articleID" : "QQPX-R-3956-#aD8", "userID" : 2, "hidden": true, "postDate": "-01-02" }

初步来说,就先搞4个字段,因为整个es是支持json document格式的,所以说扩展性和灵活性非常之好。如果后续随着业务需求的增加,要在document中增加更多的field,那么我们可以很方便的随时添加field。但是如果是在关系型数据库中,比如mysql,我们建立了一个表,现在要给表中新增一些column,那就很坑爹了,必须用复杂的修改表结构的语法去执行。而且可能对系统代码还有一定的影响。

GET /forum/_mapping/article{"forum": {"mappings": {"article": {"properties": {"articleID": {"type": "text","fields": {"keyword": {"type": "keyword","ignore_above": 256}}},"hidden": {"type": "boolean"},"postDate": {"type": "date"},"userID": {"type": "long"}}}}}}

现在es 5.2版本,type=text,默认会设置两个field,一个是field本身,比如articleID,就是分词的;还有一个的话,就是field.keyword,articleID.keyword,默认不分词,会最多保留256个字符

(2)根据用户ID搜索帖子

GET /forum/article/_search{"query" : {"constant_score" : {"filter" : {"term" : {"userID" : 1}}}}}

term filter/query:对搜索文本不分词,直接拿去倒排索引中匹配,你输入的是什么,就去匹配什么

比如说,如果对搜索文本进行分词的话,“helle world” --> “hello”和“world”,两个词分别去倒排索引中匹配

term,“hello world” --> “hello world”,直接去倒排索引中匹配“hello world”

(3)搜索没有隐藏的帖子

GET /forum/article/_search{"query" : {"constant_score" : {"filter" : {"term" : {"hidden" : false}}}}}

(4)根据发帖日期搜索帖子

GET /forum/article/_search{"query" : {"constant_score" : {"filter" : {"term" : {"postDate" : "-01-01"}}}}}

(5)根据帖子ID搜索帖子

GET /forum/article/_search{"query" : {"constant_score" : {"filter" : {"term" : {"articleID" : "XHDK-A-1293-#fJ3"}}}}}

{"took": 1,"timed_out": false,"_shards": {"total": 5,"successful": 5,"failed": 0},"hits": {"total": 0,"max_score": null,"hits": []}}

GET /forum/article/_search{"query" : {"constant_score" : {"filter" : {"term" : {"articleID.keyword" : "XHDK-A-1293-#fJ3"}}}}}

{"took": 2,"timed_out": false,"_shards": {"total": 5,"successful": 5,"failed": 0},"hits": {"total": 1,"max_score": 1,"hits": [{"_index": "forum","_type": "article","_id": "1","_score": 1,"_source": {"articleID": "XHDK-A-1293-#fJ3","userID": 1,"hidden": false,"postDate": "-01-01"}}]}}

articleID.keyword,是es最新版本内置建立的field,就是不分词的。所以一个articleID过来的时候,会建立两次索引,一次是自己本身,是要分词的,分词后放入倒排索引;另外一次是基于articleID.keyword,不分词,保留256个字符最多,直接一个字符串放入倒排索引中。

所以term filter,对text过滤,可以考虑使用内置的field.keyword来进行匹配。但是有个问题,默认就保留256个字符。所以尽可能还是自己去手动建立索引,指定not_analyzed吧。在最新版本的es中,不需要指定not_analyzed也可以,将type=keyword即可。

(6)查看分词

GET /forum/_analyze{"field": "articleID","text": "XHDK-A-1293-#fJ3"}

默认是analyzed的text类型的field,建立倒排索引的时候,就会对所有的articleID分词,分词以后,原本的articleID就没有了,只有分词后的各个word存在于倒排索引中。

term,是不对搜索文本分词的,XHDK-A-1293-#fJ3 --> XHDK-A-1293-#fJ3;但是articleID建立索引的时候,XHDK-A-1293-#fJ3 --> xhdk,a,1293,fj3

(7)重建索引

DELETE /forumPUT /forum{"mappings": {"article": {"properties": {"articleID": {"type": "keyword"}}}}}

POST /forum/article/_bulk{"index": {"_id": 1 }}{"articleID" : "XHDK-A-1293-#fJ3", "userID" : 1, "hidden": false, "postDate": "-01-01" }{"index": {"_id": 2 }}{"articleID" : "KDKE-B-9947-#kL5", "userID" : 1, "hidden": false, "postDate": "-01-02" }{"index": {"_id": 3 }}{"articleID" : "JODL-X-1937-#pV7", "userID" : 2, "hidden": false, "postDate": "-01-01" }{"index": {"_id": 4 }}{"articleID" : "QQPX-R-3956-#aD8", "userID" : 2, "hidden": true, "postDate": "-01-02" }

(8)重新根据帖子ID和发帖日期进行搜索

GET /forum/article/_search{"query" : {"constant_score" : {"filter" : {"term" : {"articleID" : "XHDK-A-1293-#fJ3"}}}}}

2、梳理学到的知识点

(1)term filter:根据exact value进行搜索,数字、boolean、date天然支持

(2)text需要建索引时指定为not_analyzed,才能用term query

(3)相当于SQL中的单个where条件

select *

from forum.article

where articleID=‘XHDK-A-1293-#fJ3’

03_搜索_filter原理(bitset机制与caching机制)

课程大纲

(1)在倒排索引中查找搜索串,获取document list

date来举例

filter:-02-02

到倒排索引中一找,发现-02-02对应的document list是doc2,doc3

(2)为每个在倒排索引中搜索到的结果,构建一个bitset,[0, 0, 0, 1, 0, 1]

非常重要

使用找到的doc list,构建一个bitset,就是一个二进制的数组,数组每个元素都是0或1,用来标识一个doc对一个filter条件是否匹配,如果匹配就是1,不匹配就是0

[0, 1, 1]

doc1:不匹配这个filter的

doc2和do3:是匹配这个filter的

尽可能用简单的数据结构去实现复杂的功能,可以节省内存空间,提升性能

(3)遍历每个过滤条件对应的bitset,优先从最稀疏的开始搜索,查找满足所有条件的document

后面会讲解,一次性其实可以在一个search请求中,发出多个filter条件,每个filter条件都会对应一个bitset

遍历每个filter条件对应的bitset,先从最稀疏的开始遍历

[0, 0, 0, 1, 0, 0]:比较稀疏

[0, 1, 0, 1, 0, 1]

先遍历比较稀疏的bitset,就可以先过滤掉尽可能多的数据

遍历所有的bitset,找到匹配所有filter条件的doc

请求:filter,postDate=-01-01,userID=1

postDate: [0, 0, 1, 1, 0, 0]

userID: [0, 1, 0, 1, 0, 1]

遍历完两个bitset之后,找到的匹配所有条件的doc,就是doc4

就可以将document作为结果返回给client了

(4)caching bitset,跟踪query,在最近256个query中超过一定次数的过滤条件,缓存其bitset。对于小segment(<1000,或<3%),不缓存bitset。

比如postDate=-01-01,[0, 0, 1, 1, 0, 0],可以缓存在内存中,这样下次如果再有这个条件过来的时候,就不用重新扫描倒排索引,反复生成bitset,可以大幅度提升性能。

在最近的256个filter中,有某个filter超过了一定的次数,次数不固定,就会自动缓存这个filter对应的bitset

segment(上半季),filter针对小segment获取到的结果,可以不缓存,segment记录数<1000,或者segment大小<index总大小的3%

segment数据量很小,此时哪怕是扫描也很快;segment会在后台自动合并,小segment很快就会跟其他小segment合并成大segment,此时就缓存也没有什么意义,segment很快就消失了

针对一个小segment的bitset,[0, 0, 1, 0]

filter比query的好处就在于会caching,但是之前不知道caching的是什么东西,实际上并不是一个filter返回的完整的doc list数据结果。而是filter bitset缓存起来。下次不用扫描倒排索引了。

(5)filter大部分情况下来说,在query之前执行,先尽量过滤掉尽可能多的数据

query:是会计算doc对搜索条件的relevance score,还会根据这个score去排序

filter:只是简单过滤出想要的数据,不计算relevance score,也不排序

(6)如果document有新增或修改,那么cached bitset会被自动更新

postDate=-01-01,[0, 0, 1, 0]

document,id=5,postDate=-01-01,会自动更新到postDate=-01-01这个filter的bitset中,全自动,缓存会自动更新。postDate=-01-01的bitset,[0, 0, 1, 0, 1]

document,id=1,postDate=-12-30,修改为postDate--01-01,此时也会自动更新bitset,[1, 0, 1, 0, 1]

(7)以后只要是有相同的filter条件的,会直接来使用这个过滤条件对应的cached bitset

04_搜索_bool组合多个filter条件

课程大纲

1、搜索发帖日期为-01-01,或者帖子ID为XHDK-A-1293-#fJ3的帖子,同时要求帖子的发帖日期绝对不为-01-02

select *

from forum.article

where (post_date=‘-01-01’ or article_id=‘XHDK-A-1293-#fJ3’)

and post_date!=‘-01-02’

GET /forum/article/_search{"query": {"constant_score": {"filter": {"bool": {"should": [{"term": {"postDate": "-01-01" }},{"term": {"articleID": "XHDK-A-1293-#fJ3"}}],"must_not": {"term": {"postDate": "-01-02"}}}}}}}

must,should,must_not,filter:必须匹配,可以匹配其中任意一个即可,必须不匹配

2、搜索帖子ID为XHDK-A-1293-#fJ3,或者是帖子ID为JODL-X-1937-#pV7而且发帖日期为-01-01的帖子

select *

from forum.article

where article_id=‘XHDK-A-1293-#fJ3’

or (article_id=‘JODL-X-1937-#pV7’ and post_date=‘-01-01’)

GET /forum/article/_search {"query": {"constant_score": {"filter": {"bool": {"should": [{"term": {"articleID": "XHDK-A-1293-#fJ3"}},{"bool": {"must": [{"term":{"articleID": "JODL-X-1937-#pV7"}},{"term": {"postDate": "-01-01"}}]}}]}}}}}

3、梳理学到的知识点

(1)bool:must,must_not,should,组合多个过滤条件

(2)bool可以嵌套

(3)相当于SQL中的多个and条件:当你把搜索语法学好了以后,基本可以实现部分常用的sql语法对应的功能

05_搜索_terms搜索

课程大纲

term: {“field”: “value”}

terms: {“field”: [“value1”, “value2”]}

sql中的in

select * from tbl where col in (“value1”, “value2”)

1、为帖子数据增加tag字段

POST /forum/article/_bulk{"update": {"_id": "1"} }{"doc" : {"tag" : ["java", "hadoop"]} }{"update": {"_id": "2"} }{"doc" : {"tag" : ["java"]} }{"update": {"_id": "3"} }{"doc" : {"tag" : ["hadoop"]} }{"update": {"_id": "4"} }{"doc" : {"tag" : ["java", "elasticsearch"]} }

2、搜索articleID为KDKE-B-9947-#kL5或QQPX-R-3956-#aD8的帖子,搜索tag中包含java的帖子

GET /forum/article/_search {"query": {"constant_score": {"filter": {"terms": {"articleID": ["KDKE-B-9947-#kL5","QQPX-R-3956-#aD8"]}}}}}

GET /forum/article/_search{"query" : {"constant_score" : {"filter" : {"terms" : {"tag" : ["java"]}}}}}

"took": 2,"timed_out": false,"_shards": {"total": 5,"successful": 5,"failed": 0},"hits": {"total": 3,"max_score": 1,"hits": [{"_index": "forum","_type": "article","_id": "2","_score": 1,"_source": {"articleID": "KDKE-B-9947-#kL5","userID": 1,"hidden": false,"postDate": "-01-02","tag": ["java"]}},{"_index": "forum","_type": "article","_id": "4","_score": 1,"_source": {"articleID": "QQPX-R-3956-#aD8","userID": 2,"hidden": true,"postDate": "-01-02","tag": ["java","elasticsearch"]}},{"_index": "forum","_type": "article","_id": "1","_score": 1,"_source": {"articleID": "XHDK-A-1293-#fJ3","userID": 1,"hidden": false,"postDate": "-01-01","tag": ["java","hadoop"]}}]}}

3、优化搜索结果,仅仅搜索tag只包含java的帖子

POST /forum/article/_bulk{"update": {"_id": "1"} }{"doc" : {"tag_cnt" : 2} }{"update": {"_id": "2"} }{"doc" : {"tag_cnt" : 1} }{"update": {"_id": "3"} }{"doc" : {"tag_cnt" : 1} }{"update": {"_id": "4"} }{"doc" : {"tag_cnt" : 2} }GET /forum/article/_search{"query": {"constant_score": {"filter": {"bool": {"must": [{"term": {"tag_cnt": 1}},{"terms": {"tag": ["java"]}}]}}}}}

[“java”, “hadoop”, “elasticsearch”]

4、学到的知识点梳理

(1)terms多值搜索

(2)优化terms多值搜索的结果

(3)相当于SQL中的in语句

06_搜索_filter range范围过滤

课程大纲

1、为帖子数据增加浏览量的字段

POST /forum/article/_bulk{"update": {"_id": "1"} }{"doc" : {"view_cnt" : 30} }{"update": {"_id": "2"} }{"doc" : {"view_cnt" : 50} }{"update": {"_id": "3"} }{"doc" : {"view_cnt" : 100} }{"update": {"_id": "4"} }{"doc" : {"view_cnt" : 80} }2、搜索浏览量在30~60之间的帖子GET /forum/article/_search{"query": {"constant_score": {"filter": {"range": {"view_cnt": {"gt": 30,"lt": 60}}}}}}

gte

lte

3、搜索发帖日期在最近1个月的帖子

POST /forum/article/_bulk{"index": {"_id": 5 }}{"articleID" : "DHJK-B-1395-#Ky5", "userID" : 3, "hidden": false, "postDate": "-03-01", "tag": ["elasticsearch"], "tag_cnt": 1, "view_cnt": 10 }GET /forum/article/_search {"query": {"constant_score": {"filter": {"range": {"postDate": {"gt": "-03-10||-30d"}}}}}}GET /forum/article/_search {"query": {"constant_score": {"filter": {"range": {"postDate": {"gt": "now-30d"}}}}}}

4、梳理一下学到的知识点

(1)range,sql中的between,或者是>=1,<=1

(2)range做范围过滤

07_搜索_operator、minimum_should_match控制精度

课程大纲

1、为帖子数据增加标题字段

POST /forum/article/_bulk{"update": {"_id": "1"} }{"doc" : {"title" : "this is java and elasticsearch blog"} }{"update": {"_id": "2"} }{"doc" : {"title" : "this is java blog"} }{"update": {"_id": "3"} }{"doc" : {"title" : "this is elasticsearch blog"} }{"update": {"_id": "4"} }{"doc" : {"title" : "this is java, elasticsearch, hadoop blog"} }{"update": {"_id": "5"} }{"doc" : {"title" : "this is spark blog"} }

2、搜索标题中包含java或elasticsearch的blog

这个,就跟之前的那个term query,不一样了。不是搜索exact value,是进行full text全文检索。

match query,是负责进行全文检索的。当然,如果要检索的field,是not_analyzed类型的,那么match query也相当于term query。

GET /forum/article/_search

{

“query”: {

“match”: {

“title”: “java elasticsearch”

}

}

}

3、搜索标题中包含java和elasticsearch的blog

搜索结果精准控制的第一步:灵活使用and关键字,如果你是希望所有的搜索关键字都要匹配的,那么就用and,可以实现单纯match query无法实现的效果

GET /forum/article/_search{"query": {"match": {"title": {"query": "java elasticsearch","operator": "and"}}}}

4、搜索包含java,elasticsearch,spark,hadoop,4个关键字中,至少3个的blog

控制搜索结果的精准度的第二步:指定一些关键字中,必须至少匹配其中的多少个关键字,才能作为结果返回

GET /forum/article/_search{"query": {"match": {"title": {"query": "java elasticsearch spark hadoop","minimum_should_match": "75%"}}}}

5、用bool组合多个搜索条件,来搜索title

GET /forum/article/_search{"query": {"bool": {"must":{"match": {"title": "java" }},"must_not": {"match": {"title": "spark" }},"should": [{"match": {"title": "hadoop" }},{"match": {"title": "elasticsearch" }}]}}}

6、bool组合多个搜索条件,如何计算relevance score

must和should搜索对应的分数,加起来,除以must和should的总数

排名第一:java,同时包含should中所有的关键字,hadoop,elasticsearch

排名第二:java,同时包含should中的elasticsearch

排名第三:java,不包含should中的任何关键字

should是可以影响相关度分数的

must是确保说,谁必须有这个关键字,同时会根据这个must的条件去计算出document对这个搜索条件的relevance score

在满足must的基础之上,should中的条件,不匹配也可以,但是如果匹配的更多,那么document的relevance score就会更高

{"took": 6,"timed_out": false,"_shards": {"total": 5,"successful": 5,"failed": 0},"hits": {"total": 3,"max_score": 1.3375794,"hits": [{"_index": "forum","_type": "article","_id": "4","_score": 1.3375794,"_source": {"articleID": "QQPX-R-3956-#aD8","userID": 2,"hidden": true,"postDate": "-01-02","tag": ["java","elasticsearch"],"tag_cnt": 2,"view_cnt": 80,"title": "this is java, elasticsearch, hadoop blog"}},{"_index": "forum","_type": "article","_id": "1","_score": 0.53484553,"_source": {"articleID": "XHDK-A-1293-#fJ3","userID": 1,"hidden": false,"postDate": "-01-01","tag": ["java","hadoop"],"tag_cnt": 2,"view_cnt": 30,"title": "this is java and elasticsearch blog"}},{"_index": "forum","_type": "article","_id": "2","_score": 0.19856805,"_source": {"articleID": "KDKE-B-9947-#kL5","userID": 1,"hidden": false,"postDate": "-01-02","tag": ["java"],"tag_cnt": 1,"view_cnt": 50,"title": "this is java blog"}}]}}

7、搜索java,hadoop,spark,elasticsearch,至少包含其中3个关键字

默认情况下,should是可以不匹配任何一个的,比如上面的搜索中,this is java blog,就不匹配任何一个should条件

但是有个例外的情况,如果没有must的话,那么should中必须至少匹配一个才可以

比如下面的搜索,should中有4个条件,默认情况下,只要满足其中一个条件,就可以匹配作为结果返回

但是可以精准控制,should的4个条件中,至少匹配几个才能作为结果返回

GET /forum/article/_search{"query": {"bool": {"should": [{"match": {"title": "java" }},{"match": {"title": "elasticsearch" }},{"match": {"title": "hadoop" }},{"match": {"title": "spark" }}],"minimum_should_match": 3 }}}

梳理一下学到的知识点

1、全文检索的时候,进行多个值的检索,有两种做法,match query;should

2、控制搜索结果精准度:and operator,minimum_should_match

08_搜索_term+bool实现的multiword

课程大纲

1、普通match如何转换为term+should

{"match": {"title": "java elasticsearch"}}

使用诸如上面的match query进行多值搜索的时候,es会在底层自动将这个match query转换为bool的语法

bool should,指定多个搜索词,同时使用term query

{"bool": {"should": [{"term": {"title": "java" }},{"term": {"title": "elasticsearch" }}]}}

2、and match如何转换为term+must

{"match": {"title": {"query": "java elasticsearch","operator": "and"}}}{"bool": {"must": [{"term": {"title": "java" }},{"term": {"title": "elasticsearch" }}]}}

3、minimum_should_match如何转换

{"match": {"title": {"query":"java elasticsearch hadoop spark","minimum_should_match": "75%"}}}{"bool": {"should": [{"term": {"title": "java" }},{"term": {"title": "elasticsearch" }},{"term": {"title": "hadoop" }},{"term": {"title": "spark" }}],"minimum_should_match": 3 }}

上一讲,为啥要讲解两种实现multi-value搜索的方式呢?实际上,就是给这一讲进行铺垫的。match query --> bool + term。

09_搜索_boost权重

课程大纲

需求:搜索标题中包含java的帖子,同时呢,如果标题中包含hadoop或elasticsearch就优先搜索出来,同时呢,如果一个帖子包含java hadoop,一个帖子包含java elasticsearch,包含hadoop的帖子要比elasticsearch优先搜索出来

知识点,搜索条件的权重,boost,可以将某个搜索条件的权重加大,此时当匹配这个搜索条件和匹配另一个搜索条件的document,计算relevance score时,匹配权重更大的搜索条件的document,relevance score会更高,当然也就会优先被返回回来

默认情况下,搜索条件的权重都是一样的,都是1

GET /forum/article/_search {"query": {"bool": {"must": [{"match": {"title": "blog"}}],"should": [{"match": {"title": {"query": "java"}}},{"match": {"title": {"query": "hadoop"}}},{"match": {"title": {"query": "elasticsearch"}}},{"match": {"title": {"query": "spark","boost": 5}}}]}}}

10_搜索_多shard下relevance score不准确

课程大纲

1、多shard场景下relevance score不准确问题大揭秘

如果你的一个index有多个shard的话,可能搜索结果会不准确

图解

2、如何解决该问题?

(1)生产环境下,数据量大,尽可能实现均匀分配

数据量很大的话,其实一般情况下,在概率学的背景下,es都是在多个shard中均匀路由数据的,路由的时候根据_id,负载均衡

比如说有10个document,title都包含java,一共有5个shard,那么在概率学的背景下,如果负载均衡的话,其实每个shard都应该有2个doc,title包含java

如果说数据分布均匀的话,其实就没有刚才说的那个问题了

(2)测试环境下,将索引的primary shard设置为1个,number_of_shards=1,index settings

如果说只有一个shard,那么当然,所有的document都在这个shard里面,就没有这个问题了

(3)测试环境下,搜索附带search_type=dfs_query_then_fetch参数,会将local IDF取出来计算global IDF

计算一个doc的相关度分数的时候,就会将所有shard对的local IDF计算一下,获取出来,在本地进行global IDF分数的计算,会将所有shard的doc作为上下文来进行计算,也能确保准确性。但是production生产环境下,不推荐这个参数,因为性能很差。

11_搜索_dis_max实现best fields多字段搜索

课程大纲

1、为帖子数据增加content字段

POST /forum/article/_bulk{"update": {"_id": "1"} }{"doc" : {"content" : "i like to write best elasticsearch article"} }{"update": {"_id": "2"} }{"doc" : {"content" : "i think java is the best programming language"} }{"update": {"_id": "3"} }{"doc" : {"content" : "i am only an elasticsearch beginner"} }{"update": {"_id": "4"} }{"doc" : {"content" : "elasticsearch and hadoop are all very good solution, i am a beginner"} }{"update": {"_id": "5"} }{"doc" : {"content" : "spark is best big data solution based on scala ,an programming language similar to java"} }

2、搜索title或content中包含java或solution的帖子

下面这个就是multi-field搜索,多字段搜索

GET /forum/article/_search{"query": {"bool": {"should": [{"match": {"title": "java solution" }},{"match": {"content": "java solution" }}]}}}

3、结果分析

期望的是doc5,结果是doc2,doc4排在了前面

计算每个document的relevance score:每个query的分数,乘以matched query数量,除以总query数量

算一下doc4的分数

{ “match”: { “title”: “java solution” }},针对doc4,是有一个分数的

{ “match”: { “content”: “java solution” }},针对doc4,也是有一个分数的

所以是两个分数加起来,比如说,1.1 + 1.2 = 2.3

matched query数量 = 2

总query数量 = 2

2.3 * 2 / 2 = 2.3

算一下doc5的分数

{ “match”: { “title”: “java solution” }},针对doc5,是没有分数的

{ “match”: { “content”: “java solution” }},针对doc5,是有一个分数的

所以说,只有一个query是有分数的,比如2.3

matched query数量 = 1

总query数量 = 2

2.3 * 1 / 2 = 1.15

doc5的分数 = 1.15 < doc4的分数 = 2.3

4、best fields策略,dis_max

best fields策略,就是说,搜索到的结果,应该是某一个field中匹配到了尽可能多的关键词,被排在前面;而不是尽可能多的field匹配到了少数的关键词,排在了前面

dis_max语法,直接取多个query中,分数最高的那一个query的分数即可

{ “match”: { “title”: “java solution” }},针对doc4,是有一个分数的,1.1

{ “match”: { “content”: “java solution” }},针对doc4,也是有一个分数的,1.2

取最大分数,1.2

{ “match”: { “title”: “java solution” }},针对doc5,是没有分数的

{ “match”: { “content”: “java solution” }},针对doc5,是有一个分数的,2.3

取最大分数,2.3

然后doc4的分数 = 1.2 < doc5的分数 = 2.3,所以doc5就可以排在更前面的地方,符合我们的需要

GET /forum/article/_search

{"query": {"dis_max": {"queries": [{"match": {"title": "java solution" }},{"match": {"content": "java solution" }}]}}}

12_搜索_tie_breaker优化dis_max搜索

课程大纲

1、搜索title或content中包含java beginner的帖子

GET /forum/article/_search{"query": {"dis_max": {"queries": [{"match": {"title": "java beginner" }},{"match": {"body": "java beginner" }}]}}}

有些场景不是太好复现的,因为是这样,你需要尝试去构造不同的文本,然后去构造一些搜索出来,去达到你要的一个效果

可能在实际场景中出现的一个情况是这样的:

(1)某个帖子,doc1,title中包含java,content不包含java beginner任何一个关键词

(2)某个帖子,doc2,content中包含beginner,title中不包含任何一个关键词

(3)某个帖子,doc3,title中包含java,content中包含beginner

(4)最终搜索,可能出来的结果是,doc1和doc2排在doc3的前面,而不是我们期望的doc3排在最前面

dis_max,只是取分数最高的那个query的分数而已。

2、dis_max只取某一个query最大的分数,完全不考虑其他query的分数

3、使用tie_breaker将其他query的分数也考虑进去

tie_breaker参数的意义,在于说,将其他query的分数,乘以tie_breaker,然后综合与最高分数的那个query的分数,综合在一起进行计算,除了取最高分以外,还会考虑其他的query的分数

tie_breaker的值,在0~1之间,是个小数,就ok

GET /forum/article/_search{"query": {"dis_max": {"queries": [{"match": {"title": "java beginner" }},{"match": {"body": "java beginner" }}],"tie_breaker": 0.3}}}

13_搜索_multi_match+best-fields策略

课程大纲

GET /forum/article/_search{"query": {"multi_match": {"query":"java solution","type": "best_fields", "fields":[ "title^2", "content" ],"tie_breaker":0.3,"minimum_should_match": "50%" }} }

GET /forum/article/_search{"query": {"dis_max": {"queries": [{"match": {"title": {"query": "java beginner","minimum_should_match": "50%","boost": 2}}},{"match": {"body": {"query": "java beginner","minimum_should_match": "30%"}}}],"tie_breaker": 0.3}} }

minimum_should_match,主要是用来干嘛的?

去长尾,long tail

长尾,比如你搜索5个关键词,但是很多结果是只匹配1个关键词的,其实跟你想要的结果相差甚远,这些结果就是长尾

minimum_should_match,控制搜索结果的精准度,只有匹配一定数量的关键词的数据,才能返回

14_搜索_multi_match+most fiels策略

课程大纲

从best-fields换成most-fields策略

best-fields策略,主要是说将某一个field匹配尽可能多的关键词的doc优先返回回来

most-fields策略,主要是说尽可能返回更多field匹配到某个关键词的doc,优先返回回来

POST /forum/_mapping/article{"properties": {"sub_title": {"type":"string","analyzer": "english","fields": {"std": {"type":"string","analyzer": "standard"}}}}}

POST /forum/article/_bulk{"update": {"_id": "1"} }{"doc" : {"sub_title" : "learning more courses"} }{"update": {"_id": "2"} }{"doc" : {"sub_title" : "learned a lot of course"} }{"update": {"_id": "3"} }{"doc" : {"sub_title" : "we have a lot of fun"} }{"update": {"_id": "4"} }{"doc" : {"sub_title" : "both of them are good"} }{"update": {"_id": "5"} }{"doc" : {"sub_title" : "haha, hello world"} }

GET /forum/article/_search{"query": {"match": {"sub_title": "learning courses"}}}{"took": 3,"timed_out": false,"_shards": {"total": 5,"successful": 5,"failed": 0},"hits": {"total": 2,"max_score": 1.219939,"hits": [{"_index": "forum","_type": "article","_id": "2","_score": 1.219939,"_source": {"articleID": "KDKE-B-9947-#kL5","userID": 1,"hidden": false,"postDate": "-01-02","tag": ["java"],"tag_cnt": 1,"view_cnt": 50,"title": "this is java blog","content": "i think java is the best programming language","sub_title": "learned a lot of course"}},{"_index": "forum","_type": "article","_id": "1","_score": 0.5063205,"_source": {"articleID": "XHDK-A-1293-#fJ3","userID": 1,"hidden": false,"postDate": "-01-01","tag": ["java","hadoop"],"tag_cnt": 2,"view_cnt": 30,"title": "this is java and elasticsearch blog","content": "i like to write best elasticsearch article","sub_title": "learning more courses"}}]}}

sub_title用的是enligsh analyzer,所以还原了单词

为什么,因为如果我们用的是类似于english analyzer这种分词器的话,就会将单词还原为其最基本的形态,stemmer

learning --> learn

learned --> learn

courses --> course

sub_titile: learning coureses --> learn course

{ “doc” : {“sub_title” : “learned a lot of course”} },就排在了{ “doc” : {“sub_title” : “learning more courses”} }的前面

GET /forum/article/_search{"query": {"match": {"sub_title": "learning courses"}}}

很绕。。。。我自己都觉得很绕

很多东西,你看文字就觉得很绕,然后用语言去表述,也很绕,但是我觉得,用语言去说,相对来说会好一点点

GET /forum/article/_search{"query": {"multi_match": {"query": "learning courses","type": "most_fields", "fields": [ "sub_title", "sub_title.std" ]}}}{"took": 2,"timed_out": false,"_shards": {"total": 5,"successful": 5,"failed": 0},"hits": {"total": 2,"max_score": 1.219939,"hits": [{"_index": "forum","_type": "article","_id": "2","_score": 1.219939,"_source": {"articleID": "KDKE-B-9947-#kL5","userID": 1,"hidden": false,"postDate": "-01-02","tag": ["java"],"tag_cnt": 1,"view_cnt": 50,"title": "this is java blog","content": "i think java is the best programming language","sub_title": "learned a lot of course"}},{"_index": "forum","_type": "article","_id": "1","_score": 1.012641,"_source": {"articleID": "XHDK-A-1293-#fJ3","userID": 1,"hidden": false,"postDate": "-01-01","tag": ["java","hadoop"],"tag_cnt": 2,"view_cnt": 30,"title": "this is java and elasticsearch blog","content": "i like to write best elasticsearch article","sub_title": "learning more courses"}}]}}

你问我,具体的分数怎么算出来的,很难说,因为这个东西很复杂, 还不只是TF/IDF算法。因为不同的query,不同的语法,都有不同的计算score的细节。

与best_fields的区别

(1)best_fields,是对多个field进行搜索,挑选某个field匹配度最高的那个分数,同时在多个query最高分相同的情况下,在一定程度上考虑其他query的分数。简单来说,你对多个field进行搜索,就想搜索到某一个field尽可能包含更多关键字的数据

优点:通过best_fields策略,以及综合考虑其他field,还有minimum_should_match支持,可以尽可能精准地将匹配的结果推送到最前面

缺点:除了那些精准匹配的结果,其他差不多大的结果,排序结果不是太均匀,没有什么区分度了

实际的例子:百度之类的搜索引擎,最匹配的到最前面,但是其他的就没什么区分度了

(2)most_fields,综合多个field一起进行搜索,尽可能多地让所有field的query参与到总分数的计算中来,此时就会是个大杂烩,出现类似best_fields案例最开始的那个结果,结果不一定精准,某一个document的一个field包含更多的关键字,但是因为其他document有更多field匹配到了,所以排在了前面;所以需要建立类似sub_title.std这样的field,尽可能让某一个field精准匹配query string,贡献更高的分数,将更精准匹配的数据排到前面

优点:将尽可能匹配更多field的结果推送到最前面,整个排序结果是比较均匀的

缺点:可能那些精准匹配的结果,无法推送到最前面

实际的例子:wiki,明显的most_fields策略,搜索结果比较均匀,但是的确要翻好几页才能找到最匹配的结果

15_搜索_most_fields策略进行cross-fields搜索

课程大纲

cross-fields搜索,一个唯一标识,跨了多个field。比如一个人,标识,是姓名;一个建筑,它的标识是地址。姓名可以散落在多个field中,比如first_name和last_name中,地址可以散落在country,province,city中。

跨多个field搜索一个标识,比如搜索一个人名,或者一个地址,就是cross-fields搜索

初步来说,如果要实现,可能用most_fields比较合适。因为best_fields是优先搜索单个field最匹配的结果,cross-fields本身就不是一个field的问题了。

POST /forum/article/_bulk{"update": {"_id": "1"} }{"doc" : {"author_first_name" : "Peter", "author_last_name" : "Smith"} }{"update": {"_id": "2"} }{"doc" : {"author_first_name" : "Smith", "author_last_name" : "Williams"} }{"update": {"_id": "3"} }{"doc" : {"author_first_name" : "Jack", "author_last_name" : "Ma"} }{"update": {"_id": "4"} }{"doc" : {"author_first_name" : "Robbin", "author_last_name" : "Li"} }{"update": {"_id": "5"} }{"doc" : {"author_first_name" : "Tonny", "author_last_name" : "Peter Smith"} }

GET /forum/article/_search{"query": {"multi_match": {"query": "Peter Smith","type": "most_fields","fields":[ "author_first_name", "author_last_name" ]}}}

Peter Smith,匹配author_first_name,匹配到了Smith,这时候它的分数很高,为什么啊???

因为IDF分数高,IDF分数要高,那么这个匹配到的term(Smith),在所有doc中的出现频率要低,author_first_name field中,Smith就出现过1次

Peter Smith这个人,doc 1,Smith在author_last_name中,但是author_last_name出现了两次Smith,所以导致doc 1的IDF分数较低

不要有过多的疑问,一定是这样吗?

{"took": 2,"timed_out": false,"_shards": {"total": 5,"successful": 5,"failed": 0},"hits": {"total": 3,"max_score": 0.6931472,"hits": [{"_index": "forum","_type": "article","_id": "2","_score": 0.6931472,"_source": {"articleID": "KDKE-B-9947-#kL5","userID": 1,"hidden": false,"postDate": "-01-02","tag": ["java"],"tag_cnt": 1,"view_cnt": 50,"title": "this is java blog","content": "i think java is the best programming language","sub_title": "learned a lot of course","author_first_name": "Smith","author_last_name": "Williams"}},{"_index": "forum","_type": "article","_id": "1","_score": 0.5753642,"_source": {"articleID": "XHDK-A-1293-#fJ3","userID": 1,"hidden": false,"postDate": "-01-01","tag": ["java","hadoop"],"tag_cnt": 2,"view_cnt": 30,"title": "this is java and elasticsearch blog","content": "i like to write best elasticsearch article","sub_title": "learning more courses","author_first_name": "Peter","author_last_name": "Smith"}},{"_index": "forum","_type": "article","_id": "5","_score": 0.51623213,"_source": {"articleID": "DHJK-B-1395-#Ky5","userID": 3,"hidden": false,"postDate": "-03-01","tag": ["elasticsearch"],"tag_cnt": 1,"view_cnt": 10,"title": "this is spark blog","content": "spark is best big data solution based on scala ,an programming language similar to java","sub_title": "haha, hello world","author_first_name": "Tonny","author_last_name": "Peter Smith"}}]}}

问题1:只是找到尽可能多的field匹配的doc,而不是某个field完全匹配的doc

问题2:most_fields,没办法用minimum_should_match去掉长尾数据,就是匹配的特别少的结果

问题3:TF/IDF算法,比如Peter Smith和Smith Williams,搜索Peter Smith的时候,由于first_name中很少有Smith的,所以query在所有document中的频率很低,得到的分数很高,可能Smith Williams反而会排在Peter Smith前面

16_搜索_copy_to定制组合field解决cross-fields搜索弊端

课程大纲

上一讲,我们其实说了,用most_fields策略,去实现cross-fields搜索,有3大弊端,而且搜索结果也显示出了这3大弊端

第一个办法:用copy_to,将多个field组合成一个field

问题其实就出在有多个field,有多个field以后,就很尴尬,我们只要想办法将一个标识跨在多个field的情况,合并成一个field即可。比如说,一个人名,本来是first_name,last_name,现在合并成一个full_name,不就ok了吗。。。。。

PUT /forum/_mapping/article{"properties": {"new_author_first_name": {"type":"string","copy_to": "new_author_full_name" },"new_author_last_name": {"type":"string","copy_to": "new_author_full_name" },"new_author_full_name": {"type":"string"}}}

用了这个copy_to语法之后,就可以将多个字段的值拷贝到一个字段中,并建立倒排索引

POST /forum/article/_bulk{"update": {"_id": "1"} }{"doc" : {"new_author_first_name" : "Peter", "new_author_last_name" : "Smith"} }--> Peter Smith{"update": {"_id": "2"} }{"doc" : {"new_author_first_name" : "Smith", "new_author_last_name" : "Williams"} }--> Smith Williams{"update": {"_id": "3"} }{"doc" : {"new_author_first_name" : "Jack", "new_author_last_name" : "Ma"} }--> Jack Ma{"update": {"_id": "4"} }{"doc" : {"new_author_first_name" : "Robbin", "new_author_last_name" : "Li"} }--> Robbin Li{"update": {"_id": "5"} }{"doc" : {"new_author_first_name" : "Tonny", "new_author_last_name" : "Peter Smith"} }--> Tonny Peter Smith

GET /forum/article/_search{"query": {"match": {"new_author_full_name": "Peter Smith"}}}

很无奈,很多时候,我们很难复现。比如官网也会给一些例子,说用什么什么文本,怎么怎么搜索,是怎么怎么样的效果。es版本在不断迭代,这个打分的算法也在不断的迭代。所以我们其实很难说,对类似这几讲讲解的best_fields,most_fields,cross_fields,完全复现出来应有的场景和效果。

更多的把原理和知识点给大家讲解清楚,带着大家演练一遍怎么操作的,做一下实验

期望的是说,比如大家自己在开发搜索应用的时候,碰到需要best_fields的场景,知道怎么做,知道best_fields的原理,可以达到什么效果;碰到most_fields的场景,知道怎么做,以及原理;碰到搜搜cross_fields标识的场景,知道怎么做,知道原理是什么,效果是什么。。。。

问题1:只是找到尽可能多的field匹配的doc,而不是某个field完全匹配的doc --> 解决,最匹配的document被最先返回

问题2:most_fields,没办法用minimum_should_match去掉长尾数据,就是匹配的特别少的结果 --> 解决,可以使用minimum_should_match去掉长尾数据

问题3:TF/IDF算法,比如Peter Smith和Smith Williams,搜索Peter Smith的时候,由于first_name中很少有Smith的,所以query在所有document中的频率很低,得到的分数很高,可能Smith Williams反而会排在Peter Smith前面 --> 解决,Smith和Peter在一个field了,所以在所有document中出现的次数是均匀的,不会有极端的偏差

17_搜索_cross-fiels解决搜索弊端

课程大纲

GET /forum/article/_search{"query": {"multi_match": {"query": "Peter Smith","type": "cross_fields", "operator": "and","fields": ["author_first_name", "author_last_name"]}}}

问题1:只是找到尽可能多的field匹配的doc,而不是某个field完全匹配的doc --> 解决,要求每个term都必须在任何一个field中出现

Peter,Smith

要求Peter必须在author_first_name或author_last_name中出现

要求Smith必须在author_first_name或author_last_name中出现

Peter Smith可能是横跨在多个field中的,所以必须要求每个term都在某个field中出现,组合起来才能组成我们想要的标识,完整的人名

原来most_fiels,可能像Smith Williams也可能会出现,因为most_fields要求只是任何一个field匹配了就可以,匹配的field越多,分数越高

问题2:most_fields,没办法用minimum_should_match去掉长尾数据,就是匹配的特别少的结果 --> 解决,既然每个term都要求出现,长尾肯定被去除掉了

java hadoop spark --> 这3个term都必须在任何一个field出现了

比如有的document,只有一个field中包含一个java,那就被干掉了,作为长尾就没了

问题3:TF/IDF算法,比如Peter Smith和Smith Williams,搜索Peter Smith的时候,由于first_name中很少有Smith的,所以query在所有document中的频率很低,得到的分数很高,可能Smith Williams反而会排在Peter Smith前面 --> 计算IDF的时候,将每个query在每个field中的IDF都取出来,取最小值,就不会出现极端情况下的极大值了

Peter Smith

Peter

Smith

Smith,在author_first_name这个field中,在所有doc的这个Field中,出现的频率很低,导致IDF分数很高;Smith在所有doc的author_last_name field中的频率算出一个IDF分数,因为一般来说last_name中的Smith频率都较高,所以IDF分数是正常的,不会太高;然后对于Smith来说,会取两个IDF分数中,较小的那个分数。就不会出现IDF分过高的情况。

18_搜索_match_phrase搜索

近似匹配

1、什么是近似匹配

两个句子

java is my favourite programming language, and I also think spark is a very good big data system.

java spark are very related, because scala is spark’s programming language and scala is also based on jvm like java.

match query,搜索java spark

{"match": {"content": "java spark"}}

match query,只能搜索到包含java和spark的document,但是不知道java和spark是不是离的很近

包含java或包含spark,或包含java和spark的doc,都会被返回回来。我们其实并不知道哪个doc,java和spark距离的比较近。如果我们就是希望搜索java spark,中间不能插入任何其他的字符,那这个时候match去做全文检索,能搞定我们的需求吗?答案是,搞不定。

如果我们要尽量让java和spark离的很近的document优先返回,要给它一个更高的relevance score,这就涉及到了proximity match,近似匹配

如果说,要实现两个需求:

1、java spark,就靠在一起,中间不能插入任何其他字符,就要搜索出来这种doc

2、java spark,但是要求,java和spark两个单词靠的越近,doc的分数越高,排名越靠前

要实现上述两个需求,用match做全文检索,是搞不定的,必须得用proximity match,近似匹配

phrase match,proximity match:短语匹配,近似匹配

这一讲,要学习的是phrase match,就是仅仅搜索出java和spark靠在一起的那些doc,比如有个doc,是java use’d spark,不行。必须是比如java spark are very good friends,是可以搜索出来的。

phrase match,就是要去将多个term作为一个短语,一起去搜索,只有包含这个短语的doc才会作为结果返回。不像是match,java spark,java的doc也会返回,spark的doc也会返回。

2、match_phrase

GET /forum/article/_search{"query": {"match": {"content": "java spark"}}}

单单包含java的doc也返回了,不是我们想要的结果

POST /forum/article/5/_update{"doc": {"content": "spark is best big data solution based on scala ,an programming language similar to java spark"}}

将一个doc的content设置为恰巧包含java spark这个短语

match_phrase语法

GET /forum/article/_search{"query": {"match_phrase": {"content": "java spark"}}}

成功了,只有包含java spark这个短语的doc才返回了,只包含java的doc不会返回

3、term position

hello world, java spark doc1

hi, spark java doc2

hello doc1(0)

wolrd doc1(1)

java doc1(2) doc2(2)

spark doc1(3) doc2(1)

了解什么是分词后的position

GET _analyze

{

“text”: “hello world, java spark”,

“analyzer”: “standard”

}

4、match_phrase的基本原理

索引中的position,match_phrase

hello world, java spark doc1

hi, spark java doc2

hello doc1(0)

wolrd doc1(1)

java doc1(2) doc2(2)

spark doc1(3) doc2(1)

java spark --> match phrase

java spark --> java和spark

java --> doc1(2) doc2(2)

spark --> doc1(3) doc2(1)

要找到每个term都在的一个共有的那些doc,就是要求一个doc,必须包含每个term,才能拿出来继续计算

doc1 --> java和spark --> spark position恰巧比java大1 --> java的position是2,spark的position是3,恰好满足条件

doc1符合条件

doc2 --> java和spark --> java position是2,spark position是1,spark position比java position小1,而不是大1 --> 光是position就不满足,那么doc2不匹配

必须理解这块原理!!!!

因为后面的proximity match就是原理跟这个一模一样!!!

19_搜索_match_phrase的slop参数实现近似匹配

GET /forum/article/_search{"query": {"match_phrase": {"title": {"query": "java spark","slop": 1}}}}

slop的含义是什么?

query string,搜索文本,中的几个term,要经过几次移动才能与一个document匹配,这个移动的次数,就是slop

实际举例,一个query string经过几次移动之后可以匹配到一个document,然后设置slop

hello world, java is very good, spark is also very good.

java spark,match phrase,搜不到

如果我们指定了slop,那么就允许java spark进行移动,来尝试与doc进行匹配

java is very good spark is

java spark

java --> spark

java --> spark

java --> spark

这里的slop,就是3,因为java spark这个短语,spark移动了3次,就可以跟一个doc匹配上了

slop的含义,不仅仅是说一个query string terms移动几次,跟一个doc匹配上。一个query string terms,最多可以移动几次去尝试跟一个doc匹配上

slop,设置的是3,那么就ok

GET /forum/article/_search{"query": {"match_phrase": {"title": {"query": "java spark","slop": 3}}}}

就可以把刚才那个doc匹配上,那个doc会作为结果返回

但是如果slop设置的是2,那么java spark,spark最多只能移动2次,此时跟doc是匹配不上的,那个doc是不会作为结果返回的

做实验,验证slop的含义

GET /forum/article/_search{"query": {"match_phrase": {"content": {"query": "spark data","slop": 3}}}}

spark is best big data solution based on scala ,an programming language similar to java spark

spark data

–> data

–> data

spark --> data

GET /forum/article/_search{"query": {"match_phrase": {"content": {"query": "data spark","slop": 5}}}}

spark is best big data

data spark

–> data/spark

spark <–data

spark --> data

spark --> data

spark --> data

slop搜索下,关键词离的越近,relevance score就会越高,做实验说明。。。

{"took": 4,"timed_out": false,"_shards": {"total": 5,"successful": 5,"failed": 0},"hits": {"total": 3,"max_score": 1.3728157,"hits": [{"_index": "forum","_type": "article","_id": "2","_score": 1.3728157,"_source": {"articleID": "KDKE-B-9947-#kL5","userID": 1,"hidden": false,"postDate": "-01-02","tag": ["java"],"tag_cnt": 1,"view_cnt": 50,"title": "this is java blog","content": "i think java is the best programming language","sub_title": "learned a lot of course","author_first_name": "Smith","author_last_name": "Williams","new_author_last_name": "Williams","new_author_first_name": "Smith"}},{"_index": "forum","_type": "article","_id": "5","_score": 0.5753642,"_source": {"articleID": "DHJK-B-1395-#Ky5","userID": 3,"hidden": false,"postDate": "-03-01","tag": ["elasticsearch"],"tag_cnt": 1,"view_cnt": 10,"title": "this is spark blog","content": "spark is best big data solution based on scala ,an programming language similar to java spark","sub_title": "haha, hello world","author_first_name": "Tonny","author_last_name": "Peter Smith","new_author_last_name": "Peter Smith","new_author_first_name": "Tonny"}},{"_index": "forum","_type": "article","_id": "1","_score": 0.28582606,"_source": {"articleID": "XHDK-A-1293-#fJ3","userID": 1,"hidden": false,"postDate": "-01-01","tag": ["java","hadoop"],"tag_cnt": 2,"view_cnt": 30,"title": "this is java and elasticsearch blog","content": "i like to write best elasticsearch article","sub_title": "learning more courses","author_first_name": "Peter","author_last_name": "Smith","new_author_last_name": "Smith","new_author_first_name": "Peter"}}]}}

GET /forum/article/_search{"query": {"match_phrase": {"content": {"query": "java best","slop": 15}}}}

{"took": 3,"timed_out": false,"_shards": {"total": 5,"successful": 5,"failed": 0},"hits": {"total": 2,"max_score": 0.65380025,"hits": [{"_index": "forum","_type": "article","_id": "2","_score": 0.65380025,"_source": {"articleID": "KDKE-B-9947-#kL5","userID": 1,"hidden": false,"postDate": "-01-02","tag": ["java"],"tag_cnt": 1,"view_cnt": 50,"title": "this is java blog","content": "i think java is the best programming language","sub_title": "learned a lot of course","author_first_name": "Smith","author_last_name": "Williams","new_author_last_name": "Williams","new_author_first_name": "Smith"}},{"_index": "forum","_type": "article","_id": "5","_score": 0.07111243,"_source": {"articleID": "DHJK-B-1395-#Ky5","userID": 3,"hidden": false,"postDate": "-03-01","tag": ["elasticsearch"],"tag_cnt": 1,"view_cnt": 10,"title": "this is spark blog","content": "spark is best big data solution based on scala ,an programming language similar to java spark","sub_title": "haha, hello world","author_first_name": "Tonny","author_last_name": "Peter Smith","new_author_last_name": "Peter Smith","new_author_first_name": "Tonny"}}]}}

其实,加了slop的phrase match,就是proximity match,近似匹配

1、java spark,短语,doc,phrase match

2、java spark,可以有一定的距离,但是靠的越近,越先搜索出来,proximity match

20_搜索_match_phrase和match召回率与精准度的平衡

召回率

比如你搜索一个java spark,总共有100个doc,能返回多少个doc作为结果,就是召回率,recall

精准度

比如你搜索一个java spark,能不能尽可能让包含java spark,或者是java和spark离的很近的doc,排在最前面,precision

直接用match_phrase短语搜索,会导致必须所有term都在doc field中出现,而且距离在slop限定范围内,才能匹配上

match phrase,proximity match,要求doc必须包含所有的term,才能作为结果返回;如果某一个doc可能就是有某个term没有包含,那么就无法作为结果返回

java spark --> hello world java --> 就不能返回了

java spark --> hello world, java spark --> 才可以返回

近似匹配的时候,召回率比较低,精准度太高了

但是有时可能我们希望的是匹配到几个term中的部分,就可以作为结果出来,这样可以提高召回率。同时我们也希望用上match_phrase根据距离提升分数的功能,让几个term距离越近分数就越高,优先返回

就是优先满足召回率,意思,java spark,包含java的也返回,包含spark的也返回,包含java和spark的也返回;同时兼顾精准度,就是包含java和spark,同时java和spark离的越近的doc排在最前面

此时可以用bool组合match query和match_phrase query一起,来实现上述效果

GET /forum/article/_search{"query": {"bool": {"must": {"match": {"title": {"query":"java spark" --> java或spark或java spark,java和spark靠前,但是没法区分java和spark的距离,也许java和spark靠的很近,但是没法排在最前面}}},"should": {"match_phrase": {--> 在slop以内,如果java spark能匹配上一个doc,那么就会对doc贡献自己的relevance score,如果java和spark靠的越近,那么就分数越高"title": {"query": "java spark","slop": 50}}}}}}

GET /forum/article/_search {"query": {"bool": {"must": [{"match": {"content": "java spark"}}]}}}{"took": 5,"timed_out": false,"_shards": {"total": 5,"successful": 5,"failed": 0},"hits": {"total": 2,"max_score": 0.68640786,"hits": [{"_index": "forum","_type": "article","_id": "2","_score": 0.68640786,"_source": {"articleID": "KDKE-B-9947-#kL5","userID": 1,"hidden": false,"postDate": "-01-02","tag": ["java"],"tag_cnt": 1,"view_cnt": 50,"title": "this is java blog","content": "i think java is the best programming language","sub_title": "learned a lot of course","author_first_name": "Smith","author_last_name": "Williams","new_author_last_name": "Williams","new_author_first_name": "Smith","followers": ["Tom","Jack"]}},{"_index": "forum","_type": "article","_id": "5","_score": 0.68324494,"_source": {"articleID": "DHJK-B-1395-#Ky5","userID": 3,"hidden": false,"postDate": "-03-01","tag": ["elasticsearch"],"tag_cnt": 1,"view_cnt": 10,"title": "this is spark blog","content": "spark is best big data solution based on scala ,an programming language similar to java spark","sub_title": "haha, hello world","author_first_name": "Tonny","author_last_name": "Peter Smith","new_author_last_name": "Peter Smith","new_author_first_name": "Tonny","followers": ["Jack","Robbin Li"]}}]}}

GET /forum/article/_search {"query": {"bool": {"must": [{"match": {"content": "java spark"}}],"should": [{"match_phrase": {"content": {"query": "java spark","slop": 50}}}]}}}{"took": 5,"timed_out": false,"_shards": {"total": 5,"successful": 5,"failed": 0},"hits": {"total": 2,"max_score": 1.258609,"hits": [{"_index": "forum","_type": "article","_id": "5","_score": 1.258609,"_source": {"articleID": "DHJK-B-1395-#Ky5","userID": 3,"hidden": false,"postDate": "-03-01","tag": ["elasticsearch"],"tag_cnt": 1,"view_cnt": 10,"title": "this is spark blog","content": "spark is best big data solution based on scala ,an programming language similar to java spark","sub_title": "haha, hello world","author_first_name": "Tonny","author_last_name": "Peter Smith","new_author_last_name": "Peter Smith","new_author_first_name": "Tonny","followers": ["Jack","Robbin Li"]}},{"_index": "forum","_type": "article","_id": "2","_score": 0.68640786,"_source": {"articleID": "KDKE-B-9947-#kL5","userID": 1,"hidden": false,"postDate": "-01-02","tag": ["java"],"tag_cnt": 1,"view_cnt": 50,"title": "this is java blog","content": "i think java is the best programming language","sub_title": "learned a lot of course","author_first_name": "Smith","author_last_name": "Williams","new_author_last_name": "Williams","new_author_first_name": "Smith","followers": ["Tom","Jack"]}}]}}

21_搜索_match_phrase的rescore近似匹配优化

match和phrase match(proximity match)区别

match --> 只要简单的匹配到了一个term,就可以理解将term对应的doc作为结果返回,扫描倒排索引,扫描到了就ok

phrase match --> 首先扫描到所有term的doc list; 找到包含所有term的doc list; 然后对每个doc都计算每个term的position,是否符合指定的范围; slop,需要进行复杂的运算,来判断能否通过slop移动,匹配一个doc

match query的性能比phrase match和proximity match(有slop)要高很多。因为后两者都要计算position的距离。

match query比phrase match的性能要高10倍,比proximity match的性能要高20倍。

但是别太担心,因为es的性能一般都在毫秒级别,match query一般就在几毫秒,或者几十毫秒,而phrase match和proximity match的性能在几十毫秒到几百毫秒之间,所以也是可以接受的。

优化proximity match的性能,一般就是减少要进行proximity match搜索的document数量。主要思路就是,用match query先过滤出需要的数据,然后再用proximity match来根据term距离提高doc的分数,同时proximity match只针对每个shard的分数排名前n个doc起作用,来重新调整它们的分数,这个过程称之为rescoring,重计分。因为一般用户会分页查询,只会看到前几页的数据,所以不需要对所有结果进行proximity match操作。

用我们刚才的说法,match + proximity match同时实现召回率和精准度

默认情况下,match也许匹配了1000个doc,proximity match全都需要对每个doc进行一遍运算,判断能否slop移动匹配上,然后去贡献自己的分数

但是很多情况下,match出来也许1000个doc,其实用户大部分情况下是分页查询的,所以可能最多只会看前几页,比如一页是10条,最多也许就看5页,就是50条

proximity match只要对前50个doc进行slop移动去匹配,去贡献自己的分数即可,不需要对全部1000个doc都去进行计算和贡献分数

rescore:重打分

match:1000个doc,其实这时候每个doc都有一个分数了; proximity match,前50个doc,进行rescore,重打分,即可; 让前50个doc,term举例越近的,排在越前面

GET /forum/article/_search {"query": {"match": {"content": "java spark"}},"rescore": {"window_size": 50,"query": {"rescore_query": {"match_phrase": {"content": {"query": "java spark","slop": 50}}}}}}

22_搜索_prefix前缀、wildcard通配符、regexp正则搜索等技术

课程大纲

1、前缀搜索

C3D0-KD345

C3K5-DFG65

C4I8-UI365

C3 --> 上面这两个都搜索出来 --> 根据字符串的前缀去搜索

不用帖子的案例背景,因为比较简单,直接用自己手动建的新索引,给大家演示一下就可以了

PUT my_index{"mappings": {"my_type": {"properties": {"title": {"type": "keyword"}}}}}

GET my_index/my_type/_search{"query": {"prefix": {"title": {"value": "C3"}}}}

2、前缀搜索的原理

prefix query不计算relevance score,与prefix filter唯一的区别就是,filter会cache bitset

扫描整个倒排索引,举例说明

前缀越短,要处理的doc越多,性能越差,尽可能用长前缀搜索

前缀搜索,它是怎么执行的?性能为什么差呢?

match

C3-D0-KD345

C3-K5-DFG65

C4-I8-UI365

全文检索

每个字符串都需要被分词

c3 doc1,doc2

d0

kd345

k5

dfg65

c4

i8

ui365

c3 --> 扫描倒排索引 --> 一旦扫描到c3,就可以停了,因为带c3的就2个doc,已经找到了 --> 没有必要继续去搜索其他的term了

match性能往往是很高的

不分词

C3-D0-KD345

C3-K5-DFG65

C4-I8-UI365

c3 --> 先扫描到了C3-D0-KD345,很棒,找到了一个前缀带c3的字符串 --> 还是要继续搜索的,因为后面还有一个C3-K5-DFG65,也许还有其他很多的前缀带c3的字符串 --> 你扫描到了一个前缀匹配的term,不能停,必须继续搜索 --> 直到扫描完整个的倒排索引,才能结束

因为实际场景中,可能有些场景是全文检索解决不了的

C3D0-KD345

C3K5-DFG65

C4I8-UI365

c3d0

kd345

c3 --> match --> 扫描整个倒排索引,能找到吗

c3 --> 只能用prefix

prefix性能很差

3、通配符搜索

跟前缀搜索类似,功能更加强大

C3D0-KD345

C3K5-DFG65

C4I8-UI365

5字符-D任意个字符5

5?-*5:通配符去表达更加复杂的模糊搜索的语义

GET my_index/my_type/_search{"query": {"wildcard": {"title": {"value": "C?K*5"}}}}

?:任意字符

*:0个或任意多个字符

性能一样差,必须扫描整个倒排索引,才ok

4、正则搜索

GET /my_index/my_type/_search {"query": {"regexp": {"title": "C[0-9].+"}}}

C[0-9].+

[0-9]:指定范围内的数字

[a-z]:指定范围内的字母

.:一个字符

+:前面的正则表达式可以出现一次或多次

wildcard和regexp,与prefix原理一致,都会扫描整个索引,性能很差

主要是给大家介绍一些高级的搜索语法。在实际应用中,能不用尽量别用。性能太差了。

23_搜索_match_phrase_prefix搜索推荐

搜索推荐,search as you type,搜索提示,解释一下什么意思

hello w --> 搜索

hello world

hello we

hello win

hello wind

hello dog

hello cat

hello w -->

hello world

hello we

hello win

hello wind

搜索推荐的功能

百度 --> elas --> elasticsearch --> elasticsearch权威指南

GET /my_index/my_type/_search {"query": {"match_phrase_prefix": {"title": "hello d"}}}

原理跟match_phrase类似,唯一的区别,就是把最后一个term作为前缀去搜索

hello就是去进行match,搜索对应的doc

w,会作为前缀,去扫描整个倒排索引,找到所有w开头的doc

然后找到所有doc中,即包含hello,又包含w开头的字符的doc

根据你的slop去计算,看在slop范围内,能不能让hello w,正好跟doc中的hello和w开头的单词的position相匹配

也可以指定slop,但是只有最后一个term会作为前缀

max_expansions:指定prefix最多匹配多少个term,超过这个数量就不继续匹配了,限定性能

默认情况下,前缀要扫描所有的倒排索引中的term,去查找w打头的单词,但是这样性能太差。可以用max_expansions限定,w前缀最多匹配多少个term,就不再继续搜索倒排索引了。

尽量不要用,因为,最后一个前缀始终要去扫描大量的索引,性能可能会很差

24_搜索_ngram分词机制实现index-time搜索推荐

1、ngram和index-time搜索推荐原理

什么是ngram

quick,5种长度下的ngram

ngram length=1,q u i c k

ngram length=2,qu ui ic ck

ngram length=3,qui uic ick

ngram length=4,quic uick

ngram length=5,quick

什么是edge ngram

quick,anchor首字母后进行ngram

q

qu

qui

quic

quick

使用edge ngram将每个单词都进行进一步的分词切分,用切分后的ngram来实现前缀搜索推荐功能

hello world

hello we

h

he

hel

hell

hello doc1,doc2

w doc1,doc2

wo

wor

worl

world

e doc2

helloworld

min ngram = 1

max ngram = 3

h

he

hel

hello w

hello --> hello,doc1

w --> w,doc1

doc1,hello和w,而且position也匹配,所以,ok,doc1返回,hello world

搜索的时候,不用再根据一个前缀,然后扫描整个倒排索引了; 简单的拿前缀去倒排索引中匹配即可,如果匹配上了,那么就好了; match,全文检索

2、实验一下ngram

PUT /my_index{"settings": {"analysis": {"filter": {"autocomplete_filter": {"type":"edge_ngram","min_gram": 1,"max_gram": 20}},"analyzer": {"autocomplete": {"type":"custom","tokenizer": "standard","filter": ["lowercase","autocomplete_filter" ]}}}}}

GET /my_index/_analyze{"analyzer": "autocomplete","text": "quick brown"}PUT /my_index/_mapping/my_type{"properties": {"title": {"type":"string","analyzer": "autocomplete","search_analyzer": "standard"}}}

hello world

h

he

hel

hell

hello

w

wo

wor

worl

world

hello w

h

he

hel

hell

hello

w

hello w --> hello --> w

GET /my_index/my_type/_search {"query": {"match_phrase": {"title": "hello w"}}}

如果用match,只有hello的也会出来,全文检索,只是分数比较低

推荐使用match_phrase,要求每个term都有,而且position刚好靠着1位,符合我们的期望的

25_搜索_深入揭秘TF&IDF算法以及向量空间模型算法

课程大纲

1、boolean model

类似and这种逻辑操作符,先过滤出包含指定term的doc

query “hello world” --> 过滤 --> hello / world / hello & world

bool --> must/must not/should --> 过滤 --> 包含 / 不包含 / 可能包含

doc --> 不打分数 --> 正或反 true or false --> 为了减少后续要计算的doc的数量,提升性能

2、TF/IDF

单个term在doc中的分数

query: hello world --> doc.content

doc1: java is my favourite programming language, hello world !!!

doc2: hello java, you are very good, oh hello world!!!

hello对doc1的评分

TF: term frequency

找到hello在doc1中出现了几次,1次,会根据出现的次数给个分数

一个term在一个doc中,出现的次数越多,那么最后给的相关度评分就会越高

IDF:inversed document frequency

找到hello在所有的doc中出现的次数,3次

一个term在所有的doc中,出现的次数越多,那么最后给的相关度评分就会越低

length norm

hello搜索的那个field的长度,field长度越长,给的相关度评分越低; field长度越短,给的相关度评分越高

最后,会将hello这个term,对doc1的分数,综合TF,IDF,length norm,计算出来一个综合性的分数

hello world --> doc1 --> hello对doc1的分数,world对doc1的分数 --> 但是最后hello world query要对doc1有一个总的分数 --> vector space model

3、vector space model

多个term对一个doc的总分数

hello world --> es会根据hello world在所有doc中的评分情况,计算出一个query vector,query向量

hello这个term,给的基于所有doc的一个评分就是2

world这个term,给的基于所有doc的一个评分就是5

[2, 5]

query vector

doc vector,3个doc,一个包含1个term,一个包含另一个term,一个包含2个term

3个doc

doc1:包含hello --> [2, 0]

doc2:包含world --> [0, 5]

doc3:包含hello, world --> [2, 5]

会给每一个doc,拿每个term计算出一个分数来,hello有一个分数,world有一个分数,再拿所有term的分数组成一个doc vector

画在一个图中,取每个doc vector对query vector的弧度,给出每个doc对多个term的总分数

每个doc vector计算出对query vector的弧度,最后基于这个弧度给出一个doc相对于query中多个term的总分数

弧度越大,分数月底; 弧度越小,分数越高

如果是多个term,那么就是线性代数来计算,无法用图表示

26_搜索_深入揭秘lucene的相关度分数算法

课程大纲

我们boolean model、TF/IDF、vector space model

深入讲解TF/IDF算法,在lucene中,底层,到底进行TF/IDF算法计算的一个完整的公式是什么?

0、boolean model

query: hello world

“match”: {

“title”: “hello world”

}

“bool”: {

“should”: [

{

“match”: {

“title”: “hello”

}

},

{

“natch”: {

“title”: “world”

}

}

]

}

普通multivalue搜索,转换为bool搜索,boolean model

1、lucene practical scoring function

practical scoring function,来计算一个query对一个doc的分数的公式,该函数会使用一个公式来计算

score(q,d) =

queryNorm(q)

· coord(q,d)

· ∑ (

tf(t in d)

· idf(t)2

· t.getBoost()

· norm(t,d)

) (t in q)

score(q,d) score(q,d) is the relevance score of document d for query q.

这个公式的最终结果,就是说是一个query(叫做q),对一个doc(叫做d)的最终的总评分

queryNorm(q) is the query normalization factor (new).

queryNorm,是用来让一个doc的分数处于一个合理的区间内,不要太离谱,举个例子,一个doc分数是10000,一个doc分数是0.1,你们说好不好,肯定不好

coord(q,d) is the coordination factor (new).

简单来说,就是对更加匹配的doc,进行一些分数上的成倍的奖励

The sum of the weights for each term t in the query q for document d.

∑:求和的符号

∑ (t in q):query中每个term,query = hello world,query中的term就包含了hello和world

query中每个term对doc的分数,进行求和,多个term对一个doc的分数,组成一个vector space,然后计算吗,就在这一步

tf(t in d) is the term frequency for term t in document d.

计算每一个term对doc的分数的时候,就是TF/IDF算法

idf(t) is the inverse document frequency for term t.

t.getBoost() is the boost that has been applied to the query (new).

norm(t,d) is the field-length norm, combined with the index-time field-level boost, if any. (new).

2、query normalization factor

queryNorm = 1 / √sumOfSquaredWeights

sumOfSquaredWeights = 所有term的IDF分数之和,开一个平方根,然后做一个平方根分之1

主要是为了将分数进行规范化 --> 开平方根,首先数据就变小了 --> 然后还用1去除以这个平方根,分数就会很小 --> 1.几 / 零点几

分数就不会出现几万,几十万,那样的离谱的分数

3、query coodination

奖励那些匹配更多字符的doc更多的分数

Document 1 with hello → score: 1.5

Document 2 with hello world → score: 3.0

Document 3 with hello world java → score: 4.5

Document 1 with hello → score: 1.5 * 1 / 3 = 0.5

Document 2 with hello world → score: 3.0 * 2 / 3 = 2.0

Document 3 with hello world java → score: 4.5 * 3 / 3 = 4.5

把计算出来的总分数 * 匹配上的term数量 / 总的term数量,让匹配不同term/query数量的doc,分数之间拉开差距

4、field level boost

27_搜索_四种常见的相关度分数优化方法

之前两节课,我觉得已经很了解整个es的相关度评分的算法了,算法思想,TF/IDF,vector model,boolean model; 实际的公式,query norm,query coordination,boost

对相关度评分进行调节和优化的常见的4种方法

1、query-time boost

GET /forum/article/_search{"query": {"bool": {"should": [{"match": {"title": {"query": "java spark","boost": 2}}},{"match": {"content": "java spark"}}]}}}

2、重构查询结构

重构查询结果,在es新版本中,影响越来越小了。一般情况下,没什么必要的话,大家不用也行。

GET /forum/article/_search {"query": {"bool": {"should": [{"match": {"content": "java"}},{"match": {"content": "spark"}},{"bool": {"should": [{"match": {"content": "solution"}},{"match": {"content": "beginner"}}]}}]}}}

3、negative boost

搜索包含java,不包含spark的doc,但是这样子很死板

搜索包含java,尽量不包含spark的doc,如果包含了spark,不会说排除掉这个doc,而是说将这个doc的分数降低

包含了negative term的doc,分数乘以negative boost,分数降低

GET /forum/article/_search {"query": {"bool": {"must": [{"match": {"content": "java"}}],"must_not": [{"match": {"content": "spark"}}]}}}

GET /forum/article/_search {"query": {"boosting": {"positive": {"match": {"content": "java"}},"negative": {"match": {"content": "spark"}},"negative_boost": 0.2}}}

negative的doc,会乘以negative_boost,降低分数

4、constant_score

如果你压根儿不需要相关度评分,直接走constant_score加filter,所有的doc分数都是1,没有评分的概念了

GET /forum/article/_search {"query": {"bool": {"should": [{"constant_score": {"query": {"match": {"title": "java"}}}},{"constant_score": {"query": {"match": {"title": "spark"}}}}]}}}

28_搜索_function_score自定义相关度分数

前言

在使用 Elasticsearch 进行全文搜索时,搜索结果默认会以文档的相关度进行排序,如果想要改变默认的排序规则,也可以通过sort指定一个或多个排序字段。但是使用sort排序过于绝对,它会直接忽略掉文档本身的相关度(根本不会去计算)。在很多时候这样做的效果并不好,这时候就需要对多个字段进行综合评估,得出一个最终的排序。Elasticsearch提供了function_score功能来满足我们的需求。

function_score简介

在使用ES进行全文搜索时,搜索结果默认会以文档的相关度进行排序,而这个 “文档的相关度”,是可以透过 function_score 自己定义的,也就是说我们可以透过使用function_score,来控制 “怎麽样的文档相关度更高” 这件事

function_score是专门用于处理文档_score的DSL,它允许爲每个主查询query匹配的文档应用加强函数, 以达到改变原始查询评分 score的目的。

function_score会在主查询query结束后对每一个匹配的文档进行一系列的重打分操作,能够对多个字段一起进行综合评估,且能够使用 filter 将结果划分爲多个子集 (每个特性一个filter),并为每个子集使用不同的加强函数。

我们可以做到自定义一个function_score函数,自己将某个field的值,跟es内置算出来的分数进行运算,然后由自己指定的field来进行分数的增强

function_score参数

3.1 function_score 提供了几种加强_score计算的函数:

weight : 设置一个简单而不被规范化的权重提升值。

weight加强函数和 boost参数类似,可以用于任何查询,不过有一点差别是weight不会被Lucene nomalize成难以理解的浮点数,而是直接被应用 (boost会被nomalize)

例如当 weight 爲 2 时,最终结果爲new_score = old_score * 2

field_value_factor : 将某个字段的值乘上old_score。

像是将 字段shareCount 或是 字段likiCount 作爲考虑因素,new_score = old_score * 那个文档的likeCount的值

random_score : 爲每个用户都使用一个不同的随机评分对结果排序,但对某一具体用户来说,看到的顺序始终是一致的。

衰减函数 (linear、exp、guass) : 以某个字段的值为基准,距离某个值越近得分越高。

script_score : 当需求超出以上范围时,可以用自定义脚本完全控制评分计算,不过因为还要额外维护脚本不好维护,因此尽量使用ES提供的评分函数,需求真的无法满足再使用script_score。

3.2 function_scroe其他辅助的参数

3.2.1 boost_mode

boost_mode决定 old_score 和 加强score 如何合并。

multiply(默认) : new_score = old_score * 加强score

sum : new_score = old_score + 加强score

min : old_score 和 加强score 取较小值,new_score = min(old_score, 加强score)

max : old_score 和 加强score 取较大值,new_score = max(old_score, 加强score)

replace : 加强score直接替换掉old_score,new_score = 加强score

3.2.2 score_mode

score_mode决定functions裡面的加强score们怎麽合併,会先合併加强score们成一个总加强score,再使用总加强score去和old_score做合併,换言之就是会先执行score_mode,再执行boost_mode。

multiply (默认)

sum

avg

first : 使用首个函数(可以有filter,也可以没有)的结果作为最终结果。

max

min

3.2.3 max_boost

max_boost : 限制加强函数的最大效果,就是限制加强score最大能多少,但要注意不会限制old_score。

如果加强score超过了max_boost限制的值,会把加强score的值设成max_boost的值。

假设加强score是5,而max_boost是2,因为加强score超出了max_boost的限制,所以max_boost就会把加强score改为2。简单的说,就是加强score = min(加强score, max_boost)

4 function_score查询模板

如果要使用function_score改变分数,要使用function_score查询。简单的说,就是在一个function_score内部的query的全文搜索得到的_score基础上,给他加上其他字段的评分标准,就能够得到把 “全文搜索 + 其他字段” 综合起来评分的效果。

4.1 单个加强函数的查询模板

GET 127.0.0.1/mytest/doc/_search{"query": {"function_score": {"query": {.....}, //主查询,查询完后这裡自己会有一个评分,就是old_score"field_value_factor": {...}, //在old_score的基础上,给他加强其他字段的评分,这裡会产生一个加强score,如果只有一个加强function时,直接将加强函数名写在query下面就可以了"boost_mode": "multiply", //指定用哪种方式结合old_score和加强score成为new_score"max_boost": 1.5 //限制加强score的最高分,但是不会限制old_score}}}

4.2 多个加强函数的查询模板

如果有多个加强函数,那就要使用functions来包含这些加强函数们,functions是一个数组,裡面放著的是将要被使用的加强函数列表。

可以为functions裡的加强函数指定一个filter,这样做的话,只有在文档满足此filter的要求,此filter的加强函数才会应用到文挡上,也可以不指定filter,这样的话此加强函数就会应用到全部的文挡上。

一个文档可以一次满足多条加强函数和多个filter,如果一次满足多个,那麽就会产生多个加强score,因此ES会使用score_mode定义的方式来合併这些加强score们,得到一个总加强score,得到总加强score之后,才会再使用boost_mode定义的方式去和old_score做合并。

例如: 下面的例子,field_value_factor和gauss这两个加强函数会应用到所有文档上,而weight只会应用到满足filter的文档上,假设有个文档满足了filter的条件,那他就会得到3个加强score,这3个加强score会使用sum的方式合併成一个总加强score,然后才和old_score使用multiply的方式合并。

GET 127.0.0.1/mytest/doc/_search{"query": {"function_score": {"query": {.....},"functions": [ //可以有多个加强函数(或是filter+加强函数),每一个加强函数会产生一个加强score,因此functions会有多个加强score{"field_value_factor": ... },{"gauss": ... },{"filter": {...}, "weight": ... }],"score_mode": "sum", //决定加强score们怎麽合併,"boost_mode": "multiply" //決定總加強score怎麼和old_score合併}}}

5总结

不要执著在调整function_score上,文档相关度的调整非常玄,“最相关的文档” 是一个难以触及的模糊概念,每个人对文档排序有著不同的想法,这很容易使人陷入持续反覆调整,但是确没有明显的进展。为了避免跳入这种死循环,在调整function_score时,一定要搭配监控用户操作,才有意义。

例如:如果返回的文档是用户想要的高相关的文档,那麽用户就会选择前10个中的一个文档,得到想要的结果,反之,用户可能会来回点击,或是尝试新的搜索条件。一旦有了这些监控手段,想要调适完美的function_score就不是问题。

因此,调整function_score的重点在于,要透过监控用户、和用户互动,慢慢去调整我们的搜索条件,而不要妄想一步登天,第一次就把文档的相关度调整到最好,这几乎是不可能的,因为,连用户自己也不知道他自己想要什麽。

function_score (field_value_factor具体实例)

给所有的帖子数据增加follower数量

POST /forum/article/_bulk{"update": {"_id": "1"} }{"doc" : {"follower_num" : 5} }{"update": {"_id": "2"} }{"doc" : {"follower_num" : 10} }{"update": {"_id": "3"} }{"doc" : {"follower_num" : 25} }{"update": {"_id": "4"} }{"doc" : {"follower_num" : 3} }{"update": {"_id": "5"} }{"doc" : {"follower_num" : 60} }

将对帖子搜索得到的分数,跟follower_num进行运算,由follower_num在一定程度上增强帖子的分数

看帖子的人越多,那么帖子的分数就越高

GET /forum/article/_search{"query": {"function_score": {"query": {"multi_match": {"query": "java spark","fields": ["tile", "content"]}},"field_value_factor": {"field": "follower_num","modifier": "log1p","factor": 0.5},"boost_mode": "sum","max_boost": 2}}}

如果只有field,那么会将每个doc的分数都乘以follower_num,如果有的doc follower是0,那么分数就会变为0,效果很不好。因此一般会加个log1p函数,公式会变为,new_score = old_score * log(1 + number_of_votes),这样出来的分数会比较合理

再加个factor,可以进一步影响分数,new_score = old_score * log(1 + factor * number_of_votes)

boost_mode,可以决定分数与指定字段的值如何计算,multiply,sum,min,max,replace

max_boost,限制计算出来的分数不要超过max_boost指定的值

数据准备

首先准备数据和索引,在ES插入三条数据,其中title是text类型,like是integer类型(代表点赞量)。

{"title": "ES 入门", "like": 2 }{"title": "ES 进阶", "like": 5 }{"title": "ES 最高难度", "like": 10 }

2function_score (field_value_factor)示例2.1 普通查询

先使用一般的query,查看普通的查询的评分会是如何:

GET 127.0.0.1/mytest/doc/_search{"query": {"match": {"title": "ES"}}}"hits": [{"_score": 0.2876821,"_source": {"title": "ES 入门", "like": 2 }},{"_score": 0.20309238,"_source": {"title": "ES 进阶", "like": 5 }},{"_score": 0.16540512,"_source": {"title": "ES 最高难度", "like": 10 }}]

2.2 function_score查询(field_value_factor)2.2.1 field_value_factor

使用function_score的field_value_factor改变_score,将old_score乘上like的值。

{"query": {"function_score": {"query": {"match": {"title": "ES"}},"field_value_factor": {"field": "like"}}}}"hits": [{"_score": 1.6540513, //原本是0.16540512"_source": {"title": "ES 最高难度", "like": 10 }},{"_score": 1.0154619, //原本是0.20309238"_source": {"title": "ES 进阶", "like": 5 }},{"_score": 0.5753642, //原本是0.2876821"_source": {"title": "ES 入门", "like": 2 }}]

本来 “ES最高难度” 的score是0.16540512,经过field_value_factor的改变,乘上了那个文档中的like值(10)之后,新的score变为 1.6540513

2.2.2 max_boost

加上max_boost,限制field_value_factor的最大加强score。

GET 127.0.0.1/mytest/doc/_search{"query": {"function_score": {"query": {"match": {"title": "ES"}},"field_value_factor": {"field": "like"},"max_boost": 3}}}"hits": [{"_score": 0.6092771, //原本是0.20309238"_source": {"title": "ES 进阶", "like": 5 }},{"_score": 0.5753642, //原本是0.2876821"_source": {"title": "ES 入门", "like": 2 }},{"_score": 0.49621537, //原本是0.16540512"_source": {"title": "ES 最高难度", "like": 10 }}]

可以看到“ES入门”的加强score是2,在max_boost限制裡,所以不受影响。而“ES进阶”和“ES最高难度”的field_value_factor函数产生的加强score因为超过max_boost的限制,所以被设为3。

2.2.3 field_value_factor中还支持 modifier、factor 参数

有时候线性的计算new_score = old_score * like值的效果并不是那麽好,field_value_factor中还支持 modifier、factor 参数,可以改变like值对old_score的影响。

modifier参数支持的值:

none : new_score = old_score * like值

默认状态就是none,线性。

log1p : new_score = old_score * log(1 + like值)

最常用,可以让like值字段的评分曲线更平滑。

log2p : new_score = old_score * log(2 + like值)

ln : new_score = old_score * ln(like值)

ln1p : new_score = old_score * ln(1 + like值)

ln2p : new_score = old_score * ln(2 + like值)

square : 计算平方

sqrt : 计算平方根

reciprocal : 计算倒数

factor参数:

factor作为一个调节用的参数,没有modifier那麽强大会改变整个曲线,他仅改变一些常量值,设置factor>1会提昇效果,factor<1会降低效果。假设modifier是log1p,那麽加入了factor的公式就是new_score = old_score * log(1 + factor * like值)。

对刚刚的例子加上 modifier、factor参数的查询语句如下:

GET 127.0.0.1/mytest/doc/_search{"query": {"function_score": {"query": {"match": {"title": "ES"}},"field_value_factor": {"field": "like","modifier": "log1p","factor": 2}}}}

2.2.4 boost_mode

就算加上了modifier,但是 “全文评分 与 field_value_factor函数值乘积” 的效果可能还是太大,我们可以通过参数boost_mode来决定 old_score 和 加强score 合併的方法。

如果将boost_mode改成sum,可以大幅弱化最终效果,特别是使用一个较小的factor时;

加入了boost_mode=sum、且factor=0.1的公式变为new_score = old_score + log(1 + 0.1 * like值);

对刚刚的例子加上max_boost参数的查询语句如下:

GET 127.0.0.1/mytest/doc/_search{"query": {"function_score": {"query": {"match": {"title": "ES"}},"field_value_factor": {"field": "like","modifier": "log1p","factor": 0.1},"boost_mode": "sum"}}}

function_score (衰减函数 linear、exp、gauss 具体实例)

前言

很多变量都可以影响用户对于酒店的选择,像是用户可能希望酒店离市中心近一点,但是如果价格足够便宜,也愿意为了省钱,妥协选择一个更远的住处。如果我们只是使用一个 filter 排除所有市中心方圆 100 米以外的酒店,再用一个filter排除每晚价格超过100元的酒店,这种作法太过强硬,可能有一间房在 500米,但是超级便宜一晚只要10元,用户可能会因此愿意妥协住这间房。

为了解决这个问题,因此function_score查询提供了一组 衰减函数 (decay functions), 让我们有能力在两个滑动标准(如地点和价格)之间权衡。

2function_score支持的衰减函数2.1 三种衰减函数(linear、exp、gauss)

function_score支持的衰减函数有三种,分别是 linear、exp 和 gauss。linear、exp、gauss三种衰减函数的差别只在于衰减曲线的形状,在DSL的语法上的用法完全一样。

linear : 线性函数是条直线,一旦直线与横轴0相交,所有其他值的评分都是0。

exp : 指数函数是先剧烈衰减然后变缓。

guass(最常用) : 高斯函数则是钟形的,他的衰减速率是先缓慢,然后变快,最后又放缓。

2.2 衰减函数们 (linear、exp、gauss) 支持的参数

origin: 中心点,或是字段可能的最佳值,落在原点(origin)上的文档评分_score为满分1.0,支持数值、时间 以及 “经纬度地理座标点”(最常用) 的字段。

offset: 从 origin 为中心,为他设置一个偏移量offset覆盖一个范围,在此范围内所有的评分_score也都是和origin一样满分1.0。

scale: 衰减率,即是一个文档从origin下落时,_score改变的速度。

decay: 从 origin 衰减到 scale 所得的评分_score,默认为0.5 (一般不需要改变,这个参数使用默认的就好了)。

以上面的图为例:

所有曲线(linear、exp、gauss)的origin都是40,offset是5,因此范围在40-5 <= value <= 40+5的文档的评分_score都是满分1.0。

而在此范围之外,评分会开始衰减,衰减率由scale值(此处是5)和decay值(此处是默认值0.5)决定,在origin +/- (offset + scale)处的评分是decay值,也就是在30、50的评分处是0.5分。

也就是说,在origin + offset + scale或是origin - offset - scale的点上,得到的分数仅有decay分。

3具体实例3.1 具体实例-1

先准备数据和索引,在ES插入三条数据,其中language是keywork类型,like是integer类型(代表点赞量):

{"language": "java", "like": 5 }{"language": "python", "like": 10 }{"language": "go", "like": 15 }

以like=15为中心,使用gauss函数:

GET 127.0.0.1/mytest/doc/_search{"query": {"function_score": {"query": {"match_all": {}},"functions": [{"gauss": {"like": {"origin": "15", //如果不设置offset,offset默认为0"scale": "5","decay": "0.2"}}}]}}}"hits": [{"_score": 1,"_source": {"language": "go", "like": 15 }},{"_score": 0.2, //因为改变了decay=0.2,所以当位于 origin-offset-scale=10 的位置时,分数为decay,就是0.2"_source": {"language": "python", "like": 10 }},{"_score": 0.0016,"_source": {"language": "java", "like": 5 }}]

3.2 具体实例-2

假设有一个用户希望租一个离市中心近一点的酒店,且每晚不超过100元的酒店,而且与距离相比,我们的用户对价格更敏感,那麽使用衰减函数guass查询如下:

其中把price语句的origin点设为50是有原因的,由于价格的特性一定是越低越好,所以0~100元的所有价格的酒店都应该认为是比较好的,而100元以上的酒店就慢慢衰减。

如果我们将price的origin点设置成100,那麽价格低于100元的酒店的评分反而会变低,这不是我们期望的结果,与其这样不如将origin和offset同时设成50,只让price大于100元时评分才会变低

虽然这样设置也会使得price小于0元的酒店评分降低没错,不过现实生活中价格不会有负数,因此就算price<0的评分会下降,也不会对我们的搜索结果造成影响(酒店的价格一定都是正的)。

换句话说,其实只要把origin + offset的值设为100,origin或offset是什麽样的值都无所谓,只要能确保酒店价格在100元以上的酒店会衰减就好了。

GET 127.0.0.1/mytest/doc/_search{"query": {"function_score": {"functions": [//第一个gauss加强函数,决定距离的衰减率{"gauss": {"location": {"origin": {//origin点设成酒店的经纬度座标"lat": 51.5,"lon": 0.12},"offset": "2km", //距离中心点2km以内都是满分1.0,2km外开始衰减"scale": "3km" //衰减率}}},//第二个gauss加强函数,决定价格的衰减率,因为用户对价格更敏感,所以给了这个gauss加强函数2倍的权重{"gauss": {"price": {"origin": "50", "offset": "50","scale": "20"}},"weight": 2}]}}}

29_搜索_fuzzy误拼写模糊搜索、_delete_by_query

搜索的时候,可能输入的搜索文本会出现误拼写的情况

doc1: hello world

doc2: hello java

搜索:hallo world

fuzzy搜索技术 --> 自动将拼写错误的搜索文本,进行纠正,纠正以后去尝试匹配索引中的数据

POST /my_index/my_type/_bulk{"index": {"_id": 1 }}{"text": "Surprise me!"}{"index": {"_id": 2 }}{"text": "That was surprising."}{"index": {"_id": 3 }}{"text": "I wasn't surprised."}

GET /my_index/my_type/_search {"query": {"fuzzy": {"text": {"value": "surprize","fuzziness": 2}}}}

surprize --> 拼写错误 --> surprise --> s -> z

surprize --> surprise -> z -> s,纠正一个字母,就可以匹配上,所以在fuziness指定的2范围内

surprize --> surprised -> z -> s,末尾加个d,纠正了2次,也可以匹配上,在fuziness指定的2范围内

surprize --> surprising -> z -> s,去掉e,ing,3次,总共要5次,才可以匹配上,始终纠正不了

fuzzy搜索以后,会自动尝试将你的搜索文本进行纠错,然后去跟文本进行匹配

fuzziness,你的搜索文本最多可以纠正几个字母去跟你的数据进行匹配,默认如果不设置,就是2

GET /my_index/my_type/_search {"query": {"match": {"text": {"query": "SURPIZE ME","fuzziness": "AUTO","operator": "and"}}}}

curl -X POST "localhost:9200/twitter/_delete_by_query" -H 'Content-Type: application/json' -d'{"query": {"match": {"name": "测试删除"}}}

第30-58节

30_IK中文分词

之前大家会发现,我们全部是用英文在玩儿。。。好玩儿不好玩儿。。。不好玩儿

中国人,其实我们用来进行搜索的,绝大多数,都是中文应用,很少做英文的

standard:没有办法对中文进行合理分词的,只是将每个中文字符一个一个的切割开来,比如说中国人 --> 中 国 人

英语的也要学:所以说,我们利用核心知识篇的相关的知识,来把es这种英文原生的搜索引擎,先学一下; 因为有些知识点,可能用英文讲更靠谱,因为比如说analyzed,palyed,students --> stemmer,analyze,play,student。有些知识点,仅仅适用于英文,不太适用于中文

从这一讲开始,大家就会觉得很爽,因为全部都是我们熟悉的中文了,没有英文了,高阶知识点,搜索,聚合,全部是中文了

在搜索引擎领域,比较成熟和流行的,就是ik分词器

中国人很喜欢吃油条

standard:中 国 人 很 喜 欢 吃 油 条

ik:中国人 很 喜欢 吃 油条

1、在elasticsearch中安装ik中文分词器

(1)git clone /medcl/elasticsearch-analysis-ik(2)git checkout tags/v5.2.0(3)mvn package(4)将target/releases/elasticsearch-analysis-ik-5.2.0.zip拷贝到es/plugins/ik目录下(5)在es/plugins/ik下对elasticsearch-analysis-ik-5.2.0.zip进行解压缩(6)重启es

2、ik分词器基础知识

两种analyzer,你根据自己的需要自己选吧,但是一般是选用ik_max_word

ik_max_word: 会将文本做最细粒度的拆分,比如会将“中华人民共和国国歌”拆分为“中华人民共和国,中华人民,中华,华人,人民共和国,人民,人,民,共和国,共和,和,国国,国歌”,会穷尽各种可能的组合;

ik_smart: 会做最粗粒度的拆分,比如会将“中华人民共和国国歌”拆分为“中华人民共和国,国歌”。

共和国 --> 中华人民共和国和国歌,搜到吗????

3、ik分词器的使用

PUT /my_index {"mappings": {"my_type": {"properties": {"text": {"type": "text","analyzer": "ik_max_word"}}}}}

POST /my_index/my_type/_bulk{"index": {"_id": "1"} }{"text": "男子偷上万元发红包求交女友 被抓获时仍然单身" }{"index": {"_id": "2"} }{"text": "16岁少女为结婚“变”22岁 7年后想离婚被法院拒绝" }{"index": {"_id": "3"} }{"text": "深圳女孩骑车逆行撞奔驰 遭索赔被吓哭(图)" }{"index": {"_id": "4"} }{"text": "女人对护肤品比对男票好?网友神怼" }{"index": {"_id": "5"} }{"text": "为什么国内的街道招牌用的都是红黄配?" }

GET /my_index/_analyze{"text": "男子偷上万元发红包求交女友 被抓获时仍然单身","analyzer": "ik_max_word"}GET /my_index/my_type/_search {"query": {"match": {"text": "16岁少女结婚好还是单身好?"}}}

31_IK中文分词_配置文件以及自定义词库

1、ik配置文件

ik配置文件地址:es/plugins/ik/config目录

IKAnalyzer.cfg.xml:用来配置自定义词库

main.dic:ik原生内置的中文词库,总共有27万多条,只要是这些单词,都会被分在一起

quantifier.dic:放了一些单位相关的词

suffix.dic:放了一些后缀

surname.dic:中国的姓氏

stopword.dic:英文停用词

ik原生最重要的两个配置文件

main.dic:包含了原生的中文词语,会按照这个里面的词语去分词

stopword.dic:包含了英文的停用词

停用词,stopword

a the and at but

一般,像停用词,会在分词的时候,直接被干掉,不会建立在倒排索引中

2、自定义词库

(1)自己建立词库:每年都会涌现一些特殊的流行词,网红,蓝瘦香菇,喊麦,鬼畜,一般不会在ik的原生词典里

自己补充自己的最新的词语,到ik的词库里面去

IKAnalyzer.cfg.xml:ext_dict,custom/mydict.dic

补充自己的词语,然后需要重启es,才能生效

(2)自己建立停用词库:比如了,的,啥,么,我们可能并不想去建立索引,让人家搜索

custom/ext_stopword.dic,已经有了常用的中文停用词,可以补充自己的停用词,然后重启es

32_IK中文分词_修改IK分词器源码来基于mysql热更新词库

每次都是在es的扩展词典中,手动添加新词语,很坑

(1)每次添加完,都要重启es才能生效,非常麻烦

(2)es是分布式的,可能有数百个节点,你不能每次都一个一个节点上面去修改

es不停机,直接我们在外部某个地方添加新的词语,es中立即热加载到这些新词语

热更新的方案

(1)修改ik分词器源码,然后手动支持从mysql中每隔一定时间,自动加载新的词库

(2)基于ik分词器原生支持的热更新方案,部署一个web服务器,提供一个http接口,通过modified和tag两个http响应头,来提供词语的热更新

用第一种方案,第二种,ik git社区官方都不建议采用,觉得不太稳定

1、下载源码

/medcl/elasticsearch-analysis-ik/tree/v5.2.0

ik分词器,是个标准的java maven工程,直接导入eclipse就可以看到源码

2、修改源码

Dictionary类,169行:Dictionary单例类的初始化方法,在这里需要创建一个我们自定义的线程,并且启动它

/*** 词典初始化 由于IK Analyzer的词典采用Dictionary类的静态方法进行词典初始化* 只有当Dictionary类被实际调用时,才会开始载入词典, 这将延长首次分词操作的时间 该方法提供了一个在应用加载阶段就初始化字典的手段** @return Dictionary*/public static synchronized Dictionary initial(Configuration cfg) {if (singleton == null) {synchronized (Dictionary.class) {if (singleton == null) {singleton = new Dictionary(cfg);singleton.loadMainDict();singleton.loadSurnameDict();singleton.loadQuantifierDict();singleton.loadSuffixDict();singleton.loadPrepDict();singleton.loadStopWordDict();new Thread(new HotDictReloadThread()).start();if(cfg.isEnableRemoteDict()){// 建立监控线程for (String location : singleton.getRemoteExtDictionarys()) {// 10 秒是初始延迟可以修改的 60是间隔时间 单位秒pool.scheduleAtFixedRate(new Monitor(location), 10, 60, TimeUnit.SECONDS);}for (String location : singleton.getRemoteExtStopWordDictionarys()) {pool.scheduleAtFixedRate(new Monitor(location), 10, 60, TimeUnit.SECONDS);}}return singleton;}}}return singleton;}

HotDictReloadThread类:就是死循环,不断调用Dictionary.getSingleton().reLoadMainDict(),去重新加载词典

package org.wltea.analyzer.dic;import org.apache.logging.log4j.Logger;import mon.logging.ESLoggerFactory;public class HotDictReloadThread implements Runnable {private static final Logger logger = ESLoggerFactory.getLogger(HotDictReloadThread.class.getName());@Overridepublic void run() {while(true) {logger.info("[==========]reload hot dict from mysql......"); Dictionary.getSingleton().reLoadMainDict();}}}

Dictionary类,389行:this.loadMySQLExtDict();

/*** 加载主词典及扩展词典*/private void loadMainDict() {// 建立一个主词典实例_MainDict = new DictSegment((char) 0);// 读取主词典文件Path file = PathUtils.get(getDictRoot(), Dictionary.PATH_DIC_MAIN);InputStream is = null;try {is = new FileInputStream(file.toFile());} catch (FileNotFoundException e) {logger.error(e.getMessage(), e);}try {BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"), 512);String theWord = null;do {theWord = br.readLine();if (theWord != null && !"".equals(theWord.trim())) {_MainDict.fillSegment(theWord.trim().toCharArray());}} while (theWord != null);} catch (IOException e) {logger.error("ik-analyzer", e);} finally {try {if (is != null) {is.close();is = null;}} catch (IOException e) {logger.error("ik-analyzer", e);}}// 加载扩展词典this.loadExtDict();// 加载远程自定义词库this.loadRemoteExtDict();// 从mysql加载词典this.loadMySQLExtDict();}

/*** 从mysql加载热更新词典*/private void loadMySQLExtDict() {Connection conn = null;Statement stmt = null;ResultSet rs = null;try {Path file = PathUtils.get(getDictRoot(), "jdbc-reload.properties"); prop.load(new FileInputStream(file.toFile()));logger.info("[==========]jdbc-reload.properties");for(Object key : prop.keySet()) {logger.info("[==========]" + key + "=" + prop.getProperty(String.valueOf(key)));}logger.info("[==========]query hot dict from mysql, " + prop.getProperty("jdbc.reload.sql") + "......"); conn = DriverManager.getConnection(prop.getProperty("jdbc.url"), prop.getProperty("jdbc.user"), prop.getProperty("jdbc.password")); stmt = conn.createStatement();rs = stmt.executeQuery(prop.getProperty("jdbc.reload.sql")); while(rs.next()) {String theWord = rs.getString("word");logger.info("[==========]hot word from mysql: " + theWord);_MainDict.fillSegment(theWord.trim().toCharArray());}Thread.sleep(Integer.valueOf(String.valueOf(prop.get("jdbc.reload.interval")))); } catch (Exception e) {logger.error("erorr", e);} finally {if(rs != null) {try {rs.close();} catch (SQLException e) {logger.error("error", e);}}if(stmt != null) {try {stmt.close();} catch (SQLException e) {logger.error("error", e);}}if(conn != null) {try {conn.close();} catch (SQLException e) {logger.error("error", e);}}}}

Dictionary类,683行:this.loadMySQLStopwordDict();

/*** 加载用户扩展的停止词词典*/private void loadStopWordDict() {// 建立主词典实例_StopWords = new DictSegment((char) 0);// 读取主词典文件Path file = PathUtils.get(getDictRoot(), Dictionary.PATH_DIC_STOP);InputStream is = null;try {is = new FileInputStream(file.toFile());} catch (FileNotFoundException e) {logger.error(e.getMessage(), e);}try {BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"), 512);String theWord = null;do {theWord = br.readLine();if (theWord != null && !"".equals(theWord.trim())) {_StopWords.fillSegment(theWord.trim().toCharArray());}} while (theWord != null);} catch (IOException e) {logger.error("ik-analyzer", e);} finally {try {if (is != null) {is.close();is = null;}} catch (IOException e) {logger.error("ik-analyzer", e);}}// 加载扩展停止词典List<String> extStopWordDictFiles = getExtStopWordDictionarys();if (extStopWordDictFiles != null) {is = null;for (String extStopWordDictName : extStopWordDictFiles) {logger.info("[Dict Loading] " + extStopWordDictName);// 读取扩展词典文件file = PathUtils.get(getDictRoot(), extStopWordDictName);try {is = new FileInputStream(file.toFile());} catch (FileNotFoundException e) {logger.error("ik-analyzer", e);}// 如果找不到扩展的字典,则忽略if (is == null) {continue;}try {BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"), 512);String theWord = null;do {theWord = br.readLine();if (theWord != null && !"".equals(theWord.trim())) {// 加载扩展停止词典数据到内存中_StopWords.fillSegment(theWord.trim().toCharArray());}} while (theWord != null);} catch (IOException e) {logger.error("ik-analyzer", e);} finally {try {if (is != null) {is.close();is = null;}} catch (IOException e) {logger.error("ik-analyzer", e);}}}}// 加载远程停用词典List<String> remoteExtStopWordDictFiles = getRemoteExtStopWordDictionarys();for (String location : remoteExtStopWordDictFiles) {logger.info("[Dict Loading] " + location);List<String> lists = getRemoteWords(location);// 如果找不到扩展的字典,则忽略if (lists == null) {logger.error("[Dict Loading] " + location + "加载失败");continue;}for (String theWord : lists) {if (theWord != null && !"".equals(theWord.trim())) {// 加载远程词典数据到主内存中logger.info(theWord);_StopWords.fillSegment(theWord.trim().toLowerCase().toCharArray());}}}this.loadMySQLStopwordDict();}

/*** 从mysql加载停用词*/private void loadMySQLStopwordDict() {Connection conn = null;Statement stmt = null;ResultSet rs = null;try {Path file = PathUtils.get(getDictRoot(), "jdbc-reload.properties"); prop.load(new FileInputStream(file.toFile()));logger.info("[==========]jdbc-reload.properties");for(Object key : prop.keySet()) {logger.info("[==========]" + key + "=" + prop.getProperty(String.valueOf(key)));}logger.info("[==========]query hot stopword dict from mysql, " + prop.getProperty("jdbc.reload.stopword.sql") + "......"); conn = DriverManager.getConnection(prop.getProperty("jdbc.url"), prop.getProperty("jdbc.user"), prop.getProperty("jdbc.password")); stmt = conn.createStatement();rs = stmt.executeQuery(prop.getProperty("jdbc.reload.stopword.sql")); while(rs.next()) {String theWord = rs.getString("word");logger.info("[==========]hot stopword from mysql: " + theWord);_StopWords.fillSegment(theWord.trim().toCharArray());}Thread.sleep(Integer.valueOf(String.valueOf(prop.get("jdbc.reload.interval")))); } catch (Exception e) {logger.error("erorr", e);} finally {if(rs != null) {try {rs.close();} catch (SQLException e) {logger.error("error", e);}}if(stmt != null) {try {stmt.close();} catch (SQLException e) {logger.error("error", e);}}if(conn != null) {try {conn.close();} catch (SQLException e) {logger.error("error", e);}}}}

3、mvn package打包代码

target\releases\elasticsearch-analysis-ik-5.2.0.zip

4、解压缩ik压缩包

在elasticsearch-5.2.0\plugins\ik 对压缩文件进行解压缩

将mysql驱动jar,放入ik的目录下

5、修改jdbc相关配置

6、重启es

观察日志,日志中就会显示我们打印的那些东西,比如加载了什么配置,加载了什么词语,什么停用词

7 测试,mysql添加前

测试分词

GET _analyze{"text": "一人饮酒","analyzer": "ik_max_word"}

结果:

{"tokens": [{"token": "一人","start_offset": 0,"end_offset": 2,"type": "CN_WORD","position": 0},{"token": "一","start_offset": 0,"end_offset": 1,"type": "TYPE_CNUM","position": 1},{"token": "人","start_offset": 1,"end_offset": 2,"type": "COUNT","position": 2},{"token": "饮酒","start_offset": 2,"end_offset": 4,"type": "CN_WORD","position": 3},{"token": "饮","start_offset": 2,"end_offset": 3,"type": "CN_WORD","position": 4},{"token": "酒","start_offset": 3,"end_offset": 4,"type": "CN_WORD","position": 5}]}

次数停用词

8在mysql中添加词库与停用词

添加分词

添加停用词

一看日志已经加载出来

9分词实验,验证热更新生效

分词实验

停用词实验

33_聚合分析_bucket与metric两个核心概念的讲解

课程大纲

1、文本编辑器介绍

(1)windows操作系统,原生的txt文本编辑器,一些json格式,不太方便去调整

(2)notepad++,功能不是太丰富

(3)sublime,整个功能也比较丰富,比较好,自己可以上网去下载,官网,免费的

2、两个核心概念:bucket和metric

bucket:一个数据分组

city name

北京 小李

北京 小王

上海 小张

上海 小丽

上海 小陈

基于city划分buckets

划分出来两个bucket,一个是北京bucket,一个是上海bucket

北京bucket:包含了2个人,小李,小王

上海bucket:包含了3个人,小张,小丽,小陈

按照某个字段进行bucket划分,那个字段的值相同的那些数据,就会被划分到一个bucket中

有一些mysql的sql知识的话,聚合,首先第一步就是分组,对每个组内的数据进行聚合分析,分组,就是我们的bucket

metric:对一个数据分组执行的统计

当我们有了一堆bucket之后,就可以对每个bucket中的数据进行聚合分词了,比如说计算一个bucket内所有数据的数量,或者计算一个bucket内所有数据的平均值,最大值,最小值

metric,就是对一个bucket执行的某种聚合分析的操作,比如说求平均值,求最大值,求最小值

select count(*)

from access_log

group by user_id

bucket:group by user_id --> 那些user_id相同的数据,就会被划分到一个bucket中

metric:count(*),对每个user_id bucket中所有的数据,计算一个数量

34_聚合分析_aggs+terms

课程大纲

1、家电卖场案例背景

以一个家电卖场中的电视销售数据为背景,来对各种品牌,各种颜色的电视的销量和销售额,进行各种各样角度的分析

PUT /tvs{"mappings": {"sales": {"properties": {"price": {"type": "long"},"color": {"type": "keyword"},"brand": {"type": "keyword"},"sold_date": {"type": "date"}}}}}

POST /tvs/sales/_bulk{"index": {}}{"price" : 1000, "color" : "红色", "brand" : "长虹", "sold_date" : "-10-28" }{"index": {}}{"price" : 2000, "color" : "红色", "brand" : "长虹", "sold_date" : "-11-05" }{"index": {}}{"price" : 3000, "color" : "绿色", "brand" : "小米", "sold_date" : "-05-18" }{"index": {}}{"price" : 1500, "color" : "蓝色", "brand" : "TCL", "sold_date" : "-07-02" }{"index": {}}{"price" : 1200, "color" : "绿色", "brand" : "TCL", "sold_date" : "-08-19" }{"index": {}}{"price" : 2000, "color" : "红色", "brand" : "长虹", "sold_date" : "-11-05" }{"index": {}}{"price" : 8000, "color" : "红色", "brand" : "三星", "sold_date" : "-01-01" }{"index": {}}

{ “price” : 2500, “color” : “蓝色”, “brand” : “小米”, “sold_date” : “-02-12” }

2、统计各种颜色的电视销量

GET /tvs/sales/_search{"size" : 0,"aggs" : {"popular_colors" : {"terms" : {"field" : "color"}}}}

size:只获取聚合结果,而不要执行聚合的原始数据

aggs:固定语法,要对一份数据执行分组聚合操作

popular_colors:就是对每个aggs,都要起一个名字,这个名字是随机的,你随便取什么都ok

terms:根据字段的值进行分组

field:根据指定的字段的值进行分组

{"took": 61,"timed_out": false,"_shards": {"total": 5,"successful": 5,"failed": 0},"hits": {"total": 8,"max_score": 0,"hits": []},"aggregations": {"popular_color": {"doc_count_error_upper_bound": 0,"sum_other_doc_count": 0,"buckets": [{"key": "红色","doc_count": 4},{"key": "绿色","doc_count": 2},{"key": "蓝色","doc_count": 2}]}}}

hits.hits:我们指定了size是0,所以hits.hits就是空的,否则会把执行聚合的那些原始数据给你返回回来

aggregations:聚合结果

popular_color:我们指定的某个聚合的名称

buckets:根据我们指定的field划分出的buckets

key:每个bucket对应的那个值

doc_count:这个bucket分组内,有多少个数据

数量,其实就是这种颜色的销量

每种颜色对应的bucket中的数据的

默认的排序规则:按照doc_count降序排序

35_聚合分析_aggs+terms+avg统计每种颜色电视平均价格

课程大纲

GET /tvs/sales/_search{"size" : 0,"aggs": {"colors": {"terms": {"field": "color"},"aggs": {"avg_price": {"avg": {"field": "price" }}}}}}

按照color去分bucket,可以拿到每个color bucket中的数量,这个仅仅只是一个bucket操作,doc_count其实只是es的bucket操作默认执行的一个内置metric

这一讲,就是除了bucket操作,分组,还要对每个bucket执行一个metric聚合统计操作

在一个aggs执行的bucket操作(terms),平级的json结构下,再加一个aggs,这个第二个aggs内部,同样取个名字,执行一个metric操作,avg,对之前的每个bucket中的数据的指定的field,price field,求一个平均值

“aggs”: {

“avg_price”: {

“avg”: {

“field”: “price”

}

}

}

就是一个metric,就是一个对一个bucket分组操作之后,对每个bucket都要执行的一个metric

第一个metric,avg,求指定字段的平均值

{"took": 28,"timed_out": false,"_shards": {"total": 5,"successful": 5,"failed": 0},"hits": {"total": 8,"max_score": 0,"hits": []},"aggregations": {"group_by_color": {"doc_count_error_upper_bound": 0,"sum_other_doc_count": 0,"buckets": [{"key": "红色","doc_count": 4,"avg_price": {"value": 3250}},{"key": "绿色","doc_count": 2,"avg_price": {"value": 2100}},{"key": "蓝色","doc_count": 2,"avg_price": {"value": 2000}}]}}}

buckets,除了key和doc_count

avg_price:我们自己取的metric aggs的名字

value:我们的metric计算的结果,每个bucket中的数据的price字段求平均值后的结果

select avg(price)

from tvs.sales

group by color

36_聚合分析_bucket嵌套实现颜色+品牌的多层下钻分析

课程大纲

从颜色到品牌进行下钻分析,每种颜色的平均价格,以及找到每种颜色每个品牌的平均价格

我们可以进行多层次的下钻

比如说,现在红色的电视有4台,同时这4台电视中,有3台是属于长虹的,1台是属于小米的

红色电视中的3台长虹的平均价格是多少?

红色电视中的1台小米的平均价格是多少?

下钻的意思是,已经分了一个组了,比如说颜色的分组,然后还要继续对这个分组内的数据,再分组,比如一个颜色内,还可以分成多个不同的品牌的组,最后对每个最小粒度的分组执行聚合分析操作,这就叫做下钻分析

es,下钻分析,就要对bucket进行多层嵌套,多次分组

按照多个维度(颜色+品牌)多层下钻分析,而且学会了每个下钻维度(颜色,颜色+品牌),都可以对每个维度分别执行一次metric聚合操作

GET /tvs/sales/_search {"size": 0,"aggs": {"group_by_color": {"terms": {"field": "color"},"aggs": {"color_avg_price": {"avg": {"field": "price"}},"group_by_brand": {"terms": {"field": "brand"},"aggs": {"brand_avg_price": {"avg": {"field": "price"}}}}}}}}

{"took": 8,"timed_out": false,"_shards": {"total": 5,"successful": 5,"failed": 0},"hits": {"total": 8,"max_score": 0,"hits": []},"aggregations": {"group_by_color": {"doc_count_error_upper_bound": 0,"sum_other_doc_count": 0,"buckets": [{"key": "红色","doc_count": 4,"color_avg_price": {"value": 3250},"group_by_brand": {"doc_count_error_upper_bound": 0,"sum_other_doc_count": 0,"buckets": [{"key": "长虹","doc_count": 3,"brand_avg_price": {"value": 1666.6666666666667}},{"key": "三星","doc_count": 1,"brand_avg_price": {"value": 8000}}]}},{"key": "绿色","doc_count": 2,"color_avg_price": {"value": 2100},"group_by_brand": {"doc_count_error_upper_bound": 0,"sum_other_doc_count": 0,"buckets": [{"key": "TCL","doc_count": 1,"brand_avg_price": {"value": 1200}},{"key": "小米","doc_count": 1,"brand_avg_price": {"value": 3000}}]}},{"key": "蓝色","doc_count": 2,"color_avg_price": {"value": 2000},"group_by_brand": {"doc_count_error_upper_bound": 0,"sum_other_doc_count": 0,"buckets": [{"key": "TCL","doc_count": 1,"brand_avg_price": {"value": 1500}},{"key": "小米","doc_count": 1,"brand_avg_price": {"value": 2500}}]}}]}}}

37_聚合分析_统计每种颜色电视最大最小价格

课程大纲

要学更多的metric

count,avg

count:bucket,terms,自动就会有一个doc_count,就相当于是count

avg:avg aggs,求平均值

max:求一个bucket内,指定field值最大的那个数据

min:求一个bucket内,指定field值最小的那个数据

sum:求一个bucket内,指定field值的总和

一般来说,90%的常见的数据分析的操作,metric,无非就是count,avg,max,min,sum

m

GET /tvs/sales/_search{"size" : 0,"aggs": {"colors": {"terms": {"field": "color"},"aggs": {"avg_price": {"avg": {"field": "price" } },"min_price" : {"min": {"field": "price"} }, "max_price" : {"max": {"field": "price"} },"sum_price" : {"sum": {"field": "price" } } }}}}

求总和,就可以拿到一个颜色下的所有电视的销售总额

{"took": 16,"timed_out": false,"_shards": {"total": 5,"successful": 5,"failed": 0},"hits": {"total": 8,"max_score": 0,"hits": []},"aggregations": {"group_by_color": {"doc_count_error_upper_bound": 0,"sum_other_doc_count": 0,"buckets": [{"key": "红色","doc_count": 4,"max_price": {"value": 8000},"min_price": {"value": 1000},"avg_price": {"value": 3250},"sum_price": {"value": 13000}},{"key": "绿色","doc_count": 2,"max_price": {"value": 3000},"min_price": {"value":}, 1200"avg_price": {"value": 2100},"sum_price": {"value": 4200}},{"key": "蓝色","doc_count": 2,"max_price": {"value": 2500},"min_price": {"value": 1500},"avg_price": {"value": 2000},"sum_price": {"value": 4000}}]}}}

38_聚合分析_hitogram按价格区间统计电视销量和销售额

课程大纲

histogram:类似于terms,也是进行bucket分组操作,接收一个field,按照这个field的值的各个范围区间,进行bucket分组操作

"histogram":{"field": "price","interval": 2000},

interval:2000,划分范围,02000,20004000,40006000,60008000,8000~10000,buckets

去根据price的值,比如2500,看落在哪个区间内,比如20004000,此时就会将这条数据放入20004000对应的那个bucket中

bucket划分的方法,terms,将field值相同的数据划分到一个bucket中

bucket有了之后,一样的,去对每个bucket执行avg,count,sum,max,min,等各种metric操作,聚合分析

GET /tvs/sales/_search{"size" : 0,"aggs":{"price":{"histogram":{"field": "price","interval": 2000},"aggs":{"revenue": {"sum": {"field" : "price"}}}}}}

{"took": 13,"timed_out": false,"_shards": {"total": 5,"successful": 5,"failed": 0},"hits": {"total": 8,"max_score": 0,"hits": []},"aggregations": {"group_by_price": {"buckets": [{"key": 0,"doc_count": 3,"sum_price": {"value": 3700}},{"key": 2000,"doc_count": 4,"sum_price": {"value": 9500}},{"key": 4000,"doc_count": 0,"sum_price": {"value": 0}},{"key": 6000,"doc_count: {"value":": 0,"sum_price" 0}},{"key": 8000,"doc_count": 1,"sum_price": {"value": 8000}}]}}}

39_聚合分析_date hitogram之统计每月电视销量

课程大纲

bucket,分组操作,histogram,按照某个值指定的interval,划分一个一个的bucket

date histogram,按照我们指定的某个date类型的日期field,以及日期interval,按照一定的日期间隔,去划分bucket

date interval = 1m,

-01-01~-01-31,就是一个bucket

-02-01~-02-28,就是一个bucket

然后会去扫描每个数据的date field,判断date落在哪个bucket中,就将其放入那个bucket

-01-05,就将其放入-01-01~-01-31,就是一个bucket

min_doc_count:即使某个日期interval,-01-01~-01-31中,一条数据都没有,那么这个区间也是要返回的,不然默认是会过滤掉这个区间的

extended_bounds,min,max:划分bucket的时候,会限定在这个起始日期,和截止日期内

GET /tvs/sales/_search{"size" : 0,"aggs": {"sales": {"date_histogram": {"field": "sold_date","interval": "month", "format": "yyyy-MM-dd","min_doc_count" : 0, "extended_bounds" : {"min" : "-01-01","max" : "-12-31"}}}}}{"took": 16,"timed_out": false,"_shards": {"total": 5,"successful": 5,"failed": 0},"hits": {"total": 8,"max_score": 0,"hits": []},"aggregations": {"group_by_sold_date": {"buckets": [{"key_as_string": "-01-01","key": 1451606400000,"doc_count": 0},{"key_as_string": "-02-01","key": 1454284800000,"doc_count": 0},{"key_as_string": "-03-01","key": 1456790400000,"doc_count": 0},{"key_as_string": "-04-01","key": 1459468800000,"doc_count": 0},{"key_as_string": "-05-01","key": 1462060800000,"doc_count": 1},{"key_as_string": "-06-01","key": 1464739200000,"doc_count": 0},{"key_as_string": "-07-01","key": 1467331200000,"doc_count": 1},{"key_as_strin"key_as_string": "-09-01","key": 1472688000000,"doc_count": 0},g": "-08-01","key": 1470009600000,"doc_count": 1},{{"key_as_string": "-10-01","key": 1475280000000,"doc_count": 1},{"key_as_string": "-11-01","key": 1477958400000,"doc_count": 2},{"key_as_string": "-12-01","key": 1480550400000,"doc_count": 0},{"key_as_string": "-01-01","key": 1483228800000,"doc_count": 1},{"key_as_string": "-02-01","key": 1485907200000,"doc_count": 1}]}}}

40_聚合分析_date_histogram统计每季度每个品牌的销售额

课程大纲

GET /tvs/sales/_search {"size": 0,"aggs": {"group_by_sold_date": {"date_histogram": {"field": "sold_date","interval": "quarter","format": "yyyy-MM-dd","min_doc_count": 0,"extended_bounds": {"min": "-01-01","max": "-12-31"}},"aggs": {"group_by_brand": {"terms": {"field": "brand"},"aggs": {"sum_price": {"sum": {"field": "price"}}}},"total_sum_price": {"sum": {"field": "price"}}}}}}{"took": 10,"timed_out": false,"_shards": {"total": 5,"successful": 5,"failed": 0},"hits": {"total": 8,"max_score": 0,"hits": []},"aggregations": {"group_by_sold_date": {"buckets": [{"key_as_string": "-01-01","key": 1451606400000,"doc_count": 0,"total_sum_price": {"value": 0},"group_by_brand": {"doc_count_error_upper_bound": 0,"sum_other_doc_count": 0,"buckets": []}},{"key_as_string": "-04-01","key": 1459468800000,"doc_count": 1,"total_sum_price": {"value": 3000},"group_by_brand": {"doc_count_error_upper_bound": 0,"sum_other_doc_count": 0,"buckets": [{"key": "小米","doc_count": 1,"sum_price": {"value": 3000}}]}},{"key_as_string": "-07-01","key": 1467331200000,"doc_count": 2,"total_sum_price": {"value": 2700},"group_by_brand": {"doc_count_error_upper_bound": 0,"sum_other_doc_count": 0,"buckets": [{"key": "TCL","doc_count": 2,"sum_price": {"value": 2700}}]}},{"key_as_string": "-10-01","key": 1475280000000,"doc_count": 3,"total_sum_price": {"value": 5000},"group_by_brand": {"doc_count_error_upper_bound": 0,"sum_other_doc_count": 0,"buckets": [{"key": "长虹","doc_count": 3,"sum_price": {"value": 5000}}]}},{"key_as_string": "-01-01","key": 1483228800000,"doc_count": 2,"total_sum_price": {"value": 10500},"group_by_brand": {"doc_count_error_upper_bound": 0,"sum_other_doc_count": 0,"buckets": [{"key": "三星","doc_count": 1,"sum_price": {"value": 8000}},{"key": "小米","doc_count": 1,"sum_price": {"value": 2500}}]}},{"key_as_string": "-04-01","key": 1491004800000,"doc_count": 0,"total_sum_price": {"value": 0},"group_by_brand": {"doc_count_error_upper_bound": 0,"sum_other_doc_count": 0,"buckets": []}},{"key_as_string": "-07-01","key": 1498867200000,"doc_count": 0,"total_sum_price": {"value": 0},"group_by_brand": {"doc_count_error_upper_bound": 0,"sum_other_doc_count": 0,"buckets": []}},{"key_as_string": "-10-01","key": 1506816000000,"doc_count": 0,"total_sum_price": {"value": 0},"group_by_brand": {"doc_count_error_upper_bound": 0,"sum_other_doc_count": 0,"buckets": []}}]}}}

41_聚合分析_query+filter:统计指定品牌下每个颜色的销量

课程大纲

实际上来说,我们之前学习的搜索相关的知识,完全可以和聚合组合起来使用

select count(*)

from tvs.sales

where brand like “%长%”

group by price

es aggregation,scope,任何的聚合,都必须在搜索出来的结果数据中之行,搜索结果,就是聚合分析操作的scope

GET /tvs/sales/_search {"size": 0,"query": {"term": {"brand": {"value": "小米"}}},"aggs": {"group_by_color": {"terms": {"field": "color"}}}}{"took": 5,"timed_out": false,"_shards": {"total": 5,"successful": 5,"failed": 0},"hits": {"total": 2,"max_score": 0,"hits": []},"aggregations": {"group_by_color": {"doc_count_error_upper_bound": 0,"sum_other_doc_count": 0,"buckets": [{"key": "绿色","doc_count": 1},{"key": "蓝色","doc_count": 1}]}}}

42_聚合分析_global bucket:单个品牌与所有品牌销量对比

课程大纲

aggregation,scope,一个聚合操作,必须在query的搜索结果范围内执行

出来两个结果,一个结果,是基于query搜索结果来聚合的; 一个结果,是对所有数据执行聚合的

GET /tvs/sales/_search {"size": 0, "query": {"term": {"brand": {"value": "长虹"}}},"aggs": {"single_brand_avg_price": {"avg": {"field": "price"}},"all": {"global": {},"aggs": {"all_brand_avg_price": {"avg": {"field": "price"}}}}}}

global:就是global bucket,就是将所有数据纳入聚合的scope,而不管之前的query

{"took": 4,"timed_out": false,"_shards": {"total": 5,"successful": 5,"failed": 0},"hits": {"total": 3,"max_score": 0,"hits": []},"aggregations": {"all": {"doc_count": 8,"all_brand_avg_price": {"value": 2650}},"single_brand_avg_price": {"value": 1666.6666666666667}}}

single_brand_avg_price:就是针对query搜索结果,执行的,拿到的,就是长虹品牌的平均价格

all.all_brand_avg_price:拿到所有品牌的平均价格

43_聚合分析_filter+aggs:统计价格大于1200的电视平均价格

课程大纲

搜索+聚合

过滤+聚合

GET /tvs/sales/_search {"size": 0,"query": {"constant_score": {"filter": {"range": {"price": {"gte": 1200}}}}},"aggs": {"avg_price": {"avg": {"field": "price"}}}}

{"took": 41,"timed_out": false,"_shards": {"total": 5,"successful": 5,"failed": 0},"hits": {"total": 7,"max_score": 0,"hits": []},"aggregations": {"avg_price": {"value": 2885.714285714286}}}

44_聚合分析_bucket filter:统计牌品最近一个月的平均价格

课程大纲

GET /tvs/sales/_search {"size": 0,"query": {"term": {"brand": {"value": "长虹"}}},"aggs": {"recent_150d": {"filter": {"range": {"sold_date": {"gte": "now-150d"}}},"aggs": {"recent_150d_avg_price": {"avg": {"field": "price"}}}},"recent_140d": {"filter": {"range": {"sold_date": {"gte": "now-140d"}}},"aggs": {"recent_140d_avg_price": {"avg": {"field": "price"}}}},"recent_130d": {"filter": {"range": {"sold_date": {"gte": "now-130d"}}},"aggs": {"recent_130d_avg_price": {"avg": {"field": "price"}}}}}}

aggs.filter,针对的是聚合去做的

如果放query里面的filter,是全局的,会对所有的数据都有影响

但是,如果,比如说,你要统计,长虹电视,最近1个月的平均值; 最近3个月的平均值; 最近6个月的平均值

bucket filter:对不同的bucket下的aggs,进行filter

45_聚合分析_排序:按每种颜色的平均销售额降序排序

课程大纲

之前的话,排序,是按照每个bucket的doc_count降序来排的

但是假如说,我们现在统计出来每个颜色的电视的销售额,需要按照销售额降序排序????

GET /tvs/sales/_search {"size": 0,"aggs": {"group_by_color": {"terms": {"field": "color"},"aggs": {"avg_price": {"avg": {"field": "price"}}}}}}

{"took": 2,"timed_out": false,"_shards": {"total": 5,"successful": 5,"failed": 0},"hits": {"total": 8,"max_score": 0,"hits": []},"aggregations": {"group_by_color": {"doc_count_error_upper_bound": 0,"sum_other_doc_count": 0,"buckets": [{"key": "红色","doc_count": 4,"avg_price": {"value": 3250}},{"key": "绿色","doc_count": 2,"avg_price": {"value": 2100}},{"key": "蓝色","doc_count": 2,"avg_price": {"value": 2000}}]}}}

GET /tvs/sales/_search {"size": 0,"aggs": {"group_by_color": {"terms": {"field": "color","order": {"avg_price": "asc"}},"aggs": {"avg_price": {"avg": {"field": "price"}}}}}}

46_聚合分析_颜色+品牌下钻分析时按最深层metric进行排序

课程大纲

GET /tvs/sales/_search {"size": 0,"aggs": {"group_by_color": {"terms": {"field": "color"},"aggs": {"group_by_brand": {"terms": {"field": "brand","order": {"avg_price": "desc"}},"aggs": {"avg_price": {"avg": {"field": "price"}}}}}}}}

{"took": 4,"timed_out": false,"_shards": {"total": 5,"successful": 5,"failed": 0},"hits": {"total": 8,"max_score": 0,"hits": []},"aggregations": {"group_by_color": {"doc_count_error_upper_bound": 0,"sum_other_doc_count": 0,"buckets": [{"key": "红色","doc_count": 4,"group_by_brand": {"doc_count_error_upper_bound": 0,"sum_other_doc_count": 0,"buckets": [{"key": "三星","doc_count": 1,"avg_price": {"value": 8000}},{"key": "长虹","doc_count": 3,"avg_price": {"value": 1666.6666666666667}}]}},{"key": "绿色","doc_count": 2,"group_by_brand": {"doc_count_error_upper_bound": 0,"sum_other_doc_count": 0,"buckets": [{"key": "小米","doc_count": 1,"avg_price": {"value": 3000}},{"key": "TCL","doc_count": 1,"avg_price": {"value": 1200}}]}},{"key": "蓝色","doc_count": 2,"group_by_brand": {"doc_count_error_upper_bound": 0,"sum_other_doc_count": 0,"buckets": [{"key": "小米","doc_count": 1,"avg_price": {"value": 2500}},{"key": "TCL","doc_count": 1,"avg_price": {"value": 1500}}]}}]}}}

47_聚合分析_易并行聚合算法,三角选择原则,近似聚合算法

课程大纲

1、画图讲解易并行聚合算法:max

有些聚合分析的算法,是很容易就可以并行的,比如说max

有些聚合分析的算法,是不好并行的,比如说,count(distinct),并不是说,在每个node上,直接就出一些distinct value,就可以的,因为数据可能会很多

es会采取近似聚合的方式,就是采用在每个node上进行近估计的方式,得到最终的结论,cuont(distcint),100万,1050万/95万 --> 5%左右的错误率

近似估计后的结果,不完全准确,但是速度会很快,一般会达到完全精准的算法的性能的数十倍

2、三角选择原则

精准+实时+大数据 --> 选择2个

(1)精准+实时: 没有大数据,数据量很小,那么一般就是单击跑,随便你则么玩儿就可以

(2)精准+大数据:hadoop,批处理,非实时,可以处理海量数据,保证精准,可能会跑几个小时

(3)大数据+实时:es,不精准,近似估计,可能会有百分之几的错误率

3、近似聚合算法

如果采取近似估计的算法:延时在100ms左右,0.5%错误

如果采取100%精准的算法:延时一般在5s~几十s,甚至几十分钟,几小时, 0%错误

未完待续。。。。。

48_聚合分析_cardinality去重每月销售品牌数量统计

课程大纲

es,去重,cartinality metric,对每个bucket中的指定的field进行去重,取去重后的count,类似于count(distcint)

GET /tvs/sales/_search{"size" : 0,"aggs" : {"months" : {"date_histogram": {"field": "sold_date","interval": "month"},"aggs": {"distinct_colors" : {"cardinality" : {"field" : "brand"}}}}}}

{"took": 70,"timed_out": false,"_shards": {"total": 5,"successful": 5,"failed": 0},"hits": {"total": 8,"max_score": 0,"hits": []},"aggregations": {"group_by_sold_date": {"buckets": [{"key_as_string": "-05-01T00:00:00.000Z","key": 1462060800000,"doc_count": 1,"distinct_brand_cnt": {"value": 1}},{"key_as_string": "-06-01T00:00:00.000Z","key": 1464739200000,"doc_count": 0,"distinct_brand_cnt": {"value": 0}},{"key_as_string": "-07-01T00:00:00.000Z","key": 1467331200000,"doc_count": 1,"distinct_brand_cnt": {"value": 1}},{"key_as_string": "-08-01T00:00:00.000Z","key": 1470009600000,"doc_count": 1,"distinct_brand_cnt": {"value": 1}},{"key_as_string": "-09-01T00:00:00.000Z","key": 1472688000000,"doc_count": 0,"distinct_brand_cnt": {"value": 0}},{"key_as_string": "-10-01T00:00:00.000Z","key": 1475280000000,"doc_count": 1,"distinct_brand_cnt": {"value": 1}},{"key_as_string": "-11-01T00:00:00.000Z","key": 1477958400000,"doc_count": 2,"distinct_brand_cnt": {"value": 1}},{"key_as_string": "-12-01T00:00:00.000Z","key": 1480550400000,"doc_count": 0,"distinct_brand_cnt": {"value": 0}},{"key_as_string": "-01-01T00:00:00.000Z","key": 1483228800000,"doc_count": 1,"distinct_brand_cnt": {"value": 1}},{"key_as_string": "-02-01T00:00:00.000Z","key": 1485907200000,"doc_count": 1,"distinct_brand_cnt": {"value": 1}}]}}}

49_聚合分析_cardinality算法之优化内存开销以及HLL算法

课程大纲

cardinality,count(distinct),5%的错误率,性能在100ms左右

1、precision_threshold优化准确率和内存开销

GET /tvs/sales/_search{"size" : 0,"aggs" : {"distinct_brand" : {"cardinality" : {"field" : "brand","precision_threshold" : 100 }}}}

brand去重,如果brand的unique value,在100个以内,小米,长虹,三星,TCL,HTL。。。

在多少个unique value以内,cardinality,几乎保证100%准确

cardinality算法,会占用precision_threshold * 8 byte 内存消耗,100 * 8 = 800个字节

占用内存很小。。。而且unique value如果的确在值以内,那么可以确保100%准确

100,数百万的unique value,错误率在5%以内

precision_threshold,值设置的越大,占用内存越大,1000 * 8 = 8000 / 1000 = 8KB,可以确保更多unique value的场景下,100%的准确

field,去重,count,这时候,unique value,10000,precision_threshold=10000,10000 * 8 = 80000个byte,80KB

2、HyperLogLog++ (HLL)算法性能优化

cardinality底层算法:HLL算法,HLL算法的性能

会对所有的uqniue value取hash值,通过hash值近似去求distcint count,误差

默认情况下,发送一个cardinality请求的时候,会动态地对所有的field value,取hash值; 将取hash值的操作,前移到建立索引的时候

PUT /tvs/{"mappings": {"sales": {"properties": {"brand": {"type": "text","fields": {"hash": {"type": "murmur3" }}}}}}}GET /tvs/sales/_search{"size" : 0,"aggs" : {"distinct_brand" : {"cardinality" : {"field" : "brand.hash","precision_threshold" : 100 }}}}

50_聚合分析_percentiles百分比算法以及网站访问时延统计

课程大纲

需求:比如有一个网站,记录下了每次请求的访问的耗时,需要统计tp50,tp90,tp99

tp50:50%的请求的耗时最长在多长时间

tp90:90%的请求的耗时最长在多长时间

tp99:99%的请求的耗时最长在多长时间

PUT /website{"mappings": {"logs": {"properties": {"latency": {"type": "long"},"province": {"type": "keyword"},"timestamp": {"type": "date"}}}}}

POST /website/logs/_bulk{"index": {}}{"latency" : 105, "province" : "江苏", "timestamp" : "-10-28" }{"index": {}}{"latency" : 83, "province" : "江苏", "timestamp" : "-10-29" }{"index": {}}{"latency" : 92, "province" : "江苏", "timestamp" : "-10-29" }{"index": {}}{"latency" : 112, "province" : "江苏", "timestamp" : "-10-28" }{"index": {}}{"latency" : 68, "province" : "江苏", "timestamp" : "-10-28" }{"index": {}}{"latency" : 76, "province" : "江苏", "timestamp" : "-10-29" }{"index": {}}{"latency" : 101, "province" : "新疆", "timestamp" : "-10-28" }{"index": {}}{"latency" : 275, "province" : "新疆", "timestamp" : "-10-29" }{"index": {}}{"latency" : 166, "province" : "新疆", "timestamp" : "-10-29" }{"index": {}}{"latency" : 654, "province" : "新疆", "timestamp" : "-10-28" }{"index": {}}{"latency" : 389, "province" : "新疆", "timestamp" : "-10-28" }{"index": {}}{"latency" : 302, "province" : "新疆", "timestamp" : "-10-29" }

pencentiles

GET /website/logs/_search {"size": 0,"aggs": {"latency_percentiles": {"percentiles": {"field": "latency","percents": [50,95,99]}},"latency_avg": {"avg": {"field": "latency"}}}}

{"took": 31,"timed_out": false,"_shards": {"total": 5,"successful": 5,"failed": 0},"hits": {"total": 12,"max_score": 0,"hits": []},"aggregations": {"latency_avg": {"value": 201.91666666666666},"latency_percentiles": {"values": {"50.0": 108.5,"95.0": 508.24999999999983,"99.0": 624.8500000000001}}}}

50%的请求,数值的最大的值是多少,不是完全准确的

GET /website/logs/_search {"size": 0,"aggs": {"group_by_province": {"terms": {"field": "province"},"aggs": {"latency_percentiles": {"percentiles": {"field": "latency","percents": [50,95,99]}},"latency_avg": {"avg": {"field": "latency"}}}}}}

{"took": 33,"timed_out": false,"_shards": {"total": 5,"successful": 5,"failed": 0},"hits": {"total": 12,"max_score": 0,"hits": []},"aggregations": {"group_by_province": {"doc_count_error_upper_bound": 0,"sum_other_doc_count": 0,"buckets": [{"key": "新疆","doc_count": 6,"latency_avg": {"value": 314.5},"latency_percentiles": {"values": {"50.0": 288.5,"95.0": 587.75,"99.0": 640.75}}},{"key": "江苏","doc_count": 6,"latency_avg": {"value": 89.33333333333333},"latency_percentiles": {"values": {"50.0": 87.5,"95.0": 110.25,"99.0": 111.65}}}]}}}

51_聚合分析_percentiles rank以及网站访问时延SLA统计

课程大纲

SLA:就是你提供的服务的标准

我们的网站的提供的访问延时的SLA,确保所有的请求100%,都必须在200ms以内,大公司内,一般都是要求100%在200ms以内

如果超过1s,则需要升级到A级故障,代表网站的访问性能和用户体验急剧下降

需求:在200ms以内的,有百分之多少,在1000毫秒以内的有百分之多少,percentile ranks metric

这个percentile ranks,其实比pencentile还要常用

按照品牌分组,计算,电视机,售价在1000占比,2000占比,3000占比

GET /website/logs/_search {"size": 0,"aggs": {"group_by_province": {"terms": {"field": "province"},"aggs": {"latency_percentile_ranks": {"percentile_ranks": {"field": "latency","values": [200,1000]}}}}}}

{"took": 38,"timed_out": false,"_shards": {"total": 5,"successful": 5,"failed": 0},"hits": {"total": 12,"max_score": 0,"hits": []},"aggregations": {"group_by_province": {"doc_count_error_upper_bound": 0,"sum_other_doc_count": 0,"buckets": [{"key": "新疆","doc_count": 6,"latency_percentile_ranks": {"values": {"200.0": 29.40613026819923,"1000.0": 100}}},{"key": "江苏","doc_count": 6,"latency_percentile_ranks": {"values": {"200.0": 100,"1000.0": 100}}}]}}}

percentile的优化

TDigest算法,用很多节点来执行百分比的计算,近似估计,有误差,节点越多,越精准

compression

限制节点数量最多 compression * 20 = 2000个node去计算

默认100

越大,占用内存越多,越精准,性能越差

一个节点占用32字节,100 * 20 * 32 = 64KB

如果你想要percentile算法越精准,compression可以设置的越大

52_聚合分析_doc value正排索引的聚合

聚合分析的内部原理是什么????aggs,term,metric avg max,执行一个聚合操作的时候,内部原理是怎样的呢?用了什么样的数据结构去执行聚合?是不是用的倒排索引?

搜索+聚合,写个示例

GET /test_index/test_type/_search

{

“query”: {

“match”: {

“search_field”: “test”

}

},

“aggs”: {

“group_by_agg_field”: {

“terms”: {

“field”: “agg_field”

}

}

}

}

纯用倒排索引来实现的弊端

es肯定不是纯用倒排索引来实现聚合+搜索的

search_field

doc1: hello world test1, test2

doc2: hello test

doc3: world test

hello doc1,doc2

world doc1,doc3

test1 doc1

test2 doc1

test doc2,doc3

“query”: {

“match”: {

“search_field”: “test”

}

}

test --> doc2,doc3 --> search result, doc2,doc3

agg_field

doc2: agg1

doc3: agg2

100万个值

agg1 doc2

agg2 doc3

doc2, doc3, search result --> 实际上,要搜索到doc2的agg_field的值是多少,doc3的agg_field的值是多少

doc2和doc3的agg_field的值之后,就可以根据值进行分组,实现terms bucket操作

doc2的agg_field的值是多少,这个时候,如果你手上只有一个倒排索引,你该怎么办???你要扫描整个倒排索引,去一个一个的搜,拿到每个值,比如说agg1,看一下,它是不是doc2的值,拿到agg2,看一下,是不是doc2的值,直到找到doc2的agg_field的值,在倒排索引中

如果用纯倒排索引去实现聚合,现实不现实啊???性能是很低下的。。。搜索,search,搜倒排索引,搜那个term,就结束了。。。聚合,搜索出了1万个doc,每个doc都要在倒排索引中搜索出它的那个聚合field的值

倒排索引+正排索引(doc value)的原理和优势

doc value:正排索引

search_field

doc1: hello world test1, test2

doc2: hello test

doc3: world test

hello doc1,doc2

world doc1,doc3

test1 doc1

test2 doc1

test doc2,doc3

“query”: {

“match”: {

“search_field”: “test”

}

}

test --> doc2,doc3 --> search result, doc2,doc3

doc value数据结构,正排索引

100万个

doc2: agg1

doc3: agg2

倒排索引的话,必须遍历完整个倒排索引才可以。。。。

因为可能你要聚合的那个field的值,是分词的,比如说hello world my name --> 一个doc的聚合field的值可能在倒排索引中对应多个value

所以说,当你在倒排索引中找到一个值,发现它是属于某个doc的时候,还不能停,必须遍历完整个倒排索引,才能说确保找到了每个doc对应的所有terms,然后进行分组聚合

100万个

doc2: agg1 hello world

doc3: agg2 test hello

我们有没有必要搜索完整个正排索引啊??1万个doc --> 搜 -> 可能跟搜索到15000次,就搜索完了,就找到了1万个doc的聚合field的所有值了,然后就可以执行分组聚合操作了

53_聚合分析_doc value机制内核级原理深入探秘

课程大纲

1、doc value原理

(1)index-time生成

PUT/POST的时候,就会生成doc value数据,也就是正排索引

(2)核心原理与倒排索引类似

正排索引,也会写入磁盘文件中,然后呢,os cache先进行缓存,以提升访问doc value正排索引的性能

如果os cache内存大小不足够放得下整个正排索引,doc value,就会将doc value的数据写入磁盘文件中

(3)性能问题:给jvm更少内存,64g服务器,给jvm最多16g

es官方是建议,es大量是基于os cache来进行缓存和提升性能的,不建议用jvm内存来进行缓存,那样会导致一定的gc开销和oom问题

给jvm更少的内存,给os cache更大的内存

64g服务器,给jvm最多16g,几十个g的内存给os cache

os cache可以提升doc value和倒排索引的缓存和查询效率

2、column压缩

doc1: 550

doc2: 550

doc3: 500

合并相同值,550,doc1和doc2都保留一个550的标识即可

(1)所有值相同,直接保留单值

(2)少于256个值,使用table encoding模式:一种压缩方式

(3)大于256个值,看有没有最大公约数,有就除以最大公约数,然后保留这个最大公约数

doc1: 36

doc2: 24

6 --> doc1: 6, doc2: 4 --> 保留一个最大公约数6的标识,6也保存起来

(4)如果没有最大公约数,采取offset结合压缩的方式:

3、disable doc value

如果的确不需要doc value,比如聚合等操作,那么可以禁用,减少磁盘空间占用

PUT my_index{"mappings": {"my_type": {"properties": {"my_field": {"type": "keyword""doc_values": false }}}}}

54_聚合分析_string field聚合实验以及fielddata

1、对于分词的field执行aggregation,发现报错。。。

GET /test_index/test_type/_search {"aggs": {"group_by_test_field": {"terms": {"field": "test_field"}}}}

{"error": {"root_cause": [{"type": "illegal_argument_exception","reason": "Fielddata is disabled on text fields by default. Set fielddata=true on [test_field] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory."}],"type": "search_phase_execution_exception","reason": "all shards failed","phase": "query","grouped": true,"failed_shards": [{"shard": 0,"index": "test_index","node": "4onsTYVZTjGvIj9_spWz2w","reason": {"type": "illegal_argument_exception","reason": "Fielddata is disabled on text fields by default. Set fielddata=true on [test_field] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory."}}],"caused_by": {"type": "illegal_argument_exception","reason": "Fielddata is disabled on text fields by default. Set fielddata=true on [test_field] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory."}},"status": 400}

对分词的field,直接执行聚合操作,会报错,大概意思是说,你必须要打开fielddata,然后将正排索引数据加载到内存中,才可以对分词的field执行聚合操作,而且会消耗很大的内存

2、给分词的field,设置fielddata=true,发现可以执行,但是结果却。。。

POST /test_index/_mapping/test_type {"properties": {"test_field": {"type": "text","fielddata": true}}}

{"test_index": {"mappings": {"test_type": {"properties": {"test_field": {"type": "text","fields": {"keyword": {"type": "keyword","ignore_above": 256}},"fielddata": true}}}}}}

GET /test_index/test_type/_search {"size": 0, "aggs": {"group_by_test_field": {"terms": {"field": "test_field"}}}}

{"took": 23,"timed_out": false,"_shards": {"total": 5,"successful": 5,"failed": 0},"hits": {"total": 2,"max_score": 0,"hits": []},"aggregations": {"group_by_test_field": {"doc_count_error_upper_bound": 0,"sum_other_doc_count": 0,"buckets": [{"key": "test","doc_count": 2}]}}}

如果要对分词的field执行聚合操作,必须将fielddata设置为true

3、使用内置field不分词,对string field进行聚合

GET /test_index/test_type/_search {"size": 0,"aggs": {"group_by_test_field": {"terms": {"field": "test_field.keyword"}}}}

{"took": 3,"timed_out": false,"_shards": {"total": 5,"successful": 5,"failed": 0},"hits": {"total": 2,"max_score": 0,"hits": []},"aggregations": {"group_by_test_field": {"doc_count_error_upper_bound": 0,"sum_other_doc_count": 0,"buckets": [{"key": "test","doc_count": 2}]}}}

如果对不分词的field执行聚合操作,直接就可以执行,不需要设置fieldata=true

4、分词field+fielddata的工作原理

doc value --> 不分词的所有field,可以执行聚合操作 --> 如果你的某个field不分词,那么在index-time,就会自动生成doc value --> 针对这些不分词的field执行聚合操作的时候,自动就会用doc value来执行

分词field,是没有doc value的。。。在index-time,如果某个field是分词的,那么是不会给它建立doc value正排索引的,因为分词后,占用的空间过于大,所以默认是不支持分词field进行聚合的

分词field默认没有doc value,所以直接对分词field执行聚合操作,是会报错的

对于分词field,必须打开和使用fielddata,完全存在于纯内存中。。。结构和doc value类似。。。如果是ngram或者是大量term,那么必将占用大量的内存。。。

如果一定要对分词的field执行聚合,那么必须将fielddata=true,然后es就会在执行聚合操作的时候,现场将field对应的数据,建立一份fielddata正排索引,fielddata正排索引的结构跟doc value是类似的,但是只会讲fielddata正排索引加载到内存中来,然后基于内存中的fielddata正排索引执行分词field的聚合操作

如果直接对分词field执行聚合,报错,才会让我们开启fielddata=true,告诉我们,会将fielddata uninverted index,正排索引,加载到内存,会耗费内存空间

为什么fielddata必须在内存?因为大家自己思考一下,分词的字符串,需要按照term进行聚合,需要执行更加复杂的算法和操作,如果基于磁盘和os cache,那么性能会很差

55_聚合分析_fielddata内存控制以及circuit breaker断路器

课程大纲

1、fielddata核心原理

fielddata加载到内存的过程是lazy加载的,对一个analzyed field执行聚合时,才会加载,而且是field-level加载的

一个index的一个field,所有doc都会被加载,而不是少数doc

不是index-time创建,是query-time创建

2、fielddata内存限制

indices.fielddata.cache.size: 20%,超出限制,清除内存已有fielddata数据

fielddata占用的内存超出了这个比例的限制,那么就清除掉内存中已有的fielddata数据

默认无限制,限制内存使用,但是会导致频繁evict和reload,大量IO性能损耗,以及内存碎片和gc

3、监控fielddata内存使用

GET /_stats/fielddata?fields=*GET /_nodes/stats/indices/fielddata?fields=*GET /_nodes/stats/indices/fielddata?level=indices&fields=*

4、circuit breaker

如果一次query load的feilddata超过总内存,就会oom --> 内存溢出

circuit breaker会估算query要加载的fielddata大小,如果超出总内存,就短路,query直接失败

indices.breaker.fielddata.limit:fielddata的内存限制,默认60%indices.breaker.request.limit:执行聚合的内存限制,默认40%indices.breaker.total.limit:综合上面两个,限制在70%以内

56_聚合分析_fielddata filter的细粒度内存加载控制

POST /test_index/_mapping/my_type{"properties": {"my_field": {"type": "text","fielddata": {"filter": {"frequency": {"min": 0.01, "min_segment_size": 500 }}}}}}

min:仅仅加载至少在1%的doc中出现过的term对应的fielddata

比如说某个值,hello,总共有1000个doc,hello必须在10个doc中出现,那么这个hello对应的fielddata才会加载到内存中来

min_segment_size:少于500 doc的segment不加载fielddata

加载fielddata的时候,也是按照segment去进行加载的,某个segment里面的doc数量少于500个,那么这个segment的fielddata就不加载

这个,就我的经验来看,有点底层了,一般不会去设置它,大家知道就好

57_聚合分析_fielddata预加载机制以及序号标记预加载

如果真的要对分词的field执行聚合,那么每次都在query-time现场生产fielddata并加载到内存中来,速度可能会比较慢

我们是不是可以预先生成加载fielddata到内存中来???

1、fielddata预加载

POST /test_index/_mapping/test_type{"properties": {"test_field": {"type": "string","fielddata": {"loading" : "eager" }}}}

query-time的fielddata生成和加载到内存,变为index-time,建立倒排索引的时候,会同步生成fielddata并且加载到内存中来,这样的话,对分词field的聚合性能当然会大幅度增强

2、序号标记预加载

global ordinal原理解释

doc1: status1

doc2: status2

doc3: status2

doc4: status1

有很多重复值的情况,会进行global ordinal标记

status1 --> 0

status2 --> 1

doc1: 0

doc2: 1

doc3: 1

doc4: 0

建立的fielddata也会是这个样子的,这样的好处就是减少重复字符串的出现的次数,减少内存的消耗

POST /test_index/_mapping/test_type{"properties": {"test_field": {"type": "string","fielddata": {"loading" : "eager_global_ordinals" }}}}

58_聚合分析_海量bucket优化机制:从深度优先到广度优先

当buckets数量特别多的时候,深度优先和广度优先的原理,图解

我们的数据,是每个演员的每个电影的评论

每个演员的评论的数量 --> 每个演员的每个电影的评论的数量

评论数量排名前10个的演员 --> 每个演员的电影取到评论数量排名前5的电影

{"aggs" : {"actors" : {"terms" : {"field" : "actors","size" : 10,"collect_mode" : "breadth_first" },"aggs" : {"costars" : {"terms" : {"field" : "films","size" : 5}}}}}}

深度优先的方式去执行聚合操作的

actor1 actor2 .... actor

film1 film2 film3 film1 film2 film3 …film

比如说,我们有10万个actor,最后其实是主要10个actor就可以了

但是我们已经深度优先的方式,构建了一整颗完整的树出来了,10万个actor,每个actor平均有10部电影,10万 + 100万 --> 110万的数据量的一颗树

裁剪掉10万个actor中的99990 actor,99990 * 10 = film,剩下10个actor,每个actor的10个film裁剪掉5个,110万 --> 10 * 5 = 50个

构建了大量的数据,然后裁剪掉了99.99%的数据,浪费了

广度优先的方式去执行聚合

actor1 actor2 actor3 … n个actor

10万个actor,不去构建它下面的film数据,10万 --> 99990,10个actor,构建出film,裁剪出其中的5个film即可,10万 -> 50个

10倍

如果觉得《Elasticsearch顶尖高手系列:高手进阶篇(一)》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。