美文网首页
Elasticsearch

Elasticsearch

作者: zhemehao819 | 来源:发表于2022-12-16 16:57 被阅读0次

一、Elasticsearch

1. ES简介

Elasticsearch 是一个分布式、RESTful 风格的搜索和数据分析引擎, 国内简称ES,Elasticsearch是用java开发的,底层基于Lucene, Lucene是一种全文检索的搜索库,直接使用Lucene还是比较麻烦的,Elasticsearch在Lucene的基础上开发了一个强大的搜索引擎。

ELK

其实代表了Elasticsearch + Logstash + Kibana 三套软件,他们的作用如下:

  • Elasticsearch - 前面简介提到过,解决海量数据搜索问题。
  • Logstash - 解决数据同步问题,因为我们数据一般存储在Mysql之类的数据库中,需要将数据导入到ES中,Logstash就支持数据同步、数据过滤、转换功能。
  • Kibana - Elasticsearch数据可视化支持,例如:通过各种图表展示ES的查询结果,也可以在Kibana通过ES查询语句分析数据,起到类似ES Web后台的作用。

应用场景

各种搜索场景,例如:订单搜索、商品搜索。
日志处理和分析,例如:通过ELK搭建日志处理和分析方案。
地理空间数据搜索,例如:查询距离最近的店铺、查询某个空间范围内的店铺。

2. ES 存储结构

我们都知道MYSQL的数据模型由数据库、表、字段、字段类型组成,自然ES也有自己的一套存储结构,下面先介绍ES存储相关的概念,然后跟MYSQL做一下对比方便大家理解。

基本概念

2.1. index(索引)

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

2.2. type(文档类型)

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

既然新的版本文档类型没什么作用了,那么index(索引)就类似mysql的表的概念,ES没有数据库的概念了。

提示:在Elasticsearch7.0以后的版本,已经废弃文档类型了,如果大家不是接手老的项目,可以不必理会文档类型,可以直接将index(索引)类比Mysql的表。

2.3. Document(文档)

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

简单的说在ES中,文档指的就是一条JSON数据。Elasticsearch中文档使用json格式存储,因此存储上比Mysql要灵活的多,Elasticsearch支持任意格式的json数据。

例如:一个订单数据,我们可以将复杂的Json结构保存到Elasticsearch中, mysql的就无法这样存储数据。

{
    "id": 12,
    "status": 1,
    "total_price": 100,
    "create_time": "2019-12-12 12:20:22",
    "user" : { // 嵌套json对象
        "id" : 11,
        "username": "tizi365",
        "phone": "13500001111",
        "address" : "上海长宁区001号"
    }
}

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

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

例子:在同一个索引存中,存储格式完全不一样的文档数据

{"id":1, "username":"tizi365"}
{"id":1, "title":"好看的包包", "price": 30}
{"domain":"www.tizi365.com", "https": true}

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

2.4. Field(文档字段)

文档由多个json字段(Field)组成, 这里的字段类似mysql中表的字段。
当然Elasticsearch中字段也有类型的,下面是常用的字段类型:

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

提示:Elasticsearch支持动态映射,我们可以不必预先定义文档的json结构和对应的字段类型,Elasticsearch会自动推断字段的类型。

2.5. mapping (映射)

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

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

类比MYSQL存储结构

Elasticsearch存储结构 MYSQL存储结构
index(索引)
文档 行,一行数据
Field(字段) 表字段
mapping (映射) 表结构定义

3. ES 安装配置和调试

3.1.依赖JDK版本

JDK 1.8以上,没有安装jdk可以安装jdk,配置好Java环境变量。

3.2.安装和启动

安装非常简单,只要下载压缩包,解压缩即可,windows和Linux,Macos系统的区别,只是下载不同版本的压缩包,其他步骤都一样。

解压缩

Windows系统直接右键解压缩即可。
Linux/macOS系统解压缩命令:

tar -zxvf elasticsearch-7.5.1-linux-x86_64.tar.gz

启动

Linux 和 macOS启动方式:

# 切换到安装目录
cd elasticsearch-7.5.1
# 启动
./bin/elasticsearch

Windows启动方式,Windows跟Linux的区别就是启动程序扩展名是bat

# 打开CMD切换到安装目录
cd elasticsearch-7.5.1\bin
# 启动
.\elasticsearch.bat

