封面来源:碧蓝航线 北境序曲 活动CG

本文参考:【狂神说Java】ElasticSearch7.6.x最新完整教程通俗易懂

本文内容基于 ElasticSearch 7.6.x,本文包含大量图片,请保持较好的网络连接。

1. ElasticSearch 概述

概述

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

Elasticsearch 是一个实时分布式搜索和分析引擎,它让你以前所未有的速度处理大数据成为可能。

主要用于全文搜索结构化搜索分析以及将这三者混合使用。

Elasticsearch 是一个基于 Apache Lucene™ 的开源搜索引擎。无论在开源还是专有领域,Lucene 可以 被认为是迄今为止先进、性能好的、功能全的搜索引擎库。

但是,Lucene 只是一个库。想要使用它,你必须使用 Java 来作为开发语言并将其直接集成到你的应用 中,更糟糕的是,Lucene 非常复杂,你需要深入了解检索的相关知识来理解它是如何工作的。 Elasticsearch 也使用 Java 开发并使用 Lucene 作为其核心来实现所有索引和搜索的功能,但是它的目的是 通过简单的 RESTful API 来隐藏 Lucene 的复杂性,从而让全文搜索变得简单。

谁在使用

1、维基百科,实现全文搜索、高亮、搜索推荐等

2、The Guardian(国外新闻网站),用户行为日志、社交网络数据、数据分析、给到每篇新闻文章的作者,让他知道他的文章的公众反馈

3、Stack Overflow

4、GitHub

5、电商网站,检索商品

6、日志数据分析

7、商品价格监控网站

等等…

2. 与 Solr 的区别

2.1 Solr 的概述

Solr 是 Apache 下的一个顶级开源项目,采用 Java 语言开发,它是基于 Lucene 的全文搜索服务器。Solr 提供了比 Lucene 更为丰富的查询语言,同时实现了可配置、可扩展,并对索引、搜索性能进行了优化。

Solr 可以独立运行,运行在 Jetty、Tomcat 等这些 Servlet 容器中,Solr 索引的实现方法很简单,用 POST 方法向 Solr 服务器发送一个描述 Field 及其内容的 XML 文档,Solr 根据 XML 文档添加、删除、更新索引 。

Solr 搜索只需要发送 HTTP GET 请求,然后对 Solr 返回 XML、JSON 等格式的查询结果进行解析,组织页面布局。Solr 不提供构建 UI 的功能,Solr 提供了一个管理界面,通过管理界面可以查询 Solr 的配置和运行情况。

Solr 是基于 Lucene 开发企业级搜索服务器,实际上就是封装了 Lucene。Solr 是一个独立的企业级搜索应用服务器,它对外提供类似于 Web-service 的 API 接口。用户可以通过 Http 请求,向搜索引擎服务器提交一定格式的文件,生成索引,也可以通过提出查找请求,并得到返回结果。

2.2 Solr 与 ElasticSearch

参考链接:ElasticSearch和solr的差别

ElasticSearch和Solr的区别_1 ElasticSearch和Solr的区别_2 ElasticSearch和Solr的区别_3

ElasticSearch和Solr的区别_4

总结

1、ES 基本是开箱即用,非常简单,而 Solr 安装略微复杂一丢丢

2、Solr 利用 Zookeeper 进行分布式管理,而 Elasticsearch 自身带有分布式协调管理功能。

3、Solr 支持更多格式的数据,比如 JSON、XML、CSV,而 Elasticsearch 仅支持 JSON 文件格式。

4、Solr 官方提供的功能更多,而 Elasticsearch 本身更注重于核心功能,高级功能多有第三方插件提供,例如图形化界面需要 Kibana 的支撑

5、Solr 查询快,但更新索引时慢(即插入删除慢),用于电商等查询多的应用;ES 建立索引快(即查询慢),即实时性查询快,用于 Facebook、新浪等搜索。Solr 是传统搜索应用的有力解决方案,但 Elasticsearch 更适用于新兴的实时搜索应用。

6、Solr比较成熟,有一个更大,更成熟的用户、开发和贡献者社区,而 Elasticsearch 相对开发维护者较少,更新太快,学习使用成本较高(这点已经不再是 ES 的缺点了)。

3. 安装 ElasticSearch

前置要求

Java 的版本至少是 JDK 8,ElasticSearch 的版本和我们以后要导入的依赖也要版本对应!

下载

官网链接:ElasticSearch

ELK 华为镜像:ElasticSearchlogstashkibana

进入系统后,点击首页的下载按钮,进入下载页面,选择和自己系统相符的压缩包下载即可。

学习 ElasticSearch 可以使用 Linux,也可以使用 Windows,它对系统没有强制要求,为了减少不必要的操作,我们 选择在 Windows 环境上学习

下载的 ELK 压缩包解压即可使用,因为涉及到图形化界面,所以需要一些必要的环境,比如:Node.js。

安装 ElasticSearch

在 Windows 下安装十分减压,解压即可。解压后如下图所示:

解压ES

根据上面给出的示意图,来熟悉一些目录:

1
2
3
4
5
6
7
8
bin   启动文件
config 配置文件
log4j2 日志配置文件
jvm.options Java 虚拟机相关的配置
elasticsearch.yml ES 的配置文件 默认端口号:9200
lib 相关 jar 包
modules 功能模块
plugins 插件,比如 ik 分词器

熟悉之后,在 Windows 上启动一下 ES,只需要前往 bin 目录下,双击 elasticsearch.bat 文件即可启动:

ES启动信息

可以看到默认端口号确实是 9200。

不关闭服务,使用浏览器,在地址栏输入 localhost:9200 访问:

本地访问9200端口

安装 ES 可视化界面

为了便于操作,我们可以安装一个 ES 的可视化界面,相当于一个插件。

这个插件名为:ElasticSearch-head,前往 GitHub 搜索下载即可。这个插件很显然是一个前端工程,因此 Node.js 环境是必不可少的。

ElasticSearch-head 下载地址:ElasticSearch-head

