Elasticsearch从入门到实战:核心概念与全平台部署及开发详解

34 阅读29分钟

在当今数据呈爆炸式增长的时代,如何从海量信息中迅速、精准地挖掘出有价值的内容,已成为众多技术领域亟待解决的核心问题。传统的数据存储与检索方式,在面对复杂的全文搜索、实时分析等需求时,往往显得力不从心。正是在这样的背景下,Elasticsearch以其强大的分布式搜索和分析能力,迅速崛起为搜索技术领域的中流砥柱。本文旨在通过对Elasticsearch的全面剖析,从其核心原理到与其他存储方案的深度对比,再到完整的环境部署与开发整合,为读者呈现一条清晰的技术实践路径。

1. Elasticsearch 概述

1.1. Elasticsearch 简介

Elasticsearch简介

The Elastic Stack, 包括 Elasticsearch、Kibana、Beats 和 Logstash(也称为 ELK Stack)。能够安全可靠地获取任何来源、任何格式的数据,然后实时地对数据进行搜索、分析和可视化。Elaticsearch,简称为ES, ES是一个开源的高扩展的分布式全文搜索引擎,是整个Elastic Stack技术栈的核心。它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理PB级别的数据。

1.2. ElashticSearch 使用场景

  • 全文搜索:为网站或应用提供快速、精准的文本搜索功能,支持复杂的查询条件和相关性排序。
  • 日志和指标分析:集中收集和分析来自服务器、应用程序的日志数据,帮助运维人员监控系统状态、排查问题。
  • 业务数据分析:对销售数据、用户行为数据等进行实时聚合分析,为企业决策提供数据支持。

1.3. 与其他数据存储比较

RedisMySQLElashticSearchHBaseHadoop/Hive
容量/容量扩展较大海量海量
查询时效性极高中等较高中等
查询灵活性较差,K-V模式非常好,支持SQL较好,关联查询(join)较弱,但是可以全文检索,DSL语言可以处理过滤、匹配、排序、聚合等操作较差,主要靠Rowkey,Scan全表性能低,或者使用Phoenix建立二级索引非常好,支持SQL
写入速度极快中等较快较快
一致性、事务

1.4. ElashticSearch 特点

Elasticsearch 的设计哲学使其具备了“天然分片”和“天然索引”的特性。

1.4.1. 天然分片,天然集群

ES 会把数据进行分片,将多个分片存放到不同服务器上,以达到负载均衡,横向扩展。这种设计使得Elasticsearch能够轻松地通过增加节点来实现水平扩展,提升系统的存储容量和处理能力。

同时,ES 集群会的分片数据会存在副本,分片数据与其副本也存放在不同服务器上,以达到高可用作用。

ES 分片与副本

1.4.2. 天然索引

与传统数据库需要手动创建索引不同,Elasticsearch 在数据写入时会自动为字段创建倒排索引(详见下文),这使得数据一经写入即可被快速检索,极大地简化了开发和运维工作。

2. 深入理解倒排索引

倒排索引是 Elasticsearch 实现快速全文搜索的核心数据结构。

2.1. 什么是倒排索引

传统的索引(正排索引)是通过文档 ID 查找文档内容,而倒排索引则是通过词汇表中的词项(Term)查找包含该词项的文档 ID 列表。这种“词项→文档”的映射关系,使得全文搜索变得异常高效。

2.2. 与传统索引的区别

传统索引适用于精确查找,台通过主键查找记录,而倒排索引适用于全文搜索,能够快速找到包含特定关键词的所有文档。

2.3. 倒排索引的结构

倒排索引通常由两部分组成:词典(Term Dictionary)和倒排列表(Posting List)。词典存储了所有不重复的词项,倒排表则记录了每个词项出现的文档ID、词频、位置等信息。

ES 倒排索引

3. ElashticSearch 安装

3.1. 相关地址

Elasticsearch 下载地址:www.elastic.co/cn/download…

ES 分词器 IK 下载地址:github.com/medcl/elast…

Kibana 下载地址:www.elastic.co/cn/download…

本文选择 Elasticsearch 版本为 7.9.3

3.2. Windows 安装 ES

Windows 安装 ES 步骤比较简单,这里我们只在 Windows 上部署一个单节点的 ES,集群部署放到下一章节,那在 Windows 上安装 ES 具体步骤如下:

  1. Elasticsearch 下载解压后,直接进入bin/目录下,双击elasticsearch.bat即可启动,浏览器访问:http://localhost:9200
  2. Elasticsearch 中文分词器,将下载好的 zip 压缩包直接解压到 ES 的plugins/ik目录下即可;
  3. 如果使用 Kibana 作为客户端操作 ES 的话,跟 ES 一样,下载解压后,直接运行bin/目录下kibanna.bat即可启动,Kibana Web 端在浏览器访问:http://localhost:5601,即可进入 Kibana WebUI 界面;

3.3. Linux 安装 Elasticsearch

这里我们直接部署一个3节点的 ES 集群,其中遇到了一些问题,都已经标注并且给出解决步骤。

3.3.1. 修改 Linux 配置

在安装 ES 之前,需要对 Linux 系统配置进行修改,否则,可能会在启动 ES 时报错。

注意:修改完以下配置后,需要重启 Linux 服务器。

3.3.1.1. 问题一

如果不修改当前配置,在启动 ES 时可能会出现错误:max file descriptors [4096] for elasticsearch process likely too low, increase to at least [65536] elasticsearch.,问题原因是系统允许 ES 打开的最大文件数不够,需要修改为65535

修改/etc/security/limits.conf文件;

* soft nofile 65536
* hard nofile 131072
* soft nproc 2048
* hard nproc 65536

注意:前面的 * 号不要省略。

这里再补充一下,如果遇到该问题,并且通过修改上述问题依旧没有解决该问题,同时,是使用systemctl start elasticsearch命令启动 ES 的,可以尝试修改一下 service 文件中的一个参数,具体如下。

[Unit]
Description=Elasticsearch
Documentation=https://www.elastic.co
Wants=network-online.target
After=network-online.target
[Service]
User=es
Group=es
ExecStart=/usr/local/elasticsearch/bin/elasticsearch
LimitNOFILE=65535 # 通过添加这两行代码,也可以解决上述问题
LimitMEMLOCK=infinity
[Install]
WantedBy=multi-user.target
3.3.1.2. 问题二

max number of threads [1024] for user [judy2] likely too low, increase to at least [4096].

