Elasticsearch

Elasticsearch

Elasticsearch 是一个基于 Lucene 的搜索服务器,它提供了一个分布式多用户能力的全文搜索引擎,基于 RESTful web 接口

Elasticsearch 是用 Java 开发的,并作为 Apache 许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索、稳定可靠、快速安装、使用方便

优点:

  • 分布式实时文件存储,并将每一个字段都编入索引,使其可以被搜索
  • 实时分析的分布式搜索引擎
  • 可以扩展到上百台服务器,处理 PB 级别的结构化或非结构化数据
  • Elasticsearch 是分布式的,不需要其他组件,分发是实时的,被叫做 “Push replication
  • 各节点组成对等的网络结构,某些节点出现故障时会自动分配其他节点代替其进行工作

ES与Lucene和Solr

Lucene

Lucene 不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎。想要使用它,你必须使用 Java 来作为开发语言并将其直接集成到你的应用中,更糟糕的是,Lucene 非常复杂,你需要深入了解检索的相关知识来理解它是如何工作的。

Solr

Solr 是 Apache Lucene 项目的开源企业搜索平台。其主要功能包括全文检索、命中标示、分面搜索、动态聚类、数据库集成,以及富文本(如 Word、PDF)的处理,Solr 是高度可扩展的,并提供了分布式搜索和索引复制。Solr 是最流行的企业级搜索引擎,Solr4 还增加了 NoSQL 的支持

当单纯的对已有数据进行搜索时,Solr更快

在这里插入图片描述

当实时建立索引时,Solr 会产生IO 阻塞,查询性能较差,Elasticsearch 具有明显的优势

在这里插入图片描述

随着数据量的增加,Solr 的搜索效率会变得更低,而 Elasticsearch 却没有明显的变化

在这里插入图片描述

综上所述,Solr 的架构并不适合实时搜索的应用

实际生产环境测试,下图为将搜素引擎从Solr转到Elasticsearch以后的平均查询速度有了 50 倍的提升

![在这里插入图片描述]](https://img-blog.csdnimg.cn/a3320ae34bb842629c060e3bcaf92e8e.png)

安装

官网下载地址:https://www.elastic.co/cn/downloads/elasticsearch

当前安装包:https://www.elastic.co/cn/downloads/past-releases/elasticsearch-7-17-2

# 下载
wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.17.2-linux-x86_64.tar.gz

# 解压
tar -zxvf elasticsearch-7.17.2-linux-x86_64.tar.gz -C /usr/local/

目录架构

在这里插入图片描述

bin 	启动文件目录
config 	配置文件目录
	log4j2.properties 	日志配置文件
	jvm.options 		java虚拟机相关配置文件
	elasticsearch.yml 	Elasticsearch配置文件
lib 	相关jar包目录
logs 	日志目录
modules 功能模块目录
plugins 插件目录

基础配置

elasticsearch.yml
# 配置es的集群名称,默认是elasticsearch
# 如果在同一网段下有多个集群,就可以用这个属性来区分不同的集群。
# 保证每个节点的名称相同,如此就能都处于一个集群之内了
cluster.name: my-application

# 节点名称
# 每一个节点的名称,必须不一样
node.name: node-1

# 默认只允许本机访问,修改为0.0.0.0后则可以远程访问
network.host: 0.0.0.0

# 主节点,作用主要是用于来管理整个集群,负责创建或删除索引,管理其他非master节点
# 允许节点是否可以成为一个master节点,ES是默认集群中的第一台机器成为master,如果这台机器停止就会重新选举。(默认开启)
node.master: true

# 数据节点,用于对文档数据的增删改查
# 允许该节点存储索引数据(默认开启)
node.data: true

# 默认端口
http.port: 9200

# 集群列表
# 设置集群中master节点的初始列表,可以通过这些节点来自动发现新加入集群的节点
discovery.seed_hosts: ["ip:port", "ip:port", "ip:port"]

# 初始化master节点
# 初始化一个新的集群时需要此配置来选举master
cluster.initial_master_nodes: ["node-1", "node-2", "node-3"]

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

jvm.options
  • Elasticsearch 通过 jvm.options 中的 Xms 和 Xmx 设置堆的大小

  • 应该来讲 Xms 和 Xmx 设为相同的值

  • Xms 和 Xmx 的值不应超过物理内存的 50%,因为 Elasticsearch 在堆内存之外还需要将内存用于其他。例如 Elasticsearch 需要利用堆外内存来进行网络通信,依赖操作系统的文件系统缓存来有效访问文件,而 JVM 本身也需要一些内存。

# 设置JVM堆的大小为 1G
-Xms1g
-Xmx1g

在这里插入图片描述

创建专用用户启动ES

  • Elasticsearch为了安全不允许使用root用户启动,es5之后的都不能使用添加启动参数或者修改配置文件等方法启动了
  • root用户不能直接启动Elasticsearch,所以需要创建一个专用用户,来启动ES
# 创建用户
useradd es
# 修改用户密码
passwd es

# 创建所属组并授权es目录权限
chown es:es -R /usr/local/elasticsearch/

在这里插入图片描述

# 切换到es用户
su es

# 进入Elasticsearch bin目录
cd /usr/local/elasticsearch/bin

# 启动Elasticsearch
./elasticsearch

# 后台启动Elasticsearch
./elasticsearch -d

启动报错处理

在这里插入图片描述

max file descriptors [4096] for elasticsearch process is too low, increase to at least [65535]

从错误信息看出应该是 Elasticsearch 程序需要的最小 max file descriptors 值是 65536,但是我的host只配置了4096(默认值)

查看 max file descriptors

[root@192 ~]# ulimit -Hn
4096
[root@192 ~]# ulimit -Sn
1024
  • ulimit -Hn: 是max number of open file descriptors的hard限制

  • ulimit -Sn: 是max number of open file descriptors的soft限制

  • 接下来要把这两个值改大,在 /etc/security/limits.conf 中添加如下内容

es hard nofile 65536
es soft nofile 65536

在这里插入图片描述

  1. es 表示运行Elasticsearch的用户
  2. hard 与 soft 表示限制的类型
  3. nofile 表示 max number of open file descriptors
  4. 65536 表示设置的大小

改完需要重新登录才能生效,或者切换用户

[root@192 ~]# su es
[es@192 root]$ ulimit -Hn
65536
[es@192 root]$ ulimit -Sn
65536

在这里插入图片描述

max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]

elasticsearch用户拥有的内存权限太小,至少需要262144,解决办法:

切换root用户,在 /etc/sysctl.conf 文件最后添加如下内容,即可永久修改

vm.max_map_count = 262144

刷新配置文件:

[root@192 ~]# sysctl -p
vm.max_map_count = 262144

再次启动成功:

访问 http://127.0.0.1:9200

在这里插入图片描述

jdk 环境报错更改

# 修改jdk环境变量
vim bin//elasticsearch-env


# 首行添加
JAVA_HOME="/usr/local/elasticsearch/jdk"

启动从节点时无法加入到集群,报错信息:

failed to validate incoming join request from node

Caused by: org.elasticsearch.cluster.coordination.CoordinationStateRejectedException: This node previously joined a cluster with UUID [Kzs2RYf-Qe6Y5VWrtmSrQw] and is now trying to join a different cluster with UUID [eRJNKjZjRfCpXJZbavulUQ]. This is forbidden and usually indicates an incorrect discovery or cluster bootstrapping configuration. Note that the cluster UUID persists across restarts and can only be changed by deleting the contents of the node's data paths [] which will also remove any data held by this node.

原因:推测是因为该节点之前启动过ES,已经创建了data文件夹,与要加入的集群冲突。

解决:因为该节点之前已经启动过,有历史数据没有清理,把该节点的data文件夹删了,再次启动就行

elasticsearch-head 安装

Elasticsearch 只是后端提供各种RESTful API ,那么怎么直观的看它的信息呢?

elasticsearch-head是一款专门针对于elasticsearch的客户端工具,用来展示数据。

elasticsearch-head是基于JavaScript语言编写的,可以使用 npm 部署,npm 是 Nodejs 下的包管理器

安装Nodejs环境

下载地址:http://nodejs.cn/download/

当前安装包:https://npmmirror.com/mirrors/node/v16.16.0/node-v16.16.0-linux-x64.tar.xz

# 下载
wget https://npmmirror.com/mirrors/node/v16.16.0/node-v16.16.0-linux-x64.tar.xz

# 解压
tar -xvf node-v16.16.0-linux-x64.tar.xz -C /usr/local/

# 修改解压目录名称
cd /usr/local/
mv node-v16.16.0-linux-x64 node-v16.16.0

# 配置环境变量
vim /etc/profile
# 追加 ":/usr/local/node-v16.16.0/bin" 内容
# 刷新配置
source /etc/profile

验证:

[root@192 local]# node -v
v16.16.0
[root@192 local]# npm -v
8.11.0

Elasticsearch-head 安装

下载地址:https://github.com/mobz/elasticsearch-head.git

当前安装包:https://github.com/mobz/elasticsearch-head/archive/refs/heads/master.zip

# 解压
unzip -d /usr/local/ elasticsearch-head-master.zip

# 更改解压目录名称
mv elasticsearch-head-master elasticsearch-head

# 安装cnpm的命令
npm install -g cnpm --registry=https://registry.npm.taobao.org

cd elasticsearch-head/
# 安装依赖
cnpm install

# 启动服务
npm run start #或者 npm run-script start

# 后台启动
nohup npm run-script start &

启动服务:npm run start

[root@192 elasticsearch-head]# npm run start

> elasticsearch-head@0.0.0 start
> grunt server

Running "connect:server" (connect) task
Waiting forever...
Started connect web server on http://localhost:9100

访问: http://127.0.0.1:9100/

使用Elasticsearch-head插件访问elasticsearch

连接失败,要允许跨域:

配置es的elasticsearch.yml文件解决跨域问题:

http.cors.enabled: true
http.cors.allow-origin: "*"

重启ES,再次连接:

head我们可以把它当做数据展示工具,后续的查询可以使用 Kibana

Kibana 安装

了解ELK

  • ELK是ElasticsearchLogstashKibana三大开源框架首字母大写简称。市面上也被称为Elastic Stack
  • 其中Elasticsearch是一个基于Lucene、分布式、通过Restful方式进行交互的近实时搜索平台框架。像类似百度、谷歌这种大数据全文搜索引擎的场景都可以使用Elasticsearch作为底层支持框架,可见Elasticsearch提供的搜索能力确实强大,市面上很多时候我们简称Elasticsearch为es。
  • Logstash是ELK的中央数据流引擎,用于从不同目标(文件/数据存储/MQ)收集的不同格式数据,经过过滤后支持输出到不同目的地(文件/MQ/redis/elasticsearch/kafka等)。
  • Kibana可以将elasticsearch的数据通过友好的页面展示出来,提供实时分析的功能。
  • 市面上很多开发只要提到ELK能够一致说出它是一个日志分析架构技术栈总称,但实际上ELK不仅仅适用于日志分析,它还可以支持其它任何数据分析和收集的场景,日志分析和收集只是更具有代表性,并非唯一性。

Kibana

  • Kibana是一个针对Elasticsearch的开源分析及可视化平台,用来搜索、查看交互存储在Elasticsearch索引中的数据。
  • 使用Kibana,可以通过各种图表进行高级数据分析及展示。Kibana让海量数据更容易理解。它操作简单,基于浏览器的用户界面可以快速创建仪表板(dashboard)实时显示Elasticsearch查询动态
  • 设置Kibana非常简单。无需编码或者额外的基础架构,几分钟内就可以完成Kibana安装并启动Elasticsearch索引监测。
  • Kibana安装版本要和ES安装版本一致

下载地址:https://www.elastic.co/cn/kibana/

当前安装包:https://artifacts.elastic.co/downloads/kibana/kibana-7.17.2-linux-x86_64.tar.gz

# 下载
wget https://artifacts.elastic.co/downloads/kibana/kibana-7.17.2-linux-x86_64.tar.gz

# 解压
tar -zxvf kibana-7.17.2-linux-x86_64.tar.gz -C /usr/local/

# 解压后重命名
mv kibana-7.17.2-linux-x86_64 kibana-7.17.2

# 启动服务
cd /usr/local/kibana-7.17.2/bin
./kibana

配置文件:config/kibana.yml

# kibana默认端口为5601
server.port: 5601

# kibana服务器地址
server.host: "0.0.0.0"

# es服务器地址
 elasticsearch.hosts: ["http://ip:port", "http://ip:port", "http://ip:port"]

# 创建的索引名字
kibana.index: ".kibana" 

# 设置kibana日志存放路径
logging.dest: stdout

# 设置使用中文显示页面
i18n.locale: "zh-CN" 

启动 Kibana

kibana 不支持root用户启动,如果是要用root用户启动,就在后面加 --allow-root ,要么就切换用户执行

# 配置目录权限给es用户
chown es:es -R /usr/local/kibana-7.17.2/

# 切换es用户
su es

# 启动服务
cd /usr/local/kibana-7.17.2/bin
./kibana

# 后台启动
nohup ./kibana &

访问: http://192.168.10.201:5601/

控制台:

最近更新到Elastic Stack 7.13以上版本的朋友可能注意到了,在默认不开启Elastic 安全功能时,Kibana的搜索结果页面会多出一行提示,建议我们开启ElasticSearch 安全功能。

在个人学习或者内网开放ES+VPN连接的情况下我们完全不需要开启安全功能,其他情况在生产集群中还是建议开启安全选项的。

这是因为没有显式禁用安全选项导致的,也就是说ElasticSearch会提示你是不是忘了启用这个选项,只要在配置文件中显式禁用即可取消这个提示。

elasticsearch.yml 配置禁用安全选项xpack.security.enabled,之后重启ElasticSearch即可:

xpack.security.enabled: false

ES 倒排索引

Lucene 作为 Apache 开源的一款搜索工具,一直以来是实现搜索功能的神兵利器,现今火热的 Solr 和 Elasticsearch 均基于该工具包进行开发,而 Lucene 之所以能在搜索中发挥至关重要的作用正是因为倒排索引

搜索的核心需求是全文检索,全文检索简单来说就是要在大量文档中找到包含某个单词出现的位置,在传统关系型数据库中,数据检索只能通过 like 来实现

