佳星辰 的个人博客

记录精彩的程序人生

努力、自律、上进
早睡、锻炼、读书
  menu
9 文章
0 浏览
0 当前访客
ღゝ◡╹)ノ❤️

Elasticsearch快速入门

Elasticsearch

简介

Elasticsearch 是一个开源的高扩展的分布式全文检索引擎,可以快速地存储、搜索和分析海量数据。它的底层是开源库 Lucene。但是,你没法直接用 Lucene,必须自己写代码去调用它的接口。Elastic 是 Lucene 的封装,提供了 REST API 的操作接口,开箱即用,天然的跨平台,为方便以下简称:ES。

存储结构

索引(index)

在 ES 中索引类似 MySQL 的表,代表文档数据的集合,文档指的是 ES 中存储的一条数据。

文档类型(type)

在新版的 ES 中,已经不使用文档类型了,在 ES 老的版本中文档类型,代表一类文档的集合,index(索引)类似 MySQL 的数据库,而文档类型类似 MySQL 的表。

文档(document)

ES 是面向文档的数据库,文档是最基本的存储单元,文档类似 MySQL 表中的一行数据。

ES 中文档使用 JSON 格式存储,因此存储上比 MySQL 要灵活的多,ES 支持任意格式的 JSON 数据

文档中的任何 JSON 字段都可以作为查询条件。

文档的 JSON 格式没有严格限制,可以随意增加、减少字段,甚至每一个文档的格式都不一样也可以。

虽然文档的格式没有限制,可以随便存储任意格式数据,但是,实际业务中不会这么干,通常一个索引只会存储格式相同的数据,例如:订单索引,只会保存订单数据,不会保存商品数据,否则你会被自己搞死,自己都不知道里面存的是什么数据。

文档字段(field)

文档由多个 JSON 字段组成,这里的字段类似 MySQL 中表的字段。

当然 ES 中字段也有类型的,下面是常用的字段类型:

  • 数值类型(包括: long、integer、short、byte、double、float)
  • text - 支持全文搜索
  • keyword - 不支持全文搜索,例如:email、电话这些数据,作为一个整体进行匹配就可以,不需要分词处理。
  • date - 日期类型
  • boolean - 布尔类型

映射(mapping)

ES 的映射类似 MySQL 中的表结构定义,每个索引都有一个映射规则,我们可以通过定义索引的映射规则,提前定义好文档的 JSON 结构和字段类型,如果没有定义索引的映射规则,ES 会在写入数据的时候,根据我们写入的数据字段推测出对应的字段类型,相当于自动定义索引的映射规则

但是实际业务中,对于关键的字段类型,通常预先定义好,避免 ES 自动生成的字段类型不是你想要的类型,例如: ES 默认将字符串类型数据自动定义为text类型,但是关于手机号,我们希望是keyword类型,这个时候就需要通过映射预先定义好对应的字段类型了

类比MySQL存储结构

ES存储结构MySQL存储结构
Index(索引)
Document(文档)一行数据
Field(字段)表字段
mapping(映射)表结构定义

文档CURD

文档元数据

文档元数据,指的是插入 JSON 文档的时候,ES 为这条数据,自动生成的系统字段。

元数据的字段名都是以下划线开头的。

常见的元数据如下:

  • _index - 代表当前JSON文档所属的文档名字
  • _type - 代表当前JSON文档所属的类型,虽然新版ES废弃了type的用法,但是元数据还是可以看到。
  • _id - 文档唯一Id, 如果我们没有为文档指定id,系统会自动生成
  • _source - 代表我们插入进去的JSON数据
  • _version - 文档的版本号,每修改一次文档数据,字段就会加1, 这个字段新版的ES已经不使用了
  • _seq_no - 文档的版本号, 替代老的_version字段
  • _primary_term - 文档所在主分区,这个可以跟_seq_no字段搭配实现乐观锁。

插入文档

在 ES 中插入一个 JSON 文档,又叫做索引文档

# 语法
PUT /{index}/{type}/{id}
{
  "field": "value",
  ...
}

# 示例
PUT /jxc_ik/jxc/1
{
      "avatar": "http://dummyimage.com/274x287.png/0c0d32/045578",
      "logo": "http://dummyimage.com/100x200.png/034c4a/2d0007",
      "email": "[email protected]",
      "userName": "Sylvia Beer",
      "createAt": 1468728819120,
      "birthday": "03/08/1996 00:43",
      "age": 2200,
      "price": 20579.85,
      "homePage": "https://bridie-utools.com",
      "phoneNumber": "15635348566",
      "city": "Loraineburgh 07597 Bella Inlet",
      "workAt": "Mayert - Treutel",
      "uid": "ca50f4a6-cc84-4b6e-8fd0-b740fe96fe77",
      "description": "Autem pariatur quia cumque aut.",
      "idNo": "755808201607016399",
      "socialCode": "177202933432851"
}

