ElasticSearch学习笔记

1-简介

1.1-什么是ElasticSearch?

Elasticsearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。Elasticsearch是用Java语言开发的,并作为Apache许可条款下的开放源码发布,是一种流行的企业级搜索引擎。Elasticsearch用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。官方客户端在Java、.NET(C#)、PHP、Python、Apache Groovy、Ruby和许多其他语言中都是可用的。根据DB-Engines的排名显示,Elasticsearch是最受欢迎的企业搜索引擎,其次是Apache Solr,也是基于Lucene。

1.2-什么是Lucene?

Lucene 采用了基于倒排表的设计原理,可以非常高效地实现文本查找,在底层采用了分段的存储模式,使它在读写时几乎完全避免了锁的出现,大大提升了读写性能。

Lucene 可以说是当下最先进、高性能、全功能的搜索引擎库——无论是开源还是私有,但它也仅仅只是一个库。为了充分发挥其功能,你需要使用 Java 并将 Lucene 直接集成到应用程序中。 更糟糕的是,您可能需要获得信息检索学位才能了解其工作原理,因为Lucene 非常复杂。

为了解决Lucene使用时的繁复性,于是Elasticsearch便应运而生。它使用 Java 编写,内部采用 Lucene 做索引与搜索,但是它的目标是使全文检索变得更简单,简单来说,就是对Lucene 做了一层封装,它提供了一套简单一致的 RESTful API 来帮助我们实现存储和检索。

当然,Elasticsearch 不仅仅是 Lucene,并且也不仅仅只是一个全文搜索引擎。 它可以被下面这样准确地形容:

  • 一个分布式的实时文档存储,每个字段可以被索引与搜索;
  • 一个分布式实时分析搜索引擎;
  • 能胜任上百个服务节点的扩展,并支持 PB 级别的结构化或者非结构化数据。

由于Elasticsearch的功能强大和使用简单,维基百科、卫报、Stack Overflow、GitHub等都纷纷采用它来做搜索。现在,Elasticsearch已成为全文搜索领域的主流软件之一。

1.3-ES是如何产生的?

1. 大规模数据如何检索?

如:当系统数据量上了10亿、100亿条的时候,我们在做系统架构的时候通常会从以下角度去考虑问题:

  • 用什么数据库好?(mysql、sybase、oracle、达梦、神通、mongodb、hbase…)
  • 如何解决单点故障;(lvs、F5、A10、Zookeep、MQ)
  • 如何保证数据安全性;(热备、冷备、异地多活)
  • 如何解决检索难题;(数据库代理中间件:mysql-proxy、Cobar、MaxScale等;)
  • 如何解决统计分析问题;(离线、近实时)

2. 传统数据库的应对解决方案

对于关系型数据,我们通常采用以下或类似架构去解决查询瓶颈和写入瓶颈:
解决要点:

  • 通过主从备份解决数据安全性问题;
  • 通过数据库代理中间件心跳监测,解决单点故障问题;
  • 通过代理中间件将查询语句分发到各个slave节点进行查询,并汇总结果 。

3. 非关系型数据库的解决方案

对于Nosql数据库,以mongodb为例,其它原理类似:
解决要点:

  • 通过副本备份保证数据安全性;
  • 通过节点竞选机制解决单点问题;
  • 先从配置库检索分片信息,然后将请求分发到各个节点,最后由路由节点合并汇总结果 。

4. 完全把数据放入内存怎么样?

我们知道,完全把数据放在内存中是不可靠的,实际上也不太现实,当我们的数据达到PB级别时,按照每个节点96G内存计算,在内存完全装满的数据情况下,我们需要的机器是:1PB=1024T=1048576G
节点数=1048576/96=10922个
实际上,考虑到数据备份,节点数往往在2.5万台左右。成本巨大决定了其不现实!

从前面讨论我们了解到,把数据放在内存也好,不放在内存也好,都不能完完全全解决问题。
全部放在内存速度问题是解决了,但成本问题上来了。
为解决以上问题,从源头着手分析,通常会从以下方式来寻找方法:

  1. 存储数据时按有序存储;
  2. 将数据和索引分离;
  3. 压缩数据;

这就引出了Elasticsearch。

1.4-什么是全文检索?

全文检索是指计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。

全文检索的方法主要分为按字检索和按词检索两种。按字检索是指对于文章中的每一个字都建立索引,检索时将词分解为字的组合。对于各种不同的语言而言,字有不同的含义,比如英文中字与词实际上是合一的,而中文中字与词有很大分别。按词检索指对文章中的词,即语义单位建立索引,检索时按词检索,并且可以处理同义项等。英文等西方文字由于按照空白切分词,因此实现上与按字处理类似,添加同义处理也很容易。中文等东方文字则需要切分字词,以达到按词索引的目的,关于这方面的问题,是当前全文检索技术尤其是中文全文检索技术中的难点,在此不做详述。

1.5-正排索引和倒排索引

1. 正排索引

正排索引也称为”前向索引”,它是创建倒排索引的基础。
这种组织方法在建立索引的时候结构比较简单,建立比较方便且易于维护;因为索引是基于文档建立的,若是有新的文档加入,直接为该文档建立一个新的索引块,挂接在原来索引文件的后面。若是有文档删除,则直接找到该文档号文档对应的索引信息,将其直接删除。
他适合根据文档ID来查询对应的内容。但是在查询一个keyword在哪些文档里包含的时候需对所有的文档进行扫描以确保没有遗漏,这样就使得检索时间大大延长,检索效率低下。
比如有几个文档及里面的内容,他正排索引构建的结果如下图:

优点:工作原理非常的简单。
缺点:检索效率太低,只能在一起简单的场景下使用。

2. 倒排索引

倒排索引(英文:Inverted Index),是一种索引方法,常被用于全文检索系统中的一种单词文档映射结构。现代搜索引擎绝大多数的索引都是基于倒排索引来进行构建的,这源于在实际应用当中,用户在使用搜索引擎查找信息时往往只输入信息中的某个属性关键字,如一些用户不记得歌名,会输入歌词来查找歌名;输入某个节目内容片段来查找该节目等等。

面对海量的信息数据,为满足用户需求,顺应信息时代快速获取信息的趋势,聪明的开发者们在进行搜索引擎开发时对这些信息数据进行逆向运算,研发了“关键词——文档”形式的一种映射结构,实现了通过物品属性信息对物品进行映射时,可以帮助用户快速定位到目标信息,从而极大降低了信息获取难度。倒排索引又叫反向索引,它是一种逆向思维运算,是现代信息检索领域里面最有效的一种索引结构。

在搜索引擎中每个文件都对应一个文件ID,文件内容被表示为一系列关键词的集合(文档要除去一些无用的词,比如’的’这些,剩下的词就是关键词,每个关键词都有自己的ID)。例如“文档1”经过分词,提取了3个关键词,每个关键词都会记录它所在在文档中的出现频率及出现位置。

那么上面的文档及内容构建的倒排索引结果会如下图(注:这个图里没有记录展示该词在出现在哪个文档的具体位置):

  • 如何来查询呢?

    比如我们要查询‘搜索引擎’这个关键词在哪些文档中出现过。首先我们通过倒排索引可以查询到该关键词出现的文档位置是在1和3中;然后再通过正排索引查询到文档1和3的内容并返回结果。

  • 倒排索引组成

    倒排索引主要由单词词典(Term Dictionary)和倒排列表(Posting List)及倒排文件(Inverted File)组成。
    他们三者的关系如下图:

    单词词典(Term Dictionary):搜索引擎的通常索引单位是单词,单词词典是由文档集合中出现过的所有单词构成的字符串集合,单词词典内每条索引项记载单词本身的一些信息以及指向“倒排列表”的指针。
    倒排列表(PostingList):倒排列表记载了出现过某个单词的所有文档的文档列表及单词在该文档中出现的位置信息及频率(作关联性算分),每条记录称为一个倒排项(Posting)。根据倒排列表,即可获知哪些文档包含某个单词。
    倒排文件(Inverted File):所有单词的倒排列表往往顺序地存储在磁盘的某个文件里,这个文件即被称之为倒排文件,倒排文件是存储倒排索引的物理文件。

    以查找搜索引擎查找为例:

  • 单词词典查询定位问题

    对于一些规模很大的文档集合来讲,他里面可能包括了上百万的关键单词(term),能否快速定位到具体单词(term),这会直接影响到响应速度。

    假设我们有很多个 term,比如:

    Carla,Sara,Elin,Ada,Patty,Kate,Selena

    如果按照这样的顺序排列,找出某个特定的 term 一定很慢,因为 term 没有排序,需要全部过滤一遍才能找出特定的 term。

    排序之后就变成了:

    Ada,Carla,Elin,Kate,Patty,Sara,Selena

    这样我们可以用二分查找的方式,比全遍历更快地找出目标的 term。这个就是 term dictionary。有了 term dictionary 之后,可以用 logN 次磁盘查找得到目标。但是磁盘的随机读操作仍然是非常昂贵的(一次 random access 大概需要 10ms 的时间)。所以尽量少的读磁盘,有必要把一些数据缓存到内存里。但是整个 term dictionary 本身又太大了,无法完整地放到内存里。于是就有了 term index。term index 有点像一本字典的大的章节表。

    目前常用的方式是通过hash加链表结构和树型结构(b树或者b+)。

    • hash加链表:

      这是很常用的一种数据结构。这种方式就可以快速计算单词的hash值从而定位到他所有在的hash表中,如果该表是又是一个链表结构(两个单词的hash值可能会一样),那么就需要遍历这个链表然后再对比返回结果。这种方式最大的缺点就是如果有范围查询的时候就很难做到。

    • 树型结构:

      B树(或者B+树)是另外一种高效查找结构,下图是一个 B树结构示意图。B树与哈希方式查找同,需要字典项能够按照大小排序(数字或者字符序),而哈希方式则无须数据满足此项要求。

      B树形成了层级查找结构,中间节点用于指出一定顺序范围的词典项目存储在哪个子树中,起到根据词典项比较大小进行导航的作用,最底层的叶子节点存储单词的地址信息,根据这个地址就可以提取出单词字符串。

1.5-ElasticSearch的基本概念

1. ElaticSearch 和 DB 的关系

在 Elasticsearch 中,文档归属于一种类型 type,而这些类型存在于索引 index 中,我们可以列一些简单的不同点,来类比传统关系型数据库:

  • Relational DB -> Databases -> Tables -> Rows -> Columns
  • Elasticsearch -> Indices -> Types -> Documents -> Fields

Elasticsearch 集群可以包含多个索引 indices,每一个索引可以包含多个类型 types,每一个类型包含多个文档 documents,然后每个文档包含多个字段 Fields。而在 DB 中可以有多个数据库 Databases,每个库中可以有多张表 Tables,没个表中又包含多行Rows,每行包含多列Columns。

ES和数据库关系对比:

2. 索引

索引基本概念(indices):

索引是含义相同属性的文档集合,是 ElasticSearch 的一个逻辑存储,可以理解为关系型数据库中的数据库,ElasticSearch 可以把索引数据存放到一台服务器上,也可以 sharding 后存到多台服务器上,每个索引有一个或多个分片,每个分片可以有多个副本。

索引类型(index_type):

索引可以定义一个或多个类型,文档必须属于一个类型。在 ElasticSearch 中,一个索引对象可以存储多个不同用途的对象,通过索引类型可以区分单个索引中的不同对象,可以理解为关系型数据库中的表。每个索引类型可以有不同的结构,但是不同的索引类型不能为相同的属性设置不同的类型。

3. 文档

文档(document):

文档是可以被索引的基本数据单位。存储在 ElasticSearch 中的主要实体叫文档 document,可以理解为关系型数据库中表的一行记录。每个文档由多个字段构成,ElasticSearch 是一个非结构化的数据库,每个文档可以有不同的字段,并且有一个唯一的标识符。

4. 映射

映射(mapping):

ElasticSearch 的 Mapping 非常类似于静态语言中的数据类型:声明一个变量为 int 类型的变量,以后这个变量都只能存储 int 类型的数据。同样的,一个 number 类型的 mapping 字段只能存储 number 类型的数据。

同语言的数据类型相比,Mapping 还有一些其他的含义,Mapping 不仅告诉 ElasticSearch 一个 Field 中是什么类型的值, 它还告诉 ElasticSearch 如何索引数据以及数据是否能被搜索到。

ElaticSearch 默认是动态创建索引和索引类型的 Mapping 的。这就相当于无需定义 Solr 中的 Schema,无需指定各个字段的索引规则就可以索引文件,很方便。但有时方便就代表着不灵活。比如,ElasticSearch 默认一个字段是要做分词的,但我们有时要搜索匹配整个字段却不行。如有统计工作要记录每个城市出现的次数。对于 name 字段,若记录 new york 文本,ElasticSearch 可能会把它拆分成 new 和 york 这两个词,分别计算这个两个单词的次数,而不是我们期望的 new york。

5. Near Realtime(NRT) 几乎实时

Elasticsearch是一个几乎实时的搜索平台。意思是,从索引一个文档到这个文档可被搜索只需要一点点的延迟,这个时间一般为毫秒级。

6. Cluster 集群

群集是一个或多个节点(服务器)的集合, 这些节点共同保存整个数据,并在所有节点上提供联合索引和搜索功能。一个集群由一个唯一集群ID确定,并指定一个集群名(默认为“elasticsearch”)。该集群名非常重要,因为节点可以通过这个集群名加入群集,一个节点只能是群集的一部分。

确保在不同的环境中不要使用相同的群集名称,否则可能会导致连接错误的群集节点。例如,你可以使用logging-dev、logging-stage、logging-prod分别为开发、阶段产品、生产集群。

7. Node节点

节点是单个服务器实例,它是群集的一部分,可以存储数据,并参与群集的索引和搜索功能。就像一个集群,节点的名称默认为一个随机的通用唯一标识符(UUID),确定在启动时分配给该节点。如果不希望默认,可以定义任何节点名。这个名字对管理很重要,目的是要确定你的网络服务器对应于你的ElasticSearch群集节点。

我们可以通过群集名配置节点以连接特定的群集。默认情况下,每个节点设置加入名为“elasticSearch”的集群。这意味着如果你启动多个节点在网络上,假设他们能发现彼此都会自动形成和加入一个名为“elasticsearch”的集群。

在单个群集中,您可以拥有尽可能多的节点。此外,如果“elasticsearch”在同一个网络中,没有其他节点正在运行,从单个节点的默认情况下会形成一个新的单节点名为”elasticsearch”的集群。

8. Shards & Replicas分片与副本

索引可以存储大量的数据,这些数据可能超过单个节点的硬件限制。例如,十亿个文件占用磁盘空间1TB的单指标可能不适合对单个节点的磁盘或可能太慢服务仅从单个节点的搜索请求。

为了解决这一问题,Elasticsearch提供细分你的指标分成多个块称为分片的能力。当你创建一个索引,你可以简单地定义你想要的分片数量。每个分片本身是一个全功能的、独立的“指数”,可以托管在集群中的任何节点。

Shards分片的重要性主要体现在以下两个特征:

  • 分片允许您水平拆分或缩放内容的大小
  • 分片允许你分配和并行操作的碎片(可能在多个节点上)从而提高性能/吞吐量
    这个机制中的碎片是分布式的以及其文件汇总到搜索请求是完全由ElasticSearch管理,对用户来说是透明的。

在同一个集群网络或云环境上,故障是任何时候都会出现的,拥有一个故障转移机制以防分片和结点因为某些原因离线或消失是非常有用的,并且被强烈推荐。为此,Elasticsearch允许你创建一个或多个拷贝,你的索引分片进入所谓的副本或称作复制品的分片,简称Replicas。

Replicas的重要性主要体现在以下两个特征:

  • 副本为分片或节点失败提供了高可用性。为此,需要注意的是,一个副本的分片不会分配在同一个节点作为原始的或主分片,副本是从主分片那里复制过来的。
  • 副本允许用户扩展你的搜索量或吞吐量,因为搜索可以在所有副本上并行执行。

2-ElasticSearch的安装和使用

2.1-ElasticSearch的安装

1. Linux下安装ES

Linux服务器是CentOS 7.x

  • Elasticsearch下载地址:

    https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.1.1-linux-x86_64.tar.gz
  • 解压elasticsearch-7.1.1-linux-x86_64.tar.gz到/usr/local/目录:

    tar -avxf elasticsearch-7.1.1-linux-x86_64.tar.gz -C /usr/local/
  • 进入解压后的elasticsearch目录

    • 新建data目录:

      mkdir data

    • 修改config/elasticsearch.yml:

      vim config/elasticsearch.yml
    • 取消下列项注释并修改:

      cluster.name: my-application #集群名称
      node.name: node-1 #节点名称
      #数据和日志的存储目录
      path.data: /usr/local/elasticsearch-7.1.1/data
      path.logs: /usr/local/elasticsearch-7.1.1/logs
      #设置绑定的ip,设置为0.0.0.0以后就可以让任何计算机节点访问到了
      network.host: 0.0.0.0
      http.port: 9200 #端口
      #设置在集群中的所有节点名称,这个节点名称就是之前所修改的,当然你也可以采用默认的也行,目前是单机,放入一个节点即可
      cluster.initial_master_nodes: ["node-1"]

      修改完毕后,:wq 保存退出vim

  • 准备启动es
    进入/bin目录执行命令:

    ./elasticsearch

    我这里出现如下错误:

    Java HotSpot(TM) 64-Bit Server VM warning: INFO: os::commit_memory(0x00000000c5330000, 986513408, 0) failed; error='Cannot allocate memory' (errno=12)
    #
    # There is insufficient memory for the Java Runtime Environment to continue.
    # Native memory allocation (mmap) failed to map 986513408 bytes for committing reserved memory.
    # An error report file with more information is saved as:
    # logs/hs_err_pid22863.log
    [root@VM_0_2_centos bin]#

    看来是我这1G的内存太小了啊,elasticsearch使用java的jvm默认是使用1G的内存的,这里我们修改一下内存,直接把内存改到200m。

    cd 到es目录修改 ./config/jvm.options:

    vim ./config/jvm.options 

    修改该内容:

    -Xms200m
    -Xmx200m

    :wq 保存并退出vim,再次启动es

    再次启动出现如下错误:

    [2019-06-21T16:20:03,039][WARN ][o.e.b.ElasticsearchUncaughtExceptionHandler] [node-1] uncaught exception in thread [main]
    org.elasticsearch.bootstrap.StartupException: java.lang.RuntimeException: can not run elasticsearch as root
    at org.elasticsearch.bootstrap.Elasticsearch.init(Elasticsearch.java:163) ~[elasticsearch-7.1.1.jar:7.1.1]
    at org.elasticsearch.bootstrap.Elasticsearch.execute(Elasticsearch.java:150) ~[elasticsearch-7.1.1.jar:7.1.1]
    at org.elasticsearch.cli.EnvironmentAwareCommand.execute(EnvironmentAwareCommand.java:86) ~[elasticsearch-7.1.1.jar:7.1.1]
    at org.elasticsearch.cli.Command.mainWithoutErrorHandling(Command.java:124) ~[elasticsearch-cli-7.1.1.jar:7.1.1]
    at org.elasticsearch.cli.Command.main(Command.java:90) ~[elasticsearch-cli-7.1.1.jar:7.1.1]
    at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:115) ~[elasticsearch-7.1.1.jar:7.1.1]
    at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:92) ~[elasticsearch-7.1.1.jar:7.1.1]
    Caused by: java.lang.RuntimeException: can not run elasticsearch as root
    at org.elasticsearch.bootstrap.Bootstrap.initializeNatives(Bootstrap.java:102) ~[elasticsearch-7.1.1.jar:7.1.1]
    at org.elasticsearch.bootstrap.Bootstrap.setup(Bootstrap.java:169) ~[elasticsearch-7.1.1.jar:7.1.1]
    at org.elasticsearch.bootstrap.Bootstrap.init(Bootstrap.java:325) ~[elasticsearch-7.1.1.jar:7.1.1]
    at org.elasticsearch.bootstrap.Elasticsearch.init(Elasticsearch.java:159) ~[elasticsearch-7.1.1.jar:7.1.1]
    ... 6 more
    [root@VM_0_2_centos elasticsearch-7.1.1]#

    这是不能使用root用户操作,添加一个其他的用户再试试:

    [root@VM_0_2_centos elasticsearch-7.1.1]# adduser es
    [root@VM_0_2_centos elasticsearch-7.1.1]# passwd es
    Changing password for user es.
    New password:
    Retype new password:
    passwd: all authentication tokens updated successfully.

    改一下es目录所属用户:

    [root@VM_0_2_centos elasticsearch-7.1.1]# chown es /usr/local/elasticsearch-7.1.1/ -R

    vim 编辑 /etc/security/limits.conf,在末尾加上:

    es soft nofile 65536
    es hard nofile 65536
    es soft nproc 4096
    es hard nproc 4096

    vim 编辑 vim /etc/security/limits.d/20-nproc.conf,将* 改为用户名(es):

    # Default limit for number of user's processes to prevent
    # accidental fork bombs.
    # See rhbz #432903 for reasoning.

    es soft nproc 4096
    root soft nproc unlimited

    vim 编辑 /etc/sysctl.conf,在末尾加上:

    vm.max_map_count = 655360

    执行:

    [root@VM_0_2_centos ~]# sysctl -p
    kernel.printk = 5
    vm.max_map_count = 655360
    [root@VM_0_2_centos ~]#

    登录刚才新建的es用户,并启动elasticsearch,OK

    [root@VM_0_2_centos elasticsearch-7.1.1]# su es
    [es@VM_0_2_centos elasticsearch-7.1.1]$ ./bin/elasticsearch