启动后,访问http://localhost:9200/ ,如果可以正常访问,就说明安装成功了。

3.3.通过Restful api调试Elasticsearch

elasticsearch是以http Restful api的形式提供接口,我们要操作ES,只要调用http接口就行,ES的默认端口是9200, 因此上面例子可以直接通过浏览器访问ES的接口。

大家都知道Http Restful api风格的请求动作,主要包括:GET、POST、PUT、DELETE四种,直接通过浏览器访问,发送的是GET请求动作,后面的三种动作,不方便用浏览器模拟,除非你自己写程序调用,但是我们平时测试,又不想写代码,所以建议使用curl命令、或者postman可视化工具发送http请求。

3.4.通过Kibana可视化操作Elasticsearch

调试的时候,除了直接通过Restful api操作ES,我们也可以使用Kibana工具操作ES,Kibana以Web后台的形式提供了一个可视化操作ES的系统,支持根据ES数据绘制图表,支持ES查询语法自动补全等高级特性。

3.4.1.安装Kibana

Kibana也是java开发的,安装启动非常简单,只要下载安装包,解压缩后启动即可。

下载安装包
解压缩

Windows直接右键解压缩即可
Linux/macOS系统解压缩命令:

tar -zxvf kibana-7.5.1-linux-x86_64.tar.gz
启动Kibana

Linux 和 macOS启动方式:

# 切换到安装目录
cd kibana-7.5.1-darwin-x86_64
# 启动
./bin/kibana

Windows类似,首先打开CMD,切换到安装目录,运行.\bin\kibana.bat即可
启动后,访问 http://localhost:5601,就可以进入kibana, 首次访问,因为没有数据,会显示如下窗口。

4. ES文档curd

4.1.什么是文档

在Elasticsearch中,文档其实就是一条JSON数据。
JSON数据格式可以非常复杂,也可以很简单。
JSON文档的例子:

{
    "id": 12,
    "status": 1,
    "total_price": 100,
    "create_time": "2019-12-12 12:20:22",
    "user" : { // 嵌套json对象
        "id" : 11,
        "username": "tizi365",
        "phone": "13500001111",
        "address" : "上海长宁区001号"
    }
}

前面章节介绍过,在ES中索引类似MYSQL表的概念,索引包含多个文档数据。

提示: ES 索引不要求,同一个索引中的所有JSON文档的格式都一样,甚至可以在同一个索引中保存完全不一样格式的JSON数据,当然不推荐这么做,建议同一个索引仅保存格式相同的数据,例如:订单索引,仅保存订单数据,不要保存其他业务数据。

4.2.文档元数据

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

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

常见的元数据如下:

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

下面是从ES查询出来的一条文档的例子:

{
  "_index" : "order",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1, // 老ES版本的文档版本号,最新版本已经不使用了
  "_seq_no" : 0, // 新ES版本的文档版本号
  "_primary_term" : 1, // 主分区id
  "_source" : { // _source字段保存的就是我们插入到ES中的JSON数据
    "id" : 1,
    "status" : 1,
    "total_price" : 100,
    "create_time" : "2019-12-12 12:20:22",
    "user" : {
      "id" : 11,
      "username" : "tizi365",
      "phone" : "13500001111",
      "address" : "上海长宁区001号"
    }
  }
}

4.3.插入文档

在Elasticsearch插入一个JSON文档,又叫索引文档, 注意这里的索引跟前面提到的文档所属的索引名,不是一回事,很晕吧,其实主要翻译问题,我们将数据插入到ES的过程,其实就是创建索引的过程,所以插入文档,也叫做索引文档,这里索引是动词, 而文档属于哪个索引(index),这里的索引代表一个分类,有数据库的概念,是个名词。

搞不清楚也没关系,知道索引文档的意思,其实就是往ES插入数据就行。
插入文档的语法:

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

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

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

提示:教程的语法说明,没有给出ES的接口地址,如果你想通过curl命令,或者postman测试的话,只要加上ES接口地址即可,ES默认地址是http://localhost:9200/

例子:

插入一条JSON数据,到order索引中,文档类型是_doc, 文档id是1