这种实现方式实际会存在很多问题:

  • 无法使用数据库索引,需要全表扫描,性能差
  • 搜索效果差,只能首尾位模糊匹配,无法实现复杂的搜索需求
  • 无法得到文档与搜索条件的相关性

搜索的核心目标实际上是保证搜索的效果和性能,为了高效的实现全文检索,我们可以通过倒排索引来解决

倒排索引是区别于正排索引的概念:

  • 正排索引forward index 是以文档对象的唯一 ID 作为索引,以文档内容作为记录的结构。
  • 倒排索引inverted index 指的是将文档内容中的单词作为索引,将包含该词的文档 ID 作为记录的结构。

通俗来讲,正向索引是通过 key 找 value,反向索引则是通过 value 找 key

正向索引

正向索引(forward index)

当用户发起查询时(假设查询为一个关键词),搜索引擎会扫描索引库中的所有文档,找出所有包含关键词的文档,这样依次从文档中去查找是否含有关键词的方法叫做正向索引。互联网上存在的网页(或称文档)不计其数,这样遍历的索引结构效率低下,无法满足用户需求。

  • 以文档的ID为关键字,表中记录文档中每个字的位置信息,查找时扫描表中每个文档中字的信息,直到找出所有包含查询关键字的文档

  • 这种组织方法在建立索引的时候结构比较简单,建立比较方便且易于维护

    • 若是有新的文档加入,直接为该文档建立一个新的索引块,挂接在原来索引文件的后面
    • 若是有文档删除,则直接找到该文档号文档对应的索引信息,将其直接删除
  • 优缺点:

    • 文本检索的效率太低

反向索引

反向索引(inverted index),一般也被别人称之为倒排索引

为了增加效率,搜索引擎会把正向索引变为反向索引,即把 “文档→单词” 的形式变为 “单词→文档” 的形式

  • 倒排表以字或词为关键字进行索引,表中关键字所对应的记录表项记录了出现这个字或词的所有文档
  • 一个表项就是一个字段,它记录该文档的ID字符串在该文档中出现的位置情况
  • 优缺点
    • 查询的时候由于可以一次得到查询关键字所对应的所有文档,所以查询效率高于正排索引
    • 由于每个字或词对应的文档数量在动态变化,所以倒排表的建立和维护都较为复杂

倒排索引的组成

倒排索引主要由 单词词典(Term Dictionary)和 倒排列表(Posting List)组成

  • 单词词典
    • 倒排索引的重要组成,记录所有文档的单词,一般都比较大,记录单词到倒排列表的关联信息
    • 单词词典一般用 B+Trees 来实现,存储在内存:

  • 倒排列表

    • 倒排列表记载了出现过某个单词的所有文档的文档列表及单词在该文档中出现的位置信息及频率(作关联性算分),每条记录称为一个倒排项(Posting)
    • 倒排列表存储在磁盘文件中,主要包含以下信息:
      • 文档 ID
      • 单词频率(TF: Term Frequency)
      • 位置(Position)
      • 偏移(Offset)

  • 单词字段和倒排列表整合在一起的结构如下:

倒排索引的更新策略

  • 搜索引擎需要处理的文档集合往往都是动态集合,即在建好初始的索引后,不断有新文档进入系统,同时原先的文档集合内有些文档可能被删除或更改

  • 动态索引通过在内存中维护临时索引,可以实现对动态文档和实时搜索的支持

  • 服务器内存总是有限的,随着新加入系统的文档越来越多,临时索引消耗的内存也会随之增加

  • 当最初分配的内存将被使用完时,要考虑将临时索引的内容更新到磁盘索引中,以释放内存空间来容纳后续的新进文档

  • 索引基本更新思想:

    • 倒排索引 就是对初始文档集合建立的索引结构,一般单词词典都存储在内存,对应的倒排列表存储在磁盘文件中
    • 临时索引 是在内存中实时建立的倒排索引,其结构和前述的倒排索引是一样的,区别在于词典和倒排列表都在内存中存储
    • 新文档 进入系统时,实时解析文件并将其加入到临时索引机构中
    • 删除文档 列表则用来存储已被删除的文档相应的文档ID,形成一个文档ID列表
    • 修改文档 可以认为是旧文档先被删除,然后系统再增加一篇新的文档,通过这种间接方式实现对内容更改的支持

常见的索引更新策略主要有四种:完全重建策略、再合并策略、原地更新策略及混合策略

  • 完全重建策略

    • 当新增文档到达一定数量,将新增文档和原先的老文档整合,然后利用静态索引创建方法对所有文档重建索引,新索引建立完成后老索引会被遗弃。

    • 此法代价高,但是主流商业搜索引擎一般是采用此方式来维护索引的更新(这句话是书中原话)

    • 优缺点:

      • 因为重建索引需要较长时间,在进行索引重建的过程中,内存中仍然需要维护老索引来对用户的查询做出相应
      • 这种策略适合比较小的文档集合
  • 再合并策略

    • 当新增文档进入系统,解析文档,之后更新内存中维护的临时索引,文档中出现的每个单词,在其倒排表列表末尾追加倒排表列表项;一旦临时索引将指定内存消耗光,即进行一次索引合并,这里需要倒排文件里的倒排列表存放顺序已经按照索引单词字典顺序由低到高排序,这样直接顺序扫描合并即可。

    • 优缺点:

      • 因为要生成新的倒排索引文件,所以对老索引中的很多单词,尽管其在倒排列表并未发生任何变化,也需要将其从老索引中取出来并写入新索引中,这样对磁盘消耗是没必要的。
  • 原地更新策略

    • 试图改进再合并策略,在原地合并倒排表,这需要提前分配一定的空间给未来插入,如果提前分配的空间不够了需要迁移。实际显示,其索引更新的效率比再合并策略要低
  • 混合策略

    • 出发点是能够结合不同索引更新策略的长处,将不同索引更新策略混合,以形成更高效的方法

分词器

定义

  • 文本分析是把全文本转换一系列单词(term/token)的过程,也叫分词,Analysis(分析) 是通过Analyzer(分析器) 来实现的
  • 分词器的作用就是把整篇文档,按一定的语义切分成一个一个的词条,目标是提升文档的召回率,并降低无效数据的噪音
    • recall 召回率,也叫可搜索性,指搜索的时候,增加能够搜索到结果的数量
    • 降噪:指降低文档中一些低相关性词条对整体搜索排序结果的干扰

组成

分析器(Analyzer)都由三种构件块组成:character filters、tokenizers、token filters

  • character filters 字符过滤器
    • 在一段文本进行分词之前,先进行预处理
    • 比如说最常见的就是,过滤html标签
  • tokennizers 分词器
    • 英文分词可以根据空格将单词拆分开,中文分词比较复杂,可以采用机器学习算法来分词
  • token filters Token过滤器
    • 将切分的单词进行加工
    • 大小写装换,去掉分词,或者增加词
  • Analyzer = CharFilters(0个或多个)+ Tokenizer(恰好一个)+ TokenFilters(0个或多个)

内置分词器

  • Whitespace Analyzer 默认分词器,按词切分,小写处理
    • standard 是默认的分析器,它提供了基于语法的标记化(基于 Unicode 文本分割算法),适用于大多数语言
    • 区分中英文,英文按照空格切分同时大写转小写,中文按照单个字分词,一个字为一个词
  • Simple Analyzer 按照非字母切分(符号被过滤),小写处理
    • simple 分析器当它遇到只要不是字母的字符,就将文本解析成 term,而且所有的 term 都是小写的
    • 先按空格分词,英文大写转小写,中文不再分词
  • Whitespace Analyzer 按照空格切分,不转小写
    • whitespace 按照空格进行分词,不区分大小写,中文不分词
    • 按空格分词,英文不区分大小写,中文不再分词
  • Stop Analyzer 小写处理,停用词过滤(the、a、is)
  • Keyword Analyzer 不分词,直接将输入当做输出
  • Patter Analyzer 正则表达式,默认 \W+ (非字符分割)
  • Language 提供了30多种常见语言的分词器
  • Customer Analyzer 自定义分词器

IK 分词器

IK Analyzer 插件将 Lucene IK分析器集成到 Elasticsearch 中,支持自定义词典

中文的分词器现在大家比较推荐的就是 IK 分词器,当然也有一些其他的分词器,比如 SmartCN、HanLP

IK 分词器的粒度:

  • ik_smart 会做最粗粒度的拆分
    • 比如会将“中华人民共和国国歌”拆分为“中华人民共和国,国歌”,适合 Phrase 查询
  • ik_max_word 会将文本做最细粒度的拆分
    • 比如会将“中华人民共和国国歌”拆分为“中华人民共和国,中华人民,中华,华人,人民共和国,人民,人,民,共和国,共和,和,国国,国歌”,会穷尽各种可能的组合,适合 Term Query

安装

下载地址:https://github.com/medcl/elasticsearch-analysis-ik

当前安装包:https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.17.2/elasticsearch-analysis-ik-7.17.2.zip

# 在es plugins 目录下新建目录 analysis-ik
mkdir analysis-ik
[root@192 plugins]# ll
总用量 0
drwxr-xr-x. 2 root root 6 83 12:10 analysis-ik

# 解压到指定目录
unzip elasticsearch-analysis-ik-7.17.2.zip -d /usr/local/elasticsearch/plugins/analysis-ik/
[root@192 plugins]# cd analysis-ik/
[root@192 analysis-ik]# ll
总用量 1432
-rw-r--r--. 1 root root 263965 118 2022 commons-codec-1.9.jar
-rw-r--r--. 1 root root  61829 118 2022 commons-logging-1.2.jar
drwxr-xr-x. 2 root root   4096 118 2022 config
-rw-r--r--. 1 root root  54931 41 15:43 elasticsearch-analysis-ik-7.17.2.jar
-rw-r--r--. 1 root root 736658 118 2022 httpclient-4.5.2.jar
-rw-r--r--. 1 root root 326724 118 2022 httpcore-4.4.4.jar
-rw-r--r--. 1 root root   1807 41 15:43 plugin-descriptor.properties
-rw-r--r--. 1 root root    125 41 15:43 plugin-security.policy

# 授权
chown es:es -R /usr/local/elasticsearch/
[root@192 analysis-ik]# ll
总用量 1432
-rw-r--r--. 1 es es 263965 118 2022 commons-codec-1.9.jar
-rw-r--r--. 1 es es  61829 118 2022 commons-logging-1.2.jar
drwxr-xr-x. 2 es es   4096 118 2022 config
-rw-r--r--. 1 es es  54931 41 15:43 elasticsearch-analysis-ik-7.17.2.jar
-rw-r--r--. 1 es es 736658 118 2022 httpclient-4.5.2.jar
-rw-r--r--. 1 es es 326724 118 2022 httpcore-4.4.4.jar
-rw-r--r--. 1 es es   1807 41 15:43 plugin-descriptor.properties
-rw-r--r--. 1 es es    125 41 15:43 plugin-security.policy

# 查看插件列表
[root@192 bin]# ./elasticsearch-plugin list
analysis-ik

重启ES,进行分词测试:

GET _analyze
{
  "analyzer": "ik_max_word",
  "text": "今天天气真好"
}

GET _analyze
{
  "analyzer": "ik_smart",
  "text": "今天天气真好"
}

ES 数据存储结构

MySQL 与 Elasticsearch 对比:

MySQLElasticsearch
数据库 database索引 index
表 tables类型 types
行记录 rows文档 document
列字段 columns字段 field
表结构定义 schema字段定义 mapping

索引 Index

  • 索引是文档(Document)的容器,是一类文档的集合
  • 索引(名词)
    • 类比传统的关系型数据库领域来说,索引相当于SQL中的一个数据库(Database)
    • 索引由其名称(必须为全小写字符)进行标识
  • 索引(动词)
    • 保存一个文档到索引(名词)的过程
    • 这非常类似于SQL语句中的 INSERT 关键词,如果该文档已存在时,那就相当于数据库的 UPDATE
  • 索引(倒排索引)
    • 关系型数据库通过增加一个 B+ 树索引到指定的列上,以便提升数据检索速度
    • 索引 Elasticsearch 使用了一个叫做 倒排索引 的结构来达到相同的目的

类型 Type

  • 从 6.0.0 开始单个索引中只能有一个类型,7.0.0 以后将不建议使用,8.0.0 以后完全不支持
  • 索引和文档中间还有个类型的概念,每个索引下可以建立多个类型,文档存储时需要指定 index 和 type
  • 弃用原因:
    • 通俗的去理解 index 比作 SQL 的 Database,Type 比作 SQL 的 Table,但这并不准确,因为在 SQL 中,Table 之间相互独立,同名的字段在两个表中毫无关系。但是在 ES 中,同一个 Index 下不同的 Type 如果有同名的字段,它们会被 Lucene 当做同一个字段,并且它们的定义必须相同。所以 Index 现在更像一个表,而 Type 字段并没有多少意义。目前 Type 已经被 Deprecated在 7.0 开始,一个索引只能建立一个 Type 为 _doc

Document

  • Elasticsearch是面向文档的,文档是所有可搜索数据的最小基础信息单元。

  • 一个Document就像数据库中的一行记录,文档会被序列化成JSON格式,保持在Elasticsearch中,多个Document存储于一个索引(Index)中。文档以JSON(Javascript Object Notation)格式来表示,而JSON是一个到处存在的互联网数据交互格式。

  • 每一个文档都有一个UniqueID

  • document核心元数据(元数据:用于标注稳定的相关信息):

    • _index

      • 代表一个document存放在哪个index中,项目约定结构类似的数据放在一个索引,不同数据放不同索引里,所以同一个index中document结构基本是类似的,个别document多一个或少一个field,这样Elasticsearch对磁盘存储的利用率最高。
        每个index有自己独立的shard存储文件,与其他index互不影响。

      • 命名规范:名称小写,不能以 ‘_’, ‘-’, 或 ‘+’ 开头

    • _type

      • ES 6.0.0之后一个index下面只能有一个type,最早指定是啥就是啥。
      • 命名规范:可以用 “_” 开头,由于只有一个,官方示例上直接使用 “_doc”
    • _id

      • document的唯一标识,与index一起唯一标识和定位一个document,可以手动指定,也可以由ES自动创建
    • _version

      • ES内部使用乐观锁对document的写操作进行控制,version版本号最初是1,更新操作成功后自动+1
    • found

      • document的搜索标志,成功是true,未搜索到是false
    • _source

      • 里面是我们在新增时放在http request body的json串内容,是保存的业务数据,默认Get操作时,会原封不动地全部返回给客户端
      • 用Get命令搜索document时,可以定制返回的结果,在请求的_source中指定想要的field即可

