ElasticSearch学习(二):Elasticsearch 查询和聚合

4,660 阅读22分钟

1. Elasticsearch之Search API介绍

1. SearchAPI概览

  • 实现了对es中存储的数据进行查询分析,endpoint为_search
  • 查询主要有两种形式
    • URI Search
      • 操作简便,方便通过命令行测试
      • 仅包含部分查询语法
    • Request Body Search
      • es提供的完备查询语法Query DSL(Domain Specific Language)

2. URI Search详解与演示

  • 通过url query参数来实现搜索,常用参数如下:
    • q 指定查询的语句,语法为Query String Syntax
    • df q中不指定字段时默认查询的字段,如果不指定,es会查询所有字段
    • sort 排序
    • timeout 指定超时时间,默认不超时
    • form,size 用于分页

Query String Syntax

  • term与phrase
    • alfred way等效于alfred OR way
    • "alfred way"词语查询,要求先后顺序
  • 泛查询
    • alfred等效于在所有字段去匹配该term
  • 指定字段
    • name:alfred
  • Group分组设定,使用括号指定匹配的规则
    • (quick OR brown) AND fox
    • status:(active OR pending) title:(full text search)
PUT test_search_index
{
  "settings": {
    "index":{
        "number_of_shards": "1"
    }
  }
}

POST test_search_index/doc/_bulk
{"index":{"_id":"1"}}
{"username":"alfred way","job":"java engineer","age":18,"birth":"1990-01-02","isMarried":false}
{"index":{"_id":"2"}}
{"username":"alfred","job":"java senior engineer and java specialist","age":28,"birth":"1980-05-07","isMarried":true}
{"index":{"_id":"3"}}
{"username":"lee","job":"java and ruby engineer","age":22,"birth":"1985-08-07","isMarried":false}
{"index":{"_id":"4"}}
{"username":"alfred junior way","job":"ruby engineer","age":23,"birth":"1989-08-07","isMarried":false}
  • 接下来我们就来做实际的查询了,首先做个泛查询,查询含义是所有字段包含alfred的文档
GET test_search_index/_search?q=alfred

{
  "took" : 29,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : 3,
    "max_score" : 1.2039728,
    "hits" : [
      {
        "_index" : "test_search_index",
        "_type" : "doc",
        "_id" : "2",
        "_score" : 1.2039728,
        "_source" : {
          "username" : "alfred",
          "job" : "java senior engineer and java specialist",
          "age" : 28,
          "birth" : "1980-05-07",
          "isMarried" : true
        }
      },
      {
        "_index" : "test_search_index",
        "_type" : "doc",
        "_id" : "1",
        "_score" : 0.33698124,
        "_source" : {
          "username" : "alfred way",
          "job" : "java engineer",
          "age" : 18,
          "birth" : "1990-01-02",
          "isMarried" : false
        }
      },
      {
        "_index" : "test_search_index",
        "_type" : "doc",
        "_id" : "4",
        "_score" : 0.27601978,
        "_source" : {
          "username" : "alfred junior way",
          "job" : "ruby engineer",
          "age" : 23,
          "birth" : "1989-08-07",
          "isMarried" : false
        }
      }
    ]
  }
}
  • 我们看下es到底是怎样执行查询条件的
GET test_search_index/_search?q=alfred
{
  "profile":true
}
  • 按照字段查询
GET test_search_index/_search?q=username:alfred
  • 满足任何一个条件就可以了
GET test_search_index/_search?q=username:alfred way
{
  "profile":true
}
  • 现在有两种改变的方式
GET test_search_index/_search?q=username:"alfred way"
{
  "profile":true
}

GET test_search_index/_search?q=username:(alfred way)
{
  "profile":true
}
  • 布尔操作符
    • AND(&&),OR(||),NOT(!)
    • name:(tom NOT lee)
    • 注意大写,不能小写
  • + - 分别对应must和must_not
    • name:(tom +lee -alfred)
    • +在url中会被解析为空格,要使用encode后的结果才可以,为%2B
GET test_search_index/_search?q=username:alfred AND way
{
  "profile":true
}

GET test_search_index/_search?q=username:(alfred AND way)
{
  "profile":true
}

GET test_search_index/_search?q=username:(alfred NOT way)

GET test_search_index/_search?q=username:(alfred +way)
{
  "profile":true
}

GET test_search_index/_search?q=username:(alfred %2Bway)
{
  "profile":true
}
  • 范围查询,支持数值和日期
    • 区间写法,闭区间用[],开区间用{}
    • 算数符号写法
GET test_search_index/_search?q=username:alfred age:>26

GET test_search_index/_search?q=username:alfred AND age:>20

GET test_search_index/_search?q=birth:(>1980 AND <1990)
  • 通配符查询
    • ?代表一个字符,*代表0或多个字符
    • 通配符匹配执行效率低,且占用较多内存,不建议使用
    • 如无特殊需求,不要将?/*放在最前面
GET test_search_index/_search?q=username:alf*
  • 正则表达式匹配
GET test_search_index/_search?q=username:/[a]?l.*/
  • 模糊匹配fuzzy query
    • name:roam~1
    • 匹配与roam差1个character的词,比如foam roams等
  • 近似度查询proximity search
    • "fox quick"~5
    • 以term为单位进行差异比较