修改/etc/security/limits.d/90-nproc.conf文件,做以下修改。

* soft nproc 1024
# 修改为
* soft nproc 4096
3.3.1.3. 问题三

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

修改/etc/sysctl.conf文件,添加配置。

vm.max_map_count=262144

3.3.2. Elasticsearch 安装

  1. 下载官网 tar 包,并上传到服务器,解压到指定目录下。
tar -zxvf elasticsearch-7.9.3-linux-x86_64.tar.gz -C /opt/module
  1. 修改conf/elasticsearch.yml配置文件

注意:

  • 每行必须顶格写,每行前面不能有空格;
  • 配置冒号 : 后面,必须有一个空格;
# 集群名称,同一集群名称必须相同
# Use a descriptive name for your cluster:
#
cluster.name: my-es

# 单个节点名称
# Use a descriptive name for the node:
#
node.name: node-1

# 允许访问的ip,0.0.0.0代表允许任意ip访问
# Set the bind address to a specific IP (IPv4 or IPv6):
#
network.host: 0.0.0.0

# 指定该节点是否有资格被选举成为master,默认是true,es是默认集群中的第一台机器为master,如果这台机挂了就会重新选举master
# Add custom attributes to the node:
#
node.master: true
# 指定该节点是否存储索引数据,默认为true
node.data: true
node.max_local_storage_nodes: 3

# 写入候选主节点的设备地址,在开启服务后可以被选为主节点
# 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]"]
#
discovery.seed_hosts: ["hadoop102", "hadoop103", "hadoop104"]

# 初始化一个新的集群时需要此配置来选举master
# Bootstrap the cluster using an initial set of master-eligible nodes:
#
cluster.initial_master_nodes: ["node-1", "node-2", "node-3"]

# 最小节点数:防止脑裂,设置为半数以上
# Prevent the "split brain" by configuring the majority of nodes (total number of master-eligible nodes / 2 + 1):
discovery.zen.minimum_master_nodes: 2
  1. 修改config/jvm.options配置文件,将虚拟机环境内存改小。ES 是在 Java 虚拟机中运行的,虚拟机默认启动占用 2G 内存,但是如果装在 PC 机学习使用,实际用不了 2G,所以可以改小点内存。生产环境根据需求修改保持默认。
# Xms represents the initial size of total
# Xmx represents the maximum size of total
-Xms256m
-Xmx256m
  1. 将配置好的 ES 进行分发,发送到其他两个节点。

  2. 修改其他两个节点的配置文件conf/ealsticsearch.yml

    • 节点名称修改为node.name: node-*,节点名称不能重复,与其他节点不重即可;
    • 网络主机名称:network.host: hadoop10*,与主机名保持一致;

3.3.3. ES 启停脚本

为了快速启动,这里写一个快速启动脚本。

#!/bin/bash 
es_home=/opt/module/elasticsearch-7.9.3

case $1  in
 "start") {
  for i in hadoop102 hadoop103 hadoop104
  do
    ssh $i  "source /etc/profile;${es_home}/bin/elasticsearch >/dev/null 2>&1 &"
  done
};;
"stop") {
  for i in hadoop102 hadoop103 hadoop104
  do
      ssh $i "ps -ef | grep $es_home | grep -v grep | awk '{print $2}' | xargs kill" >/dev/null 2>&1
  done

};;
esac

3.3.4. ES 启动测试

直接执行启停脚本快速启动 ES。

使用浏览器访问:http://192.168.88.102:9200,可以看到集群相关信息,即表示 ES 集群启动成功。

或者通过 curl 命令发送请求:curl http://192.168.88.102:9200/_cat/nodes?v,可以看到所有节点信息。

ip            heap.percent ram.percent cpu load_1m load_5m load_15m node.role master name
192.168.88.104           41          74  16    0.36    0.21     0.16 dilmrt    -      node-3
192.168.88.103           35          72  22    0.35    0.19     0.15 dilmrt    *      node-2
192.168.88.102           25          70  20    0.60    0.22     0.17 dilmrt    -      node-1

3.4. Kibana 安装

  1. 上传 tar 包,并解压到指定目录下。
tar -zxvf kibana-7.9.3-linux-x86_64.tar.gz -C /opt/module/
  1. 修改config/kibana.yml配置文件,主要是配置 ES 地址,以及访问 Kibana 地址。
server.host: "0.0.0.0"
elasticsearch.hosts: ["http://hadoop102:9200"]
  1. 启动 Kibana
./bin/kibana
  1. 浏览器访问:http://192.168.88.102:5601,就可以看到 Kibana 的 WebUI 界面。

Kibana WebUI

3.5. ES 中文分词器安装

安装比较解决,解压后的文件添加到${ES_HOME}/plugins/ik/目录中即可。

需要重启 ES 服务。

测试ik_max_word分词器。

GET _analyze
{
  "analyzer": "ik_max_word",
  "text": "我是中国人"
}

响应结果

{
  "tokens" : [
    {
      "token" : "我",
      "start_offset" : 0,
      "end_offset" : 1,
      "type" : "CN_CHAR",
      "position" : 0
    },
    {
      "token" : "是",
      "start_offset" : 1,
      "end_offset" : 2,
      "type" : "CN_CHAR",
      "position" : 1
    },
    {
      "token" : "中国人",
      "start_offset" : 2,
      "end_offset" : 5,
      "type" : "CN_WORD",
      "position" : 2
    },
    {
      "token" : "中国",
      "start_offset" : 2,
      "end_offset" : 4,
      "type" : "CN_WORD",
      "position" : 3
    },
    {
      "token" : "国人",
      "start_offset" : 3,
      "end_offset" : 5,
      "type" : "CN_WORD",
      "position" : 4
    }
  ]
}

测试ik_smart分词器。

GET _analyze
{
  "analyzer": "ik_smart",
  "text": "我是中国人"
}

响应结果

{
  "tokens" : [
    {
      "token" : "我",
      "start_offset" : 0,
      "end_offset" : 1,
      "type" : "CN_CHAR",
      "position" : 0
    },
    {
      "token" : "是",
      "start_offset" : 1,
      "end_offset" : 2,
      "type" : "CN_CHAR",
      "position" : 1
    },
    {
      "token" : "中国人",
      "start_offset" : 2,
      "end_offset" : 5,
      "type" : "CN_WORD",
      "position" : 2
    }
  ]
}