PUT代表发送一个http put请求, 后面的URL参数说明:

  • {index} - 索引名
  • {type} - 文档类型名 - 新版的Elasticsearch为了兼容老版本,只允许指定一个类型,随便设置一个类型名就行。
  • {id} - 文档的唯一id, 可以不指定, 如果不指定id, 需要使用POST发送请求

查询文档

根据文档ID查询一个文档

# 语法
GET /{index}/{type}/{id}

# 示例
GET /jxc_ik/jxc/1

# 输出结果
{
    "_index": "jxc_ik",
    "_type": "jxc",
    "_id": "1",
    "_version": 2,
    "_seq_no": 1,
    "_primary_term": 1,
    "found": true,
    "_source": {
        "avatar": "http://dummyimage.com/274x287.png/0c0d32/045578",
        "logo": "http://dummyimage.com/100x200.png/034c4a/2d0007",
        "email": "[email protected]",
        "userName": "Sylvia Beer",
        "createAt": 1468728819120,
        "birthday": "03/08/1996 00:43",
        "age": 2200,
        "price": 20579.85,
        "homePage": "https://bridie-utools.com",
        "phoneNumber": "15635348566",
        "city": "Loraineburgh 07597 Bella Inlet",
        "workAt": "Mayert - Treutel",
        "uid": "ca50f4a6-cc84-4b6e-8fd0-b740fe96fe77",
        "description": "Autem pariatur quia cumque aut",
        "idNo": "755808201607016399",
        "socialCode": "177202933432851"
    }
}

更新文档

更新整个文档的语法和前面介绍的插入文档的语法一摸一样,只要ID相同就会直接覆盖之前的文档

如果我们只想要更新文档的某些字段,可以使用局部更新

# 语法
POST /{index}/_update/{id}
{
  "doc":{ // 在doc字段中指定需要更新的字段
    // 需要更新的字段列表
  }
}

# 示例
POST /jxc_ik/_update/1
{
  "doc":{
    "description":"try to update desc by this way",
    "age":30
  }
}

# 输出结果
{
    "_index": "jxc_ik",
    "_type": "_doc",
    "_id": "1",
    "_version": 3,
    "result": "updated",
    "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
    },
    "_seq_no": 2,
    "_primary_term": 1
}

删除文档

# 语法
DELETE /{index}/{type}/{id}

文档类型定义

精确值 & 全文类型

精确值通常指的是数值类型、时间、布尔值、字符串的keyword类型,这些不可分割的数据类型,精确值搜索效率比较高,精确值匹配类似 MySQL 中根据字段搜索,要么等于,要么不等于,不会做别的计算

全文类型,指的是 text 类型,会设计分词处理,存储到 SE 中的数据不是原始数据,而是一个个关键词

实际项目中,如果不需要模糊搜索的字符类型,可以选择 keyword 类型,如果选择 text 类型,可能会出现一大堆相似的数据,而不是精确的数据

自动映射

当我们没有预先定义文档的映射,也可以插入数据,因为 ES 默认会自动检测我们插入的数据的类型,相当于自动定义文档类型。

自动映射的缺点就是会出现 ES 映射的数据类型,不是我们想要的类型,例如:手机号,我们希望是一个精确值,使用 keyword 类型,ES 映射成为了 text 类型,这就不符合业务预期了。

自定义文档的数据类型

# 语法
PUT /{索引名字}
{
  "mappings": { // 表示定义映射规则
    "properties": { // 定义属性,也就是字段类型
      "字段名1":    { "type": "字段类型" },  
      "字段名2":    { "type": "字段类型" }
      ...(提示:最后一行末尾不要加逗号)...
    }
  }
}

# 示例
PUT /order
{
  "mappings": {
    "properties": {
      "id":    	{ "type": "integer" },  
      "shop_id":    { "type": "integer" },  
      "user_id":    { "type": "integer" },
      "order_no":  { "type": "keyword"  }, 
      "create_at":  { "type": "date", "format":"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"}, 
      "phone":   { "type": "keyword"  },
      "address":   { "type": "text"  }
    }
  }
}

上面的索引构造出于如下的考虑

字段名ES类型说明
idinteger订单id,整数
shop_idinteger店铺Id, 整数
user_idinteger用户id, 整数
order_nokeyword订单编号,字符串类型,精确值
create_atdate订单创建时间,日期类型
phonekeyword电话号码,字符串类型,精确值
addresstext用户地址,字符串类型,需要模糊搜索

查询索引的映射规则

# 语法
GET /{索引名}/_mapping
{
  
}

查询

ES 支持两种基本方式索引

  • 一个是通过使用 REST request URI 发送搜索参数(uri + 索引参数)
  • 另一个是通过使用 REST requestbopdy 来发送搜索参数(uri + 请求体)

Search API

请求解释
GET bank/_search检索 bank 下所有信息,包括 type 和 docs
GET bank/_search?q=*&sort=account_number:asc请求参数方式检索

响应结果解释

