《B站-ElasticSearch》学习笔记

[TOC]

视频地址:【狂神说Java】ElasticSearch7.6.x最新完整教程通俗易懂_哔哩哔哩_bilibili

环境安装

安装ElasticSearch

声明:JDK最低要求1.8版本。(本笔记使用的ElasticSearch 8.1.0版本)

ElasticSearch是基于Java开发的,后续使用Java连接ES时,ES的版本要和Java的核心jar包版本对应。

1、下载

官网地址:https://www.elastic.co/

2、解压安装包

下载到本地后,进行解压,得到目录如下图
elasticsearch目录结构
目录结构说明

bin/        启动文件等可执行文件的目录
config/     配置文件的目录
    log4j2.properties   日志配置
    jvm.options         java虚拟机相关的配置
    elasticsearch.yml   elasticsearch的配置文件
lib/        相关jar包的目录
modules/    功能模块的目录
logs/       日志的目录
plugins/    插件的目录,如ik分词器等

3、修改配置文件:
打开elasticsearch.yml文件,对如下配置项进行修改:

xpack.security.enabled: false

xpack.security.enrollment.enabled: false

xpack.security.transport.ssl:
  enabled: false

4、启动
运行bin目录下的elasticsearch.bat文件,待ES服务启动好后,使用浏览器访问http://127.0.0.1:9200/,看到如下界面,代表ES启动成功。
ES启动后的默认回文

安装ElasticSearch Hand可视化工具

hand的github地址:https://github.com/mobz/elasticsearch-head
下载后,进行解压,解压后,进入elasticsearch-head-master目录,在其中执行命令(此步骤需要安装nodejs环境)

# 安装项目所需的依赖包
npm install
# 启动项目
npm run start

在浏览器中访问:http://localhost:9100/,看到如下界面
ElasticSearch Hand界面
打开后,若发现无法链接,可打开浏览器的控制台查看报错信息,若是因为跨域问题导致了报错,那此时可以通过修改ES的配置文件解决问题,打开elasticsearch.yml配置文件,在其中添加如下配置:

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

然后重启ES服务,此时,刷新hand的界面,就能看到ES已经链接成功了。
在这里插入图片描述
对于初学者,可以把ES看成一个数据库,不要考虑其是搜索服务器,索引可以理解为关系型数据库中的库,文档可以理解为具体的数据。

hand可以把它当做数据展示的工具,后面所有的查询,可以通过kibana实现。

安装Kibana

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

安装Kibana
Kibana是一个针对ElasticSearch的开源分析及可视化平台,用来搜索、查看交互存储在ElasticSearch索引中的数据,使用Kibana,可以通过各种图表进行高级数据分析及展示。Kibana让海量数据更容易理解。它操作简单,基于浏览器的用户界面可以快速创建仪表板(dashboard)实时显示ElasticSearch查询动态。设计Kibana非常简单。无需编码或者额外基础架构,几分钟内就可以完成Kibana安装并启动ElasticSearch索引监控。
1、下载:
官网:https://www.elastic.co/cn/kibana/
注意:Kibana的版本需要与ElasticSearch的版本一致!!!
Kibana也是开箱即用的工具。
2、解压安装:
下载到本地后,进行解压,得到目录如下图

Kibana目录结构
3、启动
运行bin目录下的kibana.bat文件,看到如下界面,代表Kibana已启动
Kibana启动
使用浏览器访问http://localhost:5601/?code=840671,看到Kibana系统界面
Kibana系统首页
4、找到开发工具(Dev Tools):

我们可以使用curl、Postman、谷歌插件等开发工具对ES进行测试,同时,Kibana也提供了相应的开发工具。
Kibana开发工具
我们之后所有的操作都在这里进行。
5、汉化Kibana:
在Kibana安装目录的config目录下,找到kibana.yml配置文件,修改其中关于国际化的配置如下:

i18n.locale: "zh-CN"

在修改前,要确保kibana安装目录下x-pack\plugins\translations\translations目录里存在zh-CN.json文件。
然后重启Kibana服务即可。
汉化后的Kibana

ES核心概念

1、索引(index)
2、字段类型(mapping);
3、文档(documents)

elasticsearch是面向文档的!

关系型数据库和ElasticSearch的对比:

| Relational DB | ElasticSearch | ElasticSearch备注 |
|:–:|:–:|:–|
| 数据库(database) | 索引(indices) | 就和关系型数据库中的库一样 |
| 表(tables) | 类型(types) | 7.x已经过时,8.0以后会彻底弃用 |
| 行(rows) | 文档(documents) | 一条条的数据 |
| 字段(columns) | fields | 每条数据内的字段 |
ElasticSearch中存储的一切都是JSON!

ElasticSearch(集群)中可以包含多个索引(数据库),每个索引中可以包含多个类型(表),每个类型下面有包含多个文档(行),每个文档中又包含了多个字段(列)。

物理设计

ElasticSearch在后台把每个索引划分成多个分片,每份分片可以在集群中的不同服务器间迁移。
在ES里,即使只有一个节点,也是集群,默认的集群名是elasticsearch
ES集群信息

逻辑设计

一个索引类型中,可以包含多个文档,比如文档1、文档2。当我们索引一篇文档时,可以通过这样的一个顺序找到它:索引 > 类型 > 文档ID,通过这个组合我们就能索引到具体的某个文档。注意:ID不必是整数,实际上它是个字符串。

文档

就是我们的一条条数据!

ElasticSearch是面向文档的,那么就意味着索引和搜索数据的最小单位是文档,ElasticSearch中,文档有几个重要的属性:
* 自我包含,一篇文档同时包含字段和对应的值,也就是同时包含key和value;
* 可以是层次性的,一个文档中包含自文档,复杂的逻辑实体就是这么来的(就是一个json对象,在java中可以使用fastjson进行自动转换);
* 灵活的结构,文档不依赖预先定义的模式,我们知道关系型数据库中,要提前定义字段才能使用,在ElasticSearch中,对于字段是非常灵活的,有时候,我们又可以忽略该字段,或者动态的添加一个新的字段。

尽管我们可以随意的新增或忽略某个字段,但是,每个字段的类型非常重要,比如一个年龄字段的类型,可以是字符串也可以是整型。因为ElasticSearch会保存字段和类型之间的映射及其他的设置。这种映射具体到每个字段的每种类型,这也是为什么在ElasticSearch中,类型有时候也称为映射类型。

类型

类似于关系型数据库的Table中每个字段的数据类型!

类型是文档的逻辑容器,就像关系型数据库一样,表格是行的容器。类型中对于字段的定义称为映射,比如name映射为字符串类型。我们说文档是无模式的,它们不需要映射中定义的所有字段,比如新增一个字段,那么ElasticSearch是怎么做的呢?ElasticSearch会自动改的将新字段加入映射,但这个字段不确定它是什么类型,ElasticSearch会根据内容去猜,如果这个字段的值是18,那ElasticSearch会认为它是整型。但是ElasticSearch也可能猜错,所以最安全的方式就是提前定义好所需要的映射,这点和关系型数据库殊途同归了,先定义好字段,然后再使用。

索引

就是数据库。

索引是映射类型的容器,ElasticSearch中的索引是一个非常大的文档集合。索引存储了映射类型的字段和其他设置。然后它们被存储在各个分片上了。我们来研究下分片是如何工作的。
物理设计:节点和分片 如何工作
新建索引

一个集群至少有一个节点(只有一个ElasticSearch服务时),而一个节点就是一个ElasticSearch进程,节点可以有多个索引,如果你创建索引,那么索引将会有5个分片(primary shard,又称为主分片)构成,每一个主分片会有一个副本(replica shard,又称为复制分片)
ES集群节点分片示意图
上图是一个3个节点的集群,可以看到主分片和对应的复制分片都不会在同一个节点内,这样有利于某个节点挂掉了,数据也不会丢失。实际上,一个分片是一各Lucene索引,一个包含倒排索引的文件目录,倒排索引的结构使得ElasticSearch在不扫描全部文档的情况下,就能告诉你哪个文档包含特定的关键字。

倒排索引

ElasticSearch使用的是一种称为倒排索引的结构,采用Lucene倒排索引作为底层。这种结构适用于快速的全文检索,一个索引由文档中所有不重复的列表构成,对于每一个词,都有一个包含它的文档列表。例如,现在有两个文档,每个文档包含如下内容:

Study every day, good good up to forever        # 文档1包含的内容
To forever, study every day, good good up       # 文档2包含的内容

为了创建倒排索引,我们首先要将每个文档拆分成独立的词(或称为词条或者tokens),然后创建一个包含所有不重复的词条的排序列表,然后列出每个词条出现在哪个文档:
| term | doc_1 | doc_2 |
| — | — | — |
| Study | √ | × |
| To | × | √ |
| every | √ | √ |
| forever | √ | √ |
| day | √ | √ |
| study | × | √ |
| good | √ | √ |
| to | √ | × |
| up | √ | √ |
现在,我们试图搜索to forever,只需要查看包含每个词条的文档
| term | doc_1 | doc_2 |
| — | — | — |
| to | √ | × |
| forever | √ | √ |
| total | 2 | 1 |
两个文档都匹配,但是第一个文档比第二个文档匹配程度更高。如果没有别的条件,现在,这两个包含关键字的文档都将返回。
再来看一个示例,比如我们通过博客标签来搜索博客文章。那么倒排索引列表就是这样一个结构:
在这里插入图片描述
如果要搜索含有python标签的文章,那相对于查找所有原始数据而言,查找倒排索引后的数据会快很多。只需要查看标签这一栏,然后获取相关文章的ID即可。完全过滤掉所有无关的数据,提高效率!
ElasticSearch的索引和Lucene的索引对比:
在ElasticSearch中,索引(库)这个词被频繁使用,这就是术语的使用。在ElasticSearch中,索引被划分为多个分片,每份分片是一个Lucene的索引。所以一个ElasticSearch索引是由多个Lucene索引组成的,因为ElasticSearch是使用Lucene作为底层。如无特指,说起索引都是只ElasticSearch的索引。
接下来的一切操作都在Kibana中的Dev Tools下的Console里完成

IK分词器插件

什么是IK分词器?

分词:即把文字(中文或其他语言)划分成一个个的关键字,我们在搜索的时候会把自己的信息进行分词,会把数据库中或索引库中的数据进行分词,然后进行匹配操作,默认的中文分词是将每一个字看成一次词,比如“我爱狂神”会被分成“我”,“爱”,“狂”,“神”,这显然是不符合要求的,所以我们需要安装IK分词器来解决这个问题。
如果要使用中文,建议使用IK分词器!
IK分词器提供了两个分词算法:ik_smart和ik_max_word,其中ik_smart为最少切分,ik_max_word为最细粒度划分!

安装IK分词器

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

2、解压安装:
将下载得到的IK分词器的压缩包解压到ElasticSearch安装目录的plugins目录下,目录命名为ik即可。
安装ik分词器
3、重启并观察ES服务:
在ES启动日志中,可以看到IK分词器的插件被加载。
在这里插入图片描述
4、使用elasticsearch-plugin命令查看现有的插件:
可以通过elasticsearch-plugin命令查看加载进来的插件有哪些。
在ElasticSearch安装目录的bin目录下,使用命令行输入命令elasticsearch-plugin list即可查看。
使用elasticsearch-plugin命令查看加载到的插件
5、使用Kibana进行测试:
在Kibana中打开开发工具界面,在其中对IK分词器进行确认。
先测试ik_smart(最少切分)分词算法,在控制台中输入

GET _analyze
{
  "analyzer": "ik_smart",
  "text": "中国共产党"
}

发送请求后,可以看到右侧的结果为:

{
  "tokens" : [
    {
      "token" : "中国共产党",
      "start_offset" : 0,
      "end_offset" : 5,
      "type" : "CN_WORD",
      "position" : 0
    }
  ]
}

测试ik_smart分词算法
发现没有进行切分。

然后测试ik_max_word(最细粒度切分)分词算法,在控制台中输入

GET _analyze
{
  "analyzer": "ik_max_word",
  "text": "中国共产党"
}

发送请求后,可以看到右侧的结果为:

{
  "tokens" : [
    {
      "token" : "中国共产党",
      "start_offset" : 0,
      "end_offset" : 5,
      "type" : "CN_WORD",
      "position" : 0
    },
    {
      "token" : "中国",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "CN_WORD",
      "position" : 1
    },
    {
      "token" : "国共",
      "start_offset" : 1,
      "end_offset" : 3,
      "type" : "CN_WORD",
      "position" : 2
    },
    {
      "token" : "共产党",
      "start_offset" : 2,
      "end_offset" : 5,
      "type" : "CN_WORD",
      "position" : 3
    },
    {
      "token" : "共产",
      "start_offset" : 2,
      "end_offset" : 4,
      "type" : "CN_WORD",
      "position" : 4
    },
    {
      "token" : "党",
      "start_offset" : 4,
      "end_offset" : 5,
      "type" : "CN_CHAR",
      "position" : 5
    }
  ]
}

测试ik_max_word分词算法
发现其除了包含最小切分的结果外,还包含了其他多种可能的分词结果。

自定义分词字典

1、输入一些自定义词组进行测试
例如:超级喜欢狂神说Java。其中“狂神说”是一个自定义词组。
在这里插入图片描述
发现了问题:“狂神说”被拆开了!
这种自己需要的词,需要自己加到分词器的字典中!
2、增加自己的字典:
在ik分词器插件的config目录里,可以新建自己的字典。
新建自己的词典文件
打开新建的kuang.dic文件,在其中补充自己想要的字典内容。
在自定义字典中输入字典值
3、在配置文件中增加自定义字典:
在ik分词器插件的config目录里,可以看到IKAnalyzer.cfg.xml配置文件。
IK分词器配置文件
在配置文件中,配置自己新建的自定义字典。
配置自定义字典
4、重启ES并进行测试:
通过观察ES启动日志,可以发现ES在启动过程中加载了刚才自定义的字典。
ES启动时加载自定义字典
5、再次测试自定义词组的分词结果:
输入“超级喜欢狂神说Java”,查看分词结果。

ik_smart(最少切分)算法分词结果:
在这里插入图片描述
ik_max_word(最细粒度切分)算法分词结果:
在这里插入图片描述
发现无论哪种分词算法,“狂神说”都已经作为了一个固定的词组被切分。
以后,我们自定义的词组,只需要在自定义的dic文件中进行配置即可!

Rest风格说明

REST是一种软件架构风格,或者说是一种规范,其强调HTTP应当以资源为中心,并且规范了URI的风格;规范了HTTP请求动作(GET/PUT/POST/DELETE/HEAD/OPTIONS)的使用,具有对应的语义。它主要用于客户端和服务器端交互类的软件。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。
基本Rest命令说明:

method url地址 描述
PUT IP:9200/索引名称/_create/文档id 创建文档(指定文档id)
POST IP:9200/索引名称/_create 创建文档(随机文档id)
POST IP:9200/索引名称/_update/文档id 修改文档
DELETE IP:9200/索引名称/_doc/文档id 删除文档
GET IP:9200/索引名称/_doc/文档id 通过文档id查询文档
GET IP:9200/索引名称/_search 查询所有数据

ES支持的数据类型:
* 字符串类型
textkeyword
* 数值类型
long、integer、short、byte、double、float、half float、scaled float、unsigned_long
* 日期类型
datedate_nanos
* 布尔值类型
boolean
* 二进制类型
binary
* 数组类型
array
* 空间数据类型
geo_pointgeo_shapepointshape
* 对象和关系类型
objectflattenednestedjoin
* 其他类型

关于索引的基本操作

1、创建一个索引:
在Kibana的开发工具的控制台中输入如下命令,便可创建一个索引为test1,文档id为1的文档

# PUT /索引名称/_create/文档id
PUT /test001/_create/1
{
  "name": "狂神说",
  "age": 3
}

创建成功后,响应的 json 如下:

{
  "_index" : "test001",
  "_id" : "1",
  "_version" : 1,
  "result" : "created",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 0,
  "_primary_term" : 1
}

创建文档
如果当前索引和文档已经存在,再次执行上面的命令,则会报错,具体效果如下:
报错内容
使用hand查看索引数据:
在hand中查看文档内容
2、创建索引并明确其中的映射规则:
在Kibana中执行以下命令:

# PUT /索引名称
# {
#   "mappings": {
#     "properties": {
#       "字段名": {
#         "type": "字段类型"
#       },
#       ……
#     }
#   }
# }
PUT /test002
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text"
      },
      "age": {
        "type": "long"
      },
      "birthday": {
        "type": "date"
      }
    }
  }
}