4. Elasticsearch RESTful API

4.1. ES 基本概念

字段说明对应MySQL概念
clusterES集群
nodeES集群中的节点(默认一个进程就是一个节点)
shardES数据分片(默认5片)
index类似RDBMS中的数据库(ES5.x,在ES6.x和ES7.x中相当于table)database
type类似RDBMS中的表(ES6.x一个index只允许建立一个type,ES7.x废弃)table
document类似RDBMS中的行row
field相当于字段、属性column

4.2. ES 索引操作

4.2.1. 创建索引

通过PUT请求创建 ES 索引。

PUT book_index

创建成功响应。

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

创建索引时,还可以添加一些参数配置,上面创建命令使用的是默认参数。

PUT book_index
{
  "settings": {
    "number_of_shards": 1, # 分片
    "number_of_replicas": 2 # 副本
  },
  "mappings": {
    "properties": {
      "nameText": { # name字段,类型:text,分词器:ik_smart
        "type": "text",
        "analyzer": "ik_smart"
      },
      "author": { # author字段,类型:keyword
        "type": "keyword"
      },
      "score":{ # score字段,类型:double
        "type": "double"
      },
      "commentText": { # commentText字段,类型:text,分词器:ik_smart
        "type": "text",
        "analyzer": "ik_smart"
      }
    }
  }
}

type 类型说明:

  • String 字符类型;

    • text,可以被分词;
    • keyword,不可被分词,例如,作者姓名,肯定是不能被分词的;
  • Numerical 数值类型;

    • 基本数据类型:long、integer、short、byte、double、float、half_float;
    • 浮点类型:scaled_float;
  • Date 日期类型;

  • Array 数组类型;

  • Object 对象类型;

4.2.2. 查看所有索引

GET _cat/indices

查询结果。

green open .kibana-event-log-7.9.3-000002 hRiBxg3eSvCuerFiFFOoQA 1 1  2  0  21.9kb  10.9kb
green open .kibana-event-log-7.9.3-000001 GAsCCcrcSDyLesBNnxqZdg 1 1  5  0  54.1kb    27kb
green open book_index                     0KmPXXDkTGyaOU296WCo3Q 1 2  0  0    416b    208b
green open .apm-custom-link               i7DhuDCaRTyVCDTlWpViJA 1 1  0  0    416b    208b
green open student                        EcuYc8bDQb-t60l6PPLfTQ 3 2  5  0  38.2kb  12.7kb
green open .kibana_task_manager_1         k11GYaMpQCO_E5YdXZHI8Q 1 1  6 76 291.3kb 154.9kb
red   open .apm-agent-configuration       dCYj6pDFTl-6LiBuF1dYXQ 1 1                      
green open .kibana_1                      vw-tqGjfSGiLVKE7LOTEaA 1 1 54 29  41.7mb  20.8mb

返回索引信息包括:索引状态,索引名称,索引分片数,索引副本数等。

4.2.3. 查看单个索引

GET /book_index

查询结果。

{
  "book_index" : {
    "aliases" : { }, #别名
    "mappings" : { #属性映射
      "properties" : {
        "author" : {
          "type" : "keyword"
        },
        "commentText" : {
          "type" : "text",
          "analyzer" : "ik_smart"
        },
        "nameText" : {
          "type" : "text",
          "analyzer" : "ik_smart"
        },
        "score" : {
          "type" : "double"
        }
      }
    },
    "settings" : { #索引设置
      "index" : {
        "creation_date" : "1719736949102", #创建时间
        "number_of_shards" : "1", #分片数
        "number_of_replicas" : "2", #副本数
        "uuid" : "Wk1OOub7SBuL0SHaNoOi-Q",
        "version" : {
          "created" : "7090399"
        },
        "provided_name" : "book_index"
      }
    }
  }
}

4.2.4. 删除索引

DELETE book_index

4.3. ES 文档操作

4.3.1. 创建文档

POST /book_index/_doc/
{
  "nameText": "三体",
  "author": "刘慈欣",
  "score": "9.8",
  "commentText": "《三体》是刘慈欣创作的长篇科幻小说系列,由《三体》《三体2:黑暗森林》《三体3:死神永生》组成,第一部于2006年5月起在《科幻世界》杂志上连载,第二部于2008年5月首次出版,第三部则于2010年11月出版。作品讲述了地球人类文明和三体文明的信息交流、生死搏杀及两个文明在宇宙中的兴衰历程。《三体》的文本叙事在“后人类”的思考上有着重大突破,构建了科学与文学的互动范式,将道德内涵引入对科技的辩证思考中,并以文学手段在文化语境中对科技进行大胆假设和重构,但科技核心只是一个叙事跳板,是完成现实超越的重要媒介,也是人类命运共同体书写的重要工具。《三体》最吸引人的地方在于通过对人类中心主义的解构,继而完成对人与自然、动物之间的伦理反思与文学表达,最终指向去人类中心化的思想内核。"
}

响应结果。

{
  "_index" : "book_index",
  "_type" : "_doc",
  "_id" : "36ZbaJABSck_HBKJUzeG", #默认生成id
  "_version" : 1,
  "result" : "created", #结果
  "_shards" : {
    "total" : 3,
    "successful" : 3,
    "failed" : 0
  },
  "_seq_no" : 0,
  "_primary_term" : 1
}

4.3.2. 查看文档

GET /book_index/_doc/36ZbaJABSck_HBKJUzeG

4.3.3. 修改文档

修改文档score属性,将 9.8 修改为 8.9,当前修改方式会覆盖原有文档。

POST /book_index/_doc/36ZbaJABSck_HBKJUzeG
{
  "nameText": "三体",
  "author": "刘慈欣",
  "score": "8.9",
  "commentText": "《三体》是刘慈欣创作的长篇科幻小说系列,由《三体》《三体2:黑暗森林》《三体3:死神永生》组成,第一部于2006年5月起在《科幻世界》杂志上连载,第二部于2008年5月首次出版,第三部则于2010年11月出版。作品讲述了地球人类文明和三体文明的信息交流、生死搏杀及两个文明在宇宙中的兴衰历程。《三体》的文本叙事在“后人类”的思考上有着重大突破,构建了科学与文学的互动范式,将道德内涵引入对科技的辩证思考中,并以文学手段在文化语境中对科技进行大胆假设和重构,但科技核心只是一个叙事跳板,是完成现实超越的重要媒介,也是人类命运共同体书写的重要工具。《三体》最吸引人的地方在于通过对人类中心主义的解构,继而完成对人与自然、动物之间的伦理反思与文学表达,最终指向去人类中心化的思想内核。"
}