GET test_search_index/_search?q=username:alfed

GET test_search_index/_search?q=username:alfed~1

GET test_search_index/_search?q=username:alfd~2

GET test_search_index/_search?q=job:"java engineer"

GET test_search_index/_search?q=job:"java engineer"~1

GET test_search_index/_search?q=job:"java engineer"~2

3. Query DSL简介

  • 将查询语句通过http request body发送到es,主要包含如下参数
    • query符合Query DSL语法的查询的语句
    • form,size
    • timeout
    • sort
    • ...
  • 基于JSON定义的查询语言,主要包含如下两种类型:
    • 字段类查询
      • 如term,match,range等,只针对某一个字段进行查询
    • 复合查询
      • 如bool查询等,包含一个或多个字段类查询或者复合查询语句

4. 字段类查询简介及match-query

  • 字段类查询主要包括以下两类:
    • 全文匹配
      • 针对text类型的字段进行全文检索,会对查询语句先进行分词处理,如match,match_phrase等query类型
    • 单词匹配
      • 不会对查询语句做分词处理,直接去匹配字段的倒排索引,如term,terms,range等query类型
GET test_search_index/_search
{
  "query": {
    "match": {
      "username": "alfred way"
    }
  }
}
  • 通过operator参数可以控制单词间的匹配关系,可选项为or和and
GET test_search_index/_search
{
  "query": {
    "match": {
      "username": {
        "query": "alfred way",
        "operator": "and"
      }
    }
  }
}
  • 通过minimum_should_match参数可以控制需要匹配的单词数
GET test_search_index/_search
{
  "query": {
    "match": {
      "job": {
        "query": "java ruby engineer",
        "minimum_should_match": "3"
      }
    }
  }
}

5. 相关性算分

  • 相关性算分是指文档与查询语句间的相关度,英文为relevance
    • 通过倒排索引可以获取与查询语句相匹配的文档列表,那么如何将最符合用户查询需求的文档放到前列呢?
    • 本质是一个排序问题,排序的依据是相关性算分
  • 相关性算分的几个重要概念如下:
    • Term Frequency(TF):词频,即单词在该文档中出现的次数,词频越高,相关度越高
    • Document Frequency(DF):文档频率,即单词出现的文档数
    • Inverse Document Frequency(IDF):逆向文档频率,与文档频率相反,简单理解为1/DF,即单词出现的文档越少,相关度越高
    • Field-length Norm:文档越短,相关性越高
  • ES目前主要有两个相关性算分模型,如下:
    • TF/IDF模型
    • BM25模型:5.X之后的默认模型

TF/IDF模型

  • 可以通过explain参数来查看具体的计算方法,但要注意:
    • es算分是按照shard进行的,即shard的分数计算是相互独立的,所以在使用explain的时候注意分片数
    • 可以通过设置索引的分片数为1来避免这个问题
GET test_search_index/_search
{
  "explain":true,
  "query": {
    "match": {
      "username": "alfred way"
    }
  }
}
  • BM25模型中BM指Best Match,25指迭代了25词才计算方法,是针对TF/IDF的一个优化

6. match-phrase-query

  • 对字段做检索,有顺序要求
GET test_search_index/_search
{
  "query": {
    "match_phrase": {
      "job": "java engineer"
    }
  }
}


GET test_search_index/_search
{
  "query": {
    "match_phrase": {
      "job": "engineer java"
    }
  }
}
  • 通过slop参数可以控制单词间的间隔
GET test_search_index/_search
{
  "query": {
    "match_phrase": {
      "job": {
        "query": "java engineer",
        "slop": "2"
      }
    }
  }
}

7. query-string-query

  • 类似于URI Search中的q参数查询
GET test_search_index/_search
{
  "profile":true,
  "query":{
    "query_string": {
      "default_field": "username",
      "query": "alfred AND way"
    }
  }
}

GET test_search_index/_search
{
  "profile":true,
  "query": {
    "query_string": {
      "fields": [
        "username",
        "job"
      ],
      "query": "alfred OR (java AND ruby)"
    }
  }
}

8. simple-query-string-query

  • 类似Query String,但是会忽略错误的查询语法,并且仅支持部分查询语法
GET test_search_index/_search
{
  "profile":true,
  "query":{
    "simple_query_string": {
      "query": "alfred +way \"java",
      "fields": ["username"]
    }
  }
}

GET test_search_index/_search
{
  "query":{
    "query_string": {
      "default_field": "username",
      "query": "alfred +way \"java"
    }
  }
}

9. term-terms-query

  • 将查询语句作为整个单词进行查询,即不对查询语句做分词处理
GET test_search_index/_search
{
  "query":{
    "term":{
      "username":"alfred"
    }
  }
}

GET test_search_index/_search
{
  "query":{
    "term":{
      "username":"alfred way"
    }
  }
}
  • terms:一个传入多个查询
