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即可打开集群节点自动探查功能
  • 相关阅读:
    iOS 改变同一个label中多行文字间的距离
    iOS改变UITableViewCell的分割线frame和颜色
    iOS动画实现改变frme和contenOffset
    iOS判断数组不为空
    jQueryMobile控件之ListView
    jQueryMobile控件之展开与合并
    jQueryMobile控件之页面切换
    jQueryMobile控件之按钮
    jQueryMobile控件之复选框
    jQueryUI 之控件们
  • 原文地址:https://www.cnblogs.com/jaychan/p/14984068.html
Copyright © 2011-2022 走看看