Field

Document 中的字段,是文档中的某一个属性

Mapping

  • 从 7.x 开始,一个 Mapping 只属于一个索引的 Type,默认为:“_doc”
  • ES 字段类型主要有:核心类型、复杂类型、地理类型以及特殊类型
  • Mapping 类似于数据库中的表结构定义 schema,它有以下几个作用:
    • 定义索引中的字段的名称
    • 定义字段的数据类型,比如字符串、数字、布尔…
    • 字段,倒排索引的相关配置,比如设置某个字段为不被索引、记录 position 等

核心类型

字符串类型

其中,在 ES 7.x 有两种字符串类型:text 和 keyword,在 ES 5.x 之后 string 类型已经不再支持了。

  • text
    • 类型适用于需要被全文检索的字段,例如新闻正文、邮件内容等比较长的文字,text 类型会被 Lucene 分词器(Analyzer)处理为一个个词项,并使用 Lucene 倒排索引存储,text 字段不能被用于排序,如果需要使用该类型的字段只需要在定义映射时指定 JSON 中对应字段的 type 为 text。
    • 非结构化的文本数据
  • keyword
    • 适合简短、结构化字符串,例如主机名、姓名、商品名称等,可以用于过滤、排序、聚合检索,也可以用于精确查询。
    • 也可以用于精确查询
      • 包括数字、日期、具体的字符串(如:“192.168.0.1”)
数字类型
  • 数字类型分为 long、integer、short、byte、double、float、half_float、scaled_float
  • 数字类型的字段在满足需求的前提下应当尽量选择范围较小的数据类型,字段长度越短,搜索效率越高
  • 对于浮点数,可以优先考虑使用 scaled_float 类型,该类型可以通过缩放因子来精确浮点数,例如 12.34 可以转换为 1234 来存储
日期类型
  • ES 底层依然采用的是时间戳的形式存储
  • “format”: “yyyy-MM-dd HH:mm:ss || yyyy-MM-dd|| epoch_millis”
布尔类型

JSON 文档中同样存在布尔类型,不过 JSON 字符串类型也可以被 ES 转换为布尔类型存储,前提是字符串的取值为 true 或者 false,布尔类型常用于检索中的过滤条件。

二进制类型

二进制类型 binary 接受 BASE64 编码的字符串,默认 store 属性为 false,并且不可以被搜索。

范围类型

范围类型可以用来表达一个数据的区间,可以分为5种:

integer_range、float_range、long_range、double_range 以及 date_range

复杂类型

复合类型主要有对象类型(object)和嵌套类型(nested)

对象类型

JSON 字符串允许嵌套对象,一个文档可以嵌套多个、多层对象。可以通过对象类型来存储二级文档,不过由于 Lucene 并没有内部对象的概念,ES 会将原 JSON 文档扁平化,列如文档:

实际上 ES 会将其转换为以下格式,并通过 Lucene 存储,即使 name 是 object 类型

嵌套类型

嵌套类型可以看成是一个特殊的对象类型,可以让对象数组独立检索,例如文档

username 字段是一个 JSON 数组,并且每个数组对象都是一个 JSON 对象。如果将 username 设置为对象类型,那么 ES 会将其转换为:

可以看出转换后的 JSON 文档中 first 和 last 的关联丢失了,如果尝试搜索 first 为 wu,last 为 xy 的文档,那么成功会检索出上述文档,但是 wu 和 xy 在原 JSON 文档中并不属于同一个 JSON 对象,应当是不匹配的,即检索不出任何结果。

地理类型

地理类型字段分为两种:经纬度类型和地理区域类型

经纬度类型

经纬度类型字段(geo_point)可以存储经纬度相关信息,通过地理类型的字段,可以用来实现诸如查找在指定地理区域内相关的文档、根据距离排序、根据地理位置修改评分规则等需求。

地理区域类型

经纬度类型可以表达一个点,而 geo_shape 类型可以表达一块地理区域,区域的形状可以是任意多边形,也可以是点、线、面、多点、多线、多面等几何类型

特殊类型

特殊类型包括 IP 类型、过滤器类型、Join 类型、别名类型等

IP 类型

IP 类型的字段可以用来存储 IPv4 或者 IPv6 地址,如果需要存储 IP 类型的字段,需要手动定义映射:

completion 类型

提供自动输入关联完成功能,如常见的Baidu搜索框

token_count 类型

用于计算字符串token的长度,使用时需提供"analyzer"定义

percolate 类型

定义为 percolate 类型的字段会被ES分析为一个查询并保存下,并可用在后续对文档的查询中。Percolate 可以理解为一个预置的查询

alias 类型

定义一个已存在域的别名

Join 类型

Join 类型是 ES 6.x 引入的类型,以取代淘汰的 _parent 元字段,用来实现文档的一对一、一对多的关系,主要用来做父子查询。

Join 类型的 Mapping 如下:

其中,my_join_field 为 Join 类型字段的名称;relations 指定关系:question 是 answer 的父类。

例如定义一个 ID 为 1 的父文档:

接下来定义一个子文档,该文档指定了父文档 ID 为 1:

analyzer 分词器

analyzer 可指定文本和检索所用的分词器

index 是否索引

index 可用于设置字段是否被索引,默认为 true,false 即为不可搜索

null_value 空值默认值

需要对 Null 值实现搜索使用,只有 keyword 类型才支持设定 null_value

Dynamic Mapping

Dynamic Mapping 机制使我们不需要手动定义 Mapping,ES 会自动根据文档信息来判断字段合适的类型,但是有时候也会推算的不对,比如地理位置信息有可能会判断为 Text,当类型如果设置不对时,会导致一些功能无法正常工作,比如 Range 查询。

ES 类型的自动识别是基于 JSON 的格式,如果输入的是 JSON 是字符串且格式为日期格式,ES 会自动设置成 Date 类型;当输入的字符串是数字的时候,ES 默认会当成字符串来处理,可以通过设置来转换成合适的类型;如果输入的是 Text 字段的时候,ES 会自动增加 keyword 子字段,还有一些自动识别如下图所示:

动态映射时 Elasticsearch 的一个重要特性:

  • 不需要提前创建index、定义 mapping 信息和 type 类型
  • 可以直接向 ES 中插入文档数据时,ES 会根据每个新 field 可能的数据类型,自动为其配置 type 等 mapping 信息
  • 这个过程就是动态映射(dynamic mapping)

万一我想修改 Mapping 的字段类型,能否更改呢?让我们分以下两种情况来探究下:

  • 如果是新增加的字段,根据 Dynamic 的设置分为以下三种状况:
    • 当 Dynamic 设置为 true 时,一旦有新增字段的文档写入,Mapping 也同时被更新
    • 当 Dynamic 设置为 false 时,索引的 Mapping 是不会被更新的,新增字段的数据无法被索引,也就是无法被搜索,但是信息会出现在 _source 中
    • 当 Dynamic 设置为 strict 时,文档写入会失败
  • 另外一种是字段已经存在,这种情况下,ES 是不允许修改字段的类型的,因为 ES 是根据 Lucene 实现的倒排索引,一旦生成后就不允许修改,如果希望改变字段类型,必须使用 Reindex API 重建索引。
    • 不能修改的原因是如果修改了字段的数据类型,会导致已被索引的无法被搜索,但是如果是增加新的字段,就不会有这样的影响。

控制Dynamic Mappings:

“true”“false”“strict”
文档可索引YESYESNO
字段可索引YESNONO
Mapping被更新YESNONO

ES 集群

  • 在Elasticsearch当中,ES分为三种角色:master、data、client
  • 三种角色由elasticsearch.yml配置文件中的node.master、node.data来控制
  • 如果不修改elasticsearch的节点角色信息,那么默认就是node.master: true、node.data: true
  • 默认情况下,es集群中的每个节点都有成为主节点的资格,也都存储数据,还可以提供查询服务,负载均衡以及数据合并等服务。在高并发的场景下集群容易出现负载过高问题。

角色划分

  • master:该节点不和应用创建连接,主要用于元数据(metadata)的处理,比如索引的新增、删除、分片分配等,master节点不占用io和CPU,内存使用量一般
  • data:该节点和索引应用创建连接、接收索引请求,该节点真正存储数据,ES集群的性能取决于该节点的个数(每个节点最优配置的情况下),data节点会占用大量的CPU、io和内存
  • client:该节点和检索应用创建连接、接受检索请求,但其本身不负责存储数据,可当负责均衡节点,client节点不占用io、cpu和内存

各节点间的关系

  • master:master节点具备主节点的选举权,有资格成为主节点,主节点控制整个集群的元数据。
  • data:data节点的分片执行查询语句获得查询结果后将结果反馈给client。此过程较消耗硬件资源。
  • client:client节点接受搜索请求后将请求转发到与查询条件相关的多个data节点的分片上,然后多个data节点的分片执行查询语句或者查询结果再返回给client节点,client来把各个data节点的返回结果进行整合、排序等一系列操作后再将最终结果返回给用户请求。

资源规划

  • master节点:ES如果做集群的话Master节点至少三台服务器或者三个Master实例加入相同集群,三个Master节点最多只能故障一台Master节点,亲测,如果故障两个Master节点,ES将无法组成集群。
  • data节点:单个索引在一个data节点上分片数保持在3个以内;每1GB堆内存对应集群的分片保持在20个以内;每个分片不要超过30G;
  • client节点:增加client节点可增加检索并发,但检索的速度还是取决于查询所命中的分片个数以及分片中的数据量。
  • data节点经验:
    • 如果单索引每个节点可支撑90G数据,依此可计算出所需data节点数 。
    • 如果是多索引按照单个data节点jvm内存最大30G来计算,一个节点的分片保持在600个以内,存储保持在18T以内。
    • 主机的cpu、io固定,建议一台主机只部署一个data节点,不同角色节点独立部署,方便扩容
    • 每条数据保持在2k以下索引性能大约3000-5000条/s/data节点,增加data节点数可大幅度增加索引速率,节点数与索引效率的增长关系呈抛物线形状

ES的四种集群角色

  • node.master: true #主节点;允许节点是否可以成为一个master节点,ES是默认集群中的第一台机器成为master,如果这台机器停止就会重新选举。(默认开启)
  • node.data: true #数据节点;允许该节点存储索引数据(默认开启)
  • 上面这两个属性可以有四种组合方式:

第一种:主节点+数据节点

第一种为ES默认选项,以下两个值都为true,该节点既有成为master的资格还要存储数据,如果该节点被选为了真正的master,还要存储数据,那么该节点的压力相对来说就较大了,生产中不建议这样,即成为了主节点还成为了数据节点。

node.master: true
node.data: true

第二种:主节点

第二种为主节点模式,该节点只有成为master节点的资格,并不正真存储数据,在没有成为master的情况下还可以进行集群内的请求转发,数据合并等功能,此选择在生产中为master节点。

node.master: true
node.data: false

第三种:数据节点

第三种为数据节点,该节点只存储数据,没有资格成为master,只做数据存储功能;在集群中需要单独设置几个这样的节点,用来存储数据。

node.master: false
node.data: true

第四种:客户端节点

第四种为client节点,该节点既没有成为master的资格,还不存储数据,主要是针对海量请求的时候可以进行负载均衡、数据合并、数据查询、请求转发等功能。

node.master: false
node.data: false

集群配置

# 配置es的集群名称,默认是elasticsearch
# 如果在同一网段下有多个集群,就可以用这个属性来区分不同的集群。
# 保证每个节点的名称相同,如此就能都处于一个集群之内了
cluster.name: my-application

# 节点名称
# 每一个节点的名称,必须不一样
node.name: node-1

# 主节点,作用主要是用于来管理整个集群,负责创建或删除索引,管理其他非master节点
# 允许节点是否可以成为一个master节点,ES是默认集群中的第一台机器成为master,如果这台机器停止就会重新选举。(默认开启)
node.master: true

# 数据节点,用于对文档数据的增删改查
# 允许该节点存储索引数据(默认开启)
node.data: true

# 集群列表
# 设置集群中master节点的初始列表,可以通过这些节点来自动发现新加入集群的节点
discovery.seed_hosts: ["ip:port", "ip:port", "ip:port"]

# 初始化master节点
# 初始化一个新的集群时需要此配置来选举master
cluster.initial_master_nodes: ["node-1", "node-2", "node-3"]

注意事项:集群需要开启9300端口进行通讯

  • 9200端口: ES节点和外部通讯使用
  • 9300端口: ES节点之间通讯使用
  • 9300是 tcp 通讯端口,集群间和TCP Client都走的它;9200是 http 协议的 RESTful 接口

查看集群状态:http://ip:9200/_cluster/health?pretty # ?pretty JSON格式显示,易读

输出里最重要的就是 status 这行。很多开源的 ES 监控脚本,其实就是拿这行数据做报警判断。status 有三个可能的值:

  • green 绿灯
    • 所有分片都正确运行,集群非常健康。
    • 所有的主分片和副本分片都已分配。你的集群是 100% 可用的。
  • yellow 黄灯
    • 所有主分片都正确运行,但是有副本分片缺失。这种情况意味着 ES 当前还是正常运行的,但是有一定风险。注意,在 Kibana4 的 server 端启动逻辑中,即使是黄灯状态,Kibana 4 也会拒绝启动,死循环等待集群状态变成绿灯后才能继续运行。
    • 所有的主分片已经分片了,但至少还有一个副本是缺失的。不会有数据丢失,所以搜索结果依然是完整的。不过,你的高可用性在某种程度上被弱化。如果更多的分片消失,你就会丢数据了。把 yellow 想象成一个需要及时调查的警告。
  • red 红灯
    • 有主分片缺失。这部分数据完全不可用。而考虑到 ES 在写入端是简单的取余算法,轮到这个分片上的数据也会持续写入报错。
    • 至少一个主分片(以及它的全部副本)都在缺失中。这意味着你在缺少数据:搜索只能返回部分数据,而分配到这个分片上的写入请求会返回一个异常。