4.3.4. 修改字段

只修改某个字段。

POST /book_index/_update/36ZbaJABSck_HBKJUzeG
{
  "doc": {
    "score": 9.5
  }
}

4.3.5. 删除文档

DELETE /book_index/_doc/36ZbaJABSck_HBKJUzeG

4.4. ES 复杂查询

以下复杂查询一般用不到,但是这里记录一下,万一哪天用到了来这里查询一下。

4.4.1. 查询所有文档

GET /book_index/_search
{
  "query": {
    "match_all": {}
  }
}

参数说明:

  • query:查询对象。
  • match_all:查询类型,表示查询所有。

查询结果。

{
  "took" : 91, #查询时间
  "timed_out" : false, #是否超时
  "_shards" : { #分片信息
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : { #搜索命中结果
    "total" : {
      "value" : 6, #命中6个结果
      "relation" : "eq"
    },
    "max_score" : 1.0, #匹配度分值
    "hits" : [ #查询的所有结果对象
      {
        "_index" : "book_index",
        "_type" : "_doc",
        "_id" : "4KZxaJABSck_HBKJ8Dcw",
        "_score" : 1.0,
        "_source" : {
          "nameText" : "三体",
          "author" : "刘慈欣",
          "score" : "8.9",
          "commentText" : "《三体》是刘慈欣创作的长篇科幻小说系列,由《三体》《三体2:黑暗森林》《三体3:死神永生》组成,第一部于2006年5月起在《科幻世界》杂志上连载,第二部于2008年5月首次出版,第三部则于2010年11月出版。作品讲述了地球人类文明和三体文明的信息交流、生死搏杀及两个文明在宇宙中的兴衰历程。《三体》的文本叙事在“后人类”的思考上有着重大突破,构建了科学与文学的互动范式,将道德内涵引入对科技的辩证思考中,并以文学手段在文化语境中对科技进行大胆假设和重构,但科技核心只是一个叙事跳板,是完成现实超越的重要媒介,也是人类命运共同体书写的重要工具。《三体》最吸引人的地方在于通过对人类中心主义的解构,继而完成对人与自然、动物之间的伦理反思与文学表达,最终指向去人类中心化的思想内核。"
        }
      },
      {
        "_index" : "book_index",
        "_type" : "_doc",
        "_id" : "4aZzaJABSck_HBKJwzcx",
        "_score" : 1.0,
        "_source" : {
          "nameText" : "球状闪电",
          "author" : "刘慈欣",
          "score" : "9.9",
          "commentText" : "《球状闪电》是科幻作家刘慈欣写的一本以球状闪电为中心展开的长篇科学幻想小说,书中描述了一个历经球状闪电的男主角对其历尽艰辛的研究历程,向我们展现了一个独特、神秘而离奇的世界。"
        }
      },
      {
        "_index" : "book_index",
        "_type" : "_doc",
        "_id" : "4qZ0aJABSck_HBKJ5Tcb",
        "_score" : 1.0,
        "_source" : {
          "nameText" : "西游记",
          "author" : "吴承恩",
          "score" : "9.9",
          "commentText" : "该小说主要讲述了孙悟空出世,跟随菩提祖师学艺及大闹天宫后,遇见了唐僧、猪八戒、沙僧和白龙马,西行取经,一路上历经艰险,降妖除魔,经历了九九八十一难,终于到达西天见到如来佛祖,最终五圣成真的故事。该小说以“玄奘取经”这一历史事件为蓝本,经作者的艺术加工,深刻地描绘出明朝时期的社会生活状况。"
        }
      },
      {
        "_index" : "book_index",
        "_type" : "_doc",
        "_id" : "46Z1aJABSck_HBKJvze1",
        "_score" : 1.0,
        "_source" : {
          "nameText" : "三国演义",
          "author" : "罗贯中",
          "score" : "9.2",
          "commentText" : "《三国演义》大致分为黄巾起义、董卓之乱、群雄逐鹿、三国鼎立、三国归晋五大部分,描写了从东汉末年到西晋初年之间近百年的历史风云,以描写战争为主,描述了东汉末年的群雄割据混战和魏、蜀、吴三国之间的政治和军事斗争,最终司马炎一统三国,建立晋朝的故事。反映了三国时期各类社会斗争与矛盾的转化,并概括了这一时期的历史巨变,塑造了一群叱咤风云的三国英雄人物。"
        }
      },
      {
        "_index" : "book_index",
        "_type" : "_doc",
        "_id" : "5KZ2aJABSck_HBKJczeN",
        "_score" : 1.0,
        "_source" : {
          "nameText" : "红楼梦",
          "author" : "曹雪芹",
          "score" : "8.8",
          "commentText" : "《红楼梦》,中国古代章回体长篇小说,中国古典四大名著之一。其通行本共120回,一般认为前80回是清代作家曹雪芹所著,后40回作者为无名氏,整理者为程伟元、高鹗。小说以贾、史、王、薛四大家族的兴衰为背景,以富贵公子贾宝玉为视角,以贾宝玉与林黛玉、薛宝钗的爱情婚姻悲剧为主线,描绘了一些闺阁佳人的人生百态,展现了真正的人性美和悲剧美,是一部从各个角度展现女性美以及中国古代社会百态的史诗性著作。"
        }
      },
      {
        "_index" : "book_index",
        "_type" : "_doc",
        "_id" : "5aZ3aJABSck_HBKJTzfa",
        "_score" : 1.0,
        "_source" : {
          "nameText" : "水浒传",
          "author" : "施耐庵",
          "score" : "8.7",
          "commentText" : "全书通过描写梁山好汉反抗压迫、水泊梁山壮大和接受宋朝招安,以及受招安后为宋朝征战,最终消亡的宏大故事,艺术地反映了中国历史上宋江起义从发生、发展直至失败的全过程,深刻揭示了起义的社会根源,满腔热情地歌颂了起义英雄的反抗斗争和他们的社会理想,也具体揭示了起义失败的内在历史原因。"
        }
      }
    ]
  }
}

4.4.2. 匹配查询

match匹配类型查询,会把查询条件进行分词,然后进行查询,多个词条之间是 or 的关系。

GET /book_index/_search
{
  "query": {
    "match": {
      "nameText": "西游记"
    }
  }
}
  1. 查询commentText包含刘慈欣的文档。
GET /book_index/_search
{
  "query": {
    "match": {
      "commentText": "刘慈欣"
    }
  }
}
# 该查询会命中三体与球状闪电

4.4.3. 字段匹配查询

multi_matchmatch类似,不同的是它可以在多个字段中查询。

authorcommentText两个字段中,匹配刘慈欣

GET /book_index/_search
{
  "query": {
    "multi_match": {
      "query": "刘慈欣",
      "fields": ["author", "commentText"]
    }
  }
}

4.4.4. 关键字精确查询

term查询,精确的关键词匹配查询,不对查询条件进行分词。

查询nameText精确匹配三国演义的结果。

GET /book_index/_search
{
  "query": {
    "term": {
      "nameText": {
        "value": "三国演义"
      }
    }
  }
}

4.4.5. 多关键字精确查询

terms查询和term查询一样,但是允许指定多个匹配值进行匹配。

查询nameText精确匹配红楼梦西游记球状闪电的结果。

GET /book_index/_search
{
  "query": {
    "terms": {
      "nameText": ["红楼梦", "西游记", "球状闪电"]
    }
  }
}

注意:这里只命中了红楼梦西游记,没有命中球状闪电,原因是“球状闪电”会被分词为“球状”和“闪电”,所以没有命中。

可以使用以下命令,查询是否被分词。

GET _analyze
{
  "analyzer": "ik_smart",
  "text": "球状闪电"
}

返回结果如下:

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

4.4.6. 指定查询字段

默认情况下,ES 在搜索的结果中,会把文档中保存在_source中的所有字段都返回。

但是,如果想查询部分字段的话,就可以添加_source进行过滤。

GET /book_index/_search
{
  "_source": ["nameText", "score"],  # 只查询 nameText 和 score 字段
  "query": {
    "terms": {
      "nameText": ["红楼梦", "西游记"]
    }
  }
}

4.4.7. 过滤字段

_source的另一个用法,可以通过以下关键字进行过滤字段。

  • includes:指定显示的字段;
  • excludes:指定不显示的字段;

查询nameText精确匹配,红楼梦西游记的结果,然后,展示除nameTextscore两个字段外的所有字段。

GET /book_index/_search
{
  "_source": {
    "excludes": ["nameText", "score"]
  }, 
  "query": {
    "terms": {
      "nameText": ["红楼梦", "西游记"]
    }
  }
}

4.4.8. 组合查询

bool可以把各种其他查询通过must必须must_not必须不should应该的方式进行组合。

GET /book_index/_search
{
  "query": {
    "bool": {
      "must": [ # score 包含 9.9
        {
          "match": {
            "score": "9.9"
          }
        }
      ],
      "must_not": [ # nameText 不包含 "水浒传"
        {
          "match": {
            "nameText": "水浒传"
          }
        }
      ],
      "should": [ # commentText 包含 "小说"
        {
          "match": {
            "commentText": "小说"
          }
        }
      ]
    }
  }
}

# 查询结果,命中西游记和球状闪电

解释一下shouldshould也是匹配,但是should核心作用是影响文档的相关性评分(score) ,匹配到的 should 条件越多,文档的得分就越高,排名也就越靠前。

上面查询示例含义:找出评分为9.9分,且书名不是水浒传的书籍,如果书籍的评论里提到了小说,则优先展示。

4.4.9. 范围查询

range查询指定区间内的数字或时间。

range查询允许以下字符:

操作符说明
gt大于
gte大于等于
lt小于
lte小于等于

查询score小于等于9.0的结果。

GET /book_index/_search
{
  "query": {
    "range": {
      "score": {
        "lte": 9.0
      }
    }
  }
}

4.4.10. 模糊查询

查询nameText匹配西游记的结果,类似 SQL 中的 like。

GET /book_index/_search
{
  "query": {
    "fuzzy": {
      "nameText": {
        "value": "西游记"
      }
    }
  }
}

4.4.11. 单字段排序

sort可以让我们按照不同的字段进行排序,并且通过order指定排序的方式。asc升序,desc降序。

查询commentText匹配小说的结果,并且按score字段降序。

GET /book_index/_search
{
  "query": {
    "match": {
      "commentText": "小说"
    }
  },
  "sort": [
    {
      "score": {
        "order": "desc"
      }
    }
  ]
}

4.4.12. 多字段排序

查询索引中所有的文档,并且将结果先按score升序,再按_score升序。

注意:_score 字段,是 ES 返回中的一个命中评分的字段,表示文档与查询条件的相关评分,评分高表示匹配度高,否则匹配度低。

GET /book_index/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "score": {
        "order": "asc"
      }
    },
    {
      "_score": {
        "order": "asc"
      }
    }
  ]
}