2. Docker下安装ES

  • 搜索ES镜像

    docker search elasticsearch
  • docker安装es

    docker pull elasticsearch:7.2.0
  • 启动es

    docker run --name elasticsearch -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -d elasticsearch:7.2.0
  • 修改配置,解决跨域访问问题

    首先进入到容器中,然后进入到指定目录修改elasticsearch.yml文件。

    -> docker exec -it elasticsearch /bin/bash
    -> cd /usr/share/elasticsearch/config/
    -> vi elasticsearch.yml
    # 追加一下内容,解决跨域问题
    http.cors.enabled: true
    http.cors.allow-origin: "*"

    :wq保存

    -> vi jvm.options 
    -Xms200m
    -Xmx200m
    # 重启容器
    -> exit
    -> docker restart elasticsearch
  • 安装ik分词器

    es自带的分词器对中文分词不是很友好,所以我们下载开源的IK分词器来解决这个问题。首先进入到plugins目录中下载分词器,下载完成后然后解压,再重启es即可。具体步骤如下:
    注意:elasticsearch的版本和ik分词器的版本需要保持一致,不然在重启的时候会失败。可以在这查看所有版本,选择合适自己版本

    -> cd /usr/share/elasticsearch/plugins/
    -> elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.2.0/elasticsearch-analysis-ik-7.2.0.zip
    -> exit
    -> docker restart elasticsearch 然后可以在kibana界面的dev tools中验证是否安装成功;
  • 验证

    POST test/_analyze
    {
    "analyzer": "ik_max_word",
    "text": "你好我是东邪Jiafly"
    }

    不添加”analyzer”: “ik_max_word”,则是每个字分词,可以在下面kibana安装完成以后尝试一下。

  • docker安装kibana

    docker pull kibana:7.2.0
  • 启动kibana

    安装完成以后需要启动kibana容器,使用–link连接到elasticsearch容器,命令如下:

    -> docker run --name kibana --link=elasticsearch:test  -p 5601:5601 -d kibana:7.2.0
    -> docker start kibana

    启动以后可以打开浏览器输入http://localhost:5601就可以打开kibana的界面了。

  • 安装elasticsearch head插件监控管理(不推荐)

    docker pull mobz/elasticsearch-head:5
    docker run -d -p 9100:9100 docker.io/mobz/elasticsearch-head:5

2.2-ElasticSearch的使用

1. ELasticSearch常用命令

(1) 集群信息
  • 查看欢迎信息
# url
http://112.xx.xx.xx:9200/
  • 查看集群是否健康
# 查看集群健康状态
# url
http://112.xx.xx.xx:9200/_cluster/health
# Kibana
GET /_cluster/health
# url
http://112.xx.xx.xx:9200/_cluster/settings?include_defaults
# Kibana
GET /_cluster/settings?include_defaults
  • 查看节点列表
# 查看节点列表
# url
http://112.xx.xx.xx:9200/_cat/nodes?v
# Kibana
GET /_cat/nodes?v
(2) 索引

查看索引:

  • 查看所有索引
# 查看所有索引
GET /_cat/indices
  • 查看某个索引的状态
# 查看某个索引的状态
GET /_cat/indices/my_index
  • 查看某个索引的 mapping
# 查看某个索引的 mapping
GET /my_index/_mapping
  • 查看某个索引的 settings
# 查看某个索引的 settings
GET /my_index/_settings
  • 如果 index 的状态为 yellow,可能是因为副本分片未分配出去
# 查看 shard 未分配(unassigned)出去的原因
GET /_cat/shards?v&h=index,shard,prirep,state,unassigned.reason

创建索引:

  • 用明确的 mapping 创建索引

    PUT /my_index
    {
    "mappings": {
    "dynamic": false,
    "properties": {
    "age": { "type": "integer" },
    "email": { "type": "keyword" },
    "name": { "type": "text" }
    }
    }
    }

删除索引:

  • 删除 my_index 索引
DELETE /my_index

重建索引:

  • 重建 my_index 到 my_index_new
POST /_reindex
{
"source": {
"index": "my_index", # 原有索引
"size": 5000 # 一个批次处理的数据量
},
"dest": {
"index": "my_index_new" # 新索引
}
}
  • 查看重建索引的进度
GET /_tasks?detailed=true&actions=*reindex

索引模板:

  • 查看有哪些模板
GET _cat/templates
  • 查看一个具体的模板
GET _template/my_index_tpl
  • 创建模板