green/yellow/red 状态是一个概览你的集群并了解眼下正在发生什么的好办法。剩下来的指标给你列出来集群的状态概要:

  • number_of_nodes 和 number_of_data_nodes 这个命名完全是自描述的。

  • active_primary_shards 指出你集群中的主分片数量。这是涵盖了所有索引的汇总值。

  • active_shards 是涵盖了所有索引的所有分片的汇总值,即包括副本分片。

  • relocating_shards 显示当前正在从一个节点迁往其他节点的分片的数量。通常来说应该是 0,不过在 Elasticsearch 发现集群不太均衡时,该值会上涨。比如说:添加了一个新节点,或者下线了一个节点。

  • initializing_shards 是刚刚创建的分片的个数。比如,当你刚创建第一个索引,分片都会短暂的处于 initializing 状态。这通常会是一个临时事件,分片不应该长期停留在 initializing 状态。你还可能在节点刚重启的时候看到 initializing 分片:当分片从磁盘上加载后,它们会从 initializing 状态开始。

  • unassigned_shards 是已经在集群状态中存在的分片,但是实际在集群里又找不着。通常未分配分片的来源是未分配的副本。比如,一个有 5 分片和 1 副本的索引,在单节点集群上,就会有 5 个未分配副本分片。如果你的集群是 red 状态,也会长期保有未分配分片(因为缺少主分片)。

查看节点列表: http://ip:9200/_cat/nodes?v

  • heap.percent:堆内存占的内存百分比
  • ram.percent:物理内存占用百分比
  • cpu:表示使用的cpu核心
  • load_1m load_5m load_15m:1分钟 5分钟 15分钟 占用系统cup百分比
  • node.role:表示节点能充当的角色主、数据 节点
  • master:表示当前是否为主节点,*表示当前为主

文档CURD

创建Index

curl -XPUT http://192.168.10.201:9200/user {
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 1
  },
  "mappings": {
    "dynamic": "false",
    "properties": {
      "username": {
        "type": "keyword"
      },
      "phone": {
        "type": "keyword",
        "index": "false"
      },
      "description": {
        "type": "text"
      },
      "create_time": {
        "type": "date",
        "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
      }
    }
  }
}

文档插入

没有指定ID POST /index/_doc {}

POST user/_doc
{
  "username": "zhangsan",
  "phone": "123456",
  "description": "勇敢、坚强、正直",
  "create_time": "2022-08-05"
}

指定ID插入 POST /index/_doc/id {}

POST user/_doc/1
{
  "username": "admin",
  "phone": "123456",
  "description": "管理员、至高无上、神一般",
  "create_time": "2022-08-05"
}

文档查询

查询索引 GET index/_search

GET user/_search

根据ID查询文档 GET index/_doc/id

GET user/_doc/1

查询指定的列 includes excludes

GET /user/_search
{
  "_source": {
    "includes": [
      "username",
      "description"
    ],
    "excludes": [
      "phone"
    ]
  }
}

文档修改

GET user/_doc/1

局部更新 POST index/_update/id

POST user/_update/1
{
  "doc": {
    "username": "test"
  }
}

直接更新 PUT index/_doc/id

PUT user/_doc/1
{
  "username": "test",
  "phone": "123456"
}

文档删除

# DELETE index/_doc/id
DELETE user/_doc/1

ES 基本查询

创建索引

PUT poetry
{
  "settings": {
    "number_of_shards": 3, 
    "number_of_replicas": 1
  },
  "mappings": {
    "dynamic": "false",
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart"
      },
      "author": {
        "type": "keyword"
      },
      "dynasty": {
        "type": "keyword"
      },
      "content": {
        "type": "text",
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart"
      }
    }
  }
}

插入数据

POST poetry/_doc
{
  "name": "望庐山瀑布",
  "author": "李白",
  "dynasty": "唐代",
  "content": "日照香炉生紫烟,遥看瀑布挂前川。飞流直下三千尺,疑是银河落九天。"
}

  • 组合条件查询

    must、must not、should 区别:

    • must 返回的文档必须满足 must 字句的条件,类似于 == and
    • must not 返回的文档必须不满足must not 字句的条件,类似于 != not
    • should 返回的文档只要满足 should 中的一个条件即可,类似于 || or
  • 各类查询参数

    • term

      • 把检索串当做一个整体来执行检索,即不会对检索串分词
      {
      	"query": {
      		"bool": {
      			"must": [{
      				"term": {
      					"content": "明月"
      				}
      			}],
      			"must_not": [],
      			"should": []
      		}
      	},
      	"from": 0,
      	"size": 10,
      	"sort": [],
      	"aggs": {}
      }
      

      GET poetry/_search
      {
        "query": {
          "term": {
            "content": "明月"
          }
        }
      }
      

    • match

      • 会将检索词进行分词,然后对数据进行匹配
      {
      	"query": {
      		"bool": {
      			"must": [{
      				"match": {
      					"content": "明月香炉"
      				}
      			}],
      			"must_not": [],
      			"should": []
      		}
      	},
      	"from": 0,
      	"size": 10,
      	"sort": [],
      	"aggs": {}
      }
      

      GET poetry/_search
      {
        "query": {
          "match": {
            "content": "明月香炉"
          }
        }
      }
      

    • prefix

      • 前缀检索
      • 扫描所有倒排索引, 性能较差
      GET poetry/_search
      {
        "query": {
          "prefix": {
            "content": "人生"
          }
        }
      }
      

    • wildcard

      • 通配符检索
      • 扫描所有倒排索引, 性能较差
      GET poetry/_search
      {
        "query": {
          "wildcard": {
            "content": "生*"
          }
        }
      }
      

    • fuzzy

      • 纠错检索
      • fuzziness 的默认值是2 ——表示最多可以纠错两次
      • fuzziness 的值太大, 将削弱检索条件的作用, 也就是说纠错次数太多, 就会导致限定检索结果的检索条件被改变, 失去了限定作用
      GET poetry/_search
      {
        "query": {
          "fuzzy": {
            "content": {
              "value": "人生大事",
              "fuzziness": 2
            }
          }
        }
      }
      

    • range

      • 区间查询,如果 type 是时间类型,可以内置 now 表示当前,-1d/h/m/s 来进行时间操作
    • query_string

      • 可以对 int、long、string 查询,对 int、long 只能本身查询,对比 string 进行分词和本身查询
    • missing

      • 返回没有字段或值为 null 的文本

ES 分片与备份

一台服务器上无法存储大量数据,ES把一个index里面的数据分成多个shard分布式的存储在多个服务器上(对大的索引分片,拆成多个,分不到不同的节点上)。

ES就是通过shard来解决节点的容量上限问题的,通过主分片可以将数据分布到集群内的所有节点上。主分片数是在索引创建时指定的,一般不允许修改,除非Reindex。

一个索引中的数据保存在多个分片中(默认为一个)相当于水平分表。一个分片表示一个Lucene的实例,它本身就是一个完整的搜索引擎。我们的文档被存储和索引到分片内,这些对应用程序是透明的,即应用程序直接与索引交互而不是分片。

由上图可以看到shard可以分为主分片(primary shard)和副分片(replaca shard)。

图示为三个节点。其中任何一个都是有可能故障或者宕机的,此时其承载的shard的数据就会丢失;通过设置一个或者多个replica shard就可以在发生故障的时候提供备份服务。一方面可以保证数据不丢失,另一方面还可以提升操作的吞吐量和性能。

  • 分片分为两种,主分片和副本:
    • 主分片用于解决数据水平扩展的问题,通过分片可以将数据分布到集群的所有节点之上
      • 一个分片是一个运行的ES实例
      • 分片数在索引创建时指定,后续不允许修改,除非 Reindex
    • 副本用于解决数据高可用的问题,副本是主分片的拷贝
      • 副本分片数,可以动态调整
      • 增加副本数,还可以在一定程度上提高服务的可用性(读取的吞吐)
  • 分片的设定:
    • 分片设置过大(分片数量少)
      • 导致后续无法增加节点实现水平扩展
      • 单个分片数据量过大,导致数据重新分片耗时
    • 分片设置过小(分片数量多)
      • 影响搜索结果的相关性打分,影响统计结果的准确性
      • 单个节点上过多分片,会导致资源浪费,同时会影响性能
      • 7.0 之后,默认的主分片是1,解决了 over-sharding 的问题
        • shard 也是一种资源,shard 过多会影响集群的稳定性,因为 shard 过多,元信息会变多,这些元信息会占用堆内存。shard 过多也会影响读写性能,因为每个读写请求都需要一个线程。

ES数据架构