上述命令的含义是:创建索引test002,然后设置字段的映射关系,name字段的类型为text、age字段的类型为long、birthday字段的类型为date。
其执行结果如下所示:
创建索引映射关系
3、获取指定索引信息:

# GET /索引名称
GET /test002

执行结果如下所示:
获取索引信息
4、查看索引的默认信息

新建一个索引和文档

# PUT 索引名称/_doc/文档ID
# {
#   "字段名": 字段值,
#   ……
# }
PUT test003/_doc/1
{
  "namme": "狂神说",
  "age": 13,
  "birth": "1997-01-05"
}

然后使用GET命令获取test003索引的信息:
ES默认的数据类型
如果文档的字段没有指定类型,那么ES就会给我们默认配置字段类型!
扩展:通过命令获取ElasticSearch的状态信息:
1)通过GET _cat/health命令获取ES的健康状态
获取ES的健康状态
和ElasticSearch hand插件中显示的信息一样
ES hand插件
2)通过GET _cat/indices?v命令获取所有索引的版本信息
查看ES中索引的版本信息
5、删除索引:

# DELETE 索引名称
DELETE test001

删除test001索引

关于文档的基本操作

1、创建文档:
创建一个文档

# PUT 索引名称/_doc/文档ID
# {
#   "字段名": 字段的值,
#   ……
# }
PUT kuangshen/_doc/1
{
  "name": "狂神说",
  "age": 23,
  "desc": "一顿操作猛如虎,一看工资2500",
  "tags": [
    "技术宅",
    "温暖",
    "直男"
  ]
}

执行结果:
创建文档
创建同类型的第二个文档

PUT kuangshen/_doc/2
{
  "name": "张三",
  "age": 3,
  "desc": "法外狂徒",
  "tags": [
    "交友",
    "旅游",
    "渣男"
  ]
}

执行结果:
创建第二个文档
创建第三个文档:

PUT kuangshen/_doc/3
{
  "name": "李四",
  "age": 30,
  "desc": "mmp,不知道如何形容",
  "tags": [
    "靓女",
    "旅游",
    "唱歌"
  ]
}

执行结果:
创建第三个文档
2、获取数据:
获取kuangshen索引中,文档ID为1的文档内容

# GET 索引名称/_doc/文档ID
GET kuangshen/_doc/1

查询结果:
获取数据
3、修改文档:
1)使用PUT命令修改数据:

# PUT 索引名称/_doc/文档ID
# {
#   "字段名": 更新的目标值,
#     ……
# }

PUT kuangshen/_doc/3
{
  "name": "李四233",
  "age": 30,
  "desc": "mmp,不知道如何形容",
  "tags": [
    "靓女",
    "旅游",
    "唱歌"
  ]
}

执行命令后,可以看到如下效果:
使用PUT命令更新数据

使用PUT命令更新数据,是全量覆盖的模式,如果某个字段在更新的时候漏掉或者没有赋值,那么ES中对应的字段也会消失,如下图所示:
PUT命令全量覆盖更新方法
先执行上面的PUT命令,再使用GET命令查询文档。会发现desc字段消失了,因为在最新一次执行的PUT命令中,没有desc字段。

2)使用POST命令修改数据(推荐使用这种更新方式!!!
先使用GET命令查看kuangshen索引中文档ID为1的文档的内容:
kuangshen索引中文档ID为1的文档内容
然后使用POST命令更新文档内容

# POST 索引名称/_update/文档ID
# {
#   "doc": {
#     "字段名": 更新的目标值,
#     ……
#   }
# }
POST kuangshen/_update/1
{
  "doc": {
    "name": "狂神说Java"
  }
}

执行结果
通过POST命令更新数据
然后使用GET命令查看刚才修改的文档:
通过GET命令查看数据
发现只有给定的字段的值发生了变化,其他值还是保持原来的内容。

3、删除文档:
根据文档ID删除文档

# DELETE 索引名称/_doc/文档ID
DELETE test001/_doc/1

删除test001索引中ID为1的文档

搜索数据

简单搜索

根据某个字段模糊搜索数据

# GET 索引名称/_search?q=字段名:搜索内容
GET kuangshen/_search?q=name:狂神说

执行结果:
在这里插入图片描述
在上面的返回结果中,可以看到一个_score字段,这个字段代表着搜索到的结果和搜索条件的匹配度分数,匹配度越高,分数也就越高,这里可以通过切换不同的搜索条件进行测试,会发现如果搜索条件和结果完全一样时,其分数会比不完全一样时更高。

复杂搜索(排序、分页、高亮、模糊匹配、精准匹配)

1、单条件查询数据:

# GET 索引名称/_search
# {
#   "query": {
#     "match": {
#       "字段名": "搜索内容" # 这里要注意字段的数据类型,如果是字符串型则是模糊匹配,如果是非字符串型则是精确匹配
#     }
#   }
# }
GET kuangshen/_search
{
  "query": {
    "match": {
      "name": "狂神"
    }
  }
}

查询结果如下:
在这里插入图片描述
为了方便后面演示,再插入一条相似的数据

PUT kuangshen/_doc/4
{
  "name": "狂神说前端",
  "age": 3,
  "desc" : "一顿操作猛如虎,一看工资2500",
  "tags" : [
    "技术宅",
    "温暖",
    "直男"
  ]
}

然后再执行上面的查询操作,可以看到如下结果:
在这里插入图片描述
2、查询结果中只显示指定的几个字段:
相当于SQL中的select a, b, c from table。一般推荐使用该方式进行查询。

# 通过"_source": ["FIELD_A", "FIELD_B", ...]参数来限制查询所得的字段
GET kuangshen/_search
{
  "query": {
    "match": {
      "name": "狂神"
    }
  },
  "_source": ["name", "desc"]
}

查询结果:
在这里插入图片描述
之后通过Java调用ES时,所有的对象和方法就是查询参数和结果中的key。
3、对查询结果排序:
通过sort参数可以实现对查询结果的排序:

# "sort": [
#     {
#       "FIELD": {
#         "order": "desc"
#       }
#     }
#   ]
# 以年龄降序查询name中包含狂神的数据
GET kuangshen/_search
{
  "query": {
    "match": {
      "name": "狂神"
    }
  },
  "sort": [
    {
      "age": {
        "order": "desc"
      }
    }
  ]
}

查询结果为:
在这里插入图片描述
也可以将排序方式改成升序(ASC),查询结果如下:
在这里插入图片描述
4、对查询结果分页:
使用from(从第几条开始)和size(每页显示数量)两个参数实现分页,相当于SQL中limit的两个参数offset和pagesize:

GET kuangshen/_search
{
  "query": {
    "match": {
      "name": "狂神"
    }
  },
  "sort": [
    {
      "age": {
        "order": "asc"
      }
    }
  ],
  "from": 0,
  "size": 1
}

查询结果:
在这里插入图片描述
ES中的数据下标也是从0开始的。
5、多条件查询:
1)在ES中,实现类似于SQL中的AND方式的多条件查询

# GET 索引名称/_search
# {
#   "query": {
#     "bool": {
#       "must": [
#         {
#           "match": {
#             "字段1": "对应的搜索内容"
#           }
#         },
#         {
#           "match": {
#             "字段2": "对应的搜索内容"  # 这里要注意字段的数据类型,如果是字符串型则是模糊匹配,如果是非字符串型则是精确匹配
#           }
#         }
#       ]
#     }
#   }
# }
GET kuangshen/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "name": "狂神"
          }
        },
        {
          "match": {
            "age": 3
          }
        }
      ]
    }
  }
}

查询结果如下:
在这里插入图片描述
2)在ES中,实现类似于SQL中的OR方式的多条件查询

GET kuangshen/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "match": {
            "name": "狂神"
          }
        },
        {
          "match": {
            "age": 23
          }
        }
      ]
    }
  }
}

查询结果:
在这里插入图片描述
3)查询不等于某一个值的数据

GET kuangshen/_search
{
  "query": {
    "bool": {
      "must_not": [
        {
          "match": {
            "age": 23
          }
        }
      ]
    }
  }
}

查询结果:
在这里插入图片描述
6、对查询结果进行过滤:
使用ES的过滤器对查询结果进行过滤。
过滤方式有很多种,其中有一种限制范围的过滤方式,条件有gt(大于)、gte(大于等于)、lt(小于)、lte(小于等于)。
查询name中包含“狂神”的数据,并过滤出age大于10的数据:

# "filter": [
# {
#   "range": {
#     "FIELD": {
#       "gte": 10,
#       "lte": 20
#     }
#   }
# }
# ]
GET kuangshen/_search
{
  "query": {
    "bool": {
      "must_not": [
        {
          "match": {
            "name": "狂神"
          }
        }
      ],
      "filter": [
        {
          "range": {
            "age": {
              "gt": 10
            }
          }
        }
      ]
    }
  }
}

查询结果:
在这里插入图片描述
7、同一个字段符合多个查询条件(OR关系):
在同一个字段中,需要查询多个条件的,可以直接使用空格将查询条件隔开(多个条件之间是OR的关系),比如查询tags中包括“男”和“技术”两个词的数据:

GET kuangshen/_search
{
  "query": {
    "match": {
      "tags": "男 技术"
    }
  }
}

查询结果:
在这里插入图片描述
8、精确查询:
使用term参数实现精确查找。
* term是单词级别的查询,这些查询通常⽤于结构化的数据,⽐如:number, date, keyword等,⽽不是对text。也就是说,全⽂本查询之前要先对⽂本内容进⾏分词,⽽单词级别的查询直接在相应字段的,反向索引中精确查找,单词级别的查询⼀般⽤于数值、⽇期等类型的字段上。
* term查询是直接通过倒排索引指定的词条进行精确查找的!
* match查询是通过分词器解析,先分析文档内容,然后通过分析的文档内容进行查询。

这里说明一下text类型和keywrod类型的区别:
* text类型的数据可以被分词器分解成多个词;
* keyword类型不可以被分解。

1)创建一个新的索引库,并为其字段指定类型

PUT testdb
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text"
      },
      "desc": {
        "type": "keyword"
      }
    }
  }
}

2)按照上面的字段插入数据

PUT testdb/_doc/1
{
  "name":"狂神说Java name",
  "desc":"狂神说Java desc"
}

3)插入第二条数据

PUT testdb/_doc/2
{
  "name":"狂神说Java name",
  "desc":"狂神说Java desc2"
}

4)使用term查询

# GET 索引名称/_search
# {
#   "query": {
#     "term": {
#       "FIELD": {  # 要查询的字段名
#         "value": "VALUE"  # 要查询的值
#       }
#     }
#   }
# }
GET testdb/_search
{
  "query": {
    "term": {
      "name": {
        "value": "狂"
      }
    }
  }
}

查询结果:
在这里插入图片描述
由于name字段的类型是text型,其值可以被分词分解的,所以这时候查出了所有name中带“狂”字的数据。

GET testdb/_search
{
  "query": {
    "term": {
      "desc": {
        "value": "狂神说Java desc"
      }
    }
  }
}

查询结果:
在这里插入图片描述
由于desc字段是keyword型,其值不可以被分词器分解,所以只能查出完全匹配的数据来。

9、多个值匹配的精确查询:
再插入两条数据:

PUT testdb/_doc/3
{
  "t1": "22",
  "t2": "2022-03-23"
}

PUT testdb/_doc/4
{
  "t1": "33",
  "t2": "2022-03-24"
}

然后查询数据:

GET testdb/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "term": {
            "t1": {
              "value": "22"
            }
          }
        },
        {
          "term": {
            "t1": {
              "value": "33"
            }
          }
        }
      ]
    }
  }
}

执行结果为:
在这里插入图片描述
若将查询条件做一些微调,如下:

GET testdb/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "term": {
            "t1": {
              "value": "2"
            }
          }
        },
        {
          "term": {
            "t1": {
              "value": "3"
            }
          }
        }
      ]
    }
  }
}

其执行结果如下:
在这里插入图片描述
可以发现精确查询是起到了作用的。
10、查询结果中高亮显示被查询内容:
1)在查询结果中,高亮显示查询内容
通过highlight属性标记需要高亮的字段:

# "highlight": {
#   "fields": {
#     "FIELD_A": {},
#     "FIELD_B": {}
#   }
# }
GET kuangshen/_search
{
  "query": {
    "match": {
      "name": "狂神"
    }
  },
  "highlight": {
    "fields": {
      "name": {},
      "desc": {}
    }
  }
}

执行结果:
在这里插入图片描述
上面这种是最简单的高亮结果,除此之外,还可以通过pre_tags、post_tags、type、boundary_scanner_locale等等配置属性对高亮内容进行更多更加个性化的配置。

2)自定义高亮内容包裹的标签

GET kuangshen/_search
{
  "query": {
    "match": {
      "name": "狂神"
    }
  },
  "highlight": {
    "pre_tags": "<p class='key' style='color:red'>",
    "post_tags": "</p>",
    "fields": {
      "name": {}
    }
  }
}

查询结果
在这里插入图片描述
这些高亮的属性除了可以作为highlight节点的子节点,也可以作为每一个字段的子节点,作为每个字段的子节点时,可以为每个字段设置不同的高亮配置信息,如有的字段的高亮为红色,有的字段的高亮为蓝色等。

总结

ES所能做的查询,MySQL基本上都能做,但是由于ES是基于Lucene,所以ES查询效率要比MySQL快很多。
* 匹配
* 按条件匹配
* 精确匹配
* 区间范围匹配
* 匹配字段过滤
* 多条件查询
* 高亮显示

SpringBoot集成ES详解

熟悉官方文档

打开ElasticSearch的官方文档:https://www.elastic.co/guide/index.html
可以看到有一个“Elasticsearch Clients”的链接
在这里插入图片描述
点击进入后,可以看到很多对应的链接,由于我使用的是ElasticSearch 8.1.0版本,所以这里使用官方推荐的Java Client
在这里插入图片描述
在介绍章节,可以看到可以看到Java API客户端关于功能特性、其和7.15版本相比较的变化及兼容性方面的描述。
了解完基本信息后,进入Introduction章节。

1、找到对应的maven依赖:

  <dependencies>
    <dependency>
      <groupId>co.elastic.clients</groupId>
      <artifactId>elasticsearch-java</artifactId>
      <version>8.1.1</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.12.3</version>
    </dependency>
  </dependencies>

2、进入Connecting链接,查看如何创建链接

// Create the low-level client
RestClient restClient = RestClient.builder(
    new HttpHost("localhost", 9200)).build();

// Create the transport with a Jackson mapper
ElasticsearchTransport transport = new RestClientTransport(
    restClient, new JacksonJsonpMapper());

// And create the API client
ElasticsearchClient client = new ElasticsearchClient(transport);

3、通过API conventions可以了解如何使用ElasticsearchClient对象和其有哪些方法。
4、由于在使用ElasticSearch时,我们经常会先在Kibana中编写对应的PUT、GET、POST、DELETE语句进行测试,然后再在程序中实现这些语句,考虑到从JSON代码转换到Java代码过程的繁琐和容易出错的情况,ElasticSearch为开发者提供了一个加载JSON的方法——withJson()。具体用法在Creating API objects from JSON data中有相应的介绍。

在代码中集成ElasticSearch Java Client API

源代码地址:https://gitee.com/whh306318848/springboot-es
参考资料:
* https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/connecting.html
* https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/api-conventions.html
* https://blog.csdn.net/yscjhghngh/article/details/123620860?utm_term=springboot%20es%208.1.0%20pom&utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2~all~sobaiduweb~default-0-123620860&spm=3001.4430

1、创建项目:
https://start.spring.io/创建项目,Group为com.kuang,Artifact为elasticsearch-01-api,Package Name为com.kuang,Java版本为8,然后添加依赖:
* Spring Boot DevTools
* Lombok
* Spring Configuration Processor
* Spring Web
* Spring Data Elasticsearch (Access+Driver)

在这里插入图片描述
然后将构建好的项目下载下来,使用IDEA打开。

若使用IDEA直接创建也是一样的内容:
在这里插入图片描述