PUT /_template/my_index_tpl
{
"order": 0,
"index_patterns": [
"my_index_*"
],
"settings": {
"index": {
"number_of_shards": 12,
"number_of_replicas": 1
}
},
"mappings": {
"_source": {
"enabled": false
},
"dynamic": false,
"properties": {
"name_en": {
"analyzer": "english",
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"name_cn": {
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart",
"type": "text"
},
"country": {
"type": "keyword"
},
"full_field": {
"index": false,
"store": true,
"type": "text"
}
}
},
"aliases": {}
}
(3) 索引别名
  • 查看有哪些别名
GET _cat/aliases
  • 添加别名
# 多对一
POST /_aliases
{
"actions": [
{
"add": {
"indices": [
"my_index_1",
"my_index_2"
],
"alias": "my_index_alias"
}
}
]
}
# 支持通配符(*)
POST /_aliases
{
"actions": [
{
"add": {
"indices": [
"my_index_*"
],
"alias": "my_index_alias"
}
}
]
}
  • 移除别名
POST /_aliases
{
"actions": [
{
"remove": {
"index": "my_index_1",
"alias": "my_index_alias"
}
},
{
"remove": {
"index": "my_index_2",
"alias": "my_index_alias"
}
}
]
}
  • 替换(移除和添加组合使用)
POST /_aliases
{
"actions": [
{
"remove": {
"indices": [
"my_index_1_20201011",
"my_index_2_20201011"
],
"alias": "my_index_alias"
}
},
{
"add": {
"indices": [
"my_index_1_20201022",
"my_index_2_20201022"
],
"alias": "my_index_alias"
}
}
]
}
(4) settings

分片(shard)

  • 初始化分片数
PUT /my_temp_index
{
"settings": {
"number_of_shards" : 1, # 主分片数,不可动态修改
"number_of_replicas" : 0 # 副本分片数,可以动态修改
}
}
  • 动态修改副本分片数
PUT /my_index/_settings
{
"number_of_replicas": 0
}
  • 查看分片情况
# 查看所有索引的分片情况
GET /_cat/shards?v
# 查看 my_index 的分片情况
GET /_cat/shards/my_index?v
(5) mapping

新增索引字段

# 无需 reindex
PUT /my_index/_mapping
{
"properties": {
"employee-id": {
"type": "keyword"
}
}
}
# 当给已有无索引字段添加索引后,
# 该字段的新增数据可以被检索到,
# 该字段的历史数据不能被检索到,
#此时可以用
POST /my_index/_update_by_query
# 语句刷新索引
# 增加子字段也可以用类似方法
(6) 文档的增删改查(CRUD)
Elasticsearch类比MySQL说明
Indexreplcae intoIndex在索引不存在时会创建索引, replace into 并不会创建库或表
Createinsert into增加
Readselect读取
Updateupdate更新
Deletedelete删除
  • Create(增加)
    • 指定 ID
    POST /my_index/_doc/1
    {"user":"walker"}
    • 系统自动生成 ID
    POST /my_index/_doc
    {"user":"walker"}
  • Read(读取)
    # 查看某个索引的文档总数
    GET /_cat/count/my_index?v
    # OR
    GET /my_index/_count
    • 返回索引的所有文档
    # 返回索引的所有文档
    GET /kibana_sample_data_ecommerce/_search
    # OR
    POST my_index/_search
    {
    "query": {
    "match_all": {}
    }
    }
    • 根据ID查看文档
    # 根据ID查看文档
    GET /kibana_sample_data_ecommerce/_doc/xPGYeWwBVtEez7y_Ku1U
    • term 查询精确匹配
    # term 查询精确匹配
    GET /_search
    {
    "query": {
    "term": {
    "currency": "EUR"
    }
    }
    }
    # 通过 Constant Score 将查询转换成一个 Filtering
    # 避免算分,并利用缓存,提高性能
    GET /_search
    {
    "query": {
    "constant_score": {
    "filter": {
    "term": {
    "currency": "EUR"
    }
    }
    }
    }
    }
    • 通配符模糊查询
    # 通配符模糊查询
    GET /_search
    {
    "query": {
    "wildcard": {
    "currency": "*U*"
    }
    }
    }
    # 通过 Constant Score 将查询转换成一个 Filtering
    # 避免算分,并利用缓存,提高性能
    GET /_search
    {
    "query": {
    "constant_score": {
    "filter": {
    "wildcard": {
    "currency": "*U*"
    }
    }
    }
    }
    }
    • 多条件组合查询
    # Boolean query
    GET /my_index/_search
    {
    "query": {
    "bool": {
    "filter": [
    {
    "term": {
    "author": "walker"
    }
    },
    {
    "wildcard": {
    "title": "*科技*"
    }
    }
    ]
    }
    }
    }
    • 返回查询数据量
    # 返回查询数据量
    GET /my_index/_count
    {
    "query": {
    "wildcard": {
    "title": "*科技*"
    }
    }
    }
    • 游标查询(深度分页 Scroll,命中数大于10000时,可返回命中总数)
    # 游标查询
    POST /my_index/_search?scroll=1m
  • Update(更新)
    • 指定 ID 更新
    POST /my_index/_update/1
    {
    "doc": {
    "user": "walker",
    "age": 99
    }
    }
  • Delete(删除)
    • 指定 ID 删除
    DELETE /my_index/_doc/1
  • 批量操作

    上面讲的都是对单文档进行操作,多文档批量操作可自行去翻看官网文档:Document APIs

(7) Elasticsearch SQL

用法示例

POST _sql?format=txt
{
"query": "SELECT Carrier FROM kibana_sample_data_flights LIMIT 100"
}

将 SQL 转化为 DSL

POST _sql/translate
{
"query": "SELECT Carrier FROM kibana_sample_data_flights LIMIT 100"
}
# 转换结果如下
{
"size" : 100,
"_source" : false,
"stored_fields" : "_none_",
"docvalue_fields" : [
{
"field" : "Carrier"
}
],
"sort" : [
{
"_doc" : {
"order" : "asc"
}
}
]
}

2. Mapping之字段类型

(1) 基本类型
  • string (字符串类型)

    string类型在ElasticSearch 旧版本中使用较多,从ElasticSearch 5.x开始不再支持string,由text和keyword类型替代。

    • text

      文本类型,在索引文件中,存储的不是原字符串,而是使用分词器对内容进行分词处理后得到一系列的词根,然后一一存储在index的倒排索引中。当一个字段是要被全文搜索的,比如Email内容、产品描述,应该使用text类型。设置text类型以后,字段内容会被分析,在生成倒排索引以前,字符串会被分析器分成一个一个词项。text类型的字段不用于排序,很少用于聚合。

      {
      "match_mapping_type": "string",
      "mapping": {
      "type": "text",
      "store": true
      }
      }
    • keyword

      关键字类型,将原始输入内容当成一个词根存储在倒排索引中,与text字段的区别是该字段不会使用分词器进行分词。 keyword类型适用于索引结构化的字段,比如email地址、主机名、状态码和标签。如果字段需要进行过滤(比如查找已发布博客中status属性为published的文章)、排序、聚合。keyword类型的字段只能通过精确值搜索到。

      {
      "foo": {
      "type": "text",
      "fields": {
      "keyword": {
      "type": "keyword",
      "ignore_above": 256
      }
      }
      }
      }
  • numeric datatypes(数字类型)

    在满足需求的情况下,尽可能选择范围小的数据类型。比如,某个字段的取值最大值不会超过100,那么选择byte类型即可。迄今为止吉尼斯记录的人类的年龄的最大值为134岁,对于年龄字段,short足矣。字段的长度越短,索引和搜索的效率越高。

  • 浮点类型

    对于float、half_float和scaled_float,-0.0和+0.0是不同的值,使用term查询查找-0.0不会匹配+0.0,同样range查询中上边界是-0.0不会匹配+0.0,下边界是+0.0不会匹配-0.0。

    其中scaled_float,比如价格只需要精确到分,price为57.34的字段缩放因子为100,存起来就是5734
    优先考虑使用带缩放因子的scaled_float浮点类型。

  • date(日期类型)

    日期类型表示格式可以是以下几种:

    1. 日期格式的字符串,比如 “2018-01-13” 或 “2018-01-13 12:10:30”
    2. long类型的毫秒数( milliseconds-since-the-epoch,epoch就是指UNIX诞生的UTC时间1970年1月1日0时0分0秒)
    3. integer的秒数(seconds-since-the-epoch)

    ElasticSearch 内部会将日期数据转换为UTC,并存储为milliseconds-since-the-epoch的long型整数。
    例子:日期格式数据

    "properties": {
    "postdate":{
    "type":"date",
    "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
    }
    }
  • boolean类型

    逻辑类型(布尔类型)可以接受true/false/”true”/”false”值

    "properties": {
    "empty":{
    "type":"boolean"
    }
    }
  • binary类型

    二进制字段是指用base64来表示索引中存储的二进制数据,可用来存储二进制形式的数据,例如图像。默认情况下,该类型的字段只存储不索引。二进制类型只支持index_name属性。

  • range datatype

    数据范围类型,一个字段表示一个范围,具体包含如下类型:

    • integer_range
    • float_range
    • double_range
    • date_range
    • ip_range

    所谓的范围类型,就是一个值自身就代表一个范围,例如:

     1PUT range_index
    2{
    3 "mappings": {
    4 "_doc": {
    5 "properties": {
    6 "expected_attendees": {
    7 "type": "integer_range"
    8 }
    9 }
    10 }
    11 }
    12}

    其索引数据时:

     1public static void index_mapping_integer_range() {
    2 RestHighLevelClient client = EsClient.getClient();
    3 try {
    4 IndexRequest request = new IndexRequest("mapping_test_ranger", "_doc");
    5 Map<String, Object> data = new HashMap<>();
    6 Map<String, Integer> pd = new HashMap<>();
    7 pd.put("gte", 10);
    8 pd.put("lte", 20);
    9 data.put("expected_attendees", pd);
    10 request.source(data);
    11 System.out.println(client.index(request, RequestOptions.DEFAULT));
    12 } catch (Throwable e) {
    13 e.printStackTrace();
    14 } finally {
    15 EsClient.close(client);
    16 }
    17 }

    搜索方式:

     1public static void search_integer_range() {
    2 RestHighLevelClient client = EsClient.getClient();
    3 try {
    4 SearchRequest searchRequest = new SearchRequest();
    5 searchRequest.indices("mapping_test_ranger");
    6 SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
    7 sourceBuilder.query(
    8 //QueryBuilders.matchAllQuery()
    9 //QueryBuilders.termQuery("expected_attendees", 12) // @1
    10 // QueryBuilders.rangeQuery("expected_attendees").lte(30).gte(19) // @2
    11 QueryBuilders.rangeQuery("expected_attendees").lte(30).gte(21) // @3
    12 );
    13 searchRequest.source(sourceBuilder);
    14 SearchResponse result = client.search(searchRequest, RequestOptions.DEFAULT);
    15 System.out.println(result);
    16 } catch (Throwable e) {
    17 e.printStackTrace();
    18 } finally {
    19 EsClient.close(client);
    20 }
    21 }

    代码@1:可以通过termQuery精确匹配。 代码@2:只有定义的范围中,任意一个值匹配查询条件,则文档匹配。 代码@3:不匹配文档。

    range类型支持如下映射类型参数:co-erce、boost、index、store。

(2) 复合类型
  • array

    数组类型,不需要使用额外的类型定义,例如定义如下字段映射:

    1"properties": {
    2 "status_code": {
    3 "type": "keyword" // 默认情况下,“doc_values”:true
    4 }
    5}

    定义的类型为:keyword,在索引时是直接支持数组的。

    1String[] keywords = new String[] {"abc","def"};
    2Map<String, Object> source = new HashMap<>();
    3souce.put("status_code",keywords )。
  • Object datatype

    数据类型,对象或json对象字符串。

  • Nested datatype

    嵌套数据类型,用于关联查询。

  • Geo datatypes

    地图数据类型。

  • geo_point

    地图坐标;存储经纬度。其使用场景:

    1. Geo Bounding Box Query 找出落在指定矩形框中的坐标点
    2. Geo Distance Query 找出与指定位置在给定距离内的点
    3. 找出与指定点距离在给定最小距离和最大)距离之间的点
    4. Geo Polygon Query 查找包含在多边形范围内的文档 与地理位置相关的查询,将在整个SearchAPI讲解完成后再详细学习。
  • geo_shape datatype

    geo_shape数据类型方便了对任意地理形状(如矩形和多边形)进行索引和搜索。当正在索引的数据或正在执行的查询包含除了点以外的形状时应该使用它。

(3) 特定类型
  • ip类型

    IP地址类型。可以存储和索引ipv4、ipv6的IP地址。

     1PUT my_index
    2{
    3 "mappings": {
    4 "_doc": {
    5 "properties": {
    6 "ip_addr": {
    7 "type": "ip"
    8 }
    9 }
    10 }
    11 }
    12}
    13
    14PUT my_index/_doc/1
    15{
    16 "ip_addr": "192.168.1.1"
    17}
    18GET my_index/_search
    19{
    20 "query": {
    21 "term": {
    22 "ip_addr": "192.168.0.0/16"
    23 }
    24 }
    25}
  • Completion datatype

    类型值:completion ;为了优化在查找时输入自动补全而设计的类型,输入自动补全会在查询部分专题详解。

  • Token count datatype

    类型值:token_count,再接收一个字符串经过分析后将返回词根的个数,举例说明如下:

     1PUT my_index
    2{
    3 "mappings": {
    4 "_doc": {
    5 "properties": {
    6 "name": {
    7 "type": "text",
    8 "fields": {
    9 "length": { // 为name定义的另外一个映射方式,其原始输入值还是name字段。
    10 "type": "token_count", // @1
    11 "analyzer": "standard" // @2
    12 }
    13 }
    14 }
    15 }
    16 }
    17 }
    18}

    @1:定义name.length字段,其类型为token_count,使用标准分词器对原始字段name的值进行分析,返回返回分析后的词根个数。 @2:分词器。

    查询示例:

     1PUT my_index/_doc/1
    2{ "name": "John Smith" }
    3
    4PUT my_index/_doc/2
    5{ "name": "Rachel Alice Williams" }
    6
    7GET my_index/_search
    8{
    9 "query": {
    10 "term": {
    11 "name.length": 3
    12 }
    13 }
    14}

    该查询条件能匹配_doc/2,因为name字段的值经过标准分词器分词后,能得到3个词根,与”name.length”:3匹配。

  • join datatype

    类型值:join。join类型允许在同一个索引中(同一个类型type)中定义多个不同类型的文档(例如学生文档、班级文档-)这些类型是个一对多关联关系(父子级联关系)。

    下面根据示例来学习join type,下面先创建一个join的映射。

     1PUT my_index
    2{
    3 "mappings": {
    4 "_doc": {
    5 "properties": {
    6 "my_join_field": { // @1
    7 "type": "join",
    8 "relations": { // @2
    9 "question": "answer"
    10 }
    11 }
    12 }
    13 }
    14 }
    15}

    代码@1:定义join字段名称。 代码@2:通过relations字段来定义父子关系,其定义方式为 “父实例名称” : “子实例名称”,question是answer的父类型。 索引父文档的方式如下:

     1PUT my_index/_doc/1?refresh
    2{
    3 "text": "This is a question",
    4 "my_join_field": {
    5 "name": "question"
    6 }
    7}
    8PUT my_index/_doc/2?refresh
    9{
    10 "text": "This is a another question",
    11 "my_join_field": {
    12 "name": "question"
    13 }
    14}

    索引父文档时,在souce字段中必须指定其关系,例如”name”: “question”,上述索引,用JAVA实现如下:

     1public static void createMapping_join_demo() { // 创建映射
    2 RestHighLevelClient client = EsClient.getClient();
    3 try {
    4 CreateIndexRequest request = new CreateIndexRequest("map_test_join");
    5 Map relations = new HashMap<>();
    6 relations.put("question", "answer");
    7 XContentBuilder jsonBuilder = XContentFactory.jsonBuilder()
    8 .startObject()
    9 .startObject("properties")
    10 .startObject("text")
    11 .field("type", "text")
    12 .endObject()
    13 .startObject("my_join_field")
    14 .field("type", "join")
    15 .field("relations", relations)
    16 .endObject()
    17 .endObject()
    18 .endObject();
    19 request.mapping("_doc", jsonBuilder);
    20 System.out.println(client.indices().create(request, RequestOptions.DEFAULT));
    21 } catch (Throwable e) {
    22 e.printStackTrace();
    23 } finally {
    24 EsClient.close(client);
    25 }
    26 }
    27 // 下面是构建souce字段,必须包含my_join_field,指明是父文档还是子文档。
    28Map<String, Object> data1 = new HashMap();
    29 data1.put("text","This is a another question");
    30 Map my_join_field = new HashMap<>();
    31 my_join_field.put("name", "question");
    32 data1.put("my_join_field",my_join_field);
    33
    34//索引子文档时,必须通过parent指定父文档ID,并且其routing字段需要设置为父文档ID,确保父子文档在同一个分片上。
    35PUT my_index/_doc/3?routing=1&refresh
    36{
    37 "text": "This is an answer",
    38 "my_join_field": {
    39 "name": "answer",
    40 "parent": "1"
    41 }
    42}

    关于join字段的查询,将在DSL Join ty-pe相关查询时重点介绍。

  • Alias datatype

    类型值为:alias,可以字段指定别名,其映射定义方式如下:

     1PUT trips
    2{
    3 "mappings": {
    4 "_doc": {
    5 "properties": {
    6 "distance": {
    7 "type": "long"
    8 },
    9 "route_length_miles": {
    10 "type": "alias",
    11 "path": "distance" // @1
    12 },
    13 "transit_mode": {
    14 "type": "keyword"
    15 }
    16 }
    17 }
    18 }
    19}

    通过使用path来指定是哪个字段的别名。

2.3-DSL语法和搜索

Elasticsearch提供了一个基于JSON的完整查询DSL(领域特定语言)来定义查询。把查询DSL看作是查询的AST(抽象语法树),由两种类型的子句组成:

  • Leaf query clauses(叶查询字句)

叶子查询子句指在特定的字段中寻找特定的值,例如匹配、范围查询或term(完全匹配)。这些查询可以单独使用。

  • Compound query clauses(复合查询字句)

复合查询字句包装其他叶子或复合字句,用于以逻辑方式组合多个查询(如bool、dis_max)或改变他们的行为(如常量查询)。

查询子句的行为取决于它是在查询上下文中使用还是在过滤上下文中使用:

  • 查询上下文

在查询上下文中使用的查询子句,查询字句回答了“这个文档与这个查询子句(查询条件)匹配得有多好?”除了决定文档是否匹配之外,查询子句还计算一个分数,表示相对与其他文档该文档匹配的程度。每当一个查询子句传递给查询参数(query)时,查询上下文就会生效,比如搜索API中的查询参数。

  • 过滤上下文

在过滤上下文中,查询子句回答“这个文档是否匹配这个查询子句?”答案是简单的“是”或“否”——没有计算出分数。过滤上下文主要用于过滤结构化数据(相当与关系型数据库的过滤条件)。例如这个时间戳是否会在2015年到2016年之间?文章的状态是为“发布”吗?等等。

经常使用的过滤器(filter context)会被Elasticsearch自动缓存,以提高性能。每当一个查询子句被传递给过滤器参数(filter)时,过滤器上下文就会生效,例如bool查询中的filter或must_not参数、或filter查询中的常量查询(constant_score)或filter查询。

举例如下:

GET /_search
{
"query": { // @1
"bool": { // @2
"must": [
{ "match": { "title": "Search" }}, // @3
{ "match": { "content": "Elasticsearch" }} // @4
],
"filter": [ // @5
{ "term": { "status": "published" }}, // @6
{ "range": { "publish_date": { "gte": "2015-01-01" }}} // @7
]
} // end bool
} // end query
}

代码@1:query参数定义查询上下文,query参数为elasticsearch的查询上下文。

代码@2:使用elasticsearch的bool查询表达式,会在后续详细介绍。

