简单的elasticsearch介绍学习记录

简单的elasticsearch介绍学习记录

1. elasticsearch介绍

Elasticsearch是一个分布式的全文检索引擎,它是对lucene的功能做了封装,能够达到实时搜索,稳定,可靠,快速等特点。如果大家对Lucene有所了解的话,那么针对Elasticsearch其实就好理解了。

针对海量数据计算分析,前面我们学习了MapReduceHiveSparkFlink这些计算引擎和分析工具,但是它们侧重的都是对数据的清洗、聚合之类的需求。
如果想要在海量数据里面快速查询出一批满足条件的数据,这些计算引擎都需要生成一个任务,提交到集群中去执行,这样中间消耗的时间就长了。

并且针对多条件组合查询需求,这些计算引擎在查询的时候基本上都要实现全表扫描了,这样查询效率也是比较低的。

所以,为了解决海量数据下的快速检索,以及多条件组合查询需求,Elasticsearch就应运而生了。

1.1 常见的全文检索引擎

  • Lucene:Lucene是Java家族中最为出名的一个开源搜索引擎,在Java世界中属于标准的全文检索程序,它提供了完整的查询引擎和索引引擎。
    但是它也存在一些缺点
    1:不支持分布式,无法扩展,海量数据下会存在瓶颈。
    2:提供的都是低级API,使用繁琐。
    3:没有提供web界面,不便于管理。

  • Solr:Solr是一个用java开发的独立的企业级搜索应用服务器,它是基于Lucene的。
    它解决了Lucene的一些痛点,提供了web界面,以及高级API接口。
    并且从Solr4.0版本开始,Solr开始支持分布式,称之为Solrcloud。

  • Elasticsearch:Elasticsearch 是一个采用Java语言开发的,基于Lucene的开源、分布式的搜索引擎,能够实现实时搜索。
    它最重要的一个特点是天生支持分布式,可以这样说,Elasticsearch就是为了分布式而生的。
    它对外提供REST API接口,便于使用,通过外部插件实现web界面支持,便于管理集群。

    Elasticsearch一般我们会简称为ES。从这里可以看出来,Solr和ES的功能基本是类似的,那在工作中该如何选择呢?

  • Solr vs Elasticsearch

    在这里插入图片描述

  • Solr从2007年就出现了,在传统企业中应用的还是比较广泛的,并且在2013年的时候,Solr推出了4.0版本,提供了Solrcloud,开始正式支持分布式集群。
    ES在2014年的时候才正式推出1.0版本,所以它的出现要比Solr晚很多年。
    但是ES从一开始就是为了解决海量数据下的全文检索,所以在分布式集群相关特性层面,ES会优于Solrcloud。建议如下:

    1. 如果之前公司里面已经深度使用了Solr,现在为了解决海量数据检索问题,建议优先考虑使用Solrcloud。
    2. 如果之前没有使用过Solr,那么在海量数据的场景下,建议优先考虑使用ES。
  • MySQL VS Elasticsearch:为了便于理解ES,在这里我们拿MySQL和ES做一个对比分析:
    在这里插入图片描述

图片内容解释:

  • 1: MySQL中有Database(数据库)的概念,对应的在ES中有Index(索引库)的概念。

  • 2:MySQL中有Table(表)的概念,对应的在ES中有Type(类型)的概念,不过需要注意,ES在1.x~5.x版本中是正常支持Type的,每一个Index下面可以有多个Type。

    从6.0版本开始,每一个Index中只支持1个Type,属于过渡阶段。
    从7.0版本开始,取消了Type,也就意味着每一个Index中存储的数据类型可以认为都是同一种,不再区分类型了。

    为何要取消Type?主要还是基于性能方面的考虑。因为ES设计初期,是直接参考了关系型数据库的设计模型,存在了 Type(表)的概念。但是,ES的搜索引擎是基于 Lucene 的,这种基因决定了 Type 是多余的。

    在关系型数据库中Table是独立的,但是在ES中同一个Index中不同Type的数据在底层是存储在同一个Lucene的索引文件中的。

    如果在同一个Index中的不同Type中都有一个id字段,那么ES会认为这两个id字段是同一个字段,你必须在不同的Type中给这个id字段定义相同的字段类型,否则,不同Type中的相同字段名称就会在处理的时候出现冲突,导致Lucene处理效率下降。

    除此之外,在同一个Index的不同Type下,存储字段个数不一样的数据,会导致存储中出现稀疏数据,影响Lucene压缩文档的能力,最终导致ES查询效率降低。

  • 3:MySQL中有Row(行)的概念,表示一条数据,在ES中对应的有Document(文档)。

  • 4:MySQL中有Column(列)的概念,表示一条数据中的某个列,在ES中对应的有Field(字段)。

1.2 Elasticsearch核心概念

在这里插入图片描述

ES中几个比较核心的概念:Cluster:集群,Shard:分片,Replica:副本,Recovery:数据恢复,接下来具体分析一下这几个概念:

  • Cluster
    代表ES集群,集群中有多个节点,其中有一个为主节点,这个主节点是通过选举产生的。

    主从节点是对于集群内部来说的,ES的一个核心特性就是去中心化,字面上理解就是无中心节点,这是对于集群外部来说的,因为从外部来看ES集群,在逻辑上是一个整体,我们与任何一个节点的通信和与整个ES集群通信是等价的。

    主节点的职责是负责管理集群状态,包括管理分片的状态和副本的状态,以及节点的发现和删除。

  • Shard
    代表索引库分片,ES集群可以把一个索引库分成多个分片。

    这样的好处是可以把一个大的索引库水平拆分成多个分片,分布到不同的节点上,构成分布式搜索,进而提高性能和吞吐量。

    注意:分片的数量只能在创建索引库的时候指定,索引库创建后不能更改。

    默认情况下一个索引库有1个分片。

    每个分片中最多存储2,147,483,519条数据,其实就是Integer.MAX_VALUE-128。
    因为每一个ES的分片底层对应的都是Lucene索引文件,单个Lucene索引文件最多存储Integer.MAX_VALUE-128个文档(数据)。
    注意:在ES7.0版本之前,每一个索引库默认是有5个分片的。

  • Replica
    代表分片的副本,ES集群可以给分片设置副本。

    副本的第一个作用是提高系统的容错性,当某个分片损坏或丢失时可以从副本中恢复。第二个作用是提高ES的查询效率,ES会自动对搜索请求进行负载均衡。

    注意:分片的副本数量可以随时修改。
    默认情况下,每一个索引库只有1个主分片和1个副本分片(前提是ES集群有2个及以上节点,如果ES集群只有1个节点,那么索引库就只有1个主分片,不会产生副本分片,因为主分片和副本分片在一个节点里面是没有意义的)。
    为了保证数据安全,以及提高查询效率,建议副本数量设置为2或者3。

  • Recovery
    代表数据恢复或者数据重新分布。

    ES集群在有节点加入或退出时会根据机器的负载对分片进行重新分配,挂掉的节点重新启动时也会进行数据恢复。

1.3 ES安装部署

ES支持单机和集群,在使用层面是完全一样的。首先下载ES的安装包,目前ES最新版本是7.x,在这使用7.13.4版本。
下载地址:https://www.elastic.co/cn/downloads/past-releases#elasticsearch 选择ES的对应版本。
在这里插入图片描述
在这里插入图片描述
ES 7.13.4版本的安装包下载地址为:https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.13.4-linux-x86_64.tar.gz,目前ES中自带的有open JDK,不用单独安装部署Oracle JDK。
在这里插入图片描述
在具体安装集群之前,先来分析一下ES中的核心配置文件:在ES_HOME的config目录下有一个elasticsearch.yml配置文件,这个文件是一个yaml格式的文件。elasticsearch.yml文件内容如下:

# ======================== Elasticsearch Configuration =========================
#
# NOTE: Elasticsearch comes with reasonable defaults for most settings.
#       Before you set out to tweak and tune the configuration, make sure you
#       understand what are you trying to accomplish and the consequences.
#
# The primary way of configuring a node is via this file. This template lists
# the most important settings you may want to configure for a production cluster.
#
# Please consult the documentation for further information on configuration options:
# https://www.elastic.co/guide/en/elasticsearch/reference/index.html
#
# ---------------------------------- Cluster -----------------------------------
#
# Use a descriptive name for your cluster:
# 集群名称,默认是elasticsearch,如果想要将多个ES实例组成一个集群,那么它们的cluster.name必须一致
#cluster.name: my-application
#
# ------------------------------------ Node ------------------------------------
#
# Use a descriptive name for the node:
# 节点名称,可以手工指定,默认也会自动生成
#node.name: node-1
#
# Add custom attributes to the node:
# 给节点指定一些自定义的参数信息
#node.attr.rack: r1
#
# ----------------------------------- Paths ------------------------------------
#
# Path to directory where to store the data (separate multiple locations by comma):
# 可以指定ES的数据存储目录,默认存储在ES_HOME/data目录下
#path.data: /path/to/data
#
# Path to log files:
# 可以指定ES的日志存储目录,默认存储在ES_HOME/logs目录下
#path.logs: /path/to/logs
#
# ----------------------------------- Memory -----------------------------------
#
# Lock the memory on startup:
# 锁定物理内存地址,防止ES内存被交换出去,也就是避免ES使用swap交换分区中的内存
#bootstrap.memory_lock: true
# 确保ES_HEAP_SIZE参数设置为系统可用内存的一半左右
# Make sure that the heap size is set to about half the memory available
# on the system and that the owner of the process is allowed to use this
# limit.
# 当系统进行内存交换的时候,会导致ES的性能变的很差
# Elasticsearch performs poorly when the system is swapping the memory.
#
# ---------------------------------- Network -----------------------------------
#
# By default Elasticsearch is only accessible on localhost. Set a different
# address here to expose this node on the network:
# 为ES设置绑定的IP,默认是127.0.0.1,也就是默认只能通过127.0.0.1 或者localhost才能访问
# ES 1.x版本默认绑定的是0.0.0.0,但是从ES 2.x版本之后默认绑定的是127.0.0.1
#network.host: 192.168.0.1
#
# By default Elasticsearch listens for HTTP traffic on the first free port it
# finds starting at 9200. Set a specific HTTP port here:
# 为ES服务设置监听的端口,默认是9200
# 如果想要在一台机器上启动多个ES实例,需要修改此处的端口号
#http.port: 9200
#
# For more information, consult the network module documentation.
#
# --------------------------------- Discovery ----------------------------------
# 
# Pass an initial list of hosts to perform discovery when this node is started:
# The default list of hosts is ["127.0.0.1", "[::1]"]
# 
# 当启动新节点时,通过这个ip列表进行节点发现,组建集群
# 默认ip列表:
# 	127.0.0.1,表示ipv4的本地回环地址。
#	[::1],表示ipv6的本地回环地址。
# 在ES 1.x中默认使用的是组播(multicast)协议,默认会自动发现同一网段的ES节点组建集群。
# 从ES 2.x开始默认使用的是单播(unicast)协议,想要组建集群的话就需要在这指定要发现的节点信息了。
# 
# 指定想要组装成一个ES集群的多个节点信息
#discovery.seed_hosts: ["host1", "host2"]
#
# Bootstrap the cluster using an initial set of master-eligible nodes:
# 初始化一批具备成为主节点资格的节点【在选择主节点的时候会优先在这一批列表中进行选择】
#cluster.initial_master_nodes: ["node-1", "node-2"]
#
# For more information, consult the discovery and cluster formation module documentation.
#
# ---------------------------------- Various -----------------------------------
#
# Require explicit names when deleting indices:
# 禁止使用通配符或_all删除索引, 必须使用名称或别名才能删除该索引。
#action.destructive_requires_name: true
1.3.1 ES单机
  1. 将ES的安装包上传到bigdata01的/usr/soft目录下

    [root@bigdata01 soft]# ll elasticsearch-7.13.4-linux-x86_64.tar.gz 
    -rw-r--r--. 1 root root 327143992 Sep  2  2021 elasticsearch-7.13.4-linux-x86_64.tar.gz 
    
  2. 在Linux中添加一个普通用户:es。因为ES目前不支持root用户启动。

    [root@bigdata01 soft]# useradd -d /home/es -m es
    [root@bigdata01 soft]# passwd es
    Changing password for user es.
    New password: bigdata1234
    Retype new password: bigdata1234
    passwd: all authentication tokens updated successfully.
    
  3. 修改Linux中最大文件描述符以及最大虚拟内存的参数,因为ES对Linux的最大文件描述符以及最大虚拟内存有一定要求,所以需要修改,否则ES无法正常启动。

    [root@bigdata01 soft]# vi /etc/security/limits.conf 
    * soft nofile 65536
    * hard nofile 131072
    * soft nproc 2048
    * hard nproc 4096
    [root@bigdata01 soft]# vi /etc/sysctl.conf
    vm.max_map_count=262144
    
  4. 重启Linux系统。前面修改的参数需要重启系统才会生效。

    [root@bigdata01 soft]# reboot -h now
    
  5. 解压ES安装包。

    [root@bigdata01 soft]# tar -zxvf elasticsearch-7.13.4-linux-x86_64.tar.gz
    
  6. 配置ES_JAVA_HOME环境变量,指向ES中内置的JDK。

    [root@bigdata01 soft]# vi /etc/profile
    ......
    export ES_JAVA_HOME=/usr/soft/elasticsearch-7.13.4/jdk
    ......
    [root@bigdata01 soft]# source /etc/profile
    
  7. 修改elasticsearch-7.13.4目录的权限
    因为前面是使用root用户解压的,elasticsearch-7.13.4目录下的文件es用户是没有权限的。

    [root@bigdata01 soft]# chmod 777 -R /usr/soft/elasticsearch-7.13.4
    
  8. 切换到es用户

    [root@bigdata01 soft]# su es
    
  9. 修改elasticsearch.yml配置文件内容,主要修改network.hostdiscovery.seed_hosts这两个参数。

    yaml文件的格式,参数和值之间需要有一个空格。

    例如:network.host: bigdata01
    bigdata01前面必须要有一个空格,否则会报错。

    [es@bigdata01 soft]$ cd elasticsearch-7.13.4
    [es@bigdata01 elasticsearch-7.13.4]$ vi config/elasticsearch.yml 
    ......
    network.host: bigdata01
    discovery.seed_hosts: ["bigdata01"]
    ......
    
  10. 启动ES服务【前台启动】

    [es@bigdata01 elasticsearch-7.13.4]$ bin/elasticsearch
    

    按ctrl+c停止服务。

  11. 启动ES服务【后台启动】。在实际工作中需要将ES放在后台运行。

    [es@bigdata01 elasticsearch-7.13.4]$ bin/elasticsearch -d
    
  12. 验证ES服务。通过jps命令验证进程是否存在。

    [es@bigdata01 elasticsearch-7.13.4]$ jps
    1849 Elasticsearch
    

    通过web界面验证服务是否可以正常访问,端口为9200http://bigdata01:9200/
    在这里插入图片描述

1.3.2 ES集群

ES集群规划:bigdata01,bigdata02,bigdata03。具体操作步骤如下

  1. bigdata01、bigdata02、bigdata03中创建普通用户:es。具体创建步骤参考ES单机中的操作。

    [root@bigdata01 soft]# useradd -d /home/es -m es
    [root@bigdata01 soft]# passwd es
    Changing password for user es.
    New password: bigdata1234
    Retype new password: bigdata1234
    passwd: all authentication tokens updated successfully.
    
  2. bigdata01、bigdata02、bigdata03中修改Linux中最大文件描述符以及最大虚拟内存的参数。具体修改步骤参考ES单机中的操作。

    [root@bigdata01 soft]# vi /etc/security/limits.conf 
    * soft nofile 65536
    * hard nofile 131072
    * soft nproc 2048
    * hard nproc 4096
    [root@bigdata01 soft]# vi /etc/sysctl.conf
    vm.max_map_count=262144
    
  3. 重启bigdata01、bigdata02、bigdata03,让前面修改的参数生效。具体操作步骤参考ES单机中的操作。

    [root@bigdata01 soft]# reboot -h now
    
  4. bigdata01、bigdata02、bigdata03中配置ES_JAVA_HOME环境变量,指向ES中内置的JDK。具体配置步骤参考ES单机中的操作。

    [root@bigdata01 soft]# vi /etc/profile
    ......
    export ES_JAVA_HOME=/usr/soft/elasticsearch-7.13.4/jdk
    ......
    [root@bigdata01 soft]# source /etc/profile
    
  5. 在bigdata01中重新解压ES的安装包以及修改目录权限

    [root@bigdata01 soft]# tar -zxvf elasticsearch-7.13.4-linux-x86_64.tar.gz
    [root@bigdata01 soft]# chmod 777 -R /usr/soft/elasticsearch-7.13.4
    
  6. 修改elasticsearch.yml配置文件,主要修改network.hostdiscovery.seed_hostscluster.initial_master_nodes这三个参数。

    [root@bigdata01 soft]$ cd elasticsearch-7.13.4
    [root@bigdata01 elasticsearch-7.13.4]$ vi config/elasticsearch.yml 
    ......
    network.host: bigdata01
    discovery.seed_hosts: ["bigdata01","bigdata02","bigdata03"]
    cluster.initial_master_nodes: ["bigdata01", "bigdata02"]
    ......
    
  7. 将bigdata01中修改好配置的elasticsearch-7.13.4目录远程拷贝到bigdata02bigdata03

    [root@bigdata01 soft]# scp -rq elasticsearch-7.13.4 bigdata02:/usr/soft/
    [root@bigdata01 soft]# scp -rq elasticsearch-7.13.4 bigdata03:/usr/soft/
    
  8. 分别修改bigdata02bigdata03中ES的elasticsearch.yml配置文件。
    修改bigdata02中的elasticsearch.yml配置文件,主要修改network.host参数的值为当前节点主机名。

    [root@bigdata02 elasticsearch-7.13.4]# vi config/elasticsearch.yml 
    ......
    network.host: bigdata02
    ......
    

    修改bigdata03中的elasticsearch.yml配置文件,主要修改network.host参数的值为当前节点主机名。

    [root@bigdata03 elasticsearch-7.13.4]# vi config/elasticsearch.yml 
    ......
    network.host: bigdata03
    ......
    
  9. bigdata01、bigdata02、bigdata03中分别启动ES。在bigdata01上启动。

    [root@bigdata01 elasticsearch-7.13.4]# su es
    [es@bigdata01 elasticsearch-7.13.4]$ bin/elasticsearch -d
    

    在bigdata02上启动。

    [root@bigdata02 elasticsearch-7.13.4]# su es
    [es@bigdata02 elasticsearch-7.13.4]$ bin/elasticsearch -d
    

    在bigdata03上启动。

    [root@bigdata03 elasticsearch-7.13.4]# su es
    [es@bigdata03 elasticsearch-7.13.4]$ bin/elasticsearch -d
    
  10. 验证集群中的进程。分别在bigdata01、bigdata02、bigdata03中验证进程是否存在。

    [es@bigdata01 elasticsearch-7.13.4]$ jps
    3080 Elasticsearch
    代码块12
    [es@bigdata02 elasticsearch-7.13.4]$ jps
    1911 Elasticsearch
    代码块12
    [es@bigdata03 elasticsearch-7.13.4]$ jps
    1879 Elasticsearch
    
  11. 验证这几个节点是否组成一个集群。通过ES的REST API可以很方便的查看集群中的节点信息。http://bigdata01:9200/_nodes/_all?pretty
    在这里插入图片描述

1.4 ES管理工具-cerebro

