上篇我们讲到了 Elasticsearch 全文检索的原理[《别只会搜日志了,求你懂点原理吧》] ,通过在本地搭建一套 ES 服务,以多个案例来分析了 ES 的原理以及基础使用。这次我们来讲下 Spring Boot 中如何整合 ES,以及如何在 Spring Cloud 微服务项目中使用 ES 来实现全文检索,来达到搜索题库的功能。
而且题库的数据量是非常大的,题目的答案也是非常长的,通过 ES 正好可以解决 mysql 模糊搜索的低效性。
通过本实战您可以学到如下知识点:
本篇主要内容如下:
主要内容
本文案例都是基于 PassJava 实战项目来演示的。
Github 地址:https://github.com/Jackson0714/PassJava-Platform
为了让大家更清晰地理解 PassJava 项目中 ES 是如何使用的,我画了三个流程图:
首先定义 question 索引,然后在 ES 中创建索引。
前端保存数据时,保存的 API 请求先经过网关,然后转发到 passjava-question 微服务,然后远程调用 passjava-search 微服务,将数据保存进 ES 中。
在讲解之前,我在这里再次提下全文检索是什么:
全文检索: 指以全部文本信息作为检索对象的一种信息检索技术。而我们使用的数据库,如 Mysql,MongoDB 对文本信息检索能力特别是中文检索并没有 ES 强大。所以我们来看下 ES 在项目中是如何来代替 SQL 来工作的。
我使用的 Elasticsearch 服务是 7.4.2 的版本,然后采用官方提供的 Elastiscsearch-Rest-Client 库来操作 ES,而且官方库的 API 上手简单。
该组件库的官方文档地址:
https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high.html
另外这个组件库是支持多种语言的:
支持多语言
注意:Elasticsearch Clients
就是指如何用 API 操作 ES 服务的组件库。
可能有同学会提问,Elasticsearch 的组件库中写着 JavaScript API,是不是可以直接在前端访问 ES 服务?可以是可以,但是会暴露 ES 服务的端口和 IP 地址,会非常不安全。所以我们还是用后端服务来访问 ES 服务。
我们这个项目是 Java 项目,自然就是用上面的两种:Java Rest Client
或者 Java API
。我们先看下 Java API,但是会发现已经废弃了。如下图所示:
Java API 已经废弃了
所以我们只能用 Java REST Client 了。而它又分成两种:高级和低级的。高级包含更多的功能,如果把高级比作MyBatis的话,那么低级就相当于JDBC。所以我们用高级的 Client。
高级和低级 Client
我们把检索服务单独作为一个服务。就称作 passjava-search 模块吧。
首先我们在 PassJava-Platform 模块创建一个 搜索服务模块 passjava-search。然后勾选 spring web 服务。如下图所示。
第一步:选择 Spring Initializr,然后点击 Next。
选择 Spring Initializr
第二步:填写模块信息,然后点击 Next。
passjava-search 服务模块
第三步:选择 Web->Spring Web 依赖,然后点击 Next。
进入到 ES 官方网站,可以看到有低级和高级的 Rest Client,我们选择高阶的(High Level Rest Client)。然后进入到高阶 Rest Client 的 Maven 仓库。官网地址如下所示:
https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.9/index.html
Rest Client 官方文档
加上 Maven 依赖。
对应文件路径:\passjava-search\pom.xml
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.4.2</version>
</dependency>
配置 elasticsearch 的版本为7.4.2
因加上 Maven 依赖后,elasticsearch 版本为 7.6.2,所以遇到这种版本不一致的情况时,需要手动改掉。
对应文件路径:\passjava-search\pom.xml
<properties>
<elasticsearch.version>7.4.2</elasticsearch.version>
</properties>
刷新 Maven Project 后,可以看到引入的 elasticsearch 都是 7.4.2 版本了,如下图所示:
设置版本为 7.4.2
引入 PassJava 的 Common 模块依赖。
Common 模块是 PassJava 项目独立的出来的公共模块,引入了很多公共组件依赖,其他模块引入 Common 模块依赖后,就不需要单独引入这些公共组件了,非常方便。
对应文件路径:\passjava-search\pom.xml
<dependency>
<groupId>com.jackson0714.passjava</groupId>
<artifactId>passjava-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
添加完依赖后,我们就可以将搜索服务注册到 Nacos
注册中心了。Nacos 注册中心的用法在前面几篇文章中也详细讲解过,这里需要注意的是要先启动 Nacos 注册中心,才能正常注册 passjava-search 服务。
修改配置文件:src/main/resources/application.properties。配置应用程序名、注册中心地址、注册中心的命名中间。
spring.application.name=passjava-search
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=passjava-search
给启动类
添加服务发现注解:@EnableDiscoveryClient
。这样 passjava-search 服务就可以被注册中心发现了。
因 Common 模块依赖数据源,但 search 模块不依赖数据源,所以 search 模块需要移除数据源依赖:
exclude = DataSourceAutoConfiguration.class
以上的两个注解如下所示:
@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class PassjavaSearchApplication {
public static void main(String[] args) {
SpringApplication.run(PassjavaSearchApplication.class, args);
}
}
接下来我们添加一个 ES 服务的专属配置类,主要目的是自动加载一个 ES Client 来供后续 ES API 使用,不用每次都 new 一个 ES Client。
配置类:PassJavaElasticsearchConfig.java
核心方法就是 RestClient.builder 方法,设置好 ES 服务的 IP 地址、端口号、传输协议就可以了。最后自动加载了 RestHighLevelClient。
package com.jackson0714.passjava.search.config;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Author: 公众号 | 悟空聊架构
* @Date: 2020/10/8 17:02
* @Site: www.passjava.cn
* @Github: https://github.com/Jackson0714/PassJava-Platform
*/
@Configuration
public class PassJavaElasticsearchConfig {
@Bean
// 给容器注册一个 RestHighLevelClient,用来操作 ES
// 参考官方文档:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.9/java-rest-high-getting-started-initialization.html
public RestHighLevelClient restHighLevelClient() {
return new RestHighLevelClient(
RestClient.builder(
new HttpHost("192.168.56.10", 9200, "http")));
}
}
接下来我们测试下 ES Client 是否自动加载成功。
在测试类 PassjavaSearchApplicationTests 中编写测试方法,打印出自动加载的 ES Client。期望结果是一个 RestHighLevelClient 对象。
package com.jackson0714.passjava.search;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class PassjavaSearchApplicationTests {
@Qualifier("restHighLevelClient")
@Autowired
private RestHighLevelClient client;
@Test
public void contextLoads() {
System.out.println(client);
}
}
运行结果如下所示,打印出了 RestHighLevelClient。说明自定义的 ES Client 自动装载成功。
ES 测试结果
测试方法 testIndexData,省略 User 类。users 索引在我的 ES 中是没有记录的,所以期望结果是 ES 中新增了一条 users 数据。
/**
* 测试存储数据到 ES。
* */
@Test
public void testIndexData() throws IOException {
IndexRequest request = new IndexRequest("users");
request.id("1"); // 文档的 id
//构造 User 对象
User user = new User();
user.setUserName("PassJava");
user.setAge("18");
user.setGender("Man");
//User 对象转为 JSON 数据
String jsonString = JSON.toJSONString(user);
// JSON 数据放入 request 中
request.source(jsonString, XContentType.JSON);
// 执行插入操作
IndexResponse response = client.index(request, RequestOptions.DEFAULT);
System.out.println(response);
}
执行 test 方法,我们可以看到控制台输出以下结果,说明数据插入到 ES 成功。另外需要注意的是结果中的 result 字段为 updated,是因为我本地为了截图,多执行了几次插入操作,但因为 id = 1,所以做的都是 updated 操作,而不是 created 操作。
控制台输出结果 我们再来到 ES 中看下 users 索引中数据。查询 users 索引:
GET users/_search
结果如下所示:
查询 users 索引结果
可以从图中看到有一条记录被查询出来,查询出来的数据的 _id = 1,和插入的文档 id 一致。另外几个字段的值也是一致的。说明插入的数据没有问题。
"age" : "18",
"gender" : "Man",
"userName" : "PassJava"
示例:搜索 bank 索引,address 字段中包含 big 的所有人的年龄分布 ( 前 10 条 ) 以及平均年龄,以及平均薪资。
我们可以参照官方文档给出的示例来创建一个 SearchRequest 对象,指定要查询的索引为 bank,然后创建一个 SearchSourceBuilder 来组装查询条件。总共有三种条件需要组装:
代码如下所示,需要源码请到我的 Github/PassJava 上下载。
查询复杂语句示例
将打印出来的检索参数复制出来,然后放到 JSON 格式化工具中格式化一下,再粘贴到 ES 控制台执行,发现执行结果是正确的。
打印出检索参数
用在线工具格式化 JSON 字符串,结果如下所示:
然后我们去掉其中的一些默认参数,最后简化后的检索参数放到 Kibana 中执行。
Kibana Dev Tools 控制台中执行检索语句如下图所示,检索结果如下图所示:
控制台中执行检索语句
找到总记录数:29 条。
第一条命中记录的详情如下:
平均 balance:13136。
平均年龄:26。
地址中包含 Road 的:263 Aviation Road。
和 IDEA 中执行的测试结果一致,说明复杂检索的功能已经成功实现。
而获取命中记录的详情数据,则需要通过两次 getHists() 方法拿到,如下所示:
// 3.1)获取查到的数据。
SearchHits hits = response.getHits();
// 3.2)获取真正命中的结果
SearchHit[] searchHits = hits.getHits();
我们可以通过遍历 searchHits 的方式打印出所有命中结果的详情。
// 3.3)、遍历命中结果
for (SearchHit hit: searchHits) {
String hitStr = hit.getSourceAsString();
BankMember bankMember = JSON.parseObject(hitStr, BankMember.class);
}
拿到每条记录的 hitStr 是个 JSON 数据,如下所示:
{
"account_number": 431,
"balance": 13136,
"firstname": "Laurie",
"lastname": "Shaw",
"age": 26,
"gender": "F",
"address": "263 Aviation Road",
"employer": "Zillanet",
"email": "laurieshaw@zillanet.com",
"city": "Harmon",
"state": "WV"
}
而 BankMember 是根据返回的结果详情定义的的 JavaBean。可以通过工具自动生成。在线生成 JavaBean 的网站如下:
https://www.bejson.com/json2javapojo/new/
把这个 JavaBean 加到 PassjavaSearchApplicationTests 类中:
@ToString
@Data
static class BankMember {
private int account_number;
private int balance;
private String firstname;
private String lastname;
private int age;
private String gender;
private String address;
private String employer;
private String email;
private String city;
private String state;
}
然后将 bankMember 打印出来:
System.out.println(bankMember);
bankMember
得到的结果确实是我们封装的 BankMember 对象,而且里面的属性值也都拿到了。
ES 返回的 response 中,年龄分布的数据是按照 ES 的格式返回的,如果想按照我们自己的格式来返回,就需要将 response 进行处理。
如下图所示,这个是查询到的年龄分布结果,我们需要将其中某些字段取出来,比如 buckets,它代表了分布在 21 岁的有 4 个。
ES 返回的年龄分布信息
下面是代码实现:
Aggregations aggregations = response.getAggregations();
Terms ageAgg1 = aggregations.get("ageAgg");
for (Terms.Bucket bucket : ageAgg1.getBuckets()) {
String keyAsString = bucket.getKeyAsString();
System.out.println("用户年龄: " + keyAsString + " 人数:" + bucket.getDocCount());
}
最后打印的结果如下,21 岁的有 4 人,26 岁的有 4 人,等等。
打印结果:用户年龄分布
现在来看看平均薪资如何按照所需的格式返回,ES 返回的结果如下图所示,我们需要获取 balanceAvg 字段的 value 值。
ES 返回的平均薪资信息 代码实现:
Avg balanceAvg1 = aggregations.get("balanceAvg");
System.out.println("平均薪资:" + balanceAvg1.getValue());
打印结果如下,平均薪资 28578 元。
打印结果:平均薪资
PassJava 这个项目可以用来配置题库,如果我们想通过关键字来搜索题库,该怎么做呢?
类似于百度搜索,输入几个关键字就可以搜到关联的结果,我们这个功能也是类似,通过 Elasticsearch 做检索引擎,后台管理界面和小程序作为搜索入口,只需要在小程序上输入关键字,就可以检索相关的题目和答案。
首先我们需要把题目和答案保存到 ES 中,在存之前,第一步是定义索引的模型,如下所示,模型中有 title
和 answer
字段,表示题目和答案。
"id": {
"type": "long"
},
"title": {
"type": "text",
"analyzer": "ik_smart"
},
"answer": {
"type": "text",
"analyzer": "ik_smart"
},
"typeName": {
"type": "keyword"
}
上面我们已经定义了索引结构,接着就是在 ES 中创建索引。
在 Kibana 控制台中执行以下语句:
PUT question
{
"mappings" : {
"properties": {
"id": {
"type": "long"
},
"title": {
"type": "text",
"analyzer": "ik_smart"
},
"answer": {
"type": "text",
"analyzer": "ik_smart"
},
"typeName": {
"type": "keyword"
}
}
}
}
执行结果如下所示:
创建 question 索引
我们可以通过以下命令来查看 question 索引是否在 ES 中:
GET _cat/indices
执行结果如下图所示:
查看 ES 中所有的索引
上面我们定义 ES 的索引,接着就是定义索引对应的模型,将数据存到这个模型中,然后再存到 ES 中。
ES 模型如下,共四个字段:id、title、answer、typeName。和 ES 索引是相互对应的。
@Data
public class QuestionEsModel {
private Long id;
private String title;
private String answer;
private String typeName;
}
当我们在后台创建题目或保存题目时,先将数据保存到 mysql 数据库,然后再保存到 ES 中。
如下图所示,在管理后台创建题目时,触发保存数据到 ES 。
第一步,保存数据到 mysql 中,项目中已经包含此功能,就不再讲解了,直接进入第二步:保存数据到 ES 中。
而保存数据到 ES 中,需要将数据组装成 ES 索引对应的数据,所以我用了一个 ES model,先将数据保存到 ES model 中。
这里的关键代码时 copyProperties
,可以将 question
对象的数据取出,然后赋值到 ES model 中。不过 ES model 中还有些字段是 question 中没有的,所以需要单独拎出来赋值,比如 typeName 字段,question 对象中没有这个字段,它对应的字段是 question.type,所以我们把 type 取出来赋值到 ES model 的 typeName 字段上。如下图所示:
用 model 来组装数据
我在 passjava-search 微服务中写了一个保存题目的 api 用来保存数据到 ES 中。
保存数据到 ES
然后在 passjava-question 微服务中调用 search 微服务的保存 ES 的方法就可以了。
// 调用 passjava-search 服务,将数据发送到 ES 中保存。
searchFeignService.saveQuestion(esModel);
我们可以通过 kibana 的控制台来查看 question 索引中的文档。通过以下命令来查看:
GET question/_search
执行结果如下图所示,有一条记录:
另外大家有没有疑问:可以重复更新题目吗?
答案是可以的,保存到 ES 的数据是幂等的,因为保存的时候带了一个类似数据库主键的 id。
我们已经将数据同步到了 ES 中,现在就是前端怎么去查询 ES 数据中,这里我们还是使用 Postman 来模拟前端查询请求。
请求参数我定义了三个:
这里我将这三个参数定义为一个类:
@Data
public class SearchParam {
private String keyword; // 全文匹配的关键字
private String id; // 题目 id
private Integer pageNum; // 查询第几页数据
}
返回的 response 我也定义了四个字段:
定义的类如下所示:
@Data
public class SearchQuestionResponse {
private List<QuestionEsModel> questionList; // 题目列表
private Integer pageNum; // 查询第几页数据
private Long total; // 总条数
private Integer totalPages; // 总页数
}
调用 ES 的查询 API 时,需要构建查询参数。
组装查询参数的核心代码如下所示:
组装查询参数
ES 返回的数据是 ES 定义的格式,真正的数据被嵌套在 ES 的 response 中,所以需要格式化返回的数据。
核心代码如下图所示:
格式化 ES 返回结果
我们现在想要验证 title 字段是否能匹配到,传的请求参数 keyword = 111,匹配到了 title = 111 的数据,且只有一条。页码 pageNum 我传的 1,表示返回第一页数据。如下图所示:
测试匹配 title
我们现在想要验证 answer 字段是否能匹配到,传的请求参数 keyword = 测试答案,匹配到了 title = 测试答案的数据,且只有一条,说明查询成功。如下图所示:
测试匹配 answer
我们现在想要匹配题目 id 的话,需要传请求参数 id,而且 id 是精确匹配。另外 id 和 keyword 是取并集,所以不能传 keyword 字段。
请求参数 id = 5,返回结果也是 id =5 的数据,说明查询成功。如下图所示:
测试 id 匹配
本文通过我的开源项目 passjava 来讲解 ES 的整合,ES 的 API 使用以及测试。非常详细地讲解了每一步该如何做,相信通过阅读本篇后,再加上自己的实践,一定能掌握前后端该如何使用 ES 来达到高效搜索的目的。
当然,ES API 还有很多功能未在本文实践,有兴趣的同学可以到 ES 官网进行查阅和学习。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/ojH-X3emovz2YOzoqNYPDQ
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。