Write 流程

  • ShardReplication 的路由规则

    • 每个 Index 由多个Shard 组成,每个 Shard 有一个主节点和多个副本节点,副本个数可配置
    • 但每次写入的时候,写入请求会根据 _routing 规则选择发给哪个 Shard
      • Index Request 中可以设置使用哪个 Filed 的值作为路由参数
      • 如果 Index 没有设置,则使用 Mapping 中的配置
      • 如果 Mapping 中也没有配置,则使用 id 作为路由参数,然后通过 _idHash 值选择出 Primary Shard
    • 请求会发送数据给 Primary Shard,在 Primary Shard 上执行成功后,再从 Primary Shard 上将请求同时发送给多个 Replica Shard

  • 数据安全策略

    • Elasticsearch 为了减少磁盘 IO 保证读写性能,一般是每隔一段时间才会把 Lucene 的 Segment 写入磁盘持久化
    • Elasticsearch 学习了数据库中的处理方式:增加 CommitLog 模块,Elasticsearch 中叫 TransLog
    • 写入请求到达 Shard 后,先写 Buffer 文件,创建好索引,此时索引还在内存里面,接着去写 TransLog
    • 写完 TransLog 后,刷新 TransLog 数据到磁盘上,写磁盘成功后,请求返回给用户
    • 具体操作细节:
      • 和数据库不同,数据库是先写 CommitLog,然后再写内存,而 Elasticsearch 是先将数据写入内存,最后才写 TransLog
      • Buffer 后,文档并不是可被搜索的,需要通过 Refresh 把内存的对象转化成完整的 Segment 后,然后再次 reopen 后才能被搜索
        • 一般这个时间设置为1秒钟,导致写入 Elasticsearch 的文档,最快需要1秒才可以被搜索到
        • Segment 中的文档可以被搜索到,但是尚未被写入硬盘,即如果此时发送断电,则这些文档可能会丢失
      • Elasticsearch 会在每隔5秒钟或是一次写入请求后将 TransLog 写入磁盘,操作记录被写入磁盘 ES 才会将操作成功的结果返回发送给此操作请求的客户端
      • TransLog 保证安全 =>(buffer + segment
      • 每隔一秒将生成一个新的 SegmentSegment 文件越来越多(内存),而 TransLog 文件将越来越大(硬盘)
      • 每隔 30 分钟或者 TransLog 文件变得很大时,将执行一次 fsync 操作,此时所有在文件系统缓存中的 Segment 将被写入磁盘,而 TransLog 将被删除

Delete 流程

如果是每一次删除,都需要从集群中找到存储那个数据的节点进行删除,那么效率也太低了吧?

Elasticsearch 是这么操作的:

如果是删除请求的话,提交的时候会生成一个 .del 文件,里面将某个 doc 标识为 deleted 状态,那么搜索的时候根据 .del 文件就知道这个 doc 被删除了,客户端搜索的时候,发现数据在 .del 文件中标识为删除状态就不会被搜索到了

Update 流程

  • ES的索引是不能修改的,因此更新和删除操作并不是直接在原索引上直接执行
  • 收到 Update请求后,从 Segment 或者 TransLog 中读取同 id 的完整 Doc,记录版本号为 V1 = 345
  • 将版本 V1 的全量 Doc 和请求中的部分字段 Doc 合并为一个完整的 Doc,同时更新内存中的 VersionMap
  • 获取到完整的 Doc 后,Update 请求就变成了 POST/PUT 请求
  • 加锁
  • 再次从 VersionMap 中读取该 id 的最大版本号 V2 = 346
  • 检测版本是否冲突(V1 == V2),如果冲突,则回退到开始的 Update doc 阶段重新执行,如果不冲突,则执行最新的 Add 请求
  • Index Doc 阶段,首先将 version + 1 得到 V3,再将 Doc 加入到 Lucene 中去,Lucene 中会先删除同 id 下的已存在 doc id,然后再增加新的 Doc。写入 Lucene 成功后,将当前 V3 更新到 VersionMap

Read 流程

  • 查询的过程大体上分为查询 query 和取回 fetch 两个阶段
  • 这个节点的任务是广播查询请求到所有相关分片,并将它们的响应整合成全局排序后的结果集合,这个结果集合会返回给客户端
  • 查询过程
    • 当一个节点接收到一个搜索请求,则这个节点就变成了协调节点
    • 如果客户端要求返回结果排序中从第 from 名开始的数量为 size 的结果集,则每个节点都需要生成一个 from + size 大小的结果集
    • 分片仅会返回一个轻量级的结果给协调节点,包含结果集中的每一个文档的 ID 和进行排序所需要的信息
    • 协调节点会将所有分片的结果汇总,并进行全局排序,得到最终的查询排序结果
  • 取回过程
    • 协调节点会确定实际需要返回的文档,并向含有该文档的分片发送给 get 请求
    • 分片获取文档返回给协调节点,协调节点将结果返回给客户端
  • 相关性计算
    • 判断文档与搜索条件的相关程度
    • TFIDF
    • BM25评分算法

实时性

  • Elasticsearch 的主要应用场景就是实时,但 Elasticsearch 本身并非实时而是 near-real-time (近实时)。Index 的实时性是由 refresh 控制的,默认是 1s,最快可到 100ms,那么也就意味着 index doc 成功后,需要等待一秒钟后才可以被搜索到。
  • Elasticsearch 中的 Get 请求也能保证是实时的,因为 Get 请求会直接读内存中尚未 Flush 到磁盘的 TransLog。但是 Get 请求只支持通过 doc_id 进行查询,所以对于条件查询依然无法实现实时。
  • 一般这个时间设置为1秒钟,导致写入 Elasticsearch 的文档最快需要1秒钟才可以被搜索到,所以 Elasticsearch 在搜索方面是 NRT(Near Real Time) 近实时的系统
  • 当 Elasticsearch 作为 NoSQL 数据库时,查询方式是 GetById,这种查询可用直接从 TransLog 中查询,这时候就成了 RT(Real Time) 实时系统,只能根据 ID 进行查询

可靠性

  • 搜索系统对可靠性要求都不高,一般数据的可靠性通过将原始数据存储在另一个存储系统来保证,当搜索系统数据发生丢失时,再从其他存储系统导一份数据过来重新 rebuild 就可以了。
  • 在 Elasticsearch 中,通过设置 TransLogFlush 频率可以控制可靠性,要么是按请求,每次请求都 Flush;要么是按时间,每隔一段时间 Flush 一次
  • 一般为了性能考虑,会设置每隔5秒或者1分钟 Flush 一次,Flush 间隔时间越长,可靠性就会越低

ES JavaAPI

Elasticsearch 有两种连接方式:transportrest

  • transport 通过 TCP 方式访问 ES(只支持java)

  • rest 方式通过 http API 访问 ES(没有语言限制)

ES 官方建议使用 rest 方式transport7.0 版本中不建议使用,在 8.x 的版本中废弃

你可以用 java 客户端做很多事:

  • 执行标准的 indexgetdeleteupdatesearch 等操作

  • 在正在运行的集群上执行管理任务

通过官方文档可以得知,现在存在至少三种 java 客户端:

  • Transport Client
  • Java Low Level Rest Client
  • Java High Level Rest Client

造成这种混乱的原因是:

  • 长久以来,ES 并没有官方的 java 客户端,并且 java 自身是可以简单支持 ES 的 API 的,于是就先做成了 TransportClient。但是 TransportClient 的缺点是显而易见的,它没有使用 RESRful 风格的接口,而是二进制的方式传输数据
  • 之后 ES 官方推出了 Java Low Level Rest Client,它支持 RESTful,用起来也不错,但是缺点也很明显,因为 TransportClient 的使用者把代码迁移到 Low Level Rest Client 的工作量比较大。官方文档专门为迁移代码出了一堆文档来提供参考
  • 现在 ES 官方推出了 Java High Level Rest Client,它是基于 Java Low Level Rest Client 的封装,并且 API 接收参数和返回值与 TransportClient 是一样的,使得代码迁移变得容易并且支持了 RESTful 风格,兼容了这两种客户端的优点。当然缺点也是存在的,就是版本的问题,ES 的小版本更新非常频繁,在最理想的情况下,客户端的版本要和 ES 的版本一致(至少主版本号一致),次版本号不一致的话,基本操作也许可以,但是新的 API 就不支持了。

下面介绍下 SpringBoot 如何通过 elasticsearch-rest-high-level-client 工具操作 ElasticSearch,这里需要说一下,为什么没有使用 Spring 家族封装的 spring-data-elasticsearch。

主要原因是灵活性和更新速度,Spring 将 ElasticSearch 过度封装,让开发者很难跟 ES 的 DSL 查询语句进行关联。再者就是更新速度,ES 的更新速度是非常快,但是 spring-data-elasticsearch 更新速度比较缓慢。

由于上面两点,所以选择了官方推出的 Java 客户端 elasticsearch-rest-high-level-client,它的代码写法跟 DSL 语句很相似,懂 ES 查询的使用其上手很快。

Maven 相关依赖

pom.xml

<!--json解析工具-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.68</version>
</dependency>

<!-- 单元测试 -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
</dependency>

<!-- Elasticsearch 服务端依赖 -->
<dependency>
    <groupId>org.elasticsearch</groupId>
    <artifactId>elasticsearch</artifactId>
    <version>7.17.2</version>
</dependency>

<!-- rest-client 客户端依赖 -->
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-client</artifactId>
    <version>7.17.2</version>
</dependency>

<!-- rest-high-level-client 客户端依赖 -->
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>7.17.2</version>
</dependency>

ElasticSearch 连接配置

application.yml 配置文件

elasticsearch:
  schema: http
  address: 192.168.10.201:9200,192.168.10.202:9200,192.168.10.203:9200
  connectTimeout: 5000
  soketTimeout: 5000
  connectionRequestTimeout: 5000
  maxConnectNum: 100
  maxConnectPerRoute: 100

java 连接配置类:

package com.moon.es.core.configuration;

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.List;

@Configuration
public class ElasticsearchConfiguration {

    /** 协议 */
    @Value("${elasticsearch.schema:http}")
    private String schema;

    /** 集群地址,如果有多个用 "," 隔开 */
    @Value("${elasticsearch.address}")
    private String address;

    /** 连接超时时间 */
    @Value("${elasticsearch.connectTimeout}")
    private Integer connectTimeout;

    /** socket 连接超时时间 */
    @Value("${elasticsearch.socketTimeout}")
    private Integer socketTimeout;

    /** 获取连接的超时时间 */
    @Value("${elasticsearch.connectionRequestTimeout}")
    private Integer connectionRequestTimeout;

    /** 最大连接数 */
    @Value("${elasticsearch.maxConnectNum}")
    private Integer maxConnectNum;

    /** 最大路由连接数 */
    @Value("${elasticsearch.maxConnectPerRoute}")
    private Integer maxConnectPerRoute;

    @Bean
    public RestHighLevelClient restHighLevelClient() {
        // 拆分地址
        List<HttpHost> hostList = new ArrayList<>();
        String[] hosts = this.address.split(",");
        for (String address : hosts) {
            String host = address.split(":")[0];
            String port = address.split(":")[1];
            hostList.add(new HttpHost(host, Integer.parseInt(port), this.schema));
        }

        // 转换成 HttpHost 数组
        HttpHost[] httpHosts = hostList.toArray(new HttpHost[]{});

        // 构建连接对象
        RestClientBuilder builder = RestClient.builder(httpHosts);

        // 异步连接延时配置
        builder.setRequestConfigCallback(requestConfigBuilder -> {
            requestConfigBuilder.setConnectTimeout(this.connectTimeout);
            requestConfigBuilder.setSocketTimeout(this.socketTimeout);
            requestConfigBuilder.setConnectionRequestTimeout(this.connectionRequestTimeout);
            return requestConfigBuilder;
        });

        // 异步连接数配置
        builder.setHttpClientConfigCallback(httpClientBuilder -> {
            httpClientBuilder.setMaxConnTotal(this.maxConnectNum);
            httpClientBuilder.setMaxConnPerRoute(this.maxConnectPerRoute);
            return httpClientBuilder;
        });

        return new RestHighLevelClient(builder);
    }

}

索引操作示例

创建名为 user 的索引与对应 Mapping

PUT /user
{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 1
  },
  "mappings": {
    "dynamic": true,
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart",  
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      },
      "address": {
        "type": "text",
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart",
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }  
      },
      "remark": {
        "type": "text",
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart"
      },
      "age": {
        "type": "integer"
      },
      "salary": {
        "type": "float"
      },
      "birthDate": {
        "type": "date",
        "format": "yyyy-MM-dd"
      },
      "createTime": {
        "type": "date"
      }
    }
  }
}

代码示例:

package com.moon.es.service.impl;

import lombok.extern.slf4j.Slf4j;
import com.moon.es.service.IndexService;
import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
import org.elasticsearch.action.admin.indices.create.CreateIndexResponse;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.xcontent.XContentBuilder;
import org.elasticsearch.xcontent.XContentFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.IOException;

@Slf4j
@Service
public class IndexServiceImpl implements IndexService {

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    @Value("${elasticsearch.indexName}")
    private String indexName;

    /**
     * 创建索引
     * @return boolean
     */
    @Override
    public boolean createIndex() {
        boolean result = false;
        try {
            // 创建 Mapping
            XContentBuilder mapping = XContentFactory.jsonBuilder()
                    .startObject()
                        .field("dynamic", true)
                        .startObject("properties")
                            .startObject("name")
                                .field("type","text")
                                .field("analyzer", "ik_max_word")
                                .field("search_analyzer", "ik_smart")
                                .startObject("fields")
                                    .startObject("keyword")
                                        .field("type","keyword")
                                    .endObject()
                                .endObject()
                            .endObject()
                            .startObject("address")
                                .field("type","text")
                                .field("analyzer", "ik_max_word")
                                .field("search_analyzer", "ik_smart")
                                .startObject("fields")
                                    .startObject("keyword")
                                        .field("type","keyword")
                                    .endObject()
                                .endObject()
                            .endObject()
                            .startObject("remark")
                                .field("type","text")
                                .field("analyzer", "ik_max_word")
                                .field("search_analyzer", "ik_smart")
                            .endObject()
                            .startObject("age")
                                .field("type","integer")
                            .endObject()
                            .startObject("salary")
                                .field("type","float")
                            .endObject()
                            .startObject("birthDate")
                                .field("type","date")
                                .field("format", "yyyy-MM-dd")
                            .endObject()
                            .startObject("createTime")
                                .field("type","date")
                            .endObject()
                        .endObject()
                    .endObject();

            // 创建索引配置信息
            Settings settings = Settings.builder()
                    .put("index.number_of_shards", 3)
                    .put("index.number_of_replicas", 1)
                    .build();

            // 新建创建索引请求对象,然后设置索引类型(ES 7.0 将不存在索引类型)和 mapping 与 index 配置
            CreateIndexRequest request = new CreateIndexRequest(this.indexName, settings);
            request.mapping("_doc", mapping);

            // RestHighLevelClient 执行创建索引
            CreateIndexResponse createIndexResponse = restHighLevelClient.indices().create(request, RequestOptions.DEFAULT);
            result = createIndexResponse.isAcknowledged();
            log.info("是否创建 index 成功: {}", result);
        } catch (IOException e) {
            log.error("创建 index 异常", e);
        }

        return result;
    }

    /**
     * 删除索引
     * @return boolean
     */
    @Override
    public boolean deleteIndex() {
        boolean result = false;
        try {
            // 新建删除索引请求对象
            DeleteIndexRequest request = new DeleteIndexRequest(this.indexName);

            // 执行删除索引
            AcknowledgedResponse response = restHighLevelClient.indices().delete(request, RequestOptions.DEFAULT);

            result = response.isAcknowledged();
            log.info("是否删除 index 成功: {}", result);
        } catch (IOException e) {
            log.error("删除 index 异常", e);
        }

        return result;
    }
}

单元测试:

package com.moon.es;

import com.moon.es.service.IndexService;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;

@WebAppConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class ElasticsearchTest {
    @Autowired
    private IndexService indexService;

    @Test
    public void indexCreate() {
        boolean result = this.indexService.createIndex();
        Assert.assertTrue("创建索引成功!", result);
    }

    @Test
    public void indexDelete() {
        boolean result = this.indexService.deleteIndex();
        Assert.assertTrue("删除索引成功!", result);
    }
}

文档操作示例

// 新增文档信息
POST /user/
{
    "address": "北京市",
    "age": 29,
    "birthDate": "1990-01-10",
    "createTime": 1579530727699,
    "name": "张三",
    "remark": "来自北京市的张先生",
    "salary": 100
}

// 获取文档信息
GET /user/doc/1

// 更新文档信息
PUT /user/1
{
    "address": "北京市海淀区",
    "age": 29,
    "birthDate": "1990-01-10",
    "createTime": 1579530727699,
    "name": "张三",
    "remark": "来自北京市的张先生",
    "salary": 100
}

// 删除文档信息
DELETE /user/1

实体类:

package com.moon.es.entity;

import lombok.*;

import java.math.BigDecimal;
import java.util.Date;

@Getter
@Setter
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private String name;
    private Integer age;
    private BigDecimal salary;
    private String address;
    private String remark;
    private String birthDate;
    private Date createTime;
}

代码示例:

package com.moon.es.service.impl;

import com.alibaba.fastjson.JSON;
import com.moon.es.entity.User;
import com.moon.es.service.DocumentService;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.action.update.UpdateResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.xcontent.XContentType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.math.BigDecimal;
import java.util.Date;

@Service
@Slf4j
public class DocumentServiceImpl implements DocumentService {

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    @Value("${elasticsearch.indexName}")
    private String indexName;

    /**
     * 新增文档
     * @return status
     */
    @Override
    public int addDocument() {
        int status = 400;
        try {
            // 创建索引请求对象
            IndexRequest request = new IndexRequest(this.indexName, "_doc");

            User user = User.builder()
                    .name("张三")
                    .age(29)
                    .salary(new BigDecimal("100.00"))
                    .address("北京市")
                    .remark("来自北京市的张先生")
                    .birthDate("1990-01-10")
                    .createTime(new Date())
                    .build();

            // 将对象转换为 byte 数组
            byte[] json = JSON.toJSONBytes(user);

            // 设置文档内容
            request.source(json, XContentType.JSON);

            IndexResponse response = restHighLevelClient.index(request, RequestOptions.DEFAULT);
            status = response.status().getStatus();

            log.info("新增文档状态: {}", status);
        } catch (IOException e) {
            log.error("新增文档异常", e);
        }

        return status;
    }

    /**
     * 获取文档信息
     * @param id 文档id
     * @return user
     */
    @Override
    public User getDocument(String id) {
        User user = null;
        try {
            GetRequest request = new GetRequest(this.indexName, "_doc", id);
            GetResponse response = restHighLevelClient.get(request, RequestOptions.DEFAULT);
            if (response.isExists()) {
                user = JSON.parseObject(response.getSourceAsBytes(), User.class);
                log.info("获取文档信息: {}", user);
            }
        } catch (IOException e) {
            log.error("获取文档信息异常", e);
        }
        return user;
    }

    /**
     * 更新文档信息
     * @param id 文档id
     * @return status
     */
    @Override
    public int updateDocument(String id) {
        int status = 400;
        try {
            UpdateRequest request = new UpdateRequest(this.indexName, "_doc", id);
            User user = User.builder()
                    .salary(new BigDecimal("200.00"))
                    .address("北京市海淀区")
                    .build();
            byte[] json = JSON.toJSONBytes(user);
            request.doc(json, XContentType.JSON);
            UpdateResponse response = restHighLevelClient.update(request, RequestOptions.DEFAULT);
            status = response.status().getStatus();

            log.info("更新文档状态: {}", status);
        } catch (IOException e) {
            log.error("更新文档信息异常", e);
        }
        return status;
    }

    /**
     * 删除文档信息
     * @param id 文档id
     * @return status
     */
    @Override
    public int deleteDocument(String id) {
        int status = 400;
        try {
            DeleteRequest request = new DeleteRequest(this.indexName, "_doc", id);
            DeleteResponse response = restHighLevelClient.delete(request, RequestOptions.DEFAULT);
            status = response.status().getStatus();

            log.info("删除文档状态: {}", status);
        } catch (IOException e) {
            log.error("删除文档信息异常", e);
        }
        return status;
    }
}

单元测试:

@Test
public void docCreate() {
    int status = this.documentService.addDocument();
    Assert.assertEquals("新增文档成功!", 201, status);
}

@Test
public void docQuery() {
    User user = this.documentService.getDocument("1");
    Assert.assertNotNull("查询文档成功!", user);
}

@Test
public void docUpdate() {
    int status = this.documentService.updateDocument("1");
    Assert.assertEquals("更新文档成功!", 200, status);
}

@Test
public void docDelete() {
    int status = this.documentService.deleteDocument("1");
    Assert.assertEquals("删除文档成功!", 200, status);
}

查询操作示例

精确查询 term

// 精确查询
// 查询条件不会进行分词,但是查询内容可能会分词,导致查询不到
// 之前在创建索引时设置 Mapping 中 address 字段存在 keyword 字段是专门用于不分词查询的子字段。
GET /user/_search
{
  "query": {
    "term": {
      "address.keyword": {
        "value": "北京市通州区"
      }
    }
  }
}

// 多内容查询精确查询
GET /user/_search
{
  "query": {
    "terms": {
      "address.keyword": [
        "北京市丰台区",
        "北京市昌平区",
        "北京市大兴区"
      ]
    }
  }
}

代码示例:

package com.moon.es.service.impl;

import com.alibaba.fastjson.JSON;
import com.moon.es.entity.User;
import com.moon.es.service.TermQueryService;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@Slf4j
@Service
public class TermQueryServiceImpl implements TermQueryService {

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    @Value("${elasticsearch.indexName}")
    private String indexName;

    /**
     * 精确查询
     * 查询条件不会进行分词,但是查询内容可能会分词,导致查询不到
     * @param field 查询字段
     * @param keyword 查询关键词
     * @return userList
     */
    @Override
    public List<User> termQuery(String field, String keyword) {
        List<User> userList = new ArrayList<>();

        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(QueryBuilders.termQuery(field, keyword));

        SearchRequest request = new SearchRequest(this.indexName);
        request.source(sourceBuilder);
        try {
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            // 根据状态和数据条数验证是否返回了数据
            if (response.status().equals(RestStatus.OK) && response.getHits().getTotalHits().value > 0) {
                SearchHits hits = response.getHits();
                for (SearchHit hit : hits) {
                    User user = JSON.parseObject(hit.getSourceAsString(), User.class);
                    userList.add(user);
                }
                log.info("精确查询成功: {}", userList);
            }
        } catch (IOException e) {
            log.error("精确查询异常", e);
        }

        return userList;
    }

    /**
     * 多个内容在一个字段中进行查询
     * @param field 查询字段
     * @param keywords 多个查询关键字
     * @return userList
     */
    @Override
    public List<User> termsQuery(String field, String... keywords) {
        List<User> userList = new ArrayList<>();

        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(QueryBuilders.termsQuery(field, keywords));

        SearchRequest request = new SearchRequest(this.indexName);
        request.source(sourceBuilder);
        try {
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            if (response.status().equals(RestStatus.OK) && response.getHits().getTotalHits().value > 0) {
                SearchHits hits = response.getHits();
                for (SearchHit hit : hits) {
                    User user = JSON.parseObject(hit.getSourceAsString(), User.class);
                    userList.add(user);
                }
                log.info("多内容精确查询成功: {}", userList);
            }
        } catch (IOException e) {
            log.error("多内容精确查询异常", e);
        }

        return userList;
    }
}

单元测试:

@Test
public void termQuery() {
    List<User> userList = this.termQueryService.termQuery("address.keyword", "北京市通州区");
    Assert.assertNotEquals("精确查询成功!", 0, userList.size());
}

@Test
public void termsQuery() {
    List<User> userList = this.termQueryService.termsQuery("address.keyword", "北京市海淀区", "北京市昌平区", "北京市大兴区");
    Assert.assertNotEquals("多内容精确查询成功!", 0, userList.size());
}

匹配查询 match

// 匹配查询全部数据与分页
// 匹配查询符合条件的所有数据,并且设置以 salary 字段升序排序,并设置分页
GET /user/_search
{
  "query": {
    "match_all": {}
  },
  "from": 0,
  "size": 10,
  "sort": [
    {
      "salary": {
        "order": "asc"
      }
    }
  ]
}

// 匹配查询数据
GET /user/_search
{
  "query": {
    "match": {
      "address": "*通州区"
    }
  }
}

//词语匹配查询
GET user/_search
{
  "query": {
    "match_phrase": {
      "address": "通州区"
    }
  }
}

// 内容多字段查询
GET user/_search
{
  "query": {
    "multi_match": {
      "query": "北京",
      "fields": [
        "address",
        "remark"
      ]
    }
  }
}

示例代码:

package com.moon.es.service.impl;

import com.alibaba.fastjson.JSON;
import com.moon.es.entity.User;
import com.moon.es.service.MatchQueryService;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@Slf4j
@Service
public class MatchQueryServiceImpl implements MatchQueryService {

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    @Value("${elasticsearch.indexName}")
    private String indexName;

    /**
     * 匹配查询符合条件的所有数据,并设置分页
     * @param from 分页偏移量
     * @param size 每页条数
     * @param sortField 排序字段
     * @return userList
     */
    @Override
    public List<User> matchAllQuery(Integer from, Integer size, String sortField) {
        List<User> userList = new ArrayList<>();

        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(QueryBuilders.matchAllQuery());
        sourceBuilder.from(from);
        sourceBuilder.size(size);
        sourceBuilder.sort(sortField, SortOrder.ASC);

        SearchRequest request = new SearchRequest(this.indexName);
        request.source(sourceBuilder);

        try {
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            if (response.status().equals(RestStatus.OK) && response.getHits().getTotalHits().value > 0) {
                SearchHits hits = response.getHits();
                for (SearchHit hit : hits) {
                    User user = JSON.parseObject(hit.getSourceAsString(), User.class);
                    userList.add(user);
                }
                log.info("全匹配查询: {}", userList);
            }
        } catch (IOException e) {
            log.error("全匹配查询异常", e);
        }

        return userList;
    }

    /**
     * 匹配查询数据
     * @param field 查询字段
     * @param keyword 查询关键字
     * @return userList
     */
    @Override
    public List<User> matchQuery(String field, String keyword) {
        List<User> userList = new ArrayList<>();

        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(QueryBuilders.matchQuery(field, keyword));

        SearchRequest request = new SearchRequest(this.indexName);
        request.source(sourceBuilder);

        try {
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            if (response.status().equals(RestStatus.OK) && response.getHits().getTotalHits().value > 0) {
                SearchHits hits = response.getHits();
                for (SearchHit hit : hits) {
                    User user = JSON.parseObject(hit.getSourceAsString(), User.class);
                    userList.add(user);
                }
                log.info("匹配查询: {}", userList);
            }
        } catch (IOException e) {
            log.error("匹配查询异常", e);
        }
        return userList;
    }

    /**
     * 词语匹配查询
     * @param field 查询字段
     * @param keyword 查询关键词
     * @return userList
     */
    @Override
    public List<User> matchPhraseQuery(String field, String keyword) {
        List<User> userList = new ArrayList<>();

        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(QueryBuilders.matchPhraseQuery(field, keyword));

        SearchRequest request = new SearchRequest(this.indexName);
        request.source(sourceBuilder);

        try {
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            if (response.status().equals(RestStatus.OK) && response.getHits().getTotalHits().value > 0) {
                SearchHits hits = response.getHits();
                for (SearchHit hit : hits) {
                    User user = JSON.parseObject(hit.getSourceAsString(), User.class);
                    userList.add(user);
                }
                log.info("词语匹配查询: {}", userList);
            }
        } catch (IOException e) {
            log.error("词语匹配查询异常", e);
        }

        return userList;
    }

    /**
     * 内容在多字段中进行查询
     * @param keyword 查询关键字
     * @param fields 查询多字段
     * @return userList
     */
    @Override
    public List<User> matchMultiQuery(String keyword, String... fields) {
        List<User> userList = new ArrayList<>();

        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(QueryBuilders.multiMatchQuery(keyword, fields));

        SearchRequest request = new SearchRequest(this.indexName);
        request.source(sourceBuilder);

        try {
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            if (response.status().equals(RestStatus.OK) && response.getHits().getTotalHits().value > 0) {
                SearchHits hits = response.getHits();
                for (SearchHit hit : hits) {
                    User user = JSON.parseObject(hit.getSourceAsString(), User.class);
                    userList.add(user);
                }
                log.info("多字段查询: {}", userList);
            }
        } catch (IOException e) {
            log.error("多字段查询异常", e);
        }

        return userList;
    }
}

单元测试:

 @Test
public void matchAllQuery() {
    List<User> userList = this.matchQueryService.matchAllQuery(0, 10, "salary");
    Assert.assertNotEquals("全匹配查询成功!", 0, userList.size());
}

@Test
public void matchQuery() {
    List<User> userList = this.matchQueryService.matchQuery("address", "*通州区");
    Assert.assertNotEquals("匹配查询成功!", 0, userList.size());
}

@Test
public void matchPhraseQuery() {
    List<User> userList = this.matchQueryService.matchPhraseQuery("address", "通州区");
    Assert.assertNotEquals("词语匹配查询成功!", 0, userList.size());
}

@Test
public void matchMultiQuery() {
    List<User> userList = this.matchQueryService.matchMultiQuery("李先生大兴区", "address", "remark");
    Assert.assertNotEquals("词语匹配查询成功!", 0, userList.size());
}

模糊查询 fuzzy

// 模糊查询所有以 "三" 结尾的姓名
GET /user/_search
{
  "query": {
    "fuzzy": {
      "name": "三"
    }
  }
}

代码示例:

package com.moon.es.service.impl;

import com.alibaba.fastjson.JSON;
import com.moon.es.entity.User;
import com.moon.es.service.FuzzyQueryService;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.unit.Fuzziness;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@Slf4j
@Service
public class FuzzyQueryServiceImpl implements FuzzyQueryService {

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    @Value("${elasticsearch.indexName}")
    private String indexName;

    /**
     * 模糊查询所有以 [keyword] 结尾的文档
     * @param field 查询字段
     * @param keyword 查询关键字
     * @return userList
     */
    @Override
    public List<User> fuzzyQuery(String field, String keyword) {
        List<User> userList = new ArrayList<>();

        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(QueryBuilders.fuzzyQuery(field, keyword).fuzziness(Fuzziness.AUTO));

        SearchRequest request = new SearchRequest(this.indexName);
        request.source(sourceBuilder);

        try {
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            if (response.status().equals(RestStatus.OK) && response.getHits().getTotalHits().value > 0) {
                SearchHits hits = response.getHits();
                for (SearchHit hit : hits) {
                    User user = JSON.parseObject(hit.getSourceAsString(), User.class);
                    userList.add(user);
                }
                log.info("模糊查询: {}", userList);
            }
        } catch (IOException e) {
            log.error("模糊查询异常", e);
        }

        return userList;
    }
}

单元测试:

@Test
public void fuzzyQuery() {
    List<User> userList = this.fuzzyQueryService.fuzzyQuery("name", "三");
    Assert.assertNotEquals("模糊查询成功!", 0, userList.size());
}

范围查询 range

// 查询岁数 ≥ 30 岁的员工数据:
GET /user/_search
{
  "query": {
    "range": {
      "age": {
        "gte": 30
      }
    }
  }
}

// 查询生日距离现在 30 年间的员工数据:
GET /user/_search
{
  "query": {
    "range": {
      "birthDate": {
        "gte": "now-30y"
      }
    }
  }
}

代码示例:

package com.moon.es.service.impl;

import com.alibaba.fastjson.JSON;
import com.moon.es.entity.User;
import com.moon.es.service.RangeQueryService;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@Slf4j
@Service
public class RangeQueryServiceImpl implements RangeQueryService {

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    @Value("${elasticsearch.indexName}")
    private String indexName;

    /**
     * 查询岁数 ≥ [value] 岁的员工数据
     * @param field 查询字段
     * @param value 范围值
     * @return userList
     */
    @Override
    public List<User> rangeGteQuery(String field, Integer value) {
        List<User> userList = new ArrayList<>();

        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(QueryBuilders.rangeQuery(field).gte(value));

        SearchRequest request = new SearchRequest(this.indexName);
        request.source(sourceBuilder);

        try {
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            if (response.status().equals(RestStatus.OK) && response.getHits().getTotalHits().value > 0) {
                SearchHits hits = response.getHits();
                for (SearchHit hit : hits) {
                    User user = JSON.parseObject(hit.getSourceAsString(), User.class);
                    userList.add(user);
                }
                log.info("gte 范围查询: {}", userList);
            }
        } catch (IOException e) {
            log.error("gte 范围查询异常", e);
        }

        return userList;
    }

    /**
     * 查询距离现在 [value] 年间的员工数据
     * [年(y)、月(M)、星期(w)、天(d)、小时(h)、分钟(m)、秒(s)]
     * now-1h 查询一小时内范围
     * now-1d 查询一天内时间范围
     * now-1y 查询最近一年内的时间范围
     * @param field 查询字段
     * @param value 范围值
     * @return userList
     */
    @Override
    public List<User> rangeDateQuery(String field, String value) {
        List<User> userList = new ArrayList<>();

        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        // includeLower(是否包含下边界)、includeUpper(是否包含上边界)
        sourceBuilder.query(QueryBuilders.rangeQuery(field)
                .gte(value).includeLower(true).includeUpper(true));

        SearchRequest request = new SearchRequest(this.indexName);
        request.source(sourceBuilder);

        try {
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            if (response.status().equals(RestStatus.OK) && response.getHits().getTotalHits().value > 0) {
                SearchHits hits = response.getHits();
                for (SearchHit hit : hits) {
                    User user = JSON.parseObject(hit.getSourceAsString(), User.class);
                    userList.add(user);
                }
                log.info("date 范围查询: {}", userList);
            }
        } catch (IOException e) {
            log.error("date 范围查询异常", e);
        }

        return userList;
    }
}

单元测试:

@Test
public void rangeGteQuery() {
    List<User> userList = rangeQueryService.rangeGteQuery("age", 30);
    Assert.assertNotEquals("gte 范围查询成功!", 0, userList.size());
}

@Test
public void rangeDateQuery() {
    List<User> userList = rangeQueryService.rangeDateQuery("birthDate", "now-30y");
    Assert.assertNotEquals("date 范围查询成功!", 0, userList.size());
}

通配符查询 wildcard

