zoukankan      html  css  js  c++  java
  • ElasticSearch进阶学习笔记

    term filter 来搜索帖子

    索引

    PUT /forum
    {
      "settings": {
        "number_of_replicas": 0,
        "number_of_shards": 1
      },
      "mappings": {
        "article": {
          "properties": {
            "articleID": {
              "type": "text",
              "fields": {
                "keyword": {
                  "type": "keyword",
                  "ignore_above": 256
                }
              }
            },
            "hidden": {
              "type": "boolean"
            },
            "postData": {
              "type": "date"
            },
            "userID": {
              "type": "long"
            }
          }
        }
      }
    }
    

    批量插入数据

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

    type=text默认会设置两个field,一个是field本身,比如articleID,就是分词的。还有一个field.keyword,articleID.keywrod,默认不分词,会最多保留256个字符

    根据用户ID来搜索帖子
    GET /forum/article/_search
    {
      "query":{
        "constant_score": {
          "filter": {
            "term": {
              "userID": "1"
            }
          }
        }
      }
    }
    

    term filter/query:对搜索结果不分词,直接去倒排索引里面找,输入什么就匹配什么。
    比如,如果对搜索文本分词的话,输入"hello world" 会分成 hello 和 world ,两个词分别去倒排索引里面找
    如果不分词的话,直接去倒排索引里面匹配"hello world"。
    constant_score表示不受词频影响

    搜索隐藏的帖子
    GET /forum/article/_search
    {
      "query":{
        "constant_score":{
          "filter":{
            "term":{
              "hidden":true
            }
          }
        }
      }
    }
    
    搜索特定日期的帖子
    GET /forum/article/_search
    {
      "query":{
        "constant_score":{
          "filter":{
            "term":{
              "postDate": "2017-01-01"
            }
          }
        }
      }
    }
    
    搜索文章ID
    GET /forum/article/_search
    {
      "query":{
        "constant_score":{
          "filter": {
            "term":{
              "articleID.keyword": "XHDK-A-1293-#fJ3"
            }
          }
        }
      }
    }
    

    直接用articleID是无法搜索出来的,因为被分词了。articleID.keyword是最新版本es建立的field,就是不分词,所以一个articleID过来的时候会建立两个索引,一个是自己本身,需要分词,分词后放入倒排索引,另外一个是基于articleID.keyword,不分词,最多保留256个字符,直接把这个字符放入倒排索引。

    所以term filter,对text过滤可以考虑用内置的field.keyword进行匹配,但是只有256个字符,所以尽可能还是去手动建立索引,置顶not_analyzed,再最新的版本里面直接type=keyword就可以解决。

    总结
    1. term filter:根据extra value进行搜索,数字,boolean,date天然支持
    2. text建立索引的时候需要指定not_analyzed才能用term_query
    3. keyword也可以用term_query
    4. term_query是针对不分词(结构化数据)进行搜索

    filter原理

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

    date来举例

    word		doc1		doc2		doc3
    
    2017-01-01	*		*
    2017-02-02			*		*
    2017-03-03	*		*		*
    

    filter:2017-02-02

    到倒排索引中一找,发现2017-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=2017-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=2017-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=2017-01-01,[0, 0, 1, 0]
    document,id=5,postDate=2017-01-01,会自动更新到postDate=2017-01-01这个filter的bitset中,全自动,缓存会自动更新。postDate=2017-01-01的bitset,[0, 0, 1, 0, 1]
    document,id=1,postDate=2016-12-30,修改为postDate-2017-01-01,此时也会自动更新bitset,[1, 0, 1, 0, 1]

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

    基于bool组合多个filter条件搜索数据

    搜索发帖日期为2017-01-01或者userID为1的帖子,同时发帖日期不能为2017-01-02

    GET forum/article/_search
    {
      "query": {
        "constant_score": {
          "filter": {
            "bool": {
              "should": [
                {
                  "term": {
                    "userID": 1
                  }
                },
                {
                  "term": {
                    "postDate": "2017-01-01"
                  }
                }
              ],
              "must_not": [
                {
                  "term": {
                    "postDate": "2017-01-02"
                  }
                }
              ]
            }
          }
        }
      }
    }
    

    must(必匹配) should(匹配任意一个) must_not(必须不匹配)

    搜索userID为2,或者userID为1而且发帖日期为2017-01-01的帖子

    GET forum/article/_search
    {
      "query": {
        "constant_score": {
          "filter": {
            "bool": {
              "should": [
                {
                  "term": {
                    "userID": 2
                  }
                },
                {
                  "bool": {
                    "must": [
                      {
                        "term": {
                          "userID": 1
                        }
                      },
                      {
                        "term": {
                          "postDate": "2017-01-01"
                        }
                      }
                    ]
                  }
                }
              ]
            }
          }
        }
      }
    }
    

    使用term搜索多个值以及多值搜索结果优化

    • term: {"field": "value"}
    • terms: {"field": ["value1", "value2"]}
    • 相当于SQL里面的IN
    //先为帖子插入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"]}}
    
    

    搜索userID为1或者2的帖子,搜索tag中包含java的帖子

    POST forum/article/_search
    {
      "query":{
        "constant_score":{
          "filter":{
            "bool":{
              "must":{
                "terms":{
                  "userID":[1,2]
                }
              }
            }
          }
        }
      }
    }
    
    
    POST forum/article/_search
    {
      "query": {
        "constant_score":{
          "filter": {
            "bool":{
              "must":{
                "terms":{
                  "tag":["java"]
                }
              }
            }
          }
        }
      }
    }
    

    仅仅搜索tag只包含java(思路,统计tag里面的词语个数,单独建立一个字段)

    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": [
                {
                  "terms": {
                    "tag": [
                      "java"
                    ]
                  }
                },
                {
                  "term": {
                    "tag_cnt": 1
                  }
                }
              ]
            }
          }
        }
      }
    }
    

    基于range filter进行过滤

    查询阅读人数是30-60的帖子

    //先添加数据
    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}}
    
    
    {
      "query":{
        "constant_score":{
          "filter":{
            "range":{
              "view_cnt": {
                "lt":60,
                "gt":30
              }
            }
          }
        }
      }
    }
    

    搜索最近一个月发的帖子

    //先添加数据
    POST /forum/article/_bulk
    {"index":{"_id":5}}
    {"articleID":"DHJK-B-1395-#Ky5","userID":3,"hidden":false,"postDate":"2017-03-01","tag":["elasticsearch"],"tag_cnt":1,"view_cnt":10}
    
    {
      "query":{
        "constant_score":{
          "filter":{
            "range":{
              "postDate": {
                "gt":"now-390d"
              }
            }
          }
        }
      }
    }
    
    

    手动控制全文检索的精准度

    //手动加入title类型
    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"}}
    
    

    搜索title中含有elasticsearch或者java的帖子

    POST /forum/article/_search
    {
      "query":{
        "match":{
          "title":"elasticsearch java"
        }
      } 
    }
    
    • 这个和之前的term query不一样,不是搜索extra value,而是用full text全文检索。
    • match query 是负责全文检索的
    • 如果要检索的field是not_analyzed,match query == term query

    搜索标题中含有elasticsearch和java的帖子

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

    要所有关键词都匹配就直接用and

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

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

    控制必须匹配多少个关键词才能在结果显示

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

    GET /forum/article/_search
    {
      "query": {
        "bool": {
          "must": {
            "match": {
              "title": "java"
            }
          },
          "must_not": {
            "match": {
              "title": "spark"
            }
          },
          "should": [
            {
              "match": {
                "title": "hadoop"
              }
            },
            {
              "match": {
                "title": "elasticsearch"
              }
            }
          ]
        }
      }
    }
    
    
    • 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就会更高

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

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

    默认情况下,should是可以不匹配任何一个的,比如上面的搜索中,this is java blog,就不匹配任何一个should条件
    但是有个例外的情况,如果没有must的话,那么should中必须至少匹配一个才可以
    比如下面的搜索,should中有4个条件,默认情况下,只要满足其中一个条件,就可以匹配作为结果返回,但是可以精准控制,should的4个条件中,至少匹配几个才能作为结果返回

    1. 全文检索的时候,进行多个值的检索,有两种做法,match query;should
    2. 控制搜索结果精准度:and operator,minimum_should_match

    基于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"}}
            ]
        }
    }
    
    1. and match 转成 term + must
    {
        "match":{
            "title":{
                "query":"java elasticsearch",
                "operator":"and"
            }
        }
    }
    

    转换成

    {
        "bool":{
            "must":[
                {"term":{"title":"java"}},
                {"term":{"title":"elasticsearch"}}
            ]
        }
    }
    
    1. 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
        }
    }
    

    基于boost的细颗粒搜索条件权重控制

    需求:搜索标题中包含JAVA的帖子,同时标题含有hadoop或者spark就搜索出来,如果一个标题既包含上面两者,那么包含spark的优先搜索出来

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

    搜索条件的权重,boost可以将某个搜索条件的权重加大,此时匹配这个搜索条件和另一个搜索条件的document,计算relevance score时候,匹配权重更大的搜索条件的document,relevance score会更高,当然会优先返回。默认条件下搜索条件的权重都是1

    多shard场景下relevance score不准确问题解密

    在某一个shard中,有很多个document,包含了title有java的关键字,比如10个doc的title包含了java。当一个搜索title包含java的请求到这个shard的时候会,会计算relevance score。

    TF/IDF算法

    1. 在一个document的title中,java出现了几次
    2. 在所有的document的title中,java出现了几次
    3. 这个document的title长度

    shard只是一部分的document,默认就在shard local计算IDF

    假设

    • shard A 有10个documenmt是match(计算shard local IDF 分数很高,相关度很高)
    • shard B 有1个document是match
    • 所以会导致有时候相关度很高的doc排在后面
    解决问题
    1. 生产环境下,数据量大,尽可能实现均匀分布。es都是在多个shard中均匀路由数据的,路由的时候根据id负载均衡,大概率会均匀分布
    2. 测试环境下primary shard设置为1个
    3. 测试环境下,搜索附带search_type=dfs_query_then_fetch,会将local IDF取出来计算global IDF,计算一个doc相关度分数的时候,会将所有shard的local IDF计算出来,在本地进行glocal IDF分数计算,会将所有shard的doc作为上下文进行计算,缺点是性能很差

    基于dis_max实现best fields策略进行多字段搜索

    1. 添加字段
    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"}}
    
    
    1. 搜索title和content字段 里面含有java 或者 solution
    GET /forum/article/_search
    {
      "query": {
        "bool": {
          "should": [
            {
              "match": {
                "title": "java solution"
              }
            },
            {
              "match": {
                "content": "java solution"
              }
            }
          ]
        }
      }
    }
    
    1. 结果分析:本来是期望doc5排在前面,因为content同时包含java和soluton,但是doc2和doc4排在前面
    • 运行should子句中的两个查询
    • 相加查询返回的分值
    • 将相加得到的分值乘以匹配的查询子句的数量
    • 除以总的查询子句的数量
    { "match": { "title": "java solution" }} 分数1.1
    { "match": { "content":  "java solution" }} 分数1.2
    
    • 所以是两个分数加起来,比如说,1.1 + 1.2 = 2.3
    • matched query数量 = 2
    • 总query数量 = 2
    • 2.3 * 2 / 2 = 2.3
    { "match": { "title": "java solution" }} 分数0
    { "match": { "content":  "java solution" }} 分数2.3
    
    • 所以说,只有一个query是有分数的,比如2.3
    • matched query数量 = 1
    • 总query数量 = 2
    • 2.3 * 1 / 2 = 1.15
    • doc5的分数 = 1.15 < doc4的分数 = 2.3
    1. 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"
              }
            }
          ]
        }
      }
    }
    
    

    基于tie_breaker参数优化dis_max搜索效果

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

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

    • doc1,title中包含java,content不包含java beginner任何一个关键词
    • doc2,content中包含beginner,title中不包含任何一个关键词
    • doc3,title中包含java,content中包含beginner
    • 最终搜索,可能出来的结果是,doc1和doc2排在doc3的前面,而不是我们期望的doc3排在最前面

    原因是 dis_max 只取分数最高的query

    1. 使用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": {
                "content": "java beginner"
              }
            }
          ],
          "tie_breaker": 0.3
        }
      }
    }
    

    基于multi_match语法实现dis_max+tie_breaker

    • minimum_should_match,主要是用来干嘛的?
      用于去长尾,长尾是比如你搜索5个关键词,但是很多结果是只匹配1个关键词的,其实跟你想要的结果相差甚远,这些结果就是长尾
      minimum_should_match,控制搜索结果的精准度,只有匹配一定数量的关键词的数据,才能返回
    GET /forum/article/_search
    {
      "query":{
        "multi_match":{
          "query": "java solution",
          "type": "best_fields",  //相当于dis_max参数 
          "fields":["title^2","content"], //相当于 boost=2
          "tie_breaker":0.3,
          "minimum_should_match":"30%"
        }
      }
    }
    

    公式: score=best_field.scoreboost+other_fieldsboost.score*tie_breaker

    基于multi_match + most fiels策略进行multi-field搜索

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

    增加字段,同时建立字field

    POST /forum/_mapping/article
    {
      "properties": {
          "sub_title": { 
              "type":     "text",
              "analyzer": "english",
              "fields": {
                  "std":   { 
                      "type":     "text",
                      "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": "learned course"
        }
      }
    }
    
    

    结果

     "sub_title": "learning more courses"
     "sub_title": "learned a lot of course"
    

    为什么learning 会排在前面?

    • 如果我们用的是类似于english analyzer这种分词器的话,就会将单词还原为其最基本的形态,stemmer . sub_title用的是enligsh analyzer,所以还原了单词

    • learning --> learn

    • learned --> learn

    • courses --> course

    • sub_titile: learning coureses --> learn course

    用most_filed

    GET /forum/article/_search
    {
      "query":{
        "multi_match":{
          "query":"learned course",
          "type":"most_fields",
          "fields":["sub_title","sub_title.std"]
        }
      }
    }
    
    1. best_fields,是对多个field进行搜索,挑选某个field匹配度最高的那个分数,同时在多个query最高分相同的情况下,在一定程度上考虑其他query的分数。简单来说,你对多个field进行搜索,就想搜索到某一个field尽可能包含更多关键字的数据
    • 优点:通过best_fields策略,以及综合考虑其他field,还有minimum_should_match支持,可以尽可能精准地将匹配的结果推送到最前面

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

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

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

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

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

    使用most_fields策略进行cross-fields search弊端大揭秘

    • 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": "2017-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": "2017-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": "2017-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前面

    使用copy_to定制组合field解决cross-fields搜索弊端

    1. 第一个办法:用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"
        }
      }
    }
    
    • 问题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中出现的次数是均匀的,不会有极端的偏差

    使用原生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分过高的情况。

    在案例实战中掌握phrase matching搜索技术

    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也会返回。

    1. 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不会返回
    1. 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"
    }
    
    1. 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不匹配

    基于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": "2017-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": "2017-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": "2017-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": "2017-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": "2017-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

    混合使用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": "2017-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": "2017-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": "2017-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": "2017-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"
              ]
            }
          }
        ]
      }
    }
    

    使用rescoring机制优化近似匹配搜索的性能

    • 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
              }
            }
          }
        }
      }
    }
    
    

    实战前缀搜索、通配符搜索、正则搜索等技术

    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"
          }
        }
      }
    }
    
    
    1. 前缀搜索的原理
    • 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性能很差
    
    1. 通配符搜索

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

    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
    
    1. 正则搜索
    GET /my_index/my_type/_search 
    {
      "query": {
        "regexp": {
          "title": "C[0-9].+"
        }
      }
    }
    
    C[0-9].+
    
    [0-9]:指定范围内的数字
    [a-z]:指定范围内的字母
    .:一个字符
    +:前面的正则表达式可以出现一次或多次
    
    • wildcard和regexp,与prefix原理一致,都会扫描整个索引,性能很差
    • 主要是给大家介绍一些高级的搜索语法。在实际应用中,能不用尽量别用。性能太差了。

    实战match_phrase_prefix实现search-time搜索推荐

    搜索推荐,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
    
    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,就不再继续搜索倒排索引了。

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

    实战通过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,全文检索

    1. 实验一下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位,符合我们的期望的

    深入揭秘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

    1. 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,那么就是线性代数来计算,无法用图表示

    深入揭秘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).
    
    
    1. query normalization factor
    queryNorm = 1 / √sumOfSquaredWeights
    
    • sumOfSquaredWeights = 所有term的IDF分数之和,开一个平方根,然后做一个平方根分之1
      主要是为了将分数进行规范化 --> 开平方根,首先数据就变小了 --> 然后还用1去除以这个平方根,分数就会很小 --> 1.几 / 零点几
      分数就不会出现几万,几十万,那样的离谱的分数
    1. 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,分数之间拉开差距
    
    1. field level boost
      对某个query 设置boost增加权重

    实战掌握四种常见的相关度分数优化方法

    1. query-time boost
    GET /forum/article/_search
    {
      "query": {
        "bool": {
          "should": [
            {
              "match": {
                "title": {
                  "query": "java spark",
                  "boost": 2
                }
              }
            },
            {
              "match": {
                "content": "java spark"
              }
            }
          ]
        }
      }
    }
    
    1. 重构查询结构
    • 重构查询结果,在es新版本中,影响越来越小了。一般情况下,没什么必要的话,大家不用也行。
    GET /forum/article/_search 
    {
      "query": {
        "bool": {
          "should": [
            {
              "match": {
                "content": "java"  1/3权重
              }
            },
            {
              "match": {
                "content": "spark"
              }
            },
            {
              "bool": {
                "should": [
                  {
                    "match": {
                      "content": "solution"  1/6权重
                    }
                  },
                  {
                    "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,降低分数
    1. 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"
                  }
                }
              }
            }
          ]
        }
      }
    }
    

    实战用function_score自定义相关度分数算法

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

    • 给所有的帖子数据增加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指定的值

    实战掌握误拼写时的fuzzy模糊搜索技术

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

    • 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"
          }
        }
      }
    }
    

    上机动手实战IK中文分词器的安装和使用

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

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

    • standard:中 国 人 很 喜 欢 吃 油 条

    • ik:中国人 很 喜欢 吃 油条

    1. 在elasticsearch中安装ik中文分词器
    (1)git clone https://github.com/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
    
    1. ik分词器基础知识
    • 两种analyzer,你根据自己的需要自己选吧,但是一般是选用ik_max_word

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

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

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

    1. 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岁少女结婚好还是单身好?"
        }
      }
    }
    

    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,会在分词的时候,直接被干掉,不会建立在倒排索引中

    1. 自定义词库

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

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

    IKAnalyzer.cfg.xml:配置项ext_dict,默认值custom/mydict.dic

    补充自己的词语,然后需要重启es,才能生效,可以用 GET _analyze 直接进行分词效果查看

    1. 自己建立停用词库(可以被忽略的词):比如了,的,啥,么,我们可能并不想去建立索引,让人家搜索
    • custom/ext_stopword.dic,已经有了常用的中文停用词,可以补充自己的停用词,然后重启es

    修改IK分词器源码来基于mysql热更新词库

    热更新

    1. 每次添加完,都要重启es才能生效,非常麻烦
    2. es是分布式的,可能有数百个节点,你不能每次都一个一个节点上面去修改
    • es不停机,直接我们在外部某个地方添加新的词语,es中立即热加载到这些新词语

    热更新的方案

    • 修改ik分词器源码,然后手动支持从mysql中每隔一定时间,自动加载新的词库(推荐)
    • 基于ik分词器原生支持的热更新方案,部署一个web服务器,提供一个http接口,通过modified和tag两个http响应头,来提供词语的热更新(不稳定)
    1. 下载源码
    1. 修改源码
    Dictionary类,169行:Dictionary单例类的初始化方法,在这里需要创建一个我们自定义的线程,并且启动它
    HotDictReloadThread类:就是死循环,不断调用Dictionary.getSingleton().reLoadMainDict(),去重新加载词典
    Dictionary类,389行:this.loadMySQLExtDict();
    Dictionary类,683行:this.loadMySQLStopwordDict();
    
    1. mvn package打包代码
    • target eleaseselasticsearch-analysis-ik-5.2.0.zip
    1. 解压缩ik压缩包
    • 将mysql驱动jar,放入ik的目录下
    1. 修改jdbc相关配置

    2. 重启es

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

    1. 在mysql中添加词库与停用词

    2. 分词实验,验证热更新生效

    bucket与metric两个核心概念的讲解

    1. bucket:一个数据分组
    city name
    
    北京 小李
    北京 小王
    上海 小张
    上海 小丽
    上海 小陈
    
    • 基于city划分buckets,划分出来两个bucket,一个是北京bucket,一个是上海bucket

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

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

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

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

    1. 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中所有的数据,计算一个数量

    家电卖场案例以及统计哪种颜色电视销量最高

    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" : "2016-10-28" }
    { "index": {}}
    { "price" : 2000, "color" : "红色", "brand" : "长虹", "sold_date" : "2016-11-05" }
    { "index": {}}
    { "price" : 3000, "color" : "绿色", "brand" : "小米", "sold_date" : "2016-05-18" }
    { "index": {}}
    { "price" : 1500, "color" : "蓝色", "brand" : "TCL", "sold_date" : "2016-07-02" }
    { "index": {}}
    { "price" : 1200, "color" : "绿色", "brand" : "TCL", "sold_date" : "2016-08-19" }
    { "index": {}}
    { "price" : 2000, "color" : "红色", "brand" : "长虹", "sold_date" : "2016-11-05" }
    { "index": {}}
    { "price" : 8000, "color" : "红色", "brand" : "三星", "sold_date" : "2017-01-01" }
    { "index": {}}
    { "price" : 2500, "color" : "蓝色", "brand" : "小米", "sold_date" : "2017-02-12" }
    
    1. 统计哪种颜色的电视销量最高
    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降序排序

    实战bucket+metric:统计每种颜色电视平均价格

    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
    

    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
                    }
                  }
                ]
              }
            }
          ]
        }
      }
    }
    
    

    统计每种颜色电视最大最小价格

    课程大纲

    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

    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
              }
            }
          ]
        }
      }
    }
    

    实战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
              }
            }
          ]
        }
      }
    }
    
    

    实战date hitogram之统计每月电视销量

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

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

    • date interval = 1m,2017-01-012017-01-31,就是一个bucket,2017-02-012017-02-28,就是一个bucket

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

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

    • min_doc_count:即使某个日期interval,2017-01-01~2017-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" : "2016-01-01",
                    "max" : "2017-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": "2016-01-01",
              "key": 1451606400000,
              "doc_count": 0
            },
            {
              "key_as_string": "2016-02-01",
              "key": 1454284800000,
              "doc_count": 0
            },
            {
              "key_as_string": "2016-03-01",
              "key": 1456790400000,
              "doc_count": 0
            },
            {
              "key_as_string": "2016-04-01",
              "key": 1459468800000,
              "doc_count": 0
            },
            {
              "key_as_string": "2016-05-01",
              "key": 1462060800000,
              "doc_count": 1
            },
            {
              "key_as_string": "2016-06-01",
              "key": 1464739200000,
              "doc_count": 0
            },
            {
              "key_as_string": "2016-07-01",
              "key": 1467331200000,
              "doc_count": 1
            },
            {
              "key_as_strin
              "key_as_string": "2016-09-01",
              "key": 1472688000000,
              "doc_count": 0
            },g": "2016-08-01",
              "key": 1470009600000,
              "doc_count": 1
            },
            {
            {
              "key_as_string": "2016-10-01",
              "key": 1475280000000,
              "doc_count": 1
            },
            {
              "key_as_string": "2016-11-01",
              "key": 1477958400000,
              "doc_count": 2
            },
            {
              "key_as_string": "2016-12-01",
              "key": 1480550400000,
              "doc_count": 0
            },
            {
              "key_as_string": "2017-01-01",
              "key": 1483228800000,
              "doc_count": 1
            },
            {
              "key_as_string": "2017-02-01",
              "key": 1485907200000,
              "doc_count": 1
            }
          ]
        }
      }
    }
    
    

    下钻分析之统计每季度每个品牌的销售额

    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": "2016-01-01",
              "max": "2017-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": "2016-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": "2016-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": "2016-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": "2016-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": "2017-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": "2017-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": "2017-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": "2017-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": []
              }
            }
          ]
        }
      }
    }
    

    搜索+聚合:统计指定品牌下每个颜色的销量

    • 实际上来说,我们之前学习的搜索相关的知识,完全可以和聚合组合起来使用
    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
            }
          ]
        }
      }
    }
    

    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:拿到所有品牌的平均价格

    过滤+聚合:统计价格大于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
        }
      }
    }
    
    

    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

    按每种颜色的平均销售额降序排序

    • 之前的话,排序,是按照每个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"
              }
            }
          }
        }
      }
    }
    

    颜色+品牌下钻分析时按最深层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
                    }
                  }
                ]
              }
            }
          ]
        }
      }
    }
    

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

    1. 画图讲解易并行聚合算法:max
    • 有些聚合分析的算法,是很容易就可以并行的,比如说max。每个shard从算自己最大的那一条数据,然后协调节点把几个max值进行比较。

    • 有些聚合分析的算法,是不好并行的,比如说,count(distinct),并不是说,在每个node上,直接就出一些distinct value,就可以的,因为数据可能会很多(假如每个Node 都有100W数据,如果把300W数据都聚合在一起,再排重性能很差)

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

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

    1. 三角选择原则
    • 精准+实时+大数据 --> 选择2个
    1. 精准+实时: 没有大数据,数据量很小,那么一般就是单击跑,随便你则么玩儿就可以

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

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

    4. 近似聚合算法

    • 如果采取近似估计的算法:延时在100ms左右,0.5%错误
    • 如果采取100%精准的算法:延时一般在5s~几十s,甚至几十分钟,几小时, 0%错误

    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": "2016-05-01T00:00:00.000Z",
              "key": 1462060800000,
              "doc_count": 1,
              "distinct_brand_cnt": {
                "value": 1
              }
            },
            {
              "key_as_string": "2016-06-01T00:00:00.000Z",
              "key": 1464739200000,
              "doc_count": 0,
              "distinct_brand_cnt": {
                "value": 0
              }
            },
            {
              "key_as_string": "2016-07-01T00:00:00.000Z",
              "key": 1467331200000,
              "doc_count": 1,
              "distinct_brand_cnt": {
                "value": 1
              }
            },
            {
              "key_as_string": "2016-08-01T00:00:00.000Z",
              "key": 1470009600000,
              "doc_count": 1,
              "distinct_brand_cnt": {
                "value": 1
              }
            },
            {
              "key_as_string": "2016-09-01T00:00:00.000Z",
              "key": 1472688000000,
              "doc_count": 0,
              "distinct_brand_cnt": {
                "value": 0
              }
            },
            {
              "key_as_string": "2016-10-01T00:00:00.000Z",
              "key": 1475280000000,
              "doc_count": 1,
              "distinct_brand_cnt": {
                "value": 1
              }
            },
            {
              "key_as_string": "2016-11-01T00:00:00.000Z",
              "key": 1477958400000,
              "doc_count": 2,
              "distinct_brand_cnt": {
                "value": 1
              }
            },
            {
              "key_as_string": "2016-12-01T00:00:00.000Z",
              "key": 1480550400000,
              "doc_count": 0,
              "distinct_brand_cnt": {
                "value": 0
              }
            },
            {
              "key_as_string": "2017-01-01T00:00:00.000Z",
              "key": 1483228800000,
              "doc_count": 1,
              "distinct_brand_cnt": {
                "value": 1
              }
            },
            {
              "key_as_string": "2017-02-01T00:00:00.000Z",
              "key": 1485907200000,
              "doc_count": 1,
              "distinct_brand_cnt": {
                "value": 1
              }
            }
          ]
        }
      }
    }
    

    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,HTC...

    • 在多少个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

    1. 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 
                }
            }
        }
    }
    

    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" : "2016-10-28" }
    { "index": {}}
    { "latency" : 83, "province" : "江苏", "timestamp" : "2016-10-29" }
    { "index": {}}
    { "latency" : 92, "province" : "江苏", "timestamp" : "2016-10-29" }
    { "index": {}}
    { "latency" : 112, "province" : "江苏", "timestamp" : "2016-10-28" }
    { "index": {}}
    { "latency" : 68, "province" : "江苏", "timestamp" : "2016-10-28" }
    { "index": {}}
    { "latency" : 76, "province" : "江苏", "timestamp" : "2016-10-29" }
    { "index": {}}
    { "latency" : 101, "province" : "新疆", "timestamp" : "2016-10-28" }
    { "index": {}}
    { "latency" : 275, "province" : "新疆", "timestamp" : "2016-10-29" }
    { "index": {}}
    { "latency" : 166, "province" : "新疆", "timestamp" : "2016-10-29" }
    { "index": {}}
    { "latency" : 654, "province" : "新疆", "timestamp" : "2016-10-28" }
    { "index": {}}
    { "latency" : 389, "province" : "新疆", "timestamp" : "2016-10-28" }
    { "index": {}}
    { "latency" : 302, "province" : "新疆", "timestamp" : "2016-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
                }
              }
            }
          ]
        }
      }
    }
    

    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可以设置的越大

    基于doc value正排索引的聚合内部原理

    • 搜索+聚合,写个示例
    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
    
    
    用倒排索引做比对会这样
    ...
    ...
    ...
    ...
    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)的原理和优势

    • 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数据结构,正排索引

    正排索引
    ...
    ...
    ...
    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的所有值了,然后就可以执行分组聚合操作了(所以不准确?)

    doc value机制内核级原理深入探秘

    1. doc value原理
    • 建立索引的时候生成,PUT/POST的时候,就会生成doc value数据,也就是正排索引
    1. 核心原理与倒排索引类似
    • 正排索引,也会写入磁盘文件中,然后呢,os cache先进行缓存,以提升访问doc value正排索引的性能
    • 如果os cache内存大小不足够放得下整个正排索引,doc value,就会将doc value的数据写入磁盘文件中
    1. 性能问题:给jvm更少内存,64g服务器,给jvm最多16g

    es官方是建议,es大量是基于os cache来进行缓存和提升性能的,不建议用jvm内存来进行缓存,那样会导致一定的gc开销和oom问题
    给jvm更少的内存,给os cache更大的内存
    64g服务器,给jvm最多16g,几十个g的内存给os cache
    os cache可以提升doc value和倒排索引的缓存和查询效率

    1. 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也保存起来
    1. 如果没有最大公约数,采取offset结合压缩的方式:

    2. disable doc value

    • 如果的确不需要doc value,比如聚合等操作,那么可以禁用(正排索引),减少磁盘空间占用
    PUT my_index
    {
      "mappings": {
        "my_type": {
          "properties": {
            "my_field": {
              "type":       "keyword"
              "doc_values": false //禁用 
            }
          }
        }
      }
    }
    

    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执行聚合操作,而且会消耗很大的内存
    1. 给分词的field,设置fielddata=true
    POST /test_index/_mapping/test_type 
    {
      "properties": {
        "test_field": {
          "type": "text",
          "fielddata": true
        }
      }
    }
    

    查看mapping

    {
      "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
    1. 分词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,那么性能会很差

    fielddata内存控制以及circuit breaker断路器

    1. fielddata核心原理
    • fielddata加载到内存的过程是lazy加载的,对一个analzyed field执行聚合时,才会加载,而且是field-level加载的
      一个index的一个field,所有doc都会被加载,而不是少数doc,不是index-time创建,是query-time创建
    1. fielddata内存限制
    • indices.fielddata.cache.size: 20%,超出限制,清除内存已有fielddata数据
    • fielddata占用的内存超出了这个比例的限制,那么就清除掉内存中已有的fielddata数据
    • 默认无限制,限制内存使用,但是会导致频繁evict和reload,大量IO性能损耗,以及内存碎片和gc
    1. 监控fielddata内存使用
    GET /_stats/fielddata?fields=*
    GET /_nodes/stats/indices/fielddata?fields=*
    GET /_nodes/stats/indices/fielddata?level=indices&fields=*
    
    1. 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%以内
    

    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就不加载

    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的聚合性能当然会大幅度增强
    1. 序号标记预加载

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

    从深度优先到广度优先

    • 当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倍

    关系型与document类型数据模型对比

    • 关系型数据库的数据模型

    • es的document数据模型

    public class Department {
    	
    	private Integer deptId;
    	private String name;
    	private String desc;
    	private List<Employee> employees;
    
    }
    
    public class Employee {
    	
    	private Integer empId;
    	private String name;
    	private Integer age;
    	private String gender;
    	private Department dept;
    
    }
    
    • 关系型数据库中
    department表
    
    dept_id
    name
    desc
    
    employee表
    
    emp_id
    name
    age
    gender
    dept_id
    
    • 三范式 --> 将每个数据实体拆分为一个独立的数据表,同时使用主外键关联关系将多个数据表关联起来 --> 确保没有任何冗余的数据

    • 一份数据,只会放在一个数据表中 --> dept - name,部门名称,就只会放在department表中,不会在employee表中也放一个dept name,如果说你要查看某个员工的部门名称,那么必须通过员工表中的外键,dept_id,找到在部门表中对应的记录,然后找到部门名称

    es文档数据模型

    {
    	"deptId": "1",
    	"name": "研发部门",
    	"desc": "负责公司的所有研发项目",
    	"employees": [
    		{
    			"empId": "1",
    			"name": "张三",
    			"age": 28,
    			"gender": "男"
    		},
    		{
    			"empId": "2",
    			"name": "王兰",
    			"age": 25,
    			"gender": "女"
    		},
    		{
    			"empId": "3",
    			"name": "李四",
    			"age": 34,
    			"gender": "男"
    		}
    	]
    }
    
    • es,更加类似于面向对象的数据模型,将所有由关联关系的数据,放在一个doc

    • json类型数据中,整个数据的关系,还有完整的数据,都放在了一起

    通过应用层join实现用户与博客的关联

    1. 构造用户与博客数据
    • 在构造数据模型的时候,还是将有关联关系的数据,然后分割为不同的实体,类似于关系型数据库中的模型

    • 案例背景:博客网站, 我们会模拟各种用户发表各种博客,然后针对用户和博客之间的关系进行数据建模,同时针对建模好的数据执行各种搜索/聚合的操作

    添加数据

    PUT /website/users/1 
    {
      "name":     "小鱼儿",
      "email":    "xiaoyuer@sina.com",
      "birthday":      "1980-01-01"
    }
    
    PUT /website/blogs/1
    {
      "title":    "我的第一篇博客",
      "content":     "这是我的第一篇博客,开通啦!!!"
      "userId":     1 
    }
    
    • 一个用户对应多个博客,一对多的关系,做了建模

    • 建模方式,分割实体,类似三范式的方式,用主外键关联关系,将多个实体关联起来

    1. 搜索小鱼儿发表的所有博客

    先搜索出来小鱼儿的用户ID,再通过ID搜索所有的BLOG

    GET /website/users/_search 
    {
      "query": {
        "term": {
          "name.keyword": {
            "value": "小鱼儿"
          }
        }
      }
    }
    
    {
      "took": 91,
      "timed_out": false,
      "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
      },
      "hits": {
        "total": 1,
        "max_score": 0.2876821,
        "hits": [
          {
            "_index": "website",
            "_type": "users",
            "_id": "1",
            "_score": 0.2876821,
            "_source": {
              "name": "小鱼儿",
              "email": "xiaoyuer@sina.com",
              "birthday": "1980-01-01"
            }
          }
        ]
      }
    }
    
    • 比如这里搜索的是,1万个用户的博客,可能第一次搜索,会得到1万个userId
    GET /website/blogs/_search 
    {
      "query": {
        "constant_score": {
          "filter": {
            "terms": {
              "userId": [
                1
              ]
            }
          }
        }
      }
    }
    
    
    • 第二次搜索的时候,要放入terms中1万个userId,才能进行搜索,这个时候性能比较差了
    {
      "took": 4,
      "timed_out": false,
      "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
      },
      "hits": {
        "total": 1,
        "max_score": 1,
        "hits": [
          {
            "_index": "website",
            "_type": "blogs",
            "_id": "1",
            "_score": 1,
            "_source": {
              "title": "小鱼儿的第一篇博客",
              "content": "大家好,我是小鱼儿,这是我写的第一篇博客!",
              "userId": 1
            }
          }
        ]
      }
    }
    
    • 上面的操作,就属于应用层的join,在应用层先查出一份数据,然后再查出一份数据,进行关联

    3。 优点和缺点

    • 优点:数据不冗余,维护方便
    • 缺点:应用层join,如果关联数据过多,导致查询过大,性能很差

    通过数据冗余实现用户与博客的关联

    1. 构造冗余的用户和博客数据
    • 第二种建模方式:用冗余数据,采用文档数据模型,进行数据建模,实现用户和博客的关联
    PUT /website/users/1
    {
      "name":     "小鱼儿",
      "email":    "xiaoyuer@sina.com",
      "birthday":      "1980-01-01"
    }
    
    PUT /website/blogs/1
    {
      "title": "小鱼儿的第一篇博客",
      "content": "大家好,我是小鱼儿。。。",
      "userInfo": {
        "userId": 1,
        "username": "小鱼儿"
      }
    }
    
    • 冗余数据,就是说,将可能会进行搜索的条件和要搜索的数据,放在一个doc中
    1. 基于冗余用户数据搜索博客
    GET /website/blogs/_search 
    {
      "query": {
        "term": {
          "userInfo.username.keyword": {
            "value": "小鱼儿"
          }
        }
      }
    }
    
    • 就不需要走应用层的join,先搜一个数据,找到id,再去搜另一份数据

    • 直接走一个有冗余数据的type即可,指定要的搜索条件,即可搜索出自己想要的数据来

    1. 优点和缺点
    • 优点:性能高,不需要执行两次搜索

    • 缺点:数据冗余,维护成本高 --> 每次如果你的username变化了,同时要更新user type和blog type

    • 一般来说,对于es这种NoSQL类型的数据存储来讲,都是冗余模式....

    • 当然,你要去维护数据的关联关系,也是很有必要的,所以一旦出现冗余数据的修改,必须记得将所有关联的数据全部更新

    对每个用户发表的博客进行分组

    1. 构造更多测试数据
    PUT /website/users/3
    {
      "name": "黄药师",
      "email": "huangyaoshi@sina.com",
      "birthday": "1970-10-24"
    }
    
    
    PUT /website/blogs/3
    {
      "title": "我是黄药师",
      "content": "我是黄药师啊,各位同学们!!!",
      "userInfo": {
        "userId": 1,
        "userName": "黄药师"
      }
    }
    
    PUT /website/users/2
    {
      "name": "花无缺",
      "email": "huawuque@sina.com",
      "birthday": "1980-02-02"
    }
    
    PUT /website/blogs/4
    {
      "title": "花无缺的身世揭秘",
      "content": "大家好,我是花无缺,所以我的身世是。。。",
      "userInfo": {
        "userId": 2,
        "userName": "花无缺"
      }
    }
    
    1. 对每个用户发表的博客进行分组
    • 比如说,小鱼儿发表的那些博客,花无缺发表了哪些博客,黄药师发表了哪些博客
    GET /website/blogs/_search 
    {
      "size": 0, 
      "aggs": {
        "group_by_username": {
          "terms": {
            "field": "userInfo.username.keyword"
          },
          "aggs": {
            "top_blogs": {
              "top_hits": {
                "_source": {  
                  "include": "title" //指定要拿到blog的信息
                }, 
                "size": 5  //前五条
              }
            }
          }
        }
      }
    }
    
    

    对文件系统进行数据建模以及文件搜索实战

    • 数据建模,对类似文件系统这种的有多层级关系的数据进行建模
    1. 文件系统数据构造
    PUT /fs
    {
      "settings": {
        "analysis": {
          "analyzer": {
            "paths": { //自定义分词器名称
              "tokenizer": "path_hierarchy"
            }
          }
        }
      }
    }
    
    • path_hierarchy tokenizer讲解(路径层级关系)

    • /a/b/c/d --> path_hierarchy -> /a/b/c/d, /a/b/c, /a/b, /a

    PUT /fs/_mapping/file
    {
      "properties": {
        "name": { 
          "type":  "keyword"
        },
        "path": { 
          "type":  "keyword",
          "fields": {
            "tree": { 
              "type":     "text",
              "analyzer": "paths" 
            }
          }
        }
      }
    }
    
    PUT /fs/file/1
    {
      "name":     "README.txt", 
      "path":     "/workspace/projects/helloworld", 
      "contents": "这是我的第一个elasticsearch程序"
    }
    
    1. 对文件系统执行搜索

    文件搜索需求:查找一份,内容包括elasticsearch,在/workspace/projects/hellworld这个目录下的文件

    GET /fs/file/_search 
    {
      "query": {
        "bool": {
          "must": [
            {
              "match": {
                "contents": "elasticsearch"
              }
            },
            {
              "constant_score": {
                "filter": {
                  "term": {
                    "path": "/workspace/projects/helloworld"
                  }
                }
              }
            }
          ]
        }
      }
    }
    
    {
      "took": 2,
      "timed_out": false,
      "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
      },
      "hits": {
        "total": 1,
        "max_score": 1.284885,
        "hits": [
          {
            "_index": "fs",
            "_type": "file",
            "_id": "1",
            "_score": 1.284885,
            "_source": {
              "name": "README.txt",
              "path": "/workspace/projects/helloworld",
              "contents": "这是我的第一个elasticsearch程序"
            }
          }
        ]
      }
    }
    
    • 搜索需求2:搜索/workspace目录下,内容包含elasticsearch的所有的文件

    用了该分词器后就会分成这三个

    /workspace/projects/helloworld    doc1
    /workspace/projects               doc1
    /workspace                        doc1
    
    GET /fs/file/_search 
    {
      "query": {
        "bool": {
          "must": [
            {
              "match": {
                "contents": "elasticsearch"
              }
            },
            {
              "constant_score": {
                "filter": {
                  "term": {
                    "path.tree": "/workspace" //这里需要用Path.tree
                  }
                }
              }
            }
          ]
        }
      }
    }
    

    基于全局锁实现悲观锁并发控制

    1. 悲观锁的简要说明
    • 基于version的乐观锁并发控制

    • 在数据建模,结合文件系统建模的这个案例,把悲观锁的并发控制,3种锁粒度,都给大家仔细讲解一下

    • 最粗的一个粒度,全局锁

    • 例子如果多个线程,都过来,要并发地给/workspace/projects/helloworld下的README.txt修改文件名

    • 实际上要进行并发的控制,避免出现多线程的并发安全问题,比如多个线程修改,纯并发,先执行的修改操作被后执行的修改操作给覆盖了

    • 每个线程都要先get current version

    • 带着这个current version去执行修改,如果一旦发现数据已经被别人给修改了,version号跟之前自己获取的已经不一样了; 那么必须重新获取新的version号再次尝试修改

    • 上来就尝试给这条数据加个锁,然后呢,此时就只有你能执行各种各样的操作了,其他人不能执行操作

    • 第一种锁:全局锁,直接锁掉整个fs 这个 index

    1. 全局锁的上锁实验
    PUT /fs/lock/global/_create
    {} //数据的空的
    
    • fs: 你要上锁的那个index

    • lock: 就是你指定的一个对这个index上全局锁的一个type

    • global: 就是你上的全局锁对应的这个doc的id

    • _create:强制必须是创建,如果/fs/lock/global这个doc已经存在,那么创建失败,报错

    • 利用了创建doc来进行上锁

    /fs/lock/global /index/type/id --> doc
    
    {
      "_index": "fs",
      "_type": "lock",
      "_id": "global",
      "_version": 1,
      "result": "created",
      "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
      },
      "created": true
    }
    
    • 另外一个线程同时尝试上锁
    PUT /fs/lock/global/_create //因为已经存在了,用create 语法创建会报错
    {}
    

    会报错

    {
      "error": {
        "root_cause": [
          {
            "type": "version_conflict_engine_exception",
            "reason": "[lock][global]: version conflict, document already exists (current version [1])",
            "index_uuid": "IYbj0OLGQHmMUpLfbhD4Hw",
            "shard": "2",
            "index": "fs"
          }
        ],
        "type": "version_conflict_engine_exception",
        "reason": "[lock][global]: version conflict, document already exists (current version [1])",
        "index_uuid": "IYbj0OLGQHmMUpLfbhD4Hw",
        "shard": "2",
        "index": "fs"
      },
      "status": 409
    }
    

    上锁后就可以执行各种操作

    POST /fs/file/1/_update
    {
      "doc": {
        "name": "README1.txt"
      }
    }
    
    {
      "_index": "fs",
      "_type": "file",
      "_id": "1",
      "_version": 2,
      "result": "updated",
      "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
      }
    }
    
    DELETE /fs/lock/global //做完操作后直接删除锁
    
    {
      "found": true,
      "_index": "fs",
      "_type": "lock",
      "_id": "global",
      "_version": 2,
      "result": "deleted",
      "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
      }
    }
    
    • 另外一个线程,因为之前发现上锁失败,反复尝试重新上锁,终于上锁成功了,因为之前获取到全局锁的那个线程已经delete /fs/lock/global全局锁了
    PUT /fs/lock/global/_create
    {}
    
    {
      "_index": "fs",
      "_type": "lock",
      "_id": "global",
      "_version": 3,
      "result": "created",
      "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
      },
      "created": true
    }
    
    

    线程2上锁成功后执行各种操作

    POST /fs/file/1/_update 
    {
      "doc": {
        "name": "README.txt"
      }
    }
    
    {
      "_index": "fs",
      "_type": "file",
      "_id": "1",
      "_version": 3,
      "result": "updated",
      "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
      }
    }
    

    线程2删除锁

    DELETE /fs/lock/global
    
    1. 全局锁的优点和缺点
    • 优点:操作非常简单,非常容易使用,成本低

    • 缺点:你直接就把整个index给上锁了,这个时候对index中所有的doc的操作,都会被block住,导致整个系统的并发能力很低

    • 上锁解锁的操作不是频繁,然后每次上锁之后,执行的操作的耗时不会太长,用这种方式,方便

    基于document锁实现悲观锁并发控制

    1. 对document level锁,详细的讲解
    • 全局锁,一次性就锁整个index,对这个index的所有增删改操作都会被block住,如果上锁不频繁,还可以,比较简单

    • 细粒度的一个锁,document锁,顾名思义,每次就锁你要操作的,你要执行增删改的那些doc,doc锁了,其他线程就不能对这些doc执行增删改操作了

    • 但是你只是锁了部分doc,其他线程对其他的doc还是可以上锁和执行增删改操作的

    document锁,是用脚本进行上锁
    POST /fs/lock/1/_update
    {
      "upsert": { "process_id": 123 },
      "script": "if ( ctx._source.process_id != process_id ) { assert false }; ctx.op = 'noop';"
      "params": {
        "process_id": 123
      }
    }
    

    query string解释:

    • /fs/lock,是固定的,就是说fs下的lock type,专门用于进行上锁
    • /fs/lock/id,比如1,id其实就是你要上锁的那个doc的id,代表了某个doc数据对应的lock(也是一个doc)
    • _update + upsert:执行upsert操作

    参数:

    • params,里面有个process_id,process_id,是你的要执行增删改操作的进程的唯一id,比如说可以在java系统,启动的时候,给你的每个线程都用UUID自动生成一个thread id,你的系统进程启动的时候给整个进程也分配一个UUID。process_id + thread_id就代表了某一个进程下的某个线程的唯一标识。可以自己用UUID生成一个唯一ID

    • process_id很重要,会在lock中,设置对对应的doc加锁的进程的id,这样其他进程过来的时候,才知道,这条数据已经被别人给锁了

    • assert false,不是当前进程加锁的话,则抛出异常

    • ctx.op='noop',不做任何修改

    • 如果该document之前没有被锁,/fs/lock/1之前不存在,也就是doc id=1没有被别人上过锁; upsert的语法,那么执行index操作,创建一个/fs/lock/id这条数据,而且用params中的数据作为这个lock的数据。process_id被设置为123,script不执行。这个时候象征着process_id=123的进程已经锁了一个doc了。

    • 如果document被锁了,就是说/fs/lock/1已经存在了,代表doc id=1已经被某个进程给锁了。那么执行update操作,script,此时会比对process_id,如果相同,就是说,某个进程,之前锁了这个doc,然后这次又过来,就可以直接对这个doc执行操作,说明是该进程之前锁的doc,则不报错,不执行任何操作,返回success; 如果process_id比对不上,说明doc被其他doc给锁了,此时报错

    /fs/lock/1
    {
      "process_id": 123
    }
    
    POST /fs/lock/1/_update
    {
      "upsert": { "process_id": 123 },
      "script": "if ( ctx._source.process_id != process_id ) { assert false }; ctx.op = 'noop';"
      "params": {
        "process_id": 123
      }
    }
    
    • script:ctx._source.process_id,123

    • process_id:加锁的upsert请求中带过来额proess_id

    • 如果两个process_id相同,说明是一个进程先加锁,然后又过来尝试加锁,可能是要执行另外一个操作,此时就不会block,对同一个process_id是不会block,ctx.op= 'noop',什么都不做,返回一个success

    • 如果说已经有一个进程加了锁了

    /fs/lock/1
    {
      "process_id": 123
    }
    
    POST /fs/lock/1/_update
    {
      "upsert": { "process_id": 123 },
      "script": "if ( ctx._source.process_id != process_id ) { assert false }; ctx.op = 'noop';"
      "params": {
        "process_id": 234
      }
    }
    
    "script": "if ( ctx._source.process_id != process_id ) { assert false }; ctx.op = 'noop';"
    
    • ctx._source.process_id:123

    • process_id: 234

    • process_id不相等,说明这个doc之前已经被别人上锁了,process_id=123上锁了;

    • process_id=234过来再次尝试上锁,失败,assert false,就会报错

    • 此时遇到报错的process,就应该尝试重新上锁,直到上锁成功

    • 有报错的话,如果有些doc被锁了,那么需要重试

    • 直到所有锁定都成功,执行自己的操作

    • 释放所有的锁

    1. 上document锁的完整实验过程
    scripts/judge-lock.groovy: 
      if ( ctx._source.process_id != process_id ) { assert false }; ctx.op = 'noop';
    
    POST /fs/lock/1/_update
    {
      "upsert": { "process_id": 123 },
      "script": {
        "lang": "groovy",
        "file": "judge-lock", 
        "params": {
          "process_id": 123
        }
      }
    }
    
    {
      "_index": "fs",
      "_type": "lock",
      "_id": "1",
      "_version": 1,
      "result": "created",
      "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
      }
    }
    
    GET /fs/lock/1
    
    {
      "_index": "fs",
      "_type": "lock",
      "_id": "1",
      "_version": 1,
      "found": true,
      "_source": {
        "process_id": 123
      }
    }
    
    POST /fs/lock/1/_update
    {
      "upsert": { "process_id": 234 },
      "script": {
        "lang": "groovy",
        "file": "judge-lock", 
        "params": {
          "process_id": 234
        }
      }
    }
    
    {
      "error": {
        "root_cause": [
          {
            "type": "remote_transport_exception",
            "reason": "[4onsTYV][127.0.0.1:9300][indices:data/write/update[s]]"
          }
        ],
        "type": "illegal_argument_exception",
        "reason": "failed to execute script",
        "caused_by": {
          "type": "script_exception",
          "reason": "error evaluating judge-lock",
          "caused_by": {
            "type": "power_assertion_error",
            "reason": "assert false
    "
          },
          "script_stack": [],
          "script": "",
          "lang": "groovy"
        }
      },
      "status": 400
    }
    
    POST /fs/lock/1/_update
    {
      "upsert": { "process_id": 123 },
      "script": {
        "lang": "groovy",
        "file": "judge-lock", 
        "params": {
          "process_id": 123
        }
      }
    }
    
    {
      "_index": "fs",
      "_type": "lock",
      "_id": "1",
      "_version": 1,
      "result": "noop",
      "_shards": {
        "total": 0,
        "successful": 0,
        "failed": 0
      }
    }
    
    POST /fs/file/1/_update
    {
      "doc": {
        "name": "README1.txt"
      }
    }
    
    {
      "_index": "fs",
      "_type": "file",
      "_id": "1",
      "_version": 4,
      "result": "updated",
      "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
      }
    }
    
    POST /fs/_refresh 
    
    GET /fs/lock/_search?scroll=1m
    {
      "query": {
        "term": {
          "process_id": 123
        }
      }
    }
    
    {
      "_scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAACPkFjRvbnNUWVZaVGpHdklqOV9zcFd6MncAAAAAAAAj5RY0b25zVFlWWlRqR3ZJajlfc3BXejJ3AAAAAAAAI-YWNG9uc1RZVlpUakd2SWo5X3NwV3oydwAAAAAAACPnFjRvbnNUWVZaVGpHdklqOV9zcFd6MncAAAAAAAAj6BY0b25zVFlWWlRqR3ZJajlfc3BXejJ3",
      "took": 51,
      "timed_out": false,
      "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
      },
      "hits": {
        "total": 1,
        "max_score": 1,
        "hits": [
          {
            "_index": "fs",
            "_type": "lock",
            "_id": "1",
            "_score": 1,
            "_source": {
              "process_id": 123
            }
          }
        ]
      }
    }
    
    PUT /fs/lock/_bulk
    { "delete": { "_id": 1}}
    
    {
      "took": 20,
      "errors": false,
      "items": [
        {
          "delete": {
            "found": true,
            "_index": "fs",
            "_type": "lock",
            "_id": "1",
            "_version": 2,
            "result": "deleted",
            "_shards": {
              "total": 2,
              "successful": 1,
              "failed": 0
            },
            "status": 200
          }
        }
      ]
    }
    
    POST /fs/lock/1/_update
    {
      "upsert": { "process_id": 234 },
      "script": {
        "lang": "groovy",
        "file": "judge-lock", 
        "params": {
          "process_id": 234
        }
      }
    }
    
    • process_id=234上锁就成功了

    基于共享锁和排他锁实现悲观锁并发控制

    1. 共享锁和排他锁的说明
    • 共享锁:这份数据是共享的,然后多个线程过来,都可以获取同一个数据的共享锁,然后对这个数据执行读操作
    • 排他锁:是排他的操作,只能一个线程获取排他锁,然后执行增删改操作
    读写锁的分离
    • 如果只是要读取数据的话,那么任意个线程都可以同时进来然后读取数据,每个线程都可以上一个共享锁
      但是这个时候,如果有线程要过来修改数据,那么会尝试上排他锁,排他锁会跟共享锁互斥,也就是说,如果有人已经上了共享锁了,那么排他锁就不能上,就得等

    • 如果有人在读数据,就不允许别人来修改数据。反之,也是一样的.如果有人在修改数据,就是加了排他锁
      那么其他线程过来要修改数据,也会尝试加排他锁,此时会失败,锁冲突,必须等待,同时只能有一个线程修改数据
      如果有人过来同时要读取数据,那么会尝试加共享锁,此时会失败,因为共享锁和排他锁是冲突的.如果有在修改数据,就不允许别人来修改数据,也不允许别人来读取数据

    1. 共享锁和排他锁的实验
    • 第一步:有人在读数据,其他人也能过来读数据

    其他人上共享锁,次数+1

    judge-lock-2.groovy: if (ctx._source.lock_type == 'exclusive') { assert false }; ctx._source.lock_count++
    

    线程1上共享锁

    POST /fs/lock/1/_update 
    {
      "upsert": { 
        "lock_type":  "shared",
        "lock_count": 1
      },
      "script": {
      	"lang": "groovy",
      	"file": "judge-lock-2"
      }
    }
    

    线程2页来上共享锁

    POST /fs/lock/1/_update 
    {
      "upsert": { 
        "lock_type":  "shared",
        "lock_count": 1
      },
      "script": {
      	"lang": "groovy",
      	"file": "judge-lock-2"
      }
    }
    

    有人上共享锁后锁次数+1

    GET /fs/lock/1
    
    {
      "_index": "fs",
      "_type": "lock",
      "_id": "1",
      "_version": 3,
      "found": true,
      "_source": {
        "lock_type": "shared",
        "lock_count": 3
      }
    }
    

    就给大家模拟了,有人上了共享锁,你还是要上共享锁,直接上就行了,没问题,只是lock_count加1

    1. 已经有人上了共享锁,然后有人要上排他锁
    PUT /fs/lock/1/_create
    { "lock_type": "exclusive" }
    
    • 排他锁用的不是upsert语法,create语法,要求lock必须不能存在,直接自己是第一个上锁的人,上的是排他锁
    {
      "error": {
        "root_cause": [
          {
            "type": "version_conflict_engine_exception",
            "reason": "[lock][1]: version conflict, document already exists (current version [3])",
            "index_uuid": "IYbj0OLGQHmMUpLfbhD4Hw",
            "shard": "3",
            "index": "fs"
          }
        ],
        "type": "version_conflict_engine_exception",
        "reason": "[lock][1]: version conflict, document already exists (current version [3])",
        "index_uuid": "IYbj0OLGQHmMUpLfbhD4Hw",
        "shard": "3",
        "index": "fs"
      },
      "status": 409
    }
    
    • 如果已经有人上了共享锁,明显/fs/lock/1是存在的,create语法去上排他锁,肯定会报错
    1. 对共享锁进行解锁
    POST /fs/lock/1/_update
    {
      "script": {
      	"lang": "groovy",
      	"file": "unlock-shared"
      }
    }
    
    • 连续解锁3次,此时共享锁就彻底没了

    • 每次解锁一个共享锁,就对lock_count先减1,如果减了1之后,是0,那么说明所有的共享锁都解锁完了,此时就就将/fs/lock/1删除,就彻底解锁所有的共享锁

    1. 上排他锁,再上排他锁
    PUT /fs/lock/1/_create
    { "lock_type": "exclusive" }
    

    其他线程

    PUT /fs/lock/1/_create
    { "lock_type": "exclusive" }
    
    {
      "error": {
        "root_cause": [
          {
            "type": "version_conflict_engine_exception",
            "reason": "[lock][1]: version conflict, document already exists (current version [7])",
            "index_uuid": "IYbj0OLGQHmMUpLfbhD4Hw",
            "shard": "3",
            "index": "fs"
          }
        ],
        "type": "version_conflict_engine_exception",
        "reason": "[lock][1]: version conflict, document already exists (current version [7])",
        "index_uuid": "IYbj0OLGQHmMUpLfbhD4Hw",
        "shard": "3",
        "index": "fs"
      },
      "status": 409
    }
    
    1. 上排他锁,上共享锁
    POST /fs/lock/1/_update 
    {
      "upsert": { 
        "lock_type":  "shared",
        "lock_count": 1
      },
      "script": {
      	"lang": "groovy",
      	"file": "judge-lock-2"
      }
    }
    
    {
      "error": {
        "root_cause": [
          {
            "type": "remote_transport_exception",
            "reason": "[4onsTYV][127.0.0.1:9300][indices:data/write/update[s]]"
          }
        ],
        "type": "illegal_argument_exception",
        "reason": "failed to execute script",
        "caused_by": {
          "type": "script_exception",
          "reason": "error evaluating judge-lock-2",
          "caused_by": {
            "type": "power_assertion_error",
            "reason": "assert false
    "
          },
          "script_stack": [],
          "script": "",
          "lang": "groovy"
        }
      },
      "status": 400
    }
    
    1. 解锁排他锁
    DELETE /fs/lock/1
    

    基于nested object实现博客与评论嵌套关系

    1. 做一个实验,引出来为什么需要nested object
    • 冗余数据方式的来建模,其实用的就是object类型,我们这里又要引入一种新的object类型,nested object类型

    • 博客,评论,做的这种数据模型

    PUT /website/blogs/6
    {
      "title": "花无缺发表的一篇帖子",
      "content":  "我是花无缺,大家要不要考虑一下投资房产和买股票的事情啊。。。",
      "tags":  [ "投资", "理财" ],
      "comments": [ 
        {
          "name":    "小鱼儿",
          "comment": "什么股票啊?推荐一下呗",
          "age":     28,
          "stars":   4,
          "date":    "2016-09-01"
        },
        {
          "name":    "黄药师",
          "comment": "我喜欢投资房产,风,险大收益也大",
          "age":     31,
          "stars":   5,
          "date":    "2016-10-22"
        }
      ]
    }
    

    现在我们要查询 被年龄是28岁的黄药师评论过的博客,搜索

    GET /website/blogs/_search
    {
      "query": {
        "bool": {
          "must": [
            { "match": { "comments.name": "黄药师" }},
            { "match": { "comments.age":  28      }} 
          ]
        }
      }
    }
    
    {
      "took": 102,
      "timed_out": false,
      "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
      },
      "hits": {
        "total": 1,
        "max_score": 1.8022683,
        "hits": [
          {
            "_index": "website",
            "_type": "blogs",
            "_id": "6",
            "_score": 1.8022683,
            "_source": {
              "title": "花无缺发表的一篇帖子",
              "content": "我是花无缺,大家要不要考虑一下投资房产和买股票的事情啊。。。",
              "tags": [
                "投资",
                "理财"
              ],
              "comments": [
                {
                  "name": "小鱼儿",
                  "comment": "什么股票啊?推荐一下呗",
                  "age": 28,
                  "stars": 4,
                  "date": "2016-09-01"
                },
                {
                  "name": "黄药师",
                  "comment": "我喜欢投资房产,风,险大收益也大",
                  "age": 31,
                  "stars": 5,
                  "date": "2016-10-22"
                }
              ]
            }
          }
        ]
      }
    }
    
    • 结果是。。。好像不太对啊???年龄不对

    • object类型数据结构的底层存储。。。

    {
      "title":            [ "花无缺", "发表", "一篇", "帖子" ],
      "content":             [ "我", "是", "花无缺", "大家", "要不要", "考虑", "一下", "投资", "房产", "买", "股票", "事情" ],
      "tags":             [ "投资", "理财" ],
      "comments.name":    [ "小鱼儿", "黄药师" ],
      "comments.comment": [ "什么", "股票", "推荐", "我", "喜欢", "投资", "房产", "风险", "收益", "大" ],
      "comments.age":     [ 28, 31 ],
      "comments.stars":   [ 4, 5 ],
      "comments.date":    [ 2016-09-01, 2016-10-22 ]
    }
    
    • object类型底层数据结构,会将一个json数组中的数据,进行扁平化,所以,直接命中了这个document,name=黄药师,age=28,正好符合
    1. 引入nested object类型,来解决object类型底层数据结构导致的问题
    • 修改mapping,将comments的类型从object设置为nested
    PUT /website
    {
      "mappings": {
        "blogs": {
          "properties": {
            "comments": {
              "type": "nested", 
              "properties": {
                "name":    { "type": "string"  },
                "comment": { "type": "string"  },
                "age":     { "type": "short"   },
                "stars":   { "type": "short"   },
                "date":    { "type": "date"    }
              }
            }
          }
        }
      }
    }
    
    { 
      "comments.name":    [ "小鱼儿" ],
      "comments.comment": [ "什么", "股票", "推荐" ],
      "comments.age":     [ 28 ],
      "comments.stars":   [ 4 ],
      "comments.date":    [ 2014-09-01 ]
    }
    { 
      "comments.name":    [ "黄药师" ],
      "comments.comment": [ "我", "喜欢", "投资", "房产", "风险", "收益", "大" ],
      "comments.age":     [ 31 ],
      "comments.stars":   [ 5 ],
      "comments.date":    [ 2014-10-22 ]
    }
    { 
      "title":            [ "花无缺", "发表", "一篇", "帖子" ],
      "body":             [ "我", "是", "花无缺", "大家", "要不要", "考虑", "一下", "投资", "房产", "买", "股票", "事情" ],
      "tags":             [ "投资", "理财" ]
    }
    

    再次搜索,成功了。。。

    GET /website/blogs/_search 
    {
      "query": {
        "bool": {
          "must": [
            {
              "match": {
                "title": "花无缺"
              }
            },
            {
              "nested": {
                "path": "comments",
                "query": {
                  "bool": {
                    "must": [
                      {
                        "match": {
                          "comments.name": "黄药师"
                        }
                      },
                      {
                        "match": {
                          "comments.age": 28
                        }
                      }
                    ]
                  }
                }
              }
            }
          ]
        }
      }
    }
    
    • score_mode:max,min,avg,none,默认是avg

    • 如果搜索命中了多个nested document,如何讲个多个nested document的分数合并为一个分数

    对嵌套的博客评论数据进行聚合分析

    • 我们讲解一下基于nested object中的数据进行聚合分析

    • 聚合数据分析的需求1:按照评论日期进行bucket划分,然后拿到每个月的评论的评分的平均值

    GET /website/blogs/_search 
    {
      "size": 0, 
      "aggs": {
        "comments_path": {
          "nested": {
            "path": "comments"
          }, 
          "aggs": {
            "group_by_comments_date": {
              "date_histogram": {
                "field": "comments.date",
                "interval": "month",
                "format": "yyyy-MM"
              },
              "aggs": {
                "avg_stars": {
                  "avg": {
                    "field": "comments.stars"
                  }
                }
              }
            }
          }
        }
      }
    }
    
    {
      "took": 52,
      "timed_out": false,
      "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
      },
      "hits": {
        "total": 2,
        "max_score": 0,
        "hits": []
      },
      "aggregations": {
        "comments_path": {
          "doc_count": 4,
          "group_by_comments_date": {
            "buckets": [
              {
                "key_as_string": "2016-08",
                "key": 1470009600000,
                "doc_count": 1,
                "avg_stars": {
                  "value": 3
                }
              },
              {
                "key_as_string": "2016-09",
                "key": 1472688000000,
                "doc_count": 2,
                "avg_stars": {
                  "value": 4.5
                }
              },
              {
                "key_as_string": "2016-10",
                "key": 1475280000000,
                "doc_count": 1,
                "avg_stars": {
                  "value": 5
                }
              }
            ]
          }
        }
      }
    }
    
    GET /website/blogs/_search 
    {
      "size": 0,
      "aggs": {
        "comments_path": {
          "nested": {
            "path": "comments"
          },
          "aggs": {
            "group_by_comments_age": {
              "histogram": {
                "field": "comments.age",
                "interval": 10
              },
              "aggs": {
                "reverse_path": {
                  "reverse_nested": {}, 
                  "aggs": {
                    "group_by_tags": {
                      "terms": {
                        "field": "tags.keyword"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
    
    {
      "took": 5,
      "timed_out": false,
      "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
      },
      "hits": {
        "total": 2,
        "max_score": 0,
        "hits": []
      },
      "aggregations": {
        "comments_path": {
          "doc_count": 4,
          "group_by_comments_age": {
            "buckets": [
              {
                "key": 20,
                "doc_count": 1,
                "reverse_path": {
                  "doc_count": 1,
                  "group_by_tags": {
                    "doc_count_error_upper_bound": 0,
                    "sum_other_doc_count": 0,
                    "buckets": [
                      {
                        "key": "投资",
                        "doc_count": 1
                      },
                      {
                        "key": "理财",
                        "doc_count": 1
                      }
                    ]
                  }
                }
              },
              {
                "key": 30,
                "doc_count": 3,
                "reverse_path": {
                  "doc_count": 2,
                  "group_by_tags": {
                    "doc_count_error_upper_bound": 0,
                    "sum_other_doc_count": 0,
                    "buckets": [
                      {
                        "key": "大侠",
                        "doc_count": 1
                      },
                      {
                        "key": "投资",
                        "doc_count": 1
                      },
                      {
                        "key": "理财",
                        "doc_count": 1
                      },
                      {
                        "key": "练功",
                        "doc_count": 1
                      }
                    ]
                  }
                }
              }
            ]
          }
        }
      }
    }
    

    研发中心管理案例以及父子关系数据建模

    • nested object的建模,有个不好的地方,就是采取的是类似冗余数据的方式,将多个数据都放在一起了,维护成本就比较高

    • parent child建模方式,采取的是类似于关系型数据库的三范式类的建模,多个实体都分割开来,每个实体之间都通过一些关联方式,进行了父子关系的关联,各种数据不需要都放在一起,父doc和子doc分别在进行更新的时候,都不会影响对方

    • 一对多关系的建模,维护起来比较方便,而且我们之前说过,类似关系型数据库的建模方式,应用层join的方式,会导致性能比较差,因为做多次搜索。父子关系的数据模型,不会,性能很好。因为虽然数据实体之间分割开来,但是我们在搜索的时候,由es自动为我们处理底层的关联关系,并且通过一些手段保证搜索性能。

    • 父子关系数据模型,相对于nested数据模型来说,优点是父doc和子doc互相之间不会影响

    • 要点:父子关系元数据映射,用于确保查询时候的高性能,但是有一个限制,就是父子数据必须存在于一个shard中

    • 父子关系数据存在一个shard中,而且还有映射其关联关系的元数据,那么搜索父子关系数据的时候,不用跨分片,一个分片本地自己就搞定了,性能当然高咯

    • 案例背景:研发中心员工管理案例,一个IT公司有多个研发中心,每个研发中心有多个员工

    PUT /company
    {
      "mappings": {
        "rd_center": {},
        "employee": {
          "_parent": {
            "type": "rd_center" 
          }
        }
      }
    }
    
    
    • 父子关系建模的核心,多个type之间有父子关系,用_parent指定父type
    POST /company/rd_center/_bulk
    { "index": { "_id": "1" }}
    { "name": "北京研发总部", "city": "北京", "country": "中国" }
    { "index": { "_id": "2" }}
    { "name": "上海研发中心", "city": "上海", "country": "中国" }
    { "index": { "_id": "3" }}
    { "name": "硅谷人工智能实验室", "city": "硅谷", "country": "美国" }
    
    • shard路由的时候,id=1的rd_center doc,默认会根据id进行路由,到某一个shard

    加入员工数据,指定parent

    PUT /company/employee/1?parent=1 
    {
      "name":  "张三",
      "birthday":   "1970-10-24",
      "hobby": "爬山"
    }
    
    • 维护父子关系的核心,parent=1,指定了这个数据的父doc的id

    • 此时,parent-child关系,就确保了说,父doc和子doc都是保存在一个shard上的。内部原理还是doc routing,employee和rd_center的数据,都会用parent id作为routing,这样就会到一个shard

    • 就不会根据id=1的employee doc的id进行路由了,而是根据parent=1进行路由,会根据父doc的id进行路由,那么就可以通过底层的路由机制,保证父子数据存在于一个shard中

    POST /company/employee/_bulk
    { "index": { "_id": 2, "parent": "1" }}
    { "name": "李四", "birthday": "1982-05-16", "hobby": "游泳" }
    { "index": { "_id": 3, "parent": "2" }}
    { "name": "王二", "birthday": "1979-04-01", "hobby": "爬山" }
    { "index": { "_id": 4, "parent": "3" }}
    { "name": "赵五", "birthday": "1987-05-11", "hobby": "骑马" }
    

    根据员工信息和研发中心互相搜索父子数据

    • 我们已经建立了父子关系的数据模型之后,就要基于这个模型进行各种搜索和聚合了
    1. 搜索有1980年以后出生的员工的研发中心
    GET /company/rd_center/_search
    {
      "query": {
        "has_child": {
          "type": "employee",
          "query": {
            "range": {
              "birthday": {
                "gte": "1980-01-01"
              }
            }
          }
        }
      }
    }
    
    {
      "took": 33,
      "timed_out": false,
      "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
      },
      "hits": {
        "total": 2,
        "max_score": 1,
        "hits": [
          {
            "_index": "company",
            "_type": "rd_center",
            "_id": "1",
            "_score": 1,
            "_source": {
              "name": "北京研发总部",
              "city": "北京",
              "country": "中国"
            }
          },
          {
            "_index": "company",
            "_type": "rd_center",
            "_id": "3",
            "_score": 1,
            "_source": {
              "name": "硅谷人工智能实验室",
              "city": "硅谷",
              "country": "美国"
            }
          }
        ]
      }
    }
    
    1. 搜索有名叫张三的员工的研发中心
    GET /company/rd_center/_search
    {
      "query": {
        "has_child": {
          "type":       "employee",
          "query": {
            "match": {
              "name": "张三"
            }
          }
        }
      }
    }
    
    {
      "took": 2,
      "timed_out": false,
      "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
      },
      "hits": {
        "total": 1,
        "max_score": 1,
        "hits": [
          {
            "_index": "company",
            "_type": "rd_center",
            "_id": "1",
            "_score": 1,
            "_source": {
              "name": "北京研发总部",
              "city": "北京",
              "country": "中国"
            }
          }
        ]
      }
    }
    
    1. 搜索有至少2个以上员工的研发中心
    GET /company/rd_center/_search
    {
      "query": {
        "has_child": {
          "type":         "employee",
          "min_children": 2, 
          "query": {
            "match_all": {}
          }
        }
      }
    }
    
    {
      "took": 5,
      "timed_out": false,
      "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
      },
      "hits": {
        "total": 1,
        "max_score": 1,
        "hits": [
          {
            "_index": "company",
            "_type": "rd_center",
            "_id": "1",
            "_score": 1,
            "_source": {
              "name": "北京研发总部",
              "city": "北京",
              "country": "中国"
            }
          }
        ]
      }
    }
    
    1. 搜索在中国的研发中心的员工
    GET /company/employee/_search 
    {
      "query": {
        "has_parent": {
          "parent_type": "rd_center",
          "query": {
            "term": {
              "country.keyword": "中国"
            }
          }
        }
      }
    }
    
    {
      "took": 5,
      "timed_out": false,
      "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
      },
      "hits": {
        "total": 3,
        "max_score": 1,
        "hits": [
          {
            "_index": "company",
            "_type": "employee",
            "_id": "3",
            "_score": 1,
            "_routing": "2",
            "_parent": "2",
            "_source": {
              "name": "王二",
              "birthday": "1979-04-01",
              "hobby": "爬山"
            }
          },
          {
            "_index": "company",
            "_type": "employee",
            "_id": "1",
            "_score": 1,
            "_routing": "1",
            "_parent": "1",
            "_source": {
              "name": "张三",
              "birthday": "1970-10-24",
              "hobby": "爬山"
            }
          },
          {
            "_index": "company",
            "_type": "employee",
            "_id": "2",
            "_score": 1,
            "_routing": "1",
            "_parent": "1",
            "_source": {
              "name": "李四",
              "birthday": "1982-05-16",
              "hobby": "游泳"
            }
          }
        ]
      }
    }
    

    对每个国家的员工兴趣爱好进行聚合统计

    GET /company/rd_center/_search 
    {
      "size": 0,
      "aggs": {
        "group_by_country": {
          "terms": {
            "field": "country.keyword"
          },
          "aggs": {
            "group_by_child_employee": {
              "children": {
                "type": "employee"
              },
              "aggs": {
                "group_by_hobby": {
                  "terms": {
                    "field": "hobby.keyword"
                  }
                }
              }
            }
          }
        }
      }
    }
    
    {
      "took": 15,
      "timed_out": false,
      "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
      },
      "hits": {
        "total": 3,
        "max_score": 0,
        "hits": []
      },
      "aggregations": {
        "group_by_country": {
          "doc_count_error_upper_bound": 0,
          "sum_other_doc_count": 0,
          "buckets": [
            {
              "key": "中国",
              "doc_count": 2,
              "group_by_child_employee": {
                "doc_count": 3,
                "group_by_hobby": {
                  "doc_count_error_upper_bound": 0,
                  "sum_other_doc_count": 0,
                  "buckets": [
                    {
                      "key": "爬山",
                      "doc_count": 2
                    },
                    {
                      "key": "游泳",
                      "doc_count": 1
                    }
                  ]
                }
              }
            },
            {
              "key": "美国",
              "doc_count": 1,
              "group_by_child_employee": {
                "doc_count": 1,
                "group_by_hobby": {
                  "doc_count_error_upper_bound": 0,
                  "sum_other_doc_count": 0,
                  "buckets": [
                    {
                      "key": "骑马",
                      "doc_count": 1
                    }
                  ]
                }
              }
            }
          ]
        }
      }
    }
    

    祖孙三层数据关系建模以及搜索实战

    • 父子关系,祖孙三层关系的数据建模,搜索
    PUT /company
    {
      "mappings": {
        "country": {},
        "rd_center": {
          "_parent": {
            "type": "country" 
          }
        },
        "employee": {
          "_parent": {
            "type": "rd_center" 
          }
        }
      }
    }
    
    • country -> rd_center -> employee,祖孙三层数据模型
    POST /company/country/_bulk
    { "index": { "_id": "1" }}
    { "name": "中国" }
    { "index": { "_id": "2" }}
    { "name": "美国" }
    
    POST /company/rd_center/_bulk
    { "index": { "_id": "1", "parent": "1" }}
    { "name": "北京研发总部" }
    { "index": { "_id": "2", "parent": "1" }}
    { "name": "上海研发中心" }
    { "index": { "_id": "3", "parent": "2" }}
    { "name": "硅谷人工智能实验室" }
    
    PUT /company/employee/1?parent=1&routing=1
    {
      "name":  "张三",
      "dob":   "1970-10-24",
      "hobby": "爬山"
    }
    
    • routing参数的讲解,必须跟grandparent相同,否则有问题

    • country,用的是自己的id去路由; rd_center,parent,用的是country的id去路由; employee,如果也是仅仅指定一个parent,那么用的是rd_center的id去路由,这就导致祖孙三层数据不会在一个shard上

    • 孙子辈儿,要手动指定routing,指定为爷爷辈儿的数据的id

    • 搜索有爬山爱好的员工所在的国家

    GET /company/country/_search
    {
      "query": {
        "has_child": {
          "type": "rd_center",
          "query": {
            "has_child": {
              "type": "employee",
              "query": {
                "match": {
                  "hobby": "爬山"
                }
              }
            }
          }
        }
      }
    }
    
    {
      "took": 10,
      "timed_out": false,
      "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
      },
      "hits": {
        "total": 1,
        "max_score": 1,
        "hits": [
          {
            "_index": "company",
            "_type": "country",
            "_id": "1",
            "_score": 1,
            "_source": {
              "name": "中国"
            }
          }
        ]
      }
    }
    

    基于term vector深入探查数据的情况

    1. term vector介绍
    • 获取document中的某个field内的各个term的统计信息

    • term information: term frequency in the field, term positions, start and end offsets, term payloads

    • term statistics: 设置term_statistics=true; total term frequency, 一个term在所有document中出现的频率; document frequency,有多少document包含这个term

    • field statistics: document count,有多少document包含这个field; sum of document frequency,一个field中所有term的df之和; sum of total term frequency,一个field中的所有term的tf之和

    GET /twitter/tweet/1/_termvectors
    GET /twitter/tweet/1/_termvectors?fields=text
    
    • term statistics和field statistics并不精准,不会被考虑有的doc可能被删除了

    我告诉大家,其实很少用,用的时候,一般来说,就是你需要对一些数据做探查的时候。比如说,你想要看到某个term,某个词条,大话西游,这个词条,在多少个document中出现了。或者说某个field,film_desc,电影的说明信息,有多少个doc包含了这个说明信息。

    1. index-iime term vector实验
    • term vector,涉及了很多的term和field相关的统计信息,有两种方式可以采集到这个统计信息
    1. index-time,你在mapping里配置一下,然后建立索引的时候,就直接给你生成这些term和field的统计信息了
    2. query-time,你之前没有生成过任何的Term vector信息,然后在查看term vector的时候,直接就可以看到了,会on the fly,现场计算出各种统计信息,然后返回给你

    建立索引

    PUT /my_index
    {
      "mappings": {
        "my_type": {
          "properties": {
            "text": {
                "type": "text",
                "term_vector": "with_positions_offsets_payloads",
                "store" : true,
                "analyzer" : "fulltext_analyzer"
             },
             "fullname": {
                "type": "text",
                "analyzer" : "fulltext_analyzer"
            }
          }
        }
      },
      "settings" : {
        "index" : {
          "number_of_shards" : 1,
          "number_of_replicas" : 0
        },
        "analysis": {
          "analyzer": {
            "fulltext_analyzer": {
              "type": "custom",
              "tokenizer": "whitespace",
              "filter": [
                "lowercase",
                "type_as_payload"
              ]
            }
          }
        }
      }
    }
    
    

    插入数据

    PUT /my_index/my_type/1
    {
      "fullname" : "Leo Li",
      "text" : "hello test test test "
    }
    
    PUT /my_index/my_type/2
    {
      "fullname" : "Leo Li",
      "text" : "other hello test ..."
    }
    
    GET /my_index/my_type/1/_termvectors
    {
      "fields" : ["text"],
      "offsets" : true,
      "payloads" : true,
      "positions" : true,
      "term_statistics" : true,
      "field_statistics" : true
    }
    
    {
      "_index": "my_index",
      "_type": "my_type",
      "_id": "1",
      "_version": 1,
      "found": true,
      "took": 10,
      "term_vectors": {
        "text": {
          "field_statistics": {
            "sum_doc_freq": 6,
            "doc_count": 2,  //text这个field存在在多少个docment里面
            "sum_ttf": 8
          },
          "terms": {
            "hello": {
              "doc_freq": 2, //多少个doc包含这个term
              "ttf": 2, 一个term再所有doc里面出现的频率
              "term_freq": 1, //
              "tokens": [ //出现每次叫一个token
                {
                  "position": 0,
                  "start_offset": 0,
                  "end_offset": 5,
                  "payload": "d29yZA=="
                }
              ]
            },
            "test": {
              "doc_freq": 2, 
              "ttf": 4,
              "term_freq": 3,
              "tokens": [
                {
                  "position": 1,
                  "start_offset": 6,
                  "end_offset": 10,
                  "payload": "d29yZA=="
                },
                {
                  "position": 2,
                  "start_offset": 11,
                  "end_offset": 15,
                  "payload": "d29yZA=="
                },
                {
                  "position": 3,
                  "start_offset": 16,
                  "end_offset": 20,
                  "payload": "d29yZA=="
                }
              ]
            }
          }
        }
      }
    }
    

    3。 query-time term vector实验

    GET /my_index/my_type/1/_termvectors
    {
      "fields" : ["fullname"],
      "offsets" : true,
      "positions" : true,
      "term_statistics" : true,
      "field_statistics" : true
    }
    
    • 一般来说,如果条件允许,你就用query time的term vector就可以了,你要探查什么数据,现场去探查一下就好了
    1. 手动指定doc的term vector
    GET /my_index/my_type/_termvectors
    {
      "doc" : {
        "fullname" : "Leo Li",
        "text" : "hello test test test"
      },
      "fields" : ["text"],
      "offsets" : true,
      "payloads" : true,
      "positions" : true,
      "term_statistics" : true,
      "field_statistics" : true
    }
    
    • 手动指定一个doc,实际上不是要指定doc,而是要指定你想要安插的词条,hello test,那么就可以放在一个field中

    • 将这些term分词,然后对每个term,都去计算它在现有的所有doc中的一些统计信息

    • 这个挺有用的,可以让你手动指定要探查的term的数据情况,你就可以指定探查“大话西游”这个词条的统计信息

    1. 手动指定analyzer来生成term vector
    GET /my_index/my_type/_termvectors
    {
      "doc" : {
        "fullname" : "Leo Li",
        "text" : "hello test test test"
      },
      "fields" : ["text"],
      "offsets" : true,
      "payloads" : true,
      "positions" : true,
      "term_statistics" : true,
      "field_statistics" : true,
      "per_field_analyzer" : {
        "text": "standard"
      }
    }
    
    1. terms filter
    GET /my_index/my_type/_termvectors
    {
      "doc" : {
        "fullname" : "Leo Li",
        "text" : "hello test test test"
      },
      "fields" : ["text"],
      "offsets" : true,
      "payloads" : true,
      "positions" : true,
      "term_statistics" : true,
      "field_statistics" : true,
      "filter" : {
          "max_num_terms" : 3,
          "min_term_freq" : 1,
          "min_doc_freq" : 1
        }
    }
    
    • 这个就是说,根据term统计信息,过滤出你想要看到的term vector统计结果
    • 也挺有用的,比如你探查数据把,可以过滤掉一些出现频率过低的term,就不考虑了
    1. multi term vector
    GET _mtermvectors
    {
       "docs": [
          {
             "_index": "my_index",
             "_type": "my_type",
             "_id": "2",
             "term_statistics": true
          },
          {
             "_index": "my_index",
             "_type": "my_type",
             "_id": "1",
             "fields": [
                "text"
             ]
          }
       ]
    }
    
    GET /my_index/_mtermvectors
    {
       "docs": [
          {
             "_type": "test",
             "_id": "2",
             "fields": [
                "text"
             ],
             "term_statistics": true
          },
          {
             "_type": "test",
             "_id": "1"
          }
       ]
    }
    
    GET /my_index/my_type/_mtermvectors
    {
       "docs": [
          {
             "_id": "2",
             "fields": [
                "text"
             ],
             "term_statistics": true
          },
          {
             "_id": "1"
          }
       ]
    }
    
    GET /_mtermvectors
    {
       "docs": [
          {
             "_index": "my_index",
             "_type": "my_type",
             "doc" : {
                "fullname" : "Leo Li",
                "text" : "hello test test test"
             }
          },
          {
             "_index": "my_index",
             "_type": "my_type",
             "doc" : {
               "fullname" : "Leo Li",
               "text" : "other hello test ..."
             }
          }
       ]
    }
    
    

    深入剖析搜索结果的highlight高亮显示

    课程大纲

    1. 一个最基本的高亮例子
    PUT /blog_website
    {
      "mappings": {
        "blogs": {
          "properties": {
            "title": {
              "type": "text",
              "analyzer": "ik_max_word"
            },
            "content": {
              "type": "text",
              "analyzer": "ik_max_word"
            }
          }
        }
      }
    }
    
    PUT /blog_website/blogs/1
    {
      "title": "我的第一篇博客",
      "content": "大家好,这是我写的第一篇博客,特别喜欢这个博客网站!!!"
    }
    
    GET /blog_website/blogs/_search 
    {
      "query": {
        "match": {
          "title": "博客"
        }
      },
      "highlight": {
        "fields": {
          "title": {}
        }
      }
    }
    
    {
      "took": 103,
      "timed_out": false,
      "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
      },
      "hits": {
        "total": 1,
        "max_score": 0.28582606,
        "hits": [
          {
            "_index": "blog_website",
            "_type": "blogs",
            "_id": "1",
            "_score": 0.28582606,
            "_source": {
              "title": "我的第一篇博客",
              "content": "大家好,这是我写的第一篇博客,特别喜欢这个博客网站!!!"
            },
            "highlight": {
              "title": [
                "我的第一篇<em>博客</em>"
              ]
            }
          }
        ]
      }
    }
    
    • 标签会变成红色,所以说你的指定的field中,如果包含了那个搜索词的话,就会在那个field的文本中,对搜索词进行红色的高亮显示
    GET /blog_website/blogs/_search 
    {
      "query": {
        "bool": {
          "should": [
            {
              "match": {
                "title": "博客"
              }
            },
            {
              "match": {
                "content": "博客"
              }
            }
          ]
        }
      },
      "highlight": {
        "fields": {
          "title": {},
          "content": {}
        }
      }
    }
    
    • highlight中的field,必须跟query中的field一一对齐的
    1. 三种highlight介绍
    • 第一种:plain highlight,lucene highlight,默认

    • 第二种: posting highlight要启用的话需要在index的feild设置index_options=offsets

    1. 性能比plain highlight要高,因为不需要重新对高亮文本进行分词
    2. 对磁盘的消耗更少
    3. 将文本切割为句子,并且对句子进行高亮,效果更好
    PUT /blog_website
    {
      "mappings": {
        "blogs": {
          "properties": {
            "title": {
              "type": "text",
              "analyzer": "ik_max_word"
            },
            "content": {
              "type": "text",
              "analyzer": "ik_max_word",
              "index_options": "offsets"
            }
          }
        }
      }
    }
    
    PUT /blog_website/blogs/1
    {
      "title": "我的第一篇博客",
      "content": "大家好,这是我写的第一篇博客,特别喜欢这个博客网站!!!"
    }
    
    GET /blog_website/blogs/_search 
    {
      "query": {
        "match": {
          "content": "博客"
        }
      },
      "highlight": {
        "fields": {
          "content": {}
        }
      }
    }
    
    • 第三种 fast vector highlight

    • index-time term vector设置在mapping中,就会用fast verctor highlight

    1. 对大field而言(大于1mb),性能更高
    PUT /blog_website
    {
      "mappings": {
        "blogs": {
          "properties": {
            "title": {
              "type": "text",
              "analyzer": "ik_max_word"
            },
            "content": {
              "type": "text",
              "analyzer": "ik_max_word",
              "term_vector" : "with_positions_offsets"
            }
          }
        }
      }
    }
    
    • 强制使用某种highlighter,比如对于开启了term vector的field而言,可以强制使用plain highlight
    GET /blog_website/blogs/_search 
    {
      "query": {
        "match": {
          "content": "博客"
        }
      },
      "highlight": {
        "fields": {
          "content": {
            "type": "plain"
          }
        }
      }
    }
    

    总结一下,其实可以根据你的实际情况去考虑,一般情况下,用plain highlight也就足够了,不需要做其他额外的设置
    如果对高亮的性能要求很高,可以尝试启用posting highlight
    如果field的值特别大,超过了1M,那么可以用fast vector highlight

    1. 设置高亮html标签,默认是标签
    GET /blog_website/blogs/_search 
    {
      "query": {
        "match": {
          "content": "博客"
        }
      },
      "highlight": {
        "pre_tags": ["<tag1>"],
        "post_tags": ["</tag2>"], 
        "fields": {
          "content": {
            "type": "plain"
          }
        }
      }
    }
    

    4。 高亮片段fragment的设置

    GET /_search
    {
        "query" : {
            "match": { "user": "kimchy" }
        },
        "highlight" : {
            "fields" : {
                "content" : {"fragment_size" : 150, "number_of_fragments" : 3, "no_match_size": 150 }
            }
        }
    }
    
    • fragment_size: N个字符拆分成一个fagment

    • number_of_fragments:你可能你的高亮的fragment文本片段有多个片段,你可以指定就显示几个片段

    使用search template将搜索模板化

    课程大纲

    搜索模板,search template,高级功能,就可以将我们的一些搜索进行模板化,然后的话,每次执行这个搜索,就直接调用模板,给传入一些参数就可以了

    越高级的功能,越少使用,可能只有在你真的遇到特别合适的场景的时候,才会去使用某个高级功能。但是,这些高级功能你是否掌握,其实就是普通的es开发人员,和es高手之间的一个区别。高手,一般来说,会把一个技术掌握的特别好,特别全面,特别深入,也许他平时用不到这个技术,但是当真的遇到一定的场景的时候,高手可以基于自己的深厚的技术储备,立即反应过来,找到一个合适的解决方案。

    如果是一个普通的技术人员,一般只会学习跟自己当前工作相关的一些知识和技术,只要求自己掌握的技术可以解决工作目前遇到的问题就可以了,就满足了,就会止步不前了,然后就不会更加深入的去学习一个技术。但是,当你的项目真正遇到问题的时候,遇到了一些难题,你之前的那一点技术储备已经没法去应付这些更加困难的问题了,此时,普通的技术人员就会扎耳挠腮,没有任何办法。

    高手,对技术是很有追求,能够精通很多自己遇到过的技术,但是也许自己学的很多东西,自己都没用过,但是不要紧,这是你的一种技术储备。

    1、search template入门

    GET /blog_website/blogs/_search/template
    {
    "inline" : {
    "query": {
    "match" : {
    "{{field}}" : "{{value}}"
    }
    }
    },
    "params" : {
    "field" : "title",
    "value" : "博客"
    }
    }

    GET /blog_website/blogs/_search
    {
    "query": {
    "match" : {
    "title" : "博客"
    }
    }
    }

    search template:"{{field}}" : "{{value}}"

    2、toJson

    GET /blog_website/blogs/_search/template
    {
    "inline": "{"query": {"match": {{#toJson}}matchCondition{{/toJson}}}}",
    "params": {
    "matchCondition": {
    "title": "博客"
    }
    }
    }

    GET /blog_website/blogs/_search
    {
    "query": {
    "match" : {
    "title" : "博客"
    }
    }
    }

    3、join

    GET /blog_website/blogs/_search/template
    {
    "inline": {
    "query": {
    "match": {
    "title": "{{#join delimiter=' '}}titles{{/join delimiter=' '}}"
    }
    }
    },
    "params": {
    "titles": ["博客", "网站"]
    }
    }

    博客,网站

    GET /blog_website/blogs/_search
    {
    "query": {
    "match" : {
    "title" : "博客 网站"
    }
    }
    }

    4、default value

    POST /blog_website/blogs/1/_update
    {
    "doc": {
    "views": 5
    }
    }

    GET /blog_website/blogs/_search/template
    {
    "inline": {
    "query": {
    "range": {
    "views": {
    "gte": "{{start}}",
    "lte": "{{end}}{{^end}}20{{/end}}"
    }
    }
    }
    },
    "params": {
    "start": 1,
    "end": 10
    }
    }

    GET /blog_website/blogs/_search
    {
    "query": {
    "range": {
    "views": {
    "gte": 1,
    "lte": 10
    }
    }
    }
    }

    GET /blog_website/blogs/_search/template
    {
    "inline": {
    "query": {
    "range": {
    "views": {
    "gte": "{{start}}",
    "lte": "{{end}}{{^end}}20{{/end}}"
    }
    }
    }
    },
    "params": {
    "start": 1
    }
    }

    GET /blog_website/blogs/_search
    {
    "query": {
    "range": {
    "views": {
    "gte": 1,
    "lte": 20
    }
    }
    }
    }

    5、conditional

    es的config/scripts目录下,预先保存这个复杂的模板,后缀名是.mustache,文件名是conditonal

    {
    "query": {
    "bool": {
    "must": {
    "match": {
    "line": "{{text}}"
    }
    },
    "filter": {
    {{#line_no}}
    "range": {
    "line_no": {
    {{#start}}
    "gte": "{{start}}"
    {{#end}},{{/end}}
    {{/start}}
    {{#end}}
    "lte": "{{end}}"
    {{/end}}
    }
    }
    {{/line_no}}
    }
    }
    }
    }

    GET /my_index/my_type/_search

    {
    "took": 4,
    "timed_out": false,
    "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
    },
    "hits": {
    "total": 1,
    "max_score": 1,
    "hits": [
    {
    "_index": "my_index",
    "_type": "my_type",
    "_id": "1",
    "_score": 1,
    "_source": {
    "line": "我的博客",
    "line_no": 5
    }
    }
    ]
    }
    }

    GET /my_index/my_type/_search/template
    {
    "file": "conditional",
    "params": {
    "text": "博客",
    "line_no": true,
    "start": 1,
    "end": 10
    }
    }

    6、保存search template

    config/scripts,.mustache

    提供一个思路

    比如说,一般在大型的团队中,可能不同的人,都会想要执行一些类似的搜索操作
    这个时候,有一些负责底层运维的一些同学,就可以基于search template,封装一些模板出来,然后是放在各个es进程的scripts目录下的
    其他的团队,其实就不用各个团队自己反复手写复杂的通用的查询语句了,直接调用某个搜索模板,传入一些参数就好了

    使用search template将搜索模板化

    • 搜索模板,search template,高级功能,就可以将我们的一些搜索进行模板化,然后的话,每次执行这个搜索,就直接调用模板,给传入一些参数就可以了

    • 越高级的功能,越少使用,可能只有在你真的遇到特别合适的场景的时候,才会去使用某个高级功能。但是,这些高级功能你是否掌握,其实就是普通的es开发人员,和es高手之间的一个区别。高手,一般来说,会把一个技术掌握的特别好,特别全面,特别深入,也许他平时用不到这个技术,但是当真的遇到一定的场景的时候,高手可以基于自己的深厚的技术储备,立即反应过来,找到一个合适的解决方案。

    • 如果是一个普通的技术人员,一般只会学习跟自己当前工作相关的一些知识和技术,只要求自己掌握的技术可以解决工作目前遇到的问题就可以了,就满足了,就会止步不前了,然后就不会更加深入的去学习一个技术。但是,当你的项目真正遇到问题的时候,遇到了一些难题,你之前的那一点技术储备已经没法去应付这些更加困难的问题了,此时,普通的技术人员就会扎耳挠腮,没有任何办法。

    • 高手,对技术是很有追求,能够精通很多自己遇到过的技术,但是也许自己学的很多东西,自己都没用过,但是不要紧,这是你的一种技术储备。

    1. search template入门
    GET /blog_website/blogs/_search/template
    {
      "inline" : {
        "query": { 
          "match" : { 
            "{{field}}" : "{{value}}" 
          } 
        }
      },
      "params" : {
          "field" : "title",
          "value" : "博客"
      }
    }
    

    底层会翻译成

    GET /blog_website/blogs/_search
    {
      "query": { 
        "match" : { 
          "title" : "博客" 
        } 
      }
    }
    
    • search template:"{{field}}" : "{{value}}" 是一段模板
    1. toJson,传入整段JSON作为参数
    GET /blog_website/blogs/_search/template
    {
      "inline": "{"query": {"match": {{#toJson}}matchCondition{{/toJson}}}}", //替换json变量
      "params": {
        "matchCondition": {
          "title": "博客"
        }
      }
    }
    
    GET /blog_website/blogs/_search
    {
      "query": { 
        "match" : { 
          "title" : "博客" 
        } 
      }
    }
    
    1. join,数组转字符串
    GET /blog_website/blogs/_search/template
    {
      "inline": {
        "query": {
          "match": {
            "title": "{{#join delimiter=' '}}titles{{/join delimiter=' '}}"
          }
        }
      },
      "params": {
        "titles": ["博客", "网站"]
      }
    }
    
    • 数组会转成 博客,网站
    GET /blog_website/blogs/_search
    {
      "query": { 
        "match" : { 
          "title" : "博客 网站" 
        } 
      }
    }
    
    1. default value

    更新数据

    POST /blog_website/blogs/1/_update
    {
      "doc": {
        "views": 5
      }
    }
    
    GET /blog_website/blogs/_search/template
    {
      "inline": {
        "query": {
          "range": {
            "views": {
              "gte": "{{start}}",
              "lte": "{{end}}{{^end}}20{{/end}}"  //如果指定了end就用end否则用默认值
            }
          }
        }
      },
      "params": {
        "start": 1,
        "end": 10
      }
    }
    
    GET /blog_website/blogs/_search
    {
      "query": {
        "range": {
          "views": {
            "gte": 1,
            "lte": 10
          }
        }
      }
    }
    
    GET /blog_website/blogs/_search/template
    {
      "inline": {
        "query": {
          "range": {
            "views": {
              "gte": "{{start}}",
              "lte": "{{end}}{{^end}}20{{/end}}"
            }
          }
        }
      },
      "params": {
        "start": 1
      }
    }
    
    GET /blog_website/blogs/_search
    {
      "query": {
        "range": {
          "views": {
            "gte": 1,
            "lte": 20
          }
        }
      }
    }
    
    1. conditional
    • es的config/scripts目录下,预先保存这个复杂的模板,后缀名是.mustache,文件名是conditonal
    {
      "query": {
        "bool": {
          "must": {
            "match": {
              "line": "{{text}}" 
            }
          },
          "filter": {
            {{#line_no}}  //如果指定了这个值才会执行下面操作
              "range": {
                "line_no": {
                  {{#start}} 
                    "gte": "{{start}}" 
                    {{#end}},{{/end}} 
                  {{/start}} 
                  {{#end}} 
                    "lte": "{{end}}" 
                  {{/end}} 
                }
              }
            {{/line_no}} 
          }
        }
      }
    }
    
    GET /my_index/my_type/_search 
    
    {
      "took": 4,
      "timed_out": false,
      "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
      },
      "hits": {
        "total": 1,
        "max_score": 1,
        "hits": [
          {
            "_index": "my_index",
            "_type": "my_type",
            "_id": "1",
            "_score": 1,
            "_source": {
              "line": "我的博客",
              "line_no": 5
            }
          }
        ]
      }
    }
    
    GET /my_index/my_type/_search/template
    {
      "file": "conditional",
      "params": {
        "text": "博客",
        "line_no": true,
        "start": 1,
        "end": 10
      }
    }
    
    1. 保存search template
    • config/scripts,.mustache

    提供一个思路:比如说,一般在大型的团队中,可能不同的人,都会想要执行一些类似的搜索操作
    这个时候,有一些负责底层运维的一些同学,就可以基于search template,封装一些模板出来,然后是放在各个es进程的scripts目录下的
    其他的团队,其实就不用各个团队自己反复手写复杂的通用的查询语句了,直接调用某个搜索模板,传入一些参数就好了

    基于completion suggest实现搜索提示

    PUT /news_website
    {
      "mappings": {
        "news" : {
          "properties" : {
            "title" : {
              "type": "text",
              "analyzer": "ik_max_word",
              "fields": {
                "suggest" : {
                  "type" : "completion",
                  "analyzer": "ik_max_word"
                }
              }
            },
            "content": {
              "type": "text",
              "analyzer": "ik_max_word"
            }
          }
        }
      }
    }
    

    completion,es实现的时候,是非常高性能的,会构建不是倒排索引,也不是正拍索引,就是纯的用于进行前缀搜索的一种特殊的数据结构,而且会全部放在内存中,所以auto completion进行的前缀搜索提示,性能是非常高的

    大话西游

    PUT /news_website/news/1
    {
      "title": "大话西游电影",
      "content": "大话西游的电影时隔20年即将在2017年4月重映"
    }
    PUT /news_website/news/2
    {
      "title": "大话西游小说",
      "content": "某知名网络小说作家已经完成了大话西游同名小说的出版"
    }
    PUT /news_website/news/3
    {
      "title": "大话西游手游",
      "content": "网易游戏近日出品了大话西游经典IP的手游,正在火爆内测中"
    }
    
    GET /news_website/news/_search
    {
      "suggest": {
        "my-suggest" : {
          "prefix" : "大话西游",
          "completion" : {
            "field" : "title.suggest"
          }
        }
      }
    }
    
    {
      "took": 6,
      "timed_out": false,
      "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
      },
      "hits": {
        "total": 0,
        "max_score": 0,
        "hits": []
      },
      "suggest": {
        "my-suggest": [
          {
            "text": "大话西游",
            "offset": 0,
            "length": 4,
            "options": [
              {
                "text": "大话西游小说",
                "_index": "news_website",
                "_type": "news",
                "_id": "2",
                "_score": 1,
                "_source": {
                  "title": "大话西游小说",
                  "content": "某知名网络小说作家已经完成了大话西游同名小说的出版"
                }
              },
              {
                "text": "大话西游手游",
                "_index": "news_website",
                "_type": "news",
                "_id": "3",
                "_score": 1,
                "_source": {
                  "title": "大话西游手游",
                  "content": "网易游戏近日出品了大话西游经典IP的手游,正在火爆内测中"
                }
              },
              {
                "text": "大话西游电影",
                "_index": "news_website",
                "_type": "news",
                "_id": "1",
                "_score": 1,
                "_source": {
                  "title": "大话西游电影",
                  "content": "大话西游的电影时隔20年即将在2017年4月重映"
                }
              }
            ]
          }
        ]
      }
    }
    
    GET /news_website/news/_search 
    {
      "query": {
        "match": {
          "content": "大话西游电影"
        }
      }
    }
    
    {
      "took": 9,
      "timed_out": false,
      "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
      },
      "hits": {
        "total": 3,
        "max_score": 1.3495269,
        "hits": [
          {
            "_index": "news_website",
            "_type": "news",
            "_id": "1",
            "_score": 1.3495269,
            "_source": {
              "title": "大话西游电影",
              "content": "大话西游的电影时隔20年即将在2017年4月重映"
            }
          },
          {
            "_index": "news_website",
            "_type": "news",
            "_id": "3",
            "_score": 1.217097,
            "_source": {
              "title": "大话西游手游",
              "content": "网易游戏近日出品了大话西游经典IP的手游,正在火爆内测中"
            }
          },
          {
            "_index": "news_website",
            "_type": "news",
            "_id": "2",
            "_score": 1.1299736,
            "_source": {
              "title": "大话西游小说",
              "content": "某知名网络小说作家已经完成了大话西游同名小说的出版"
            }
          }
        ]
      }
    }
    

    使用动态映射模板定制自己的映射策略

    比如说,我们本来没有某个type,或者没有某个field,但是希望在插入数据的时候,es自动为我们做一个识别,动态映射出这个type的mapping,包括每个field的数据类型,一般用的动态映射,dynamic mapping。这里有个问题,如果说,我们其实对dynamic mapping有一些自己独特的需求,比如说,es默认来说,如经过识别到一个数字,field: 10,默认是搞成这个field的数据类型是long,再比如说,如果我们弄了一个field : "10",默认就是text,还会带一个keyword的内置field。我们没法改变。但是我们现在就是希望动态映射的时候,根据我们的需求去映射,而不是让es自己按照默认的规则去玩儿

    dyanmic mapping template,动态映射模板

    》 我们自己预先定义一个模板,然后插入数据的时候,相关的field,如果能够根据我们预先定义的规则,匹配上某个我们预定义的模板,那么就会根据我们的模板来进行mapping,决定这个Field的数据类型

    1. 默认的动态映射的效果咋样
    DELETE /my_index
    
    PUT /my_index/my_type/1
    {
      "test_string": "hello world",
      "test_number": 10
    }
    

    es的自动的默认的,动态映射

    GET /my_index/_mapping/my_type
    
    {
      "my_index": {
        "mappings": {
          "my_type": {
            "properties": {
              "test_number": {
                "type": "long"
              },
              "test_string": {
                "type": "text",
                "fields": {
                  "keyword": {
                    "type": "keyword",
                    "ignore_above": 256
                  }
                }
              }
            }
          }
        }
      }
    }
    
    • 我们比如希望,test_number,如果是个数字,我们希望默认就是integer类型的
      test_string,如果是字符串,我们希望默认是个text,这个没问题,但是内置的field名字,叫做raw,不叫座keyword,类型还是keyword,保留500个字符
    1. 根据类型匹配映射模板
    • 动态映射模板,有两种方式,第一种,是根据新加入的field的默认的数据类型,来进行匹配,匹配上某个预定义的模板;第二种,是根据新加入的field的名字,去匹配预定义的名字,或者去匹配一个预定义的通配符,然后匹配上某个预定义的模板
    PUT my_index
    {
      "mappings": {
        "my_type": {
          "dynamic_templates": [
            {
              "integers": {
                "match_mapping_type": "long", //我的模板要匹配哪种默认类型
                "mapping": {
                  "type": "integer"
                }
              }
            },
            {
              "strings": {
                "match_mapping_type": "string",
                "mapping": {
                  "type": "text",
                  "fields": {
                    "raw": {
                      "type": "keyword",
                      "ignore_above": 500
                    }
                  }
                }
              }
            }
          ]
        }
      }
    }
    
    PUT /my_index/my_type/1
    {
      "test_long": 1,
      "test_string": "hello world"
    }
    
    {
      "my_index": {
        "mappings": {
          "my_type": {
            "dynamic_templates": [
              {
                "integers": {
                  "match_mapping_type": "long",
                  "mapping": {
                    "type": "integer"
                  }
                }
              },
              {
                "strings": {
                  "match_mapping_type": "string",
                  "mapping": {
                    "fields": {
                      "raw": {
                        "ignore_above": 500,
                        "type": "keyword"
                      }
                    },
                    "type": "text"
                  }
                }
              }
            ],
            "properties": {
              "test_number": {
                "type": "integer"
              },
              "test_string": {
                "type": "text",
                "fields": {
                  "raw": {
                    "type": "keyword",
                    "ignore_above": 500
                  }
                }
              }
            }
          }
        }
      }
    
    1. 根据字段名配映射模板
    PUT /my_index 
    {
      "mappings": {
        "my_type": {
          "dynamic_templates": [
            {
              "string_as_integer": {
                "match_mapping_type": "string",
                "match": "long_*",
                "unmatch": "*_text",
                "mapping": {
                  "type": "integer"
                }
              }
            }
          ]
        }
      }
    }
    
    • 举个例子,field : "10",把类似这种field,弄成long型
    {
      "my_index": {
        "mappings": {
          "my_type": {
            "dynamic_templates": [
              {
                "string_as_integer": {
                  "match": "long_*",
                  "unmatch": "*_text",
                  "match_mapping_type": "string",
                  "mapping": {
                    "type": "integer"
                  }
                }
              }
            ],
            "properties": {
              "long_field": {
                "type": "integer"
              },
              "long_field_text": {
                "type": "text",
                "fields": {
                  "keyword": {
                    "type": "keyword",
                    "ignore_above": 256
                  }
                }
              }
            }
          }
        }
      }
    }
    

    场景,有些时候,dynamic mapping + template,每天有一堆日志,每天有一堆数据。这些数据,每天的数据都放一个新的type中,每天的数据都会哗哗的往新的tye中写入,此时你就可以定义一个模板,搞一个脚本,每天都预先生成一个新type的模板,里面讲你的各个Field都匹配到一个你预定义的模板中去,就好了

    使用geo point地理位置数据类型

    1. 建立geo_point类型的mapping

    第一个地理位置的数据类型,就是geo_point,geo_point,说白了,就是一个地理位置坐标点,包含了一个经度,一个维度,经纬度,就可以唯一定位一个地球上的坐标

    PUT /my_index 
    {
      "mappings": {
        "my_type": {
          "properties": {
            "location": {
              "type": "geo_point"
            }
          }
        }
      }
    }
    
    1. 写入geo_point的3种方法
    PUT my_index/my_type/1
    {
      "text": "Geo-point as an object",
      "location": { 
        "lat": 41.12,
        "lon": -71.34
      }
    }
    
    • latitude:维度

    • longitude:经度

    • 我们这里就不用去关心,这些坐标到底代表什么地方,其实都是我自己随便写的,只要能够作为课程,给大家演示清楚就可以了,自己去找一些提供地理位置的一些公司,供应商,api,百度地图,也是提供各个地方的经纬度的

    • 不建议用下面两种语法

    PUT my_index/my_type/2
    {
      "text": "Geo-point as a string",
      "location": "41.12,-71.34" 
    }
    
    PUT my_index/my_type/4
    {
      "text": "Geo-point as an array",
      "location": [ -71.34, 41.12 ] 
    }
    
    1. 根据地理位置进行查询
    • 最最简单的,根据地理位置查询一些点,比如说,下面geo_bounding_box查询,查询某个矩形的地理位置范围内的坐标点
    GET /my_index/my_type/_search 
    {
      "query": {
        "geo_bounding_box": {
          "location": {
            "top_left": {  //左上角 
              "lat": 42,
              "lon": -72
            },
            "bottom_right": {  //右下角
              "lat": 40,
              "lon": -74
            }
          }
        }
      }
    }
    
    {
      "took": 81,
      "timed_out": false,
      "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
      },
      "hits": {
        "total": 1,
        "max_score": 1,
        "hits": [
          {
            "_index": "my_index",
            "_type": "my_type",
            "_id": "1",
            "_score": 1,
            "_source": {
              "location": {
                "lat": 41.12,
                "lon": -71.34
              }
            }
          }
        ]
      }
    }
    
    • 比如41.12,-71.34就是一个酒店,然后我们现在搜索的是从42,-72(代表了大厦A)和40,-74(代表了马路B)作为矩形的范围,在这个范围内的酒店,是什么

    酒店o2o搜索案例以及搜索指定区域内的酒店

    • 稍微真实点的案例,酒店o2o app作为一个背景,用各种各样的方式,去搜索你当前所在的地理位置附近的酒店

    • 搜索指定区域范围内的酒店,比如说,我们可以在搜索的时候,指定两个地点,就要在东方明珠大厦和上海路组成的矩阵的范围内,搜索我想要的酒店

    PUT /hotel_app
    {
        "mappings": {
            "hotels": {
                "properties": {
                    "pin": {
                        "properties": {
                            "location": {
                                "type": "geo_point"
                            }
                        }
                    }
                }
            }
        }
    }
    
    PUT /hotel_app/hotels/1
    {
        "name": "喜来登大酒店",
        "pin" : {
            "location" : {
                "lat" : 40.12,
                "lon" : -71.34
            }
        }
    }
    
    GET /hotel_app/hotels/_search
    {
      "query": {
        "bool": {
          "must": [
            {
              "match_all": {}
            }
          ],
          "filter": {
            "geo_bounding_box": {
              "pin.location": {
                "top_left" : {
                    "lat" : 40.73,
                    "lon" : -74.1
                },
                "bottom_right" : {
                    "lat" : 40.01,
                    "lon" : -71.12
                }
              }
            }
          }
        }
      }
    }
    
    GET /hotel_app/hotels/_search 
    {
      "query": {
        "bool": {
          "must": [
            {
              "match_all": {}
            }
          ],
          "filter": {
            "geo_polygon": {
              "pin.location": {
                "points": [
                  {"lat" : 40.73, "lon" : -74.1},
                  {"lat" : 40.01, "lon" : -71.12},
                  {"lat" : 50.56, "lon" : -90.58}
                ]
              }
            }
          }
        }
      }
    }
    
    • 我们现在要指定东方明珠大厦,上海路,上海博物馆,这三个地区组成的多边形的范围内,我要搜索这里面的酒店

    实战搜索距离当前位置一定范围内的酒店

    • 酒店o2o app,作为案例背景

    • 比如说,现在用户,所在的位置,是个地理位置的坐标,我是知道我的坐标的,app是知道的,android,地理位置api,都可以拿到当前手机app的经纬度。我说,我现在就要搜索出,举例我200m,或者1公里内的酒店.上节课讲解的,其实也很重要,一般来说,发生在我们在去旅游之前,会现在旅游app上搜索一个区域内的酒店,比如说,指定了西安火车站、西安博物馆,拿指定的几个地方的地理位置,组成一个多边形区域范围,去搜索这个区域内的酒店

    GET /hotel_app/hotels/_search
    {
      "query": {
        "bool": {
          "must": [
            {
              "match_all": {}
            }
          ],
          "filter": {
            "geo_distance": {
              "distance": "200km",
              "pin.location": {
                "lat": 40,
                "lon": -70
              }
            }
          }
        }
      }
    }
     
    

    统计当前位置每个距离范围内有多少家酒店

    • 最后一个知识点,基于地理位置进行聚合分析

    • 我的需求就是,统计一下,举例我当前坐标的几个范围内的酒店的数量,比如说举例我0100m有几个酒店,100m300m有几个酒店,300m以上有几个酒店

    • 一般来说,做酒店app,一般来说,我们是不是会有一个地图,用户可以在地图上直接查看和搜索酒店,此时就可以显示出来举例你当前的位置,几个举例范围内,有多少家酒店,让用户知道,心里清楚,用户体验就比较好

    GET /hotel_app/hotels/_search
    {
      "size": 0,
      "aggs": {
        "agg_by_distance_range": {
          "geo_distance": {
            "field": "pin.location",
            "origin": {
              "lat": 40,
              "lon": -70
            },
            "unit": "mi", 
            "ranges": [
              {
                "to": 100
              },
              {
                "from": 100,
                "to": 300
              },
              {
                "from": 300
              }
            ]
          }
        }
      }
    }
    
    {
      "took": 5,
      "timed_out": false,
      "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
      },
      "hits": {
        "total": 1,
        "max_score": 0,
        "hits": []
      },
      "aggregations": {
        "agg_by_distance_range": {
          "buckets": [
            {
              "key": "*-100.0",
              "from": 0,
              "to": 100,
              "doc_count": 1
            },
            {
              "key": "100.0-300.0",
              "from": 100,
              "to": 300,
              "doc_count": 0
            },
            {
              "key": "300.0-*",
              "from": 300,
              "doc_count": 0
            }
          ]
        }
      }
    }
    
    • m (metres) but it can also accept: m (miles), km (kilometers)

    • sloppy_arc (the default), arc (most accurate) and plane (fastest)

    Java API_client集群自动探查

    1. client集群自动探查
    • 默认情况下,是根据我们手动指定的所有节点,依次轮询这些节点,来发送各种请求的,如下面的代码,我们可以手动为client指定多个节点
    TransportClient client = new PreBuiltTransportClient(settings)
    				.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("localhost1"), 9300))
    				.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("localhost2"), 9300))
    				.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("localhost3"), 9300));
    
    
    • 但是问题是,如果我们有成百上千个节点呢?难道也要这样手动添加吗?

    • es client提供了一种集群节点自动探查的功能,打开这个自动探查机制以后,es client会根据我们手动指定的几个节点连接过去,然后通过集群状态自动获取当前集群中的所有data node,然后用这份完整的列表更新自己内部要发送请求的node list。默认每隔5秒钟,就会更新一次node list。

    • 但是注意,es cilent是不会将Master node纳入node list的,因为要避免给master node发送搜索等请求。

    • 这样的话,我们其实直接就指定几个master node,或者1个node就好了,client会自动去探查集群的所有节点,而且每隔5秒还会自动刷新。非常棒。

    -建议用这种方式

    Settings settings = Settings.builder()
            .put("client.transport.sniff", true).build();
    TransportClient client = new PreBuiltTransportClient(settings);
    
    • 使用上述的settings配置,将client.transport.sniff设置为true即可打开集群节点自动探查功能
  • 相关阅读:
    70.BOM
    69.捕获错误try catch
    68.键盘事件
    523. Continuous Subarray Sum
    901. Online Stock Span
    547. Friend Circles
    162. Find Peak Element
    1008. Construct Binary Search Tree from Preorder Traversal
    889. Construct Binary Tree from Preorder and Postorder Traversal
    106. Construct Binary Tree from Inorder and Postorder Traversal
  • 原文地址:https://www.cnblogs.com/jaychan/p/14984068.html
Copyright © 2011-2022 走看看