4.4.13. 高亮查询

ES 可以对查询中内容中的关键字部分进行标签和样式(高亮)的设置。

在使用match查询的同时,加上一个highlight属性:

  • pre_tags:前置标签;
  • post_tags:后置标签;
  • fields:需要高亮的字段;
  • title:这里声明 title 字段需要高亮,后面可以为这个字段设置特有配置,也可以为空;
GET /book_index/_search
{
  "query": {
    "match": {
      "commentText": "故事"
    }
  },
  "highlight": {
    "pre_tags": "<font color='red'>",
    "post_tags": "</font>",
    "fields": {
      "commentText": {}
    }
  }
}

响应结果。

{
  "took" : 94,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 0.77720195,
    "hits" : [
      {
        "_index" : "book_index",
        "_type" : "_doc",
        "_id" : "4qZ0aJABSck_HBKJ5Tcb",
        "_score" : 0.77720195,
        "_source" : {
          "nameText" : "西游记",
          "author" : "吴承恩",
          "score" : "9.9",
          "commentText" : "该小说主要讲述了孙悟空出世,跟随菩提祖师学艺及大闹天宫后,遇见了唐僧、猪八戒、沙僧和白龙马,西行取经,一路上历经艰险,降妖除魔,经历了九九八十一难,终于到达西天见到如来佛祖,最终五圣成真的故事。该小说以“玄奘取经”这一历史事件为蓝本,经作者的艺术加工,深刻地描绘出明朝时期的社会生活状况。"
        },
        "highlight" : { # 高亮字段,下面“故事”关键字已经添加前后标签
          "commentText" : [
            "该小说主要讲述了孙悟空出世,跟随菩提祖师学艺及大闹天宫后,遇见了唐僧、猪八戒、沙僧和白龙马,西行取经,一路上历经艰险,降妖除魔,经历了九九八十一难,终于到达西天见到如来佛祖,最终五圣成真的<font color='red'>故事</font>。"
          ]
        }
      },
      # 其他命中文档省略......
    ]
  }
}