在这里插入图片描述
2、使用maven下载依赖包:
当项目导入IDEA后,使用maven下载项目所需的依赖包,这里建议可以将maven镜像换成阿里云的,具体操作方法可自行百度。
下载完依赖包之后,可以查看对应的spring-data-elasticsearch的底层依赖,发现其所使用的相关ES依赖和我们前面在ES官网上看到的不一致,这会导致我们无法正常的链接ES服务,这时我们需要对依赖做出相应的调整。
ES依赖的版本必须和ES服务器的版本对应
在这里插入图片描述
将pom配置文件中的spring-boot-starter-data-elasticsearch依赖替换成ES官方指定的依赖,然后重新编译项目。pom文件内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.kuang</groupId>
    <artifactId>elasticsearch-01-api</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>elasticsearch-01-api</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <!--        <dependency>-->
        <!--            <groupId>org.springframework.boot</groupId>-->
        <!--            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>-->
        <!--        </dependency>-->
        <!--    引入ES官网推荐的依赖包 START    -->
        <dependency>
            <groupId>co.elastic.clients</groupId>
            <artifactId>elasticsearch-java</artifactId>
            <version>8.1.1</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.12.3</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.glassfish/jakarta.json -->
        <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>jakarta.json</artifactId>
            <version>2.0.1</version>
        </dependency>

        <!--    引入ES官网推荐的依赖包 END    -->

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

3、创建配置类:
根据官方文档,需要创建一个ES的API client,这里使用配置类的方式对这个对象进行配置和管理。
创建package包,并在其下创建ElasticSearchClientConfig配置类,具体代码如下:

@Configuration
public class ElasticSearchClientConfig {

    @Bean
    public ElasticsearchClient elasticsearchClient() {
        // Create the low-level client
        RestClient restClient = RestClient.builder(
                new HttpHost("127.0.0.1", 9200)).build();
        // Create the transport with a Jackson mapper
        ElasticsearchTransport transport = new RestClientTransport(
                restClient, new JacksonJsonpMapper());
        // And create the API client
        ElasticsearchClient client = new ElasticsearchClient(transport);
        return client;
    }
}

3、编写测试代码:
在测试类中编写测试代码:
1)创建一个索引

@SpringBootTest
class Elasticsearch01ApiApplicationTests {

    @Autowired
    private ElasticsearchClient elasticsearchClient;

    // 测试创建索引
    @Test
    void testCeateIndex() {
        try {
            // 1、创建索引
            CreateIndexRequest kuang_index = new CreateIndexRequest.Builder().index("kuang_index").build();
            // 2、客户端执行请求 ElasticsearchIndicesClient,请求后获得响应
            CreateIndexResponse createIndexResponse = elasticsearchClient.indices().create(kuang_index);
            System.out.println(createIndexResponse);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

代码执行后,可以在Kibana中执行GET kuang_index/命令,就可以看到上述代码创建的索引了
在这里插入图片描述

关于索引的API操作详解

1、创建索引

// 测试创建索引
@Test
void testCeateIndex() throws IOException {
    CreateIndexResponse indexResponse = elasticsearchClient.indices().create(c->c.index(ESConst.ES_INDEX));
    System.out.printf("创建索引结果:" + indexResponse);
}

上面的代码,执行结果是创建的索引对象。相当于在Kibana中执行PUT kuang_index/命令。
其中ESConst.ES_INDEX是自定义的常量,其值为字符串kuang_index
2、判断索引是否存在

// 判断索引是否存在
@Test
void testExistIndex() throws IOException {
    BooleanResponse indexResponse = elasticsearchClient.indices().exists(e -> e.index(ESConst.ES_INDEX));
    System.out.printf("查询索引结果:" + indexResponse.value());
}

上面的代码执行结果,若该索引存在,则返回true,否则返回false。
在这里插入图片描述
3、删除索引

// 删除索引
@Test
void testDeleteIndex() throws IOException {
    DeleteIndexResponse indexResponse = elasticsearchClient.indices().delete(d -> d.index(ESConst.ES_INDEX));
    System.out.printf("删除索引结果:" + indexResponse.acknowledged());
}

执行结果如下:
在这里插入图片描述
上面的代码相当于在Kibana中执行DELETE kuang_index/命令。其返回值也正好与执行上述命令的返回值对应。
在这里插入图片描述

关于文档的API操作详解

1、创建文档
一般需要放入ES的都是一个个的对象或者JSON字符串,所以先创建一个测试用的实体类

package com.kuang.pojo;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    // 姓名
    private String name;
    // 年龄
    private Integer age;
}

然后在测试类中编写测试代码:

// 插入一个文档
@Test
void testAddDocument() throws IOException {
    User user = new User("张三", 18);
    IndexResponse indexResponse = elasticsearchClient.index(i -> i
            .index(ESConst.ES_INDEX)    // 指定索引
            .id("1")                    // 指定ID,若不指定则随机生成
            .document(user));           // 指定插入的文档内容
    System.out.printf("插入一个文档:" + indexResponse.result());
}

执行结果如下:
在这里插入图片描述
可以看到,其执行结果返回的是Created(已创建),如果是该ID已存在,这里则会返回Updated(已更新),同时也可以通过indexResponse.version()的方式获取版本号信息,从而判断该id是否已经存在。
上面的代码相当于在Kibana中执行以下命令:

PUT kuang_index/_doc/1
{
  "name": "张三",
  "age": 18
}

执行上面的代码之后,可以在Kibana中使用GET kuang_index/_doc/1命令查看刚才插入的数据
在这里插入图片描述
2、更新文档
除了上面说的使用类似于PUT命令的方式覆盖更新现有数据外,还可以对单个字段进行更新。

// 更新一个文档
@Test
void testUpdateDocument() throws IOException {
    User user = new User("李四", 20);
    UpdateResponse<User> updateResponse = elasticsearchClient.update(userBuilder -> userBuilder
            .index(ESConst.ES_INDEX)
            .id("1")
            .doc(user), User.class);
    System.out.printf("更新一个文档:" + updateResponse.result());
}

执行结果如下:
在这里插入图片描述
上面的代码相当于在Kibana中执行以下命令:

POST kuang_index/_update/1
{
  "doc": {
    "name": "李四",
    "age": 20
  }
}

3、删除文档

// 删除一个文档
@Test
void testDeleteDocument() throws IOException {
    DeleteResponse deleteResponse = elasticsearchClient.delete(d -> d
            .index(ESConst.ES_INDEX)
            .id("1"));
    System.out.printf("删除一个文档:" + deleteResponse.result());
}

执行结果如下:
在这里插入图片描述
上面的代码相当于在Kibana中执行命令:

DELETE kuang_index/_doc/1

4、批量操作文档
先演示批量插入操作:

// 批量插入数据
@Test
void testBulkAddDocument() throws IOException {
    // 准备要批量插入的数据
    List<User> userList = new ArrayList<>();
    userList.add(new User("张三", 18));
    userList.add(new User("李四", 19));
    userList.add(new User("王五", 20));
    userList.add(new User("赵六", 21));
    userList.add(new User("测试1", 30));
    userList.add(new User("测试2", 40));

    List<BulkOperation> bulkOperationList = new ArrayList<>();
    for (int i = 0; i < userList.size(); i++) {
        int id = i + 1;
        bulkOperationList.add(BulkOperation.of(fn -> fn.index(u -> u
                .id(String.valueOf(id))         // 这里设置文档的ID,如果不写,系统会生成随机ID
                .document(userList.get(id - 1)))));
    }

    BulkResponse bulkResponse = elasticsearchClient.bulk(builder -> builder.index(ESConst.ES_INDEX).operations(bulkOperationList));
    System.out.printf("批量插入数据:" + bulkResponse.items());
}

执行结果如下如所示:
在这里插入图片描述
若执行过程中出现了问题,则会返回true,除了使用.errors()方法判断执行结果之外,也可以通过BulkResponse对象的items()方法获取到每一个操作的具体执行结果,然后遍历查看具体每一个操作的执行情况。

批量更新文档代码如下:

// 批量更新文档
@Test
void testBulkUpdateDocument() throws IOException {
    // 准备要批量插入的数据
    List<User> userList = new ArrayList<>();
    userList.add(new User("张三", 28));
    userList.add(new User("李四", 29));

    List<BulkOperation> bulkOperationList = new ArrayList<>();
    for (int i = 0; i < userList.size(); i++) {
        int id = i + 1;
        bulkOperationList.add(BulkOperation.of(fn -> fn.update(io -> io
                .id(String.valueOf(id))         // 需要更新的文档的id
                .action(a -> a.doc(userList.get(id - 1))))));
    }
    BulkResponse bulkResponse = elasticsearchClient.bulk(builder -> builder.index(ESConst.ES_INDEX).operations(bulkOperationList));
    System.out.printf("批量更新数据:" + bulkResponse.errors());
}

批量删除文档代码如下:

// 批量删除数据
@Test
void testBulkDeleteDocument() throws IOException {
    List<BulkOperation> bulkOperationList = new ArrayList<>();
    bulkOperationList.add(BulkOperation.of(fn -> fn.delete(d -> d.id("3"))));
    bulkOperationList.add(BulkOperation.of(fn -> fn.delete(d -> d.id("4"))));

    BulkResponse bulkResponse = elasticsearchClient.bulk(builder -> builder.index(ESConst.ES_INDEX).operations(bulkOperationList));
    System.out.printf("批量删除数据:" + bulkResponse.errors());
}

上述批量操作代码执行之后,可以通过Kibana或者ElasticSearch-hand插件查看ES中的数据信息。
5、搜索文档
使用Java API查询数据有两种方式,一种就是在Java代码中编写查询条件,第二种则是在Kibana中先写好命令,然后通过Java API提供的.withJson()方法读入JSON文件的方式,自动构建查询条件。
第一种,在代码中编写查询条件:

// 根据条件查询数据
@Test
void testSearch() throws IOException {
    // 查询name字段,被分词器分词之后包含“赵”字的数据,与下面的命令等价
//        GET kuang_index/_search
//        {
//            "query": {
//            "term": {
//                "name": {
//                    "value": "赵"
//                }
//            }
//        }
//        }
    SearchResponse<User> searchResponse = elasticsearchClient.search(s -> s
            .index(ESConst.ES_INDEX)        // 设置查询的索引
            .timeout("1s")      // 设置查询超时时间
            // 设置查询的具体条件
            .query(q -> q
                    .term(t -> t
                            .field("name")
                            .value(v -> v.stringValue("赵"))
                    )
            )
            // 设置高亮字段
            .highlight(h -> h
                    .fields("name", f->f
                            .preTags("<p class='key' style='color:red'>")
                            .postTags("</p>")
                    )
            ), User.class);

    // 查询age字段不等于28的数据,与下面的命令等价
//        GET kuang_index/_search
//        {
//            "query": {
//            "bool": {
//                "must_not": [
//                {
//                    "match": {
//                    "age": 28
//                }
//                }
//      ]
//            }
//        },
//            "from": 0,
//                "size": 5
//        }
//        SearchResponse<User> searchResponse = elasticsearchClient.search(s -> s
//                .index(ESConst.ES_INDEX)        // 设置查询的索引
//                .timeout("1s")      // 设置查询超时时间
//                .from(0)        // 分页的偏移量
//                .size(5)         // 当前页面的数据条数
//                // 设置查询的具体条件
//                .query(q -> q
//                        .bool(b -> b
//                                .mustNot(m -> m
//                                        .match(fn -> fn
//                                                .field("age")
//                                                .query(28)
//                                        )
//                                )
//                        )
//                ), User.class);
    if (searchResponse == null || searchResponse.hits() == null || searchResponse.hits().hits() == null || searchResponse.hits().hits().size() < 1) {
        System.out.println("未查询到相关数据");
        return;
    }
    for (Hit<User> hit : searchResponse.hits().hits()) {
        System.out.println(hit.source().toString());
        if (hit.highlight() != null && hit.highlight().size() > 0) {
            for (Map.Entry<String, List<String>> entry : hit.highlight().entrySet()) {
                // 如果有高亮内容,则打印高亮内容
                System.out.println("Key = " + entry.getKey());
                entry.getValue().forEach(System.out::println);
            }
        }
    }
}

通过上面的例子可以看到,只要能够在Kibana中写出查询条件,都可以对应的转换为Java代码,上面的例子中只列举了几种简单的查询条件,其他查询方式,可自行练习。

第二种,通过JSON文件编写查询条件:
假设我们在Kibana中的命令如下:

GET kuang_index/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "range": {
            "age": {
              "gte": 20,
              "lt": 30
            }
          }
        }
      ]
    }
  },
  "from": 0,
  "size": 2
}