字段名解释
took执行搜索的时间(臺秒)
time_out搜索是否超时
_shards统计成功/失败的搜索分片
hits搜索结果
hits.total搜索结果
hits.hits搜索结果数组,默认为前十的文档
sort结果的排序 key (键) (没有则按 score 排序)
score 和 max_score相关性得分和最高得分(全文检索用)

查询结果

Query DSL

基本语法格式

Elastisearch 提供了一个可以执行查询的 Json 风格的 DSl (domain-specific language 领域特定语言) 。这个被称为Query DSL,该查询语言非常全面

  • {FIELD} - 字段名
  • {TEXT} - 匹配内容
  • {索引名} - 索引名称,类比SQL数据库中的 database

匹配单个字段

通过 match 实现全文搜索,如果字段的数据类型是text类型,搜索关键词会进行分词处理

# 语法
GET /{索引名}/_search
{
  "query": {
    "match": {
      "{FIELD}": "{TEXT}"
    }
  }
}

# 示例
GET /article/_search
{
    "query": {
        "match" : {
            "title" : "ES教程"
        }
    }
}

精准匹配单个字段

如果我们想要类似SQL语句中的等值匹配,不需要进行分词处理,例如:订单号、手机号、时间字段,不需要分值处理,只要精确匹配,可以通过term实现精确匹配语法

# 语法
GET /{索引名}/_search
{
  "query": {
    "term": {
      "{FIELD}": "{VALUE}"
    }
  }
}

# 示例
GET /order_v2/_search
{
  "query": {
    "term": {
      "order_no": "202003131209120999"
    }
  }
}

# 等价SQL:select * from order_v2 where order_no = "202003131209120999"

in 查询

实现一个字段包含给定数组中的任意一个值就匹配的操作,使用terms

# 语法
GET /{索引名}/_search
{
  "query": {
    "terms": {
      "{FIELD}": [
        "{VALUE1}",
        "{VALUE2}"
      ]
    }
  }
}

# 示例
GET /order_v2/_search
{
  "query": {
    "terms": {
      "shop_id": [123,100,300]
    }
  }
}

# 等价SQL:select * from order_v2 where shop_id in (123,100,300)

范围查询

通过 range 实现范围查询,类似SQL语句中的:> >= < <= 表达式

# 语法
GET /{索引名}/_search
{
  "query": {
    "range": {
      "{FIELD}": {
        "gte": 10, 
        "lte": 20
      }
    }
  }
}

# 示例
GET /order_v2/_search
{
  "query": {
    "range": {
      "shop_id": {
        "gte": 10,
        "lte": 200
      }
    }
  }
}

# 等价SQL:select * from order_v2 where shop_id >= 10 and shop_id <= 200

范围参数如下:

  • gt - 大于(>)
  • gte - 大于等于(>=)
  • lt - 小于(<)
  • lte - 小于等于(<=)

bool 组合查询

如果需要编写类似 SQL 的where 语句,组合多个字段的查询,可以使用 bool 语句

bool 查询基本语法结构

在 ES 中 bool 查询就是用来这和不二查询条件的,类似 SQL 中的 and 和 or

# 语法
GET /{索引名}/_search
{
  "query": {
    "bool": { // bool查询
      "must": [], // must条件,类似SQL中的and, 代表必须匹配条件
      "must_not": [], // must_not条件,跟must相反,必须不匹配条件
      "should": [] // should条件,类似SQL中or, 代表匹配其中一个条件
    }
  }
}

可以任意选择 must、must_not和should 条件,并且参数值都是一个数组,这意味着它们都支持设置多个条件

并且,前面介绍的单个字段的匹配语句,都可以用在 bool 查询语句中进行组合

must 条件

代表必须匹配的条件,类似 SQL 的 and

# 语法
GET /{索引名}/_search
{
  "query": {
    "bool": {
      "must": [
         {匹配条件1},
         {匹配条件2},
         ...可以有N个匹配条件...
        ]
    }
  }
}

# 示例
GET /order_v2/_search
{
  "query": {
    "bool": {
      "must": [
          {
            "term": {
              "order_no":  "202003131209120999"
            }
          },
          {
            "term": {
              "shop_id":  123
            }
          }
        ]
    }
  }
}

# 等价SQL:select * from order_v2 where order_no="202003131209120999" and shop_id=123
must_not 条件

跟 must 作用相反,即不匹配

# 语法
GET /{索引名}/_search
{
  "query": {
    "bool": {
      "must_not": [
         {匹配条件1},
         {匹配条件2},
         ...可以有N个匹配条件...
        ]
    }
  }
}

# 示例:GET /order_v2/_search
{
  "query": {
    "bool": {
      "must_not": [
          {
            "term": {
              "shop_id": 1
            }
          },
          {
            "term": {
              "shop_id":  2
            }
          }
        ]
    }
  }
}

# 等价SQL:select * from order_v2 where shop_id != 1 and shop_id != 2
should 条件

匹配其中一个条件即可,类似 SQL 中的 or