代码@3:查询上下文,使用关键字match,表示title字段中包含”Search”字符即认为匹配。(可以类比关系型数据库 a.title like ‘%Search%’)

代码@4:查询上下文,使用关键字match,表示content字段中包含”Elasticsearch”字符即认为匹配。

代码@5:定义过滤上下文。

代码@6:使用term(完整匹配),即status字段的值是否是“published”。(相当于关系型数据库的 a.status = ‘published’)

代码@7:使用range,代表范围匹配,即publish_date字段的值是否大于等于2015-01-01。(相当于a.publish_date >= 2015-01-01’)。

1. 全文检索

  • match query

    标准的全文检索模式,包含模糊匹配、前缀或近似匹配等。

  • match_phrase query

    与match query类似,但只是用来精确匹配的短语。

  • match_phrase_prefix query

    与match_phrase查询类似,但是在最后一个单词上执行通配符搜索。

  • multi_match query

    支持多字段的match query。

  • common terms query

    相比match query,消除停用词与高频词对相关度的影响。

  • query_string query

    查询字符串方式。

  • simple_query_string query

    简单查询字符串方式。

(1)match query详解
# 查询所有
GET /tmpl-word-log*/_search
{
"query": {
"match_all": {}
}
}

# 查询固定条数(默认size=10)
GET /tmpl-word-log*/_search
{
"size": 2,
"query": {
"match_all": {}
}
}

# 查询从10-15条数据(from默认从0开始)
GET /tmpl-word-log*/_search
{
"query": {
"match_all": {}
}
"form": 9,
"size": 5
}

# 按某字段(时间)排序,desc降序,asc升序
GET /tmpl-word-log*/_search
{
"query": {
"match_all": {}
}
, "sort": [
{
"@timestamp": {
"order": "desc"
}
}
]
}

# 只查询某些字段
GET /tmpl-word-log*/_search
{
"size": 2,
"query": {
"match_all": {}
}
, "_source": ["@timestamp", "index"]
}

全文索引查询,这意外着首先会对待查字符串(查询条件)进行分词,然后再去匹配,返回结果中会待上本次匹配的关联度分数。

例如存在这样一条数据:

"_source":{
"post_date":"2009-11-16T14:12:12",
"message":"trying out Elasticsearch",
"user":"dingw2"
}

查询字符串:

"query": {        
"match" : {
"message" : "this out Elasticsearch"
}
}

其JAVA代码对应:

SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchQuery("message", "this out elasticsearch"));

其大体步骤如下:

首先对this out Elasticsearch分词,最终返回结果为 this、out、Elasticsearch,然后分别去库中进行匹配,默认只要一个匹配,就认为匹配,但会加入一个匹配程度(关联度),用scoce分数表示。

match query常用参数详解:

  • operator(操作类型) 可选值为:Operator.OR 和 Operator.AND。表示对查询字符串分词后,返回的词根列表,OR只需一个满足及认为匹配,而AND则需要全部词根都能匹配,默认值为:Operator.OR。
  • minimum_should_match 最少需要匹配个数。在操作类型为Operator.OR时生效,指明分词后的词根,至少minimum_should_match 个词根匹配,则命中。
"match" : {        
"message" : "this out Elasticsearch"
“minimum_should_match ”:“3
}

此时由于this词根并不在原始数据”trying out Elasticsearch”中,又要求必须匹配的词根个数为3,故本次查询,无法命中。minimum_should_match 可选值如下:

TypeExampleDescription
Integer3直接数字,不考虑查询字符串分词后的个数。如果分词的个数小于3个,则无法匹配到任何条目。
Negative integer-2负数表示最多不允许不匹配的个数。也就是需要匹配的个数为(total-2)。
Percentage75%百分比,表示需要匹配的词根占总数的百分比。
Negative percentage-25%允许不匹配的个数占总数的百分比。
Combination3<90%如果查询字符串分词的个数小于等于3(前面的整数),则只要全部匹配则返回,如果分词的个数大于3个,则只要90%的匹配即可。
Multiple combinations2<-25% 9<-3支持多条件表达式,中间用空格分开。该表达式的意义如下:1、如果分词的个数小于等于2,则必须全部匹配;如果大于2小于9,则除了25%(注意负号)之外都需要满足。2、如果大于9个,则只允许其中3个不满足。
  • analyzer

设置分词器,默认使用字段映射中定义的分词器或elasticsearch默认的分词器。

  • lenient

是否忽略由于数据类型不匹配引起的异常,默认为false。例如尝试用文本查询字符串查询数值字段,默认会抛出错误。

  • fuzziness

模糊匹配。

  • zero_terms_query

默认情况下,如果分词器会过滤查询字句中的停用词,可能会造成查询字符串分词后变成空字符串,此时默认的行为是无法匹配到任何文档,如果想改变该默认情况,可以设置zero_terms_query=all,类似于match_all,默认值为none。

  • cutoff_frequency

match查询支持cutoff_frequency,允许指定绝对或相对的文档频率:

  • OR:高频单词被放入“或许有”的类别,仅在至少有一个低频(低于cutoff_frequency)单词满足条件时才积分;
  • AND:高频单词被放入“或许有”的类别,仅在所有低频(低于cutoff_frequency)单词满足条件时才积分。该查询允许在运行时动态处理停用词而不需要使用停用词文件。它阻止了对高频短语(停用词)的评分/迭代,并且只在更重要/更低频率的短语与文档匹配时才会考虑这些文档。然而,如果所有查询条件都高于给定的cutoff_frequency,则查询将自动转换为纯连接(and)查询,以确保快速执行。

cutoff_frequency取值是相对于文档的总数的小数[0..1),也可以是绝对值[1, +∞)。

  • Synonyms(同义词)

可在分词器中定义同义词,具体同义词将在后续章节中会单独介绍。

match query示例:

public static void testMatchQuery() {
RestHighLevelClient client = EsClient.getClient();
try {
SearchRequest searchRequest = new SearchRequest();
searchRequest.indices("twitter");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(
QueryBuilders.matchQuery("message", "is out Elasticsearch")
.zeroTermsQuery(ZeroTermsQuery.ALL)
.operator(Operator.OR)
.minimumShouldMatch("4<90%")
).sort(new FieldSortBuilder("post_date").order(SortOrder.DESC))
.docValueField("post_date", "epoch_millis");
searchRequest.source(sourceBuilder);
SearchResponse result = client.search(searchRequest, RequestOptions.DEFAULT);
System.out.println(result);
} catch (Throwable e) {
e.printStackTrace();
} finally {
EsClient.close(client);
}
}
(2)match_phrase

与match query类似,但只是用来精确匹配的短语。

其主要工作流程:

首先,Elasticsearch(lucene)会使用分词器对全文本进行分词(返回一个一个的词根(顺序排列)),然后同样使用分词器对查询字符串进行分析,返回一个一个的词根(顺序性)。如果能在全字段中能够精确找到与查询字符串通用的词根序列,则认为匹配,否则认为不匹配。

举例如下:

如果原文字段message:”quick brown fox test we will like to you”,则使用标准分词器(analyzer=standard)返回的结果如下:

使用命令:

curl -X GET "192.168.1.10:9200/_analyze" -H 'Content-Type: application/json' -d'
{
"tokenizer" : "standard",
"text" : "quick brown fox test we will like to you",
"attributes" : ["keyword"]
}'

得出如下结果:

{
"tokens":[
{
"token":"quick",
"start_offset":0,
"end_offset":5,
"type":"<ALPHANUM>",
"position":0
},
{
"token":"brown",
"start_offset":6,
"end_offset":11,
"type":"<ALPHANUM>",
"position":1
},
{
"token":"fox",
"start_offset":12,
"end_offset":15,
"type":"<ALPHANUM>",
"position":2
},
{
"token":"test",
"start_offset":16,
"end_offset":20,
"type":"<ALPHANUM>",
"position":3
},
{
"token":"we",
"start_offset":21,
"end_offset":23,
"type":"<ALPHANUM>",
"position":4
},
{
"token":"will",
"start_offset":24,
"end_offset":28,
"type":"<ALPHANUM>",
"position":5
},
{
"token":"like",
"start_offset":29,
"end_offset":33,
"type":"<ALPHANUM>",
"position":6
},
{
"token":"to",
"start_offset":34,
"end_offset":36,
"type":"<ALPHANUM>",
"position":7
},
{
"token":"you",
"start_offset":37,
"end_offset":40,
"type":"<ALPHANUM>",
"position":8
}
]
}

其词根具有顺序性(词根序列)为quick、brown、fox、test 、we 、will、 like、 to 、you

如果查询字符串为 quick brown,分词后的词根序列为 quick brown,则是原词根序列的子集,则匹配。

如果查询字符串为 quick fox,分词后的词根序列为 quick fox,与原词根序列不匹配。如果指定slop属性,设置为1,则匹配,其表示每一个词根直接跳过一个词根形成新的序列,与搜索词根进行比较,是否匹配。

如果查询字符串为quick fox test,其特点是quick与原序列跳过一个词brown,但fox后面不跳过任何次,与test紧挨着,如果指定slop=1,同样能匹配到文档,但查询字符串quick fox test will,却匹配不到文档,说明slop表示整个搜索词根中为了匹配流,能跳过的最大次数。

按照match_phrase的定义,与match query的区别一个在与精确匹配,一个在于词组term(理解为词根序列),故match_phrase与match相比,不会有如下参数:fuzziness、cutoff_frequency、operator、minimum_should_match 这些参数。

(3)match_phrase_prefix

与match phrase基本相同,只是该查询模式会对最后一个词根进行前缀匹配。

GET /_search
{
"query": {
"match_phrase_prefix" : {
"message" : {
"query" : "quick brown f",
"max_expansions" : 10
}
}
}
}

其工作流程如下:首先先对除最后一个词进行分词,得到词根序列 quick brown,然后遍历整个elasticsearch倒排索引,查找以f开头的词根,依次组成多个词根流,例如(quick brown fox) (quick brown foot),默认查找50组,受参数max_expansions控制,在使用时请设置合理的max_expansions,该值越大,查询速度将会变的更慢。该技术主要完成及时搜索,指用户在输入过程中,就根据前缀返回查询结果,随着用户输入的字符越多,查询的结果越接近用户的需求。

(4)multi_match

multi_match查询建立在match查询之上,允许多字段查询。

GET /_search
{
"query": {
"multi_match" : {
"query": "this is a test",
"fields": [ "subject", "message" ] // @1
}
}
}

@1执行作用(查询)的字段,有如下几种用法:

  1. [ “subject”, “message” ] ,表示针对查询自动对subject,message字段进行查询匹配。

  2. [ “title”, “*name” ],支持通配符,表示对title,以name结尾的字段进行查询匹配。

  3. [ “subject^3”, “message” ],表示subject字段是message的重要性的3倍,类似于字段权重

2. 布尔查询

# 匹配word包含中秋(两个字中有一个就能匹配)且index=2的数据
GET /tmpl-word-log*/_search
{
"query": {
"bool": {
"must": [
{"match":{"word": "中秋"}},
{"match": {"index": 2}}
]
}
}
}

# 匹配word包含中秋(两个字中有一个就能匹配)或index=2的数据
GET /tmpl-word-log*/_search
{
"query": {
"bool": {
"should": [
{"match":{"word": "中秋"}},
{"match": {"index": 2}}
]
}
}
}

# 匹配word不包含中秋(两个字中有一个就能匹配)且index=2的数据
GET /tmpl-word-log*/_search
{
"query": {
"bool": {
"must_not": [
{"match":{"word": "中秋"}},
{"match": {"index": 2}}
]
}
}
}

# 在bool中must,must_not,should等关键字可以混用,表示多条件同时满足
# 匹配word包含中秋(两个字中有一个就能匹配)且index!=2的数据
GET /tmpl-word-log*/_search
{
"query": {
"bool": {
"must": [
{"match":{"word": "中秋"}}
]
, "must_not": [
{"match": {"index": 2}}
]
}
}
}

3. 范围查询

# 查询索引在11-20之间的数据
GET /tmpl-word-log*/_search
{
"query": {
"range": {
"index": {
"gte": 10,
"lte": 20
}
}
}
}

4. 过滤查询(filters)

# 在Elasticsearch5.x版本中filtered方法弃用改用bool中的filter
# 匹配日期在9-129-24日的数据
GET /tmpl-word-log*/_search
{
"query": {
"bool":{
"must": {"match_all": {}},
"filter":{
"range":{
"@timestamp":{
"gte":"2018-09-12T00:00:00.000+0800",
"lte":"2018-09-24T23:59:59.000+0800"
}
}
}
}
}
}

5. 分页查询

(1)from / size 分页

from - 表示起始位置,size - 表示每页数量;类似与 MySQL 的 limit + offset。
示例:

GET /_search
{
"from" : 10, "size" : 10,
"query" : {
"term" : { "user" : "kimchy" }
}
}

需要注意的是,from + size 不能超过10000,也就是说在前10000条之内,可以随意翻页,10000条之后就不行了。

实际上,通过设置 index.max_result_window 可以修改这个限制,但是不建议这么做,因为这种方式翻页越深效率越低。

原理:

Query阶段:

  1. 当一个请求发送到某个ES节点时,该节点(Node1)会根据from和size,建立一个结果集窗口,窗口大小为from+size。假如from=10000,size=100,则窗口大小为10100。
  2. 此节点将请求广播到其他相关节点上,从每个Shards中取Top 10100条的score和id。
  3. 所有Shards获取的结果汇聚到Node1,假如有5个Shards,则一共会取到5 * 10100 = 50500条数据。
  4. Node1进行归并排序,并选择Top 10100条,存入结果集窗口。

Fetch阶段:
根据Query阶段得到的排序结果,从 from 位置取 size 条数据,抓取文档详细内容返回。

从此过程中可以看出,翻页越靠后,需要参与排序的文档就越多,效率也就越低。所以,如果结果集很大,不建议用这种分页方式。

(2)使用 scroll 分页

使用scroll就像传统数据库中的游标一样,方式如下:

第一步

POST /twitter/_search?scroll=1m
{
"size": 100,
"query": {
"match" : {
"title" : "elasticsearch"
}
}
}

scroll=1m,表示“search context”存活时间1分钟。返回结果中会带有一个“_scroll_id”,这个值在后续的翻页过程中使用。

第二步

POST /_search/scroll
{
"scroll" : "1m",
"scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
}

不用指定index和type,也不用其他查询条件,只要把上一步的_scroll_id即可。

之后翻页一直如此,每次执行会自动滚动100条数据,直到返回的结果为空为止。

每次执行间隔不要超过1分钟,否则“search context”会释放掉。

第三步

DELETE /_search/scroll
{
"scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
}

结果遍历完成后,删除scroll_id。这一步也可以不做,等1分钟后没有继续翻页请求,“search context”会自动释放掉,不过建议还是手动清除,节省资源。

优化:
如果目的是为了遍历所有结果,而不关心结果的顺序,那么可以按“_doc”排序来提高性能

POST /twitter/_search?scroll=1m
{
"size": 100,
"query": {
"match" : {
"title" : "elasticsearch"
}
},
"sort": ["_doc"]
}

与 from/size 分页方式不同,使用 scroll 分页只能单向顺序翻页,不能随机翻页,适用于遍历结果集的场景。

scroll 翻页能够深度翻页,但是翻页期间需要维护“search context”,这是需要占用一定资源的。

所以对于用户高并发访问的场景,不推荐用这种方式,scroll 更适用于批处理类的后台任务。