PUT /order/_doc/1
{
    "id": 1,
    "status": 1,
    "total_price": 100,
    "create_time": "2019-12-12 12:20:22",
    "user" : { 
        "id" : 11,
        "username": "tizi365",
        "phone": "13500001111",
        "address" : "上海长宁区001号"
    }
}

提示:将例子代码,直接贴到Kibana的console中,就可以执行, 在console页面右侧可以直接看到执行结果。

image

提示:ES支持动态映射,可以根据我们插入的JSON数据,自动推测出JSON字段的类型,所以我们不需要在ES中提前定义JSON的格式。

4.4.查询一个文档

根据文档ID查询一个文档。
语法:

GET /{index}/{type}/{id}

例子:

GET /order/_doc/1

返回:

{
  "_index" : "order",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1,
  "_seq_no" : 0,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "id" : 1,
    "status" : 1,
    "total_price" : 100,
    "create_time" : "2019-12-12 12:20:22",
    "user" : {
      "id" : 11,
      "username" : "tizi365",
      "phone" : "13500001111",
      "address" : "上海长宁区001号"
    }
  }
}

4.5.更新文档

4.5.1 更新整个文档

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

4.5.2 文档局部更新

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

语法:

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

例子:
更新order索引中,文档id等于1的json文档,更新status和total_price两个字段

POST /order/_update/1
{
  "doc":{
    "status":3,
    "total_price":200
  }
}

提示:虽然说elasticsearch支持通过Api更新文档,但是ES底层文档是不可变的,每次修改文档,本质上是创建一个新的文档,然后把老的文档标记成删除。

4.6.删除文档

删除语法:

DELETE /{index}/{type}/{id}

例子:

DELETE /order/_doc/1

5. ES基于乐观锁的并发控制

如果我们同时修改一个文档,Elasticsearch怎么保证文档的原子性呢?一般类似Mysql 都是通过加锁确保数据原子性,在Elasticsearch中主要是通过乐观锁确保文档的原子性。

Elasticsearch的乐观锁是基于版本号实现的,前面的章节介绍文档CRUD的时候,提到文档的元数据中_seq_no、version,都代表当前文档的版本号,每次更新、删除文档的时候,版本号都会加1,ES就是借着这个版本号实现乐观锁。

提示:本教程使用的Elasticsearch7.5.x版本,已经不再使用_version字段作为乐观锁判断的依据,主要使用_seq_no作为版本号,结合_primary_term字段实现乐观锁控制。

下面介绍如何使用Elasticsearch的乐观锁机制确保数据的原子性。

首先插入一个文档

PUT /order/_doc/2
{
"id": 1,
"status": 1,
"total_price": 100,
"create_time": "2019-12-12 12:20:22"
}

然后我们查询下插入的文档,观察下版本号是多少?

GET /order/_doc/2
返回
{
"_index" : "order",
"_type" : "_doc",
"_id" : "2",
"_version" : 1,
"_seq_no" : 6, // 版本号是6
"_primary_term" : 1, // 所在主分区是1
"found" : true,
"_source" : {
"id" : 1,
"status" : 1,
"total_price" : 100,
"create_time" : "2019-12-12 12:20:22"
}
}
我们现在以指定的版本号更新数据

PUT /order/_doc/2?if_seq_no=6&if_primary_term=1
{
"id": 1,
"status": 2,
"total_price": 300,
"create_time": "2019-12-12 12:20:22"
}
提示:注意观察url,多了两个参数,if_seq_no参数,指定了当前文档的版本号是6,if_primary_term参数指定文档所在的主分区,如果ES发现文档当前的版本号跟我们指定的版本号相等,就会更新文档,否则直接忽略(报错)。
重复执行上面的语句,ES就会输出下面的错误提示:版本冲突 (version conflict)。

{
"error": {
"root_cause": [
{
"type": "version_conflict_engine_exception",
"reason": "[2]: version conflict, required seqNo [6], primary term [1]. current document has seqNo [7] and primary term [1]",
"index_uuid": "CRQJQH4uTA2ffetowYCMNA",
"shard": "0",
"index": "order"
}
],
"type": "version_conflict_engine_exception",
"reason": "[2]: version conflict, required seqNo [6], primary term [1]. current document has seqNo [7] and primary term [1]",
"index_uuid": "CRQJQH4uTA2ffetowYCMNA",
"shard": "0",
"index": "order"
},
"status": 409
}
我们可以通过版本号实现乐观锁,控制出现并发修改文档的时候,确保数据不会出问题。