# 语法
GET /{索引名}/_search
 {
   "query": {
     "bool": {
       "should": [
          {匹配条件1},
          {匹配条件2},
          …可以有N个匹配条件…
         ]
     }
   }
 }
 
# 示例
GET /order_v2/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "match": {
            "order_no": "202003131209120999"
          }
        },
        {
          "match": {
            "order_no": "22222222222222222"
          }
        }
      ]
    }
  }
}

# 等价SQL:select * from order_v2 where order_no="202003131209120999" or order_no="22222222222222222"

最后举一个嵌套的例子,深度嵌套能构造更复杂的查询条件,上述提到的查询关键词能任意嵌套

# 示例
GET /order_v2/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "bool": {
            "must": [
              {
                "term": {
                  "order_no": "2020031312091209991"
                }
              },
              {
                "range": {
                  "shop_id": {
                    "gte": 10,
                    "lte": 200
                  }
                }
              }
            ]
          }
        },
        {
          "terms": {
            "tag": [
              1,
              2,
              3,
              4,
              5,
              12
            ]
          }
        }
      ]
    }
  }
}

# 等价SQL:select * from order_v2 where (order_no='202003131209120999' and (shop_id>=10 and shop_id<=200)) or tag in (1,2,3,4,5)

全文搜索

全文搜索是ES的关键特性之一,平时我们使用SQL的like语句,搜索一些文本、字符串是否包含指定的关键词,但是如果两篇文章,都包含我们的关键词,具体那篇文章内容的相关度更高?这个SQL的like语句是做不到的,更别说like语句的性能问题了。

ES通过分词处理、相关度计算可以解决这个问题,ES内置了一些相关度算法,例如:TF/IDF算法,大体上思想就是,如果一个关键词在一篇文章出现的频率高,并且在其他文章中出现的少,那说明这个关键词与这篇文章的相关度很高。

分词的目的:

主要就是为了提取搜索关键词,理解搜索的意图,我们平时在百度搜索内容的时候,输入的内容可能很长,但不是每个字都对搜索有帮助,所以通过分词算法,我们输入的搜索关键词,会进一步分解成多个关键词,例如:搜索输入 "上海陆家嘴在哪里?",分词算法可能分解出:“上海、陆家嘴、哪里”,具体会分解出什么关键词,跟具体的分词算法有关。

默认全文搜索

默认情况下,使用全文搜索很简单,只要将字段类型定义为 text 类型,然后用 match 语句匹配即可。

ES对于 text 类型的字段,在插入数据的时候,会进行分词处理,然后根据分词的结果索引文档,当我们搜索 text 类型字段的时候,也会先对搜索关键词进行分词处理、然后根据分词的结果去搜索。

ES默认的分词器是 standard,对英文比较友好,例如:hello world 会被分解成 hello 和 world 两个关键词,如果是中文会分解成一个一个字,例如:上海大学 会分解成: 上、海、大、学。

在ES中,我们可以通过下面方式测试分词效果:

# 语法
GET /_analyze
{
  "text": "需要分词的内容",
  "analyzer": "分词器"
}

使用 standard 分词器对hello world进行分词,token 就是分解出料的关键词

GET /_analyze
{
  "text": "hello wolrd",
  "analyzer": "standard"
}

# 下面是输出结果
{
    "tokens": [
        {
            "token": "hello",
            "start_offset": 0,
            "end_offset": 5,
            "type": "<ALPHANUM>",
            "position": 0
        },
        {
            "token": "wolrd",
            "start_offset": 6,
            "end_offset": 11,
            "type": "<ALPHANUM>",
            "position": 1
        }
    ]
}

而对中文的分词结果:

GET /_analyze
{
  "text": "工业熊",
  "analyzer": "standard"
}

# 下面是输出结果
{
    "tokens": [
        {
            "token": "工",
            "start_offset": 0,
            "end_offset": 1,
            "type": "<IDEOGRAPHIC>",
            "position": 0
        },
        {
            "token": "业",
            "start_offset": 1,
            "end_offset": 2,
            "type": "<IDEOGRAPHIC>",
            "position": 1
        },
        {
            "token": "熊",
            "start_offset": 2,
            "end_offset": 3,
            "type": "<IDEOGRAPHIC>",
            "position": 2
        }
    ]
}

明显被切割成一个个的字了,中文关键词被分解成一个个字的主要问题就是搜索的结果可能不太准确。基本上包含分解出来单个字的内容都会被搜到,区别就是相关度的问题,会搜索到大量跟搜索目的没什么关系的数据。

中文分词器

ES 默认的 standard 分词器,对英文单词比较友好,对中文的分词效果不好,不过 ES 支持安装分词插件,增加新的分词器。

如何指定分词器

默认的分词器不满足需求,可以在定义索引映射的时候,指定 text 字段的分词器

# 示例
PUT /article
{
  "mappings": {
    "properties": {
      "title":   { 
          "type": "text",
          "analyzer": "smartcn"
      }
    }
  }
}