G`ET test_search_index/_search
{
  "query": {
    "terms": {
      "username": [
        "alfred",
        "way"
      ]
    }
  }
}

10. range-query

  • 范围查询主要针对数值和日期类型
GET test_search_index/_search
{
  "query":{
    "range": {
      "age": {
        "gte": 10,
        "lte": 30
      }
    }
  }
}

GET test_search_index/_search
{
  "query":{
    "range": {
      "birth": {
        "gte": "1980-01-01"
      }
    }
  }
}
  • 针对日期提供的一种更友好的计算方式
GET test_search_index/_search
{
  "query":{
    "range": {
      "birth": {
        "gte": "now-30y"
      }
    }
  }
}

GET test_search_index/_search
{
  "query":{
    "range": {
      "birth": {
        "gte": "2010||-20y"
      }
    }
  }
}

11. 复合查询介绍及ConstantScore

  • 复合查询是指包含字段类或复合查询的类型,主要包括以下几类:
    • constant_score_query
    • bool query
    • dis_max query
    • function_score_query
    • boosting query

Constant Score Query

  • 该查询将其内部的查询结果文档得分都设定为1或者boost的值
    • 多用于结合bool查询实现自定义得分
GET test_search_index/_search
{
  "query":{
    "constant_score": {
      "filter": {
        "match":{
          "username":"alfred"
        }
      }
    }
  }
}

GET test_search_index/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "constant_score": {
            "filter": {
              "match": {
                "job": "java"
              }
            }
          }
        },
        {
          "constant_score": {
            "filter": {
              "match": {
                "job": "ruby"
              }
            }
          }
        }
      ]
    }
  }
}

12. bool-query

  • 布尔查询由一个或多个布尔子句组成,主要包含如下四个:
    • filter:只过滤符合条件的文档,不计算相关性得分
    • must:文档必须符合must中的所有条件,会影响相关性得分
    • must_not:文档必须不符合must_not中的所有条件
    • should:文档可以符合should中的条件,会影响相关性得分

Filter

  • Filter查询只过滤符合条件的文档,不会进行相关性算分
    • es针对filter会有智能缓存,因此其执行效率很高
    • 做简单匹配查询且不考虑算分时,推荐使用filter替代query等
GET test_search_index/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "username": "alfred"
          }
        }
      ]
    }
  }
}

Must

GET test_search_index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "username": "alfred"
          }
        },
        {
          "match": {
            "job": "specialist"
          }
        }
      ]
    }
  }
}

Must_Not

GET test_search_index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "job": "java"
          }
        }
      ],
      "must_not": [
        {
          "match": {
            "job": "ruby"
          }
        }
      ]
    }
  }
}

should

  • 只包含should时,文档必须满足至少一个条件
    • minimum_should_match可以控制满足条件的个数或者百分比
GET test_search_index/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "match": {
            "username": "junior"
          }
        },
        {
          "match": {
            "job": "ruby"
          }
        }
      ]
    }
  }
}

GET test_search_index/_search
{
  "query": {
    "bool": {
      "should": [
        {"term": {"job": "java"}},
        {"term": {"job": "ruby"}},
        {"term": {"job": "specialist"}}
      ],
      "minimum_should_match": 2
    }
  }
}
  • 同时包含should和must时,文档不必要满足should中的条件,但是如果满足条件,会增加相关性得分
GET test_search_index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "username": "alfred"
          }
        }
      ],
      "should": [
        {
          "term": {
            "job": "ruby"
          }
        }
      ]
    }
  }
}
  • 当一个查询语句位于Query或者Filter上下文时,es执行的结果会不同
    • query:查找与查询语句最匹配的文档,对所有文档进行相关性算分并排序
    • filter:查找与查询语句相匹配的文档

13. count and source filtering

  • 获取符合条件的文档数,endpoint为_count
GET test_search_index/_count
{
  "query":{
    "match":{
      "username": "alfred"
    }
  }
}

source filtering

  • 过滤返回结果中_source中的字段
GET test_search_index/_search

GET test_search_index/_search?_source=username

GET test_search_index/_search
{
  "_source": false
}

GET test_search_index/_search
{
  "_source": ["username","age"]
}

GET test_search_index/_search
{
  "_source": {
    "includes": "*i*",
    "excludes": "birth"
  }
}

2. Elasticsearch之深入了解Search的运行机制

1. Query Then Fetch

  • Search执行的时候实际分两个步骤运作的
    • Query阶段
    • Fetch阶段

Query阶段

Fetch阶段

2. 相关性算分

  • 相关性算分在shard与shard间是相互独立的,也就意味着同一个Term的IDF等值在不同Shard上是不同的,文档的相关性算分和它所处的shard相关
  • 在文档数量不多时,会导致相关性算分严重不准的情况发生
POST test_search_relevance/doc
{
  "name":"hello"
}

POST test_search_relevance/doc
{
  "name":"hello,world"
}

POST test_search_relevance/doc
{
  "name":"hello,world!a beautiful world"
}

GET test_search_relevance/_search
{
  "explain": true, 
  "query": {
    "match":{
      "name":"hello"
    }
  }
}
  • 解决问题的思路有两个:
    • 一是设置分片数为1个,从根本上排除问题,在文档数量不多的时候可以考虑该方案,比如百万到千万级别的文档数量
    • 二是使用DFS Query-then-Fetch查询方式
  • DFS Query-then-Fetch是在拿到所有文档后再重新完整的计算一次相关性得分,耗费更多的cpu和内存,执行性能也比较低下,一般不建议使用,使用方式如下:
GET test_search_relevance/_search?search_type=dfs_query_then_fetch
{
  "query": {
    "match":{
      "name":"hello"
    }
  }
}

3. sorting doc values fielddata

  • es默认会采用相关性算分排序,用户可以通过设定sorting参数来自行设定排序规则
GET test_search_index/_search
{
  "query":{
    "match": {
      "username": "alfred"
    }
  },
  "sort":{
    "birth":"desc"
  }
}

GET test_search_index/_search
{
  "query":{
    "match": {
      "username": "alfred"
    }
  },
  "sort": [
    {
      "birth": "desc"
    },
    {
      "_score": "desc"
    },
    {
      "_doc": "desc"
    }
  ]
}
  • 按照字符串排序比较特殊,因为es有text和keyword两种类型
    • 针对text类型排序,会产生报错
    • 针对keyword类型排序,可以返回预期结果
GET test_search_index/_search
{
  "sort":{
    "username.keyword":"desc"
  }
}

排序

  • 排序的过程实质是对字段原始内容排序的过程,这个过程中倒排索引无法发挥作用,需要用到正排索引,也就是通过文档Id和字段可以快速得到字段原始内容
  • es对此提供了2种实现方式:
    • fielddata默认禁用
    • doc values默认启用,除了text类型

Fielddata

  • Fielddata默认是关闭的,可以通过如下api开启:
    • 此时字符串是按照分词后的term排序,往往结果很难符合预期
    • 一般是在对分词做聚合分析的时候开启
PUT test_search_index/_mapping/doc
{
  "properties": {
    "job":{
      "type":"text",
      "fielddata": true
    }
  }
} 

Doc Values

  • Doc Values默认是启用的,可以在创建索引的时候关闭:
    • 如果后面要再开启Doc Values,需要做reindex操作
PUT test_doc_values/_mapping/doc
{
  "properties": {
    "username": {
      "type": "keyword",
      "doc_values": false
    },
    "hobby": {
      "type": "keyword"
    }
  }
}

docvalue_fields

  • 可以通过该字段获取fielddata或者doc values中存储的内容
GET test_search_index/_search
{
  "docvalue_fields": [
    "username",
    "username.keyword",
    "age"
  ]
}

4. 分页与遍历-fromsize

  • es提供了3种方式来解决分页与遍历的问题:
    • from/size
      • from指明开始位置
      • size指明获取总数
    • scoll
    • search_after
  • 深度分页是一个经典的问题:在数据分片存储的情况下如何获取前1000个文档?
    • 获取990~1000的文档时,会在每个分片都先获取1000个文档,然后再由Coordinating Node聚合所有分片的结果后再排序获取前1000个文档
    • 页数越深,处理文档越多,占用内存越多,耗时越长,尽量避免深度分页,es通过index.max_result_window限定最多到10000条数据
GET test_search_index/_search
{
  "from":0,
  "size":2
}

GET test_search_index/_search
{
  "from":10000,
  "size":2
}

5. 分页与遍历-scroll

  • 遍历文档集的api,以快照的方式来避免深度分页的问题
    • 不能用来做实时搜索,因为数据不是实时的
    • 尽量不要使用复杂的sort条件,使用_doc最高效
    • 使用稍嫌复杂
  • 第一步需要发起1个scroll search
    • es在收到该请求后根据查询条件创建文档Id合集的快照
GET test_search_index/_search?scroll=5m
{
  "size":1  指明每次scroll返回的文档数
}
  • 第二步调用scroll search的api,获取文档集合
    • 不断迭代调用直到返回hits.hits数组为空时停止
POST _search/scroll
{
  "scroll" : "5m", 
  "scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAABswWX3FLSTZFOF9URFdqWHlvX3gtYmhtdw=="
}
  • 因为是快照,所以新的文档无法被检索
PUT test_search_index/doc/10
{
  "username":"doc10"
}
  • 过多的scroll调用会占用大量内存,可以用clear api删除过多的scroll快照

    DELETE _search/scroll/_all

6. 分页与遍历-search_after

  • 避免深度分页的性能问题,提供实时的下一页文档获取功能
    • 缺点是不能使用from参数,即不能指定页数
    • 只能下一页,不能上一页
    • 使用简单
  • 第一步为正常的搜索,但要指定sort值,并保证值唯一
GET test_search_index/_search
{
  "size":1,
  "sort":{
    "age":"desc",
    "_id":"desc"
  }
}
  • 第二步为使用上一步最后一个文档的sort值进行查询
GET test_search_index/_search
{
  "size":1,
  "search_after":[23,"4"],
  "sort":{
    "age":"desc",
    "_id":"desc"
  }
}

应用场景

  • From/Size:需要实时获取顶部的部分文档,且需要自由翻页
  • Scroll:需要全部文档,如导出所有数据的功能
  • Search_After:需要全部文档,不需要自由翻页

3. Elasticsearch之聚合分析入门

1. 聚合分析简介

  • 搜索引擎用来回答如下问题:
    • 请告诉我地址为上海的所有订单?
    • 请告诉我最近1天内创建但没有付款的所有订单?
  • 聚合分析可以回答如下问题:
    • 请告诉我最近1周每天的订单成交量有多少?
    • 请告诉我最近1个月每天的平均订单金额是多少?
    • 请告诉我最近半年卖的最火的前5个商品是那些?

聚合分析

  • 聚合分析,英文为Aggregation,是es除搜索功能外提供的针对es数据做统计分析的功能
    • 功能丰富,提供Bucket,Metric,Pipeline等多种分析方式,可以满足大部分的分析需求
    • 实时性高,所有的计算结果都是及时返回的,而Hadoop等大数据系统一般都是T+1的

分类

  • 为了便于理解,es将聚合分析主要分为如下4类
    • Bucket,分桶类型,类似SQL中的GROUP BY语法
    • Metric,指标分析类型,如计算最大值,最小值,平均值等等
    • Pipeline,管道分析类型,基于上一级的聚合分析结果进行再分析
    • Matrix,矩阵分析类型

2. metric聚合分析

POST test_search_index/doc/_bulk
{"index":{"_id":"1"}}
{"username":"alfred way","job":"java engineer","age":18,"birth":"1990-01-02","isMarried":false,"salary":10000}
{"index":{"_id":"2"}}
{"username":"tom","job":"java senior engineer","age":28,"birth":"1980-05-07","isMarried":true,"salary":30000}
{"index":{"_id":"3"}}
{"username":"lee","job":"ruby engineer","age":22,"birth":"1985-08-07","isMarried":false,"salary":15000}
{"index":{"_id":"4"}}
{"username":"Nick","job":"web engineer","age":23,"birth":"1989-08-07","isMarried":false,"salary":8000}
{"index":{"_id":"5"}}
{"username":"Niko","job":"web engineer","age":18,"birth":"1994-08-07","isMarried":false,"salary":5000}
{"index":{"_id":"6"}}
{"username":"Michell","job":"ruby engineer","age":26,"birth":"1987-08-07","isMarried":false,"salary":12000}
  • 主要分如下两类:
    • 单值分析,只输出一个分析结果
      • min,max,avg,sum
      • cardinality
    • 多值分析,输出多个分析结果
      • stats,entended stats
      • percentile,percentile rank
      • top hits

Min

  • 返回数值类字段的最小值
GET test_search_index/_search
{
  "size":0,
  "aggs":{
    "min_age":{
      "min": {
        "field": "age"
      }
    }
  }
}

Max

  • 返回数值类字段的最大值
GET test_search_index/_search
{
  "size":0,
  "aggs":{
    "max_age":{
      "max": {
        "field": "age"
      }
    }
  }
}

Avg

  • 返回数值类字段的平均值
GET test_search_index/_search
{
  "size":0,
  "aggs":{
    "avg_age":{
      "avg": {
        "field": "age"
      }
    }
  }
}

Sum

  • 返回数值类字段的总和
GET test_search_index/_search
{
  "size":0,
  "aggs":{
    "sum_age":{
      "sum": {
        "field": "age"
      }
    }
  }
}
  • 一次返回多个聚合结果
GET test_search_index/_search
{
  "size": 0,
  "aggs": {
    "min_age": {
      "min": {
        "field": "age"
      }
    },
    "max_age": {
      "max": {
        "field": "age"
      }
    },
    "avg_age": {
      "avg": {
        "field": "age"
      }
    },
    "sum_age": {
      "sum": {
        "field": "age"
      }
    }
  }
}

Cardinality

  • Cardinality,意为集合的势,或者基数,是指不同数值的个数,类似SQL中的distinct count概念
GET test_search_index/_search
{
  "size":0,
  "aggs":{
    "count_of_job":{
      "cardinality": {
        "field": "job.keyword"
      }
    }
  }
}

Stats

  • 返回一系列数值类型的统计值,包含min,max,avg,sum和count
GET test_search_index/_search
{
  "size":0,
  "aggs":{
    "stats_age":{
      "stats": {
        "field": "age"
      }
    }
  }
}

Extended Stats

  • 对stats的扩展,包含了更多的统计数据,如方差,标准差等
GET test_search_index/_search
{
  "size":0,
  "aggs":{
    "exstats_salary":{
      "extended_stats": {
        "field": "salary"
      }
    }
  }
}

Percentile

  • 百分位数统计
GET test_search_index/_search
{
  "size":0,
  "aggs":{
    "per_salary":{
      "percentiles": {
        "field": "salary"
      }
    }
  }
}

GET test_search_index/_search
{
  "size": 0,
  "aggs": {
    "per_age": {
      "percentiles": {
        "field": "salary",
        "percents": [
          95,
          99,
          99.9
        ]
      }
    }
  }
}

Percentile Rank

  • 百分位数统计
GET test_search_index/_search
{
  "size": 0,
  "aggs": {
    "per_salary": {
      "percentile_ranks": {
        "field": "salary",
        "values": [
          11000,
          30000
        ]
      }
    }
  }
}

Top Hits

  • 一般用于分桶后获取桶内最匹配的顶部文档列表,即详情数据
GET test_search_index/_search
{
  "size": 0,
  "aggs": {
    "jobs": {
      "terms": {
        "field": "job.keyword",
        "size": 10
      },
      "aggs": {
        "top_employee": {
          "top_hits": {
            "size": 10,
            "sort": [
              {
                "age": {
                  "order": "desc"
                }
              }
            ]
          }
        }
      }
    }
  }
}

3. bucket聚合分析

  • Bucket,意为桶,即按照一定的规则将文档分配到不同的桶中,达到分类分析的目的
  • 按照Bucket的分桶策略,常见的Bucket聚合分析如下:
    • Terms
    • Range
    • Date Range
    • Histogram
    • Date Histogram

Terms

  • 该分桶策略最简单,直接按照term来分桶,如果是text类型,则按照分词后的结果分桶
GET test_search_index/_search
{
  "size": 0,
  "aggs": {
    "jobs": {
      "terms": {
        "field": "job",
        "size": 5
      }
    }
  }
}

Range

  • 按照指定数值的范围来设定分桶规则
GET test_search_index/_search
{
  "size": 0,
  "aggs": {
    "salary_range": {
      "range": {
        "field": "salary",
        "ranges": [
          {
            "key":"<10000",
            "to": 10000
          },
          {
            "from": 10000,
            "to": 20000
          },
          {
            "key":">20000",
            "from": 20000
          }
        ]
      }
    }
  }
}

Date Range

  • 通过指定日期的范围来设定分桶规则
GET test_search_index/_search
{
  "size": 0,
  "aggs": {
    "date_range": {
      "range": {
        "field": "birth",
        "format": "yyyy",
        "ranges": [
          {
            "from":"1980",
            "to": "1990"
          },
          {
            "from": "1990",
            "to": "2000"
          },
          {
            "from": "2000"
          }
        ]
      }
    }
  }
}

Histogram

  • 直方图,以固定间隔的策略来分割数据
GET test_search_index/_search
{
  "size":0,
  "aggs":{
    "salary_hist":{
      "histogram": {
        "field": "salary",
        "interval": 5000,
        "extended_bounds": {
          "min": 0,
          "max": 40000
        }
      }
    }
  }
}

Date Histogram

  • 针对日期的直方图或者柱状图,是时序数据分析中常用的聚合分析类型
GET test_search_index/_search
{
  "size":0,
  "aggs":{
    "by_year":{
      "date_histogram": {
        "field": "birth",
        "interval": "year",
        "format":"yyyy"
      }
    }
  }
}

4. bucket和metric聚合分析

  • Bucket聚合分析允许通过添加子分析来进一步进行分析,该子分析可以是Bucket也可以是Metric,这也使得es的聚合分析能力变得异常强大
  • 分桶后再分桶
GET test_search_index/_search
{
  "size": 0,
  "aggs": {
    "jobs": {
      "terms": {
        "field": "job.keyword",
        "size": 10
      },
      "aggs": {
        "age_range": {
          "range": {
            "field": "age",
            "ranges": [
              {
                "to": 20
              },
              {
                "from": 20,
                "to": 30
              },
              {
                "from": 30
              }
            ]
          }
        }
      }
    }
  }
}
  • 分桶后进行数据分析
GET test_search_index/_search
{
  "size": 0,
  "aggs": {
    "jobs": {
      "terms": {
        "field": "job.keyword",
        "size": 10
      },
      "aggs": {
        "salary": {
          "stats": {
            "field": "salary"
          }
        }
      }
    }
  }
}

5. pipeline聚合分析

  • 针对聚合分析的结果进行再次聚合分析,而且支持链式调用,可以回答如下问题:
    • 订单月平均销售额是多少?
  • Pipeline的分析结果会输出到原结果中,根据输出位置的不同,分为以下两类:
    • Parent结果内嵌到现有的聚合分析结果中
      • Derivative
      • Moving Average
      • Cumulative Sum
    • Sibling结果与聚合分析结果同级
      • Max/Min/Avg/Sum Bucket
      • Stats/Extended Stats Bucket
      • Percentiles Bucket

Min Bucket

GET test_search_index/_search
{
  "size":0,
  "aggs":{
    "jobs":{
      "terms": {
        "field": "job.keyword",
        "size": 10
      },
      "aggs":{
        "avg_salary":{
          "avg": {
            "field": "salary"
          }
        }
      }
    },
    "min_salary_by_job":{
      "min_bucket": {
        "buckets_path": "jobs>avg_salary"
      }
    }
  }
} 

Derivative

  • 计算Bucket值的导数
GET test_search_index/_search
{
  "size": 0,
  "aggs": {
    "birth": {
      "date_histogram": {
        "field": "birth",
        "interval": "year",
        "min_doc_count": 0
      },
      "aggs": {
        "avg_salary": {
          "avg": {
            "field": "salary"
          }
        },
        "derivative_avg_salary": {
          "derivative": {
            "buckets_path": "avg_salary"
          }
        }
      }
    }
  }
}

Moving Average

  • 计算Bucket值的移动平均值

Cumulative Sum

  • 计算Bucket值得累计加和

6. 作用范围

  • es聚合分析默认作用范围是query的结果集,可以通过如下的方式改变其作用范围:
    • filter
    • post_filter
    • global

filter

  • 为某个聚合分析设定过滤条件,从而在不更改整体query语句的情况下修改了作用范围
GET test_search_index/_search
{
  "size": 0,
  "aggs": {
    "jobs_salary_small": {
      "filter": {
        "range": {
          "salary": {
            "to": 10000
          }
        }
      },
      "aggs": {
        "jobs": {
          "terms": {
            "field": "job.keyword"
          }
        }
      }
    },
    "jobs": {
      "terms": {
        "field": "job.keyword"
      }
    }
  }
}

post_filter

  • 作用于文本过滤,但在聚合分析后生效
GET test_search_index/_search
{
  "aggs": {
    "jobs": {
      "terms": {
        "field": "job.keyword"
      }
    }
  },
  "post_filter": {
    "match":{
      "job.keyword":"java engineer"
    }
  }
}

global

  • 无视query过滤条件,基于全部文档进行分析
GET test_search_index/_search
{
  "query": {
    "match": {
      "job.keyword": "java engineer"
    }
  },
  "aggs": {
    "java_avg_salary": {
      "avg": {
        "field": "salary"
      }
    },
    "all": {
      "global": {},
      "aggs": {
        "avg_salary": {
          "avg": {
            "field": "salary"
          }
        }
      }
    }
  }
}

7. 排序

  • 可以使用自带的关键数据进行排序,比如:
    • _count文档数
    • _key按照key值排序
GET test_search_index/_search
{
  "size": 0,
  "aggs": {
    "jobs": {
      "terms": {
        "field": "job.keyword",
        "size": 10,
        "order": [
          {
            "avg_salary": "desc"
          }
        ]
      },
      "aggs": {
        "avg_salary": {
          "avg": {
            "field": "salary"
          }
        }
      }
    }
  }
}

4. Elasticsearch 篇之数据建模

1. 数据建模简介

  • 英文为Data Modeling,为创建数据模型的过程
  • 数据模型(Data Model)
    • 对现实世界进行抽象描述的一种工具和方法
    • 通过抽象的实体及实体之间的联系的形式去描述业务规则,从而实现对现实世界的映射

数据建模的过程

  • 概念模型
    • 确定系统的核心需求和范围边界,设计实体和实体间的关系
  • 逻辑模型
    • 进一步梳理业务需求,确定每个实体的属性,关系和约束等
  • 物理模型
    • 结合具体的数据库产品,在满足也读读写性能等需求的前提下确定最终的定义
    • MySQL,MongoDB,elasticsearch等
    • 第三范式

2. ES数据建模配置相关介绍

  • ES是基于Lucene以倒排索引为基础实现的存储体系,不遵循关系型数据库中的范式约定

Mapping字段的相关设置

  • enbaled
    • true | false
    • 仅存储,不搜索或聚合分析
  • index
    • true | false
    • 是否构建倒排索引
  • index_options
    • docs | freqs | positions | offsets
    • 存储倒排索引的哪些信息
  • norms
    • true | false
    • 是否存储归一化相关参数,如果字段仅用于过滤和聚合分析,可关闭
  • doc_values
    • true | false
    • 是否启用doc_values,用于排序和聚合分析
  • field_data
    • false | true
    • 是否为text类型启动fielddata,实现排序和聚合分析
  • store
    • false | true
    • 是否存储该字段值
  • coerce
    • true | false
    • 是否开启自动数据类型转换功能,比如字符串转为数字,浮点转为整型等
  • multifields 多字段
    • 灵活使用多字段特性来解决多样的业务需求
  • dynamic
    • true | false | strict
    • 控制mapping自动更新
  • data_detection
    • true | false
    • 是否自动识别日期类型

设定流程

  • 是何种类型?
    • 字符串类型
    • 枚举类型
    • 数值类型
    • 其他类型
  • 是否需要检索?
    • 完全不需要检索,排序,聚合分析的字段
      • enabled设置为false
    • 不需要检索的字段
      • index设置为false
    • 需要检索的字段,可以通过如下配置设定需要的存储力度
      • index_options结合需要设定
      • norms不需要归一化数据时关闭即可
  • 是否需要排序和聚合分析?
    • doc_values设定为false
    • fielddata设定为false
  • 是否需要另行存储?
    • 是否需要专门存储当前字段的数据?

3. ES数据建模实例

  • 博客文章blog_index
    • 标题title
    • 发布日期publish_date
    • 作者author
    • 摘要abstract
    • 内容content
    • 网络地址url
PUT blog_index
{
  "mappings": {
    "doc": {
      "_source": {
        "enabled": false
      },
      "properties": {
        "title": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 100
            }
          },
          "store": true
        },
        "publish_date": {
          "type": "date",
          "store": true
        },
        "author": {
          "type": "keyword",
          "ignore_above": 100, 
          "store": true
        },
        "abstract": {
          "type": "text",
          "store": true
        },
        "content": {
          "type": "text",
          "store": true
        },
        "url": {
          "type": "keyword",
          "doc_values":false,
          "norms":false,
          "ignore_above": 100, 
          "store": true
        }
      }
    }
  }
}
  • 查询
GET blog_index/_search
{
  "stored_fields": ["title","publish_date","author","abstract","url"], 
  "query": {
    "match": {
      "content": "blog"
    }
  },
  "highlight": {
    "fields":{
      "content": {}
    }
  }
}

4. Nested_Object

  • ES不擅长处理关系型数据库中的关联关系,比如文章表blog与评论表comment之间通过blog_id关联,在ES中可以通过如下两种手段变相解决
    • Nested Object
    • Parent/Child

关联关系处理

  • 文章Id blog_id
  • 评论人 username
  • 评论日期 date
  • 评论内容 content
DELETE blog_index_nested
PUT blog_index_nested
{
"mappings": {
  "doc":{
    "properties": {
      "title":{
        "type": "text",
        "fields": {
          "keyword":{
            "type":"keyword",
            "ignore_above": 100
          }
        }
      },
      "publish_date":{
        "type":"date"
      },
      "author":{
        "type":"keyword",
        "ignore_above": 100
      },
      "abstract":{
          "type": "text"
        },
        "url":{
          "enabled":false
        },
        "comments":{
          "type":"nested", 
          "properties": {
            "username":{
              "type":"keyword",
              "ignore_above":100
            },
            "date":{
              "type":"date"
            },
            "content":{
              "type":"text"
            }
          }
        }
      }
    }
  }
}

PUT blog_index_nested/doc/2
{
  "title": "Blog Number One",
  "author": "alfred",
  "comments": [
    {
      "username": "lee",
      "date": "2017-01-02",
      "content": "awesome article!"
    },
    {
      "username": "fax",
      "date": "2017-04-02",
      "content": "thanks!"
    }
  ]
}

GET blog_index_nested/_search
{
  "query": {
    "nested": {
      "path": "comments",
      "query": {
        "bool": {
          "must": [
            {
              "match": {
                "comments.username": "lee"
              }
            },
            {
              "match": {
                "comments.content": "thanks"
              }
            }
          ]
        }
      }
    }
  }
}

5. Parent_Child

  • ES还提供了类似关系数据库中join的实现方式,使用join数据类型实现
PUT blog_index_parent_child
{
  "mappings": {
    "doc": {
      "properties": {
        "join": {
          "type": "join",
          "relations": {
            "blog": "comment"
          }
        }
      }
    }
  }
}

PUT blog_index_parent_child/doc/1
{
  "title":"blog",
  "join":"blog"  指明父类型
}

PUT blog_index_parent_child/doc/2
{
  "title":"blog2",
  "join":"blog"
}
PUT blog_index_parent_child/doc/comment-1?routing=1  指明routing值,确保父子文档在一个分片上,一般使用父文档Id
{
  "comment":"comment world",
  "join":{
    "name":"comment",  指明子类型
    "parent":1  指明父文档Id
  }
}


PUT blog_index_parent_child/doc/comment-2?routing=2
{
  "comment":"comment hello",
  "join":{
    "name":"comment",
    "parent":2
  }
}
  • 常见query语法包括如下几种:
    • parent_id:返回某父文档的子文档
    • has_child:返回包含某子文档的父文档
    • has_parent:返回包含某父文档的子文档
GET blog_index_parent_child/_search
{
  "query":{
    "parent_id":{
      "type":"comment",
      "id":"2"
    }
  }
}
GET blog_index_parent_child/_search
{
  "query":{
    "has_child": {
      "type": "comment",
      "query": {
        "match": {
          "comment": "world"
        }
      }
    }
  }
}
GET blog_index_parent_child/_search
{
  "query":{
    "has_parent": {
      "parent_type": "blog",
      "query": {
        "match": {
          "title": "blog"
        }
      }
    }
  }
}

6. nested_vs_parent_child

nested object

  • 优点:文档存储在一起,因此读取性能高
  • 缺点:更新父或子文档时需要更新整个文档
  • 场景:子文档偶尔更新,查询频繁

parent child

  • 优点:父子文档可以独立更新,互不影响
  • 缺点:为了维护join的关系,需要占用部分内存,读取性能较差
  • 场景:子文档更新频繁

建议尽量选择nested object来解决问题

7. reindex

  • 指重建所有数据的过程,一般发生在如下情况:
    • mapping设置更改,比如字段类型变化,分词器字典更新等
    • index设置变更,比如分片数更改等
    • 迁移数据
  • ES提供了现成的API用于完成该工作
    • _update_by_query在现有索引上重建
    • _reindex在其他索引上重建
POST blog_index/_update_by_query?conflicts=proceed

POST _reindex
{
  "source": {
    "index": "blog_index"
  },
  "dest": {
    "index": "blog_new_index"
  }
}
  • 数据重建的时间受原索引文档规模的影响,当规模越大时,所需时间越多,此时需要通过设定url参数wait_for_completion为false来异步执行,ES以task来描述此类执行任务
POST blog_index/_update_by_query?conflicts=proceed&wait_for_completion=false

GET _tasks/_qKI6E8_TDWjXyo_x-bhmw:11996

8. 其他建议

数据模型版本管理

  • 对Mapping进行版本管理
    • 包含在代码或者以专门的文件进行管理,添加好注释,并加入Git等版本管理仓库中,方便回顾
    • 为每个增加一个metadata字段,在其中维护一些文档相关的元数据,方便对数据进行管理

防止字段过多

  • 字段过多主要有如下的坏处:
    • 难于维护,当字段成百上千时,基本很难有人明确知道每个字段的含义
    • mapping的信息存储在cluster state里面,过多的字段会导致mapping过大,最终导致更新变慢
    • 一般字段过多的原因是由于没有高质量的数据建模导致的,比如dynamic设置为true
    • 考虑拆分多个索引来解决问题

最后

大家可以关注我的微信公众号一起学习进步。