下载完成后得到一个压缩包,解压到 ES 的同目录下,解压后的目录:

解压ES-head的目录

进入 head-master 目录下打开 CMD,执行命令 cnpm install

安装head-master的依赖包

依赖下好后,执行命令 npm run start 启动可视化界面:

启动ES可视化界面

可以看到可视化界面的访问端口是 9100,在浏览器上试试:

访问9100端口

那么问题也来了,一个端口是 9100,一个是 9200,这不就 跨域 了,那我们怎么解决呢?

解决也很简单,关闭 ES,进入 ES 的目录,找到 config 目录,找到 elasticsearch.yml 配置文件,进行配置即可。在文件内添加以下配置:

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

保存并退出,再次双击 elasticsearch.bat 文件运行 ES。这个时候查看可视化界面,点击连接,我们可以看到:

可视化界面连接ES

在上图中,可以看到有一个 索引 的选项。点击进入,然后新建一个名为 mofan 的索引:

ES可视化界面新建索引

在初学的时候,可以将 ES 当成一个数据库,然后可以建立库(索引),添加文档(库中的数据)。

将可视化界面 ElasticSearch-head 当成一个数据展示工具,书写查询语句在 Kibana 中操作。

4. 安装 Kibana

Kibana 简介

Kibana 是一个针对 Elasticsearch 的开源分析及可视化平台,用来搜索、查看交互存储在 Elasticsearch 索引中的数据。 使用 Kibana,可以通过各种图表进行高级数据分析及展示。Kibana 让海量数据更容易理解。它操作简单,基于浏览器的用户界面可以快速创建仪表板(dashboard)实时显示 Elasticsearch 查询动态。

设置Kibana非常简单,无需编码或者额外的基础架构,几分钟内就可以完成 Kibana 安装并启动 Elasticsearch 索引监测。

安装 Kibana

官网下载地址:Kibana

下载 Kibana 时,需要注意其版本要和 ES 版本一致。下载完毕后,解压至 ES 相同的目录中,Kibana 是一个标准的前端化工程,别看它下载了只有将近 300M,解压还是需要一点时间的。

Kibana解压完成示意图

Kibana 解压完毕后的目录:

Kibana目录

可以看到有一个 bin 目录,打卡它,能找到一个名为 kibana.bat 的文件,这就是启动文件,双击启动即可。

当然,在启动前需要先启动 ES,启动之后我们可以看到:

Kibana默认端口号

Kibana 默认的端口号是 5601。因此,可以在浏览器的地址栏输入本地 ip 与端口号,即可访问 Kibana,然后可以找到 开发者工具 的图标,点击即可进入。以后关于 ES 操作的语句我们都可以在开发者工具中书写:

Kibana开发者工具

我们还发现整个页面的语言都是英文的,Kibana 支持国际化,内置了中文语言,只不过需要设置一下。

进入 Kibana 的安装目录,找到 config 配置文件夹,然后找到 kibana.yml 文件进行配置。配置很简单,一句配置即可搞定:

1
i18n.locale: "zh-CN"

修改了配置文件然后重启 Kibana,在浏览器地址栏输入:localhost:5601 再次查看:

Kibana的汉化

奈斯!成功汉化!🎉

5. ES 的核心概念

安装完 ES 后,我们来熟悉一下 ES 中的一些核心概念,比如:集群、节点、索引、类型、文档、分片、映射等等。

ES 是面向文档的,一切都是 JSON,来看看关系型数据库与 ES 的一个小对比:

Relational DB ElasticSearch
数据库(database) 索引(indices)
表(tables) types
行(rows) documents
字段(columns) fields

ES 集群中可以包含多个索引(类似于数据库),每个索引中又可以包含多个类型(表),每个类型下又可以包含多个文档(行),每个文档下还包含多个字段(列)。

物理设计:

ES 在后台把每个索引划分成多个分片,每个分片可以在集群的不同服务器间迁移。

“一个人”就是一个集群,可以在 ElasticSearch-head 中看到默认集群名就叫:elasticsearch。

elasticsearch集群名

逻辑设计:

一个索引类型中,可以包含多个文档,比如:文档 1,文档 2。 当我们索引一篇文档时,可以通过这样一个顺序找到它:索引 ▷ 类型 ▷ 文档 ID,通过这个组合我们就能找到到某个具体的文档。

注意:ID 不必是整数,实际上它是个字符串。

5.1 文档

文档(document),就是一条条数据。比方说:

1
2
3
user
1 MOFAN 18
2 yang 20

我们知道 ES 是面向文档的,那么就是说索引和数据搜索的最小单位是文档,ES 中,文档有几个重要属性:

  • 自我包含,一篇文档同时包含字段和对应的值,也就是同时包含 key 和 value。

  • 可以是层次型的,一个文档中包含自文档,复杂的逻辑实体就是这么来的

  • 灵活的结构,文档不依赖预先定义的模式。我们知道关系型数据库中,要提前定义字段才能使用,在 ES 中,对于字段是非常灵活的,我们可以忽略该字段,或者动态的添加一个新的字段。

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

5.2 类型

这里的类型和我们以前说的类型区别不大。类型是文档的逻辑容器,就像关系型数据库一样,表格是行的容器。

类型中对于字段的定义称为映射,比如 name 映射为字符串类型。 文档是无模式的,它们不需要拥有映射中所定义的所有字段。比如新增一个字段,那么 ES 是怎么做的呢?

ES 会自动的将新字段加入映射,但是不知道这个字段是什么类型,因此 ES 就就会进行推测。比如这个值是18,那么 ES 会认为它是整型。 既然是预测,也就可能猜不对, 所以最安全的方式就是提前定义好所需要的映射,然后再使用,这就和关系型数据库差不多了。

5.3 索引

根据前面的对比,可以看出 ES 中的索引就相当于数据库。

索引是映射类型的容器,ES 中的索引是一个非常大的文档集合。索引存储了映射类型的字段和其他设置, 然后它们被存储到各个分片上。