只要在定义 text 字段的时候,增加一个analyzer配置,指定分词器即可,这里指定的分词器是smartcn

目前中文分词器比较常用的有:smartcn 和 ik 两种

smartcn 分词器

smartcn 是目前 ES 官方推荐的中文分词插件,不过目前不支持自定义词库

插件安装方式:

{ES安装目录}/bin/elasticsearch-plugin install analysis-smartcn

安装完成后,重启 ES 即可,smartcn 分词器的名字就叫做 smartcn

# 示例
GET /_analyze
{
  "text": "工业熊",
  "analyzer": "smartcn"
}

# 下面是输出结果
{
    "tokens": [
        {
            "token": "工业",
            "start_offset": 0,
            "end_offset": 2,
            "type": "word",
            "position": 0
        },
        {
            "token": "熊",
            "start_offset": 2,
            "end_offset": 3,
            "type": "word",
            "position": 1
        }
    ]
}

ik 分词器

ik支持自定义扩展词库,有时候分词的结果不满足我们业务需要,需要根据业务设置专门的词库,词库的作用就是自定义一批关键词,分词的时候优先根据词库设置的关键词分割内容。

插件安装方式

// 在下面地址找到与使用ES版本一致的插件地址
https://github.com/medcl/elasticsearch-analysis-ik/releases

//  这里以我安装的7.3.2为例,插件地址为:
https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.3.2/elasticsearch-analysis-ik-7.3.2.zip

//  安装命令
{ES安装目录}/bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.3.2/elasticsearch-analysis-ik-7.3.2.zip

同样安装后重启 ES 即可

ik分词插件支持ik_smartik_max_word两种分词器

  • ik_smart - 粗粒度的分词
  • ik_max_word - 会尽可能的枚举可能的关键词,就是分词比较细致一些,会分解出更多的关键词
# 示例
GET /_analyze
{
  "text": "执子之手",
  "analyzer": "ik_max_word"
}

# 下面是输出结果
{
    "tokens": [
        {
            "token": "执子之手",
            "start_offset": 0,
            "end_offset": 4,
            "type": "CN_WORD",
            "position": 0
        },
        {
            "token": "之手",
            "start_offset": 2,
            "end_offset": 4,
            "type": "CN_WORD",
            "position": 1
        }
    ]
}

# ik_smart 的输出结果:
{
    "tokens": [
        {
            "token": "执子之手",
            "start_offset": 0,
            "end_offset": 4,
            "type": "CN_WORD",
            "position": 0
        }
    ]
}
ik 自定义词库

自定义扩展词库步骤如下:

1.创建配词库文件,以 dic 为扩展名,一行一个词条就行
例如:词库文件:{ES安装目录}/analysis-ik/config/demo.dic

上海大学
复旦大学
人民广场

2.创建或修改配置文件
配置文件路径:{ES安装目录}/analysis-ik/config/IKAnalyzer.cfg.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
	<comment>IK Analyzer 扩展配置</comment>
	<!--用户可以在这里配置自己的扩展字典 -->
	<entry key="ext_dict">{ES安装目录}/analysis-ik/config/demo.dic</entry>
	 <!--用户可以在这里配置自己的扩展停止词字典,没有可用删掉配置-->
	<entry key="ext_stopwords">custom/ext_stopword.dic</entry>
 	<!--用户可以在这里配置远程扩展字典,这个配置需要结合下面配置一起使用,没有可用删掉配置 -->
	<entry key="remote_ext_dict">location</entry>
 	<!--用户可以在这里配置远程扩展停止词字典,没有可用删掉-->
	<entry key="remote_ext_stopwords">http://xxx.com/xxx.dic</entry>
</properties>

3.重启 ES 即可
ik 新增扩展词库,支持热更新,不用重启 ES ,使用 remote_ext_dictremote_ext_stopwords 配置远程词库地址即可,词库地址需要返回两个头部(header),一个是Last-Modified,一个是 ETag,ES靠这两个头识别是否需要更新词库

排序

ES 的默认排序是根据相关分数排序,如果我们想根据查询结果中指定字段排序,需要使用 sort 处理

# 语法
GET /{索引名}/_search
{
  "query": {
    ...查询条件....
  },
  "sort": [
    {
      "{Field1}": { // 排序字段1
        "order": "desc" // 排序方向,asc或者desc, 升序和降序
      }
    },
    {
      "{Field2}": { // 排序字段2
        "order": "desc" // 排序方向,asc或者desc, 升序和降序
      }
    }
    ....多个排序字段.....
  ]
}

# 示例
GET /order_v2/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "order_no": {
        "order": "desc"
      }
    },
    {
      "shop_id": {
        "order": "asc"
      }
    }
  ]
}

# 等价SQL:select * from order_v2 order by order_no desc, shop_id asc

聚合分析

ES 中的聚合查询,类似 SQL 的 SUM/AVG/GOUNT/GROUP BY 分组查询,主要用于统计分析场景