// 查询所有以 "张" 开头的姓名:
GET /user/_search
{
  "query": {
    "wildcard": {
      "name": {
        "value": "张*"
      }
    }
  }
}

代码示例:

package com.moon.es.service.impl;

import com.alibaba.fastjson.JSON;
import com.moon.es.entity.User;
import com.moon.es.service.WildcardQueryService;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@Slf4j
@Service
public class WildcardQueryServiceImpl implements WildcardQueryService {

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    @Value("${elasticsearch.indexName}")
    private String indexName;
    
    /**
     * 查询以 [keyword] 开头/结尾的文档
     * *: 表示多个字符(0个或多个字符)
     * ?: 表示单个字符
     * @param field 查询字段
     * @param keyword 查询关键字
     * @return userList
     */
    @Override
    public List<User> wildcardQuery(String field, String keyword) {
        List<User> userList = new ArrayList<>();

        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(QueryBuilders.wildcardQuery(field, keyword));

        SearchRequest request = new SearchRequest(this.indexName);
        request.source(sourceBuilder);

        try {
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            if (response.status().equals(RestStatus.OK) && response.getHits().getTotalHits().value > 0) {
                SearchHits hits = response.getHits();
                for (SearchHit hit : hits) {
                    User user = JSON.parseObject(hit.getSourceAsString(), User.class);
                    userList.add(user);
                }
                log.info("通配符查询: {}", userList);
            }
        } catch (IOException e) {
            log.error("通配符查询异常", e);
        }

        return userList;
    }
}

单元测试:

@Test
public void wildcardQuery() {
    List<User> userList = this.wildcardQueryService.wildcardQuery("name", "张*");
    Assert.assertNotEquals("通配符查询成功!", 0, userList.size());
}

布尔查询 bool

// 查询出生在 1990-1995 年期间,且地址在 北京市昌平区、北京市大兴区、北京市房山区 的员工信息:
GET /user/_search
{
  "query": {
    "bool": {
      "filter": {
        "range": {
          "birthDate": {
            "format": "yyyy",
            "gte": 1990,
            "lte": 1995
          }
        }
      },
      "must": [
        {
          "terms": {
            "address.keyword": [
              "北京市昌平区",
              "北京市大兴区",
              "北京市房山区"
            ]
          }
        }
      ]
    }
  }
}

代码示例:

package com.moon.es.service.impl;

import com.alibaba.fastjson.JSON;
import com.moon.es.entity.User;
import com.moon.es.service.BoolQueryService;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@Slf4j
@Service
public class BoolQueryServiceImpl implements BoolQueryService {

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    @Value("${elasticsearch.indexName}")
    private String indexName;

    /**
     * 查询出生在 1990-1995 年期间,且地址在 北京市昌平区、北京市大兴区、北京市房山区 的员工信息
     * @return userList
     */
    @Override
    public List<User> boolQuery() {
        List<User> userList = new ArrayList<>();

        // 创建 Bool 查询构建器
        BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
        queryBuilder.must(QueryBuilders.termsQuery("address.keyword", "北京市昌平区", "北京市大兴区", "北京市房山区"))
                .filter().add(QueryBuilders.rangeQuery("birthDate").format("yyyy").gte("1990").lte("1995"));

        // 构建查询源构建器
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(queryBuilder);

        // 创建查询请求对象,将查询对象配置到其中
        SearchRequest request = new SearchRequest(this.indexName);
        request.source(sourceBuilder);

        // 执行查询,然后处理响应结果
        try {
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            if (response.status().equals(RestStatus.OK) && response.getHits().getTotalHits().value > 0) {
                SearchHits hits = response.getHits();
                for (SearchHit hit : hits) {
                    User user = JSON.parseObject(hit.getSourceAsString(), User.class);
                    userList.add(user);
                }
                log.info("布尔查询: {}", userList);
            }
        } catch (IOException e) {
            log.error("布尔查询异常", e);
        }

        return userList;
    }
}

单元测试:

@Test
public void boolQuery() {
    List<User> userList = this.boolQueryService.boolQuery();
    Assert.assertNotEquals("布尔查询成功!", 0, userList.size());
}

聚合查询操作示例

度量聚合 Metric

// 统计员工总数、工资最高值、工资最低值、工资平均工资、工资总和:
GET /user/_search
{
  "size": 0,
  "aggs": {
    "salary_stats": {
      "stats": {
        "field": "salary"
      }
    }
  }
}

// 统计员工工资最低值:
GET /user/_search
{
  "size": 0,
  "aggs": {
    "salary_min": {
      "min": {
        "field": "salary"
      }
    }
  }
}

// 统计员工工资最高值:
GET /user/_search
{
  "size": 0,
  "aggs": {
    "salary_max": {
      "max": {
        "field": "salary"
      }
    }
  }
}

// 统计员工工资平均值:
GET /user/_search
{
  "size": 0,
  "aggs": {
    "salary_sum": {
      "sum": {
        "field": "salary"
      }
    }
  }
}

// 统计员工工资总值:
GET /user/_search
{
  "size": 0,
  "aggs": {
    "salary_sum": {
      "sum": {
        "field": "salary"
      }
    }
  }
}

// 统计员工总数:
GET /user/_search
{
  "size": 0,
  "aggs": {
    "employee_count": {
      "value_count": {
        "field": "salary"
      }
    }
  }
}

// 统计员工工资百分位:
GET /user/_search
{
  "size": 0,
  "aggs": {
    "salary_percentiles": {
      "percentiles": {
        "field": "salary"
      }
    }
  }
}

示例代码:

package com.moon.es.service.impl;

import com.moon.es.entity.Stats;
import com.moon.es.service.AggsMetricService;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.search.aggregations.AggregationBuilder;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.metrics.*;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Service
public class AggsMetricServiceImpl implements AggsMetricService {

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    @Value("${elasticsearch.indexName}")
    private String indexName;

    /**
     * stats 统计员工总数、员工工资最高值、员工工资最低值、员工平均工资、员工工资总和
     *
     * @return {@link Stats}
     */
    @Override
    public Stats aggregationStats() {
        Stats stats = null;

        // 设置聚合条件
        AggregationBuilder aggregationBuilder = AggregationBuilders.stats("salary_stats").field("salary");
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.aggregation(aggregationBuilder);
        // 设置查询结果不返回,只返回聚合结果
        sourceBuilder.size(0);

        SearchRequest request = new SearchRequest(this.indexName);
        request.source(sourceBuilder);

        try {
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            // 获取响应中的聚合信息
            Aggregations aggs = response.getAggregations();
            if (response.status().equals(RestStatus.OK) && aggs != null) {
                // 转换为 Stats 对象
                ParsedStats parsedStats = aggs.get("salary_stats");
                stats = Stats.builder()
                        .count(parsedStats.getCount())
                        .min(BigDecimal.valueOf(parsedStats.getMin()))
                        .max(BigDecimal.valueOf(parsedStats.getMax()))
                        .avg(BigDecimal.valueOf(parsedStats.getAvg()))
                        .sum(BigDecimal.valueOf(parsedStats.getSum()))
                        .build();
            }
            log.info("stats 度量聚合查询: {}", stats);
        } catch (IOException e) {
            log.error("stats 度量聚合查询异常", e);
        }

        return stats;
    }

    /**
     * min 统计员工工资最低值
     *
     * @return {@link BigDecimal}
     */
    @Override
    public BigDecimal aggregationMin() {
        BigDecimal min = BigDecimal.valueOf(-1);

        AggregationBuilder aggregationBuilder = AggregationBuilders.min("salary_min").field("salary");
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.aggregation(aggregationBuilder);
        sourceBuilder.size(0);

        SearchRequest request = new SearchRequest(this.indexName);
        request.source(sourceBuilder);

        try {
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            Aggregations aggs = response.getAggregations();
            if (response.status().equals(RestStatus.OK) && aggs != null) {
                ParsedMin parsedMin = aggs.get("salary_min");
                min = BigDecimal.valueOf(parsedMin.getValue());
            }
            log.info("min 度量聚合查询: {}", min);
        } catch (IOException e) {
            log.error("min 度量聚合查询异常", e);
        }

        return min;
    }

    /**
     * max 统计员工工资最高值
     *
     * @return {@link BigDecimal}
     */
    @Override
    public BigDecimal aggregationMax() {
        BigDecimal max = BigDecimal.valueOf(-1);

        AggregationBuilder aggregationBuilder = AggregationBuilders.max("salary_max").field("salary");
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.aggregation(aggregationBuilder);
        sourceBuilder.size(0);

        SearchRequest request = new SearchRequest(this.indexName);
        request.source(sourceBuilder);

        try {
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            Aggregations aggs = response.getAggregations();
            if (response.status().equals(RestStatus.OK) && aggs != null) {
                ParsedMax parsedMax = aggs.get("salary_max");
                max = BigDecimal.valueOf(parsedMax.getValue());
            }
            log.info("max 度量聚合查询: {}", max);
        } catch (IOException e) {
            log.error("max 度量聚合查询异常");
        }

        return max;
    }

    /**
     * avg 统计员工工资平均值
     *
     * @return {@link BigDecimal}
     */
    @Override
    public BigDecimal aggregationAvg() {
        BigDecimal avg = BigDecimal.valueOf(-1);

        AggregationBuilder aggregationBuilder = AggregationBuilders.avg("salary_avg").field("salary");
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.aggregation(aggregationBuilder);
        sourceBuilder.size(0);

        SearchRequest request = new SearchRequest(this.indexName);
        request.source(sourceBuilder);

        try {
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            Aggregations aggs = response.getAggregations();
            if (response.status().equals(RestStatus.OK) && aggs != null) {
                ParsedAvg parsedAvg = aggs.get("salary_avg");
                avg = BigDecimal.valueOf(parsedAvg.getValue());
            }
            log.info("avg 度量聚合查询: {}", avg);
        } catch (IOException e) {
            log.error("avg 度量聚合查询异常");
        }

        return avg;
    }

    /**
     * sum 统计员工工资总值
     *
     * @return {@link BigDecimal}
     */
    @Override
    public BigDecimal aggregationSum() {
        BigDecimal sum = BigDecimal.valueOf(-1);

        AggregationBuilder aggregationBuilder = AggregationBuilders.sum("salary_sum").field("salary");
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.aggregation(aggregationBuilder);
        sourceBuilder.size(0);

        SearchRequest request = new SearchRequest(this.indexName);
        request.source(sourceBuilder);

        try {
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            Aggregations aggs = response.getAggregations();
            if (response.status().equals(RestStatus.OK) && aggs != null) {
                ParsedSum parsedSum = aggs.get("salary_sum");
                sum = BigDecimal.valueOf(parsedSum.getValue());
            }
            log.info("sum 度量聚合查询: {}", sum);
        } catch (IOException e) {
            log.error("sum 度量聚合查询异常");
        }

        return sum;
    }

    /**
     * count 统计员工总数
     *
     * @return {@link Long}
     */
    @Override
    public Long aggregationCount() {
        long count = -1L;

        AggregationBuilder aggregationBuilder = AggregationBuilders.count("user_count").field("salary");
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.aggregation(aggregationBuilder);
        sourceBuilder.size(0);

        SearchRequest request = new SearchRequest(this.indexName);
        request.source(sourceBuilder);

        try {
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            Aggregations aggs = response.getAggregations();
            if (response.status().equals(RestStatus.OK) && aggs != null) {
                ParsedValueCount valueCount = aggs.get("user_count");
                count = valueCount.getValue();
            }
            log.info("count 度量聚合查询: {}", count);
        } catch (IOException e) {
            log.error("count 度量聚合查询异常");
        }

        return count;
    }

    /**
     * percentiles 统计员工工资百分位
     *
     * @return {@link Map}<{@link BigDecimal}, {@link BigDecimal}>
     */
    @Override
    public Map<BigDecimal, BigDecimal> aggregationPercentiles() {
        Map<BigDecimal, BigDecimal> map = new HashMap<>();

        AggregationBuilder aggregationBuilder = AggregationBuilders.percentiles("salary_percentiles").field("salary");
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.aggregation(aggregationBuilder);
        sourceBuilder.size(0);

        SearchRequest request = new SearchRequest(this.indexName);
        request.source(sourceBuilder);

        try {
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            Aggregations aggs = response.getAggregations();
            if (response.status().equals(RestStatus.OK) && aggs != null) {
                ParsedPercentiles percentiles = aggs.get("salary_percentiles");
                for (Percentile percentile : percentiles) {
                    map.put(BigDecimal.valueOf(percentile.getPercent()), BigDecimal.valueOf(percentile.getValue()));
                }
            }
            log.info("percentiles 度量聚合查询: {}", map);
        } catch (IOException e) {
            log.error("percentiles 度量聚合查询异常", e);
        }

        return map;
    }
}

单元测试:

@Test
public void aggregationMin() {
    BigDecimal min = this.aggsMetricService.aggregationMin();
    Assert.assertNotEquals("min 度量聚合查询成功!", -1, min.intValue());
}

@Test
public void aggregationMax() {
    BigDecimal max = this.aggsMetricService.aggregationMax();
    Assert.assertNotEquals("max 度量聚合查询成功!", -1, max.intValue());
}

@Test
public void aggregationAvg() {
    BigDecimal avg = this.aggsMetricService.aggregationAvg();
    Assert.assertNotEquals("avg 度量聚合查询成功!", -1, avg.intValue());
}

@Test
public void aggregationSum() {
    BigDecimal sum = this.aggsMetricService.aggregationSum();
    Assert.assertNotEquals("sum 度量聚合查询成功!", -1, sum.intValue());
}

@Test
public void aggregationCount() {
    Long count = this.aggsMetricService.aggregationCount();
    Assert.assertNotEquals("count 度量聚合查询成功!", -1, count.intValue());
}

@Test
public void aggregationPercentiles() {
    Map<BigDecimal, BigDecimal> map = this.aggsMetricService.aggregationPercentiles();
    Assert.assertNotEquals("percentiles 度量聚合查询成功", 0, map.size());
}

桶聚合 Bucket

// 按岁数进行聚合分桶,统计各个岁数员工的人数:
GET /user/_search
{
  "size": 0,
  "aggs": {
    "age_bucket": {
      "terms": {
        "field": "age",
        "size": "10"
      }
    }
  }
}