那我们可以将上面命令中的JSON字符串部分放入文件(我是在resource目录下新建了一个searchTerms目录,然后在里面创建json文件,放在其他地方也是可以的)

{
  "query": {
    "bool": {
      "filter": [
        {
          "range": {
            "age": {
              "gte": 20,
              "lt": 30
            }
          }
        }
      ]
    }
  },
  "from": 0,
  "size": 2
}

然后编写代码

// 读取JSON文件,进行数据查询
@Test
void testSearchWithJson() throws IOException {
    // 读取查询条件的JSON文件
    ClassPathResource classPathResource = new ClassPathResource("searchTerms/age.json");
    File file = classPathResource.getFile();
    FileReader reader = new FileReader(file);

    SearchResponse<User> searchResponse = elasticsearchClient.search(s -> s
            .index(ESConst.ES_INDEX)
            .withJson(reader), User.class);

    if (searchResponse == null || searchResponse.hits() == null || searchResponse.hits().hits() == null || searchResponse.hits().hits().size() < 1) {
        System.out.println("未查询到相关数据");
        return;
    }
    for (Hit<User> hit : searchResponse.hits().hits()) {
        System.out.println(hit.source().toString());
        if (hit.highlight() != null && hit.highlight().size() > 0) {
            for (Map.Entry<String, List<String>> entry : hit.highlight().entrySet()) {
                // 如果有高亮内容,则打印高亮内容
                System.out.println("Key = " + entry.getKey());
                entry.getValue().forEach(System.out::println);
            }
        }
    }
}

可以看到,使用JSON字符串构建查询条件,与直接在代码中编写查询条件的效果是完全一样的,而且由于少了从json到java代码的转换过程,这样还更加方便了。
除了查询外,.withJson()方法也可以用于创建索引、插入数据、更新数据等操作。

京东搜索项目

项目搭建

1、新建一个项目
在这里插入图片描述
勾选一些依赖项,然后创建项目。
在这里插入图片描述
2、打开pom.xml文件,添加ElasticSearch相关的依赖信息

<!--    引入ES官网推荐的依赖包 START    -->
<dependency>
    <groupId>co.elastic.clients</groupId>
    <artifactId>elasticsearch-java</artifactId>
    <version>8.1.1</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.12.3</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.glassfish/jakarta.json -->
<dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>jakarta.json</artifactId>
    <version>2.0.1</version>
</dependency>
<!--    引入ES官网推荐的依赖包 END    -->

3、打开application.properties文件填写基本配置

server.port=9090
# 关闭thymeleaf的缓存
spring.thymeleaf.cache=false

4、将前端代码放入工程resources目录下
前端代码下载链接:https://pan.baidu.com/s/1PT3jLvCksOhq7kgAKzQm7g 提取码:s824
放入后,目录结构如下:
在这里插入图片描述
5、添加页面控制器
新建controllerpackage,在其下新建IndexController控制器类,编写如下代码

@Controller
public class IndexController {

    @GetMapping({"/", "/index"})
    public String index() {
        return "index";
    }
}

6、启动项目
启动项目,然后再浏览器中访问http://localhost:9090/,若能够看到如下界面,则代表项目搭建成功。
在这里插入图片描述

爬取数据

接下来,需要想办法获取到数据,打开京东官网,在搜索框中搜索关键字(如:Java),观察其url,可以发现其具有一定的规则。
在这里插入图片描述
经过测试,发现红框部分是真正对搜索有用的url,根据这个特点,加上京东搜索结果页面的格式,我们可以使用Java代码发起模拟的请求,然后通过解析京东返回的html代码,将其中的数据提取出来。
为了满足上面描述的这个过程,我们需要使用到jsoup包实现对html代码的爬取。
1、在pom.xml中导入jsoup依赖

<!--    解析网页,引入jsoup包 START    -->
<dependency>
    <groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <version>1.14.3</version>
</dependency>
<!--    解析网页,引入jsoup包 END    -->

注意:jsoup包只能用来爬取网页,不能用来下载电影 、音乐等,如果需要下载电影、音乐等,可以使用tika包。
3、新建一个Bean承接从京东解析到的数据
新建pojopackage,在其下新建Commodity类。

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Commodity {
    private String id;
    private String title;
    private String img_url;
    private String price_str;
    private Double price;

    public void setPrice_str(String price_str) {
        this.price_str = price_str;
        if (this.price_str != null && !this.price_str.trim().equals("")) {
            this.price = Double.valueOf(price_str);
        }
    }

    public void setPrice(Double price) {
        this.price = price;
        this.price_str = String.format("%.2f", this.price);
    }
}