ES 聚合查询流程

ES 聚合查询类似 SQL 的 GROUP BY,一般统计分析主要分为两个步骤:

  • 分组
  • 组内聚合

对查询的数据首先进行一轮分组,可以设置分组条件,组内聚合就是对组内的数据进行统计,例如:计算总数、求评价困滞等等。

核心概念

满足特定条件的文档的集合,叫做桶。桶就是一组数据的集合,对数据分组后得到的一组组数据,就是一个个的桶。

指标

指标是对文档进行统计计算的方式,又叫指标聚合

ES 聚合查询语法

先大致了解下ES聚合查询的基本语法结构

# 语法
{
  "aggregations" : {
    "<aggregation_name>" : {
        "<aggregation_type>" : {
            <aggregation_body>
        }
        [,"aggregations" : { [<sub_aggregation>]+ } ]? // 嵌套聚合查询,支持多层嵌套
    }
    [,"<aggregation_name_2>" : { ... } ]* // 多个聚合查询,每个聚合查询取不同的名字
  }
}

# 示例
GET /order/_search
{
    "size" : 0, // 设置size=0的意思就是,仅返回聚合查询结果,不返回普通query查询结果。
    "aggs" : { // 聚合查询语句的简写
        "popular_colors" : { // 给聚合查询取个名字,叫popular_colors
            "terms" : { // 聚合类型为,terms,terms是桶聚合的一种,类似SQL的group by的作用,根据字段分组,相同字段值的文档分为一组。
              "field" : "color" // terms聚合类型的参数,这里需要设置分组的字段为color,根据color分组
            }
        }
    }
}

# 等价SQL:select count(color) from order group by color
  • aggregations - 达标聚合查询语句,可以简写为:aggs
  • <aggregation_name> - 代表一个聚合计算的名字,可以随意命名,因为ES支持一次进行多次统计分析查询,后面需要通过这个名字在查询结果中找到我们想要的记过
  • <aggregation_type> - 聚合类型,代表我们想要怎样统计数据,主要有两大类聚合类型,桶聚合和指标聚合,这两类聚合又包括多种聚合类型,例如:指标聚合:sum、avg,桶聚合:terms、Date histogram等
  • <aggregation_body> - 聚合类型的参数,选择不同的聚合类型,有不同的参数
  • aggregation_name_2 - 代表其他聚合计算的名字,即可以一次进行多种类型的统计

指标聚合 metrics

ES 指标聚合,类似 SQL 的统计函数,指标聚合可以单独使用,也可以和桶聚合一起使用

常见的统计函数如下:

  • Value Count - 类似 SQL 的 count 函数,统计总数
  • Cardinality - 类似 SQL 的 count(DISTINCT 字段),统计不重复的数据总数
  • Avg - 求平均值
  • Sum - 求和
  • Max - 求最大值
  • Min - 求最小值
Value Count

值聚合,主要用于统计文档总数,类似 SQL 的 count 函数

# 示例
GET /sales/_search?size=0
{
  "aggs": {
    "types_count": { // 聚合查询的名字,随便取个名字
      "value_count": { // 聚合类型为:value_count
        "field": "type" // 计算type这个字段值的总数
      }
    }
  }
}

# 等价SQL:select count(type) from sales
Cardinality

基数聚合,也是用于统计文档的总数,跟 Value Count 区别是,基数聚合会去重,不会统计重复的值,类似 SQL 的 count(DISTINCT 字段) 用法

# 示例
POST /sales/_search?size=0
{
    "aggs" : {
        "type_count" : { // 聚合查询的名字,随便取一个
            "cardinality" : { // 聚合查询类型为:cardinality
                "field" : "type" // 根据type这个字段统计文档总数
            }
        }
    }
}

# 等价SQL:select count(DISTINCT type) from sales

提示:

前面提到基数聚合的作用等价于SQL的count(DISTINCT字段)的用法,
    其实不太准确,因为SQL的count统计结果是精确统计不会丢失精度,
    但是ES的cardinality基数聚合统计的总数是一个近似值,会有一定的误差,
    这么做的目的是为了性能,因为在海量的数据中精确统计总数是非常消耗性能的,
    但是很多业务场景不需要精确的结果,只要近似值,例如:统计网站一天的访问量,有点误差没关系。
Avg、Sum、Max、Min

求平均值、求和、求最大值和求最小值

# 示例
POST /exams/_search?size=0
{
  "aggs": {
    "avg_grade": { // 聚合查询名字,随便取一个名字
      "avg": { // 聚合查询类型为: avg
        "field": "grade" // 统计grade字段值的平均值
      }
    }
  }
}

POST /sales/_search?size=0
{
  "aggs": {
    "hat_prices": { // 聚合查询名字,随便取一个名字
      "sum": { // 聚合类型为:sum
        "field": "price" // 计算price字段值的总和
      }
    }
  }
}