(3)使用scroll-scan 的高效滚动

scroll API 保持了那些已经返回记录结果,所以能更加高效地返回排序的结果。但是,按照默认设定排序结果仍然需要代价。

一般来说,你仅仅想要找到结果,不关心顺序。你可以通过组合 scroll 和 scan 来关闭任何打分或者排序,以最高效的方式返回结果。你需要做的就是将 search_type=scan 加入到查询的字符串中:

POST /my_index/my_type/_search?scroll=1m&search_type=scan
{
"query": {
"match" : {
"cityName" : "杭州"
}
}
}

设置 search_type 为 scan 可以关闭打分,让滚动更加高效。
扫描式的滚动请求和标准的滚动请求有四处不同:

(1)不算分,关闭排序。结果会按照在索引中出现的顺序返回;
(2)不支持聚合;
(3)初始 search 请求的响应不会在 hits 数组中包含任何结果。第一批结果就会按照第一个 scroll 请求返回。
(4)参数 size 控制了每个分片上而非每个请求的结果数目,所以 size 为 10 的情况下,如果命中了 5 个分片,那么每个 scroll 请求最多会返回 50 个结果。

如果你想支持打分,即使不进行排序,将 track_scores 设置为 true。

(4)使用 search after 分页

这种方式同样可以深度翻页,但是弥补了 scroll 方式的不足。其思想是:用前一次的查询结果作为下一次的查询条件。

示例:

首次查询

GET /user_model/_search
{
"size": 10,
"query": {"match_all": {}},
"sort": [
{"_id": "asc"}
]
}

返回结果:

{
"took" : 4379,
"timed_out" : false,
"_shards" : {
"total" : 6,
"successful" : 6,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 38213940,
"max_score" : null,
"hits" : [
...
{
"_index" : "user_model",
"_type" : "_doc",
"_id" : "00000f78f59644b1967783986c35496c",
"_score" : null,
"_source" : {
...
},
"sort" : [
"00000f78f59644b1967783986c35496c"
]
}
]
}
}

后续查询

GET /user_model/_search
{
"size": 10,
"query": {"match_all": {}},
"sort": [
{"_id": "asc"}
],
"search_after": ["00000f78f59644b1967783986c35496c"]
}

其中,search_after 为上次查询结果中最后一条记录的 sort 值。

(5)总结:
  1. 如果数据量小(10000条内),或者只关注结果集的TopN数据,可以使用from / size 分页,简单粗暴
  2. 数据量大,深度翻页,后台批处理任务(数据迁移)之类的任务,使用 scroll 方式
  3. 数据量大,深度翻页,用户实时、高并发查询需求,使用 search after 方式

6. 批量查询

Elasticsearch 的速度已经很快了,但甚至能更快。 将多个请求合并成一个,避免单独处理每个请求花费的网络延时和开销。 如果你需要从 Elasticsearch 检索很多文档,那么使用 multi-get 或者 mget API 来将这些检索请求放在一个请求中,将比逐个文档请求更快地检索到全部文档。

mget API 要求有一个 docs 数组作为参数,每个元素包含需要检索文档的元数据, 包括 _index_type_id 。如果你想检索一个或者多个特定的字段,那么你可以通过 _source 参数来指定这些字段的名字:

GET /_mget
{
"docs" : [
{
"_index" : "website",
"_type" : "blog",
"_id" : 2
},
{
"_index" : "website",
"_type" : "pageviews",
"_id" : 1,
"_source": "views"
}
]
}

该响应体也包含一个 docs 数组, 对于每一个在请求中指定的文档,这个数组中都包含有一个对应的响应,且顺序与请求中的顺序相同。 其中的每一个响应都和使用单个 get request 请求所得到的响应体相同:

{
"docs" : [
{
"_index" : "website",
"_id" : "2",
"_type" : "blog",
"found" : true,
"_source" : {
"text" : "This is a piece of cake...",
"title" : "My first external blog entry"
},
"_version" : 10
},
{
"_index" : "website",
"_id" : "1",
"_type" : "pageviews",
"found" : true,
"_version" : 2,
"_source" : {
"views" : 2
}
}
]
}

如果想检索的数据都在相同的 _index 中(甚至相同的 _type 中),则可以在 URL 中指定默认的 /_index 或者默认的 /_index/_type

你仍然可以通过单独请求覆盖这些值:

GET /website/blog/_mget
{
"docs" : [
{ "_id" : 2 },
{ "_type" : "pageviews", "_id" : 1 }
]
}

事实上,如果所有文档的 _index_type 都是相同的,你可以只传一个 ids 数组,而不是整个 docs 数组:

GET /website/blog/_mget
{
"ids" : [ "2", "1" ]
}

注意,我们请求的第二个文档是不存在的。我们指定类型为 blog ,但是文档 ID 1 的类型是 pageviews ,这个不存在的情况将在响应体中被报告:

{
"docs" : [
{
"_index" : "website",
"_type" : "blog",
"_id" : "2",
"_version" : 10,
"found" : true,
"_source" : {
"title": "My first external blog entry",
"text": "This is a piece of cake..."
}
},
{
"_index" : "website",
"_type" : "blog",
"_id" : "1",
"found" : false
}
]
}

2.4-批量操作

mget 可以使我们一次取回多个文档同样的方式, bulk API 允许在单个步骤中进行多次 createindexupdatedelete 请求。 如果你需要索引一个数据流比如日志事件,它可以排队和索引数百或数千批次。

bulk 与其他的请求体格式稍有不同,如下所示:

{ action: { metadata }}\n
{ request body }\n
{ action: { metadata }}\n
{ request body }\n
...

这种格式类似一个有效的单行 JSON 文档 ,它通过换行符(\n)连接到一起。注意两个要点:

  • 每行一定要以换行符(\n)结尾, 包括最后一行 。这些换行符被用作一个标记,可以有效分隔行。
  • 这些行不能包含未转义的换行符,因为他们将会对解析造成干扰。这意味着这个 JSON 能使用 pretty 参数打印。

bulk的请求模板

分成action、metadata和doc三部份

action : 必须是以下4种选项之一

  • index(最常用) : 如果文档不存在就创建他,如果文档存在就更新他

  • create : 如果文档不存在就创建他,但如果文档存在就返回错误

    使用时一定要在metadata设置_id值,他才能去判断这个文档是否存在

  • update : 更新一个文档,如果文档不存在就返回错误

    使用时也要给_id值,且后面文档的格式和其他人不一样

  • delete : 删除一个文档,如果要删除的文档id不存在,就返回错误

    使用时也必须在metadata中设置文档_id,且后面不能带一个doc,因为没意义,他是用_id去删除文档的

metadata : 设置这个文档的metadata,像是_id、_index、_type…

doc : 就是一般的文档格式

metadata 应该指定被索引、创建、更新或者删除的文档的 _index_type_id

例如,一个 delete 请求看起来是这样的:

{ "delete": { "_index": "website", "_type": "blog", "_id": "123" }}

request body 行由文档的 _source 本身组成—文档包含的字段和值。它是 indexcreate 操作所必需的,这是有道理的:你必须提供文档以索引。

它也是 update 操作所必需的,并且应该包含你传递给 update API 的相同请求体: docupsertscript 等等。 删除操作不需要 request body 行。

{ "create":  { "_index": "website", "_type": "blog", "_id": "123" }}
{ "title": "My first blog post" }

如果不指定 _id ,将会自动生成一个 ID :

{ "index": { "_index": "website", "_type": "blog" }}
{ "title": "My second blog post" }

为了把所有的操作组合在一起,一个完整的 bulk 请求 有以下形式:

POST /_bulk
{ "delete": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "create": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "title": "My first blog post" }
{ "index": { "_index": "website", "_type": "blog" }}
{ "title": "My second blog post" }
{ "update": { "_index": "website", "_type": "blog", "_id": "123", "_retry_on_conflict" : 3} }
{ "doc" : {"title" : "My updated blog post"} }

3-ES整合SpringBoot

面介绍下 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 查询的使用其上手很快。

3.1-Maven 引入相关依赖

  • lombok:lombok 工具依赖。

  • fastjson:用于将 JSON 转换对象的依赖。

  • spring-boot-starter-web: SpringBoot 的 Web 依赖。

  • elasticsearch:ElasticSearch:依赖,需要和 ES 版本保持一致。

  • elasticsearch-rest-high-level-client:用于操作 ES 的 Java 客户端。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>club.mydlq</groupId>
<artifactId>springboot-elasticsearch-example</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-elasticsearch-example</name>
<description>Demo project for Spring Boot ElasticSearch</description>

<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.61</version>
</dependency>
<!--elasticsearch-->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>6.5.4</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>6.5.4</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

3.2-ElasticSearch 连接配置

1. application.yml 配置文件

为了方便更改连接 ES 的连接配置,所以我们将配置信息放置于 application.yaml 中:

#base
server:
port: 8080
#spring
spring:
application:
name: springboot-elasticsearch-example
#elasticsearch
elasticsearch:
schema: http
address: 127.0.0.1:9200
connectTimeout: 5000
socketTimeout: 5000
connectionRequestTimeout: 5000
maxConnectNum: 100
maxConnectPerRoute: 100

2. java 连接配置类

这里需要写一个 Java 配置类读取 application 中的配置信息:

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;

/**
* ElasticSearch 配置
*/
@Configuration
public class ElasticSearchConfig {

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

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

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

/** Socket 连接超时时间 */
@Value("${elasticsearch.socketTimeout:10000}")
private int socketTimeout;

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

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

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

@Bean
public RestHighLevelClient restHighLevelClient() {
// 拆分地址
List<HttpHost> hostLists = new ArrayList<>();
String[] hostList = address.split(",");
for (String addr : hostList) {
String host = addr.split(":")[0];
String port = addr.split(":")[1];
hostLists.add(new HttpHost(host, Integer.parseInt(port), schema));
}
// 转换成 HttpHost 数组
HttpHost[] httpHost = hostLists.toArray(new HttpHost[]{});
// 构建连接对象
RestClientBuilder builder = RestClient.builder(httpHost);
// 异步连接延时配置
builder.setRequestConfigCallback(requestConfigBuilder -> {
requestConfigBuilder.setConnectTimeout(connectTimeout);
requestConfigBuilder.setSocketTimeout(socketTimeout);
requestConfigBuilder.setConnectionRequestTimeout(connectionRequestTimeout);
return requestConfigBuilder;
});
// 异步连接数配置
builder.setHttpClientConfigCallback(httpClientBuilder -> {
httpClientBuilder.setMaxConnTotal(maxConnectNum);
httpClientBuilder.setMaxConnPerRoute(maxConnectPerRoute);
return httpClientBuilder;
});
return new RestHighLevelClient(builder);
}

}

3.3-索引操作示例

1. Restful 操作示例

创建索引

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

PUT /mydlq-user
{
"mappings": {
"doc": {
"dynamic": true,
"properties": {
"name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"address": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"remark": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"age": {
"type": "integer"
},
"salary": {
"type": "float"
},
"birthDate": {
"type": "date",
"format": "yyyy-MM-dd"
},
"createTime": {
"type": "date"
}
}
}
}
}

删除索引

删除 mydlq-user 索引。

DELETE /mydlq-user

2. Java 代码示例

import lombok.extern.slf4j.Slf4j;
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.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;

@Slf4j
@Service
public class IndexService2 {

@Autowired
private RestHighLevelClient restHighLevelClient;

/**
* 创建索引
*/
public void createIndex() {
try {
// 创建 Mapping
XContentBuilder mapping = XContentFactory.jsonBuilder()
.startObject()
.field("dynamic", true)
.startObject("properties")
.startObject("name")
.field("type","text")
.startObject("fields")
.startObject("keyword")
.field("type","keyword")
.endObject()
.endObject()
.endObject()
.startObject("address")
.field("type","text")
.startObject("fields")
.startObject("keyword")
.field("type","keyword")
.endObject()
.endObject()
.endObject()
.startObject("remark")
.field("type","text")
.startObject("fields")
.startObject("keyword")
.field("type","keyword")
.endObject()
.endObject()
.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", 1)
.put("index.number_of_replicas", 0)
.build();
// 新建创建索引请求对象,然后设置索引类型(ES 7.0 将不存在索引类型)和 mapping 与 index 配置
CreateIndexRequest request = new CreateIndexRequest("mydlq-user", settings);
request.mapping("doc", mapping);
// RestHighLevelClient 执行创建索引
CreateIndexResponse createIndexResponse = restHighLevelClient.indices().create(request, RequestOptions.DEFAULT);
// 判断是否创建成功
boolean isCreated = createIndexResponse.isAcknowledged();
log.info("是否创建成功:{}", isCreated);
} catch (IOException e) {
log.error("", e);
}
}

/**
* 删除索引
*/
public void deleteIndex() {
try {
// 新建删除索引请求对象
DeleteIndexRequest request = new DeleteIndexRequest("mydlq-user");
// 执行删除索引
AcknowledgedResponse acknowledgedResponse = restHighLevelClient.indices().delete(request, RequestOptions.DEFAULT);
// 判断是否删除成功
boolean siDeleted = acknowledgedResponse.isAcknowledged();
log.info("是否删除成功:{}", siDeleted);
} catch (IOException e) {
log.error("", e);
}
}

}

3.4-文档操作示例

1. Restful 操作示例

增加文档信息

在索引 mydlq-user 中增加一条文档信息。

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

获取文档信息

获取 mydlq-user 的索引 id=1 的文档信息。

GET /mydlq-user/doc/1

更新文档信息

更新之前创建的 id=1 的文档信息。

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

删除文档信息

删除之前创建的 id=1 的文档信息。

DELETE /mydlq-user/doc/1

2. Java 代码示例

import club.mydlq.elasticsearch.model.entity.UserInfo;
import com.alibaba.fastjson.JSON;
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.common.xcontent.XContentType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.Date;

@Slf4j
@Service
public class IndexService {

@Autowired
private RestHighLevelClient restHighLevelClient;

/**
* 增加文档信息
*/
public void addDocument() {
try {
// 创建索引请求对象
IndexRequest indexRequest = new IndexRequest("mydlq-user", "doc", "1");
// 创建员工信息
UserInfo userInfo = new UserInfo();
userInfo.setName("张三");
userInfo.setAge(29);
userInfo.setSalary(100.00f);
userInfo.setAddress("北京市");
userInfo.setRemark("来自北京市的张先生");
userInfo.setCreateTime(new Date());
userInfo.setBirthDate("1990-01-10");
// 将对象转换为 byte 数组
byte[] json = JSON.toJSONBytes(userInfo);
// 设置文档内容
indexRequest.source(json, XContentType.JSON);
// 执行增加文档
IndexResponse response = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
log.info("创建状态:{}", response.status());
} catch (Exception e) {
log.error("", e);
}
}

/**
* 获取文档信息
*/
public void getDocument() {
try {
// 获取请求对象
GetRequest getRequest = new GetRequest("mydlq-user", "doc", "1");
// 获取文档信息
GetResponse getResponse = restHighLevelClient.get(getRequest, RequestOptions.DEFAULT);
// 将 JSON 转换成对象
if (getResponse.isExists()) {
UserInfo userInfo = JSON.parseObject(getResponse.getSourceAsBytes(), UserInfo.class);
log.info("员工信息:{}", userInfo);
}
} catch (IOException e) {
log.error("", e);
}
}

/**
* 更新文档信息
*/
public void updateDocument() {
try {
// 创建索引请求对象
UpdateRequest updateRequest = new UpdateRequest("mydlq-user", "doc", "1");
// 设置员工更新信息
UserInfo userInfo = new UserInfo();
userInfo.setSalary(200.00f);
userInfo.setAddress("北京市海淀区");
// 将对象转换为 byte 数组
byte[] json = JSON.toJSONBytes(userInfo);
// 设置更新文档内容
updateRequest.doc(json, XContentType.JSON);
// 执行更新文档
UpdateResponse response = restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);
log.info("创建状态:{}", response.status());
} catch (Exception e) {
log.error("", e);
}
}