// 按工资范围进行聚合分桶,统计工资在 200以下、200-500 和 500 以上的员工信息:
GET /user/_search
{
  "size": 0,
  "aggs": {
    "salary_range_bucket": {
      "range": {
        "field": "salary",
        "ranges": [
          {
            "key": "初级员工",
            "to": 200
          },
          {
            "key": "中级员工",
            "from": 200,
            "to": 500
          },
          {
            "key": "高级员工",
            "from": 500
          }
        ]
      }
    }
  }
}

// 按照时间范围进行分桶,统计 1985-1990 年和 1990-1995 年出生的员工信息:
GET /user/_search
{
  "size": 0,
  "aggs": {
    "date_range_bucket": {
      "date_range": {
        "field": "birthDate",
        "format": "yyyy",
        "ranges": [
          {
            "key": "出生日期1990-1995的员工",
            "from": "1990",
            "to": "1995"
          },
          {
            "key": "出生日期1995-2000的员工",
            "from": "1995",
            "to": "2000"
          }
        ]
      }
    }
  }
}
 
// 按工资多少进行聚合分桶,设置统计的最小值为 0,最大值为 1000,区段间隔为 200:
GET /user/_search
{
  "size": 0,
  "aggs": {
    "salary_histogram": {
      "histogram": {
        "field": "salary",
        "extended_bounds": {
          "min": 0,
          "max": 1000
        },
        "interval": 200
      }
    }
  }
}

// 按出生日期进行分桶:
GET /user/_search
{
  "size": 0,
  "aggs": {
    "birthday_histogram": {
      "date_histogram": {
        "format": "yyyy",
        "field": "birthDate",
        "interval": "year"
      }
    }
  }
}

示例代码:

package com.moon.es.service.impl;

import com.moon.es.service.AggsBucketService;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.search.aggregations.AggregationBuilder;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval;
import org.elasticsearch.search.aggregations.bucket.histogram.Histogram;
import org.elasticsearch.search.aggregations.bucket.range.Range;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

@Slf4j
@Service
public class AggsBucketServiceImpl implements AggsBucketService {

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    @Value("${elasticsearch.indexName}")
    private String indexName;

    /**
     * 按岁数进行聚合分桶
     *
     * @return {@link Map}<{@link String}, {@link Long}>
     */
    @Override
    public Map<String, Long> bucketTerms() {
        Map<String, Long> map = new LinkedHashMap<>();

        AggregationBuilder aggregationBuilder = AggregationBuilders.terms("age_bucket").field("age");
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.size(0);
        sourceBuilder.aggregation(aggregationBuilder);

        SearchRequest request = new SearchRequest(this.indexName);
        request.source(sourceBuilder);

        try {
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            Aggregations aggs = response.getAggregations();
            if (response.status().equals(RestStatus.OK) && aggs != null) {
                // 分桶
                Terms termsBucket = aggs.get("age_bucket");
                List<? extends Terms.Bucket> buckets = termsBucket.getBuckets();

                for (Terms.Bucket bucket : buckets) {
                    map.put(bucket.getKeyAsString(), bucket.getDocCount());
                }
            }
            log.info("term 桶聚合查询: {}", map);
        } catch (IOException e) {
            log.error("term 桶聚合查询异常");
        }
        return map;
    }

    /**
     * 按工资范围进行聚合分桶
     *
     * @return {@link Map}<{@link String}, {@link Long}>
     */
    @Override
    public Map<String, Long> bucketRange() {
        Map<String, Long> map = new LinkedHashMap<>();

        AggregationBuilder aggregationBuilder = AggregationBuilders.range("salary_range_bucket")
                .field("salary")
                .addUnboundedTo("初级员工", 200)
                .addRange("中级员工", 200, 500)
                .addUnboundedFrom("高级员工", 500);
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.size(0);
        sourceBuilder.aggregation(aggregationBuilder);

        SearchRequest request = new SearchRequest(this.indexName);
        request.source(sourceBuilder);

        try {
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            Aggregations aggs = response.getAggregations();
            if (response.status().equals(RestStatus.OK) && aggs != null) {
                Range rangeBucket = aggs.get("salary_range_bucket");
                List<? extends Range.Bucket> buckets = rangeBucket.getBuckets();

                for (Range.Bucket bucket : buckets) {
                    map.put(bucket.getKeyAsString(), bucket.getDocCount());
                }
            }
            log.info("range 桶聚合查询: {}", map);
        } catch (IOException e) {
            log.error("range 桶聚合查询异常");
        }
        return map;
    }

    /**
     * 按照时间范围进行分桶
     *
     * @return {@link Map}<{@link String}, {@link Long}>
     */
    @Override
    public Map<String, Long> bucketDateRange() {
        Map<String, Long> map = new LinkedHashMap<>();

        AggregationBuilder aggregationBuilder = AggregationBuilders.dateRange("date_range_bucket")
                .field("birthDate")
                .format("yyyy")
                .addRange("1990-1995", "1990", "1995")
                .addRange("1995-2000", "1995", "2000");
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.size(0);
        sourceBuilder.aggregation(aggregationBuilder);

        SearchRequest request = new SearchRequest(this.indexName);
        request.source(sourceBuilder);

        try {
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            Aggregations aggs = response.getAggregations();
            if (response.status().equals(RestStatus.OK) && aggs != null) {
                Range dateRangeBucket = aggs.get("date_range_bucket");
                List<? extends Range.Bucket> buckets = dateRangeBucket.getBuckets();

                for (Range.Bucket bucket : buckets) {
                    map.put(bucket.getKeyAsString(), bucket.getDocCount());
                }
            }
            log.info("date_range 桶聚合查询: {}", map);
        } catch (IOException e) {
            log.error("date_range 桶聚合查询异常");
        }

        return map;
    }

    /**
     * 按工资多少进行聚合分桶
     *
     * @return {@link Map}<{@link String}, {@link Long}>
     */
    @Override
    public Map<String, Long> bucketHistogram() {
        Map<String, Long> map = new LinkedHashMap<>();

        AggregationBuilder aggregationBuilder = AggregationBuilders.histogram("salary_histogram")
                .field("salary")
                .extendedBounds(0, 1000)
                .interval(200);
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.size(0);
        sourceBuilder.aggregation(aggregationBuilder);

        SearchRequest request = new SearchRequest(this.indexName);
        request.source(sourceBuilder);

        try {
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            Aggregations aggs = response.getAggregations();
            if (response.status().equals(RestStatus.OK) && aggs != null) {
                Histogram histogramBucket = aggs.get("salary_histogram");
                List<? extends Histogram.Bucket> buckets = histogramBucket.getBuckets();
                for (Histogram.Bucket bucket : buckets) {
                    map.put(bucket.getKeyAsString(), bucket.getDocCount());
                }
            }
            log.info("histogram 桶聚合查询: {}", map);
        } catch (IOException e) {
            log.error("histogram 桶聚合查询异常");
        }

        return map;
    }

    /**
     * 按出生日期进行分桶
     *
     * @return {@link Map}<{@link String}, {@link Long}>
     */
    @Override
    public Map<String, Long> bucketDateHistogram() {
        Map<String, Long> map = new LinkedHashMap<>();

        AggregationBuilder aggregationBuilder = AggregationBuilders.dateHistogram("birthdate_histogram")
                .field("birthDate")
                .format("yyyy")
                .calendarInterval(DateHistogramInterval.YEAR);
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.size(0);
        sourceBuilder.aggregation(aggregationBuilder);

        SearchRequest request = new SearchRequest(this.indexName);
        request.source(sourceBuilder);

        try {
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            Aggregations aggs = response.getAggregations();
            if (response.status().equals(RestStatus.OK) && aggs != null) {
                Histogram dateHistogramBucket = aggs.get("birthdate_histogram");
                List<? extends Histogram.Bucket> buckets = dateHistogramBucket.getBuckets();

                for (Histogram.Bucket bucket : buckets) {
                    map.put(bucket.getKeyAsString(), bucket.getDocCount());
                }
            }
            log.info("date_histogram 桶聚合查询: {}", map);
        } catch (IOException e) {
            log.error("date_histogram 桶聚合查询异常");
        }

        return map;
    }
}

单元测试:

 @Test
public void bucketTerms() {
    Map<String, Long> map = this.aggsBucketService.bucketTerms();
    Assert.assertNotEquals("term 桶聚合查询成功!", 0, map.size());
}

@Test
public void bucketRange() {
    Map<String, Long> map = this.aggsBucketService.bucketRange();
    Assert.assertNotEquals("range 桶聚合查询成功!", 0, map.size());
}

@Test
public void bucketDateRange() {
    Map<String, Long> map = this.aggsBucketService.bucketDateRange();
    Assert.assertNotEquals("date_range 桶聚合查询成功!", 0, map.size());
}

@Test
public void bucketHistogram() {
    Map<String, Long> map = this.aggsBucketService.bucketHistogram();
    Assert.assertNotEquals("histogram 桶聚合查询成功!", 0, map.size());
}

@Test
public void bucketDateHistogram() {
    Map<String, Long> map = this.aggsBucketService.bucketDateHistogram();
    Assert.assertNotEquals("date_histogram 桶聚合查询成功!", 0, map.size());
}

Metric 与 Bucket 聚合

GET /user/_search
{
  "size": 0,
  "aggs": {
    "salary_bucket": {
      "terms": {
        "field": "age",
        "size": "10"
      },
      "aggs": {
        "salary_max_user": {
          "top_hits": {
            "size": 1,
            "sort": [
              {
                "salary": {
                  "order": "desc"
                }
              }
            ]
          }
        }
      }
    }
  }
}

示例代码:

package com.moon.es.service.impl;

import com.alibaba.fastjson.JSON;
import com.moon.es.entity.User;
import com.moon.es.service.AggsBucketMetricService;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.aggregations.AggregationBuilder;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.metrics.ParsedTopHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

@Slf4j
@Service
public class AggsBucketMetricServiceImpl implements AggsBucketMetricService {

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    @Value("${elasticsearch.indexName}")
    private String indexName;

    /**
     * topHits 按岁数分桶、然后统计每个员工工资最高值
     *
     * @return {@link Map}<{@link String}, {@link User}>
     */
    @Override
    public Map<String, User> aggregationTopHits() {
        Map<String, User> map = new LinkedHashMap<>();

        AggregationBuilder salaryBucket = AggregationBuilders.terms("salary_bucket")
                .field("age")
                .size(10);
        AggregationBuilder salaryMaxUser = AggregationBuilders.topHits("salary_max_user")
                .size(1)
                .sort("salary", SortOrder.DESC);
        salaryBucket.subAggregation(salaryMaxUser);

        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.size(0);
        sourceBuilder.aggregation(salaryBucket);

        SearchRequest request = new SearchRequest(this.indexName);
        request.source(sourceBuilder);

        try {
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            Aggregations aggs = response.getAggregations();
            if (response.status().equals(RestStatus.OK) && aggs != null) {
                Terms termsBucket = aggs.get("salary_bucket");
                List<? extends Terms.Bucket> buckets = termsBucket.getBuckets();

                for (Terms.Bucket bucket : buckets) {
                    ParsedTopHits topHits = bucket.getAggregations().get("salary_max_user");
                    for (SearchHit hit : topHits.getHits())
                    map.put(bucket.getKeyAsString() + "=>" + bucket.getDocCount(), JSON.parseObject(hit.getSourceAsString(), User.class));
                }
            }
            log.error("top_hits 聚合查询: {}", map);
        } catch (IOException e) {
            log.error("top_hits 聚合查询异常", e);
        }

        return map;
    }
}

单元测试:

@Test
public void aggregationTopHits() {
    Map<String, User> map = this.aggsBucketMetricService.aggregationTopHits();
    Assert.assertNotEquals("top_hits 聚合查询成功!", 0, map.size());
}

gram 桶聚合查询成功!", 0, map.size());
}


### Metric 与 Bucket 聚合

```json
GET /user/_search
{
  "size": 0,
  "aggs": {
    "salary_bucket": {
      "terms": {
        "field": "age",
        "size": "10"
      },
      "aggs": {
        "salary_max_user": {
          "top_hits": {
            "size": 1,
            "sort": [
              {
                "salary": {
                  "order": "desc"
                }
              }
            ]
          }
        }
      }
    }
  }
}

示例代码:

package com.moon.es.service.impl;

import com.alibaba.fastjson.JSON;
import com.moon.es.entity.User;
import com.moon.es.service.AggsBucketMetricService;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.aggregations.AggregationBuilder;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.metrics.ParsedTopHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

@Slf4j
@Service
public class AggsBucketMetricServiceImpl implements AggsBucketMetricService {

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    @Value("${elasticsearch.indexName}")
    private String indexName;

    /**
     * topHits 按岁数分桶、然后统计每个员工工资最高值
     *
     * @return {@link Map}<{@link String}, {@link User}>
     */
    @Override
    public Map<String, User> aggregationTopHits() {
        Map<String, User> map = new LinkedHashMap<>();

        AggregationBuilder salaryBucket = AggregationBuilders.terms("salary_bucket")
                .field("age")
                .size(10);
        AggregationBuilder salaryMaxUser = AggregationBuilders.topHits("salary_max_user")
                .size(1)
                .sort("salary", SortOrder.DESC);
        salaryBucket.subAggregation(salaryMaxUser);

        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.size(0);
        sourceBuilder.aggregation(salaryBucket);

        SearchRequest request = new SearchRequest(this.indexName);
        request.source(sourceBuilder);

        try {
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            Aggregations aggs = response.getAggregations();
            if (response.status().equals(RestStatus.OK) && aggs != null) {
                Terms termsBucket = aggs.get("salary_bucket");
                List<? extends Terms.Bucket> buckets = termsBucket.getBuckets();

                for (Terms.Bucket bucket : buckets) {
                    ParsedTopHits topHits = bucket.getAggregations().get("salary_max_user");
                    for (SearchHit hit : topHits.getHits())
                    map.put(bucket.getKeyAsString() + "=>" + bucket.getDocCount(), JSON.parseObject(hit.getSourceAsString(), User.class));
                }
            }
            log.info("top_hits 聚合查询: {}", map);
        } catch (IOException e) {
            log.error("top_hits 聚合查询异常", e);
        }

        return map;
    }
}

单元测试:

@Test
public void aggregationTopHits() {
    Map<String, User> map = this.aggsBucketMetricService.aggregationTopHits();
    Assert.assertNotEquals("top_hits 聚合查询成功!", 0, map.size());
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值