5. SpringBoot 整合 ES

SpringBoot 整合 Elasticsearch 有三种方式可供选择,分别是RestHighLevelClientElasticsearchRestTemplateElasticsearchRepository。这里主要给出RestHighLevelClient示例。

5.1. 创建索引

PUT /book_index
{
  "settings": {
    "number_of_replicas": 1,
    "number_of_shards": 2
  },
  "mappings": {
    "properties": {
      "nameText": {
        "search_analyzer": "ik_smart",
        "analyzer": "ik_max_word",
        "type": "text"
      },
      "author": {
        "type":"text",
        "fields":{
            "keyword":{
                "ignore_above":256,
                "type":"keyword"
            }
        }
      },
      "score":{
          "type":"float"
      },
      "commentText": {
        "search_analyzer": "ik_smart",
        "analyzer": "ik_max_word",
        "type": "text"
      },
      "publishTime":{
          "type":"date",
          "format": "yyyy-MM-d HH:mm:ss"
      }
    }
  }
}

5.2. 创建项目

  1. 创建项目。
<groupId>com.example</groupId>
<artifactId>ElasticsearchDemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
  1. 添加依赖。
<!--ES-->
<dependency>
    <groupId>org.elasticsearch</groupId>
    <artifactId>elasticsearch</artifactId>
    <version>7.9.3</version>
    <exclusions>
        <exclusion>
            <groupId>org.elasticsearch.client</groupId>
            <artifactId>elasticsearch-rest-high-level-client</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.elasticsearch.client</groupId>
            <artifactId>elasticsearch-rest-client</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>7.9.3</version>
</dependency>

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-client</artifactId>
    <version>7.9.3</version>
</dependency>

<!--lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.22</version>
</dependency>

<!--fastjson-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>2.0.3</version>
</dependency>
  1. 修改配置文件。
#应用名
spring.application.name=elasticsearch-demo
#服务端口号
server.port=8080

#es地址
spring.elasticsearch.uris=http://192.168.88.102:9200
  1. 创建实体类。

package com.example.elasticsearch.demo.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