从HTML中解析出来的价格数据是String类型的,为了方便后续的查询,可以自己重写Set方法,在set的时候将其转换成Double型。
2、编写解析HTML工具类:
新建utilspackage,在其下新建HtmlParseUtil类。

@Component
public class HtmlParseUtil {

    /**
     * 在京东搜索关键字并解析其结果
     *
     * @param keyword 要搜索的关键字
     * @return {@link List< Commodity>} 返回一个商品列表
     * @throws IOException 解析HTML代码时可能会抛出该异常
     * @author: wuhaohua
     * @date: 2022/4/10
     * @description:
     **/
    public List<Commodity> parseJD(String keyword) throws IOException {
        // 获取请求:https://search.jd.com/Search?keyword=java
        // 注意:使用jsoup无法获取ajax请求,如果想获取ajax请求返回的数据,需要单独用java代码模拟ajax请求
        String url = "https://search.jd.com/Search?keyword=" + keyword.trim();
        // 解析网页(Jsoup返回的Document对象就是浏览器中的Document对象),并设置超时时间为10秒
        Document parse = Jsoup.parse(new URL(url), 10000);
        // 所有在js代码中可以使用的方法,这里也同样可以使用
        // 分析京东网站的代码,获取存放搜索结果的div元素
        Element element = parse.getElementById("J_goodsList");
        // 打印获取的源码
//        System.out.println(element.html());

        // 暂存解析出来的数据
        List<Commodity> result = new ArrayList<>();

        // 获取每一个搜索结果的列表元素
        Elements elements = element.getElementsByTag("li");
        // 获取元素的内容,这里的el就是每一个标签了
        for (Element el : elements) {
            // 获取我们需要的数据
            // 使用下面的这种方法,是无法获取到图片的url的,因为图片是懒加载的方式加载的,当页面刚请求回来时,该图片还没有被加载
//            String img_url = el.getElementsByTag("img").eq(0).attr("src");`
            // 通过分析源码,可以得知在懒加载完成前,图片的url存放在data-lazy-img属性里
            String img_url = el.getElementsByTag("img").eq(0).attr("data-lazy-img");
            String price_str = el.getElementsByClass("p-price").get(0).getElementsByTag("i").eq(0).text();
            String title = el.getElementsByClass("p-name").eq(0).text();
//            System.out.println("标题:" + title + ",价格:" + price_str + ",图片:" + img_url);
            // 将解析的参数放入对象
            Commodity commodity = new Commodity();
            commodity.setId(id);
            commodity.setImg_url(img_url);
            commodity.setPrice_str(price_str);
            commodity.setTitle(title);
            result.add(commodity);
        }

        return result;
    }
}

业务编写

1、将之前编写的ElasticSearchClientConfig类放入本项目
新建configpackage,将ElasticSearchClientConfig类复制到包内。

@Configuration
public class ElasticSearchClientConfig {

    @Bean
    public ElasticsearchClient elasticsearchClient() {
        // Create the low-level client
        RestClient restClient = RestClient.builder(
                new HttpHost("127.0.0.1", 9200)).build();
        // Create the transport with a Jackson mapper
        ElasticsearchTransport transport = new RestClientTransport(
                restClient, new JacksonJsonpMapper());
        // And create the API client
        ElasticsearchClient client = new ElasticsearchClient(transport);
        return client;
    }
}

2、编写Service代码
新建servicepackage,在其下新建CommodityService类。

@Service
public class CommodityService {

    @Autowired
    private ElasticsearchClient client;

    // ES索引名称
    private static final String ES_INDEX = "jd_goods";

    /**
     * 解析商品信息
     *
     * @param keyword 搜索商品的关键字
     * @return {@link Boolean} 解析成功返回true,失败返回false
     * @throws
     * @author: wuhaohua
     * @date: 2022/4/10
     **/
    public Boolean parseCommodity(String keyword) {
        // 解析数据
        List<Commodity> commodityList = null;
        try {
            commodityList = new HtmlParseUtil().parseJD(keyword);
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
        if (commodityList == null || commodityList.size() < 1) {
            return false;
        }
        // 把查询出来的数据批量放入ES中
        List<BulkOperation> bulkOperationList = new ArrayList<>();
        for (Commodity commodity : commodityList) {
            bulkOperationList.add(BulkOperation.of(fn -> fn
                            .index(io -> io
                                    .id(commodity.getId())
                                    .document(commodity)
                            )
                    )
            );
        }
        try {
            BulkResponse bulkResponse = client.bulk(builder -> builder
                    .index(ES_INDEX)
                    .operations(bulkOperationList)
            );
            // 如果执行出现错误,则返回失败
            if (bulkResponse.errors()) {
                List<BulkResponseItem> items = bulkResponse.items();
                for (BulkResponseItem item : items) {
                    System.out.println(item.error().reason());
                }
                return false;
            }
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }

        return true;
    }

    /**
     * 从ES中搜索相关的数据
     *
     * @param keyword  搜索关键字
     * @param pageNum  页码
     * @param pageSize 页面大小
     * @return {@link List<Commodity>} 搜索结果
     * @author: wuhaohua
     * @date: 2022/4/10
     **/
    public List<Commodity> searchPage(String keyword, int pageNum, int pageSize) throws IOException {
        if (pageNum < 1) {
            pageNum = 1;
        }

        // 计算分页偏移量的起始值
        int from = (pageNum - 1) * pageSize;

        // 条件搜索
        SearchResponse<Commodity> commoditySearchResponse = client.search(s -> s
                .index(ES_INDEX)
                .from(from)
                .size(pageSize)
                .query(q -> q
                        .match(m -> m
                                .field("title")
                                .query(keyword)
                        )
                ), Commodity.class);
        if (commoditySearchResponse == null || commoditySearchResponse.hits() == null || commoditySearchResponse.hits().hits() == null || commoditySearchResponse.hits().hits().size() < 1) {
            System.out.println("未查询到相关数据");
            return null;
        }
        List<Commodity> result = new ArrayList<>();
        for (Hit<Commodity> hit : commoditySearchResponse.hits().hits()) {
            result.add(hit.source());
        }

        return result;
    }
}

3、编写Controller代码
controllerpackage中新建CommodityController类。

@RestController
public class CommodityController {

    @Autowired
    private CommodityService commodityService;

    @GetMapping("/parse/{keyword}")
    public Boolean parse(@PathVariable("keyword") String keyword) {
        return commodityService.parseCommodity(keyword);
    }

    @GetMapping("/search/{keyword}/{pageNum}/{pageSize}")
    public List<Commodity> search(@PathVariable("keyword") String keyword,
                                  @PathVariable("pageNum") int pageNum,
                                  @PathVariable("pageSize") int pageSize) throws IOException {
        return commodityService.searchPage(keyword, pageNum, pageSize);

    }
}

4、新建jd_goods索引
在ElasticSearch-hand中,新建jd_goods索引。
在这里插入图片描述
6、启动SpringBoot测试解析代码
启动SpringBoot,在浏览器中访问http://localhost:9090/parse/java,查看其返回值,如果是true,则代表解析代码通过测试,此时可以在Kibana中查看jd_goods相关的数据。
在这里插入图片描述
然后再在浏览器中访问http://localhost:9090/search/java/1/10,查看其返回值,若能返回对应的json对象,则代表代码正确。
在这里插入图片描述

前后端分离

在项目中引入vue.js和axios.js代码
在这里插入图片描述
然后修改index.html文件,编写js控制代码和axios网络请求代码。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="utf-8"/>
    <title>狂神说Java-ES仿京东实战</title>
    <link rel="stylesheet" th:href="@{/css/style.css}"/>
</head>

<body class="pg">
<div class="page" id="app">
    <div id="mallPage" class=" mallist tmall- page-not-market ">

        <!-- 头部搜索 -->
        <div id="header" class=" header-list-app">
            <div class="headerLayout">
                <div class="headerCon ">
                    <!-- Logo-->
                    <h1 id="mallLogo">
                        <img th:src="@{/images/jdlogo.png}" alt="">
                    </h1>

                    <div class="header-extra">