POST /sales/_search?size=0
{
  "aggs": {
    "max_price": { // 聚合查询名字,随便取一个名字
      "max": { // 聚合类型为:max
        "field": "price" // 求price字段的最大值
      }
    }
  }
}

POST /sales/_search?size=0
{
  "aggs": {
    "min_price": { // 聚合查询名字,随便取一个
      "min": { // 聚合类型为: min
        "field": "price" // 求price字段值的最小值
      }
    }
  }
}
综合例子

实际应用中经常是先通过 query 查询,搜索索引中的数据,然后对 query 查询的结果进行统计分析

GET /sales/_search
{
  "size": 0, // size = 0,代表不想返回query查询结果,只要统计结果
  "query": { // 设置query查询条件,后面的aggs统计,仅对query查询结果进行统计
    "constant_score": {
      "filter": {
        "match": {
          "type": "hat"
        }
      }
    }
  },
  "aggs": { // 统计query查询结果, 默认情况如果不写query语句,则代表统计所有数据
    "hat_prices": { // 聚合查询名字,计算price总和
      "sum": {
        "field": "price"
      }
    },
    "min_price": { // 聚合查询名字,计算price最小值
      "min": { 
        "field": "price" 
      }
    },
    "max_price": { // 聚合查询名字,计算price最大值
      "max": { 
        "field": "price"
      }
    }
  }
}

分组统计 bucket

ES桶聚合,目的就是数据分组,组的概念和桶是等同的,在ES中统一使用桶这个术语

ES 桶聚合的作用和 SQL 的 group by 的作用是一样的,区别是 ES 支持更加强大的数据分组能力,SQL 只能根据字段的唯一值进行分组,分组的数量跟字段的唯一值的数量相等。

ES 常用的桶聚合如下:

  • Terms聚合 - 类似SQL的group by,根据字段唯一值分组
  • Histogram聚合 - 根据数值间隔分组,例如: 价格按100间隔分组,0、100、200、300等等
  • Date histogram聚合 - 根据时间间隔分组,例如:按月、按天、按小时分组
  • Range聚合 - 按数值范围分组,例如: 0-150一组,150-200一组,200-500一组。

桶聚合一般不单独使用,都是配合指标聚合一起使用,对数据分组之后肯定要统计桶内数据,在ES中如果没有明确指定指标聚合,默认使用Value Count指标聚合,统计桶内文档总数

Terms聚合

terms聚合的作用跟SQL中group by作用一样,都是根据字段唯一值对数据进行分组(分桶),字段值相等的文档都分到同一个桶内。

# 示例
GET /order/_search?size=0
{
  "aggs": {
    "shop": { // 聚合查询的名字,随便取个名字
      "terms": { // 聚合类型为: terms
        "field": "shop_id" // 根据shop_id字段值,分桶
      }
    }
  }
}

# 返回结果
{
    ...
    "aggregations" : {
        "shop" : { // 聚合查询名字
            "buckets" : [ // 桶聚合结果,下面返回各个桶的聚合结果
                {
                    "key" : "1", // key分桶的标识,在terms聚合中,代表的就是分桶的字段值
                    "doc_count" : 6 // 默认的指标聚合是统计桶内文档总数
                },
                {
                    "key" : "5",
                    "doc_count" : 3
                }
                ...
            ]
        }
    }
}

# 等价SQL:select shop_id, count(*) from order group by shop_id
Histogram聚合

histogram(直方图)聚合,主要根据数值间隔分组,使用histogram聚合分桶统计结果,通常用在绘制条形图报表

# 示例
POST /sales/_search?size=0
{
    "aggs" : {
        "prices" : { // 聚合查询名字,随便取一个
            "histogram" : { // 聚合类型为:histogram
                "field" : "price", // 根据price字段分桶
                "interval" : 50 // 分桶的间隔为50,意思就是price字段值按50间隔分组
            }
        }
    }
}

# 返回结果
{
    ...
    "aggregations": {
        "prices" : { // 聚合查询名字
            "buckets": [ // 分桶结果
                {
                    "key": 0.0, // 桶的标识,histogram分桶,这里通常是分组的间隔值
                    "doc_count": 1 // 默认按Value Count指标聚合,统计桶内文档总数
                },
                { 
                    "key": 50.0,
                    "doc_count": 1
                }
                ...
            ]
        }
    }
}
Date histogram聚合

类似histogram聚合,区别是Date histogram可以很好的处理时间类型字段,主要用于根据时间、日期分桶的场景

# 示例
POST /sales/_search?size=0
{
    "aggs" : {
        "sales_over_time" : { // 聚合查询名字,随便取一个
            "date_histogram" : { // 聚合类型为: date_histogram
                "field" : "date", // 根据date字段分组
                "calendar_interval" : "month", // 分组间隔:month代表每月、支持minute(每分钟)、hour(每小时)、day(每天)、week(每周)、year(每年)
                "format" : "yyyy-MM-dd" // 设置返回结果中桶key的时间格式
            }
        }
    }
}