6. ES数据类型和映射

映射(mapping)这个概念, 类似数据库中的表结构定义 (schema) ,描述了文档包含哪些字段 、每个字段的数据类型是什么。

6.1 ES数据基础类型

下面是ES支持的数据类型:

  • 字符串:主要包括: text和keyword两种类型,keyword代表精确值不会参与分词,text类型的字符串会参与分词处理。
  • 数值:包括: long, integer, short, byte, double, float
  • 布尔值:boolean
  • 时间:date
  • 数组:数组类型不需要专门定义,只要插入的字段值是json数组就行。
  • GEO类型:主要涉及地理信息检索、多边形区域的表达,后面GEO相关的章节单独讲解

提示:text类型,支持全文搜索,因为text涉及分词,所以可以配置使用什么分词器,尤其涉及中文分词,这些涉及全文搜索的内容,请参考后面的全文搜索章节。

6.2.精确值 & 全文类型

精确值通常指的就是数值类型、时间、布尔值、字符串的keyword类型,这些不可分割的数据类型,精确值搜索效率比较高,精确值匹配类似MYSQL中根据字段搜索,例如:拿一个手机号去搜索数据,对于每一个文档的手机号字段,要么相等,要么不等,不会做别的计算。

全文类型,指的就是text类型,会涉及分词处理,存储到ES中的数据不是原始数据,是一个个关键词。

例如:我们有一个title字段,数据类型是text, 我们插入"上海复旦大学"这个字符串,经过分词处理,可能变成:"上海"、"复旦大学"、"大学" 这些关键词,然后根据这些关键词建倒排索引。

提示:实际项目中,如果不需要模糊搜索的字符类型,可以选择keyword类型,例如:手机号、email、微信的openid等等,如果选text类型,可能会出现搜出一大堆相似的数据,而且不是精确的数据。
全文搜索后面的章节单独介绍。

6.3.自动映射 (dynamic mapping)

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

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

提示:实际项目中,我们通常会预先定义好ES的映射规则,也就是类似MYSQL一样,提前定义好表结构。

6.4.自定义文档的数据类型

语法:

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

例子:
创建一个订单索引,索引名字order
创建ES索引:

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

6.5.查询索引的映射规则

如果想知道索引的映射规则(索引结构)是怎么样的,可以通过下面语法查询。

语法:

GET /索引名/_mapping
{
  
}

例子:

查询上面的订单索引的映射规则

GET /order/_mapping
{
}

输出:

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

JSON嵌套类型定义

例如下面json,我们如何在ES中定义。

{
    "order_no" : "20200313120000123123",
    "shop_id" : 12,
    "user" : {
        "id" : 100,
        "nickname" : "dacui",
    }
}

这里user属性是一个Object类型,json类型本身支持这种通过对象无线嵌套的结构。

ES的映射也支持Object类型,嵌套json的定义如下:

PUT /order_v2
{
  "mappings": {
    "properties": { // 第一层json属性定义
      "order_no":  { "type": "keyword"  }, 
      "shop_id":    { "type": "integer" },  
      "user": { // user属性是Object类型,可以单独定义属性类型
        "properties" : { // 第二层user对象的属性定义
            "id":    { "type": "integer" },
            "nickname":  { "type": "text" }
        }
      }
    }
  }
}

通过上面例子,如果属性是Object类型只要使用properties单独定义即可支持多层Json对象嵌套。

二、查询

1. ES 查询语法

通过ES查询表达式(Query DSL),可以实现复杂的查询功能,ES查询表达式主要由JSON格式编写,可以灵活的组合各种查询语句。

1.1.查询基本语法结构

GET /{索引名}/_search
{
    "from" : 0,  // 返回搜索结果的开始位置
    "size" : 10, // 分页大小,一次返回多少数据
    "_source" :[ ...需要返回的字段数组... ],
    "query" : { ...query子句... },
    "aggs" : { ..aggs子句..  },
    "sort" : { ..sort子句..  }
}

{索引名},支持支持一次搜索多个索引,多个索引使用逗号分隔,例子:

GET /order1,order2/_search

按前缀匹配索引名:

GET /order*/_search

搜索索引名以order开头的索引。

查询结果格式:

当我们执行查询语句,返回的JSON数据格式如下

{
  "took" : 5, // 查询消耗时间,单位毫秒 
  "timed_out" : false, // 查询是否超时
  "_shards" : { // 本次查询参与的ES分片信息,查询中参与分片的总数,以及这些分片成功了多少个失败了多少个
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : { // hits字段包含我们搜索匹配的结果
    "total" : { // 匹配到的文档总数
      "value" : 1, // 找到1个文档
      "relation" : "eq"
    },
    "max_score" : 1.0, // 匹配到的最大分值
    "hits" : [ 
         // 这里就是我们具体的搜索结果,是一个JSON文档数组
    ]
  }
}

1.2.query子句

query子句主要用来编写类似SQL的Where语句,支持布尔查询(and/or)、IN、全文搜索、模糊匹配、范围查询(大于小于)。

1.3.aggs子句

aggs子句,主要用来编写统计分析语句,类似SQL的group by语句

1.4.sort子句

sort子句,用来设置排序条件,类似SQL的order by语句

1.5.ES查询分页

ES查询的分页主要通过from和size参数设置,类似MYSQL 的limit和offset语句

例子:

GET /order_v2/_search
{
  "from": 0,
  "size": 20, 
  "query": {
    "match_all": {}
  }
}

查询所有数据,从第0条数据开始,一次返回20条数据。

1.6. _source

_source用于设置查询结果返回什么字段,类似Select语句后面指定字段。

例子:

GET /order_v2/_search
{
  "_source": ["order_no","shop_id"], 
  "query": {
    "match_all": {}
  }
}

仅返回,order_no和shop_id字段。

2. ES query查询语法

ES 的query子句的语法,query子句主要用于编写查询条件,类似SQL中的where语句。

2.1.匹配单个字段

通过match实现全文搜索,全文搜索的后面有单独的章节讲解,这里大家只要知道简单的用法就可以。

语法:

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

说明:

  • {FIELD} - 就是我们需要匹配的字段名
  • {TEXT} - 就是我们需要匹配的内容

例子:

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

article索引中,title字段匹配ES教程的所有文档。
如果title字段的数据类型是text类型,搜索关键词会进行分词处理。

2.2.精确匹配单个字段

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

通过term实现精确匹配语法:

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

说明:

  • {FIELD} - 就是我们需要匹配的字段名
  • {VALUE} - 就是我们需要匹配的内容,除了TEXT类型字段以外的任意类型。

例子:

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

搜索订单号order_no = "202003131209120999"的文档。

类似SQL语句:
select * from order_v2 where order_no = "202003131209120999"

2.3.通过terms实现SQL的in语句

如果我们要实现SQL中的in语句,一个字段包含给定数组中的任意一个值就匹配。

terms语法:

GET /order_v2/_search
{
  "query": {
    "terms": {
      "{FIELD}": [
        "{VALUE1}",
        "{VALUE2}"
      ]
    }
  }
}

说明:

  • {FIELD} - 就是我们需要匹配的字段名
  • {VALUE1}, {VALUE2} .... {VALUE N} - 就是我们需要匹配的内容,除了TEXT类型字段以外的任意类型。

例子:

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

搜索order_v2索引中,shop_id字段,只要包含[123,100,300]其中一个值,就算匹配。

类似SQL语句:
select * from order_v2 where shop_id in (123,100,300)

2.4.范围查询

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

range语法:

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

参数说明:

  • {FIELD} - 字段名
  • gte范围参数 - 等价于>=
  • lte范围参数 - 等价于 <=
  • 范围参数可以只写一个,例如:仅保留 "gte": 10, 则代表 FIELD字段 >= 10

范围参数如下:

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

例子1:

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

查询order_v2索引中,shop_id >= 10 且 shop_id <= 200的文档

类似SQL:
select * from order_v2 where shop_id >= 10 and shop_id <= 200

2.5.bool组合查询

前面的例子都是设置单个字段的查询条件,如果需要编写类似SQL的Where语句,组合多个字段的查询条件,可以使用bool语句。

2.5.1. bool查询基本语法结构

在ES中bool查询就是用来组合布尔查询条件,布尔查询条件,就是类似SQL中的and (且)、or (或)。

在SQL中,我们需要and和or,还有括号来组合查询条件,在ES中使用bool查询可用做到同样的效果。

bool语法结构:

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

可以任意选择must、must_not和should条件的参数都是一个数组,意味着他们都支持设置多个条件。

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

2.5.2. must条件

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

语法:

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

例子1:

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

这里的Must条件,使用了term精确匹配。

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

2.5.3. 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

2.5.3. 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"

2.5.4. bool综合例子

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)