使用 ElasticSearch-head 创建 ES 的索引时,就会出现分片数的设置:

新建索引设置分片数

一个集群中至少有一个节点,而一个节点就是一个 ES 进程,节点可以有多个索引默认。

按照上图来说,如果创建索引,那么索引将会有个 5 个分片(primary shard,又称主分片)构成,而每一个主分片还会有一个副本(replica shard,又称复制分片)。

简单来说,分片就是数据分成多个文件放在不同服务器上。

ES的集群和节点

在上图是一个有 3 个节点的集群,可以看到主分片和对应的复制分片都不会在同一个节点内,如果某个节点挂了,数据也不会丢失。 实际上,一个分片是一个 Lucene 索引,一个包含 倒排索引 的文件目录,倒排索引的结构使得 ES 在不扫描全部文档的情况下,就能告诉你哪些文档包含特定的关键字。

那么什么是倒排索引?

5.4 倒排索引

简单来说,倒排索引就是分片之后,以关键词做 key,分片 ID 做 value。

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

1
2
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
every
to ×
up

如果我们试图搜索 to forever,就只需要查看包含每个词条的文档权重:

term doc_1 doc_2
to ×
forever
total 2 1

两个文档都匹配,但是第一个文档比第二个匹配度更高。

如果没有别的条件,两个包含关键词的文档都会返回。

再看一个示例,比如我们通过博客标签来搜索博客文章。倒排索引列表有这样的一个结构:

博客文章倒排索引

如果要搜索含有 python 标签的文章,那相对于查找所有原始数据而言,查找倒排索引后的数据将会快得多。只需要查看标签这一栏,然后获取相关的文章 ID 即可,这样可以完全过滤掉无关的所有数据。

ES 的索引和 Lucene 的索引对比

在 ES 中,索引被分为多个分片,每份分片是一个 Lucene 的索引。所以一个 ES 索引是由多个 Lucene 索引组成
的。

如无特指,说起索引都是指 ES 的索引。

接下来,我们在 Kibana 中的开发者工具中编写一下 ES 的操作语句。

6. IK 分词器

6.1 什么是 IK 分词器

所谓分词,就是把一段中文或者别的划分成一个个的关键字,我们在搜索时候会把自己的信息进行分词,并把数据库中或者索引库中的数据也进行分词,然后进行一个匹配操作,默认的中文分词是将每个字看成一个词。比如:“默烦帅逼”一段字,就会分割成“默”、“烦”、“帅”和“逼”四个字,这显然是不正确的。

如果要使用中文,建议使用 IK 分词器!

IK 提供了两个分词算法:ik_smartik_max_word,其中 ik_smart 为最少切分,ik_max_word 为最细粒度划分!

6.2 下载安装

下载地址:Github 下载地址

下载完毕后(注意版本匹配),放入我们的 ES 目录中的 plugins 目录中:

解压ik

然后重启 ES:

加载ik分词器

我们可以看到,解压之后运行 ES 就自动加载了 IK 分词器。

除此之外,可以在 ES 的 bin 目录下打开 CMD,然后运行 elasticsearch-plugin list 命令来查看加载的插件:

命令行查看加载插件

6.3 IK 分词器的使用

分词算法测试

安装完毕后,我们使用 Kibana 来测试一下:

分词算法ik_smart测试

先使用分词算法 ik_smart 进行测试,可以在左边看到结果,是一种最小切分(没有重复的字)。

再使用分词算法 ik_max_word 进行测试,也可以看到结果,将所有可能的组合都列了出来,是一种最细粒度的气切分(有重复的字)。

分词算法ik_max_word测试

那为什么会切分成这样,很明显有一个字典在引导切分。

发现问题

再换个文本测试一下分词算法:

分词器问题发现

更换测试文本后,使用两种算法得到一样的结果。但这不是最重要,最重要的是:我们发现“默烦”两字被切分开了,显然是在字典中没有这个词,那怎么办呢?

就需要自己增加字典配置。

增加字典配置

操作很简单!

1、进入 IK 分词器的 config 目录,创建文件 mofan.dic,文件内添加“默烦”两字,如:

添加字典配置

2、然后打开这个目录下的 IKAnalyzer.cfg.xml 文件,拓展字典:

配置文件拓展字典

3、重启 ES,重启 Kibana。重启 ES 时,可以看到已经加载了我们配置的字典:

加载自定义字典配置

4、浏览器进入 Kibana 的开发者工具,输入与前文相同的一段字进行测试:

测试字典配置

我们发现“默烦”两字没有被拆开,而是一个词,证明我们的字典配置正确!

7. 基本操作

7.1 Rest 风格说明

Rest 是一种软件架构风格,或者说是一种规范,其强调 HTTP 应当以资源为中心,并且规范了 URI 的风格,规范了 HTTP 请求动作(GET / PUT / POST / DELETE / HEAD / OPTIONS)的使用,具有对应的语义。

基本 Rest 命令说明:

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

7.2 索引的基本操作

创建一个索引

先运行 elasticsearch-head 查看当前有哪些索引:

1
2
3
.apm-agent-configuration
.kibana_1
.kibana_task_manager_1

我们发现只有上述三个索引,并且都是 ES 自带的。

打开 Kibana,创建一个索引:

1
2
PUT /索引/~类型名~/文档id
{请求体}

创建一个索引

执行命令后,前往 elasticsearch-head 看看是否真的成功创建了索引:

查看创建的索引

再看看数据是否成功添加:

创建索引成功添加数据

指定字段类型

前面创建了一个索引,并向其中添加了数据,但是并没有指定数据的类型,那怎么指定数据的类型呢?

在 ES 中,有以下类型:

  • 字符串类型:text、keyword

  • 数值类型:long、integer、short、byte、double、float、half float、scaled float

  • 日期类型:date

  • 布尔值类型:boolean

  • 二进制类型:binary

  • 等等…

创建一个名为 test2 的索引,设置字段的类型,不添加数据:

创建索引设置字段类型

获取信息

那我们怎么获取 test2 的信息呢?这里有一个 GET 的命令可以获取:

查看test2索引的信息

刚刚是获取了 test2 的信息,test2 中的字段是指定了类型的,那如果没指定类型呢?

创建一个新索引,直接插入数据,不指定数据类型:

创建test3索引并添加数据

再使用 GET 命令查看一下 test3 索引的信息:

查看test3索引信息

很显然,如果文档的字段没有指定类型,ES 会默认配置字段类型。

GET 其他用法:

查看集群健康状态:

1
GET _cat/health

查看索引信息:

1
GET _cat/indices?v

查看索引信息

通过 GET _cat/ 可以获得 ES 当前的很多信息,参考链接:curl命令操作elasticsearch

数据修改

可以直接通过 PUT 命令实现数据的修改,比如:

PUT实现数据修改

可以发现,_version 为 2,result 为 updated。

但这样有一个弊端,需要将没有修改的数据也写上,否则数据会丢失。

那还有其他的办法吗?

可以使用 POST 命令来更新数据:

POST更新数据

删除索引

直接使用 DELETE 命令来实现删除,ES 会根据编写的命令来判断是删除文档还是删除索引,比如:

DELETE删除索引

执行上述命令,删除索引 test1,去 elasticsearch-head 看看是否成功删除索引:

验证删除索引

可以看到 test1 索引确实被删除了!

7.3 文档的基本操作

数据准备

1
2
3
4
5
6
7
PUT /mofan/user/1
{
"name": "张三",
"age": 3,
"desc": "法外狂徒",
"tags":["渣男", "唱歌", "蹦迪"]
}

按照这样的语法,插入三条数据:

向同一索引插入三条数据

获取数据(简单版)

1
GET mofan/user/3

获取索引中的数据

更新数据

前文以及说了,可以使用 PUT 命令,也可以使用 POST 命令(推荐使用 POST),比如:

1
2
3
4
5
6
POST mofan/user/1/_update
{
"doc":{
"name": "王五"
}
}

POST更新

简单的条件查询

查询 name 为李四的数据:

1
GET mofan/user/_search?q=name:李四

查询name为李四的数据

7.4 复杂搜索

在前文的查询结果中,可以看到有一个名为 _score 的字段,这个字段就表示 权重 的意思。在有多个查询结果的时候,这个字段的值越大,就表示匹配度越高。

PS:百度就是利用这玩意赚钱的 🤮

score含义

前文涉及的查询是一种简单查询,通过参数的方式进行查询,但是实际使用时我们一般不会这么做。

现在对 mofan 索引增加一条数据:

1
2
3
4
5
6
7
PUT mofan/user/4
{
"name": "默烦",
"age": 18,
"desc": "小菜鸡",
"tags": ["学习", "写博客", "看番"]
}

假设要查询 name 为“默烦”的数据,像前文一样,可以这么查询:

1
GET mofan/user/_search?q=name:默

这相当于一种省略的写法,其实完整的写法是这样的:

1
2
3
4
5
6
7
8
GET mofan/user/_search
{
"query": {
"match": {
"name": "默"
}
}
}

这样都可以查询出对应的数据:
查询name为默烦的数据

假设又添加一条数据:

1
2
3
4
5
6
7
PUT mofan/user/5
{
"name": "默小烦",
"age": 6,
"desc": "菜鸡",
"tags": ["学习", "写博客", "看番"]
}

然后再次查询:

查询得到name为默的两条结果

可以看到,查询得到了两条结果(图片大小所限,只能显示一条,但从 hits 的 value 可见,确实得到了两条结果),两条结果的排序按照 _score 大小进行降序排序,同时需要注意 hits 的作用,在 Java API 中也有一个类型的类。

查询结果字段过滤

上面的查询会将所有字段都查询出来,如果想 查询指定的字段 该怎么做呢?

类似于 MySQL 中的 select * 变成了 select name, desc

查询结果字段过滤

查询结果排序

前面的结果的排序是按照 _score 降序排序的,那如果要指定排序规则呢?比如按照 age 进行升序排序。

相当于 MySQL 中的 order by

查询结果指定字段升序排序

分页

数据太多,想进行分页查询该怎么办呢?

相当于 MySQL 中的 limit

分页查询数据

在【查询结果排序】中,一共查得两条数据,第一条 name 为“默小烦”,第二条 name 为“默烦”。我们这里从第二条数据开始(第一条数据的 form 值为 0),每页一条数据,因此可以查得 name 为“默烦”的数据。

注意: 数据小标从 0 开始,第一条数据的索引是 0。

布尔值查询

如果想要多条件查询,应该怎么办呢?可以使用布尔值查询。

比如查询 name 中带有“默”的,且 age 为 6 的数据。使用 must 命令,相当于 MySQL 中的 and

must布尔查询

既然有 and 那么就有 or,在 ES 中可以使用 should 命令:

should布尔查询

图片大小所限,只能显示一条结果,但从 value 的值来看,确实查询出 两条 数据。

那如果要查询 name 中不带有“默”字的数据应该怎么操作呢?

可以使用 must_not 操作,相当于 MySQL 中的 not

must_not布尔查询

图片大小所限,只能显示一条结果,但从 value 的值来看,确实查询出 三条 数据。因为 name 中不带有“默”字的有三条数据,分别是:李四、王五和鲲鲲。

那如果要进行范围查询呢?比如查询 name 中带有“默”字的,且 age 大于 16 的数据。在 ES 中可以使用 filter

filter单条件过滤

当然也可以进行多条件过滤:

filter多条件过滤

多条件匹配

如果我想要根据 tags 进行查询,查询 tags 中包含“博客”和“男”的数据,应该怎么做?
多条件匹配查询

针对同一字段的多个条件,使用空格隔开就可以了,而且只要满足其中一个条件都会被查询出来,可以通过 _score 进行判断。

查询结果有三条,分别是 name 为“默烦”、“默小烦”和“王五”的数据(上图中省略了第一条 name 为“默烦”的数据)。

精确查询