/**
* 删除文档信息
*/
public void deleteDocument() {
try {
// 创建删除请求对象
DeleteRequest deleteRequest = new DeleteRequest("mydlq-user", "doc", "1");
// 执行删除文档
DeleteResponse response = restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT);
log.info("删除状态:{}", response.status());
} catch (IOException e) {
log.error("", e);
}
}

}

3.5-插入初始化数据

1. 单条插入

POST mydlq-user/_doc

{
"name":"零零",
"address":"北京市丰台区",
"remark":"低层员工",
"age":29,
"salary":3000,
"birthDate":"1990-11-11",
"createTime":"2019-11-11T08:18:00.000Z"
}

2. 批量插入

POST _bulk

{"index":{"_index":"mydlq-user","_type":"doc"}}
{"name":"刘一","address":"北京市丰台区","remark":"低层员工","age":30,"salary":3000,"birthDate":"1989-11-11","createTime":"2019-03-15T08:18:00.000Z"}
{"index":{"_index":"mydlq-user","_type":"doc"}}
{"name":"陈二","address":"北京市昌平区","remark":"中层员工","age":27,"salary":7900,"birthDate":"1992-01-25","createTime":"2019-11-08T11:15:00.000Z"}
{"index":{"_index":"mydlq-user","_type":"doc"}}
{"name":"张三","address":"北京市房山区","remark":"中层员工","age":28,"salary":8800,"birthDate":"1991-10-05","createTime":"2019-07-22T13:22:00.000Z"}
{"index":{"_index":"mydlq-user","_type":"doc"}}
{"name":"李四","address":"北京市大兴区","remark":"高层员工","age":26,"salary":9000,"birthDate":"1993-08-18","createTime":"2019-10-17T15:00:00.000Z"}
{"index":{"_index":"mydlq-user","_type":"doc"}}
{"name":"王五","address":"北京市密云区","remark":"低层员工","age":31,"salary":4800,"birthDate":"1988-07-20","createTime":"2019-05-29T09:00:00.000Z"}
{"index":{"_index":"mydlq-user","_type":"doc"}}
{"name":"赵六","address":"北京市通州区","remark":"中层员工","age":32,"salary":6500,"birthDate":"1987-06-02","createTime":"2019-12-10T18:00:00.000Z"}
{"index":{"_index":"mydlq-user","_type":"doc"}}
{"name":"孙七","address":"北京市朝阳区","remark":"中层员工","age":33,"salary":7000,"birthDate":"1986-04-15","createTime":"2019-06-06T13:00:00.000Z"}
{"index":{"_index":"mydlq-user","_type":"doc"}}
{"name":"周八","address":"北京市西城区","remark":"低层员工","age":32,"salary":5000,"birthDate":"1987-09-26","createTime":"2019-01-26T14:00:00.000Z"}
{"index":{"_index":"mydlq-user","_type":"doc"}}
{"name":"吴九","address":"北京市海淀区","remark":"高层员工","age":30,"salary":11000,"birthDate":"1989-11-25","createTime":"2019-09-07T13:34:00.000Z"}
{"index":{"_index":"mydlq-user","_type":"doc"}}
{"name":"郑十","address":"北京市东城区","remark":"低层员工","age":29,"salary":5000,"birthDate":"1990-12-25","createTime":"2019-03-06T12:08:00.000Z"}
{"index":{"_index":"mydlq-user","_type":"doc"}}
{"name":"萧十一","address":"北京市平谷区","remark":"低层员工","age":29,"salary":3300,"birthDate":"1990-11-11","createTime":"2019-03-10T08:17:00.000Z"}
{"index":{"_index":"mydlq-user","_type":"doc"}}
{"name":"曹十二","address":"北京市怀柔区","remark":"中层员工","age":27,"salary":6800,"birthDate":"1992-01-25","createTime":"2019-12-03T11:09:00.000Z"}
{"index":{"_index":"mydlq-user","_type":"doc"}}
{"name":"吴十三","address":"北京市延庆区","remark":"中层员工","age":25,"salary":7000,"birthDate":"1994-10-05","createTime":"2019-07-27T14:22:00.000Z"}
{"index":{"_index":"mydlq-user","_type":"doc"}}
{"name":"冯十四","address":"北京市密云区","remark":"低层员工","age":25,"salary":3000,"birthDate":"1994-08-18","createTime":"2019-04-22T15:00:00.000Z"}
{"index":{"_index":"mydlq-user","_type":"doc"}}
{"name":"蒋十五","address":"北京市通州区","remark":"低层员工","age":31,"salary":2800,"birthDate":"1988-07-20","createTime":"2019-06-13T10:00:00.000Z"}
{"index":{"_index":"mydlq-user","_type":"doc"}}
{"name":"苗十六","address":"北京市门头沟区","remark":"高层员工","age":32,"salary":11500,"birthDate":"1987-06-02","createTime":"2019-11-11T18:00:00.000Z"}
{"index":{"_index":"mydlq-user","_type":"doc"}}
{"name":"鲁十七","address":"北京市石景山区","remark":"高员工","age":33,"salary":9500,"birthDate":"1986-04-15","createTime":"2019-06-06T14:00:00.000Z"}
{"index":{"_index":"mydlq-user","_type":"doc"}}
{"name":"沈十八","address":"北京市朝阳区","remark":"中层员工","age":31,"salary":8300,"birthDate":"1988-09-26","createTime":"2019-09-25T14:00:00.000Z"}
{"index":{"_index":"mydlq-user","_type":"doc"}}
{"name":"吕十九","address":"北京市西城区","remark":"低层员工","age":31,"salary":4500,"birthDate":"1988-11-25","createTime":"2019-09-22T13:34:00.000Z"}
{"index":{"_index":"mydlq-user","_type":"doc"}}
{"name":"丁二十","address":"北京市东城区","remark":"低层员工","age":33,"salary":2100,"birthDate":"1986-12-25","createTime":"2019-03-07T12:08:00.000Z"}

3. 查询数据

插入完成后再查询数据,查看之前插入的数据是否存在:

GET mydlq-user/_search

执行后得到下面记录:

{
"took": 2,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 20,
"max_score": 1,
"hits": [
{
"_index": "mydlq-user",
"_type": "_doc",
"_id": "BeN0BW8B7BNodGwRFTRj",
"_score": 1,
"_source": {
"name": "刘一",
"address": "北京市丰台区",
"remark": "低层员工",
"age": 30,
"salary": 3000,
"birthDate": "1989-11-11",
"createTime": "2019-03-15T08:18:00.000Z"
}
},
{
"_index": "mydlq-user",
"_type": "_doc",
"_id": "BuN0BW8B7BNodGwRFTRj",
"_score": 1,
"_source": {
"name": "陈二",
"address": "北京市昌平区",
"remark": "中层员工",
"age": 27,
"salary": 7900,
"birthDate": "1992-01-25",
"createTime": "2019-11-08T11:15:00.000Z"
}
},
{
"_index": "mydlq-user",
"_type": "_doc",
"_id": "B-N0BW8B7BNodGwRFTRj",
"_score": 1,
"_source": {
"name": "张三",
"address": "北京市房山区",
"remark": "中层员工",
"age": 28,
"salary": 8800,
"birthDate": "1991-10-05",
"createTime": "2019-07-22T13:22:00.000Z"
}
},
{
"_index": "mydlq-user",
"_type": "_doc",
"_id": "CON0BW8B7BNodGwRFTRj",
"_score": 1,
"_source": {
"name": "李四",
"address": "北京市大兴区",
"remark": "高层员工",
"age": 26,
"salary": 9000,
"birthDate": "1993-08-18",
"createTime": "2019-10-17T15:00:00.000Z"
}
},
{
"_index": "mydlq-user",
"_type": "_doc",
"_id": "CeN0BW8B7BNodGwRFTRj",
"_score": 1,
"_source": {
"name": "王五",
"address": "北京市密云区",
"remark": "低层员工",
"age": 31,
"salary": 4800,
"birthDate": "1988-07-20",
"createTime": "2019-05-29T09:00:00.000Z"
}
},
{
"_index": "mydlq-user",
"_type": "_doc",
"_id": "CuN0BW8B7BNodGwRFTRj",
"_score": 1,
"_source": {
"name": "赵六",
"address": "北京市通州区",
"remark": "中层员工",
"age": 32,
"salary": 6500,
"birthDate": "1987-06-02",
"createTime": "2019-12-10T18:00:00.000Z"
}
},
{
"_index": "mydlq-user",
"_type": "_doc",
"_id": "C-N0BW8B7BNodGwRFTRj",
"_score": 1,
"_source": {
"name": "孙七",
"address": "北京市朝阳区",
"remark": "中层员工",
"age": 33,
"salary": 7000,
"birthDate": "1986-04-15",
"createTime": "2019-06-06T13:00:00.000Z"
}
},
{
"_index": "mydlq-user",
"_type": "_doc",
"_id": "DON0BW8B7BNodGwRFTRj",
"_score": 1,
"_source": {
"name": "周八",
"address": "北京市西城区",
"remark": "低层员工",
"age": 32,
"salary": 5000,
"birthDate": "1987-09-26",
"createTime": "2019-01-26T14:00:00.000Z"
}
},
{
"_index": "mydlq-user",
"_type": "_doc",
"_id": "DeN0BW8B7BNodGwRFTRj",
"_score": 1,
"_source": {
"name": "吴九",
"address": "北京市海淀区",
"remark": "高层员工",
"age": 30,
"salary": 11000,
"birthDate": "1989-11-25",
"createTime": "2019-09-07T13:34:00.000Z"
}
},
{
"_index": "mydlq-user",
"_type": "_doc",
"_id": "DuN0BW8B7BNodGwRFTRj",
"_score": 1,
"_source": {
"name": "郑十",
"address": "北京市东城区",
"remark": "低层员工",
"age": 29,
"salary": 5000,
"birthDate": "1990-12-25",
"createTime": "2019-03-06T12:08:00.000Z"
}
}
]
}
}

3.6-查询操作示例

1. 精确查询(term)

(1)Restful 操作示例

精确查询

精确查询,查询地址为 北京市通州区 的人员信息:

查询条件不会进行分词,但是查询内容可能会分词,导致查询不到。之前在创建索引时设置 Mapping 中 address 字段存在 keyword 字段是专门用于不分词查询的子字段。

GET mydlq-user/_search
{
"query": {
"term": {
"address.keyword": {
"value": "北京市通州区"
}
}
}
}

精确查询-多内容查询

精确查询,查询地址为 北京市丰台区、北京市昌平区 或 北京市大兴区 的人员信息:

GET mydlq-user/_search
{
"query": {
"terms": {
"address.keyword": [
"北京市丰台区",
"北京市昌平区",
"北京市大兴区"
]
}
}
}
(2)Java 代码示例
import club.mydlq.elasticsearch.model.entity.UserInfo;
import com.alibaba.fastjson.JSON;
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.stereotype.Service;
import java.io.IOException;

@Slf4j
@Service
public class TermQueryService {

@Autowired
private RestHighLevelClient restHighLevelClient;

/**
* 精确查询(查询条件不会进行分词,但是查询内容可能会分词,导致查询不到)
*/
public void termQuery() {
try {
// 构建查询条件(注意:termQuery 支持多种格式查询,如 boolean、int、double、string 等,这里使用的是 string 的查询)
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.termQuery("address.keyword", "北京市通州区"));
// 创建查询请求对象,将查询对象配置到其中
SearchRequest searchRequest = new SearchRequest("mydlq-user");
searchRequest.source(searchSourceBuilder);
// 执行查询,然后处理响应结果
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 根据状态和数据条数验证是否返回了数据
if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().totalHits > 0) {
SearchHits hits = searchResponse.getHits();
for (SearchHit hit : hits) {
// 将 JSON 转换成对象
UserInfo userInfo = JSON.parseObject(hit.getSourceAsString(), UserInfo.class);
// 输出查询信息
log.info(userInfo.toString());
}
}
} catch (IOException e) {
log.error("", e);
}
}

/**
* 多个内容在一个字段中进行查询
*/
public void termsQuery() {
try {
// 构建查询条件(注意:termsQuery 支持多种格式查询,如 boolean、int、double、string 等,这里使用的是 string 的查询)
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.termsQuery("address.keyword", "北京市丰台区", "北京市昌平区", "北京市大兴区"));
// 创建查询请求对象,将查询对象配置到其中
SearchRequest searchRequest = new SearchRequest("mydlq-user");
searchRequest.source(searchSourceBuilder);
// 执行查询,然后处理响应结果
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 根据状态和数据条数验证是否返回了数据
if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().totalHits > 0) {
SearchHits hits = searchResponse.getHits();
for (SearchHit hit : hits) {
// 将 JSON 转换成对象
UserInfo userInfo = JSON.parseObject(hit.getSourceAsString(), UserInfo.class);
// 输出查询信息
log.info(userInfo.toString());
}
}
} catch (IOException e) {
log.error("", e);
}
}

}

2. 匹配查询(match)

(1)Restful 操作示例

匹配查询全部数据与分页

匹配查询符合条件的所有数据,并且设置以 salary 字段升序排序,并设置分页:

GET mydlq-user/_search
{
"query": {
"match_all": {}
},
"from": 0,
"size": 10,
"sort": [
{
"salary": {
"order": "asc"
}
}
]
}

匹配查询数据

匹配查询地址为 通州区 的数据:

GET mydlq-user/_search
{
"query": {
"match": {
"address": "通州区"
}
}
}

词语匹配查询

词语匹配进行查询,匹配 address 中为 北京市通州区 的员工信息:

GET mydlq-user/_search
{
"query": {
"match_phrase": {
"address": "北京市通州区"
}
}
}

内容多字段查询

查询在字段 address、remark 中存在 北京 内容的员工信息:

GET mydlq-user/_search
{
"query": {
"multi_match": {
"query": "北京",
"fields": ["address","remark"]
}
}
}
(2)Java 代码示例
import club.mydlq.elasticsearch.model.entity.UserInfo;
import com.alibaba.fastjson.JSON;
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.MatchAllQueryBuilder;
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.stereotype.Service;
import java.io.IOException;

@Slf4j
@Service
public class MatchQueryService {

@Autowired
private RestHighLevelClient restHighLevelClient;

/**
* 匹配查询符合条件的所有数据,并设置分页
*/
public Object matchAllQuery() {
try {
// 构建查询条件
MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery();
// 创建查询源构造器
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(matchAllQueryBuilder);
// 设置分页
searchSourceBuilder.from(0);
searchSourceBuilder.size(3);
// 设置排序
searchSourceBuilder.sort("salary", SortOrder.ASC);
// 创建查询请求对象,将查询对象配置到其中
SearchRequest searchRequest = new SearchRequest("mydlq-user");
searchRequest.source(searchSourceBuilder);
// 执行查询,然后处理响应结果
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 根据状态和数据条数验证是否返回了数据
if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().totalHits > 0) {
SearchHits hits = searchResponse.getHits();
for (SearchHit hit : hits) {
// 将 JSON 转换成对象
UserInfo userInfo = JSON.parseObject(hit.getSourceAsString(), UserInfo.class);
// 输出查询信息
log.info(userInfo.toString());
}
}
} catch (IOException e) {
log.error("", e);
}
}