3. ES 全文搜索

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

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

分词的目的:

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

3.1.默认全文搜索

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

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

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

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

语法:

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

例如:

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

使用standard分词器,对hello world进行分词,下面是输出结果:

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

token就是分解出来的关键词。

下面是对中文分词的结果:

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
    },
    {
      "token" : "学",
      "start_offset" : 3,
      "end_offset" : 4,
      "type" : "<IDEOGRAPHIC>",
      "position" : 3
    }
  ]
}

明显被切割成一个个的字了。
中文关键词被分解成一个个的字的主要问题就是搜索结果可能不太准确。

例如:
搜索:上海大学
分词结果:上、海、大、学

下面的内容都会被搜到:

上海大学
海上有条船
上海有好吃的
这条船又大又好看

基本上包含这四个字的内容都会被搜到,区别就是相关度的问题,这里除了第一条是相关的,后面的内容基本上跟搜索目的没什么关系。

3.2.中文分词器

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

3.2.0. 如何指定analyzer

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

例子:

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

只要在定义text字段的时候,增加一个analyzer配置,指定分词器即可,这里指定的分词器是smartcn,后面会介绍怎么安装smartcn插件。

目前中文分词器比较常用的有:smartcn和ik两种, 下面分别介绍这两种分词器。

3.2.1. smartcn分词器

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

插件安装方式:

{ES安装目录}/bin/elasticsearch-plugin install analysis-smartcn
安装完成后,重启ES即可。

smartcn的分词器名字就叫做:smartcn

3.2.2. smartcn中文分词效果

测试分词效果:

GET /_analyze
{
  "text": "红烧牛肉面",
  "analyzer": "smartcn"
}

输出:

{
  "tokens" : [
    {
      "token" : "红烧",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "牛肉面",
      "start_offset" : 2,
      "end_offset" : 5,
      "type" : "word",
      "position" : 1
    }
  ]
}

红烧牛肉面,被切割为: 红烧、牛肉面 两个词。

3.2.3. ik分词器

ik支持自定义扩展词库,有时候分词的结果不满足我们业务需要,需要根据业务设置专门的词库,词库的作用就是自定义一批关键词,分词的时候优先根据词库设置的关键词分割内容,例如:词库中包含 “上海大学” 关键词,如果对“上海大学在哪里?”进行分词,“上海大学” 会做为一个整体被切割出来。

3.2.4. ik中文分词效果

ik分词插件支持 ik_smart 和 ik_max_word 两种分词器

  • ik_smart - 粗粒度的分词
  • ik_max_word - 会尽可能的枚举可能的关键词,就是分词比较细致一些,会分解出更多的关键词

3.2.5. ik自定义词库

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

一、创建配词库文件,以dic作为扩展名

例如:
词库文件:{ES安装目录}/analysis-ik/config/demo.dic

上海大学
复旦大学
人民广场
一行一个词条即可

提示:config目录不存在创建一个即可。
二、创建或者修改配置文件

配置文件路径:{ES安装目录}/analysis-ik/config/IKAnalyzer.cfg.xml
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>

三、重启ES即可

提示:Ik新增扩展词库,支持热更新,不用重启ES,使用remote_ext_dict和remote_ext_stopwords配置远程词库地址即可,词库地址需要返回两个头部(header),一个是 Last-Modified,一个是 ETag,ES靠这两个头识别是否需要更新词库,不了解这两个HTTP头,可以搜一下。

4. ES 排序

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

sort语法:

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

sort子句支持多个字段排序,类似SQL的order by。

例子1:

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

查询order_v2索引的所有结果,结果根据order_no字段降序,order_no相等的时候,再根据shop_id字段升序排序。

类似SQL:

select * from order_v2 order by order_no desc, shop_id asc

例子2:

GET /order_v2/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "user.id": { // 嵌套json对象,使用 点 连接字段名即可
        "order": "desc"
      }
    }
  ]
}

三、聚合分析

1. ES聚合查询(aggs)基本概念

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

下面先介绍ES聚合查询的核心流程和核心概念。

1.1 ES聚合查询流程

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

  • 分组
  • 组内聚合

对查询的数据首先进行一轮分组,可以设置分组条件,例如:新生入学,把所有的学生按专业分班,这个分班的过程就是对学生进行了分组。

组内聚合,就是对组内的数据进行统计,例如:计算总数、求平均值等等,接上面的例子,学生都按专业分班了,那么就可以统计每个班的学生总数, 这个统计每个班学生总数的计算,就是组内聚合计算。

提示:分组类似SQL的group by语句设定的条件,组内聚合,就是在select编写的avg、sum、count统计函数;熟悉SQL语句都知道sum、count这些统计函数不一定要跟group by语句配合使用,单独使用统计函数等同于将所有数据分成一个组,直接对所有数据进行统计。

1.2 核心概念

通过上面的聚合查询流程,下面是ES聚合的核心概念就很容易理解了

1.2.1 桶

满足特定条件的文档的集合,叫做桶。

桶的就是一组数据的集合,对数据分组后,得到一组组的数据,就是一个个的桶。

提示:桶等同于组,分桶和分组是一个意思,ES使用桶代表一组相同特征的数据。

ES中桶聚合,指的就是先对数据进行分组,ES支持多种分组条件,例如:支持类似SQL的group by根据字段分组,当然ES比SQL更强大,支持更多的分组条件,以满足各种统计需求。

1.2.2 指标

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

桶内聚合,说的就是先对数据进行分组(分桶),然后对每一个桶内的数据进行指标聚合。

说白了就是,前面将数据经过一轮桶聚合,把数据分成一个个的桶之后,我们根据上面计算指标对桶内的数据进行统计。

常用的指标有:SUM、COUNT、MAX等统计函数。

借助SQL的统计语句理解桶和指标:

SELECT COUNT(*) 
FROM order
GROUP BY shop_id 

说明:

  • COUNT(*) 相当于指标, 也叫统计指标。
  • GROUP BY shop_id 相当于分桶的条件,也可以叫分组条件,相同shop_id的数据都分到一个桶内。

这条SQL语句的作用就是统计每一个店铺的订单数,所以SQL统计的第一步是根据group by shop_id这个条件,把shop_id(店铺ID)相同的数据分到一个组(桶)里面,然后每一组数据使用count(*)统计函数(指标)计算总数,最终得到每一个店铺的订单总数,ES也是类似的过程。

1.3 ES聚合查询语法

大家可以先大致了解下ES聚合查询的基本语法结构,后面的章节会介绍具体的用法。

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

说明:

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

下面看个简单的聚合查询的例子:

假设存在一个order索引,存储了每一笔汽车销售订单,里面包含了汽车颜色字段color.

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

上面使用了terms桶聚合,而且没有明确指定指标聚合函数,默认使用的是Value Count聚合指标统计文档总数, 整个统计的意思是统计每一种汽车颜色的销量。

等价SQL如下:

select count(color) from order group by color

查询结果如下

{
...
   "hits": { // 因为size=0,所以query查询结果为空
      "hits": [] 
   },
   "aggregations": { // 聚合查询结果
      "popular_colors": { // 这个就是popular_colors聚合查询的结果,这就是为什么需要给聚合查询取个名字的原因,如果有多个聚合查询,可以通过名字查找结果
         "buckets": [ // 因为是桶聚合,所以看到返回一个buckets数组,代表分组的统计情况,下面可以看到每一种颜色的销量情况
            {
               "key": "red", 
               "doc_count": 4 // 红色的汽车销量为4
            },
            {
               "key": "blue",
               "doc_count": 2
            },
            {
               "key": "green",
               "doc_count": 2
            }
         ]
      }
   }
}

2. ES指标聚合

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

常用的统计函数如下:

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

下面分别介绍Elasticsearch常用统计函数的用法。

2.1 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

返回结果:

{
    ...
    "aggregations": {
        "types_count": { // 聚合查询的名字
            "value": 7 // 统计结果
        }
    }
}

2.2 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