前文涉及的查询都是一种 模糊查询,但如果要进行精确查询应该怎么办?

term 查询是直接通过 倒排索引 指定的词条进程精确查找的。

关于分词:

term:直接查询精确

match:会使用分词器解析(先分析文档,然后在通过分析的文档进行查询)

很显然,使用 term 效率更高!

两个类型的解析:test 和 keyword

又创建一个索引 testdb,并添加两条数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
PUT testdb
{
"mappings": {
"properties": {
"name":{
"type": "text"
},
"desc":{
"type": "keyword"
}
}
}
}

PUT testdb/_doc/1
{
"name": "默烦 name",
"desc": "默烦 desc"
}

PUT testdb/_doc/2
{
"name": "默烦 name",
"desc": "默烦 desc2"
}

我们先来测试一下:

keyword分词算法测试

换种分词算法:

standard分词算法测试

查询 name 中带有“默”的数据:

name中带有默的数据

name 是 text 类型的,可以看到能查询出两条结果,再来试试查询 desc 中带有“默”的数据:

desc中带有默的数据

由于 desc 的类型是 keyword 的,这个类型的值不会被分词器解析。也就是说,查询 desc 中带有“默”的数据只会查询出 desc 的值为“默”的数据,很显然并没有,因此不会查询出数据。换种方式试试:

keyword字段查询再测试

这样的话就可以精确匹配了! 🎉

多个值匹配精确查询

先往 testdb 索引中插入两条数据:

1
2
3
4
5
6
7
8
9
10
11
PUT testdb/_doc/3
{
"t1": "22",
"t2": "2020-11-15"
}

PUT testdb/_doc/4
{
"t1": "33",
"t2": "2020-11-16"
}

实现精确查询,需要使用 term;实现多个值匹配,需要使用 should

因此,要实现多个值匹配查询,只需要将两者结合就可以了:

多个值匹配精确查询

高亮查询

除此之外,ES 还支持一个很 牛逼 的功能——高亮查询!

高亮查询就像百度、淘宝、京东搜索某些东西时,会在搜索结果中根据搜索的内容进行高亮显示。比如,我想将 mofan 索引中,name 字段值带有“默烦”的文档高亮显示出来:

高亮查询测试

那如果不想让高亮字段被 <em> 标签包裹起来呢?ES 支持自定义高亮包裹标签:

自定义高亮结果标签

当然,这些功能 MySQL 也能做到,但是效率就不如 ES 了。

8. 集成 SpringBoot

8.1 集成准备

看官方文档,官方怎么搞,我们怎么搞。

官方文档链接:ES docs

进入文档后,可以看到:

ElasticStackAndProductDocumentation

点击 Elasticsearch Clients 进入:

ElasticsearchClients

由于官网版本是 7.10,而我们的版本是 7.6,因此点击 other versions,选择 7.6 的版本:

JavaRESTClient

进入后,可以看到:

JavaRESTClient-7.6

点击 Java High Level REST Client 旁边的 + 号,然后点击 Getting started

Getting started

点击 Maven Repository 找到原生依赖:

1
2
3
4
5
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.6.2</version>
</dependency>

点击右边的 Initialization

Initialization

接下来,使用 IDEA 导入对应的依赖,分析一下这个类中的方法。

配置基本的项目

SpringBoot 需要导入的依赖:

项目需要导入的依赖

项目创建完毕后,查看一下导入的依赖:

依赖版本与本地版本不一致

注意: 一定要保证导入的依赖和 ES 版本一致!

因此,需要在 pom.xml 文件中进行配置:

1
2
3
4
5
<properties>
<java.version>1.8</java.version>
<!--自定义 ES 的版本,保证和本地的版本一致-->
<elasticsearch.version>7.6.1</elasticsearch.version>
</properties>

我在此顺便也将 SpringBoot 的版本降至 2.2.6。

更换依赖版本

编写一个配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package indi.mofan.config;
/**
* @author 默烦 2020/11/15
*/
@Configuration
public class ElasticSearchConfig {
@Bean
public RestHighLevelClient restHighLevelClient() {
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("127.0.0.1", 9200, "http"))
);
return client;
}
}

在项目的依赖中,找到 ES 自动配置的依赖:

ES自动配置依赖

查看源码中提供的对象:

ES源码中提供的对象

从上图中可以看出,虽然 @Import 导入了三个类,其实都是内部类,核心类只有一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class RestClientConfigurations {

@Configuration(proxyBeanMethods = false)
static class RestClientBuilderConfiguration {
// 省略代码
}

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RestHighLevelClient.class)
static class RestHighLevelClientConfiguration {
// 省略代码
}

@Configuration(proxyBeanMethods = false)
static class RestClientFallbackConfiguration {
// 省略代码
}
}

8.2 索引 API 测试

创建索引

在 SpringBoot 项目的测试类中进行编写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@SpringBootTest
class EsApiApplicationTests {

@Autowired
@Qualifier("restHighLevelClient")
private RestHighLevelClient client;

// 测试索引的创建 Request
@Test
void testCreateIndex() throws IOException {
// 1. 创建索引请求
CreateIndexRequest request = new CreateIndexRequest("mofan_index");
// 2. 执行创建请求 IndicesClient,请求后获得响应
CreateIndexResponse createIndexResponse
= client.indices().create(request, RequestOptions.DEFAULT);
System.out.println(createIndexResponse);
}

}

运行上述代码,启动 ElasticSearch-head,然后访问 9100 端口,可以看到 mofan_index 索引已经创建成功:

成功创建mofan_index索引

判断索引是否存在

1
2
3
4
5
6
7
// 测试获取索引,只能判断其是否存在
@Test
void testExistIndex() throws IOException {
GetIndexRequest request = new GetIndexRequest("mofan_index");
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
System.out.println(exists); // true
}

删除索引

1
2
3
4
5
6
7
8
// 测试删除索引
@Test
void testDeleteIndex() throws IOException {
DeleteIndexRequest request = new DeleteIndexRequest("mofan_index");
AcknowledgedResponse response = client.indices().delete(request, RequestOptions.DEFAULT);
// 如果返回 true 证明删除成功
System.out.println(response.isAcknowledged()); // true
}