/**
* 匹配查询数据
*/
public Object matchQuery() {
try {
// 构建查询条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.matchQuery("address", "*通州区"));
// 创建查询请求对象,将查询对象配置到其中
SearchRequest searchRequest = new SearchRequest("mydlq-user");
searchRequest.source(searchSourceBuilder);
// 执行查询,然后处理响应结果
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 根据状态和数据条数验证是否返回了数据
if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().totalHits > 0) {
SearchHits hits = searchResponse.getHits();
for (SearchHit hit : hits) {
// 将 JSON 转换成对象
UserInfo userInfo = JSON.parseObject(hit.getSourceAsString(), UserInfo.class);
// 输出查询信息
log.info(userInfo.toString());
}
}
} catch (IOException e) {
log.error("", e);
}
}

/**
* 词语匹配查询
*/
public Object matchPhraseQuery() {
try {
// 构建查询条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.matchPhraseQuery("address", "北京市通州区"));
// 创建查询请求对象,将查询对象配置到其中
SearchRequest searchRequest = new SearchRequest("mydlq-user");
searchRequest.source(searchSourceBuilder);
// 执行查询,然后处理响应结果
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 根据状态和数据条数验证是否返回了数据
if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().totalHits > 0) {
SearchHits hits = searchResponse.getHits();
for (SearchHit hit : hits) {
// 将 JSON 转换成对象
UserInfo userInfo = JSON.parseObject(hit.getSourceAsString(), UserInfo.class);
// 输出查询信息
log.info(userInfo.toString());
}
}
} catch (IOException e) {
log.error("", e);
}
}

/**
* 内容在多字段中进行查询
*/
public Object matchMultiQuery() {
try {
// 构建查询条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.multiMatchQuery("北京市", "address", "remark"));
// 创建查询请求对象,将查询对象配置到其中
SearchRequest searchRequest = new SearchRequest("mydlq-user");
searchRequest.source(searchSourceBuilder);
// 执行查询,然后处理响应结果
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 根据状态和数据条数验证是否返回了数据
if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().totalHits > 0) {
SearchHits hits = searchResponse.getHits();
for (SearchHit hit : hits) {
// 将 JSON 转换成对象
UserInfo userInfo = JSON.parseObject(hit.getSourceAsString(), UserInfo.class);
// 输出查询信息
log.info(userInfo.toString());
}
}
} catch (IOException e) {
log.error("", e);
}
}

}

3. 模糊查询(fuzzy)

(1)Restful 操作示例

模糊查询所有以 三 结尾的姓名

GET mydlq-user/_search
{
"query": {
"fuzzy": {
"name": "三"
}
}
}
(2)Java 代码示例
import club.mydlq.elasticsearch.model.entity.UserInfo;
import com.alibaba.fastjson.JSON;
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.stereotype.Service;
import java.io.IOException;

@Slf4j
@Service
public class FuzzyQueryService {

@Autowired
private RestHighLevelClient restHighLevelClient;

/**
* 模糊查询所有以 “三” 结尾的姓名
*/
public Object fuzzyQuery() {
try {
// 构建查询条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.fuzzyQuery("name", "三").fuzziness(Fuzziness.AUTO));
// 创建查询请求对象,将查询对象配置到其中
SearchRequest searchRequest = new SearchRequest("mydlq-user");
searchRequest.source(searchSourceBuilder);
// 执行查询,然后处理响应结果
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 根据状态和数据条数验证是否返回了数据
if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().totalHits > 0) {
SearchHits hits = searchResponse.getHits();
for (SearchHit hit : hits) {
// 将 JSON 转换成对象
UserInfo userInfo = JSON.parseObject(hit.getSourceAsString(), UserInfo.class);
// 输出查询信息
log.info(userInfo.toString());
}
}
} catch (IOException e) {
log.error("", e);
}
}

}

4. 范围查询(range)

(1)Restful 操作示例

查询岁数 ≥ 30 岁的员工数据:

GET /mydlq-user/_search
{
"query": {
"range": {
"age": {
"gte": 30
}
}
}
}

查询生日距离现在 30 年间的员工数据:

GET mydlq-user/_search
{
"query": {
"range": {
"birthDate": {
"gte": "now-30y"
}
}
}
}
(2)Java 代码示例
import club.mydlq.elasticsearch.model.entity.UserInfo;
import com.alibaba.fastjson.JSON;
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.stereotype.Service;
import java.io.IOException;

@Slf4j
@Service
public class RangeQueryService {

@Autowired
private RestHighLevelClient restHighLevelClient;

/**
* 查询岁数 ≥ 30 岁的员工数据
*/
public void rangeQuery() {
try {
// 构建查询条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.rangeQuery("age").gte(30));
// 创建查询请求对象,将查询对象配置到其中
SearchRequest searchRequest = new SearchRequest("mydlq-user");
searchRequest.source(searchSourceBuilder);
// 执行查询,然后处理响应结果
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 根据状态和数据条数验证是否返回了数据
if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().totalHits > 0) {
SearchHits hits = searchResponse.getHits();
for (SearchHit hit : hits) {
// 将 JSON 转换成对象
UserInfo userInfo = JSON.parseObject(hit.getSourceAsString(), UserInfo.class);
// 输出查询信息
log.info(userInfo.toString());
}
}
} catch (IOException e) {
log.error("", e);
}
}

/**
* 查询距离现在 30 年间的员工数据
* [年(y)、月(M)、星期(w)、天(d)、小时(h)、分钟(m)、秒(s)]
* 例如:
* now-1h 查询一小时内范围
* now-1d 查询一天内时间范围
* now-1y 查询最近一年内的时间范围
*/
public void dateRangeQuery() {
try {
// 构建查询条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// includeLower(是否包含下边界)、includeUpper(是否包含上边界)
searchSourceBuilder.query(QueryBuilders.rangeQuery("birthDate")
.gte("now-30y").includeLower(true).includeUpper(true));
// 创建查询请求对象,将查询对象配置到其中
SearchRequest searchRequest = new SearchRequest("mydlq-user");
searchRequest.source(searchSourceBuilder);
// 执行查询,然后处理响应结果
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 根据状态和数据条数验证是否返回了数据
if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().totalHits > 0) {
SearchHits hits = searchResponse.getHits();
for (SearchHit hit : hits) {
// 将 JSON 转换成对象
UserInfo userInfo = JSON.parseObject(hit.getSourceAsString(), UserInfo.class);
// 输出查询信息
log.info(userInfo.toString());
}
}
} catch (IOException e) {
log.error("", e);
}
}

}

5. 通配符查询(wildcard)

(1)Restful 操作示例

查询所有以 “三” 结尾的姓名:

GET mydlq-user/_search
{
"query": {
"wildcard": {
"name.keyword": {
"value": "*三"
}
}
}
}
(2)Java 代码示例
import club.mydlq.elasticsearch.model.entity.UserInfo;
import com.alibaba.fastjson.JSON;
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.stereotype.Service;
import java.io.IOException;

@Slf4j
@Service
public class WildcardQueryService {

@Autowired
private RestHighLevelClient restHighLevelClient;

/**
* 查询所有以 “三” 结尾的姓名
*
* *:表示多个字符(0个或多个字符)
* ?:表示单个字符
*/
public Object wildcardQuery() {
try {
// 构建查询条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.wildcardQuery("name.keyword", "*三"));
// 创建查询请求对象,将查询对象配置到其中
SearchRequest searchRequest = new SearchRequest("mydlq-user");
searchRequest.source(searchSourceBuilder);
// 执行查询,然后处理响应结果
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 根据状态和数据条数验证是否返回了数据
if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().totalHits > 0) {
SearchHits hits = searchResponse.getHits();
for (SearchHit hit : hits) {
// 将 JSON 转换成对象
UserInfo userInfo = JSON.parseObject(hit.getSourceAsString(), UserInfo.class);
// 输出查询信息
log.info(userInfo.toString());
}
}
} catch (IOException e) {
log.error("", e);
}
}

}

6. 布尔查询(bool)

(1)Restful 操作示例

查询出生在 1990-1995 年期间,且地址在 北京市昌平区、北京市大兴区、北京市房山区 的员工信息:

GET /mydlq-user/_search
{
"query": {
"bool": {
"filter": {
"range": {
"birthDate": {
"format": "yyyy",
"gte": 1990,
"lte": 1995
}
}
},
"must": [
{
"terms": {
"address.keyword": [
"北京市昌平区",
"北京市大兴区",
"北京市房山区"
]
}
}
]
}
}
}
(2)Java 代码示例
import club.mydlq.elasticsearch.model.entity.UserInfo;
import com.alibaba.fastjson.JSON;
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.stereotype.Service;
import java.io.IOException;

@Slf4j
@Service
public class BoolQueryService {

@Autowired
private RestHighLevelClient restHighLevelClient;

public Object boolQuery() {
try {
// 创建 Bool 查询构建器
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 构建查询条件
boolQueryBuilder.must(QueryBuilders.termsQuery("address.keyword", "北京市昌平区", "北京市大兴区", "北京市房山区"))
.filter().add(QueryBuilders.rangeQuery("birthDate").format("yyyy").gte("1990").lte("1995"));
// 构建查询源构建器
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(boolQueryBuilder);
// 创建查询请求对象,将查询对象配置到其中
SearchRequest searchRequest = new SearchRequest("mydlq-user");
searchRequest.source(searchSourceBuilder);
// 执行查询,然后处理响应结果
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 根据状态和数据条数验证是否返回了数据
if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().totalHits > 0) {
SearchHits hits = searchResponse.getHits();
for (SearchHit hit : hits) {
// 将 JSON 转换成对象
UserInfo userInfo = JSON.parseObject(hit.getSourceAsString(), UserInfo.class);
// 输出查询信息
log.info(userInfo.toString());
}
}
}catch (IOException e){
log.error("",e);
}
}

}

3.7-聚合查询操作示例

1. Metric 聚合分析

(1)Restful 操作示例

统计员工总数、工资最高值、工资最低值、工资平均工资、工资总和:

GET /mydlq-user/_search
{
"size": 0,
"aggs": {
"salary_stats": {
"stats": {
"field": "salary"
}
}
}
}

统计员工工资最低值:

GET /mydlq-user/_search
{
"size": 0,
"aggs": {
"salary_min": {
"min": {
"field": "salary"
}
}
}
}

统计员工工资最高值:

GET /mydlq-user/_search
{
"size": 0,
"aggs": {
"salary_max": {
"max": {
"field": "salary"
}
}
}
}

统计员工工资平均值:

GET /mydlq-user/_search
{
"size": 0,
"aggs": {
"salary_avg": {
"avg": {
"field": "salary"
}
}
}
}

统计员工工资总值:

GET /mydlq-user/_search
{
"size": 0,
"aggs": {
"salary_sum": {
"sum": {
"field": "salary"
}
}
}
}

统计员工总数:

GET /mydlq-user/_search
{
"size": 0,
"aggs": {
"employee_count": {
"value_count": {
"field": "salary"
}
}
}
}

统计员工工资百分位:

GET /mydlq-user/_search
{
"size": 0,
"aggs": {
"salary_percentiles": {
"percentiles": {
"field": "salary"
}
}
}
}
(2)Java 代码示例
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.avg.ParsedAvg;
import org.elasticsearch.search.aggregations.metrics.max.ParsedMax;
import org.elasticsearch.search.aggregations.metrics.min.ParsedMin;
import org.elasticsearch.search.aggregations.metrics.percentiles.ParsedPercentiles;
import org.elasticsearch.search.aggregations.metrics.percentiles.Percentile;
import org.elasticsearch.search.aggregations.metrics.stats.ParsedStats;
import org.elasticsearch.search.aggregations.metrics.sum.ParsedSum;
import org.elasticsearch.search.aggregations.metrics.sum.SumAggregationBuilder;
import org.elasticsearch.search.aggregations.metrics.valuecount.ParsedValueCount;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;