返回结果:

{
    ...
    "aggregations" : {
        "type_count" : { // 聚合查询的名字
            "value" : 3 // 统计结果
        }
    }
}

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

2.3 Avg

求平均值

例子:

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

返回结果:

{
    ...
    "aggregations": {
        "avg_grade": { // 聚合查询名字
            "value": 75.0 // 统计结果
        }
    }
}

2.4 Sum

求和计算

例子:

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

返回结果:

{
    ...
    "aggregations": {
        "hat_prices": { // 聚合查询名字
           "value": 450.0 // 统计结果
        }
    }
}

2.5 Max

求最大值

例子:

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

返回结果:

{
    ...
    "aggregations": {
        "max_price": { // 聚合查询名字
            "value": 200.0 // 最大值
        }
    }
}

2.6 Min

求最小值

例子:

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

返回:

{
    ...

    "aggregations": {
        "min_price": { // 聚合查询名字
            "value": 10.0 // 最小值
        }
    }
}

2.7 综合例子

前面的例子,仅仅介绍聚合指标单独使用的情况,实际应用中经常先通过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"
      }
    }
  }
}

返回:

{
    ...
    "aggregations": {
        "hat_prices": { // 求和
           "value": 450.0
        },
        "min_price": { // 最小值
            "value": 10.0 
        },
        "max_price": { // 最大值
            "value": 200.0 
        }
    }
}

3. 分组聚合查询

Elasticsearch桶聚合,目的就是数据分组,先将数据按指定的条件分成多个组,然后对每一个组进行统计。 组的概念跟桶是等同的,在ES中统一使用桶(bucket)这个术语。

ES桶聚合的作用跟SQL的group by的作用是一样的,区别是ES支持更加强大的数据分组能力,SQL只能根据字段的唯一值进行分组,分组的数量跟字段的唯一值的数量相等,例如: group by 店铺id, 去掉重复的店铺ID后,有多少个店铺就有多少个分组。

ES常用的桶聚合如下:

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

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

3.1 Terms聚合

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

例子:

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

等价SQL:

select shop_id, count(*) from order group by shop_id

返回结果:

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

3.2 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
                },
                {
                    "key": 100.0,
                    "doc_count": 0
                },
                {
                    "key": 150.0,
                    "doc_count": 2
                }
            ]
        }
    }
}

3.3 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
                },
                {
                    "key_as_string": "2015-03-01",
                    "key": 1425168000000,
                    "doc_count": 2
                }
            ]
        }
    }
}

3.4 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
                },
                {
                    "key": "200.0-*",
                    "from": 200.0,
                    "doc_count": 3
                }
            ]
        }
    }
}

大家仔细观察的话,发现range分桶,默认key的值不太友好,尤其开发的时候,不知道key长什么样子,处理起来比较麻烦,我们可以为每一个分桶指定一个有意义的名字。

例子:

GET /_search
{
    "aggs" : {
        "price_ranges" : {
            "range" : {
                "field" : "price",
                "keyed" : true,
                "ranges" : [
                    // 通过key参数,配置每一个分桶的名字
                    { "key" : "cheap", "to" : 100 },
                    { "key" : "average", "from" : 100, "to" : 200 },
                    { "key" : "expensive", "from" : 200 }
                ]
            }
        }
    }
}

3.5 综合例子

前面的例子,都是单独使用aggs聚合语句,代表直接统计所有的文档,实际应用中,经常需要配合query语句,先搜索目标文档,然后使用aggs聚合语句对搜索结果进行统计分析。

例子:

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字段求和
                }
              }
            }
        }
    }
}

聚合查询支持多层嵌套。

4. ES聚合后排序(多桶排序)

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

1.多桶排序

默认情况,ES会根据doc_count文档总数,降序排序。

ES桶聚合支持两种方式排序:

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

1.1内置排序

内置排序参数:

  • _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降序
              }
            }
        }
    }
}

1.2按度量排序

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

例子:

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字段平均值
                }
            }
        }
    }
}

2.限制返回桶的数量

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

例子:

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

四、GEO地理信息搜索

省略

五、Elasticsearch SQL入门教程


省略

相关文章

网友评论

      本文标题:Elasticsearch

      本文链接:https://www.haomeiwen.com/subject/ktizfdtx.html