为了便于我们管理监控ES集群,推荐使用cerebro这个工具。

  1. 首先到github上下载cerebro的安装包。https://github.com/lmenezes/cerebro/releases
    在这里插入图片描述

  2. 将下载好的cerebro-0.9.4.zip安装包上传到bigdata01的/usr/soft目录中并且解压。

    注意:cerebro部署在任意节点上都可以,只要能和ES集群通信即可。

    [root@bigdata01 soft]# ll cerebro-0.9.4.zip 
    -rw-r--r--. 1 root root 57251010 Sep 11  2021 cerebro-0.9.4.zip
    [root@bigdata01 soft]# unzip cerebro-0.9.4.zip
    
  3. 启动cerebro。将cerebro放在后台启动。

    [root@bigdata01 cerebro-0.9.4]# nohup bin/cerebro 2>&1 >/dev/null &
    

    默认cerebro监听的端口是9000,如果出现端口冲突,需要修改cerebro监控的端口,在启动cerebro的时候可以通过http.port指定端口号,如下命令: bin/cerebro -Dhttp.port=1234

    默认通过9000端口可以访问cerebro的web界面。
    在这里插入图片描述

  4. 使用cerebro。在Node address中输入ES集群任意一个节点的连接信息即可。
    在这里插入图片描述

集群有三种状态,greenyellowred

  • green:表示集群处于健康状态,可以正常使用。
  • yellow:表示集群处于风险状态,可以正常使用,可能是分片的副本个数不完整。例如:分片的副本数为2,但是现在分片的副本只有1份。
  • red:表示集群处于故障状态,无法正常使用,可能是集群分片不完整。

cerebro的所有功能。
在这里插入图片描述

1.5 ES的基本操作

针对ES的操作,官方提供了很多种操作方式。https://www.elastic.co/guide/index.html
在这里插入图片描述
在这里插入图片描述
在实际工作中使用ES的时候,如果想屏蔽语言的差异,建议使用REST API,这种兼容性比较好,但是个人感觉有的操作使用起来比较麻烦,需要拼接组装各种数据字符串。针对Java程序员而言,还有一种选择是使用Java API,这种方式相对于REST API而言,代码量会大一些,但是代码层面看起来是比较清晰的。下面在操作ES的时候,分别使用一下这两种方式。

1.5.1 使用REST API的方式操作ES

如果想要在Linux命令行中使用REST API操作ES,需要借助于CURL工具。CURL是利用URL语法在命令行下工作的开源文件传输工具,使用CURL可以简单实现常见的get/post请求。curl后面通过-X参数指定请求类型,通过-d指定要传递的参数。

  • 索引库的操作(创建、删除)

创建索引库:curl -XPUT ‘http://bigdata01:9200/test/’ 这里使用PUT或者POST都可以。

[root@bigdata01 soft]# curl  -XPUT 'http://bigdata01:9200/test/'
{"acknowledged":true,"shards_acknowledged":true,"index":"test"}

注意:索引库名称必须要全部小写,不能以_、 -、 +开头,也不能包含逗号。

错误示例:

[root@bigdata01 soft]# curl  -XPUT 'http://bigdata01:9200/_test/'     
{"error":{"root_cause":[{"type":"invalid_index_name_exception","reason":"Invalid index name [_test], must not start with '_', '-', or '+'","index_uuid":"_na_","index":"_test"}],"type":"invalid_index_name_exception","reason":"Invalid index name [_test], must not start with '_', '-', or '+'","index_uuid":"_na_","index":"_test"},"status":400}

[root@bigdata01 soft]# curl  -XPUT 'http://bigdata01:9200/Test/' 
{"error":{"root_cause":[{"type":"invalid_index_name_exception","reason":"Invalid index name [Test], must be lowercase","index_uuid":"_na_","index":"Test"}],"type":"invalid_index_name_exception","reason":"Invalid index name [Test], must be lowercase","index_uuid":"_na_","index":"Test"},"status":400}

删除索引库:

[root@bigdata01 soft]# curl  -XDELETE 'http://bigdata01:9200/test/'
{"acknowledged":true}

注意:索引库可以提前创建,也可以在后期添加数据的时候直接指定一个不存在的索引库,ES默认会自动创建这个索引库。

手工创建索引库和自动创建索引库的区别就是,手工创建可以自定义索引库的分片数量。下面创建一个具有3个分片的索引库。

[root@bigdata01 soft]# curl -H "Content-Type: application/json" -XPUT 'http://bigdata01:9200/test/' -d'{"settings":{"index.number_of_shards":3}}'

{"acknowledged":true,"shards_acknowledged":true,"index":"test"}

在这里插入图片描述
其中实线的框表示是主分片,虚线框是副本分片。
索引分片编号是从0开始的,并且索引分片在物理层面是存在的,可以到集群中查看一下,从界面中也看到test索引库的0号和1号分片是在bigdata01节点上的。

到bigdata01节点中看一下,ES中的所有数据都在ES的数据存储目录中,默认是在ES_HOME下的data目录里面:
在这里插入图片描述
这里面的cYwxbVrFS0q1rV7YSaMhfw表示的是索引库的UUID。
在这里插入图片描述
在这里插入图片描述
索引的操作(增、删、改、查、Bulk批量操作)

  • 添加索引:

    [root@bigdata01 soft]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/emp/_doc/1' -d '{"name":"tom","age":20}'
    {"_index":"emp","_type":"_doc","_id":"1","_version":1,"result":"created","_shards":{"total":2,"successful":1,"failed":0},"_seq_no":0,"_primary_term":1}
    

    注意:这里emp索引库是不存在的,在使用的时候ES会自动创建,只不过索引分片数量默认是1。
    在这里插入图片描述
    为了兼容之前的API,虽然ES现在取消了Type,但是API中Type的位置还是预留出来了,官方建议统一使用_doc 。

在添加索引的时候,如果没有指定数据的ID,那么ES会自动生成一个随机的唯一ID。

[root@bigdata01 soft]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/emp/_doc' -d '{"name":"jack","age":30}' 
{"_index":"emp","_type":"_doc","_id":"EFND8aMBpApLBooiIWda","_version":1,"result":"created","_shards":{"total":2,"successful":2,"failed":0},"_seq_no":1,"_primary_term":1}

查询索引:查看id=1的索引数据。

[root@bigdata01 soft]# curl -XGET 'http://bigdata01:9200/emp/_doc/1?pretty'
{
  "_index" : "emp",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1,
  "_seq_no" : 0,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "name" : "tom",
    "age" : 20
  }
}

只获取部分字段内容。

[root@bigdata01 soft]# curl -XGET 'http://bigdata01:9200/emp/_doc/1?_source=name&pretty'
{
  "_index" : "emp",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1,
  "_seq_no" : 0,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "name" : "tom"
  }
}
[root@bigdata01 soft]# curl -XGET 'http://bigdata01:9200/emp/_doc/1?_source=name,age&pretty'
{
  "_index" : "emp",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1,
  "_seq_no" : 0,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "name" : "tom",
    "age" : 20
  }
}

查询指定索引库中所有数据。

[root@bigdata01 soft]# curl -XGET 'http://bigdata01:9200/emp/_search?pretty'
{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "emp",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "name" : "tom",
          "age" : 20
        }
      },
      {
        "_index" : "emp",
        "_type" : "_doc",
        "_id" : "EVPO8aMBpApLBooib2e7",
        "_score" : 1.0,
        "_source" : {
          "name" : "jack",
          "age" : 30
        }
      }
    ]
  }
}

注意:针对这种查询操作,可以在浏览器里面执行,或者在cerebo中查询都是可以的,看起来更加清晰。
在这里插入图片描述
在这里插入图片描述

  • 更新索引,可以分为全部更新和局部更新
    全部更新:同添加索引,如果指定id的索引数据(文档)已经存在,则执行更新操作。执行更新操作的时候,ES首先将旧的文标记为删除状态,然后添加新的文档,旧的文档不会立即消失,但是你也无法访问,ES会在你继续添加更多数据的时候在后台清理已经标记为删除状态的文档。

    局部更新:可以添加新字段或者更新已有字段,必须使用POST请求。

    [root@bigdata01 soft]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/emp/_doc/1/_update' -d '{"doc":{"age":25}}'
    {"_index":"emp","_type":"_doc","_id":"1","_version":2,"result":"updated","_shards":{"total":2,"successful":2,"failed":0},"_seq_no":2,"_primary_term":1}
    
  • 删除索引:删除id=1的索引数据。

    删除id=1的索引数据。

    [root@bigdata01 soft]# curl -XDELETE 'http://bigdata01:9200/emp/_doc/1'
    {"_index":"emp","_type":"_doc","_id":"1","_version":3,"result":"deleted","_shards":{"total":2,"successful":2,"failed":0},"_seq_no":3,"_primary_term":1}
    
    [root@bigdata01 soft]# curl -XDELETE 'http://bigdata01:9200/emp/_doc/1'
    {"_index":"emp","_type":"_doc","_id":"1","_version":4,"result":"not_found","_shards":{"total":2,"successful":2,"failed":0},"_seq_no":4,"_primary_term":1}
    

    如果索引数据(文档)存在,ES返回的数据中,result属性值为deleted,_version(版本)属性的值+1。
    如果索引数据不存在,ES返回的数据中,result属性值为not_found,但是_version属性的值依然会+1,这属于ES的版本控制系统,它保证了我们在多个节点间的不同操作的顺序都被正确标记了。
    对于索引数据的每次写操作,无论是 index,update 还是 delete,ES都会将_version增加 1。该增加是原子的,并且保证在操作成功返回时会发生。

    注意:删除一条索引数据(文档)也不会立即生效,它只是被标记成已删除状态。ES将会在你之后添加更多索引数据的时候才会在后台清理标记为删除状态的内容。

  • Bulk批量操作:Bulk API可以帮助我们同时执行多个请求,提高效率。

    格式:

    { action: { metadata }}
    { request body }
    

    语法格式解释:

    • action:index/create/update/delete
    • metadata:_index,_type,_id
    • request body:_source(删除操作不需要)

    create 和 index的区别:如果数据存在,使用create操作失败,会提示文档已经存在,使用index则可以成功执行。

    下面来看一个案例,假设在MySQL中有一批数据,首先需要从MySQL中把数据读取出来,然后将数据转化为Bulk需要的数据格式。在这直接手工生成Bulk需要的数据格式。

    [root@bigdata01 elasticsearch-7.13.4]# vi request 
    {"index":{"_index":"test","_type":"_doc","_id":"1"}}
    {"field1":"value1"}
    {"index":{"_index":"test","_type":"_doc","_id":"2"}}
    {"field1":"value1"}
    {"delete":{"_index":"test","_type":"_doc","_id":"2"}}
    {"create":{"_index":"test","_type":"_doc","_id":"3"}}
    {"field1":"value1"}
    {"update":{"_index":"test","_type":"_doc","_id":"1"}}
    {"doc":{"field2":"value2"}}
    

    执行Bulk API。

    [root@bigdata01 elasticsearch-7.13.4]# curl -H "Content-Type: application/json"  -XPUT 'http://bigdata01:9200/test/_doc/_bulk' --data-binary @request
    {"took":167,"errors":false,"items":[{"index":{"_index":"test","_type":"_doc","_id":"1","_version":1,"result":"created","_shards":{"total":2,"successful":2,"failed":0},"_seq_no":0,"_primary_term":1,"status":201}},{"index":{"_index":"test","_type":"_doc","_id":"2","_version":1,"result":"created","_shards":{"total":2,"successful":2,"failed":0},"_seq_no":0,"_primary_term":1,"status":201}},{"delete":{"_index":"test","_type":"_doc","_id":"2","_version":2,"result":"deleted","_shards":{"total":2,"successful":2,"failed":0},"_seq_no":1,"_primary_term":1,"status":200}},{"create":{"_index":"test","_type":"_doc","_id":"3","_version":1,"result":"created","_shards":{"total":2,"successful":2,"failed":0},"_seq_no":2,"_primary_term":1,"status":201}},{"update":{"_index":"test","_type":"_doc","_id":"1","_version":2,"result":"updated","_shards":{"total":2,"successful":2,"failed":0},"_seq_no":1,"_primary_term":1,"status":200}}]}
    

    查看结果:

    [root@bigdata01 elasticsearch-7.13.4]# curl -XGET 'http://bigdata01:9200/test/_search?pretty'
    {
      "took" : 6,
      "timed_out" : false,
      "_shards" : {
        "total" : 3,
        "successful" : 3,
        "skipped" : 0,
        "failed" : 0
      },
      "hits" : {
        "total" : {
          "value" : 2,
          "relation" : "eq"
        },
        "max_score" : 1.0,
        "hits" : [
          {
            "_index" : "test",
            "_type" : "_doc",
            "_id" : "3",
            "_score" : 1.0,
            "_source" : {
              "field1" : "value1"
            }
          },
          {
            "_index" : "test",
            "_type" : "_doc",
            "_id" : "1",
            "_score" : 1.0,
            "_source" : {
              "field1" : "value1",
              "field2" : "value2"
            }
          }
        ]
      }
    }
    

    Bulk一次最大可以处理多少数据量?

    Bulk会把将要处理的数据加载到内存中,所以数据量是有限制的,最佳的数据量不是一个确定的数值,它取决于集群硬件,文档大小、文档复杂性,索引以及ES集群的负载。
    一般建议是1000-5000个文档,如果文档很大,可以适当减少,文档总大小建议是5-15MB,默认不能超过100M
    如果想要修改最大限制大小,可以在ES的配置文件中修改http.max_content_length: 100mb,但是不建议,因为太大的话Bulk操作也会慢。

1.5.2 java操作ES

针对Java API,目前ES提供了两个Java REST Client版本:

  1. Java Low Level REST Client:
    低级别的REST客户端,通过HTTP与集群交互,用户需自己组装请求JSON串,以及解析响应JSON串。兼容所有Elasticsearch版本。
    这种方式其实就相当于使用Java对前面讲的REST API做了一层简单的封装,前面我们是使用的CURL这个工具执行的,现在是使用Java代码模拟执行HTTP请求了。
  2. Java High Level REST Client:
    高级别的REST客户端,基于低级别的REST客户端进行了封装,增加了组装请求JSON串、解析响应JSON串等相关API,开发代码使用的ES版本需要和集群中的ES版本一致,否则会有版本冲突问题。这种方式是从ES 6.0版本开始加入的,目的是以Java面向对象的方式进行请求、响应处理。高级别的REST客户端会兼容高版本的ES集群,例如:使用ES7.0版本开发的代码可以和任何7.x版本的ES集群交互。如果ES集群后期升级到了8.x版本,那么也要升级之前基于ES 7.0版本开发的代码。

如果考虑到代码后期的兼容性,建议使用Java Low Level REST Client。如果考虑到易用性,建议使用Java High Level REST Client。在这我们使用Java High Level REST Client

具体操作如下:

  1. pom.xml文件中添加ES的依赖和日志的依赖。

    <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>7.13.4</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.14.1</version>
    </dependency>
    
  2. 在resources目录下添加log4j2.properties

    appender.console.type = Console
    appender.console.name = console
    appender.console.layout.type = PatternLayout
    appender.console.layout.pattern = [%d{ISO8601}][%-5p][%-25c] %marker%m%n
    
    rootLogger.level = info
    rootLogger.appenderRef.console.ref = console
    
  3. 索引库的操作(创建、删除)

    package cn.git.es;
    
    import org.apache.http.HttpHost;
    import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
    import org.elasticsearch.client.RequestOptions;
    import org.elasticsearch.client.RestClient;
    import org.elasticsearch.client.RestHighLevelClient;
    import org.elasticsearch.client.indices.CreateIndexRequest;
    import org.elasticsearch.common.settings.Settings;
    import java.io.IOException;
    
    /**
     * 针对ES中索引库的操作
     * 1:创建索引库
     * 2:删除索引库
     * @author lixuchun
     */
    public class EsDataOp {
    
        public static void main(String[] args) throws IOException {
            // 创建环境
            RestHighLevelClient client = new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("bigdata01", 9200, "http"),
                            new HttpHost("bigdata02", 9200, "http"),
                            new HttpHost("bigdata03", 9200, "http")
                    )
            );
    
            // 创建索引库
            //createIndex(client);
    
            // 删除索引库
            deleteIndex(client);
    
            // 关闭连接
            client.close();
        }
    
        private static void deleteIndex(RestHighLevelClient client) throws IOException {
            DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest("java_test");
            client.indices().delete(deleteIndexRequest, RequestOptions.DEFAULT);
        }
    
        private static void createIndex(RestHighLevelClient client) throws IOException {
            CreateIndexRequest createRequest = new CreateIndexRequest("java_test");
            // 指定索引库的配置信息
            createRequest.settings(Settings.builder()
                    // 指定分片个数
                    .put("index.number_of_shards", 3)
            );
    
            // 执行创建
            client.indices().create(createRequest, RequestOptions.DEFAULT);
        }
    }
    
    

    执行代码的时候会有一个警告信息,提示ES集群没有开启权限校验机制,其实在企业中只要在运维层面控制好了ES集群IP和端口的访问其实就足够了。

  4. 索引的操作(增、删、改、查、Bulk批量操作)

package cn.git.es;

import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpHost;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.action.bulk.BulkItemResponse;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.search.fetch.subphase.FetchSourceContext;

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

/**
 * 针对ES中索引数据的操作
 * 增删改查
 * Created by lixuchun
 */
public class EsDataOp {
    private static Logger logger = LogManager.getLogger(EsDataOp.class);

    public static void main(String[] args) throws Exception{
        //获取RestClient连接
        RestHighLevelClient client = new RestHighLevelClient(
                RestClient.builder(
                        new HttpHost("bigdata01", 9200, "http"),
                        new HttpHost("bigdata02", 9200, "http"),
                        new HttpHost("bigdata03", 9200, "http")));

        // 创建索引
        //addIndexByJson(client);
        //addIndexByMap(client);

        // 查询索引
        //getIndex(client);
        //getIndexByFiled(client);

        // 更新索引
        //注意:可以使用创建索引直接完整更新已存在的数据
        //updateIndexByPart(client);//局部更新

        // 删除索引
        //deleteIndex(client);

        // Bulk批量操作
        //bulkIndex(client);

        // 关闭连接
        client.close();
    }

    private static void bulkIndex(RestHighLevelClient client) throws IOException {
        BulkRequest request = new BulkRequest();
        request.add(new IndexRequest("emp").id("20")
                .source(XContentType.JSON,"field1", "value1","field2","value2"));
        // id为10的数据不存在,但是执行删除是不会报错的
        request.add(new DeleteRequest("emp", "10"));
        request.add(new UpdateRequest("emp", "11")
                .doc(XContentType.JSON,"age", 19));
        // id为12的数据不存在,这一条命令在执行的时候会失败
        request.add(new UpdateRequest("emp", "12")
                .doc(XContentType.JSON,"age", 19));
        // 执行
        BulkResponse bulkResponse = client.bulk(request, RequestOptions.DEFAULT);
        // 如果Bulk中的个别语句出错不会导致整个Bulk执行失败,所以可以在这里判断一下是否有返回执行失败的信息
        for (BulkItemResponse bulkItemResponse : bulkResponse) {
            if (bulkItemResponse.isFailed()) {
                BulkItemResponse.Failure failure = bulkItemResponse.getFailure();
                logger.error("Bulk中出现了异常:"+failure);
            }
        }
    }

    private static void deleteIndex(RestHighLevelClient client) throws IOException {
        DeleteRequest request = new DeleteRequest("emp", "10");
        // 执行
        client.delete(request, RequestOptions.DEFAULT);
    }

