本章开始学习springboot整合ElasticSearch 7.X版本并通过小demo实现基本的增删改查。实现如下案例:
1、当向数据新增一个商品信息时,同时向rabbitMQ发起消息(异步实现),让监听到消息的类去向ElasticSearch 也新增这个商品信息。
2、当去修改数据库的商品信息时,同时向rabbitMQ发起消息(异步实现),让监听到消息的类去向ElasticSearch 也去修改这个商品信息。
3、当删除数据库的商品信息时,同时也向rabbitMQ发起消息(异步实现),让监听到消息的类去向ElasticSearch 也去删除这个商品信息。
4、实现ElasticSearch的条件查询。
5、实现ElasticSearch的分页查询。
6、实现ElasticSearch的条件+分页查询。
7、实现ElasticSearch的高亮显示与查询条件相符合的值。
8、实现ElasticSearch的高亮显示与查询条件相符合的值并分页和排序。
最终效果:
注意: 本章整合的ElasticSearch下载的客户端是windows版本,然后现在时间是2022.3.31。
官网提供的最新最新版本是8.1.1。最新版的使用跟以前7.X版本的完全不同,网上学习资料太少了,不得不降低版本学习7.X,本章节的学习下载的是elasticsearch-7.3.2,尝试了8.1.1的学习,问题实在是太多了,能力卑微,先不强求自己了。
注意:客户端下载的什么版本最好别去下最新版本,太难搞了。
!!!!提醒:本教程只适合elasticsearch-7.X版本,高版本不支持。
一、下载安装elasticsearch-7.3.2
下载本章节所用版本:
安装教程,转载自:
二、下载安装elasticsearch图像化插件(elasticsearch-head-master)
安装完成elasticsearch之后,需要安装一个插件去图形化操作elasticsearch,教程上讲的需要先下载node,然后用node自带的npm下载grunt,随后去修改一下elasticsearch的配置文件即可通过9100端口访问图形界面,教程如下:
执行命令运行head插件。
ES双击elasticsearch-7.3.2\bin目录下的elasticsearch.bat即可。
启动后长这个样子(启动时若会提示需要密码,这时需要去修改elasticsearch的配置文件)。
三、建立父子工程
为了测试上述功能,先建立一个父子工程,在学习RabbitMQ和KafKa时都有建过,直接拿来使用,如下:
四、准备工作
在开始之前,先做好准备工作,如下:
- 构建好父子模块的关系
- 在父工程引入所需依赖
- 修改provider和consumer的配置文件
- 创建mysql表
- 安装rabbitMQ(默认已经安装好,前面章节有安装过)
- 在common工程创建好公共模块
1、构建好父子模块的关系:
此处不讲了,在前面的章节有讲过。
2、在父工程引入所需依赖
此次学习需要用到mybatis-plus和rabbitMQ和elasticsearch,所有依赖情况如下:
我的springboot版本是:2.6.4,其他依赖不加版本会根据springboot版本去自动匹配版本导入。
3、provider的配置文件
4、consumer的配置文件
两者配置文件差不多,主要就是连接rabbitmq和elasticsearch,上图main:allow-bean-definition-overriding: true是我在测试时的一个报错,根据报错的一个bean的重复解决方案。
注意:版本不一样,连接elasticsearch的写法不一样,高版本好像最基本的配置不需要了。
5、mysql表如下:
6、common模块代码如下:
此处我的这个实体类有两个用处:
- 用于作为mybatis-plus的实体类(TableName注解映射mysql表名、TableField注解映射mysql字段)
- 用于作为elasticsearch的实体类(Document注解标记该实体类属于的索引和类型,类型默认是_doc类型,索引就相当于数据库的表名吧,若elasticsearch中不存在这个索引,会根据这个索引去创建一个、Field注解指定这个字段在elasticsearch是数值类型,但是在后面根据该字段排序时会使用。)
- 此处采用序列化是因为在rabbitmq传输该实体类对象时,若不序列化传输会报错。
7、在消费者模块准备rabbitMQ的绑定配置类
@Configuration public class TopicElasticSearchConfig { @Bean public TopicExchange saveExchange() { return new TopicExchange("es_save_exchange", true, false); } @Bean public TopicExchange deleteExchange() { return new TopicExchange("es_delete_exchange", true, false); } @Bean public Queue esSaveQueue() { return new Queue("es.save.queue", true); } @Bean public Queue esDeleteQueue() { return new Queue("es.delete.queue", true); } @Bean public Binding esSaveBinding() { return BindingBuilder.bind(esSaveQueue()).to(saveExchange()).with("es"); } @Bean public Binding esDeleteBinding() { return BindingBuilder.bind(esDeleteQueue()).to(deleteExchange()).with("es"); } }
建立一个接收修改和新增消息的交换机(因为对于elasticsearch来说,新增和修改方法一样),再建立一个接收删除消息的交换机,建立两个队列和绑定,如上图。
8、建立消费端的监听器
监听上面建立的两个队列。
9、建立mybatis-plus所需要的mapper层
10、建立测试用controller
准备工作完成。
五、实现ElasticSearch 新增商品信息
要实现向mysql数据库新增数据的同时通过rabbitMQ接收消息然后向elasticsearch也插入数据,所以实现如下:
先在controller编写新增接口
向数据库新增数据,同时向rabbitmq发送新增的消息。
然后再新增消息队列监听器编写向elasticsearch新增数据的逻辑。
操作elasticsearch是通过继承Repository接口完成的,在消费者服务和生产者服务都新增如下类
里面自定义的方法是后面所用到的,继承ElasticsearchRepository接口后默认提供了最基本的增删改查操作,类似Mybatis-plus的BaseMapper。
ElasticsearchRepository可以直接调用save方法进行报错。
结果如下:
同步保存成功。
六、实现ElasticSearch 修改商品信息
elasticsearch的修改操作也是调用Repository的save方法,若传入的id已存在则是执行修改,若传入的id不存在,则是新增。代码如下:
同样在controller里加上修改接口,然后向数据修改,然后发送消息到MQ,然后再MQ的队列监听器进行处理。
还是上面的新增逻辑,原因已经讲了。
查看效果:
这是原来的数据。
调用接口后,这是现在的数据
修改成功。
七、实现ElasticSearch 删除商品信息
删除跟新增修改差不多,代码如下:
controller新增删除接口。
在删除队列监听器处理elasticsearch的删除方法。
测试结果删除成功。
八、实现ElasticSearch 条件查询商品信息
后面就只针对provider服务实现了,不通过MQ了。
基本的增删改完了,然后一步一步的实现各种查询操作,指令实现基本的条件查询,如下。
新增条件查询测试接口。
根据name和content模糊查询,采用自定义查询语句的方式实现,这里要注意Repository的写法。
根据什么查询就写findBy什么然后加字段名And或者Or之类的,然后写下一个字段名,这里只学习了最基本的使用,详细的就不展开了,后面再面向百度学习,然后方法传入的参数会根据参数位置一一匹配方法名前面写的字段名,Repository会自动生成查询语句。
测试结果如下:
条件查询成功。
九、实现ElasticSearch 分页查询商品信息
然后实现一下分页查询,需要用到Pageable类,提供分页参数,Repository的方法支持这个类来查询,代码如下:
依旧新增一个测试接口。
测试结果:
测试成功。注意:Pageable默认的入参是第0页,按每页20条查询。网上有说默认是(0,10),可能是该类的版本不同,我用的版本是默认0,20。对于elasticsearch来说是先从0开始的。还要注意传参是page和size,别写错了。返回的Page数据结构如下:
十、实现ElasticSearch 条件+分页查询商品信息
当完成了分页查询和条件查询,下面写一个接口同时满足上面两个,代码如下。
新增一个测试接口,然后同时传入查询条件和分页参数,重载一个刚才的方法,修改入参和返回值。
测试结果如下。
测试成功。
十一、实现ElasticSearch 高亮显示与查询条件相符合的值
下面简单实现一个elasticsearch的特色,高亮显示。当我们在百度搜索时,例如搜索牛批。
它能够高亮显示搜索到的信息中所匹配的值,下面通过elasticsearch简单实现一下,代码如下。
//高亮显示 @CrossOrigin @GetMapping("getLightShopList") public List<Shop> getLightShopList(@RequestParam(required = false) String name) { //构建查询条件,只要name或者content其中一个满足传入的name的查询条件即可 // QueryBuilder queryBuilder1 = new MatchQueryBuilder("name", name); // QueryBuilder queryBuilder2 = new MatchQueryBuilder("content", name); //上面这样写只能用到最后一个,要下面这样写,组合查询 QueryBuilder queryBuilder = QueryBuilders.boolQuery() .should(QueryBuilders.matchQuery("name", name)) .should(QueryBuilders.matchQuery("content", name)); //查询 NativeSearchQuery searchQuery = new NativeSearchQueryBuilder() .withQuery(queryBuilder) //添加高亮显示字段 .withHighlightFields( new HighlightBuilder.Field("name") , new HighlightBuilder.Field("content")) //自定义高亮显示颜色 .withHighlightBuilder(new HighlightBuilder().preTags("<span style='color:yellow'>").postTags("</span>")) .build(); //开始查询 SearchHits<Shop> search = elasticsearchRestTemplate.search(searchQuery, Shop.class); //得到查询返回的内容 List<SearchHit<Shop>> searchHits = search.getSearchHits(); //设置一个最后需要返回的实体类集合 List<Shop> shops = new ArrayList<>(); //遍历返回的内容进行处理 for (SearchHit<Shop> searchHit : searchHits) { //高亮的内容 Map<String, List<String>> highlightFields = searchHit.getHighlightFields(); //将高亮的内容填充到content中 searchHit.getContent().setName(highlightFields.get("name") == null ? searchHit.getContent().getName() : highlightFields.get("name").get(0)); searchHit.getContent().setContent(highlightFields.get("content") == null ? searchHit.getContent().getContent() : highlightFields.get("content").get(0)); //放到实体类中 shops.add(searchHit.getContent()); } return shops; }
上面的例子都是通过Repository提供的APi方法实现的,高亮显示通过它加上注解好像也可以实现,上面的方法是引入elasticsearchRestTemplate来实现的。
- 先构建查询条件,通过第二种方式实现,第一种方式创建两个对象,在使用时第二个会覆盖第一个。
- 整合查询条件以及高亮显示设置,自定义需要高亮显示的字段,并自定义高亮显示的字段的颜色。
- 调用方法查询,并将返回的数据重新封装。
测试结果如下:
name和content字段中,相匹配的就会查询出来并高亮显示。具体的匹配规则和优先级都是可以设置的。
十二、实现ElasticSearch的高亮显示与查询条件相符合的值并分页和排序
完成了最基本的高亮显示,最后再同时整点分页和排序。
代码如下。
//高亮显示+分页+条件+排序 @CrossOrigin @GetMapping("getLightShopByPageList") public Map getLightShopByPageList(@RequestParam(required = false) String name,Pageable pageable) { //组合查询 QueryBuilder queryBuilder = QueryBuilders.boolQuery() .should(QueryBuilders.matchQuery("name", name)) .should(QueryBuilders.matchQuery("content", name)); //构建排序,如果实体类上该字段没有加type,这里排序字段上面要加keyword,不然会报错。 // FieldSortBuilder priceSortBuilder = SortBuilders.fieldSort("price.keyword").order(SortOrder.ASC); FieldSortBuilder priceSortBuilder = SortBuilders.fieldSort("price").order(SortOrder.ASC); //查询 NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(queryBuilder) //添加高亮显示字段 .withHighlightFields(new HighlightBuilder.Field("name"), new HighlightBuilder.Field("content")) //自定义高亮显示颜色 .withHighlightBuilder(new HighlightBuilder().preTags("<span style='color:yellow'>").postTags("</span>")) .withPageable(pageable) .withSorts(priceSortBuilder) .build(); //开始查询 SearchHits<Shop> search = elasticsearchRestTemplate.search(searchQuery, Shop.class); //得到查询返回的内容 List<SearchHit<Shop>> searchHits = search.getSearchHits(); //设置一个最后需要返回的实体类集合 List<Shop> shops = new ArrayList<>(); //遍历返回的内容进行处理 for (SearchHit<Shop> searchHit : searchHits) { //高亮的内容 Map<String, List<String>> highlightFields = searchHit.getHighlightFields(); //将高亮的内容填充到content中 searchHit.getContent().setName(highlightFields.get("name") == null ? searchHit.getContent().getName() : highlightFields.get("name").get(0)); searchHit.getContent().setContent(highlightFields.get("content") == null ? searchHit.getContent().getContent() : highlightFields.get("content").get(0)); //放到实体类中 shops.add(searchHit.getContent()); } Map map = new HashMap(); map.put("currentPageSIze",pageable.getPageSize()); map.put("currentNumber",pageable.getPageNumber()); map.put("content",shops); return map; }
对比上一个就是增加了排序和分页,所有直接在上面改动了。
- 通过FieldSortBuilder构建一个排序Builder通过withSorts方法加入到NativeSearchQuery中。
- 新增传入对象Pageable,再通过withPageable方法加入到NativeSearchQuery。
- 封装一下。
注意:排序时,若实体类上price字段没加@Field(type = FieldType.Integer), 查询会报错,在查询时查询字段必须写成price.keyword,否则就必须在实体类上加@Field注解,不然请求接口测试会报elasticsearch的错。若一个分区的数据(不设置默认在一个分区),若存在price数据不是同种类型,排序也会失效,不知道对不对,反正我测试时遇到了这个问题,后面测试时把前面该索引(shop)直接删了重新来过的(为了保证price类型一致)。
测试结果如下。
若修改成SortOrder.DESC。
查看排序成功。
(2022.4.3新增)上面是通过elasticsearchRestTemplate的方法实现的,下面简单使用注解方式实现一下:
修改Repository类:
仍然借助Repository自动给我们生成查询语句,不同的是,我们自定义高亮显示字段,配合Highlight注解、HighlightField注解、HighlightParameters注解实现。
- Highlight:设置高亮显示参数。
- HighlightField:注明哪些参数需要高亮显示。
- HighlightParameters:自定义高亮的样式参数设置,类似elasticsearchRestTemplate里面的HighlightBuilder。
controller层新增测试接口:
然后写了一个简单的前端界面进行测试。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> </head> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script> <body> <div class="row" style="margin-left: 30%;margin-top: 30px;"> <div class="col-lg-6"> <div class="input-group"> <div class="input-group-prepend"> <span class="input-group-text " id="basic-addon1">名称</span> </div> <input type="text" class="form-control name" placeholder="搜索"> <div class="input-group-prepend" style="margin-left: 20px;"> <span class="input-group-text " id="basic-addon1">内容</span> </div> <input type="text" class="form-control content" placeholder="搜索"> <span class="input-group-btn"> <button class="btn btn-primary" type="button" onclick="fun1()">搜索</button> </span> </div><!-- /input-group --> </div><!-- /.col-lg-6 --> </div><!-- /.row --> <div class="neirong" style="width: 100%;height: 500px;background-color: aliceblue;display: flex;justify-content: center;"> </div> </body> <script> function fun1(){ console.log("点击"); $.ajax({ url:"http://localhost:7778/shopController/hightLightShow2", data:{"page":"0","size":"10","name":$(".name").val(),"content":$(".content").val()}, type:"get", dataType: "json", success: function(data) { $(".neirong").html(''); console.log(data); var html=""; for(var i=0;i<data.length;i++){ name = data[i].highlightFields.name==undefined?data[i].content.name:data[i].highlightFields.name content = data[i].highlightFields.content==undefined?data[i].content.content:data[i].highlightFields.content html = html+` <div class="card" style="width: 18rem;height:18rem;margin-right:10px"> <div class="card-body"> <h5 class="card-title">`+name+`</h5> <p class="card-text">`+content+`</p> <a href="#" class="btn btn-primary">购买</a> </div> </div> ` } $(".neirong").append(html); } }); } </script> </html>
最终效果就是下图这个样子:
后面继续学习elasticsearch。