运行上述代码,启动 ElasticSearch-head,然后访问 9100 端口,可以看到 mofan_index 索引已经删除成功:

成功删除mofan_index索引

为了便于后续测试查看,我们删除除 mofan_index 以外的所有索引:

删除多余的索引

8.3 文档 API 测试

为便于使用 JSON,在 pom.xml 中导入 fastjson 依赖:

1
2
3
4
5
6
7
<!--引入 fastJson -->
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>

创建一个实体类:

1
2
3
4
5
6
7
8
9
10
/**
* @author 默烦 2020/11/16
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private String name;
private int age;
}

创建文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 测试添加文档
@Test
void testAddDocument() throws IOException {
// 创建对象
User user = new User("默烦", 18);
// 创建请求
IndexRequest request = new IndexRequest("mofan_index");
// 规则 put/mofan_index/_doc/1
// 支持链式编程
request.id("1").timeout(TimeValue.timeValueSeconds(1));
// 将我们的数据放入请求 json
IndexRequest source = request.source(JSON.toJSONString(user), XContentType.JSON);
// 客户端发送请求
IndexResponse indexResponse = client.index(request, RequestOptions.DEFAULT);
System.out.println(indexResponse.toString());
System.out.println(indexResponse.status()); // CREATED
}

运行上述代码后,进 9100 端口查看:

测试创建文档

测试某一文档是否存在

1
2
3
4
5
6
7
8
9
10
11
// 测试获取文档,判断文档是否存在
@Test
void testIsExistDocument() throws IOException {
GetRequest getRequest = new GetRequest("mofan_index", "1");
// 不获取返回的 _source 的上下文
getRequest.fetchSourceContext(new FetchSourceContext(false));
getRequest.storedFields("_none_");

boolean exists = client.exists(getRequest, RequestOptions.DEFAULT);
System.out.println(exists); // true
}

获取文档的内容

1
2
3
4
5
6
7
8
9
10
// 获得文档的信息
@Test
void testGetDocument() throws IOException {
GetRequest getRequest = new GetRequest("mofan_index", "1");
GetResponse response = client.get(getRequest, RequestOptions.DEFAULT);

// 打印文档的内容
System.out.println(response.getSourceAsString());
System.out.println(response); // 返回的全部内容和 Kibana 看到的是一样的
}

运行后,控制台输出:

1
2
{"age":18,"name":"默烦"}
{"_index":"mofan_index","_type":"_doc","_id":"1","_version":1,"_seq_no":0,"_primary_term":1,"found":true,"_source":{"age":18,"name":"默烦"}}

更新文档的信息

1
2
3
4
5
6
7
8
9
10
11
12
// 更新文档的信息
@Test
void testUpdateDocument() throws IOException {
UpdateRequest updateRequest = new UpdateRequest("mofan_index", "1");
updateRequest.timeout("1s");

User user = new User("mofan", 5);
updateRequest.doc(JSON.toJSONString(user), XContentType.JSON);

UpdateResponse updateResponse = client.update(updateRequest, RequestOptions.DEFAULT);
System.out.println(updateResponse.status()); // OK
}

运行上述代码后,前往 9100 端口查看:

测试更新文档

删除文档记录

1
2
3
4
5
6
7
8
9
// 删除文档记录
@Test
void testDeleteRequest() throws IOException {
DeleteRequest deleteRequest = new DeleteRequest("mofan_index", "1");
deleteRequest.timeout("1s");

DeleteResponse deleteResponse = client.delete(deleteRequest, RequestOptions.DEFAULT);
System.out.println(deleteResponse.status()); // OK
}

运行上述代码后,前往 9100 端口查看:

测试删除文档

批量插入文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 批量插入数据
@Test
void testBulkRequest() throws IOException {
BulkRequest bulkRequest = new BulkRequest();
bulkRequest.timeout("1s");

ArrayList<User> userList = new ArrayList<>();
userList.add(new User("mofan1", 5));
userList.add(new User("mofan2", 6));
userList.add(new User("mofan3", 7));
userList.add(new User("mofan4", 8));
userList.add(new User("mofan5", 9));
userList.add(new User("mofan6", 10));

for (int i = 0; i < userList.size(); i++) {
bulkRequest.add(
new IndexRequest("mofan_index")
.id("" + (i + 1))
.source(JSON.toJSONString(userList.get(i)), XContentType.JSON)
);
}

BulkResponse bulkResponse = client.bulk(bulkRequest, RequestOptions.DEFAULT);
System.out.println(bulkResponse.hasFailures()); // false
}

运行上述代码后,前往 9100 端口查看:

批量插入文档

查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 查询
@Test
void testSearch() throws IOException {
SearchRequest searchRequest = new SearchRequest("mofan_index");
// 构建搜索条件
SearchSourceBuilder builder = new SearchSourceBuilder();
// 查询条件
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("name", "mofan1");
builder.query(termQueryBuilder);
builder.timeout(new TimeValue(60, TimeUnit.SECONDS));

searchRequest.source(builder);

SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
System.out.println(JSON.toJSONString(searchResponse.getHits()));
System.out.println("===============================");
for (SearchHit documentFields : searchResponse.getHits().getHits()) {
System.out.println(documentFields.getSourceAsMap());
}
}

上述代码实现了精确查询,控制台输出结果:

1
2
3
{"fragment":true,"hits":[{"fields":{},"fragment":false,"highlightFields":{},"id":"1","matchedQueries":[],"primaryTerm":0,"rawSortValues":[],"score":1.540445,"seqNo":-2,"sortValues":[],"sourceAsMap":{"name":"mofan1","age":5},"sourceAsString":"{\"age\":5,\"name\":\"mofan1\"}","sourceRef":{"fragment":true},"type":"_doc","version":-1}],"maxScore":1.540445,"totalHits":{"relation":"EQUAL_TO","value":1}}
===============================
{name=mofan1, age=5}

此处的 name 字段值如果是中文,由于使用了默认的分词器,将会导致查询失败!

其他查询 API:

termQuery(key, obj):单个精准匹配

termsQuery(key, obj1, obj2..):多个精准匹配

matchQuery(key, Obj):单个匹配

multiMatchQuery(text, field1, field2...):多字段匹配

matchAllQuery():匹配所有文件

9. 京东搜索小 Demo

9.1 初始化 Demo

关闭前面创建的 Module,在当前工程下新建一个 Module。创建一个 SpringBoot 项目,导入以下依赖:

Demo依赖

创建好项目后,更改 pom.xml 文件,修改 SpringBoot 版本至 2.2.6,设置 ES 的版本,导入 FastJSON 的依赖:

1
2
3
4
5
6
7
8
9
10
11
12
<properties>
<java.version>1.8</java.version>
<!--自定义 ES 的版本,保证和本地的版本一致-->
<elasticsearch.version>7.6.1</elasticsearch.version>
</properties>

<!--引入 fastJson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>

然后导入必要的素材,比如 JS、CSS和图片。

修改 application.properties 配置文件:

1
2
3
server.port=9090
# 关闭 Thymeleaf 的缓存
spring.thymeleaf.cache=false

接下来,就是数据的问题了。

9.2 爬取数据

我们可以使用 JSoup 来解析网页,因此需要导入对应的依赖:

1
2
3
4
5
6
7
<!--引入jsoup-->
<!-- https://mvnrepository.com/artifact/org.jsoup/jsoup -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.10.2</version>
</dependency>

需要将解析的网页内容放入一个对象中,因此需要自定义一个对象:

1
2
3
4
5
6
7
8
9
10
11
/**
* @author 默烦 2020/11/17
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Content {
private String title;
private String img;
private String price;
}

然后就解析网页:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* @author 默烦 2020/11/17
*/
@Component
public class HtmlParseUtil {
// public static void main(String[] args) throws IOException{
// // 测试
// new HtmlParseUtil().parseJD("Java").forEach(System.out::println);
// }


public List<Content> parseJD(String keywords) throws IOException{
// 获取请求 https://search.jd.com/Search?keyword=Java
// 前提:需要联网,且 Ajax 的请求无法获取
String url = "https://search.jd.com/Search?keyword=" + keywords;

// 解析网页 Document 对象就是浏览器的 Document 对象
Document document = Jsoup.parse(new URL(url), 30000);
// 所有在 JS 中能使用的方法,在这里都可以使用
Element element = document.getElementById("J_goodsList");

// System.out.println(element.html());
// 获取所有的 <li> 标签
Elements elements = element.getElementsByTag("li");

ArrayList<Content> goodsList = new ArrayList<>();
// 获取元素中的内容
for (Element el : elements) {
// 这是 20-11-17 获取元素的方式,随着时间的推移,获取方式可能会发生改变
String img = el.getElementsByTag("img").eq(0).attr("data-lazy-img");
String price = el.getElementsByClass("p-price").eq(0).text();
String title = el.getElementsByClass("p-name").eq(0).text();

// System.out.println("================================");
//
// System.out.println(img);
// System.out.println(price);
// System.out.println(title);

Content content = new Content();
content.setTitle(title);
content.setPrice(price);
content.setImg(img);
goodsList.add(content);
}
return goodsList;
}
}

