Elasticsearch 教程

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

ELK

大家估计都经常见到ELK这个关键词,他其实代表了Elasticsearch + Logstash + Kibana 三套软件,他们的作用如下:

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

应用场景

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

多种客户端支持

因为Elasticsearch支持RESTful风格的Api, 协议使用的是JSON,所以我们可以直接通过http api操作Elasticsearch,除了直接通过http api操作ES,Elasticsearch还支持下面各种开发语言封装的客户端:

  • curl
  • c#
  • go
  • php
  • java
  • python
  • ruby
  • sql - 你没看错,ES支持SQL查询,意味着我们可以使用SQL语句查询ES的数据,后面会有相关教程介绍。

Elasticsearch 存储结构

本章从使用角度介绍Elasticsearch的数据模型,而不是分析Elasticsearch底层的存储结构,教程的目的是帮助大家快速上手ES,我们要使用ES得先知道ES是怎么存储数据的,结构是怎么样的。

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

提示:MYSQL 是关系数据库,Elasticsearch是NOSQL类型的数据库,虽然他们都是数据库,但是他们定位不一样,也不是同一类型的数据库,拿来做对比,是因为一方面大家对MYSQL比较熟悉,另外从使用角度ES的存储结构跟MYSQL比较相似。

1. 基本概念

1.1. index(索引)

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

1.2. type(文档类型)

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

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

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

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

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

1.4. Field(文档字段)

文档由多个json字段(Field)组成, 这里的字段类似mysql中表的字段。

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

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

后面的章节会专门介绍字段类型,Elasticsearch支持的字段类型远比这里介绍的多。

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

1.5. mapping (映射)

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

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

2. 类比MYSQL存储结构

Elasticsearch存储结构

MYSQL存储结构

index(索引)


文档

行,一行数据

Field(字段)

表字段

mapping (映射)

表结构定义

Elasticsearch 安装配置和调试

本节介绍如何安装Elasticsearch,以及如何调试Elasticsearch。

内容如下:

  • 依赖JDK版本
  • 安装和启动Elasticsearch
  • 通过restful接口调试Elasticsearch
  • 通过Kibana可视化操作Elasticsearch

1.依赖JDK版本

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

2.安装和启动

目前Elasticsearch 最新的版本是7.5.x

2.1. 安装

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

下载

各个版本下载地址:

如果觉得上面的版本比较老,可以到官网下载最新的版本,https://www.elastic.co/cn/downloads/elasticsearch

解压缩

Windows系统直接右键解压缩即可。

Linux/macOS系统解压缩命令:

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

2.2. 启动

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/ ,如果可以正常访问,就说明安装成功了。

提示:下面介绍操作ES的方式,具体的ES查询语法后续章节会讲解。

3.通过Restful api调试Elasticsearch

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

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

curl例子:

curl -X GET "localhost:9200/_cat/health?v&pretty"

postman例子:

使用postman我们可以模拟任意http请求。

es菜鸟教程 elasticsearch菜鸟教程_Elastic

4.通过Kibana可视化操作Elasticsearch

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

4.1.安装Kibana

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

下载安装包

如果下载链接地址失效或者觉得版本太老,可以直接到官网下载最新版本https://www.elastic.co/cn/downloads/kibana

解压缩

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, 首次访问,因为没有数据,会显示如下窗口。

es菜鸟教程 elasticsearch菜鸟教程_Elastic_02

只要点击,Explore on my own 就可以进入后台。

es菜鸟教程 elasticsearch菜鸟教程_字段_03

4.2.通过Kibana的Console调试Elasticsearch

我们平时开发的时候,编写ES查询语句,可以使用Kibana提供的开发工具Console(控制台),调试ES查询有没有问题,Console支持语法补全和语法提示非常方便。

只要进入Kibana后台,点击左侧菜单的Dev Tools就可以进入Console后台。

es菜鸟教程 elasticsearch菜鸟教程_es菜鸟教程_04