/**
 * @Description TODO
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Book {

    /**
     * ID
     */
    private String id;
    /**
     * 书名
     */
    private String nameText;
    /**
     * 作者
     */
    private String author;
    /**
     * 评分
     */
    private Double score;
    /**
     * 评语
     */
    private String commentText;
    /**
     * 发布时间
     */
    private Date publishTime;
}
  1. 创建测试类进行测试。
   package com.example.elasticsearch.demo;

    import com.alibaba.fastjson2.JSON;
    import com.example.elasticsearch.demo.domain.Book;
    import org.elasticsearch.action.DocWriteRequest;
    import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
    import org.elasticsearch.action.bulk.BulkRequest;
    import org.elasticsearch.action.bulk.BulkResponse;
    import org.elasticsearch.action.delete.DeleteRequest;
    import org.elasticsearch.action.delete.DeleteResponse;
    import org.elasticsearch.action.get.GetRequest;
    import org.elasticsearch.action.get.GetResponse;
    import org.elasticsearch.action.index.IndexRequest;
    import org.elasticsearch.action.index.IndexResponse;
    import org.elasticsearch.action.search.SearchRequest;
    import org.elasticsearch.action.search.SearchResponse;
    import org.elasticsearch.action.support.master.AcknowledgedResponse;
    import org.elasticsearch.client.RequestOptions;
    import org.elasticsearch.client.RestHighLevelClient;
    import org.elasticsearch.client.indices.CreateIndexRequest;
    import org.elasticsearch.client.indices.CreateIndexResponse;
    import org.elasticsearch.client.indices.GetIndexRequest;
    import org.elasticsearch.client.indices.GetIndexResponse;
    import org.elasticsearch.common.xcontent.XContentType;
    import org.elasticsearch.index.query.*;
    import org.elasticsearch.index.reindex.BulkByScrollResponse;
    import org.elasticsearch.index.reindex.DeleteByQueryRequest;
    import org.elasticsearch.search.SearchHit;
    import org.elasticsearch.search.builder.SearchSourceBuilder;
    import org.elasticsearch.search.sort.SortOrder;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;

    import java.io.IOException;
    import java.text.SimpleDateFormat;
    import java.util.Arrays;
    import java.util.Date;
    import java.util.List;

    @SpringBootTest
    class ElasticsearchDemoApplicationTests {

        @Autowired
        private RestHighLevelClient restHighLevelClient;

        private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        private final String index = "book_index";
        public final String routing = "test";

        // todo === === === === === === === === index === === === === === === === ===

        @Test
        void indexIsExists() throws IOException {
            GetIndexRequest indexRequest = new GetIndexRequest(index);
            boolean exists = restHighLevelClient.indices().exists(indexRequest, RequestOptions.DEFAULT);
            System.out.println("指定 index 是否存在: " + exists);
        }

        /**
         * 创建索引
         */
        @Test
        void createIndex() throws IOException {
            CreateIndexRequest indexRequest = new CreateIndexRequest(index);
            CreateIndexResponse indexResponse = restHighLevelClient.indices().create(indexRequest, RequestOptions.DEFAULT);
            System.out.println("创建 index:" + JSON.toJSONString(indexResponse));
        }

        /**
         * 删除索引
         */
        @Test
        void deleteIndex() throws IOException {
            DeleteIndexRequest indexRequest = new DeleteIndexRequest(index);
            AcknowledgedResponse delete = restHighLevelClient.indices().delete(indexRequest, RequestOptions.DEFAULT);
            System.out.println("删除 index:" + JSON.toJSONString(delete));
        }

        /**
         * 查询索引信息
         */
        @Test
        void queryIndex() throws IOException {
            GetIndexRequest indexRequest = new GetIndexRequest(index);
            GetIndexResponse indexResponse = restHighLevelClient.indices().get(indexRequest, RequestOptions.DEFAULT);
            //index 索引信息:{"book_index":{"sourceAsMap":{"properties":{"publishTime":{"format":"yyyy-MM-d HH:mm:ss","type":"date"},"score":{"type":"float"},"author":{"type":"text","fields":{"keyword":{"ignore_above":256,"type":"keyword"}}},"nameText":{"search_analyzer":"ik_smart","analyzer":"ik_max_word","type":"text"},"commentText":{"search_analyzer":"ik_smart","analyzer":"ik_max_word","type":"text"}}}}}
            System.out.println("index 索引信息:" + JSON.toJSONString(indexResponse.getMappings()));
        }

        // todo === === === === === === === === document === === === === === === === ===

        /**
         * 插入文档
         */
        @Test
        void insertDocument() throws IOException {
            Book book = new Book("1001", "西游记", "吴承恩", 8.7,
                    "《西游记》的作者是明代的吴承恩。这是一本古代神话小说。它主要写了唐僧师徒四人,经历千辛万苦经过九九八十一难,到西天取回真经的故事", simpleDateFormat.format(new Date()));
            //构建IndexRequest
            IndexRequest indexRequest = new IndexRequest()
                    .index(index) //索引
                    .routing(routing) //路由/租户
                    .id(book.getId()).opType(DocWriteRequest.OpType.CREATE) //id,不指定id,可自动生成
                    .source(JSON.toJSONString(book), XContentType.JSON);
            //插入获取结果
            IndexResponse indexResponse = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
            //插入结果:{"fragment":false,"id":"1001","index":"book_index","primaryTerm":1,"result":0,"seqNo":0,"shardId":{"fragment":true,"id":-1,"index":{"UUID":"_na_","fragment":false,"name":"book_index"},"indexName":"book_index"},"shardInfo":{"failed":0,"failures":[],"fragment":false,"successful":2,"total":2},"type":"_doc","version":1}
            System.out.println("插入结果:" + JSON.toJSONString(indexResponse));
        }

        /**
         * 批量插入文档
         */
        @Test
        void insertBatchDocument() throws IOException {
            List<Book> books = Arrays.asList(
    //                new Book("1002", "红楼梦", "曹雪芹", 9.9,
    //                        "细读《红楼梦》,感慨于他们的悲欢离合,更是在品味人生的匆匆。旧时王谢堂前燕,早已飞入寻常百姓之家。纵使繁华如贾府,也逃不了时光的推动。" +
    //                                "盛极则衰,时光,多么盛大的舞台,昨日可重现脑海,今宵,却已别有一番洞天", simpleDateFormat.format(new Date())),
    //                new Book("1003", "水浒传", "施耐庵", 9.9,
    //                        "每个人都有梦想,但如果不行动,梦想只会成为泡影。所以,从此时起,我要用辛勤的努力换来梦想的实现", simpleDateFormat.format(new Date())),
    //                new Book("1004", "三国演义", "罗贯中", 9.9, "三国的每个人都是我们的老师,我们要从他们的身上学到各种优良的品质", simpleDateFormat.format(new Date())),
    //                new Book("1001", "西游记", "吴承恩", 8.7,
    //                        "《西游记》的作者是明代的吴承恩。这是一本古代神话小说。它主要写了唐僧师徒四人,经历千辛万苦经过九九八十一难,到西天取回真经的故事", simpleDateFormat.format(new Date())),
    //                new Book("1005", "三体", "刘慈欣", 9.9,
    //                        "《三体》这本书讲述三体文明与地球文明之间的碰撞,三体作为外太空另一个可适宜生物居住的星球同样也居住着三体人,三体人把地球人当作敌人," +
    //                                "地球人却对他们的看法褒贬不一于是地球上展开了一场人类关于三体的激烈斗争", simpleDateFormat.format(new Date())),
                    new Book("1006", "东游记", "张三", 3.9, "小说三体人寻常百姓老师", simpleDateFormat.format(new Date()))
            );
            //构建BulkRequest
            BulkRequest bulkRequest = new BulkRequest();
            for (Book book : books) {
                IndexRequest indexRequest = new IndexRequest()
                        .index(index)
                        .routing(routing)
                        .id(book.getId())
                        .source(JSON.toJSONString(book), XContentType.JSON);
                bulkRequest.add(indexRequest);
            }
            //插入获取结果
            BulkResponse bulkItemResponses = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
            //插入结果:[{"failed":false,"fragment":false,"id":"1002","index":"book_index","itemId":0,"opType":0,"response":{"fragment":false,"id":"1002","index":"book_index","primaryTerm":1,"result":0,"seqNo":1,"shardId":{"fragment":true,"id":-1,"index":{"UUID":"_na_","fragment":false,"name":"book_index"},"indexName":"book_index"},"shardInfo":{"failed":0,"failures":[],"fragment":false,"successful":2,"total":2},"type":"_doc","version":1},"type":"_doc","version":1},{"failed":false,"fragment":false,"id":"1003","index":"book_index","itemId":1,"opType":0,"response":{"fragment":false,"id":"1003","index":"book_index","primaryTerm":1,"result":0,"seqNo":2,"shardId":{"fragment":true,"id":-1,"index":{"UUID":"_na_","fragment":false,"name":"book_index"},"indexName":"book_index"},"shardInfo":{"failed":0,"failures":[],"fragment":false,"successful":2,"total":2},"type":"_doc","version":1},"type":"_doc","version":1},{"failed":false,"fragment":false,"id":"1004","index":"book_index","itemId":2,"opType":0,"response":{"fragment":false,"id":"1004","index":"book_index","primaryTerm":1,"result":0,"seqNo":3,"shardId":{"fragment":true,"id":-1,"index":{"UUID":"_na_","fragment":false,"name":"book_index"},"indexName":"book_index"},"shardInfo":{"failed":0,"failures":[],"fragment":false,"successful":2,"total":2},"type":"_doc","version":1},"type":"_doc","version":1}]
            System.out.println("插入结果:" + JSON.toJSONString(bulkItemResponses));
        }

        /**
         * 根据id删除文档
         */
        @Test
        void deleteDocument() throws IOException {
            DeleteRequest deleteRequest = new DeleteRequest().index(index).routing(routing).id("1001");
            DeleteResponse deleteResponse = restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT);
            //删除结果:{"fragment":false,"id":"1001","index":"book_index","primaryTerm":1,"result":2,"seqNo":4,"shardId":{"fragment":true,"id":-1,"index":{"UUID":"_na_","fragment":false,"name":"book_index"},"indexName":"book_index"},"shardInfo":{"failed":0,"failures":[],"fragment":false,"successful":2,"total":2},"type":"_doc","version":2}
            System.out.println("删除结果:" + JSON.toJSONString(deleteResponse));
        }

        /**
         * 条件删除
         */
        @Test
        void queryDeletDocument() throws IOException {
            DeleteByQueryRequest deleteByQueryRequest = new DeleteByQueryRequest(index)
                    .setRouting(routing)
                    .setQuery(QueryBuilders.boolQuery().must(QueryBuilders.matchQuery("author", "罗贯中")));
            BulkByScrollResponse deleteByQuery = restHighLevelClient.deleteByQuery(deleteByQueryRequest, RequestOptions.DEFAULT);
            //删除结果:{"batches":1,"bulkFailures":[],"bulkRetries":0,"created":0,"deleted":1,"fragment":true,"noops":0,"searchFailures":[],"searchRetries":0,"status":{"batches":1,"bulkRetries":0,"created":0,"deleted":1,"fragment":false,"noops":0,"requestsPerSecond":Infinity,"searchRetries":0,"sliceStatuses":[],"successfullyProcessed":1,"throttled":{"days":0,"daysFrac":0.0,"hours":0,"hoursFrac":0.0,"micros":0,"microsFrac":0.0,"millis":0,"millisFrac":0.0,"minutes":0,"minutesFrac":0.0,"nanos":0,"seconds":0,"secondsFrac":0.0,"stringRep":"0ms"},"throttledUntil":{"days":0,"daysFrac":0.0,"hours":0,"hoursFrac":0.0,"micros":0,"microsFrac":0.0,"millis":0,"millisFrac":0.0,"minutes":0,"minutesFrac":0.0,"nanos":0,"seconds":0,"secondsFrac":0.0,"stringRep":"0ms"},"total":1,"updated":0,"versionConflicts":0,"writeableName":"bulk-by-scroll"},"timedOut":false,"took":{"days":0,"daysFrac":6.712962962962963E-6,"hours":0,"hoursFrac":1.611111111111111E-4,"micros":580000,"microsFrac":580000.0,"millis":580,"millisFrac":580.0,"minutes":0,"minutesFrac":0.009666666666666667,"nanos":580000000,"seconds":0,"secondsFrac":0.58,"stringRep":"580ms"},"total":1,"updated":0,"versionConflicts":0}
            System.out.println("删除结果:" + JSON.toJSONString(deleteByQuery));
        }

        /**
         * 根据id修改文档
         */
        @Test
        void updateDocument() {
            /*
            实现逻辑一(参数:id和修改字段):
              1、根据id查询出指定的文档
              2、获取结果,并转换为 ContentDTO 对象,然后修改指定字段
              3、修改完成后,重新插入
            实现逻辑二(参数:id和已修改的对象):
              1、直接删除根据id删除原有数据
              2、把修改后的对象,直接插入(插入时,需要指定id,否则id就会随机指定)
             */
        }

        /**
         * 根据id查询文档
         */
        @Test
        void queryDocument() throws IOException {
            GetRequest getRequest = new GetRequest().index(index).routing(routing).id("1003");
            GetResponse getResponse = restHighLevelClient.get(getRequest, RequestOptions.DEFAULT);
            //查询文档:{"author":"施耐庵","commentText":"每个人都有梦想,但如果不行动,梦想只会成为泡影。所以,从此时起,我要用辛勤的努力换来梦想的实现","id":"1003","nameText":"水浒传","publishTime":"2023-09-16 07:42:18","score":9.9}
            System.out.println("查询文档:" + getResponse.getSourceAsString());
        }

        /**
         * match查询
         */
        @Test
        void matchQueryDocument() throws IOException {
            // 查询所有
    //        MatchAllQueryBuilder queryBuilder = QueryBuilders.matchAllQuery();

            // 匹配条件查询
    //        MatchQueryBuilder queryBuilder = QueryBuilders.matchQuery("author", "罗贯中");
            //commentText使用分词,查询时命中分词后结果即可被查询出来
    //        MatchQueryBuilder queryBuilder = QueryBuilders.matchQuery("commentText", "小说");
            MatchQueryBuilder queryBuilder = QueryBuilders.matchQuery("score", 9.9);

            // 短语查询
    //        MatchPhraseQueryBuilder queryBuilder = QueryBuilders.matchPhraseQuery("commentText", "古代神话小说");

            // 通配符查询
    //        WildcardQueryBuilder queryBuilder = QueryBuilders.wildcardQuery("nameText", "*记*");

            // 属性级联,通过.来实现
    //        WildcardQueryBuilder queryBuilder = QueryBuilders.wildcardQuery("actorList.name", "张*");

            // 查询构建器
            SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
            searchSourceBuilder.query(queryBuilder);
            // 查询请求
            SearchRequest searchRequest = new SearchRequest(index).routing(routing).source(searchSourceBuilder);
            System.out.println(searchRequest);
            // 执行查询
            SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
            System.out.println("查询结果:");
            for (SearchHit hit : searchResponse.getHits().getHits()) {
                System.out.println(hit.getSourceAsString());
            }
        }


        /**
         * 条件查询
         */
        @Test
        void boolQueryDocument() throws IOException {
            BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
            // score = 9.9
            queryBuilder.must(QueryBuilders.matchQuery("score", 9.9));
            // label != Redis
            queryBuilder.mustNot(QueryBuilders.wildcardQuery("id", "1003"));

            SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(queryBuilder);
            SearchRequest searchRequest = new SearchRequest().indices(index).routing(routing).source(searchSourceBuilder);
            SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
            System.out.println("查询结果:");
            for (SearchHit hit : searchResponse.getHits().getHits()) {
                System.out.println(hit.getSourceAsString());
            }
        }

        /**
         * 分页查询
         */
        @Test
        void pageQueryDocument() throws IOException {
            MatchAllQueryBuilder queryBuilder = QueryBuilders.matchAllQuery();

            SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
            searchSourceBuilder.from(1);
            searchSourceBuilder.size(2);
            searchSourceBuilder.sort("score", SortOrder.ASC);
            searchSourceBuilder.query(queryBuilder);

            SearchRequest searchRequest = new SearchRequest().indices(index).routing(routing).source(searchSourceBuilder);
            SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
            System.out.println("查询结果:");
            for (SearchHit hit : searchResponse.getHits().getHits()) {
                System.out.println(hit.getSourceAsString());
            }
        }

    }