9.3 业务编写

同样,需要 ES 的配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @author 默烦 2020/11/15
*/
@Configuration
public class ElasticSearchConfig {
@Bean
public RestHighLevelClient restHighLevelClient() {
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("127.0.0.1", 9200, "http"))
);
return client;
}
}

编写 Service,为了简洁,就不面向接口了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
package indi.mofan.service;

import com.alibaba.fastjson.JSON;
import indi.mofan.pojo.Content;
import indi.mofan.utils.HtmlParseUtil;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.TermQueryBuilder;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
* @author 默烦 2020/11/17
*/
@Service
public class ContentService {

@Autowired
@Qualifier("restHighLevelClient")
private RestHighLevelClient client;

@Autowired
private RestHighLevelClient restHighLevelClient;

// 1. 解析数据放入 es 索引中
public Boolean parseContent(String keywords) throws IOException {
List<Content> contents = new HtmlParseUtil().parseJD(keywords);

GetIndexRequest getIndexRequest = new GetIndexRequest("jd_goods");
if (!client.indices().exists(getIndexRequest, RequestOptions.DEFAULT)) {
// 创建索引
CreateIndexRequest request = new CreateIndexRequest("jd_goods");
// 执行创建请求 IndicesClient,请求后获得响应
client.indices().create(request, RequestOptions.DEFAULT);
}

// 把得到的数据放入 es 中
BulkRequest bulkRequest = new BulkRequest();
bulkRequest.timeout("2m");

for (int i = 0; i < contents.size(); i++) {
bulkRequest.add(
new IndexRequest("jd_goods")
.source(JSON.toJSONString(contents.get(i)), XContentType.JSON));
}

BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
return !bulk.hasFailures();
}

// 获取数据,实现搜索功能
public List<Map<String, Object>> searchPage(String keyword, int pageNo, int pageSize) throws IOException {
if (pageNo <= 1) {
pageNo = 1;
}

// 条件搜索
SearchRequest searchRequest = new SearchRequest("jd_goods");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

// 分页
sourceBuilder.from(pageNo);
sourceBuilder.size(pageSize);

// 精准匹配
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("title", keyword);
sourceBuilder.query(termQueryBuilder);
sourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));

// 执行搜索
searchRequest.source(sourceBuilder);
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

// 解析结果
ArrayList<Map<String, Object>> list = new ArrayList<>();
for (SearchHit documentFields : searchResponse.getHits().getHits()) {
list.add(documentFields.getSourceAsMap());
}
return list;
}
}