@Slf4j
@Service
public class AggrMetricService {

@Autowired
private RestHighLevelClient restHighLevelClient;

/**
* stats 统计员工总数、员工工资最高值、员工工资最低值、员工平均工资、员工工资总和
*/
public Object aggregationStats() {
String responseResult = "";
try {
// 设置聚合条件
AggregationBuilder aggr = AggregationBuilders.stats("salary_stats").field("salary");
// 查询源构建器
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.aggregation(aggr);
// 设置查询结果不返回,只返回聚合结果
searchSourceBuilder.size(0);
// 创建查询请求对象,将查询条件配置到其中
SearchRequest request = new SearchRequest("mydlq-user");
request.source(searchSourceBuilder);
// 执行请求
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
// 获取响应中的聚合信息
Aggregations aggregations = response.getAggregations();
// 输出内容
if (RestStatus.OK.equals(response.status()) || aggregations != null) {
// 转换为 Stats 对象
ParsedStats aggregation = aggregations.get("salary_stats");
log.info("-------------------------------------------");
log.info("聚合信息:");
log.info("count:{}", aggregation.getCount());
log.info("avg:{}", aggregation.getAvg());
log.info("max:{}", aggregation.getMax());
log.info("min:{}", aggregation.getMin());
log.info("sum:{}", aggregation.getSum());
log.info("-------------------------------------------");
}
// 根据具体业务逻辑返回不同结果,这里为了方便直接将返回响应对象Json串
responseResult = response.toString();
} catch (IOException e) {
log.error("", e);
}
return responseResult;
}

/**
* min 统计员工工资最低值
*/
public Object aggregationMin() {
String responseResult = "";
try {
// 设置聚合条件
AggregationBuilder aggr = AggregationBuilders.min("salary_min").field("salary");
// 查询源构建器
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.aggregation(aggr);
searchSourceBuilder.size(0);
// 创建查询请求对象,将查询条件配置到其中
SearchRequest request = new SearchRequest("mydlq-user");
request.source(searchSourceBuilder);
// 执行请求
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
// 获取响应中的聚合信息
Aggregations aggregations = response.getAggregations();
// 输出内容
if (RestStatus.OK.equals(response.status()) || aggregations != null) {
// 转换为 Min 对象
ParsedMin aggregation = aggregations.get("salary_min");
log.info("-------------------------------------------");
log.info("聚合信息:");
log.info("min:{}", aggregation.getValue());
log.info("-------------------------------------------");
}
// 根据具体业务逻辑返回不同结果,这里为了方便直接将返回响应对象Json串
responseResult = response.toString();
} catch (IOException e) {
log.error("", e);
}
return responseResult;
}

/**
* max 统计员工工资最高值
*/
public Object aggregationMax() {
String responseResult = "";
try {
// 设置聚合条件
AggregationBuilder aggr = AggregationBuilders.max("salary_max").field("salary");
// 查询源构建器
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.aggregation(aggr);
searchSourceBuilder.size(0);
// 创建查询请求对象,将查询条件配置到其中
SearchRequest request = new SearchRequest("mydlq-user");
request.source(searchSourceBuilder);
// 执行请求
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
// 获取响应中的聚合信息
Aggregations aggregations = response.getAggregations();
// 输出内容
if (RestStatus.OK.equals(response.status()) || aggregations != null) {
// 转换为 Max 对象
ParsedMax aggregation = aggregations.get("salary_max");
log.info("-------------------------------------------");
log.info("聚合信息:");
log.info("max:{}", aggregation.getValue());
log.info("-------------------------------------------");
}
// 根据具体业务逻辑返回不同结果,这里为了方便直接将返回响应对象Json串
responseResult = response.toString();
} catch (IOException e) {
log.error("", e);
}
return responseResult;
}

/**
* avg 统计员工工资平均值
*/
public Object aggregationAvg() {
String responseResult = "";
try {
// 设置聚合条件
AggregationBuilder aggr = AggregationBuilders.avg("salary_avg").field("salary");
// 查询源构建器
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.aggregation(aggr);
searchSourceBuilder.size(0);
// 创建查询请求对象,将查询条件配置到其中
SearchRequest request = new SearchRequest("mydlq-user");
request.source(searchSourceBuilder);
// 执行请求
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
// 获取响应中的聚合信息
Aggregations aggregations = response.getAggregations();
// 输出内容
if (RestStatus.OK.equals(response.status()) || aggregations != null) {
// 转换为 Avg 对象
ParsedAvg aggregation = aggregations.get("salary_avg");
log.info("-------------------------------------------");
log.info("聚合信息:");
log.info("avg:{}", aggregation.getValue());
log.info("-------------------------------------------");
}
// 根据具体业务逻辑返回不同结果,这里为了方便直接将返回响应对象Json串
responseResult = response.toString();
} catch (IOException e) {
log.error("", e);
}
return responseResult;
}

/**
* sum 统计员工工资总值
*/
public Object aggregationSum() {
String responseResult = "";
try {
// 设置聚合条件
SumAggregationBuilder aggr = AggregationBuilders.sum("salary_sum").field("salary");
// 查询源构建器
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.aggregation(aggr);
searchSourceBuilder.size(0);
// 创建查询请求对象,将查询条件配置到其中
SearchRequest request = new SearchRequest("mydlq-user");
request.source(searchSourceBuilder);
// 执行请求
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
// 获取响应中的聚合信息
Aggregations aggregations = response.getAggregations();
// 输出内容
if (RestStatus.OK.equals(response.status()) || aggregations != null) {
// 转换为 Sum 对象
ParsedSum aggregation = aggregations.get("salary_sum");
log.info("-------------------------------------------");
log.info("聚合信息:");
log.info("sum:{}", String.valueOf((aggregation.getValue())));
log.info("-------------------------------------------");
}
// 根据具体业务逻辑返回不同结果,这里为了方便直接将返回响应对象Json串
responseResult = response.toString();
} catch (IOException e) {
log.error("", e);
}
return responseResult;
}

/**
* count 统计员工总数
*/
public Object aggregationCount() {
String responseResult = "";
try {
// 设置聚合条件
AggregationBuilder aggr = AggregationBuilders.count("employee_count").field("salary");
// 查询源构建器
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.aggregation(aggr);
searchSourceBuilder.size(0);
// 创建查询请求对象,将查询条件配置到其中
SearchRequest request = new SearchRequest("mydlq-user");
request.source(searchSourceBuilder);
// 执行请求
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
// 获取响应中的聚合信息
Aggregations aggregations = response.getAggregations();
// 输出内容
if (RestStatus.OK.equals(response.status()) || aggregations != null) {
// 转换为 ValueCount 对象
ParsedValueCount aggregation = aggregations.get("employee_count");
log.info("-------------------------------------------");
log.info("聚合信息:");
log.info("count:{}", aggregation.getValue());
log.info("-------------------------------------------");
}
// 根据具体业务逻辑返回不同结果,这里为了方便直接将返回响应对象Json串
responseResult = response.toString();
} catch (IOException e) {
log.error("", e);
}
return responseResult;
}

/**
* percentiles 统计员工工资百分位
*/
public Object aggregationPercentiles() {
String responseResult = "";
try {
// 设置聚合条件
AggregationBuilder aggr = AggregationBuilders.percentiles("salary_percentiles").field("salary");
// 查询源构建器
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.aggregation(aggr);
searchSourceBuilder.size(0);
// 创建查询请求对象,将查询条件配置到其中
SearchRequest request = new SearchRequest("mydlq-user");
request.source(searchSourceBuilder);
// 执行请求
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
// 获取响应中的聚合信息
Aggregations aggregations = response.getAggregations();
// 输出内容
if (RestStatus.OK.equals(response.status()) || aggregations != null) {
// 转换为 Percentiles 对象
ParsedPercentiles aggregation = aggregations.get("salary_percentiles");
log.info("-------------------------------------------");
log.info("聚合信息:");
for (Percentile percentile : aggregation) {
log.info("百分位:{}:{}", percentile.getPercent(), percentile.getValue());
}
log.info("-------------------------------------------");
}
// 根据具体业务逻辑返回不同结果,这里为了方便直接将返回响应对象Json串
responseResult = response.toString();
} catch (IOException e) {
log.error("", e);
}
return responseResult;
}

}

2. Bucket 聚合分析

(1)Restful 操作示例

按岁数进行聚合分桶,统计各个岁数员工的人数:

GET mydlq-user/_search
{
"size": 0,
"aggs": {
"age_bucket": {
"terms": {
"field": "age",
"size": "10"
}
}
}
}

按工资范围进行聚合分桶,统计工资在 3000-5000、5000-9000 和 9000 以上的员工信息:

GET mydlq-user/_search
{
"aggs": {
"salary_range_bucket": {
"range": {
"field": "salary",
"ranges": [
{
"key": "低级员工",
"to": 3000
},{
"key": "中级员工",
"from": 5000,
"to": 9000
},{
"key": "高级员工",
"from": 9000
}
]
}
}
}
}

按照时间范围进行分桶,统计 1985-1990 年和 1990-1995 年出生的员工信息:

GET mydlq-user/_search
{
"size": 10,
"aggs": {
"date_range_bucket": {
"date_range": {
"field": "birthDate",
"format": "yyyy",
"ranges": [
{
"key": "出生日期1985-1990的员工",
"from": "1985",
"to": "1990"
},{
"key": "出生日期1990-1995的员工",
"from": "1990",
"to": "1995"
}
]
}
}
}
}

按工资多少进行聚合分桶,设置统计的最小值为 0,最大值为 12000,区段间隔为 3000:

GET mydlq-user/_search
{
"size": 0,
"aggs": {
"salary_histogram": {
"histogram": {
"field": "salary",
"extended_bounds": {
"min": 0,
"max": 12000
},
"interval": 3000
}
}
}
}

按出生日期进行分桶:

GET mydlq-user/_search
{
"size": 0,
"aggs": {
"birthday_histogram": {
"date_histogram": {
"format": "yyyy",
"field": "birthDate",
"interval": "year"
}
}
}
}
(2)Java 代码示例
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.stereotype.Service;
import java.io.IOException;
import java.util.List;

@Slf4j
@Service
public class AggrBucketService {

@Autowired
private RestHighLevelClient restHighLevelClient;

/**
* 按岁数进行聚合分桶
*/
public Object aggrBucketTerms() {
try {
AggregationBuilder aggr = AggregationBuilders.terms("age_bucket").field("age");
// 查询源构建器
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.size(10);
searchSourceBuilder.aggregation(aggr);
// 创建查询请求对象,将查询条件配置到其中
SearchRequest request = new SearchRequest("mydlq-user");
request.source(searchSourceBuilder);
// 执行请求
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
// 获取响应中的聚合信息
Aggregations aggregations = response.getAggregations();
// 输出内容
if (RestStatus.OK.equals(response.status())) {
// 分桶
Terms byCompanyAggregation = aggregations.get("age_bucket");
List<? extends Terms.Bucket> buckets = byCompanyAggregation.getBuckets();
// 输出各个桶的内容
log.info("-------------------------------------------");
log.info("聚合信息:");
for (Terms.Bucket bucket : buckets) {
log.info("桶名:{} | 总数:{}", bucket.getKeyAsString(), bucket.getDocCount());
}
log.info("-------------------------------------------");
}
} catch (IOException e) {
log.error("", e);
}
}

/**
* 按工资范围进行聚合分桶
*/
public Object aggrBucketRange() {
try {
AggregationBuilder aggr = AggregationBuilders.range("salary_range_bucket")
.field("salary")
.addUnboundedTo("低级员工", 3000)
.addRange("中级员工", 5000, 9000)
.addUnboundedFrom("高级员工", 9000);
// 查询源构建器
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.size(0);
searchSourceBuilder.aggregation(aggr);
// 创建查询请求对象,将查询条件配置到其中
SearchRequest request = new SearchRequest("mydlq-user");
request.source(searchSourceBuilder);
// 执行请求
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
// 获取响应中的聚合信息
Aggregations aggregations = response.getAggregations();
// 输出内容
if (RestStatus.OK.equals(response.status())) {
// 分桶
Range byCompanyAggregation = aggregations.get("salary_range_bucket");
List<? extends Range.Bucket> buckets = byCompanyAggregation.getBuckets();
// 输出各个桶的内容
log.info("-------------------------------------------");
log.info("聚合信息:");
for (Range.Bucket bucket : buckets) {
log.info("桶名:{} | 总数:{}", bucket.getKeyAsString(), bucket.getDocCount());
}
log.info("-------------------------------------------");
}
} catch (IOException e) {
log.error("", e);
}
}

/**
* 按照时间范围进行分桶
*/
public Object aggrBucketDateRange() {
try {
AggregationBuilder aggr = AggregationBuilders.dateRange("date_range_bucket")
.field("birthDate")
.format("yyyy")
.addRange("1985-1990", "1985", "1990")
.addRange("1990-1995", "1990", "1995");
// 查询源构建器
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.size(0);
searchSourceBuilder.aggregation(aggr);
// 创建查询请求对象,将查询条件配置到其中
SearchRequest request = new SearchRequest("mydlq-user");
request.source(searchSourceBuilder);
// 执行请求
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
// 获取响应中的聚合信息
Aggregations aggregations = response.getAggregations();
// 输出内容
if (RestStatus.OK.equals(response.status())) {
// 分桶
Range byCompanyAggregation = aggregations.get("date_range_bucket");
List<? extends Range.Bucket> buckets = byCompanyAggregation.getBuckets();
// 输出各个桶的内容
log.info("-------------------------------------------");
log.info("聚合信息:");
for (Range.Bucket bucket : buckets) {
log.info("桶名:{} | 总数:{}", bucket.getKeyAsString(), bucket.getDocCount());
}
log.info("-------------------------------------------");
}
} catch (IOException e) {
log.error("", e);
}
}

/**
* 按工资多少进行聚合分桶
*/
public Object aggrBucketHistogram() {
try {
AggregationBuilder aggr = AggregationBuilders.histogram("salary_histogram")
.field("salary")
.extendedBounds(0, 12000)
.interval(3000);
// 查询源构建器
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.size(0);
searchSourceBuilder.aggregation(aggr);
// 创建查询请求对象,将查询条件配置到其中
SearchRequest request = new SearchRequest("mydlq-user");
request.source(searchSourceBuilder);
// 执行请求
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
// 获取响应中的聚合信息
Aggregations aggregations = response.getAggregations();
// 输出内容
if (RestStatus.OK.equals(response.status())) {
// 分桶
Histogram byCompanyAggregation = aggregations.get("salary_histogram");
List<? extends Histogram.Bucket> buckets = byCompanyAggregation.getBuckets();
// 输出各个桶的内容
log.info("-------------------------------------------");
log.info("聚合信息:");
for (Histogram.Bucket bucket : buckets) {
log.info("桶名:{} | 总数:{}", bucket.getKeyAsString(), bucket.getDocCount());
}
log.info("-------------------------------------------");
}
} catch (IOException e) {
log.error("", e);
}
}

/**
* 按出生日期进行分桶
*/
public Object aggrBucketDateHistogram() {
try {
AggregationBuilder aggr = AggregationBuilders.dateHistogram("birthday_histogram")
.field("birthDate")
.interval(1)
.dateHistogramInterval(DateHistogramInterval.YEAR)
.format("yyyy");
// 查询源构建器
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.size(0);
searchSourceBuilder.aggregation(aggr);
// 创建查询请求对象,将查询条件配置到其中
SearchRequest request = new SearchRequest("mydlq-user");
request.source(searchSourceBuilder);
// 执行请求
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
// 获取响应中的聚合信息
Aggregations aggregations = response.getAggregations();
// 输出内容
if (RestStatus.OK.equals(response.status())) {
// 分桶
Histogram byCompanyAggregation = aggregations.get("birthday_histogram");

List<? extends Histogram.Bucket> buckets = byCompanyAggregation.getBuckets();
// 输出各个桶的内容
log.info("-------------------------------------------");
log.info("聚合信息:");
for (Histogram.Bucket bucket : buckets) {
log.info("桶名:{} | 总数:{}", bucket.getKeyAsString(), bucket.getDocCount());
}
log.info("-------------------------------------------");
}
} catch (IOException e) {
log.error("", e);
}
}

}

3. Metric 与 Bucket 聚合分析

(1)Restful 操作示例

按照员工岁数分桶、然后统计每个岁数员工工资最高值:

GET mydlq-user/_search
{
"size": 0,
"aggs": {
"salary_bucket": {
"terms": {
"field": "age",
"size": "10"
},
"aggs": {
"salary_max_user": {
"top_hits": {
"size": 1,
"sort": [
{
"salary": {
"order": "desc"
}
}
]
}
}
}
}
}
}
(2)Java 代码示例
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.tophits.ParsedTopHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.List;

@Slf4j
@Service
public class AggrBucketMetricService {

@Autowired
private RestHighLevelClient restHighLevelClient;

/**
* topHits 按岁数分桶、然后统计每个员工工资最高值
*/
public Object aggregationTopHits() {
try {
AggregationBuilder testTop = AggregationBuilders.topHits("salary_max_user")
.size(1)
.sort("salary", SortOrder.DESC);
AggregationBuilder salaryBucket = AggregationBuilders.terms("salary_bucket")
.field("age")
.size(10);
salaryBucket.subAggregation(testTop);
// 查询源构建器
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.size(0);
searchSourceBuilder.aggregation(salaryBucket);
// 创建查询请求对象,将查询条件配置到其中
SearchRequest request = new SearchRequest("mydlq-user");
request.source(searchSourceBuilder);
// 执行请求
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
// 获取响应中的聚合信息
Aggregations aggregations = response.getAggregations();
// 输出内容
if (RestStatus.OK.equals(response.status())) {
// 分桶
Terms byCompanyAggregation = aggregations.get("salary_bucket");
List<? extends Terms.Bucket> buckets = byCompanyAggregation.getBuckets();
// 输出各个桶的内容
log.info("-------------------------------------------");
log.info("聚合信息:");
for (Terms.Bucket bucket : buckets) {
log.info("桶名:{}", bucket.getKeyAsString());
ParsedTopHits topHits = bucket.getAggregations().get("salary_max_user");
for (SearchHit hit:topHits.getHits()){
log.info(hit.getSourceAsString());
}
}
log.info("-------------------------------------------");
}
} catch (IOException e) {
log.error("", e);
}
}

}

参考:

——https://segmentfault.com/a/1190000020205776

——https://www.jianshu.com/p/60b242cbd8b4

——https://segmentfault.com/a/1190000020022504

——https://mp.weixin.qq.com/s/Lz5Gq1Lwkn8GgMUozqUVjQ