# 返回结果
{
    ...
    "aggregations": {
        "sales_over_time": { // 聚合查询名字
            "buckets": [ // 桶聚合结果
                {
                    "key_as_string": "2015-01-01", // 每个桶key的字符串标识,格式由format指定
                    "key": 1420070400000, // key的具体字段值
                    "doc_count": 3 // 默认按Value Count指标聚合,统计桶内文档总数
                },
                {
                    "key_as_string": "2015-02-01",
                    "key": 1422748800000,
                    "doc_count": 2
                }
                ...
            ]
        }
    }
}
Range聚合

range聚合,按数值范围分桶

# 示例
GET /_search
{
    "aggs" : {
        "price_ranges" : { // 聚合查询名字,随便取一个
            "range" : { // 聚合类型为: range
                "field" : "price", // 根据price字段分桶
                "ranges" : [ // 范围配置
                    { "to" : 100.0 }, // 意思就是 price <= 100的文档归类到一个桶
                    { "from" : 100.0, "to" : 200.0 }, // price>100 and price<200的文档归类到一个桶
                    { "from" : 200.0 } // price>200的文档归类到一个桶
                ]
            }
        }
    }
}

# 返回结果
{
    ...
    "aggregations": {
        "price_ranges" : { // 聚合查询名字
            "buckets": [ // 桶聚合结果
                {
                    "key": "*-100.0", // key可以表达分桶的范围
                    "to": 100.0, // 结束值
                    "doc_count": 2 // 默认按Value Count指标聚合,统计桶内文档总数
                },
                {
                    "key": "100.0-200.0",
                    "from": 100.0, // 起始值
                    "to": 200.0, // 结束值
                    "doc_count": 2
                }
                ...
            ]
        }
    }
}
综合例子
GET /cars/_search
{
    "size": 0, // size=0代表不需要返回query查询结果,仅仅返回aggs统计结果
    "query" : { // 设置查询语句,先赛选文档
        "match" : {
            "make" : "ford"
        }
    },
    "aggs" : { // 然后对query搜索的结果,进行统计
        "colors" : { // 聚合查询名字
            "terms" : { // 聚合类型为:terms 先分桶
              "field" : "color"
            },
            "aggs": { // 通过嵌套聚合查询,设置桶内指标聚合条件
              "avg_price": { // 聚合查询名字
                "avg": { // 聚合类型为: avg指标聚合
                  "field": "price" // 根据price字段计算平均值
                }
              },
              "sum_price": { // 聚合查询名字
                "sum": { // 聚合类型为: sum指标聚合
                  "field": "price" // 根据price字段求和
                }
              }
            }
        }
    }
}

多桶排序

类似terms、histogram、date_histogram这类桶聚合都会动态生成多个桶,如果生成的桶特别多,我们如何确定这些桶的排序顺序,如何限制返回桶的数量。

默认情况,ES会根据doc_count文档总数,降序排序。ES桶聚合支持两种方式排序

  • 内置排序
  • 按度量指标排序
内置排序

内置排序参数

  • _count - 按文档数排序。对 terms 、 histogram 、 date_histogram 有效
  • _term - 按词项的字符串值的字母顺序排序。只在 terms 内使用
  • _key - 按每个桶的键值数值排序, 仅对 histogram 和 date_histogram 有效
# 示例
GET /cars/_search
{
    "size" : 0,
    "aggs" : {
        "colors" : { // 聚合查询名字,随便取一个
            "terms" : { // 聚合类型为: terms
              "field" : "color", 
              "order": { // 设置排序参数
                "_count" : "asc"  // 根据_count排序,asc升序,desc降序
              }
            }
        }
    }
}
按度量排序

通常情况下,我们根据桶聚合分桶后,都会对桶内进行多个维度的指标聚合,所以我们也可以根据桶内指标聚合的结果进行排序。

# 示例
GET /cars/_search
{
    "size" : 0,
    "aggs" : {
        "colors" : { // 聚合查询名字
            "terms" : { // 聚合类型: terms,先分桶
              "field" : "color", // 分桶字段为color
              "order": { // 设置排序参数
                "avg_price" : "asc"  // 根据avg_price指标聚合结果,升序排序。
              }
            },
            "aggs": { // 嵌套聚合查询,设置桶内聚合指标
                "avg_price": { // 聚合查询名字,前面排序引用的就是这个名字
                    "avg": {"field": "price"} // 计算price字段平均值
                }
            }
        }
    }
}
限制返回桶的数量

如果分桶的数量太多,可以通过给桶聚合增加一个size参数限制返回桶的数量。

# 示例
GET /_search
{
    "aggs" : {
        "products" : { // 聚合查询名字
            "terms" : { // 聚合类型为: terms
                "field" : "product", // 根据product字段分桶
                "size" : 5 // 限制最多返回5个桶
            }
        }
    }
}

标题:jxc321
作者:jxc
地址:https://jxc321.com/articles/2023/06/30/1688123616005.html