                        <!--搜索-->
                        <div id="mallSearch" class="mall-search">
                            <form name="searchTop" class="mallSearch-form clearfix">
                                <fieldset>
                                    <legend>天猫搜索</legend>
                                    <div class="mallSearch-input clearfix">
                                        <div class="s-combobox" id="s-combobox-685">
                                            <div class="s-combobox-input-wrap">
                                                <input v-model="keyword" type="text" autocomplete="off" value="dd"
                                                       id="mq"
                                                       class="s-combobox-input" aria-haspopup="true">
                                            </div>
                                        </div>
                                        <button type="submit" @click.prevent="searchKey" id="searchbtn">搜索</button>
                                    </div>
                                </fieldset>
                            </form>
                            <ul class="relKeyTop">
                                <li><a>狂神说Java</a></li>
                                <li><a>狂神说前端</a></li>
                                <li><a>狂神说Linux</a></li>
                                <li><a>狂神说大数据</a></li>
                                <li><a>狂神聊理财</a></li>
                            </ul>
                        </div>
                    </div>
                </div>
            </div>
        </div>

        <!-- 商品详情页面 -->
        <div id="content">
            <div class="main">
                <!-- 品牌分类 -->
                <form class="navAttrsForm">
                    <div class="attrs j_NavAttrs" style="display:block">
                        <div class="brandAttr j_nav_brand">
                            <div class="j_Brand attr">
                                <div class="attrKey">
                                    品牌
                                </div>
                                <div class="attrValues">
                                    <ul class="av-collapse row-2">
                                        <li><a href="#"> 狂神说 </a></li>
                                        <li><a href="#"> Java </a></li>
                                    </ul>
                                </div>
                            </div>
                        </div>
                    </div>
                </form>

                <!-- 排序规则 -->
                <div class="filter clearfix">
                    <a class="fSort fSort-cur">综合<i class="f-ico-arrow-d"></i></a>
                    <a class="fSort">人气<i class="f-ico-arrow-d"></i></a>
                    <a class="fSort">新品<i class="f-ico-arrow-d"></i></a>
                    <a class="fSort">销量<i class="f-ico-arrow-d"></i></a>
                    <a class="fSort">价格<i class="f-ico-triangle-mt"></i><i class="f-ico-triangle-mb"></i></a>
                </div>

                <!-- 商品详情 -->
                <div class="view grid-nosku">

                    <div class="product" v-for="result in results">
                        <div class="product-iWrap" sku="{{result.id}}">
                            <!--商品封面-->
                            <div class="productImg-wrap">
                                <a class="productImg">
                                    <img :src="result.img_url">
                                </a>
                            </div>
                            <!--价格-->
                            <p class="productPrice">
                                <em><b>¥</b>{{result.price_str}}</em>
                            </p>
                            <!--标题-->
                            <p class="productTitle">
                                <a> {{result.title}} </a>
                            </p>
                            <!-- 店铺名 -->
                            <div class="productShop">
                                <span>店铺: 狂神说Java </span>
                            </div>
                            <!-- 成交信息 -->
                            <p class="productStatus">
                                <span>月成交<em>999笔</em></span>
                                <span>评价 <a>3</a></span>
                            </p>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

<script th:src="@{/js/axios.js}"></script>
<script th:src="@{/js/vue.js}"></script>
<script>
    new Vue({
        el: "#app",
        data: {
            keyword: "",    // 搜索关键字
            results: []     // 搜索结果
        },
        methods: {
            searchKey() {
                var keyword = this.keyword;
                console.log(keyword);
                // 对接后端的接口
                axios.get("/search/" + keyword + "/1/20").then(response => {
                    if (response.status == 200 || response.status == 301 || response.status == 302) {
                        // 请求成功
                        if (response.data && response.data.length > 0) {
                            // 有数据返回
                            this.results = response.data;
                        }else  {
                            // 无数据返回
                            this.results = [];
                        }
                    }else {
                        // 请求失败
                        alert("服务器繁忙,请稍后重试,错误代码:"+response.status)
                    }
                    // console.log(response)
                })
            }
        }
    });
</script>

</body>
</html>

然后使用浏览器访问http://localhost:9090/,并在搜索框中输入java,点击搜索按钮,看到如下界面,代表代码没有问题。
在这里插入图片描述

关键字高亮实现

CommodityService类中增加一个函数,用于处理高亮关键字的功能,代码如下:

public List<Commodity> searchHighLightPage(String keyword, int pageNum, int pageSize) throws IOException {
        if (pageNum < 1) {
            pageNum = 1;
        }

        // 计算分页偏移量的起始值
        int from = (pageNum - 1) * pageSize;

        // 条件搜索
        SearchResponse<Commodity> commoditySearchResponse = client.search(s -> s
                .index(ES_INDEX)
                .from(from)
                .size(pageSize)
                .highlight(h -> h
                        .requireFieldMatch(false)       // 只需要第一次匹配的地方高亮
                        .fields("title", fn -> fn   // 设置高亮字段
                                .preTags("<span style='color:red'>")   // 设置高亮标签的前缀
                                .postTags("</span>")                    // 设置高亮标签的后缀
                        )
                )
                .query(q -> q
                        .match(m -> m
                                .field("title")
                                .query(keyword)
                        )
                ), Commodity.class);
        if (commoditySearchResponse == null || commoditySearchResponse.hits() == null || commoditySearchResponse.hits().hits() == null || commoditySearchResponse.hits().hits().size() < 1) {
            System.out.println("未查询到相关数据");
            return null;
        }
        List<Commodity> result = new ArrayList<>();
        for (Hit<Commodity> hit : commoditySearchResponse.hits().hits()) {
            // 搜索出来的结果
            Commodity commodityTemp = hit.source();
            // 解析高亮的字段
            if (hit.highlight() != null && hit.highlight().get("title") != null) {
                // 如果有高亮内容,则替换标题
                List<String> hightLightTitleList = hit.highlight().get("title");
                if (hightLightTitleList.size() > 0) {
                    String newTitle = "";
                    for (String highLightTemp: hightLightTitleList) {
                        newTitle += highLightTemp.trim();
                    }
                    if (!newTitle.trim().equals("")) {
                        commodityTemp.setTitle(newTitle);
                    }
                }
            }
            result.add(commodityTemp);
        }

        return result;
    }

可以看到,上面的代码与普通的搜索代码只是在条件搜索部分增加了关于高亮字段的相关设置

.highlight(h -> h
        .requireFieldMatch(false)       // 只需要第一次匹配的地方高亮
        .fields("title", fn -> fn   // 设置高亮字段
                .preTags("<span style='color:red'>")   // 设置高亮标签的前缀
                .postTags("</span>")                    // 设置高亮标签的后缀
        )
)

然后在结果解析部分,使用高亮的结果替换掉了商品的title字段的内容:

for (Hit<Commodity> hit : commoditySearchResponse.hits().hits()) {
    // 搜索出来的结果
    Commodity commodityTemp = hit.source();
    // 解析高亮的字段
    if (hit.highlight() != null && hit.highlight().get("title") != null) {
        // 如果有高亮内容,则替换标题
        List<String> hightLightTitleList = hit.highlight().get("title");
        if (hightLightTitleList.size() > 0) {
            String newTitle = "";
            for (String highLightTemp: hightLightTitleList) {
                newTitle += highLightTemp.trim();
            }
            if (!newTitle.trim().equals("")) {
                commodityTemp.setTitle(newTitle);
            }
        }
    }
    result.add(commodityTemp);
}

然后在controller代码中调用该函数

@GetMapping("/searchHighLight/{keyword}/{pageNum}/{pageSize}")
    public List<Commodity> searchHighLight(@PathVariable("keyword") String keyword,
                                  @PathVariable("pageNum") int pageNum,
                                  @PathVariable("pageSize") int pageSize) throws IOException {
        if (pageNum < 1) {
            pageNum = 1;
        }
        if (pageSize < 1) {
            pageSize = 10;
        }

        //查询数据
        return commodityService.searchHighLightPage(keyword, pageNum, pageSize);
    }

同时,前端代码index.html中也需要做两处调整,一处是在商品标题的渲染代码,需要由原来的

<!--标题-->
<p class="productTitle">
    <a>{{result.title}}</a>
</p>

修改为:

<!--标题-->
<p class="productTitle">
    <a v-html="result.title"></a>
</p>

以便浏览器能够正确解析高亮的前缀和后缀。
第二处是修改js代码中修改请求路径为最新的url

axios.get("/searchHighLight/" + keyword + "/1/20").then(response => {

此时,强制刷新浏览器(Ctrl + F5),并搜索java,如果能看到以下效果,则表示代码正确。
在这里插入图片描述
可以看到,所有商品标题的第一个Java单词都被高亮显示了。

至此,本学习笔记结束。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据