编写控制器,同时还可以进行简单的测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @author 默烦 2020/11/17
*/
@RestController
public class ContentController {

@Autowired
private ContentService contentService;

@GetMapping("/parse/{keyword}")
public Boolean parse(@PathVariable("keyword") String keyword) throws IOException {
return contentService.parseContent(keyword);
}

@GetMapping("/search/{keyword}/{pageNo}/{pageSize}")
public List<Map<String, Object>> search(@PathVariable("keyword") String keyword,
@PathVariable("pageNo") int pageNo,
@PathVariable("pageSize") int pageSize) throws IOException {
return contentService.searchPage(keyword, pageNo, pageSize);
}
}

可以打开浏览器,输入对应的地址,向 ES 中插入一些数据,也可以进行简单的搜索。

比如:localhost:9090/parse/java 就可以向 ES 中添加关于 Java 书籍的信息。由于京东商品页做了分页,因此添加数据是一次 30 条数据。

又比如:http://localhost:9090/search/java/1/10 就可以从 ES 中查出 10 条关于 Java 书籍的信息。

9.4 前后端交互

要实现前后端交互,需要使用到 Vue,因此也就需要下载 Vue 的一些 JS 文件。我们可以在桌面(当然,任何地方都行)创建一个文件夹(最好命名不含中文),然后使用 Git 或 CMD,在这个文件夹内下载需要的依赖。

1
2
3
npm init
cnpm install vue
cnpm install axios

当然,前提需要在所用电脑上安装 Node.js 和 cnpm,可以参看【Node.js 的安装与 Hexo 的升级】一文。

然后将 \node_modules\axios\dist\ 目录下的 axios.min.js 复制到我们的项目中,同样需要将 \node_modules\vue\dist 目录下的 vue.min.js 文件复制到我们的项目中。

最后在前端页面引用这两个 JS 文件。如:

1
2
3
4
<!--前端使用 Vue,实现前后端分离-->
<!-- 注意这两个 JS 的顺序不要颠倒 -->
<script th:src="@{/js/vue.min.js}"></script>
<script th:src="@{/js/axios.min.js}"></script>

其实,更推荐使用 CDN 的方式应用 JS,不然后续可能会出现找不到对应 JS 文件的情况:

1
2
3
<!--其实更推荐使用 CDN 的方式导入-->
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.min.js"></script>
<script src="https://cdn.bootcss.com/axios/0.18.0/axios.min.js"></script>

然后就在前端页面书写对应的 JS 代码,整个前端页面代码在本文末给出

编写完毕之后,重启 IDEA,再运行项目。

注意: 记得保持 ES 处于运行状态!

实现高亮

在 Serveice 层添加一个和高亮相关的 searchPageHighlightBuilder 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 实现搜索高亮功能
public List<Map<String, Object>> searchPageHighlightBuilder(String keyword, int pageNo, int pageSize) throws IOException {
if (pageNo <= 1) {
pageNo = 1;
}

// 条件搜索
SearchRequest searchRequest = new SearchRequest("jd_goods");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

// 分页
sourceBuilder.from(pageNo);
sourceBuilder.size(pageSize);

// 精准匹配
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("title", keyword);
sourceBuilder.query(termQueryBuilder);
sourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));

// 高亮
HighlightBuilder highlightBuilder = new HighlightBuilder();
// 多个高亮显示
highlightBuilder.requireFieldMatch(false);
highlightBuilder.field("title").preTags("<span style='color:red'>").postTags("</span>");
sourceBuilder.highlighter(highlightBuilder);

// 执行搜索
searchRequest.source(sourceBuilder);
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

// 解析结果
ArrayList<Map<String, Object>> list = new ArrayList<>();
for (SearchHit hit : searchResponse.getHits().getHits()) {

Map<String, HighlightField> highlightFields = hit.getHighlightFields();
HighlightField title = highlightFields.get("title");
Map<String, Object> sourceAsMap = hit.getSourceAsMap(); // 原来的结果
// 解析高亮的字段,将原来的字段换为我们高亮的字段即可!
if (title != null) {
Text[] fragments = title.fragments();
String n_title = "";
for (Text text : fragments) {
n_title += text;
}
// 高亮字段替换原来的内容
sourceAsMap.put("title", n_title);
}
list.add(sourceAsMap);
}
return list;
}

修改 Controller 层的 search 方法,在这个方法中调用高亮显示方法:

1
2
3
4
5
6
@GetMapping("/search/{keyword}/{pageNo}/{pageSize}")
public List<Map<String, Object>> search(@PathVariable("keyword") String keyword,
@PathVariable("pageNo") int pageNo,
@PathVariable("pageSize") int pageSize) throws IOException {
return contentService.searchPageHighlightBuilder(keyword, pageNo, pageSize);
}

9.5 前端页面资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
<meta charset="utf-8"/>
<title>商品搜索-京东</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>spring</a></li>
<li><a>python</a></li>
<li><a>java核心技术</a></li>
<li><a>mysql</a></li>
<li><a>java编程思想</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">
<!--商品封面-->
<div class="productImg-wrap">
<a class="productImg">
<img :src="result.img">
</a>
</div>
<!--价格-->
<p class="productPrice">
<em>{{result.price}}</em>
</p>
<!--标题-->
<p class="productTitle">
<a v-html="result.title"> </a>
</p>
<!-- 店铺名 -->
<div class="productShop">
<span>店铺: </span>
</div>
<!-- 成交信息 -->
<p class="productStatus">
<span>月成交<em>999笔</em></span>
<span>评价 <a>3</a></span>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

<!--前端使用 Vue,实现前后端分离-->
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.min.js"></script>
<script src="https://cdn.bootcss.com/axios/0.18.0/axios.min.js"></script>
<script>

new Vue({
el: '#app',
data: {
keyword: '', //搜索的关键字
results: [] // 搜索的结果
},
methods: {
searchKey() {
let keyword = this.keyword;
console.log(keyword);
// 对接后端的接口
axios.get('search/' + keyword + "/1/10").then(response=>{
console.log(response.data);
this.results = response.data; // 绑定数据
})
}
}
})

</script>

</body>
</html>

ElasticSearch 入门完