    private static void updateIndexByPart(RestHighLevelClient client) throws IOException {
        UpdateRequest request = new UpdateRequest("emp", "10");
        String jsonString = "{\"age\":23}";
        request.doc(jsonString, XContentType.JSON);
        // 执行
        client.update(request, RequestOptions.DEFAULT);
    }

    private static void getIndexByFiled(RestHighLevelClient client) throws IOException {
        GetRequest request = new GetRequest("emp", "10");
        // 只查询部分字段
        String[] includes = new String[]{"name"};//指定包含哪些字段
        String[] excludes = Strings.EMPTY_ARRAY;//指定多滤掉哪些字段
        FetchSourceContext fetchSourceContext = new FetchSourceContext(true, includes, excludes);
        request.fetchSourceContext(fetchSourceContext);
        // 执行
        GetResponse response = client.get(request, RequestOptions.DEFAULT);
        // 通过response获取index、id、文档详细内容(source)
        String index = response.getIndex();
        String id = response.getId();
        if(response.isExists()){//如果没有查询到文档数据,则isExists返回false
            // 获取json字符串格式的文档结果
            String sourceAsString = response.getSourceAsString();
            System.out.println(sourceAsString);
            // 获取map格式的文档结果
            Map<String, Object> sourceAsMap = response.getSourceAsMap();
            System.out.println(sourceAsMap);
        }else{
            logger.warn("没有查询到索引库{}中id为{}的文档!",index,id);
        }
    }

    private static void getIndex(RestHighLevelClient client) throws IOException {
        GetRequest request = new GetRequest("emp", "10");
        // 执行
        GetResponse response = client.get(request, RequestOptions.DEFAULT);
        // 通过response获取index、id、文档详细内容(source)
        String index = response.getIndex();
        String id = response.getId();
        if(response.isExists()){//如果没有查询到文档数据,则isExists返回false
            // 获取json字符串格式的文档结果
            String sourceAsString = response.getSourceAsString();
            System.out.println(sourceAsString);
            // 获取map格式的文档结果
            Map<String, Object> sourceAsMap = response.getSourceAsMap();
            System.out.println(sourceAsMap);
        }else{
            logger.warn("没有查询到索引库{}中id为{}的文档!",index,id);
        }
    }

    private static void addIndexByMap(RestHighLevelClient client) throws IOException {
        IndexRequest request = new IndexRequest("emp");
        request.id("11");
        HashMap<String, Object> jsonMap = new HashMap<String, Object>();
        jsonMap.put("name", "tom");
        jsonMap.put("age", 17);
        request.source(jsonMap);
        // 执行
        client.index(request, RequestOptions.DEFAULT);
    }

    private static void addIndexByJson(RestHighLevelClient client) throws IOException {
        IndexRequest request = new IndexRequest("emp");
        request.id("10");
        String jsonString = "{" +
                "\"name\":\"jessic\"," +
                "\"age\":20" +
                "}";
        request.source(jsonString, XContentType.JSON);
        // 执行
        client.index(request, RequestOptions.DEFAULT);
    }
}

1.6 ES分词介绍

ES中在添加数据,也就是创建索引的时候,会先对数据进行分词。在查询索引数据的时候,也会先根据查询的关键字进行分词。所以在ES中分词这个过程是非常重要的,涉及到查询的效率和准确度。

假设有一条数据,数据中有一个字段是titile,这个字段的值为LexCorp BFG-9000。我们想要把这条数据在ES中创建索引,方便后期检索。创建索引和查询索引的大致流程是这样的:
在这里插入图片描述
图中左侧是创建索引的过程:

  • 首先对数据进行空白字符分割,将LexCorp BFG-9000切分为LexCorpBFG-9000
  • 然后进行单词切割,将LexCorp切分为LexCorpBFG-9000切分为BFG9000
  • 最后执行小写转换操作,将英文单词全部转换为小写。

图中右侧是查询索引的过程:

  • 后期想要查询LexCorp BFG-9000这条数据,但是具体的内容记不清了,大致想起来了一些关键词Lex corp bfg9000

  • 接下来就根据这些关键词进行查询,
    首先还是对数据进行空白符分割,将Lex corp bfg9000切分为Lexcorpbfg9000

  • 然后进行单词切割,Lexcorp不变,将bfg9000切分为bfg9000

  • 最后执行小写转换操作,将英文单词全部转换为小写。

    这样其实在检索的时候就可以忽略英文大小写了,因为前面在创建索引的时候也会对英文进行小写转换。

到这可以发现,使用Lex corp bfg9000是可以查找到LexCorp BFG-9000这条数据的,因为在经过空白符分割、单词切割、小写转换之后,这两条数据是一样的,其实只要能有一个单词是匹配的,就可以把这条数据查找出来。

了解了这个流程之后,我们以后在搜索引擎里面搜索一些内容的时候其实就知道要怎么快速高效的检索内容了,只需要输入一些关键词,中间最好用空格隔开,针对英文字符不用纠结大小写了。

这些数据在ES中分词之后,其实在底层会产生倒排索引,注意了,倒排索引是ES能够提供快速检索能力的核心,下面来看一下这个倒排索引

1.6.1 倒排索引介绍

假设有一批数据,数据中有两个字段,文档编号和文档内容。
在这里插入图片描述
针对这一批数据,在ES中创建索引之后,最终产生的倒排索引内容大致是这样的:
在这里插入图片描述
上图操作解释:

  • 单词ID:记录每个单词的单词编号。
  • 单词:对应的单词。
  • 文档频率:代表文档集合中有多少个文档包含某个单词。
  • 倒排列表:包含单词ID及其它必要信息。
    DocId:单词出现的文档id。
    TF:单词在某个文档中出现的次数。
    POS:单词在文档中出现的位置。

以单词 加盟 为例,其单词编号为6,文档频率为3,代表整个文档集合中有3个文档包含这个单词,对应的倒排列表为{(2;1;<4>),(3;1;<7>),(5;1;<5>)},含义是在文档2,3,5中出现过这个单词,在每个文档中都只出现过1次,单词 加盟 在第一个文档的POS(位置)是4,即文档的第四个单词是 加盟 ,其它的类似。

这个倒排索引已经是一个非常完备的索引系统,实际搜索系统的索引结构基本如此。

1.6.1 分词器的作用

前面分析了ES在创建索引和查询索引的时候都需要进行分词,分词需要用到分词器。

下面来具体分析一下分词器的作用:

  • 分词器的作用是把一段文本中的词按照一定规则进行切分。

  • 分词器对应的是Analyzer类,这是一个抽象类,切分词的具体规则是由子类实现的。
    也就是说不同的分词器分词的规则是不同的!

所以对于不同的语言,要用不同的分词器。在创建索引时会用到分词器,在搜索时也会用到分词器,这两个地方要使用同一个分词器,否则可能会搜索不出结果。

1.6.2 分词器的工作流程

分词器的工作流程一般是这样的:

  1. 切分关键词,把关键的、核心的单词切出来。
  2. 去除停用词。
  3. 对于英文单词,把所有字母转为小写(搜索时不区分大小写)
1.6.3 停用词

有些词在文本中出现的频率非常高,但是对文本所携带的信息基本不产生影响。
例如:
英文停用词:a、an、the、of等
中文停用词:的、了、着、是、标点符号等

文本经过分词之后,停用词通常被过滤掉,不会被进行索引。
在检索的时候,用户的查询中如果含有停用词,检索系统也会将其过滤掉(因为用户输入的查询字符串也要进行分词处理)。
排除停用词可以加快建立索引的速度,减小索引库文件的大小,并且还可以提高查询的准确度。
如果不去除停用词,可能会存在这个情况:
假设有一批文章数据,基本上每篇文章里面都有 的 这个词,那我在检索的时候只要输入了的这个词,那么所有文章都认为是满足条件的数据,但是这样是没有意义的。

常见的英文停用词汇总:

a
about
above
after
again
against
all
am
an
and
any
are
aren't
as
at
be
because
been
before
being
below
between
both
but
by
can't
cannot
could
couldn't
did
didn't
do
does
doesn't
doing
don't
down
during
each
few
for
from
further
had
hadn't
has
hasn't
have
haven't
having
he
he'd
he'll
he's
her
here
here's
hers
herself
him
himself
his
how
how's
i
i'd
i'll
i'm
i've
if
in
into
is
isn't
it
it's
its
itself
let's
me
more
most
mustn't
my
myself
no
nor
not
of
off
on
once
only
or
other
ought
our
ours
ourselves
out
over
own
same
shan't
she
she'd
she'll
she's
should
shouldn't
so
some
such
than
that
that's
the
their
theirs
them
themselves
then
there
there's
these
they
they'd
they'll
they're
they've
this
those
through
to
too
under
until
up
very
was
wasn't
we
we'd
we'll
we're
we've
were
weren't
what
what's
when
when's
where
where's
which
while
who
who's
whom
why
why's
with
won't
would
wouldn't
you
you'd
you'll
you're
you've
your
yours
yourself
yourselves

常见的中文停用词汇总:

的
一
不
在
人
有
是
为
以
于
上
他
而
后
之
来
及
了
因
下
可
到
由
这
与
也
此
但
并
个
其
已
无
小
我
们
起
最
再
今
去
好
只
又
或
很
亦
某
把
那
你
乃
它
吧
被
比
别
趁
当
从
到
得
打
凡
儿
尔
该
各
给
跟
和
何
还
即
几
既
看
据
距
靠
啦
了
另
么
每
们
嘛
拿
哪
那
您
凭
且
却
让
仍
啥
如
若
使
谁
虽
随
同
所
她
哇
嗡
往
哪
些
向
沿
哟
用
于
咱
则
怎
曾
至
致
着
诸
自
1.6.4 中文分词方式

针对中文而言,在分词的时候有多种分词规则:常见的有单字分词、二分法分词、词库分词等

  • 单字分词:"我"、"们"、"是"、"中"、"国"、"人"
  • 二分法分词:"我们"、"们是"、"是中"、"中国"、"国人"。
  • 词库分词:按照某种算法构造词,然后去匹配已建好的词库集合,如果匹配到就切分出来成为词语。

从这里面可以看出来,其实最理想的中文分词方式是词库分词

1.6.5 常见的中文分词器

针对前面分析的几种中文分词方式,对应的有一些已经实现好的中分分词器。
在这里插入图片描述
在词库分词方式领域里面,最经典的就是IK分词器,你懂得!

1.6.6 ES中文分词插件(es-ik)

在中文数据检索场景中,为了提供更好的检索效果,需要在ES中集成中文分词器,因为ES默认是按照英文的分词规则进行分词的,基本上可以认为是单字分词,对中文分词效果不理想。

ES之前是没有提供中文分词器的,现在官方也提供了一些,但是在中文分词领域,IK分词器是不可撼动的,所以在这里我们主要讲一下如何在ES中集成IK这个中文分词器。

首先下载es-ik插件,需要到github上下载。https://github.com/medcl/elasticsearch-analysis-ik
在这里插入图片描述
在这里插入图片描述
最终的下载地址为:https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.13.4/elasticsearch-analysis-ik-7.13.4.zip

注意:在ES中安装IK插件的时候,需要在ES集群的所有节点中都安装。

具体安装步骤如下:

  • 将下载好的elasticsearch-analysis-ik-7.13.4.zip上传到bigdata01的/usr/soft/ elasticsearch-7.13.4目录中。

    [root@bigdata01 elasticsearch-7.13.4]# ll elasticsearch-analysis-ik-7.13.4.zip 
    -rw-r--r--. 1 root root 4504502 Sep  3  2021 elasticsearch-analysis-ik-7.13.4.zip
    
  • 将elasticsearch-analysis-ik-7.13.4.zip远程拷贝到bigdata02和bigdata03上。

    [root@bigdata01 elasticsearch-7.13.4]# scp -rq elasticsearch-analysis-ik-7.13.4.zip  bigdata02:/usr/soft/elasticsearch-7.13.4
    [root@bigdata01 elasticsearch-7.13.4]# scp -rq elasticsearch-analysis-ik-7.13.4.zip  bigdata03:/usr/soft/elasticsearch-7.13.4
    
  • 在bigdata01节点离线安装IK插件。

    [root@bigdata01 elasticsearch-7.13.4]# bin/elasticsearch-plugin install file:///usr/soft/elasticsearch-7.13.4/elasticsearch-analysis-ik-7.13.4.zip 
    

    注意:在安装的过程中会有警告信息提示需要输入y确认继续向下执行。

    [=================================================] 100%   
    @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
    @     WARNING: plugin requires additional permissions     @
    @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
    * java.net.SocketPermission * connect,resolve
    See http://docs.oracle.com/javase/8/docs/technotes/guides/security/permissions.html
    for descriptions of what these permissions allow and the associated risks.
    
    Continue with installation? [y/N]y
    

    最后看到如下内容就表示安装成功了。

    -> Installed analysis-ik
    -> Please restart Elasticsearch to activate any plugins installed
    

    注意:插件安装成功之后在elasticsearch-7.13.4的config和plugins目录下会产生一个analysis-ik目录。

    config目录下面的analysis-ik里面存储的是ik的配置文件信息。

    [root@bigdata01 elasticsearch-7.13.4]# cd config/
    [root@bigdata01 config]# ll analysis-ik/
    total 8260
    -rwxrwxrwx. 1 root root 5225922 Feb 27 20:57 extra_main.dic
    -rwxrwxrwx. 1 root root   63188 Feb 27 20:57 extra_single_word.dic
    -rwxrwxrwx. 1 root root   63188 Feb 27 20:57 extra_single_word_full.dic
    -rwxrwxrwx. 1 root root   10855 Feb 27 20:57 extra_single_word_low_freq.dic
    -rwxrwxrwx. 1 root root     156 Feb 27 20:57 extra_stopword.dic
    -rwxrwxrwx. 1 root root     625 Feb 27 20:57 IKAnalyzer.cfg.xml
    -rwxrwxrwx. 1 root root 3058510 Feb 27 20:57 main.dic
    -rwxrwxrwx. 1 root root     123 Feb 27 20:57 preposition.dic
    -rwxrwxrwx. 1 root root    1824 Feb 27 20:57 quantifier.dic
    -rwxrwxrwx. 1 root root     164 Feb 27 20:57 stopword.dic
    -rwxrwxrwx. 1 root root     192 Feb 27 20:57 suffix.dic
    -rwxrwxrwx. 1 root root     752 Feb 27 20:57 surname.dic
    

    plugins目录下面的analysis-ik里面存储的是ik的核心jar包。

    [root@bigdata01 elasticsearch-7.13.4]# cd plugins/
    [root@bigdata01 plugins]# ll analysis-ik/
    total 1428
    -rwxrwxrwx. 1 root root 263965 Feb 27 20:56 commons-codec-1.9.jar
    -rwxrwxrwx. 1 root root  61829 Feb 27 20:56 commons-logging-1.2.jar
    -rwxrwxrwx. 1 root root  54626 Feb 27 20:56 elasticsearch-analysis-ik-7.13.4.jar
    -rwxrwxrwx. 1 root root 736658 Feb 27 20:56 httpclient-4.5.2.jar
    -rwxrwxrwx. 1 root root 326724 Feb 27 20:56 httpcore-4.4.4.jar
    -rwxrwxrwx. 1 root root   1807 Feb 27 20:56 plugin-descriptor.properties
    -rwxrwxrwx. 1 root root    125 Feb 27 20:56 plugin-security.policy
    
  • 在bigdata02节点离线安装IK插件。

    [root@bigdata02 elasticsearch-7.13.4]# bin/elasticsearch-plugin install file:///usr/soft/elasticsearch-7.13.4/elasticsearch-analysis-ik-7.13.4.zip 
    
  • 在bigdata03节点离线安装IK插件。

    [root@bigdata03 elasticsearch-7.13.4]# bin/elasticsearch-plugin install file:///usr/soft/elasticsearch-7.13.4/elasticsearch-analysis-ik-7.13.4.zip 
    
  • 如果集群正在运行,则需要停止集群。

    分别在bigdata01,02,03上停止。

    [root@bigdata01 elasticsearch-7.13.4]# jps
    1680 Elasticsearch
    2047 Jps
    [root@bigdata01 elasticsearch-7.13.4]# kill 1680
    
  • 修改elasticsearch-7.13.4的plugins目录下analysis-ik子目录的权限。直接修改elasticsearch-7.13.4目录的权限即可。

    分别在bigdata01,02,03上执行。

    [root@bigdata01 elasticsearch-7.13.4]# cd ..
    [root@bigdata01 soft]# chmod -R 777 elasticsearch-7.13.4
    
  • 重新启动ES集群。分别在bigdata01,02,03上执行。

    [root@bigdata01 soft]# su es
    [es@bigdata01 soft]$ cd /usr/soft/elasticsearch-7.13.4
    [es@bigdata01 elasticsearch-7.13.4]$ bin/elasticsearch -d
    
  • 验证IK的分词效果。首先使用默认分词器测试中文分词效果。

    [root@bigdata01 soft]# curl -H "Content-Type: application/json" -XPOST  'http://bigdata01:9200/emp/_analyze?pretty' -d '{"text":"我们是中国人"}'
    {
      "tokens" : [
        {
          "token" : "我",
          "start_offset" : 0,
          "end_offset" : 1,
          "type" : "<IDEOGRAPHIC>",
          "position" : 0
        },
        {
          "token" : "们",
          "start_offset" : 1,
          "end_offset" : 2,
          "type" : "<IDEOGRAPHIC>",
          "position" : 1
        },
        {
          "token" : "是",
          "start_offset" : 2,
          "end_offset" : 3,
          "type" : "<IDEOGRAPHIC>",
          "position" : 2
        },
        {
          "token" : "中",
          "start_offset" : 3,
          "end_offset" : 4,
          "type" : "<IDEOGRAPHIC>",
          "position" : 3
        },
        {
          "token" : "国",
          "start_offset" : 4,
          "end_offset" : 5,
          "type" : "<IDEOGRAPHIC>",
          "position" : 4
        },
        {
          "token" : "人",
          "start_offset" : 5,
          "end_offset" : 6,
          "type" : "<IDEOGRAPHIC>",
          "position" : 5
        }
      ]
    }
    

    然后使用IK分词器测试中文分词效果。

    [root@bigdata01 soft]# curl -H "Content-Type: application/json" -XPOST  'http://bigdata01:9200/emp/_analyze?pretty' -d '{"text":"我们是中国人","tokenizer":"ik_max_word"}'
    {
      "tokens" : [
        {
          "token" : "我们",
          "start_offset" : 0,
          "end_offset" : 2,
          "type" : "CN_WORD",
          "position" : 0
        },
        {
          "token" : "是",
          "start_offset" : 2,
          "end_offset" : 3,
          "type" : "CN_CHAR",
          "position" : 1
        },
        {
          "token" : "中国人",
          "start_offset" : 3,
          "end_offset" : 6,
          "type" : "CN_WORD",
          "position" : 2
        },
        {
          "token" : "中国",
          "start_offset" : 3,
          "end_offset" : 5,
          "type" : "CN_WORD",
          "position" : 3
        },
        {
          "token" : "国人",
          "start_offset" : 4,
          "end_offset" : 6,
          "type" : "CN_WORD",
          "position" : 4
        }
      ]
    }
    

    在这里我们发现分出来的单词里面有一个 是,这个单词其实可以认为是一个停用词,在分词的时候是不需要切分出来的。
    在这被切分出来了,那也就意味着在进行停用词过滤的时候没有过滤掉。

    针对ik这个词库而言,它的停用词词库里面都有哪些单词呢?

    [root@bigdata01 elasticsearch-7.13.4]# cd config/analysis-ik/
    [root@bigdata01 analysis-ik]# ll
    total 8260
    -rwxrwxrwx. 1 root root 5225922 Feb 27 20:57 extra_main.dic
    -rwxrwxrwx. 1 root root   63188 Feb 27 20:57 extra_single_word.dic
    -rwxrwxrwx. 1 root root   63188 Feb 27 20:57 extra_single_word_full.dic
    -rwxrwxrwx. 1 root root   10855 Feb 27 20:57 extra_single_word_low_freq.dic
    -rwxrwxrwx. 1 root root     156 Feb 27 20:57 extra_stopword.dic
    -rwxrwxrwx. 1 root root     625 Feb 27 20:57 IKAnalyzer.cfg.xml
    -rwxrwxrwx. 1 root root 3058510 Feb 27 20:57 main.dic
    -rwxrwxrwx. 1 root root     123 Feb 27 20:57 preposition.dic
    -rwxrwxrwx. 1 root root    1824 Feb 27 20:57 quantifier.dic
    -rwxrwxrwx. 1 root root     164 Feb 27 20:57 stopword.dic
    -rwxrwxrwx. 1 root root     192 Feb 27 20:57 suffix.dic
    -rwxrwxrwx. 1 root root     752 Feb 27 20:57 surname.dic
    [root@bigdata01 analysis-ik]# more stopword.dic 
    a
    an
    and
    are
    as
    at
    be
    but
    by
    for
    if
    in
    into
    is
    it
    no
    not
    of
    on
    or
    

    ik的停用词词库是stopword.dic这个文件,这个文件里面目前都是一些英文停用词。我们可以手工在这个文件中把中文停用词添加进去,先添加 这个停用词。

    [root@bigdata01 analysis-ik]# vi stopword.dic 
    .....
    是
    

    然后把这个文件的改动同步到集群中的所有节点上。

    [root@bigdata01 analysis-ik]# scp -rq stopword.dic bigdata02:/usr/soft/elasticsearch-7.13.4/config/analysis-ik/
    [root@bigdata01 analysis-ik]# scp -rq stopword.dic bigdata03:/usr/soft/elasticsearch-7.13.4/config/analysis-ik/
    

    重启集群让配置生效。先停止bigdata01、bigdata02、bigdata03上的ES服务。

    [root@bigdata01 analysis-ik]# jps
    3051 Elasticsearch
    3358 Jps
    [root@bigdata01 analysis-ik]# kill 3051
    

    启动bigdata01、bigdata02、bigdata03上的ES服务。

    [es@bigdata01 elasticsearch-7.13.4]$ bin/elasticsearch -d
    [es@bigdata02 elasticsearch-7.13.4]$ bin/elasticsearch -d
    [es@bigdata03 elasticsearch-7.13.4]$ bin/elasticsearch -d
    

    再使用IK分词器测试一下中文分词效果。

    [root@bigdata01 analysis-ik]# curl -H "Content-Type: application/json" -XPOST  'http://bigdata01:9200/test/_analyze?pretty' -d '{"text":"我们是中国人","tokenizer":"ik_max_word"}'
    {
      "tokens" : [
        {
          "token" : "我们",
          "start_offset" : 0,
          "end_offset" : 2,
          "type" : "CN_WORD",
          "position" : 0
        },
        {
          "token" : "中国人",
          "start_offset" : 3,
          "end_offset" : 6,
          "type" : "CN_WORD",
          "position" : 1
        },
        {
          "token" : "中国",
          "start_offset" : 3,
          "end_offset" : 5,
          "type" : "CN_WORD",
          "position" : 2
        },
        {
          "token" : "国人",
          "start_offset" : 4,
          "end_offset" : 6,
          "type" : "CN_WORD",
          "position" : 3
        }
      ]
    }
    

    此时再查看会发现没有"是" 这个单词了,相当于在过滤停用词的时候把它过滤掉了。