Elasticsearch 快速入门

前面介绍过Elasticsearch是面向文档的存储引擎,存储的数据是以JSON文档为基本单位进行的,本节主要介绍ES中文档的CRUD操作。

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数据,当然不推荐这么做,建议同一个索引仅保存格式相同的数据,例如:订单索引,仅保存订单数据,不要保存其他业务数据。

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

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页面右侧可以直接看到执行结果。

es菜鸟教程 elasticsearch菜鸟教程_数据_05

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

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

5.更新文档

5.1. 更新整个文档

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

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底层文档是不可变的,每次修改文档,本质上是创建一个新的文档,然后把老的文档标记成删除。

6.删除文档

删除语法:

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

例子:

DELETE /order/_doc/1

说明:ES最强大的特性是搜索,本节没有介绍ES的文档搜索语法,后面的章节会单独介绍。

Elasticsearch 基于乐观锁的并发控制

如果我们同时修改一个文档,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
}

我们可以通过版本号实现乐观锁,控制出现并发修改文档的时候,确保数据不会出问题。

Elasticsearch 数据类型和映射

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

1.ES数据基础类型

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

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

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

2.精确值 & 全文类型

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

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

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

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

全文搜索后面的章节单独介绍。

2.自动映射 (dynamic mapping)

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

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

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

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

语法:

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

例子:

创建一个订单索引,索引名字order

订单索引结构如下表:

字段名

ES类型

说明

id

integer

订单id,整数

shop_id

integer

店铺Id, 整数

user_id

integer

用户id, 整数

order_no

keyword

订单编号,字符串类型,精确值

create_at

date

订单创建时间,日期类型

phone

keyword

电话号码,字符串类型,精确值

address

text

用户地址,字符串类型,需要模糊搜索

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

4.查询索引的映射规则

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

语法:

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

5.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对象嵌套。

查询

Elasticsearch 查询语法

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

提示:本章先介绍基本的语法结构,后续的章节单独讲解具体的查询语法。

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文档数组
    ]
  }
}

2.query子句

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

3.aggs子句

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

4.sort子句

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

5.ES查询分页

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

例子:

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

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

6. _source

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

例子:

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

仅返回,order_no和shop_id字段。

Elasticsearch query查询语法

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

1.匹配单个字段

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

语法:

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

说明:

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

例子:

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

article索引中,title字段匹配ES教程的所有文档。

如果title字段的数据类型是text类型,搜索关键词会进行分词处理。

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"

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)

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:

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

类似SQL:

select * from order_v2 where shop_id >= 10

5.bool组合查询

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

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查询语句中进行组合。

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

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

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"

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)

Elasticsearch 全文搜索

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

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

分词的目的:

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

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

明显被切割成一个个的字了。

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

例如:

搜索:上海大学

分词结果:上、海、大、学

下面的内容都会被搜到:

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

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

2.中文分词器

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

2.0. 如何指定analyzer

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

例子:

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

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

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

2.1. smartcn分词器

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

插件安装方式:

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

安装完成后,重启ES即可。

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

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

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

2.3. ik分词器

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

安装ik插件:

// 到这里找跟自己ES版本一致的插件地址
https://github.com/medcl/elasticsearch-analysis-ik/releases

我本地使用的ES版本是7.5.1,所以选择的Ik插件版本地址是:

https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.5.1/elasticsearch-analysis-ik-7.5.1.zip

当然你也可以尝试根据我给出这个地址,直接修改版本号,试试看行不行。

安装命令

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

2.4. ik中文分词效果

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

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

例1:

GET /_analyze
{
  "text": "上海人民广场麻辣烫",
  "analyzer": "ik_max_word"
}

输出:

{
  "tokens" : [
    {
      "token" : "上海人",
      "start_offset" : 0,
      "end_offset" : 3,
      "type" : "CN_WORD",
      "position" : 0
    },
    {
      "token" : "上海",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "CN_WORD",
      "position" : 1
    },
    {
      "token" : "人民",
      "start_offset" : 2,
      "end_offset" : 4,
      "type" : "CN_WORD",
      "position" : 2
    },
    {
      "token" : "广场",
      "start_offset" : 4,
      "end_offset" : 6,
      "type" : "CN_WORD",
      "position" : 3
    },
    {
      "token" : "麻辣烫",
      "start_offset" : 6,
      "end_offset" : 9,
      "type" : "CN_WORD",
      "position" : 4
    },
    {
      "token" : "麻辣",
      "start_offset" : 6,
      "end_offset" : 8,
      "type" : "CN_WORD",
      "position" : 5
    },
    {
      "token" : "烫",
      "start_offset" : 8,
      "end_offset" : 9,
      "type" : "CN_CHAR",
      "position" : 6
    }
  ]
}

例2:

GET /_analyze
{
  "text": "上海人民广场麻辣烫",
  "analyzer": "ik_smart"
}

输出:

{
  "tokens" : [
    {
      "token" : "上海",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "CN_WORD",
      "position" : 0
    },
    {
      "token" : "人民",
      "start_offset" : 2,
      "end_offset" : 4,
      "type" : "CN_WORD",
      "position" : 1
    },
    {
      "token" : "广场",
      "start_offset" : 4,
      "end_offset" : 6,
      "type" : "CN_WORD",
      "position" : 2
    },
    {
      "token" : "麻辣烫",
      "start_offset" : 6,
      "end_offset" : 9,
      "type" : "CN_WORD",
      "position" : 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头,可以搜一下。

Elasticsearch sort排序子句

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

聚合分析

Elasticsearch 聚合查询(aggs)基本概念

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

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

1. ES聚合查询流程

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

  • 分组
  • 组内聚合

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

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

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

2. 核心概念

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

2.1. 桶

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

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

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

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

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也是类似的过程。

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

Elasticsearch 指标聚合(metrics)

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

常用的统计函数如下:

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

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

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.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基数聚合统计的总数是一个近似值,会有一定的误差,这么做的目的是为了性能,因为在海量的数据中精确统计总数是非常消耗性能的,但是很多业务场景不需要精确的结果,只要近似值,例如:统计网站一天的访问量,有点误差没关系。

3.Avg

求平均值

例子:

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

返回结果:

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

4.Sum

求和计算

例子:

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

返回结果:

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

5.Max

求最大值

例子:

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

返回结果:

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

6.Min

求最小值

例子:

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

返回:

{
    ...

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

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

Elasticsearch 分组聚合查询(bucket)

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指标聚合,统计桶内文档总数。

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

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

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

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

聚合查询支持多层嵌套。

Elasticsearch 聚合后排序

类似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 GEO基本概念

地理信息检索是Elasticsearch的重要特性之一,ES geo主要用于地理信息的存储和搜索。

在实际应用场景中,凡是涉及线下业务、O2O业务多少都会遇到地理信息搜索需求,例如:搜索附近的KTV、搜索附近的餐馆并且按距离排序、地理围栏技术。

ES中涉及地理信息存储的核心概念有两个:

  • 坐标点
  • 地理形状

地理信息搜索主要从数学几何角度去处理,主要有下面几种方式:

  • 按坐标点之间的距离搜索
  • 坐标点和几何图形之间的包含关系,例如:在地图上画一个圈圈,把包含在这个圈圈内的坐标点找出来。
  • 几何图形之间的关系(包含、交集),例如:在地图上面两个多边形图形交集部分。

1.坐标点

ES中最常用的地理信息存储方式,通过坐标点表示一个地理位置,通常指的就是经度和维度。

例如:

一家餐馆的经纬度、外卖小哥当前的经纬度、消费者家庭地址的经纬度。

经纬度在地图上就是一个点(point),ES通过geo_point类型存储经纬度。

2.基于坐标点的搜索

当我们在业务系统中将相关的经纬度(坐标点)数据存储下来后,就需要对这些坐标点进行搜索。

ES中根据坐标点搜索的方式如下:

  • geo_bounding_box - 在地图上画一个矩形,凡是被矩形包含的坐标点,对应的文档都会搜索出来。
  • geo_distance - 给出一个坐标点和距离,搜索出这个距离范围内的相关文档。
  • geo_polygon - 在地图上画一个多边形,凡是被这个图形包含的坐标点,对应的文档都会被搜索出来。

3.地理形状

有时候,单纯使用经纬度表达地理信息是不够的,例如:一所大学,占地几千平方米,如果仅使用经纬度这种坐标点表示学校的位置,既不够准确,也无法满足所有业务需求,在地图上一个大学的范围通常都是不规则的多边形,能够代表学校的坐标点有成千上万个,到底使用哪个坐标点代表学校?

我们先看下面业务场景:

  • 当学生进入大学校园,才可以登录校园网,校外不允许登录。
  • 当客户进入商场的时候,就给是会员的客户发短信,推送优惠券。
  • 共享单车,只能在指定的区域停车,否则罚款。

上面业务场景涉及一个关键问题,我们如何在地图上标识一个区域,这个区域可以是一个大学校园、一个商场、一个停车的区域。

在ES中通过geo_shape类型,以图形的方式存储地理信息,而不仅仅是一个经纬度坐标点。

geo_shape类型支持的图形包括:点、线、多边形、圆等等,通过geo_shape我们可以存储一个图形,用来表示一个地理信息。

提示:在几何图形上面,一个几何图形也是由无数个坐标点组成,因此geo_shape的图形也是有多个经纬度组成。

4.基于地理形状的搜索

ES中如果我们存储的数据是geo_shape类型,那么只能通过geo_shape类型进行图形搜索。

下面是常用的地理形状搜索:

  • 交集 - 两个geo_shape图形重叠的话,就匹配成功(默认方式)。
  • 不重叠 - 两个geo_shape图形不重叠的话,就匹配成功。
  • 包含 - 一个geo_shape图形包含另外一个图形,就匹配成功。

基于地理形状,我们很容易实现地理围栏技术。

例如:

共享单车必须在指定位置停车的例子,在地图上画一个区域代表停车区域,如果共享单车的坐标落入这个停车区域停车就是合法的,否则违规。

Elasticsearch 地理坐标点存储

在ES中地理坐标指的就是经度和纬度,ES中存储经纬度坐标的数据类型为:geo_point。

1.定义映射的数据类型

我们可以通过定义索引映射的时候,指定字段类型为geo_point,表示存储的是一个经纬度坐标值。

例子:

PUT /shop
{
  "mappings": {
    "properties": {
      "location": { 
        "type": "geo_point" // location字段的类型为geo_point
      }
    }
  }
}

2.geo_point的存储格式

保存geo_point类型的数据主要有下面几种格式

2.1. 格式1

PUT /shop/_doc/1
{
  "location": { 
    "lat": 41.12,
    "lon": -71.34
  }
}

说明:

  • lat - 代表纬度
  • lon - 代表经度

2.2. 格式2

PUT /shop/_doc/2
{
  "location": [ -71.34, 41.12 ] 
}

说明:

  • [ 经度, 纬度] - 以数组的形式存储经纬度

2.3. 格式3

PUT /shop/_doc/3
{
  "location": "41.12,-71.34" 
}

说明:

以字符串的形式存储经纬度,格式: "纬度,经度"

Elasticsearch geo_point 按距离搜索

如果ES索引的字段类型为:geo_point,我们就可以按距离搜索文档。

下面以搜索附近店铺为例子介绍具体用法。

1.创建店铺索引

PUT /shops
{
  "mappings": {
    "properties": {
      "id": { // 店铺Id
        "type": "integer"
      },
      "title": { // 店铺名
        "type": "text"
      },
      "location": { // 店铺经纬度
        "type": "geo_point"
      }
    }
  }
}

2.按距离搜索

通过geo_distance搜索指定距离内的文档。

例子:

GET /shops/_search
{
  "query": { // query查询
    "bool": { // 布尔组合查询
      "must": { 
        "match_all": {} // 这里不设置其他查询条件,所以匹配全部文档
      },
      "filter": { // 因为地理信息搜索不打算参与相关度计算,所以使用filter包裹geo_distance
        "geo_distance": { // geo_distance按距离搜索
          "distance": "1km", // 设置搜索距离为1千米
          "location": { // 搜索location字段存储的经纬度和当前经纬度之间的距离。
            "lat": 39.889916, // 当前纬度
            "lon": 116.379547 // 当前经度
          }
        }
      }
    }
  }
}

搜索当前经纬度location字段存储的经纬度之间的距离在1千米范围之内的所有文档。

geo_distance参数说明:

  • distance - 设置搜索距离,常用距离单位:km(千米)、m(米)
  • 当前经纬度坐标点。

提示:既然要计算距离,肯定需要知道两个坐标点才能计算,所以geo_distance参数需要提供当前坐标点。

Elasticsearch geo 按距离排序

根据地理信息搜索文档之后,返回多个文档数据的时候,很多业务场景都希望根据距离由近到远排序。

下面是按距离排序的例子

GET /shops/_search
{
  "query": {
    "bool": {
      "must": {
        "match_all": {}
      },
      "filter": {
        "geo_distance": { // 按距离搜索
          "distance": "1km", // 搜索1千米范围
          "location": { // 搜索字段为location
            "lat": 39.889916, // 当前纬度
            "lon": 116.379547 // 当前经度
          }
        }
      }
    }
  },
  "sort": [ // 设置排序条件
    {
      "_geo_distance": { // _geo_distance代表根据距离排序
        "location": { // 根据location存储的经纬度计算距离。
          "lat": 39.889916, // 当前纬度
          "lon": 116.379547 // 当前经度
        },
        "order": "asc" // asc 表示升序,desc 表示降序
      }
    }
  ]
}

说明:

按距离排序条件,需要再次设置当前经纬度坐标,这样ES才知道计算距离的参考坐标。

Elasticsearch geo_bounding_box 按矩形范围搜索

ES中通过geo_bounding_box搜索,在地图画一个矩形,搜索矩形范围内的坐标点。

例如:

在地图画一个矩形,把矩形范围内的店铺找出来。

GET /shops/_search
{
  "query": {
    "bool": { // 布尔组合查询
      "must": {
        "match_all": {} // 这里设置其他搜索条件,直接搜索所有文档
      },
      "filter": { // 地理信息搜索不打算参与相关度评分,所以使用filter包裹起来。
        "geo_bounding_box": { // geo_bounding_box实现按矩形范围搜索
          "location": { // 店铺的坐标点保存在location字段中
            "top_left": { // 设置矩形的左上角坐标
              "lat": 40.73, // 纬度
              "lon": -74.1 // 经度
            },
            "bottom_right": { // 设置矩形的右下角坐标
              "lat": 40.717,
              "lon": -73.99
            }
          }
        }
      }
    }
  },
  "sort": [ // 设置排序条件
    {
      "_geo_distance": { // _geo_distance代表按距离排序
        "location": { // 计算 location保存的坐标和当前坐标的距离
          "lat": 40, // 当前纬度
          "lon": -70 // 当前经度
        },
        "order": "asc" // 根据距离升序排序
      }
    }
  ]
}

说明:

按矩形范围搜索店铺,如果需要对搜索的结果按距离排序,需要指定一个当前参考坐标,否则ES无法计算距离,也就是无法按距离排序。

Elasticsearch geo_polygon 按多边形范围搜索

ES 通过geo_polygon,在地图上画一个多边形,搜索包含在多边形内部的坐标点。

例子:

在地图上画一个多边形,搜索店铺

GET /shops/_search
{
  "query": {
    "bool": {
      "must": {
        "match_all": {} // 这里不设置其他搜索条件,直接匹配所有文档
      },
      "filter": { // 地理信息搜索不参与相关度算分,所以使用filter包裹起来
        "geo_polygon": { // geo_polygon搜索语句
          "location": { // 店铺的坐标存储在location字段.
            "points": [ // 设置组成多边形的经纬度坐标点数组。
              {
                "lat": 40.73, // 纬度
                "lon": -74.1 // 经度
              },
              {
                "lat": 40.83,
                "lon": -75.1
              },
              {
                "lat": 20,
                "lon": -90
              }
            ]
          }
        }
      }
    }
  }
}

说明:

通常ES搜索中为了提升性能,可以使用filter禁掉相关度计算,还可以缓存查询结果。

Elasticsearch geo_shape地理形状

通常情况,我们使用一个经纬度坐标表示一个店铺的位置、一个用户的位置,经纬度在地图上仅仅表示一个点,有时候需要表示一个区域,例如:停车场、商场、学校等等,这些区域拥有各种各样的形状,包括:圆形、多边形等等。

ES中存储地理形状的数据类型为: geo_shape

geo_shape支持存储的常用形状数据如下:

  • 点(point)
  • 圆形(circle)
  • 矩形(envelope)
  • 多边形 (polygon)

提示: 在geo_shape中,点作为一种特殊的形状,geo_shape可以存储一个点。

1.定义geo_shape类型映射

只要将字段类型定义为geo_shape即可。

PUT /example
{
    "mappings": {
        "properties": {
            "location": {
                "type": "geo_shape" // 定义location字段类型为geo_shape
            }
        }
    }
}

2.存储一个点

例子:

POST /example/_doc
{
    "location" : {
        "type" : "point", // 存储的图形类型为:point,表示存储一个坐标点
        "coordinates" : [-77.03653, 38.897676] // 坐标点格式: [经度, 纬度]
    }
}

3.存储一个圆形

POST /example/_doc
{
    "location" : {
        "type" : "circle", // 存储的图形类型为: circle, 表示一个圆形
        "coordinates" : [101.0, 1.0], // 圆心坐标,格式: [经度, 纬度]
        "radius" : "100m" // 圆的半径,常用单位: m (米), km (千米)
    }
}

4.存储一个矩形

POST /example/_doc
{
    "location" : {
        "type" : "envelope", // 存储的图形类型为: envelope, 表示一个矩形
        "coordinates" : [ 
             [100.0, 1.0], // 矩形左上角坐标, 坐标格式: [经度, 纬度]
             [101.0, 0.0]  // 矩形右下角坐标
        ]
    }
}

5.存储一个多边形

POST /example/_doc
{
  "location": {
    "type": "polygon", // 存储的图形类型为: polygon,表示一个多边形
    "coordinates": [ // 支持多个多边形
      [ // 第一个多边形,多边形由下面的坐标数组组成。
        [100, 0], // 第一个坐标点,坐标格式: [经度, 纬度]
        [101, 0],
        [101, 1],
        [100, 1],
        [100, 0] // 最后一个坐标点,要跟第一个坐标点相同,这样多边形才能形成闭合
      ]
    ]
  }
}

例子中提到,支持存储多个多边形数据,如果保存多个多边形,则第一个多边形表示外轮廓,第二个多边形表示内轮廓,这样内外多边形轮廓就组成一个空洞的图形,相当于,在多边形中间挖了个坑。

Elasticsearch geo_shape地理形状搜索

当索引的字段类型定义为geo_shape之后,我们就可以通过geo_shape实现图形搜索。

1.图形搜索类型

下面是geo_shape支持的图形搜索类型:

  • intersects - 查询的形状与索引的形状有重叠(默认), 即图形有交集则匹配。
  • disjoint - 查询的形状与索引的形状完全不重叠。
  • within - 查询的形状包含索引的形状。

2.图形搜索例子

GET /example/_search
{
    "query":{
        "bool": { // 布尔组合查询语句
            "must": {
                "match_all": {} // 这里设置其他查询条件,直接匹配全部文档
            },
            "filter": { // 地理信息搜索,通常不参与相关性计算,所以使用filter包裹起来
                "geo_shape": { // geo_shape搜索语句
                    "location": { // 图形数据存储在location字段
                        "shape": { // 设置我们想要通过什么形状去搜索location存储的图形数据
                            "type": "envelope", // 设置图形类型为envelope,表示矩形,各种图形格式参考下面提示
                            "coordinates" : [[13.0, 53.0], [14.0, 52.0]] // 设置矩形的坐标
                        },
                        "relation": "within" // 设置图形搜索类型,这里设置为包含关系
                    }
                }
            }
        }
    }
}

这个例子表达的意思是:使用一个矩形去搜索location字段值,返回包含在这个矩形范围内的所有文档。

geo_shape支持我们使用一个图形(点、线、圆、多边形等等)去匹配存储在文档中的图形数据。

提示:geo_shape在query语句中各种图形的格式和参数,请参考geo_shape存储格式,他们的格式是一样的,这里就不在重复介绍了。

SQL

Elasticsearch SQL入门教程

除了使用RESTful API查询Elasticsearch索引数据,也可以使用SQL语句查询索引数据,相信大部分人都使用过MYSQL,那么对SQL语句的语法结构肯定很熟悉了,下面介绍如何通过SQL语句查询ES数据。

提示: 需要安装X-Pack扩展组件,才可以让ES支持SQL查询,不过新版本的ES默认已经安装了X-Pack,老版本的ES可以检查下是否已经安装。

1.SQL和ES概念

在使用SQL查询ES数据之前,需要先了解下SQL和ES的概念映射关系。

SQL

Elasticsearch

描述

column

field

SQL中的列等价于ES中的字段

row

document

SQL中的一行数据等价于ES的一条文档

table

index

SQL的表跟ES中的索引名对应

schema


SQL中的schema通常指的就是数据库,在ES中没有这个概念,可以简单的认为ES中一个index就是一个数据库

2.例子

2.1.准备测试数据

下面创建一个library索引,插入一些测试数据。

PUT /library/_bulk?refresh
{"index":{"_id": "Leviathan Wakes"}}
{"name": "Leviathan Wakes", "author": "James S.A. Corey", "release_date": "2011-06-02", "page_count": 561}
{"index":{"_id": "Hyperion"}}
{"name": "Hyperion", "author": "Dan Simmons", "release_date": "1989-05-26", "page_count": 482}
{"index":{"_id": "Dune"}}
{"name": "Dune", "author": "Frank Herbert", "release_date": "1965-06-01", "page_count": 604}

2.2.执行SQL语句

我们可以直接通过REST API执行SQL语句,语法格式如下:

POST /_sql?format=txt
{
    "query": "这里书写SQL语句"
}

format属性可以指定数据返回格式:txt 格式代表返回表格形式便于阅读,json格式代表返回Json格式便于数据处理。

执行例子

POST /_sql?format=txt
{
    "query": "SELECT * FROM library WHERE release_date < '2000-01-01'"
}

格式说明

SELECT {查询字段} FROM {索引名} WHERE {查询条件}

执行结果:

author     |     name      |  page_count   |      release_date      
---------------+---------------+---------------+------------------------
Dan Simmons    |Hyperion       |482            |1989-05-26T00:00:00.000Z
Frank Herbert  |Dune           |604            |1965-06-01T00:00:00.000Z

Elasticsearch SQL查询

ES SQL查询的用法跟MYSQL基本一样,熟悉MYSQL的同学,可以直接通过SQL查询ES数据。

1.SQL语法

SELECT select_expr [, ...]
[ FROM table_name ]
[ WHERE condition ]
[ GROUP BY grouping_element [, ...] ]
[ HAVING condition]
[ ORDER BY expression [ ASC | DESC ] [, ...] ]
[ LIMIT [ count ] ]
[ PIVOT ( aggregation_expr FOR column IN ( value [ [ AS ] alias ] [, ...] ) ) ]

SQL语法结构跟MYSQL类似都是SQL标准语法结构,区别就是SQL标准的支持程度。

SQL例子

// emp 就是ES索引名
SELECT * FROM emp LIMIT 1;

SELECT gender AS g, COUNT(*) AS c FROM emp GROUP BY gender;

SELECT MIN(salary) AS min, MAX(salary) AS max, AVG(salary) AS avg, COUNT(*) AS count FROM emp;

都是标准的SQL写法。

2.ES支持的SQL命令

2.1.SHOW COLUMNS

查询ES索引的字段和类型

语法:

SHOW COLUMNS [ FROM | IN ]?
    [table identifier | 
    [LIKE pattern] ]

例子:

POST /_sql?format=txt
{
    "query": "SHOW COLUMNS FROM library"
}

输出:

column     |     type      |    mapping    
---------------+---------------+---------------
author         |VARCHAR        |text           
author.keyword |VARCHAR        |keyword        
name           |VARCHAR        |text           
name.keyword   |VARCHAR        |keyword        
page_count     |BIGINT         |long           
release_date   |TIMESTAMP      |datetime

2.2.SHOW TABLES

将ES中所有的索引都列出来

例子:

POST /_sql?format=txt
{
    "query": "SHOW TABLES"
}

输出:

name          |     type      |     kind      
------------------------+---------------+---------------
.apm-agent-configuration|BASE TABLE     |INDEX          
.kibana                 |VIEW           |ALIAS          
.kibana_1               |BASE TABLE     |INDEX          
.kibana_task_manager    |VIEW           |ALIAS          
.kibana_task_manager_1  |BASE TABLE     |INDEX          
library                 |BASE TABLE     |INDEX          
order                   |BASE TABLE     |INDEX                 
shops                   |BASE TABLE     |INDEX

2.3.SHOW FUNCTIONS

展示ES支持的SQL函数有哪些

例子:

POST /_sql?format=txt
{
    "query": "SHOW FUNCTIONS"
}

输出:

name       |     type      
-----------------+---------------
AVG              |AGGREGATE      
COUNT            |AGGREGATE      
FIRST            |AGGREGATE      
FIRST_VALUE      |AGGREGATE      
LAST             |AGGREGATE      
LAST_VALUE       |AGGREGATE      
MAX              |AGGREGATE      
MIN              |AGGREGATE      
SUM              |AGGREGATE      
KURTOSIS         |AGGREGATE     
.....忽略.....

Elasticsearch SQL全文搜索

ES SQL语法虽然支持like语句,但是like并不是使用全文搜索算法,ES SQL语句中主要通过MATCH函数实现全文搜索。

1.MATCH函数

语法:

MATCH(
    field_exp,   
    constant_exp 
    [, options])

说明:

  • field_exp - 需要搜索的字段
  • constant_exp - 需要搜索的关键词
  • options - 可选参数

2.全文搜索例子

POST /_sql?format=txt
{
    "query": "SELECT author, name FROM library WHERE MATCH(author, 'frank')"
}

说明:

查询library索引中author字段,匹配frank关键词的文档。

返回结果:

author     |     name      
---------------+---------------
Frank Herbert  |Dune

3.SQL相关度排序

通过SCORE()函数实现相关度排序

例子:

POST /_sql?format=txt
{
    "query": "SELECT author, name FROM library WHERE MATCH(author, 'frank') ORDER BY SCORE()"
}

也可以将相关度分数返回

例子:

SELECT SCORE(), * FROM library WHERE MATCH(name, 'dune') ORDER BY SCORE() DESC;