1.6.7 es-ik添加自定义词库

自定义词库,针对一些特殊的词语在分词的时候也需要能够识别。

例如:公司产品的名称或者网络上新流行的词语,假设我们公司开发了一款新产品,命名为:数据大脑,我们希望ES在分词的时候能够把这个产品名称直接识别成一个词语。现在使用ik分词器测试一下分词效果:

[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST  'http://bigdata01:9200/test/_analyze?pretty' -d '{"text":"数据大脑","tokenizer":"ik_max_word"}'
{
  "tokens" : [
    {
      "token" : "数据",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "CN_WORD",
      "position" : 0
    },
    {
      "token" : "大脑",
      "start_offset" : 2,
      "end_offset" : 4,
      "type" : "CN_WORD",
      "position" : 1
    }
  ]
}

结果发现ik分词器会把数据大脑分为 数据大脑这两个单词。
因为这个词语是我们自己造出来的,并不是通用的词语,所以ik分词器识别不出来也属于正常。
想要让IK分词器识别出来,就需要自定义词库了,也就是把我们自己造的词语添加到词库里面,这样在分词的时候就可以识别到了。
下面演示一下如何在IK中自定义词库:

  • 首先在ik插件对应的配置文件目录下创建一个自定义词库文件my.dic
    首先在bigdata01节点上操作。
    切换到es用户,进入到ik插件对应的配置文件目录

    [root@bigdata01 ~]# su es
    [es@bigdata01 root]$ cd /usr/soft/elasticsearch-7.13.4
    [es@bigdata01 elasticsearch-7.13.4]$ cd config
    [es@bigdata01 config]$ cd analysis-ik
    [es@bigdata01 analysis-ik]$ ll
    total 8260
    -rwxrwxrwx. 1 root root 5225922 Feb 27 20:57 extra_main.dic
    -rwxrwxrwx. 1 root root   63188 Feb 27 20:57 extra_single_word.dic
    -rwxrwxrwx. 1 root root   63188 Feb 27 20:57 extra_single_word_full.dic
    -rwxrwxrwx. 1 root root   10855 Feb 27 20:57 extra_single_word_low_freq.dic
    -rwxrwxrwx. 1 root root     156 Feb 27 20:57 extra_stopword.dic
    -rwxrwxrwx. 1 root root     625 Feb 27 20:57 IKAnalyzer.cfg.xml
    -rwxrwxrwx. 1 root root 3058510 Feb 27 20:57 main.dic
    -rwxrwxrwx. 1 root root     123 Feb 27 20:57 preposition.dic
    -rwxrwxrwx. 1 root root    1824 Feb 27 20:57 quantifier.dic
    -rwxrwxrwx. 1 root root     171 Feb 27 21:42 stopword.dic
    -rwxrwxrwx. 1 root root     192 Feb 27 20:57 suffix.dic
    -rwxrwxrwx. 1 root root     752 Feb 27 20:57 surname.dic
    

    创建自定义词库文件my.dic直接在文件中添加词语即可,每一个词语一行。

    [es@bigdata01 analysis-ik]$ vi my.dic
    数据大脑
    

    注意:这个my.dic词库文件可以在Linux中直接使用vi命令创建,或者在Windows中创建之后上传到这里。

    • 如果是在Linux中直接使用vi命令创建,可以直接使用。

    • 如果是在Windows中创建的,需要注意文件的编码必须是UTF-8 without BOM 格式【UTF-8 无 BOM格式】

    以Notepad++为例:新版本的Notepad++里面的文件编码有这么几种,需要选择【使用UTF-8编码】,这个就是UTF-8 without BOM 格式。
    在这里插入图片描述

  • 修改ik的IKAnalyzer.cfg.xml配置文件,进入到ik插件对应的配置文件目录中,修改IKAnalyzer.cfg.xml配置文件

    [es@bigdata01 analysis-ik]$ vi IKAnalyzer.cfg.xml 
    
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
    <properties>
            <comment>IK Analyzer 扩展配置</comment>
            <!--用户可以在这里配置自己的扩展字典 -->
            <entry key="ext_dict">my.dic</entry>
             <!--用户可以在这里配置自己的扩展停止词字典-->
            <entry key="ext_stopwords"></entry>
            <!--用户可以在这里配置远程扩展字典 -->
            <!-- <entry key="remote_ext_dict">words_location</entry> -->
            <!--用户可以在这里配置远程扩展停止词字典-->
            <!-- <entry key="remote_ext_stopwords">words_location</entry>-->
    </properties>
    

    注意:需要把my.dic词库文件添加到key="ext_dict"这个entry中,切记不要随意新增entry,随意新增的entry是不被IK识别的,并且entry的名称也不能乱改,否则也不会识别。

    如果需要指定多个自定义词库文件的话需要使用分号;隔开。例如:<entry key="ext_dict">my.dic;your.dic</entry>

  • 将修改好的IK配置文件复制到集群中的所有节点中,如果是多个节点的ES集群,一定要把配置远程拷贝到其他节点。

    先从bigdata01上将my.dic拷贝到bigdata02和bigdata03

    [es@bigdata01 analysis-ik]$ scp -rq my.dic bigdata02:/usr/soft/elasticsearch-7.13.4/config/analysis-ik/
    The authenticity of host 'bigdata02 (192.168.182.101)' can't be established.
    ECDSA key fingerprint is SHA256:SnzVynyweeRcPIorakoDQRxFhugZp6PNIPV3agX/bZM.
    ECDSA key fingerprint is MD5:f6:1a:48:78:64:77:89:52:c4:ad:63:82:a5:d5:57:92.
    Are you sure you want to continue connecting (yes/no)? yes
    es@bigdata02's password: 
    
    [es@bigdata01 analysis-ik]$ scp -rq my.dic bigdata03:/usr/soft/elasticsearch-7.13.4/config/analysis-ik/
    The authenticity of host 'bigdata03 (192.168.182.102)' can't be established.
    ECDSA key fingerprint is SHA256:SnzVynyweeRcPIorakoDQRxFhugZp6PNIPV3agX/bZM.
    ECDSA key fingerprint is MD5:f6:1a:48:78:64:77:89:52:c4:ad:63:82:a5:d5:57:92.
    Are you sure you want to continue connecting (yes/no)? yes
    es@bigdata03's password: 
    

    注意:因为现在使用的是普通用户es,所以在使用scp的时候需要指定目标机器的用户名(如果是root可以省略不写),并且还需要手工输入密码,因为之前是基于root用户做的免密码登录。

    再从bigdata01上将IKAnalyzer.cfg.xml拷贝到bigdata02和bigdata03

    [es@bigdata01 analysis-ik]$ scp -rq IKAnalyzer.cfg.xml bigdata02:/usr/soft/elasticsearch-7.13.4/config/analysis-ik/
    es@bigdata02's password: 
    
    [es@bigdata01 analysis-ik]$ scp -rq IKAnalyzer.cfg.xml bigdata03:/usr/soft/elasticsearch-7.13.4/config/analysis-ik/
    es@bigdata03's password: 
    
    

    注意:如果后期想增加自定义停用词库,也需要按照这个思路进行添加,只不过停用词库需要配置到 key="ext_stopwords"这个entry中。

  • 重启ES验证一下自定义词库的分词效果,先停止bigdata01,bigdata02,bigdata03ES集群。

    [es@bigdata01 ~]$ jps
    1892 Jps
    1693 Elasticsearch
    [es@bigdata01 ~]$ kill -9 1693+
    

    再启动ES集群。

    [es@bigdata01 ~]$ cd /usr/soft/elasticsearch-7.13.4/
    [es@bigdata01 elasticsearch-7.13.4]$ bin/elasticsearch -d
    
    [es@bigdata02 ~]$ cd /usr/soft/elasticsearch-7.13.4/
    [es@bigdata02 elasticsearch-7.13.4]$ bin/elasticsearch -d
    
    [es@bigdata03 ~]$ cd /usr/soft/elasticsearch-7.13.4/
    [es@bigdata03 elasticsearch-7.13.4]$ bin/elasticsearch -d
    

    验证:

    [es@bigdata01 elasticsearch-7.13.4]$ curl -H "Content-Type: application/json" -XPOST  'http://bigdata01:9200/test/_analyze?pretty' -d '{"text":"数据大脑","tokenizer":"ik_max_word"}'
    {
      "tokens" : [
        {
          "token" : "数据大脑",
          "start_offset" : 0,
          "end_offset" : 4,
          "type" : "CN_WORD",
          "position" : 0
        },
        {
          "token" : "数据",
          "start_offset" : 0,
          "end_offset" : 2,
          "type" : "CN_WORD",
          "position" : 1
        },
        {
          "token" : "大脑",
          "start_offset" : 2,
          "end_offset" : 4,
          "type" : "CN_WORD",
          "position" : 2
        }
      ]
    }
    

    现在发现数据大脑这个词语可以被识别出来了,说明自定义词库生效了。

1.6.8 热更新词库

针对前面分析的自定义词库,后期只要词库内容发生了变动,就需要重启ES才能生效,在实际工作中,频繁重启ES集群不是一个好办法,所以ES提供了热更新词库的解决方案,在不重启ES集群的情况下识别新增的词语,这样就很方便了,也不会对线上业务产生影响。

下面来演示一下热更新词库的使用:

  1. 在bigdata04上部署HTTP服务
    在这使用tomcat作为Web容器,先下载一个tomcat 8.x版本。tomcat 8.0.52版本下载地址:
    https://archive.apache.org/dist/tomcat/tomcat-8/v8.0.52/bin/apache-tomcat-8.0.52.tar.gz
    上传到bigdata04上的/usr/soft目录里面,并且解压

    [root@bigdata04 soft]# ll apache-tomcat-8.0.52.tar.gz 
    -rw-r--r--. 1 root root 9435483 Sep 22  2021 apache-tomcat-8.0.52.tar.gz
    [root@bigdata04 soft]# tar -zxvf apache-tomcat-8.0.52.tar.gz 
    

    tomcat的ROOT项目中创建一个自定义词库文件hot.dic,在文件中输入一行内容:测试

    [root@bigdata04 soft]# cd apache-tomcat-8.0.52
    [root@bigdata04 apache-tomcat-8.0.52]# cd webapps/ROOT/
    [root@bigdata04 ROOT]# vi hot.dic
    测试
    

    启动Tomcat

    [root@bigdata04 ROOT]# cd /usr/soft/apache-tomcat-8.0.52
    [root@bigdata04 apache-tomcat-8.0.52]# bin/startup.sh 
    Using CATALINA_BASE:   /usr/soft/apache-tomcat-8.0.52
    Using CATALINA_HOME:   /usr/soft/apache-tomcat-8.0.52
    Using CATALINA_TMPDIR: /usr/soft/apache-tomcat-8.0.52/temp
    Using JRE_HOME:        /usr/soft/jdk1.8
    Using CLASSPATH:       /usr/soft/apache-tomcat-8.0.52/bin/bootstrap.jar:/usr/soft/apache-tomcat-8.0.52/bin/tomcat-juli.jar
    Tomcat started.
    

    验证一下hot.dic文件是否可以通过浏览器访问:
    在这里插入图片描述

  2. 修改ES集群中ik插件的IKAnalyzer.cfg.xml配置文件,在bigdata01上修改。
    在key="remote_ext_dict"这个entry中添加 hot.dic的远程访问链接 http://bigdata04:8080/hot.dic

    一定要记得去掉key="remote_ext_dict"这个entry外面的注释,否则添加的内容是不生效的。

    [es@bigdata01 analysis-ik]$ vi IKAnalyzer.cfg.xml 
    
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
    <properties>
            <comment>IK Analyzer 扩展配置</comment>
            <!--用户可以在这里配置自己的扩展字典 -->
            <entry key="ext_dict">my.dic</entry>
             <!--用户可以在这里配置自己的扩展停止词字典-->
            <entry key="ext_stopwords"></entry>
            <!--用户可以在这里配置远程扩展字典 -->
            <entry key="remote_ext_dict">http://bigdata04:8080/hot.dic</entry> 
            <!--用户可以在这里配置远程扩展停止词字典-->
            <!-- <entry key="remote_ext_stopwords">words_location</entry>-->
    </properties>
    
  • 将修改好的IK配置文件复制到集群中的所有节点中

    [es@bigdata01 analysis-ik]$ scp -rq IKAnalyzer.cfg.xml bigdata02:/usr/soft/elasticsearch-7.13.4/config/analysis-ik/
    es@bigdata02's password: 
    [es@bigdata01 analysis-ik]$ scp -rq IKAnalyzer.cfg.xml bigdata03:/usr/soft/elasticsearch-7.13.4/config/analysis-ik/
    es@bigdata03's password: 
    
  • 重启ES集群验证效果。因为修改了配置,所以需要重启集群。先停止ES集群。

    [es@bigdata01 ~]$ jps
    1892 Jps
    1693 Elasticsearch
    [es@bigdata01 ~]$ kill 1693
    
    [es@bigdata02 ~]$ jps
    1873 Jps
    1725 Elasticsearch
    [es@bigdata02 ~]$ kill 1725
    
    [es@bigdata02 ~]$ jps
    1844 Jps
    1694 Elasticsearch
    [es@bigdata02 ~]$ kill 1694
    

    再启动ES集群。

    [es@bigdata01 ~]$ cd /usr/soft/elasticsearch-7.13.4/
    [es@bigdata01 elasticsearch-7.13.4]$ bin/elasticsearch -d
    
    [es@bigdata02 ~]$ cd /usr/soft/elasticsearch-7.13.4/
    [es@bigdata02 elasticsearch-7.13.4]$ bin/elasticsearch -d
    
    [es@bigdata03 ~]$ cd /usr/soft/elasticsearch-7.13.4/
    [es@bigdata03 elasticsearch-7.13.4]$ bin/elasticsearch -d
    

    验证:对北京雾霾这个词语进行分词

    [es@bigdata01 elasticsearch-7.13.4]$ curl -H "Content-Type: application/json" -XPOST  'http://bigdata01:9200/test/_analyze?pretty' -d '{"text":"北京雾霾","tokenizer":"ik_max_word"}'
    {
      "tokens" : [
        {
          "token" : "北京",
          "start_offset" : 0,
          "end_offset" : 2,
          "type" : "CN_WORD",
          "position" : 0
        },
        {
          "token" : "雾",
          "start_offset" : 2,
          "end_offset" : 3,
          "type" : "CN_CHAR",
          "position" : 1
        },
        {
          "token" : "霾",
          "start_offset" : 3,
          "end_offset" : 4,
          "type" : "CN_CHAR",
          "position" : 2
        }
      ]
    }
    

    正常情况下 北京雾霾 会被分被拆分为多个词语,但是在这我希望ES能够把 北京雾霾 认为是一个完整的词语,又不希望重启ES。
    这样就可以修改前面配置的hot.dic文件,在里面增加一个词语:北京雾霾
    在bigdata04里面操作,此时可以在Linux中直接编辑文件。

    [root@bigdata04 apache-tomcat-8.0.52]# cd webapps/ROOT/
    [root@bigdata04 ROOT]# vi hot.dic 
    测试
    北京雾霾
    

    文件保存之后,在bigdata01上查看ES的日志会看到如下日志信息:

    [2027-03-09T18:43:12,700][INFO ][o.w.a.d.Dictionary       ] [bigdata01] start to reload ik dict.
    [2027-03-09T18:43:12,701][INFO ][o.w.a.d.Dictionary       ] [bigdata01] try load config from /usr/soft/elasticsearch-7.13.4/config/analysis-ik/IKAnalyzer.cfg.xml
    [2027-03-09T18:43:12,929][INFO ][o.w.a.d.Dictionary       ] [bigdata01] [Dict Loading] /usr/soft/elasticsearch-7.13.4/config/analysis-ik/my.dic
    [2027-03-09T18:43:12,929][INFO ][o.w.a.d.Dictionary       ] [bigdata01] [Dict Loading] http://bigdata04:8080/hot.dic
    [2027-03-09T18:43:12,934][INFO ][o.w.a.d.Dictionary       ] [bigdata01] 测试
    [2027-03-09T18:43:12,935][INFO ][o.w.a.d.Dictionary       ] [bigdata01] 北京雾霾
    [2027-03-09T18:43:12,935][INFO ][o.w.a.d.Dictionary       ] [bigdata01] reload ik dict finished.
    

    再对北京雾霾这个词语进行分词

    [es@bigdata01 elasticsearch-7.13.4]$ curl -H "Content-Type: application/json" -XPOST  'http://bigdata01:9200/test/_analyze?pretty' -d '{"text":"北京雾霾","tokenizer":"ik_max_word"}'
    {
      "tokens" : [
        {
          "token" : "北京雾霾",
          "start_offset" : 0,
          "end_offset" : 4,
          "type" : "CN_WORD",
          "position" : 0
        },
        {
          "token" : "北京",
          "start_offset" : 0,
          "end_offset" : 2,
          "type" : "CN_WORD",
          "position" : 1
        },
        {
          "token" : "雾",
          "start_offset" : 2,
          "end_offset" : 3,
          "type" : "CN_CHAR",
          "position" : 2
        },
        {
          "token" : "霾",
          "start_offset" : 3,
          "end_offset" : 4,
          "type" : "CN_CHAR",
          "position" : 3
        }
      ]
    }
    

    注意:默认情况下,最多一分钟之内就可以识别到新增的词语。
    通过查看es-ik插件的源码可以发现
    https://github.com/medcl/elasticsearch-analysis-ik/blob/master/src/main/java/org/wltea/analyzer/dic/Monitor.java
    在这里插入图片描述

1.7 ES 查询详解

在ES中查询单条数据可以使用Get,想要查询一批满足条件的数据的话,就需要使用Search了。
下面来看一个案例,查询索引库中的所有数据,代码如下:

package cn.git.es;

import org.apache.http.HttpHost;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;

/**
 * Search详解
 * Created by lixuchun
 */
public class EsSearchOp {
    public static void main(String[] args) throws Exception{
        // 获取RestClient连接
        RestHighLevelClient client = new RestHighLevelClient(
                RestClient.builder(
                        new HttpHost("bigdata01", 9200, "http"),
                        new HttpHost("bigdata02", 9200, "http"),
                        new HttpHost("bigdata03", 9200, "http")));


        SearchRequest searchRequest = new SearchRequest();
        // 指定索引库,支持指定一个或者多个,也支持通配符,例如:user*
        searchRequest.indices("user");
        // 执行查询操作
        SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);

        // 获取查询返回的结果
        SearchHits hits = searchResponse.getHits();
        // 获取数据总量
        long numHits = hits.getTotalHits().value;
        System.out.println("数据总数:"+numHits);
        // 获取具体内容
        SearchHit[] searchHits = hits.getHits();
        // 迭代解析具体内容
        for (SearchHit hit : searchHits) {
            String sourceAsString = hit.getSourceAsString();
            System.out.println(sourceAsString);
        }

        //关闭连接
        client.close();
    }
}

在执行代码之前先初始化数据:

[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/user/_doc/1' -d '{"name":"tom","age":20}'
[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/user/_doc/2' -d '{"name":"tom","age":15}'
[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/user/_doc/3' -d '{"name":"jack","age":17}'
[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/user/_doc/4' -d '{"name":"jess","age":19}'
[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/user/_doc/5' -d '{"name":"mick","age":23}'
[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/user/_doc/6' -d '{"name":"lili","age":12}'
[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/user/_doc/7' -d '{"name":"john","age":28}'
[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/user/_doc/8' -d '{"name":"jojo","age":30}'
[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/user/_doc/9' -d '{"name":"bubu","age":16}'
[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/user/_doc/10' -d '{"name":"pig","age":21}'
[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/user/_doc/11' -d '{"name":"mary","age":19}'

在IDEA中执行代码,可以看到下面结果:
显示数据总数有11条,但是下面的明细内容只有10条,这是因为ES默认只会返回10条数据,如果默认返回所有满足条件的数据,对ES的压力就比较大了。

数据总数:11
{"name":"tom","age":20}
{"name":"tom","age":15}
{"name":"jack","age":17}
{"name":"jess","age":19}
{"name":"mick","age":23}
{"name":"lili","age":12}
{"name":"john","age":28}
{"name":"jojo","age":30}
{"name":"bubu","age":16}
{"name":"pig","age":21}
1.7.1 searchType详解

ES在查询数据的时候可以指定searchType,也就是搜索类型

// 指定searchType
searchRequest.searchType(SearchType.QUERY_THEN_FETCH);

searchType之前是可以指定为下面这4种:
在这里插入图片描述
其中QUERY AND FETCHDFS QUERY AND FETCH这两种searchType现在已经不支持了。

这4种搜索类型到底有什么区别,下面我们来详细分析一下:

在具体分析这4种搜索类型的区别之前,我们先分析一下分布式搜索的背景:
ES天生就是为分布式而生的,但分布式有分布式的缺点,比如要搜索某个单词,但是数据却分别在5个分片(Shard)上面,这5个分片可能在5台主机上面。因为全文搜索天生就要排序(按照匹配度进行排名),但数据却在5个分片上,如何得到最后正确的排序呢?ES是这样做的,大概分两步。

  • 第1步:ES客户端将会同时向5个分片发起搜索请求。
  • 第2步:这5个分片基于本分片的内容独立完成搜索,然后将符合条件的结果全部返回。

大致流程如下图所示:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
然而这其中有两个问题。

  • 第一:数量问题。比如,用户需要搜索"衣服",要求返回符合条件的前10条。但在5个分片中,可能都存储着衣服相关的数据。所以ES会向这5个分片都发出查询请求,并且要求每个分片都返回符合条件的10条记录。这种情况,ES中5个分片最多会收到10*5=50条记录,这样返回给用户的结果数量会多于用户请求的数量。

  • 第二:排名问题。上面说的搜索,每个分片计算符合条件的前10条数据都是基于自己分片的数据进行打分计算的。计算分值使用的词频和文档频率等信息都是基于自己分片的数据进行的,而ES进行整体排名是基于每个分片计算后的分值进行排序的(相当于打分依据就不一样,最终对这些数据统一排名的时候就不准确了),这就可能会导致排名不准确的问题。如果我们想更精确的控制排序,应该先将计算排序和排名相关的信息(词频和文档频率等打分依据)从5个分片收集上来,进行统一计算,然后使用整体的词频和文档频率为每个分片中的数据进行打分,这样打分依据就一样了。

再举个例子解释一下【排名问题】
假设某学校有一班和二班两个班级。期末考试之后,学校要给全校前十名学员发奖金。但是一班和二班考试的时候使用的不是一套试卷。

  • 一班:使用的是A卷【A卷偏容易】
  • 二班:使用的是B卷【B卷偏难】

结果就是一班的最高分是100分,最低分是80分。二班的最高分是70分,最低分是30分。这样全校前十名就都是一班的学员了。

这显然是不合理的。因为一班和二班的试卷难易程度不一样,也就是打分依据不一样,所以不能放在一块排名,这个就解释了刚才的排名问题。如果想要保证排名准确的话,需要保证一班和二班使用的试卷内容一样。可以这样做,把A卷和B卷的内容组合到一块,作为C卷。

一班和二班考试都使用C卷,这样他们的打分依据就一样了,最终再根据所有学员的成绩排名求前十名就准确合理了。

这两个问题,ES也没有什么较好的解决方法,最终把选择的权利交给用户,方法就是在搜索的时候指定searchType

  1. QUERY AND FETCH
    向索引的所有分片都发出查询请求,各分片返回的时候把元素文档(document)和计算后的排名信息一起返回。
    这种搜索方式是最快的。因为相比下面的几种搜索方式,这种查询方法只需要去分片查询一次。但是各个分片返回的结果的数量之和可能是用户要求的数据量的N倍。
    优点:
    只需要查询一次
    缺点:
    返回的数据量不准确,可能返回(N*分片数量)的数据
    并且数据排名也不准确
  2. QUERY THEN FETCH(ES默认的搜索方式
    如果你搜索时,没有指定搜索方式,就是使用的这种搜索方式。这种搜索方式,大概分两个步骤,
    第一步,先向所有的分片发出请求,各分片只返回文档id(注意,不包括文档document)和排名相关的信息(也就是文档对应的分值),然后按照各分片返回的文档的分数进行重新排序和排名,取前size个文档。
    第二步,根据文档id去相关的分片取文档。这种方式返回的文档数量与用户要求的数量是相等的。
    优点:
    返回的数据量是准确的
    缺点:
    性能一般,
    并且数据排名不准确
  3. DFS QUERY AND FETCH
    这种方式比第一种方式多了一个DFS步骤,有这一步,可以更精确控制搜索打分和排名。
    也就是在进行查询之前,先对所有分片发送请求,把所有分片中的词频和文档频率等打分依据全部汇总到一块,再执行后面的操作、
    优点:
    数据排名准确
    缺点:
    性能一般
    返回的数据量不准确,可能返回(N*分片数量)的数据
  4. DFS QUERY THEN FETCH
    比第2种方式多了一个DFS步骤。
    也就是在进行查询之前,先对所有分片发送请求,把所有分片中的词频和文档频率等打分依据全部汇总到一块,再执行后面的操作、
    优点:
    返回的数据量是准确的
    数据排名准确
    缺点:
    性能最差【这个最差只是表示在这四种查询方式中性能最慢,也不至于不能忍受,如果对查询性能要求不是非常高,而对查询准确度要求比较高的时候可以考虑这个】

DFS是一个什么样的过程?

DFS其实就是在进行真正的查询之前,先把各个分片的词频率和文档频率收集一下,然后进行词搜索的时候,各分片依据全局的词频率和文档频率进行搜索和排名。显然如果使用DFS_QUERY_THEN_FETCH这种查询方式,效率是最低的,因为一个搜索,可能要请求3次分片。但使用DFS方法,搜索精度是最高的。

总结一下,从性能考虑QUERY_AND_FETCH是最快的,DFS_QUERY_THEN_FETCH是最慢的。从搜索的准确度来说,DFS要比非DFS的准确度更高。

目前官方舍弃了QUERY AND FETCH和DFS QUERY AND FETCH这两种类型,保留了QUERY THEN FETCH和DFS QUERY THEN FETCH,这两种都是可以保证数据量是准确的。如果对查询的精确度要求没那么高,就使用QUERY THEN FETCH,如果对查询数据的精确度要求非常高,就使用DFS QUERY THEN FETCH

1.7.2 ES 查询扩展

在查询数据的时候可以在searchRequest中指定一些参数,实现过滤、分页、排序、高亮等功能

1.7.2.1 过滤

首先看一下如何在查询的时候指定过滤条件
核心代码如下:

//指定查询条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// 查询所有,可以不指定,默认就是查询索引库中的所有数据
//searchSourceBuilder.query(QueryBuilders.matchAllQuery());
// 对指定字段的值进行过滤,注意:在查询数据的时候会对数据进行分词
// 如果指定多个query,后面的query会覆盖前面的query
// 针对字符串类型内容的查询,不支持通配符
//searchSourceBuilder.query(QueryBuilders.matchQuery("name","tom"));
//searchSourceBuilder.query(QueryBuilders.matchQuery("age","17"));//针对age的值,这里可以指定字符串或者数字都可以
// 针对字符串类型内容的查询,支持通配符,但是性能较差,可以认为是全表扫描
//searchSourceBuilder.query(QueryBuilders.wildcardQuery("name","t*"));
// 区间查询,主要针对数据类型,可以使用from+to 或者gt,gte+lt,lte
//searchSourceBuilder.query(QueryBuilders.rangeQuery("age").from(0).to(20));
//searchSourceBuilder.query(QueryBuilders.rangeQuery("age").gte(0).lte(20));
// 不限制边界,指定为null即可
//searchSourceBuilder.query(QueryBuilders.rangeQuery("age").from(0).to(null));
// 同时指定多个条件,条件之间的关系支持and(must)、or(should)
//searchSourceBuilder.query(QueryBuilders.boolQuery().should(QueryBuilders.matchQuery("name","tom")).should(QueryBuilders.matchQuery("age",19)));
// 多条件组合查询的时候,可以设置条件的权重值,将满足高权重值条件的数据排到结果列表的前面
//searchSourceBuilder.query(QueryBuilders.boolQuery().should(QueryBuilders.matchQuery("name","tom").boost(1.0f)).should(QueryBuilders.matchQuery("age",19).boost(5.0f)));
// 对多个指定字段的值进行过滤,注意:多个字段的数据类型必须一致,否则会报错,如果查询的字段不存在不会报错
//searchSourceBuilder.query(QueryBuilders.multiMatchQuery("tom","name","tag"));
// 这里通过queryStringQuery可以支持Lucene的原生查询语法,更加灵活,注意:AND、OR、TO之类的关键字必须大写
//searchSourceBuilder.query(QueryBuilders.queryStringQuery("name:tom AND age:[15 TO 30]"));
//searchSourceBuilder.query(QueryBuilders.boolQuery().must(QueryBuilders.matchQuery("name","tom")).must(QueryBuilders.rangeQuery("age").from(15).to(30)));
//queryStringQuery支持通配符,但是性能也是比较差
//searchSourceBuilder.query(QueryBuilders.queryStringQuery("name:t*"));
// 精确查询,查询的时候不分词,针对人名、手机号、主机名、邮箱号码等字段的查询时一般不需要分词
// 初始化一条测试数据name=刘德华,默认情况下在建立索引的时候刘德华 会被切分为刘、德、华这三个词
// 所以这里精确查询是查不出来的,使用matchQuery是可以查出来的
//searchSourceBuilder.query(QueryBuilders.matchQuery("name","刘德华"));
//searchSourceBuilder.query(QueryBuilders.termQuery("name","刘德华"));
// 正常情况下想要使用termQuery实现精确查询的字段不能进行分词
// 但是有时候会遇到某个字段已经分词建立索引了,后期还想要实现精确查询
// 重新建立索引也不现实,怎么办呢?
//searchSourceBuilder.query(QueryBuilders.queryStringQuery("name:\"刘德华\""));
//matchQuery默认会根据分词的结果进行 or 操作,满足任意一个词语的数据都会查询出来
//searchSourceBuilder.query(QueryBuilders.matchQuery("name","刘德华"));
// 如果想要对matchQuery的分词结果实现and操作,可以通过operator进行设置
// 这种方式也可以解决某个字段已经分词建立索引了,后期还想要实现精确查询的问题(间接实现,其实是查询了满足刘、德、华这三个词语的内容)
//searchSourceBuilder.query(QueryBuilders.matchQuery("name","刘德华").operator(Operator.AND));

默认情况下ES会对刘德华这个词语进行分词,效果如下(使用的默认分词器):

[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST  'http://bigdata01:9200/emp/_analyze?pretty' -d '{"text":"刘德华"}'      
{
  "tokens" : [
    {
      "token" : "刘",
      "start_offset" : 0,
      "end_offset" : 1,
      "type" : "<IDEOGRAPHIC>",
      "position" : 0
    },
    {
      "token" : "德",
      "start_offset" : 1,
      "end_offset" : 2,
      "type" : "<IDEOGRAPHIC>",
      "position" : 1
    },
    {
      "token" : "华",
      "start_offset" : 2,
      "end_offset" : 3,
      "type" : "<IDEOGRAPHIC>",
      "position" : 2
    }
  ]
}

初始化数据:

[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/user/_doc/12' -d '{"name":"刘德华","age":60}'
[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/user/_doc/13' -d '{"name":"刘老二","age":20}'
1.7.2.2 分页

ES每次返回的数据默认最多是10条,可以认为是一页的数据,这个数据量是可以控制的。核心代码如下:

// 分页
// 设置每页的起始位置,默认是0
//searchSourceBuilder.from(0);

// 设置每页的数据量,默认是10
//searchSourceBuilder.size(10);
1.7.2.3 排序

在返回满足条件的结果之前,可以按照指定的要求对数据进行排序,默认是按照搜索条件的匹配度返回数据的。核心代码如下:

// 排序
// 按照age字段,倒序排序
//searchSourceBuilder.sort("age", SortOrder.DESC);
// 注意:age字段是数字类型,不需要分词,name字段是字符串类型(Text),默认会被分词,所以不支持排序和聚合操作
// 如果想要根据这些会被分词的字段进行排序或者聚合,需要指定使用他们的keyword类型,这个类型表示不会对数据分词
//searchSourceBuilder.sort("name.keyword", SortOrder.DESC);
//keyword类型的特性其实也适用于精确查询的场景,可以在matchQuery中指定字段的keyword类型实现精确查询,不管在建立索引的时候有没有被分词都不影响使用
//searchSourceBuilder.query(QueryBuilders.matchQuery("name.keyword", "刘德华"));
1.7.2.4 高亮

针对用户搜索时的关键词,如果匹配到了,最终在页面展现的时候可以标红高亮显示,看起来比较清晰。
设置高亮的核心代码如下:

// 高亮
// 设置高亮字段
HighlightBuilder highlightBuilder = new HighlightBuilder()
        // 支持多个高亮字段,使用多个field方法指定即可
        .field("name");
// 设置高亮字段的前缀和后缀内容
highlightBuilder.preTags("<font color='red'>");
highlightBuilder.postTags("</font>");
searchSourceBuilder.highlighter(highlightBuilder);

解析高亮内容的核心代码如下:

// 迭代解析具体内容
for (SearchHit hit : searchHits) {
    /*String sourceAsString = hit.getSourceAsString();
    System.out.println(sourceAsString);*/
    Map<String, Object> sourceAsMap = hit.getSourceAsMap();
    String name = sourceAsMap.get("name").toString();
    int age = Integer.parseInt(sourceAsMap.get("age").toString());
    // 获取高亮字段内容
    Map<String, HighlightField> highlightFields = hit.getHighlightFields();
    // 获取name字段的高亮内容
    HighlightField highlightField = highlightFields.get("name");
    if(highlightField!=null){
        Text[] fragments = highlightField.getFragments();
        name = "";
        for (Text text : fragments) {
            name += text;
        }
    }
    //获取最终的结果数据
    System.out.println(name+"---"+age);
}

注意:必须要设置查询的字段,否则无法实现高亮。

// 高亮查询name字段
searchSourceBuilder.query(QueryBuilders.matchQuery("name","tom"));
searchSourceBuilder.query(QueryBuilders.matchQuery("name","刘德华"));
1.7.2.5 评分依据(了解)

ES在返回满足条件的数据的时候,按照搜索条件的匹配度返回数据的,匹配度最高的数据排在最前面,这个匹配度其实就是ES中返回结果中的score字段的值。

// 获取数据的匹配度分值,值越大说明和搜索的关键字匹配度越高
float score = hit.getScore();
// 获取最终的结果数据
System.out.println(name+"---"+age+"---"+score);

此时,我们搜索name=刘华 的数据

searchSourceBuilder.query(QueryBuilders.matchQuery("name", "刘华"));

结果如下:

// 数据总数:2
<font color='red'></font><font color='red'></font>---60---2.591636
<font color='red'></font>老二---20---1.0036464

可以看到第一条数据的score分值为2.59,第二条数据的score分值为1.00

score分值具体是如何计算出来的呢?可以通过开启评分依据进行查看详细信息:

// 首先开启评分依据:
// 评分依据,true:开启,false:关闭
searchSourceBuilder.explain(true);

获取评分依据信息:

// 获取Score的评分依据
Explanation explanation = hit.getExplanation();
// 打印评分依据
if(explanation!=null){
    System.out.println(explanation.toString());
}

再执行程序,就可以看到具体的评分依据信息了:

// 数据总数:2
<font color='red'></font><font color='red'></font>---60---2.591636
2.591636 = sum of:
  1.0036464 = weight(name:刘 in 1) [PerFieldSimilarity], result of:
    1.0036464 = score(freq=1.0), computed as boost * idf * tf from:
      2.2 = boost
      1.4552872 = idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:
        3.0 = n, number of documents containing term
        14.0 = N, total number of documents with field
      0.3134796 = tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:
        1.0 = freq, occurrences of term within document
        1.2 = k1, term saturation parameter
        0.75 = b, length normalization parameter
        3.0 = dl, length of field
        1.4285715 = avgdl, average length of field
  1.5879896 = weight(name:华 in 1) [PerFieldSimilarity], result of:
    1.5879896 = score(freq=1.0), computed as boost * idf * tf from:
      2.2 = boost
      2.3025851 = idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:
        1.0 = n, number of documents containing term
        14.0 = N, total number of documents with field
      0.3134796 = tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:
        1.0 = freq, occurrences of term within document
        1.2 = k1, term saturation parameter
        0.75 = b, length normalization parameter
        3.0 = dl, length of field
        1.4285715 = avgdl, average length of field

<font color='red'></font>老二---20---1.0036464
1.0036464 = sum of:
  1.0036464 = weight(name:刘 in 2) [PerFieldSimilarity], result of:
    1.0036464 = score(freq=1.0), computed as boost * idf * tf from:
      2.2 = boost
      1.4552872 = idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:
        3.0 = n, number of documents containing term
        14.0 = N, total number of documents with field
      0.3134796 = tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:
        1.0 = freq, occurrences of term within document
        1.2 = k1, term saturation parameter
        0.75 = b, length normalization parameter
        3.0 = dl, length of field
        1.4285715 = avgdl, average length of field

评分依据这块内容了解即可。

1.7.3 ES中分页的性能问题

在使用ES实现分页查询的时候,不要一次请求过多或者页码过大的结果,这样会对服务器造成很大的压力,因为它们会在返回前排序。

ES是分布式搜索,所以ES客户端的一个查询请求会发送到索引对应的多个分片中,每个分片都会生成自己的排序结果,最后再进行集中排序,以确保最终结果的正确性。

我们假设在搜索一个拥有5个主分片的索引,当我们请求第一页数据的时候,每个分片产生自己前10名,然后将它们返回给请求节点,然后这个请求节点会将收到的50条结果重新排序以产生最终的前10名。

现在想象一下我们如果要获得第1,000页的数据,也就是第10,001到第10,010条数据,每一个分片都会先产生自己的前10,010名,然后请求节点统一处理这50,050条数据,最后再丢弃掉其中的50,040条!

现在我们就明白了,在分布式系统中,大页码请求所消耗的系统资源是呈指数式增长的。这也是为什么网络搜索引擎一般不会提供超过1,000条搜索结果的原因。
例如:百度上的效果。
在这里插入图片描述
当然还有一点原因是后面的搜索结果基本上也不是我们想要的数据了,我们在使用搜索引擎的时候,一般只会看第1页和第2页的数据。

1.7.4 aggregations聚合统计

ES中可以实现基于字段进行分组聚合的统计,聚合操作支持count()、sum()、avg()、max()、min()

下面来看两个案例:

  • 统计相同年龄的学员个数,需求:统计相同年龄的学员个数数据如下所示:
    在这里插入图片描述

  • 首先在ES中初始化这份数据:

    [root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/1' -d'{"name":"tom","age":18}'
    [root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/2' -d'{"name":"jack","age":29}'
    [root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/3' -d'{"name":"jessica","age":18}'
    [root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/4' -d'{"name":"dave","age":19}'
    [root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/5' -d'{"name":"lilei","age":18}'
    [root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/6' -d'{"name":"lili","age":29}'
    

    开发代码:

    package cn.git.es;
    
    import org.apache.http.HttpHost;
    import org.elasticsearch.action.search.SearchRequest;
    import org.elasticsearch.action.search.SearchResponse;
    import org.elasticsearch.client.RequestOptions;
    import org.elasticsearch.client.RestClient;
    import org.elasticsearch.client.RestHighLevelClient;
    import org.elasticsearch.search.aggregations.AggregationBuilders;
    import org.elasticsearch.search.aggregations.bucket.terms.Terms;
    import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
    import org.elasticsearch.search.builder.SearchSourceBuilder;
    
    import java.util.List;
    
    /**
     * 聚合统计:统计相同年龄的学员个数
     * Created by lixuchun
     */
    public class EsAggOp01 {
        public static void main(String[] args) throws Exception{
            // 获取RestClient连接
            RestHighLevelClient client = new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("bigdata01", 9200, "http"),
                            new HttpHost("bigdata02", 9200, "http"),
                            new HttpHost("bigdata03", 9200, "http")));
            SearchRequest searchRequest = new SearchRequest();
            searchRequest.indices("stu");
    
            // 指定查询条件
            SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
            // 指定分组信息,默认是执行count聚合
            TermsAggregationBuilder aggregation = AggregationBuilders.terms("age_term")
                    .field("age");
            searchSourceBuilder.aggregation(aggregation);
    
            searchRequest.source(searchSourceBuilder);
    
            // 执行查询操作
            SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
    
            // 获取分组信息
            Terms terms = searchResponse.getAggregations().get("age_term");
            List<? extends Terms.Bucket> buckets = terms.getBuckets();
            for (Terms.Bucket bucket: buckets) {
                System.out.println(bucket.getKey()+"---"+bucket.getDocCount());
            }
    
            // 关闭连接
            client.close();
        }
    
    }
    
  • 统计每个学员的总成绩,需求:统计每个学员的总成绩,数据如下所示:
    在这里插入图片描述

  • 首先在ES中初始化这份数据:

    [root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/score/_doc/1' -d'{"name":"tom","subject":"chinese","score":59}'
    [root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/score/_doc/2' -d'{"name":"tom","subject":"math","score":89}'
    [root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/score/_doc/3' -d'{"name":"jack","subject":"chinese","score":78}'
    [root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/score/_doc/4' -d'{"name":"jack","subject":"math","score":85}'
    [root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/score/_doc/5' -d'{"name":"jessica","subject":"chinese","score":97}'
    [root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/score/_doc/6' -d'{"name":"jessica","subject":"math","score":68}'
    代码块123456
    

    开发代码:

    package cn.git.es;
    
    import org.apache.http.HttpHost;
    import org.elasticsearch.action.search.SearchRequest;
    import org.elasticsearch.action.search.SearchResponse;
    import org.elasticsearch.client.RequestOptions;
    import org.elasticsearch.client.RestClient;
    import org.elasticsearch.client.RestHighLevelClient;
    import org.elasticsearch.search.aggregations.Aggregation;
    import org.elasticsearch.search.aggregations.AggregationBuilders;
    import org.elasticsearch.search.aggregations.bucket.terms.Terms;
    import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
    import org.elasticsearch.search.aggregations.metrics.Sum;
    import org.elasticsearch.search.builder.SearchSourceBuilder;
    
    import java.util.List;
    
    /**
     * 聚合统计:统计每个学员的总成绩
     * Created by lixuchun
     */
    public class EsAggOp02 {
        public static void main(String[] args) throws Exception{
            // 获取RestClient连接
            RestHighLevelClient client = new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("bigdata01", 9200, "http"),
                            new HttpHost("bigdata02", 9200, "http"),
                            new HttpHost("bigdata03", 9200, "http")));
            SearchRequest searchRequest = new SearchRequest();
            searchRequest.indices("score");
    
            // 指定查询条件
            SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
            // 指定分组和求sum
            TermsAggregationBuilder aggregation = AggregationBuilders.terms("name_term")
    				// 指定分组字段,如果是字符串(Text)类型,则需要指定使用keyword类型
    				.field("name.keyword")
    				// 指定求sum,也支持avg、min、max等操作
                    .subAggregation(AggregationBuilders.sum("sum_score").field("score"));
            searchSourceBuilder.aggregation(aggregation);
    
            searchRequest.source(searchSourceBuilder);
    
            // 执行查询操作
            SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
    
            // 获取分组信息
            Terms terms = searchResponse.getAggregations().get("name_term");
            List<? extends Terms.Bucket> buckets = terms.getBuckets();
            for (Terms.Bucket bucket: buckets) {
                // 获取sum聚合的结果
                Sum sum = bucket.getAggregations().get("sum_score");
                System.out.println(bucket.getKey()+"---"+sum.getValue());
            }
    
            // 关闭连接
            client.close();
        }
    }
    
1.7.5 aggregations获取所有分组数据

默认情况下,ES只会返回10个分组的数据,如果分组之后的结果超过了10组,如何解决?

可以通过在聚合操作中使用size方法进行设置,获取指定个数的数据组或者获取所有的数据组。在案例1的基础上再初始化一批测试数据:

[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/61' -d'{"name":"s1","age":31}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/62' -d'{"name":"s2","age":32}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/63' -d'{"name":"s3","age":33}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/64' -d'{"name":"s4","age":34}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/65' -d'{"name":"s5","age":35}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/66' -d'{"name":"s6","age":36}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/67' -d'{"name":"s7","age":37}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/68' -d'{"name":"s8","age":38}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/69' -d'{"name":"s9","age":39}'

支持案例1的代码,查看返回的分组个数:

18---3
29---2
19---1
31---1
32---1
33---1
34---1
35---1
36---1
37---1

发现结果中返回的分组个数是10个,没有全部都显示出来,这个其实和分页也没关系,尝试增加分页的代码发现也是无效的:

// 增加分页参数,注意:分页参数针对分组数据是无效的。
searchSourceBuilder.from(0).size(20);

执行案例1的代码,结果发现还是10条数据。

18---3
29---2
19---1
31---1
32---1
33---1
34---1
35---1
36---1
37---1

通过在聚合操作上使用size方法进行设置:

TermsAggregationBuilder aggregation = AggregationBuilders.terms("age_term")
        .field("age")
		// 获取指定分组个数的数据
    	.size(20);

执行案例1的代码:

18---3
29---2
19---1
31---1
32---1
33---1
34---1
35---1
36---1
37---1
38---1
39---1

此时可以获取到所有分组的数据,因为结果一共有12个分组,在代码中通过size设置最多可以获取到20个分组的数据。

如果前期不确定到底有多少个分组的数据,还想获取到所有分组的数据,此时可以在size中设置一个Integer的最大值,这样基本上就没什么问题了,但是注意:如果最后的分组个数太多,会给ES造成比较大的压力,所以官方在这做了限制,让用户手工指定获取多少分组的数据。

TermsAggregationBuilder aggregation = AggregationBuilders.terms("age_term")
        .field("age")
    	// 获取指定分组个数的数据
        .size(Integer.MAX_VALUE);

注意:在ES7.x版本之前,想要获取所有的分组数据,只需要在size中指定参数为0即可。现在ES7.x版本不支持这个数值了。

1.8 ES的高级特性

1.8.1 ES中的settings

ES中的settings可以设置索引库的一些配置信息,主要是针对分片数量和副本数量,其中分片数量只能在一开始创建索引库的时候指定,后期不能修改。副本数量可以随时修改。

首先查看一下ES中目前已有的索引库的默认settings信息。

[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/emp/_settings?pretty'
{
  "emp" : {
    "settings" : {
      "index" : {
        "routing" : {
          "allocation" : {
            "include" : {
              "_tier_preference" : "data_content"
            }
          }
        },
        "number_of_shards" : "1",
        "provided_name" : "emp",
        "creation_date" : "1803648122805",
        "number_of_replicas" : "1",
        "uuid" : "kBpwz6kAQ2eS0uCISVcaew",
        "version" : {
          "created" : "7130499"
        }
      }
    }
  }
}

此时分片和副本数量默认都是1。尝试手工指定分片和副本数量。针对不存在的索引,在创建的时候可以同时指定分片(5)和副本(1)数量:

[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPUT 'http://bigdata01:9200/test1/' -d'{"settings":{"number_of_shards":5,"number_of_replicas":1}}'

查看这个索引库的settings信息:

[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/test1/_settings?pretty'
{
  "test1" : {
    "settings" : {
      "index" : {
        "routing" : {
          "allocation" : {
            "include" : {
              "_tier_preference" : "data_content"
            }
          }
        },
        "number_of_shards" : "5",
        "provided_name" : "test1",
        "creation_date" : "1804844538706",
        "number_of_replicas" : "1",
        "uuid" : "WEUwvKVoRzWfna-KFntdqQ",
        "version" : {
          "created" : "7130499"
        }
      }
    }
  }
}

针对已存在的索引,只能通过settings指定副本信息。将刚才创建的索引的副本数量修改为0

[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPUT 'http://bigdata01:9200/test1/_settings' -d'{"index":{"number_of_replicas":0}}'

查看这个索引库目前的settings信息:

[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/test1/_settings?pretty'
{
  "test1" : {
    "settings" : {
      "index" : {
        "routing" : {
          "allocation" : {
            "include" : {
              "_tier_preference" : "data_content"
            }
          }
        },
        "number_of_shards" : "5",
        "provided_name" : "test1",
        "creation_date" : "1804844538706",
        "number_of_replicas" : "0",
        "uuid" : "WEUwvKVoRzWfna-KFntdqQ",
        "version" : {
          "created" : "7130499"
        }
      }
    }
  }
}
1.8.2 ES中的mapping

mapping表示索引库中数据的字段类型信息,类似于MySQL中的表结构信息。一般不需要手工指定mapping,因为ES会自动根据数据格式识别它的类型

如果你需要对某些字段添加特殊属性(例如:指定分词器),就必须手工指定字段的mapping。

下面先来看一下ES中的常用数据类型:
在这里插入图片描述

  • 字符串:支持text和keyword类型
    text类型支持分词,支持模糊、精确查询,但是不支持聚合和排序操作,text类型不限制存储的内容长度,适合大字段存储。
    https://www.elastic.co/guide/en/elasticsearch/reference/7.13/text.html
    keyword类型不支持分词,会直接对数据建立索引,支持模糊、精确查询,支持聚合和排序操作。keyword类型最大支持存储内容长度为32766个UTF-8类型的字符,可以通过设置ignore_above参数指定某个字段最大支持的字符长度,超过给定长度后的数据将不被索引,此时就无法通过termQuery精确查询返回结果了。keyword类型适合存储手机号、姓名等不需要分词的数据。
    https://www.elastic.co/guide/en/elasticsearch/reference/7.13/keyword.html
  • 数字:最常用的就是long和double了,整数使用long、小数使用double。当然也支持integer、short、byte、float这些数据类型。
    https://www.elastic.co/guide/en/elasticsearch/reference/7.13/number.html
  • 日期:最常用的就是date类型了,date类型可以支持到毫秒,如果特殊情况下需要精确到纳秒需要使用date_nanos这个类型。
    其中date日期类型可以自定义日期格式,通过format参数指定:
    {“type”:“date”,“format”:“yyyy-MM-dd”}
    https://www.elastic.co/guide/en/elasticsearch/reference/7.13/date.html
  • 布尔型:支持true或者false。
  • 二进制:该字段存储编码为Base64字符串的二进制值。如果想要存储图片,可以存储图片地址,或者图片本身,存储图片本身的话就需要获取图片的二进制值进行存储了。
    https://www.elastic.co/guide/en/elasticsearch/reference/7.13/binary.html

ES中还支持一些其他数据类型,感兴趣的话可以到文档里面看一下:
https://www.elastic.co/guide/en/elasticsearch/reference/7.13/mapping-types.html

下面查询一下目前已有的索引库score的mapping信息:

[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/score/_mapping?pretty'
{
  "score" : {
    "mappings" : {
      "properties" : {
        "name" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "score" : {
          "type" : "long"
        },
        "subject" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        }
      }
    }
  }
}

通过返回的mapping信息可以看到score这个索引库里面有3个字段,name、score和subject

  1. nametext类型,其中还通过fields属性指定了一个keyword类型,表示name字段会按照text类型和keyword类型存储2份。
    针对fields属性的解释官网里面有详细介绍,见下图:
    大致意思是说,一个字段可以设置多种数据类型,这样ES会按照多种数据类型的特性对这个字段进行存储和建立索引。
    在这里插入图片描述
    "ignore_above" : 256,表示keyword类型最大支持的字符串长度是256
    ES默认会把字符串类型的数据同时指定text类型和keyword类型。
    想要实现分词检索的时候需要使用text类型,在代码层面直接指定这个name字段就表示使用text类型。
    想要实现精确查询的时候需要使用keyword类型,在代码层面指定name.keyword表示使用namekeyword类型。

    其实前面我们在讲排序的时候也用到了这个keyword类型的特性,因为直接指定name字段会使用text类型,text类型不支持排序和聚合,所以使用的是name.keyword

  • score是long类型,整数默认会被识别为long类型。

  • subject也是text类型,和name是一样的。

    注意:ES 7.x版本之前字符串默认只会被识别为text类型,不会附加一个keyword类型。

下面我们首先操作一个不存在的索引库的mapping信息:指定name为text类型,并且使用ik分词器。age为integer类型。

[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPUT 'http://bigdata01:9200/test2' -d'{"mappings":{"properties":{"name":{"type":"text","analyzer": "ik_max_word"},"age":{"type":"integer"}}}}'

查看这个索引库的mapping信息。

[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/test2/_mapping?pretty'
{
  "test2" : {
    "mappings" : {
      "properties" : {
        "age" : {
          "type" : "integer"
        },
        "name" : {
          "type" : "text",
          "analyzer" : "ik_max_word"
        }
      }
    }
  }
}

在这个已存在的索引库中增加mapping信息。

注意:只能新增字段,不能修改已有字段的类型,否则会报错。

[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/test2/_mapping' -d'{"properties":{"age":{"type":"long"}}}'
{"error":{"root_cause":[{"type":"illegal_argument_exception","reason":"mapper [age] cannot be changed from type [integer] to [long]"}],"type":"illegal_argument_exception","reason":"mapper [age] cannot be changed from type [integer] to [long]"},"status":400}

可以假设一下,假设支持修改已有字段的类型,之前name是text类型,如果我修改为long类型,这样就会出现矛盾了,所以ES不支持修改已有字段的类型。
在已存在的索引库中增加一个flag字段,类型为boolean类型

[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/test2/_mapping' -d'{"properties":{"flag":{"type":"boolean"}}}'

查看索引库的最新mapping信息。

[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/test2/_mapping?pretty'
{
  "test2" : {
    "mappings" : {
      "properties" : {
        "age" : {
          "type" : "integer"
        },
        "flag" : {
          "type" : "boolean"
        },
        "name" : {
          "type" : "text",
          "analyzer" : "ik_max_word"
        }
      }
    }
  }
}
1.8.3 ES的偏好查询(分片查询方式)

ES中的索引数据最终都是存储在分片里面的,分片有多个,并且分片还分为主分片和副本分片。

ES在查询索引库中的数据时,具体是到哪些分片里面查询数据呢?

在具体分析这个之前,我们先分析一下ES的分布式查询过程:
在这里插入图片描述
这个表示是一个3个节点的ES集群,集群内有一个索引库,索引库里面有P0P1两个主分片,这两个主分片分别都有2个副本分片,R0R1

查询过程主要包含三个步骤:

  1. 客户端发送一个查询 请求到 Node 3 , Node 3 会创建一个空优先队列,主要为了存储结果数据。
  2. Node 3 将查询请求转发到索引的主分片或副本分片中。每个分片在本地执行查询并将查询到的结果添加到本地的有序优先队列中。
    具体这里面Node 3 将查询请求转发到索引的哪个分片中,可以是随机的,也可以由我们程序员来控制。
    默认是randomize across shards:表示随机从分片中取数据。
  3. 每个分片返回各自优先队列中所有文档的 ID 和排序值给到 Node 3 ,Node 3合并这些值到自己的优先队列中来产生一个全局排序后的结果列表。

注意:当客户端的一个搜索请求被发送到某个节点时,这个节点就变成了协调节点。 这个节点的任务是广播查询请求到所有相关分片,并将它们查询到的结果整合成全局排序后的结果集合,这个结果集合会返回给客户端。
这里面的Node3节点其实就是协调节点。

接下来我们来具体分析一下如何控制查询请求到分片之间的分发规则:先创建一个具有5个分片,0个副本的索引库,分片太少不好验证效果。

[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPUT 'http://bigdata01:9200/pre/' -d'{"settings":{"number_of_shards":5,"number_of_replicas":0}}'

在这里插入图片描述
在索引库中初始化一批测试数据:

[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/pre/_doc/1' -d'{"name":"tom","age":18}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/pre/_doc/2' -d'{"name":"jack","age":29}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/pre/_doc/3' -d'{"name":"jessica","age":18}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/pre/_doc/4' -d'{"name":"dave","age":19}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/pre/_doc/5' -d'{"name":"lilei","age":18}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/pre/_doc/6' -d'{"name":"lili","age":29}'

这些测试数据在索引库的分片中的分布情况是这样的:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在代码层面通过preference(...)来设置具体的分片查询方式:

// 指定分片查询方式
// 默认随机
searchRequest.preference();
  • _local:表示查询操作会优先在本地节点(协调节点)的分片中查询,没有的话再到其它节点中查询。

这种方式可以提高查询性能,假设一个索引库有5个分片,这5个分片都在Node3节点里面,客户端的查询请求正好也分配到了Node3节点上,这样在查询这5个分片的数据就都在Node3节点上进行查询了,每个分片返回结果数据的时候就不需要跨网络传输数据了,可以节省网络传输的时间。
但是这种方式也会有弊端,如果这个节点在某一时刻接收到的查询请求比较多的时候,会对当前节点造成比较大的压力,因为这些查询请求都会优先查询这个节点上的分片数据。

searchRequest.preference("_local");

此时是可以查到所有数据的。

数据总数:6
{"name":"jessica","age":18}
{"name":"lilei","age":18}
{"name":"dave","age":19}
{"name":"jack","age":29}
{"name":"lili","age":29}
{"name":"tom","age":18}
  • _only_local:表示查询只会在本地节点的分片中查询。
searchRequest.preference("_only_local");

这种方式只会在查询请求所在的节点上进行查询,查询速度比较快,但是数据可能不完整,因为我们无法保证索引库的分片正好都在这一个节点上。

数据总数:2
{"name":"dave","age":19}
{"name":"tom","age":18}

注意:大家在自己本地试验的时候,结果和我的可能不一样,因为请求不一定会分到哪一个节点上面。

  • _only_nodes:表示只在指定的节点中查询。

    可以控制只在指定的节点中查询某一个索引库的分片信息。
    但是注意:这里指定的节点列表里面,必须包含指定索引库的所有分片,如果从这些节点列表中获取到的索引库的分片个数不完整,程序会报错。
    这种情况适用于在某种特殊情况下,集群中的个别节点压力比较大,短时间内又无法恢复,那么我们在查询的时候可以规避掉这些节点,只选择一些正常的节点进行查询。
    前提是索引库的分片有副本,如果没有副本,只有一个主分片,就算主分片的节点压力比较大,那也只能查询这个节点了。

    在这里面需要指定节点ID,节点ID应该如何获取呢?

    通过ES中针对节点信息的RestAPI可以快速获取:http://bigdata01:9200/_nodes?pretty 返回的信息有点多,建议在浏览器中访问。
    在这里插入图片描述
    最终可以获取到三个节点的ID:

bigdata01: l7H4B3pdRm6ckBWd7ODS5Q
bigdata02: nS_RptvTQDuRYTplia24WA
bigdata03: KzjZauWGRt6hJRKTgZ7BcA

注意:这个节点ID是集群随机生成的一个字符串,所以每个人的集群节点ID都是不一样的。

目前这个索引库的分片分布在3个节点上
在这里插入图片描述

  • 在这里可以指定一个或者多个节点ID,多个节点ID之间使用逗号分割即可
    首先指定bigdata01节点的ID

    searchRequest.preference("_only_nodes:KzjZauWGRt6hJRKTgZ7BcA");
    

    注意:执行这个查询会报错,错误提示找不到pre索引库的2号分片的数据,2号分片是在bigdata03节点上

    {"error":{"root_cause":[{"type":"illegal_argument_exception","reason":"no data nodes with criteria [l7H4B3pdRm6ckBWd7ODS5Q] found for shard: [pre][2]"}],"type":"illegal_argument_exception","reason":"no data nodes with criteria [l7H4B3pdRm6ckBWd7ODS5Q] found for shard: [pre][2]"},"status":400}
    

    再把bigdata03的节点ID添加到里面

    searchRequest.preference("_only_nodes:l7H4B3pdRm6ckBWd7ODS5Q,KzjZauWGRt6hJRKTgZ7BcA");
    

    执行发现还是会报错,提示找不到pre索引库的3号分片,3号分片是在bigdata02节点上的

    {"error":{"root_cause":[{"type":"illegal_argument_exception","reason":"no data nodes with criterion [l7H4B3pdRm6ckBWd7ODS5Q,KzjZauWGRt6hJRKTgZ7BcA] found for shard: [pre][3]"}],"type":"illegal_argument_exception","reason":"no data nodes with criterion [l7H4B3pdRm6ckBWd7ODS5Q,KzjZauWGRt6hJRKTgZ7BcA] found for shard: [pre][3]"},"status":400}
    

    再把bigdata02的节点ID添加到里面

    searchRequest.preference("_only_nodes:l7H4B3pdRm6ckBWd7ODS5Q,KzjZauWGRt6hJRKTgZ7BcA,nS_RptvTQDuRYTplia24WA");
    

    此时执行就可以正常执行了:

    数据总数:6
    {"name":"jessica","age":18}
    {"name":"lilei","age":18}
    {"name":"dave","age":19}
    {"name":"jack","age":29}
    {"name":"lili","age":29}
    {"name":"tom","age":18}
    

    注意:在ES7.x版本之前,_only_nodes后面可以只指定某一个索引库部分分片所在的节点信息,如果不完整,不会报错,只是返回的数据是不完整的。

  • _prefer_nodes:表示优先在指定的节点上查询。

    优先在指定的节点上查询索引库分片中的数据
    如果某个节点比较空闲,尽可能的多在这个节点上查询,减轻集群中其他节点的压力,尽可能实现负载均衡。
    这里可以指定一个或者多个节点

    searchRequest.preference("_prefer_nodes:l7H4B3pdRm6ckBWd7ODS5Q");
    

    最终可以查询到完整的结果:

    数据总数:6
    {"name":"jessica","age":18}
    {"name":"lilei","age":18}
    {"name":"dave","age":19}
    {"name":"jack","age":29}
    {"name":"lili","age":29}
    {"name":"tom","age":18}
    
    • _shards:表示只查询索引库中指定分片的数据。

    在查询的时候可以指定只查询索引库中指定分片中的数据,其实有点类似于Hive中的分区表的特性。
    如果我们提前已经知道需要查询的数据都在这个索引库的哪些分片里面,在这里提前指定对应分片编号,这样查询请求就只会到这些分片里面进行查询,这样可以提高查询效率,减轻集群压力。
    可以指定一个或者多个分片编号,分片编号是从0开始的。

    searchRequest.preference("_shards:0,1");
    

    最终可以看到这两个分区里面的数据:

    数据总数:3
    {"name":"jessica","age":18}
    {"name":"lilei","age":18}
    {"name":"dave","age":19}
    

    那我们如何控制将某一类型的数据添加到指定分片呢?后续说明

  • custom-string:自定义一个参数,不能以下划线(_)开头。

    有时候我们希望多次查询使用索引库中相同的分片,因为分片会有副本,正常情况下如果不做控制,那么两次查询的时候使用的分片可能会不一样,第一次查询可能使用的是主分片,第二次查询可能使用的是副本分片。

    大家可能会有疑问,不管是主分片,还是副本分片,这些分片里面的数据是完全一样的,就算两次查询使用的不是相同分片又会有什么问题吗?

    会有问题的!如果searchType使用的是QUERY_THEN_FETCH,此时分片里面的数据在计算打分依据的时候是根据当前节点里面的词频和文档频率,两次查询使用的分片不是同一个,这样就会导致在计算打分依据的时候使用的样本不一致,最终导致两次相同的查询条件返回的结果不一样。
    当然了,如果你使用的是DFS_QUERY_THEN_FETCH就不会有这个问题了,但是DFS_QUERY_THEN_FETCH对性能损耗会大一些,所以并不是所有情况下都使用这种searchType

    通过自定义参数的设置,只要两次查询使用的自定义参数是同一个,这样就可以保证这两次查询使用的分片是一样的,那么这两次查询的结果肯定是一样的。

    注意:自定义参数不能以_开头。

    // 自定义参数
    searchRequest.preference("abc");
    

    查询到的结果还是完整的。

    数据总数:6
    {"name":"jessica","age":18}
    {"name":"lilei","age":18}
    {"name":"dave","age":19}
    {"name":"jack","age":29}
    {"name":"lili","age":29}
    {"name":"tom","age":18}
    
1.8.4 ES中的routing路由功能

ES在添加数据时,会根据id或者routing参数进行hash,得到hash值再与该索引库的分片数量取模,得到的值即为存入的分片编号

如果多条数据使用相同的routing,那么最终计算出来的分片编号都是一样的,那么这些数据就可以存储到相同的分片里面了。

后期查询的只需要到指定分片中查询即可,可以显著提高查询性能。

如果在面试的时候面试官问你如何在ES中实现极速查询,其实就是问这个routing路由功能的。

下面来演示一下:创建一个新的索引库,指定5个分片,0个副本。

[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPUT 'http://bigdata01:9200/rout/' -d'{"settings":{"number_of_shards":5,"number_of_replicas":0}}'

初始化数据:

[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/rout/_doc/1?routing=class1' -d'{"name":"tom","age":18}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/rout/_doc/2?routing=class1' -d'{"name":"jack","age":29}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/rout/_doc/3?routing=class1' -d'{"name":"jessica","age":18}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/rout/_doc/4?routing=class1' -d'{"name":"dave","age":19}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/rout/_doc/5?routing=class1' -d'{"name":"lilei","age":18}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/rout/_doc/6?routing=class1' -d'{"name":"lili","age":29}'

如果是使用的JavaAPI,那么需要通过使用routing函数指定。

private static void addIndexByJson(RestHighLevelClient client) throws IOException {
    IndexRequest request = new IndexRequest("emp");
    request.id("10");
    String jsonString = "{" +
            "\"name\":\"jessic\"," +
            "\"age\":20" +
            "}";
    request.source(jsonString, XContentType.JSON);
    request.routing("class1");
    // 执行
    client.index(request, RequestOptions.DEFAULT);
}

查看数据在分片中的分布情况,发现所有数据都在0号分片里面,说明routing参数生效了。
在这里插入图片描述
通过代码查询的时候,可以通过偏好查询指定只查询0号分片里面的数据。

SearchRequest searchRequest = new SearchRequest();
searchRequest.indices("rout");

//指定分片查询方式
searchRequest.preference("_shards:0");

这样就可以查看所有的数据:

数据总数:6
{"name":"tom","age":18}
{"name":"jack","age":29}
{"name":"jessica","age":18}
{"name":"dave","age":19}
{"name":"lilei","age":18}
{"name":"lili","age":29}

通过偏好查询中的_shard手工指定分片编号在使用的时候不太友好,需要我们单独维护一份数据和分片之间的关系,比较麻烦。
还有一种比较简单常用的方式是在查询的时候设置相同的路由参数,这样就可以快速查询到使用这个路由参数添加的数据了。
底层其实是会计算这个路由参数对应的分片编号,最终到指定的分片中查询数据。

SearchRequest searchRequest = new SearchRequest();
// 指定索引库,支持指定一个或者多个,也支持通配符,例如:user*
searchRequest.indices("rout");

// 指定分片查询方式
//searchRequest.preference("_shards:0");

// 指定路由参数
searchRequest.routing("class1");

结果如下

数据总数:6
{"name":"tom","age":18}
{"name":"jack","age":29}
{"name":"jessica","age":18}
{"name":"dave","age":19}
{"name":"lilei","age":18}
{"name":"lili","age":29}

我们把routing参数修改一下,改为class2

// 指定路由参数
searchRequest.routing("class2");

此时结果如下:

数据总数:0

从这可以看出来,这个routing参数确实生效了。routing机制使用不好可能会导致数据倾斜,就是有的分片里面数据很多,有的分片里面数据很少。

1.8.5 ES的索引库模板(了解)

在实际工作中针对一批大量数据存储的时候需要使用多个索引库,如果手工指定每个索引库的配置信息的话就很麻烦了。
配置信息其实主要就是settings和mapping
可以通过提前创建一个索引库模板,后期在创建索引库的时候,只要索引库的命名符合一定的要求就可以直接套用模板中的配置。

下面看一个案例:首先创建两个索引库模板:
第一个索引库模板:

[root@bigdata01 ~]#curl -H "Content-Type: application/json" -XPUT 'http://bigdata01:9200/_template/t_1' -d '
{
    "template" : "*",
    "order" : 0,
    "settings" : {
        "number_of_shards" : 2
    },
    "mappings" : {
        "properties":{
            "name":{"type":"text"},
            "age":{"type":"integer"}
        }
    }
}

第二个索引库模板:

[root@bigdata01 ~]#curl -H "Content-Type: application/json" -XPUT 'http://bigdata01:9200/_template/t_2' -d '
{
    "template" : "te*",
    "order" : 1,
    "settings" : {
        "number_of_shards" : 3
    },
    "mappings" : {
        "properties":{
            "name":{"type":"text"},
            "age":{"type":"long"}
        }
    }
}

注意:order值大的模板内容会覆盖order值小的。

第一个索引库模板默认会匹配所有的索引库,第二个索引库模板只会匹配索引库名称以te开头的索引库,通过template属性配置的。
如果我们创建的索引库名称满足第二个就会使用第二个模板,不满足的话才会使用第一个模板。
下面创建一个索引库,索引库名称为:test10

[root@bigdata01 ~]# curl  -XPUT 'http://localhost:9200/test10'

查看索引库test10的setting和mapping信息。

[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/test10/_settings?pretty'
{
  "test10" : {
    "settings" : {
      "index" : {
        "routing" : {
          "allocation" : {
            "include" : {
              "_tier_preference" : "data_content"
            }
          }
        },
        "number_of_shards" : "3",
        "provided_name" : "test10",
        "creation_date" : "1804935156129",
        "number_of_replicas" : "1",
        "uuid" : "iJLdIRwQSpagzEtIu0LDEw",
        "version" : {
          "created" : "7130499"
        }
      }
    }
  }
}
[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/test10/_mapping?pretty'
{
  "test10" : {
    "mappings" : {
      "properties" : {
        "age" : {
          "type" : "long"
        },
        "name" : {
          "type" : "text"
        }
      }
    }
  }
}

通过结果可以看出来test10这个索引库使用到了第二个索引库模板。接下来再创建一个索引库,索引库名称为hello

[root@bigdata01 ~]# curl  -XPUT 'http://bigdata01:9200/hello'

查看索引库hello的setting和mapping信息。

[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/hello/_settings?pretty' 
{
  "hello" : {
    "settings" : {
      "index" : {
        "routing" : {
          "allocation" : {
            "include" : {
              "_tier_preference" : "data_content"
            }
          }
        },
        "number_of_shards" : "2",
        "provided_name" : "hello",
        "creation_date" : "1804935301339",
        "number_of_replicas" : "1",
        "uuid" : "xkg-XXSQSHKcJ_5nxyTPTQ",
        "version" : {
          "created" : "7130499"
        }
      }
    }
  }
}
[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/hello/_mapping?pretty'
{
  "hello" : {
    "mappings" : {
      "properties" : {
        "age" : {
          "type" : "integer"
        },
        "name" : {
          "type" : "text"
        }
      }
    }
  }
}

通过结果可以看出来hello这个索引库使用到了第一个索引库模板。后期想要查看索引库模板内容可以这样查看:

[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/_template/t_*?pretty'
{
  "t_1" : {
    "order" : 0,
    "index_patterns" : [
      "*"
    ],
    "settings" : {
      "index" : {
        "number_of_shards" : "2"
      }
    },
    "mappings" : {
      "properties" : {
        "name" : {
          "type" : "text"
        },
        "age" : {
          "type" : "integer"
        }
      }
    },
    "aliases" : { }
  },
  "t_2" : {
    "order" : 1,
    "index_patterns" : [
      "te*"
    ],
    "settings" : {
      "index" : {
        "number_of_shards" : "3"
      }
    },
    "mappings" : {
      "properties" : {
        "name" : {
          "type" : "text"
        },
        "age" : {
          "type" : "long"
        }
      }
    },
    "aliases" : { }
  }
}

想要删除索引库模板可以这样做:

[root@bigdata01 ~]# curl -XDELETE 'http://bigdata01:9200/_template/t_2'
1.8.6 ES的索引库别名(了解)

在工作中使用ES收集应用的运行日志,每个星期创建一个索引库,这样时间长了就会创建很多的索引库,操作和管理的时候很不方便。
由于新增索引数据只会操作当前这个星期的索引库,所以为了使用方便,我们就创建了两个索引库别名:curr_week和last_3_month

  • curr_week:这个别名指向当前这个星期的索引库,新增数据使用这个索引库别名。
  • last_3_month:这个别名指向最近三个月的所有索引库,因为我们的需求是需要查询最近三个月的日志信息。

后期只需要修改这两个别名和索引库之间的指向关系即可,应用层代码不需要任何改动。

下面来演示一下这个案例:
假设ES已经收集了一段时间的日志数据,每一星期都会创建一个索引库,所以目前创建了4个索引库:

[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPUT 'http://bigdata01:9200/log_20260301/' 
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPUT 'http://bigdata01:9200/log_20260308/' 
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPUT 'http://bigdata01:9200/log_20260315/' 
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPUT 'http://bigdata01:9200/log_20260322/' 

分别向每个索引库里面初始化1条测试数据:

[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/log_20260301/_doc/1' -d'{"log":"info->20260301"}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/log_20260308/_doc/1' -d'{"log":"info->20260308"}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/log_20260315/_doc/1' -d'{"log":"info->20260315"}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/log_20260322/_doc/1' -d'{"log":"info->20260322"}'

为了使用方便,我们创建了两个索引库别名:curr_week和last_3_monthcurr_week指向最新的索引库:log_20260322

[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/_aliases' -d '
{
    "actions" : [
        { "add" : { "index" : "log_20260322", "alias" : "curr_week" } }
    ]
}'

last_3_month指向之前3个月内的索引库:可以同时增加多个索引别名。

[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/_aliases' -d '
{
    "actions" : [
        { "add" : { "index" : "log_20260301", "alias" : "last_3_month" } },
        { "add" : { "index" : "log_20260308", "alias" : "last_3_month" } },
        { "add" : { "index" : "log_20260315", "alias" : "last_3_month" } }
    ]
}'

以后使用的时候,想要操作当前星期内的数据就使用curr_week这个索引库别名就行了。
查询一下curr_week里面的数据:这里面就1条数据,和使用索引库log_20260322查询的结果是一样的。

[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/curr_week/_search?pretty'
{
  "took" : 987,
  "timed_out" : false,
  "_shards" : {
    "total" : 2,
    "successful" : 2,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "log_20260322",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "log" : "info->20260322"
        }
      }
    ]
  }
}

再使用last_3_month查询一下数据:这里面返回了log_20260301、log_20260308和log_20260315这3个索引库里面的数据。

[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/last_3_month/_search?pretty'
{
  "took" : 73,
  "timed_out" : false,
  "_shards" : {
    "total" : 6,
    "successful" : 6,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "log_20260301",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "log" : "info->20260301"
        }
      },
      {
        "_index" : "log_20260308",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "log" : "info->20260308"
        }
      },
      {
        "_index" : "log_20260315",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "log" : "info->20260315"
        }
      }
    ]
  }
}

过了一个星期之后,又多了一个新的索引库:log_20260329
创建这个索引库并初始化一条数据

[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPUT 'http://bigdata01:9200/log_20260329/' 
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/log_20260329/_doc/1' -d'{"log":"info->20260329"}'

此时就需要修改curr_week别名指向的索引库了,需要先删除之前的关联关系,再增加新的。
删除curr_weeklog_20260322之间的关联关系。

[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/_aliases' -d '
{
    "actions" : [
        { "remove" : { "index" : "log_20260322", "alias" : "curr_week" } }
    ]
}'

新增curr_weeklog_20260329之间的关联关系

[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/_aliases' -d '
{
    "actions" : [
        { "add" : { "index" : "log_20260329", "alias" : "curr_week" } }
    ]
}'

此时再查询curr_week中的数据其实就是查询索引库log_20260329里面的数据了。

[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/curr_week/_search?pretty'
{
  "took" : 25,
  "timed_out" : false,
  "_shards" : {
    "total" : 2,
    "successful" : 2,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "log_20260329",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "log" : "info->20260329"
        }
      }
    ]
  }
}

然后再把索引库log_20260322添加到last_3_month别名中:

[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/_aliases' -d '
{
    "actions" : [
        { "add" : { "index" : "log_20260322", "alias" : "last_3_month" } }
    ]
}'

这些关联别名映射关系和移除别名映射关系的操作需要写个脚本定时执行,这样就可以实现别名自动关联到指定索引库了。

假设时间长了,我们如果忘记了这个别名下对应的都有哪些索引库,可以使用下面的方法查看一下:

[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/_alias/curr_week?pretty'  
{
  "log_20260329" : {
    "aliases" : {
      "curr_week" : { }
    }
  }
}
[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/_alias/last_3_month?pretty'
{
  "log_20260315" : {
    "aliases" : {
      "last_3_month" : { }
    }
  },
  "log_20260308" : {
    "aliases" : {
      "last_3_month" : { }
    }
  },
  "log_20260301" : {
    "aliases" : {
      "last_3_month" : { }
    }
  },
  "log_20260322" : {
    "aliases" : {
      "last_3_month" : { }
    }
  }
}

如果想知道哪些别名指向了这个索引,可以这样查看:

[root@bigdata01 ~]#  curl -XGET 'http://bigdata01:9200/log_20260301/_alias/*?pretty'
{
  "log_20260301" : {
    "aliases" : {
      "last_3_month" : { }
    }
  }
}

注意:针对3个月以前的索引基本上就很少再使用了,为了减少对ES服务器的性能损耗(主要是内存的损耗),建议把这些长时间不使用的索引库close掉,close之后这个索引库里面的索引数据就不支持读写操作了,close并不会删除索引库里面的数据,后期想要重新读写这个索引库里面的数据的话,可以通过open把索引库打开。

log_20260301索引库close掉:

[root@bigdata01 ~]# curl -XPOST 'http://bigdata01:9200/log_20260301/_close'

此时再查看这个索引库的数据就查询不到了:会提示这个索引库已经被close掉了。

[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/log_20260301/_search?pretty'
{
  "error" : {
    "root_cause" : [
      {
        "type" : "index_closed_exception",
        "reason" : "closed",
        "index_uuid" : "VGfSvKVJRjy5h3aCcsveKQ",
        "index" : "log_20260301"
      }
    ],
    "type" : "index_closed_exception",
    "reason" : "closed",
    "index_uuid" : "VGfSvKVJRjy5h3aCcsveKQ",
    "index" : "log_20260301"
  },
  "status" : 400
}

注意:这些close之后的索引库需要从索引库别名中移除掉,否则会导致无法使用从索引库别名查询数据,因为这个索引库别名中映射的有已经close掉的索引库。

[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/last_3_month/_search?pretty'
{
  "error" : {
    "root_cause" : [
      {
        "type" : "index_closed_exception",
        "reason" : "closed",
        "index_uuid" : "VGfSvKVJRjy5h3aCcsveKQ",
        "index" : "log_20260301"
      }
    ],
    "type" : "index_closed_exception",
    "reason" : "closed",
    "index_uuid" : "VGfSvKVJRjy5h3aCcsveKQ",
    "index" : "log_20260301"
  },
  "status" : 400
}

接下来将log_20260301索引库重新open(打开)。

[root@bigdata01 ~]# curl -XPOST 'http://bigdata01:9200/log_20260301/_open'

索引库open之后就可以查询了:

[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/log_20260301/_search?pretty'
{
  "took" : 5,
  "timed_out" : false,
  "_shards" : {
    "total" : 2,
    "successful" : 2,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "log_20260301",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "log" : "info->20260301"
        }
      }
    ]
  }
}

索引库别名也可以正常使用了:

[root@bigdata01 ~]#  curl -XGET 'http://bigdata01:9200/last_3_month/_search?pretty'
{
  "took" : 12,
  "timed_out" : false,
  "_shards" : {
    "total" : 8,
    "successful" : 8,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 4,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "log_20260301",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "log" : "info->20260301"
        }
      },
      {
        "_index" : "log_20260308",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "log" : "info->20260308"
        }
      },
      {
        "_index" : "log_20260315",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "log" : "info->20260315"
        }
      },
      {
        "_index" : "log_20260322",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "log" : "info->20260322"
        }
      }
    ]
  }
}

索引库close掉之后,虽然对ES服务器没有性能损耗了,但是对ES集群的磁盘占用还是存在的,所以可以根据需求,将一年以前的索引库彻底删除掉。

1.8.7 ES SQL

针对ES中的结构化数据,使用SQL实现聚合统计会很方便,可以减少很多工作量。ES SQL支持常见的SQL语法,包括分组、排序、函数等,但是目前不支持JOIN

在使用的时候可以使用SQL命令行、RestAPI、JDBC、ODBC等方式操作。本地测试的时候使用SQL命令行更加方便。想要实现跨语言调用使用RestAPI更加方便。Java程序员使用JDBC方式更方便。

  • ES SQL命令行下的使用:

    [es@bigdata01 elasticsearch-7.13.4]$ bin/elasticsearch-sql-cli http://bigdata01:9200
    sql> select * from user;
          age      |     name      
    ---------------+---------------
    20             |tom            
    15             |tom            
    17             |jack           
    19             |jess           
    23             |mick           
    12             |lili           
    28             |john           
    30             |jojo           
    16             |bubu           
    21             |pig            
    19             |mary           
    60             |刘德华            
    20             |刘老二
    sql> select * from user where age > 20;
          age      |     name      
    ---------------+---------------
    23             |mick           
    28             |john           
    30             |jojo           
    21             |pig            
    60             |刘德华 
    

    如果想要实现模糊查询,使用sql中的like是否可行?

    sql> select * from user where name like '刘华';                     
          age      |     name      
    ---------------+---------------
    sql> select * from user where name like '刘%';  
          age      |     name      
    ---------------+---------------
    60             |刘德华            
    20             |刘老二            
    

    like这种方式其实就是普通的查询了,无法实现分词查询。
    想要实现分词查询,需要使用match

    sql> select * from user where match(name,'刘华');       
          age      |     name      
    ---------------+---------------
    60             |刘德华            
    20             |刘老二 
    

    退出ES SQL命令行,需要输入exit

    sql> exit;
    Bye!
    
  • RestAPI下ES SQL的使用。

    查询user索引库中的数据,根据age倒序排序,获取前5条数据。

    [root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/_sql?format=txt' -d'
    {
    "query":"select * from user order by age desc limit 5"
    }
    '
          age      |     name      
    ---------------+---------------
    60             |刘德华            
    30             |jojo           
    28             |john           
    23             |mick           
    21             |pig 
    
  • JDBC操作ES SQL。首先添加ES sql-jdbc的依赖。

    <dependency>
        <groupId>org.elasticsearch.plugin</groupId>
        <artifactId>x-pack-sql-jdbc</artifactId>
        <version>7.13.4</version>
    </dependency>
    

    开发代码:

    package com.imooc.es;
    
    import java.sql.Connection;
    import java.sql.DriverManager;
    import java.sql.ResultSet;
    import java.sql.Statement;
    import java.util.Properties;
    
    /**
     * JDBC操作ES SQL
     * Created by lixuchun
     */
    public class EsJdbcOp {
        public static void main(String[] args) throws Exception{
            //指定jdbcUrl
            String jdbcUrl = "jdbc:es://http://bigdata01:9200/?timezone=UTC+8";
            Properties properties = new Properties();
            //获取JDBC连接
            Connection conn = DriverManager.getConnection(jdbcUrl, properties);
            Statement stmt = conn.createStatement();
            ResultSet results = stmt.executeQuery("select name,age from user order by age desc limit 5");
            while (results.next()){
                String name = results.getString(1);
                int age = results.getInt(2);
                System.out.println(name+"--"+age);
            }
    
            //关闭连接
            stmt.close();
            conn.close();
        }
    }
    

    注意:jdbc这种方式目前无法免费使用,需要购买授权。

    Exception in thread "main" java.sql.SQLInvalidAuthorizationSpecException: current license is non-compliant for [jdbc]
    

    所以在工作中常用的是RestAPI这种方式。

1.8.8 ES优化策略
  1. ES中Too many open files的问题。

    ES中的索引数据都是存储在磁盘文件中的,每一条数据在底层都会产生一份索引片段文件,这些索引数据默认的存储目录是在ES安装目录下的data目录里面。

    [es@bigdata01 index]$ pwd
    /usr/soft/elasticsearch-7.13.4/data/nodes/0/indices/28q_rMHAR4GJBImzb40woA/0/index
    [es@bigdata01 index]$ ll
    total 52
    -rw-rw-r--. 1 es es  479 Mar 12 15:21 _0.cfe
    -rw-rw-r--. 1 es es 3302 Mar 12 15:21 _0.cfs
    -rw-rw-r--. 1 es es  363 Mar 12 15:21 _0.si
    -rw-rw-r--. 1 es es  479 Mar 12 15:21 _1.cfe
    -rw-rw-r--. 1 es es 2996 Mar 12 15:21 _1.cfs
    -rw-rw-r--. 1 es es  363 Mar 12 15:21 _1.si
    -rw-rw-r--. 1 es es  923 Mar 12 16:28 _2_1.fnm
    -rw-rw-r--. 1 es es  103 Mar 12 16:28 _2_1_Lucene80_0.dvd
    -rw-rw-r--. 1 es es  160 Mar 12 16:28 _2_1_Lucene80_0.dvm
    -rw-rw-r--. 1 es es  479 Mar 12 16:28 _2.cfe
    -rw-rw-r--. 1 es es 3718 Mar 12 16:28 _2.cfs
    -rw-rw-r--. 1 es es  363 Mar 12 16:28 _2.si
    -rw-rw-r--. 1 es es  533 Mar 12 16:33 segments_4
    -rw-rw-r--. 1 es es    0 Mar 12 15:21 write.lock
    

    注意:路径中的28q_rMHAR4GJBImzb40woA表示是索引库的UUID。
    在这里插入图片描述

  2. ES在查询索引库里面数据的时候需要读取所有的索引片段,如果索引库中数据量比较多,那么ES在查询的时候就需要读取很多索引片段文件,此时可能就会达到Linux系统的极限,因为Linux会限制系统内最大文件打开数。
    这个最大文件打开数的的配置在安装ES集群的时候我们已经修改过了:
    主要就是这些参数:

    [root@bigdata01 soft]# vi /etc/security/limits.conf 
    * soft nofile 65536
    * hard nofile 131072
    * soft nproc 2048
    * hard nproc 4096
    

    理论上来说,不管我们将最大文件打开数修改为多大,在使用的时候都有可能会出问题,因为ES中的数据是越来越多的,那如何解决?
    其实也不用过于担心,因为ES中默认会有一个自动的索引片段合并机制,这样可以保证ES中不会产生过多的索引片段。
    只要是单个索引片段文件小于5G的,在自动索引片段合并机制触发的时候都会进行合并。

  3. 索引合并优化,清除标记为删除状态的索引数据。

    咱们前面分析过,ES中的删除并不是真正的删除,只是会给数据标记一个删除状态,索引片段在合并的时候,是会把索引片段中标记为删除的数据真正删掉,这样也是可以提高性能的,因为标记为删除状态的数据是会参与查询的,只不过会被过滤掉。

    索引片段合并除了可以避免产生Too many open files这个问题,其实它也是可以显著提升查询性能的,因为我们读取一个中等大小的文件肯定是比读取很多个小文件效率更高的

    除了等待自动的索引片段合并,也可以手工执行索引片段合并操作,但是要注意:索引片段合并操作是比较消耗系统IO资源的,不要在业务高峰期执行,也没必要频繁调用,可以每天凌晨执行一次。

    [root@bigdata01 ~]# curl -XPOST 'http://bigdata01:9200/stu/_forcemerge'
    

    合并之后会的索引片段就变成了这样,这些文件其实属于一个索引片段,都是以_3开头的:

    [es@bigdata01 index]$ ll
    total 72
    -rw-rw-r--. 1 es es 158 Mar 14 15:47 _3.fdm
    -rw-rw-r--. 1 es es 527 Mar 14 15:47 _3.fdt
    -rw-rw-r--. 1 es es  64 Mar 14 15:47 _3.fdx
    -rw-rw-r--. 1 es es 922 Mar 14 15:47 _3.fnm
    -rw-rw-r--. 1 es es 202 Mar 14 15:47 _3.kdd
    -rw-rw-r--. 1 es es  69 Mar 14 15:47 _3.kdi
    -rw-rw-r--. 1 es es 200 Mar 14 15:47 _3.kdm
    -rw-rw-r--. 1 es es 159 Mar 14 15:47 _3_Lucene80_0.dvd
    -rw-rw-r--. 1 es es 855 Mar 14 15:47 _3_Lucene80_0.dvm
    -rw-rw-r--. 1 es es  78 Mar 14 15:47 _3_Lucene84_0.doc
    -rw-rw-r--. 1 es es  92 Mar 14 15:47 _3_Lucene84_0.pos
    -rw-rw-r--. 1 es es 305 Mar 14 15:47 _3_Lucene84_0.tim
    -rw-rw-r--. 1 es es  74 Mar 14 15:47 _3_Lucene84_0.tip
    -rw-rw-r--. 1 es es 261 Mar 14 15:47 _3_Lucene84_0.tmd
    -rw-rw-r--. 1 es es  59 Mar 14 15:47 _3.nvd
    -rw-rw-r--. 1 es es 103 Mar 14 15:47 _3.nvm
    -rw-rw-r--. 1 es es 575 Mar 14 15:47 _3.si
    -rw-rw-r--. 1 es es 316 Mar 14 15:47 segments_5
    -rw-rw-r--. 1 es es   0 Mar 12 15:21 write.lock
    

    如果一个索引库中的数据已经非常多了,手工执行索引片段合并操作可能会产生一些非常大的索引片段(超过5G的),如果继续向这个索引库里面写入新的数据,那么ES的自动索引片段合并机制就不会再考虑这些非常大的索引片段了(超过5G的),这样会导致索引库中保留了非常大的索引片段,从而降低搜索性能。

    这种问题该如何解决呢?往下面继续看!

  4. 分片和副本个数调整。

    分片多的话,可以提升建立索引的能力,单个索引库,建议使用5-20个分片比较合适。
    分片数过少或过多,都会降低检索效率。
    分片数过多会导致检索时打开比较多的文件,另外也会导致多台服务器之间的通讯。
    而分片数过少会导致单个分片索引过大,所以检索速度也会慢。
    建议单个分片存储20G左右的索引数据【最高也不要超过50G,否则性能会很差】
    所以,大致有一个公式:分片数量=数据总量/20G

    当数据规模超过单个索引库最大存储能力的时候,只需要新建一个索引库即可,所以ES中的海量数据存储能力是需要依靠多个索引库的,这样就可以解决前面所说的索引库中单个索引片段过大的问题。

    副本多的话,理论上来说可以提升检索的能力,但是如果设置很多副本的话也会对服务器造成额外的压力,因为主分片需要给所有副本分片同步数据,所以建议最多设置1-2个副本即可。

    注意:从ES7.x版本开始,集群中每个节点默认支持最多1000个了片,这块主要是考虑到单个节点的性能问题,如果集群内每个节点的性能都比较强,当然也是支持修改的。

    先查看一下现在集群默认的参数配置:
    现在里面的参数都是空的。

    [root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/_cluster/settings?pretty'
    {
      "persistent" : { },
      "transient" : { }
    }
    

    修改节点支持的最大分片数量:

    [root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPUT 'http://bigdata01:9200/_cluster/settings'  -d '{ "persistent": { "cluster.max_shards_per_node": "10000" } }'
    

    重新查询集群最新的参数配置:

    [root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/_cluster/settings?pretty'
    {
      "persistent" : {
        "cluster" : {
          "max_shards_per_node" : "10000"
        }
      },
      "transient" : { }
    }
    
  5. 初始化数据时,建议将副本数设置为0。

    如果是在项目初期,ES集群刚安装好,需要向里面批量初始化大量数据,此时建议将副本数设置为0,这样是可以显著提高入库效率的。
    如果有副本的话,在批量初始化数据的同时,索引库的主分片还需要负责向副本分片同步数据,这样会影响数据的入库性能。

  6. 针对不使用的index,建议close,减少性能损耗。具体的操作方式在前面讲索引库别名的时候已经讲过了。

  7. 调整ES的JVM内存大小,单个ES实例最大不超过32G

    单个ES实例官方建议最大使用32G内存,如果超过这个内存ES也使用不了,这样会造成资源浪费。
    所以在前期申请ES集群机器的时候,建议单机内存在32G左右即可。
    如果由于历史遗留问题导致每台机器的内存都很大,假设是128G的,如果在这台机器上只部署一个ES实例,会造成内存资源浪费,此时有一种取巧的方式,在同一台机器上部署多个ES实例,只需要让这台机器中的每个ES实例监听不同的端口就行了。
    这样这个128G内存的机器理论上至少可以部署4个ES实例。
    但是这样会存在一个弊端,如果后期这台机器出现了故障,那么ES集群会同时丢失4个节点,可能会丢数据,所以还是尽量避免这种情况。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值