ProjectDay19
续 完成达内知道的搜索功能
执行搜索业务
上次课我们完成了将mysql中question表中的所有数据复制到ES的操作
现在ES中包含所有mysql中包含的问题
要进行查询直接在查询ES中的信息,查出的信息就是mysql中的
分析实现搜索的思路
用户输入一个关键字
question表中title列包含这个关键字或content列包含这个关键字就查询出来
同时这个问题应该是登录用户提问或其它用户公开的
这样的查询逻辑编写为sql语句如下
SELECT * FROM question
WHERE
(title LIKE '%java%'
OR
content LIKE '%java%')
and
(user_id=11
OR
public_status=1)
如果上面的查询转换到es中进行
查询示意图如下
我们查询结果需要满足下面条件
1.问题的title或者content要包含查询的关键字
2.问题必须是登录用户提问的或是公开状态的
1和2两个条件又是"并且\and"的关系
match(匹配):相当于数据库中的like,进行模糊查询用的,ES中就是匹配指定的分词
term(相等):相当于数据库中的"=",执行判等操作,这里判断是整型数值
should(应该):相当于数据库中的or也就是"或"关系
must(必须):相等于数据库中的and也就是"与"关系
上面的查询逻辑在ES中可以编写为:
### 条件搜索,查询用户11 或者 公开的 同时 标题或者内容中包含Java的问题
POST http://localhost:9200/knows/_search
Content-Type: application/json{"query": {"bool": {"must": [{"bool": {"should": [{"match": {"title": "java"}}, {"match": {"content": "java"}}]}}, {"bool": {"should": [{"term": {"publicStatus": 1}}, {"term": {"userId": 11}}]}}]}}
}
这样的查询不适合再使用SpringData中用方法名来表示查询逻辑的情况了
编写搜索功能的数据访问层
QuestionRepository接口中需要编写上面章节中确定的查询语句
// 数据访问层注解@Repository必须要添加
@Repository
public interface QuestionRepositoryextends ElasticsearchRepository<QuestionVO,Integer> {@Query("{\n" +" \"bool\": {\n" +" \"must\": [{\n" +" \"bool\": {\n" +" \"should\": [\n" +" {\"match\": {\"title\": \"?0\"}}, \n" +" {\"match\": {\"content\": \"?1\"}}]\n" +" }\n" +" }, {\n" +" \"bool\": {\n" +" \"should\": [\n" +" {\"term\": {\"publicStatus\": 1}}, \n" +" {\"term\": {\"userId\": ?2}}]\n" +" }\n" +" }]\n" +" }\n" +" }")Page<QuestionVO> queryAllByParams(String title, String content,Integer userId, Pageable pageable);}
上面的数据访问层方法需要进行测试才能确保能够正确运行
在保证Nacos和ES运行的情况下
运行测试代码如下
// 测试搜索查询的数据访问层方法
@Test
public void search(){Page<QuestionVO> questions=questionRepository.queryAllByParams("java","java",11, PageRequest.of(0,8));questions.forEach(q-> System.out.println(q));
}
如果运行正常
表示数据访问层一切正常
创建分页信息转换类
编写完数据访问层应该编写业务逻辑层
但是在此之前我们需要先编写实现一个能够将Page转换为PageInfo类型的功能
因为我们的页面前端代码,都是在支持PageInfo类型对象的
如果返回值类型变为Page对象,那么前端代码就会有很多维护工作
如果在业务逻辑层返回前能够将Page转换为PageInfo,那么前端就可以直接复用了
所以要在编写业务逻辑层之前先编写这个转换代码
knows-search模块
创建utils包,包中创建转换类Pages
public class Pages {/*** 将Spring-Data提供的翻页数据,转换为Pagehelper翻页数据对象* @param page Spring-Data提供的翻页数据* @return PageInfo*/public static <T> PageInfo<T> pageInfo(Page<T> page){//当前页号从1开始, Spring-Data从0开始,所以要加1int pageNum = page.getNumber()+1;//当前页面大小int pageSize = page.getSize();//总页数 pagesint pages = page.getTotalPages();//当前页面中数据List<T> list = new ArrayList<>(page.toList());//当前页面实际数据大小,有可能能小于页面大小int size = page.getNumberOfElements();//当前页的第一行在数据库中的行号, 这里从0开始int startRow = page.getNumber()*pageSize;//当前页的最后一行在数据库中的行号, 这里从0开始int endRow = page.getNumber()*pageSize+size-1;//当前查询中的总行数long total = page.getTotalElements();PageInfo<T> pageInfo = new PageInfo<>(list);pageInfo.setPageNum(pageNum);pageInfo.setPageSize(pageSize);pageInfo.setPages(pages);pageInfo.setStartRow(startRow);pageInfo.setEndRow(endRow);pageInfo.setSize(size);pageInfo.setTotal(total);pageInfo.calcByNavigatePages(PageInfo.DEFAULT_NAVIGATE_PAGES);return pageInfo;}
}
编写搜索功能的业务逻辑层代码
业务逻辑层开始编写
先在IQuestionService接口中定义搜索方法
// 按用户输入的关键字进行搜索的业务逻辑层方法
PageInfo<QuestionVO> search(String key,String username,Integer pageNum,Integer pageSize);
QuestionServiceImpl类实现代码如下
@Override
public PageInfo<QuestionVO> search(String key, String username, Integer pageNum, Integer pageSize) {// 根据用户名查询用户对象String url="http://sys-service/v1/auth/user?username={1}";User user=restTemplate.getForObject(url,User.class,username);// 定义分页条件和排序规则的Pageable对象Pageable pageable= PageRequest.of(pageNum-1,pageSize,Sort.Direction.DESC,"createtime");// 调用数据访问层进行查询Page<QuestionVO> page=questionRepository.queryAllByParams(key,key,user.getId(),pageable);// 将page转换为PageInfo返回return Pages.pageInfo(page);
}
业务逻辑层也要测试
之前的服务不需要重启继续运行即可
新启动sys模块
进行测试代码如下
// 测试搜索的业务逻辑层方法
@Test
void testService(){PageInfo<QuestionVO> pageInfo=questionService.search("java","st2",1,8);pageInfo.getList().forEach(q-> System.out.println(q));}
如果代码没问题但是运行失败
可以尝试删除分页条件中的排序参数,再运行试试
如果成功了,就是ES不稳定造成的,需要重启ES甚至重新安装ES才能解决
编写控制层代码
knows-search模块中创建controller包
包中创建QuestionController类
@RestController
@RequestMapping("/v3/questions")
public class QuestionController {@Resourceprivate IQuestionService questionService;@PostMappingpublic PageInfo<QuestionVO> search(String key,Integer pageNum,@AuthenticationPrincipal UserDetails user){Integer pageSize=8;if(pageNum==null){pageNum=1;}PageInfo<QuestionVO> pageInfo=questionService.search(key,user.getUsername(),pageNum,pageSize);// 别忘了返回pageInforeturn pageInfo;}
}
控制器编写完成
但是在运行之前要将所有微服务项目的支持添加配置完成
完成微服务相关配置
配置网关信息
gateway模块
application.yml文件,添加新的路由信息
- id: gateway-searchuri: lb://search-servicepredicates:- Path=/v3/**
跨域和SpringSecurity放行以及拦截器注册
转回knows-search模块
我们当下项目还没有设置跨域放行和拦截器注册的配置
这些配置可以直接从faq模块复制
建议直接复制security包和Interceptor包,直接粘贴到search模块的相同位置
粘贴后WebConfig类需要修改包的导入
并且删除原有的拦截器注册路径,修改为:
@Override
public void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(authInterceptor).addPathPatterns("/v3/questions" //搜索问题);
}
到此为止
微服务后端搜索功能就完成配置编写完成了
前端显示搜索结果
前端调用思路
我们要先明确前端整体的调用流程
我们现在开发的功能是学生首页执行搜索功能
index_student.html页面搜索框输入内容后点击搜索按钮
我们设计为跳转到search.html页面,然后将用户输入的内容
保存到search.html页面地址栏?之后
search.html页面加载完毕后,按?之后的条件,使用axios向search模块进行异步的搜索查询调用,最后将搜索的结果显示在页面上
学生首页跳转到搜索页面
转到knows-client项目
在index_student.html页面中的搜索输入框的位置修改代码如下
index_student.html的40行附近
<div class="form-inline my-2 my-lg-0" id="searchApp"><input class="form-control form-control-sm mr-sm-2 rounded-pill"type="search" placeholder="Search" aria-label="Search"v-model="key"><!-- ↑↑↑↑↑↑ --><button class="btn btn-sm btn-outline-secondary my-2 my-sm-0 rounded-pill"type="button"@click="search"><!-- ↑↑↑↑↑↑ --><i class="fa fa-search" aria-hidden="true"></i></button>
</div>
我们可以再index_student.html页面的尾部
添加vue代码.用户点击按钮时跳转到search.html
<script>let searchApp=new Vue({el:"#searchApp",data: {key:""},methods:{search:function(){// 这里的功能是跳转到search.html页面,并且携带用户输入的关键字// 1.location.href进行跳转// 2.路径是/search.html?// 3.encodeURI()是能够将中文转换为可以传递的格式// 4.this.key是Vue绑定的用户输入的关键字location.href="/search.html?"+encodeURI(this.key);}}})
</script>
启动或重启client项目
在学生首页输入关键字点击搜索按钮,观察是否能够跳转的设计的路径
创建搜索结果页search.html
上面章节实现跳转效果显示404,因为我们的项目还没有创建search.html
显示结果的格式和讲师任务列表\学生问题列表是一致的
所谓我们建议大家直接复制讲师首页为search.html
将页面中"我的任务"修改为"搜索结果"(也可以修改一下搜索图标)
search.html的183行附近修改
<h4 class="border-bottom m-2 p-2 font-weight-light"><i class="fa fa-search" aria-hidden="true"></i> 搜索结果
</h4>
因为当前页面不再是查询讲师首页信息的页面,所以要修改页面尾部的引用
不再引用index_teacher.js,修改为引用search.js
<script src="js/utils.js"></script>
<script src="js/tags_nav_temp.js"></script>
<script src="js/tags_nav.js"></script>
<script src="js/user_info_temp.js"></script>
<script src="js/user_info.js"></script>
<script src="js/search.js"></script>
<!-- ↑↑↑↑↑↑ ↑↑↑↑↑↑ ↑↑↑↑↑↑ -->
</body>
其实js文件的内容也和讲师js文件逻辑类似
我们可以复制index_teacher.js文件为search.js文件
然后进行一些修改即可完成搜索功能的调用
loadQuestions:function (pageNum) {if(! pageNum){pageNum = 1;}// 这里开始代码的修改 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓// 获得地址栏?之后的内容let key=location.search;if(!key){return;}// 获取?之后的搜索关键字,这里要注意中文的转换key=decodeURI(key.substring(1));// 定义搜索功能提交的表单let form=new FormData();form.append("key",key);form.append("pageNum",pageNum);form.append("accessToken",token);axios({url: 'http://localhost:9000/v3/questions',method: "post",data:form// 修改的代码结束↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑})......
下面可以进行测试
需要启动Nacos\ES\Redis
gateway\sys\faq\auth\search
重启client
登录学生,在学生首页输入关键字
查询出的搜索结果是可以分页的
但是没有图片和标签
显示问题的标签和配图
上面的查询不能实现标签和图片的原因是因为
QuestionVO类中的tags属性没有赋值
之前portal项目中我们是利用Question的tagNames属性来获得对应标签的集合
在当前search模块中,我们也要利用QuestionVO的tagNames属性来获得对应标签的集合
只是所有标签内容的获取要通过Ribbon来调用faq模块获得
转到knows-search模块
在QuestionServiceImpl类中添加一个方法获得对应的标签集合
并在搜索功能的方法中来调用
具体代码如下
@Override
public PageInfo<QuestionVO> search(String key, String username, Integer pageNum, Integer pageSize) {// 根据用户名查询用户对象String url="http://sys-service/v1/auth/user?username={1}";User user=restTemplate.getForObject(url,User.class,username);// 定义分页条件和排序规则的Pageable对象Pageable pageable= PageRequest.of(pageNum-1,pageSize,Sort.Direction.DESC,"createtime");// 调用数据访问层进行查询Page<QuestionVO> page=questionRepository.queryAllByParams(key,key,user.getId(),pageable);// 新增的for循环↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓// 遍历所有问题新增tags属性for(QuestionVO vo:page){vo.setTags(tagNamesToTags(vo.getTagNames()));}// 新增代码结束 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑// 将page转换为PageInfo返回return Pages.pageInfo(page);
}
// 新增的方法↓↓↓↓↓↓↓↓↓↓↓↓↓↓
// 编写一个根据tagNames属性返回对应的List<Tag>对象的方法
private List<Tag> tagNamesToTags(String tagNames){// tagNames:"Java基础,Java SE,面试题"String[] names=tagNames.split(",");// 获得包含所有标签的map,以便通过标签名称获得标签对象// 但是faq模块只有获得所有标签对象List的Rest接口// 所以我们先获得List(数组)在转换为MapString url="http://faq-service/v2/tags";Tag[] tagArr=restTemplate.getForObject(url,Tag[].class);// 创建一个Map,遍历数组向启动赋值即可Map<String,Tag> tagMap=new HashMap<>();for(Tag t:tagArr){tagMap.put(t.getName(),t);}// 实例化一个空List,用户保存标签名称对应的标签对象List<Tag> tags=new ArrayList<>();for(String name : names){tags.add(tagMap.get(name));}return tags;}
重启search模块
建议重新登录学生,到学生首页进行搜索
搜索结果中就能显示所有标签和配图了
消息队列
下载Kafka
上面章节为止
我们已经完成了搜索整体功能四个阶段中的3个
1.数据同步
2.按关键字搜索
3.显示搜索结果
4. 学生发布问题时,将该问题同时新增到ES(未完成)
学生发布问题新增到ES的性能问题
我们要完成新增问题时将这个问题也新增到ES的功能
需要两个模块的协作,
1.faq模块完成mysql的新增
2.search模块完成ES的新增
实现功能的思路变化如图
右侧的图是添加了新增问题同时新增到ES的业务流程
如果使用我们现在为止学习的技术,跨微服务调用只能使用Ribbon
但是faq模块的线程运行到这个位置时,需要等待Ribbon做出响应才能继续后面的内容,这样faq模块的线程就会进入阻塞状态
而进入阻塞的时间内,这个线程无法释放资源也无法进行其它操作,造成了资源和性能的浪费
如果要解决这个问题主要就是要消除faq模块调用Ribbon之后的等待时间
上图使用消息队列
faq模块将要新增的信息发送给消息队列
faq模块不需要等待search模块完成ES的新增,就能向前端做出响应,实现释放当前线程占用的资源,用于接受后面的请求
这样做就消除了faq模块的等待,提高了运行效率
什么是消息队列
消息队列(Message Queue)检查MQ
一般情况下用于代替等待时间较长的Ribbon请求
Ribbon请求必须是等待目标有响应之后才能继续运行的
但是消息队列是采用"异步(两个微服务项目并不需要同时完成请求)"的方式来传递数据
faq模块不等待search模块做出响应,就可以完成自己响应了!
消息队列特征
面试题:如何理解消息队列
- 利用异步的特性,提高服务器的运行效率,减少线程阻塞的时间
- 削峰填谷:在并发峰值的瞬间将信息保存到消息队列中,依次处理,不会因短时间需要处理大量请求而出现意外,在并发较少时在依次处理队列中的内容,直至处理完毕
- 消息队列的弊端:因为是异步执行,faq模块完成响应时,search模块可能还没有运行,这样的话就可能出现延迟的现象,如果不能接受这种延迟,就不要使用消息队列
我们开发中常见的消息队列有:
- ActiveMQ
- RabbitMQ
- RocketMQ(阿里)
- Kafka
kafka简介
Kafka是由Apache软件基金会开发的一个开源流处理平台,由Scala和Java编写。该项目的目标是为处理实时数据提供一个统一、高吞吐、低延迟的平台。Kafka最初是由LinkedIn开发,并随后于2011年初开源。
kafka的软件结构
Producer: 消息的发送方,既消息的来源,是生产者
faq就是消息的发送方
Consumer:消息的接收方,既消息的去处,是消费者
search模块就是消息的接收方
Topic:就是话题或主题的意思,消息的发送方和接收方需要统一一个话题名称,才能不会错误的将消息发送给其它人,或错误的接收其它人的信息
Record:消息记录,就是生产者和消费者传递的消息,保存在topic中
faq模块和search模块传递的信息就是一个Question对象的json格式字符串
Kafka的安装和启动
将下载的kafka压缩包在根目录解压
路径尽量短,否则运行时报错,路径不要有中文和空格
在当前目录下,再创建一个文件夹,名称随意,但必须是空的
本次创建的目录名称为data,它来保存kafka运行过程中的临时文件和日志文件
它并不需要进行安装操作
下面就可以启动了
kafka软件启动顺序是先启动zookeeper再启动kafka
启动zookeeper
我们kafka压缩包中是包含zookeeper软件的
在运行之前先做一些配置
F:\kafka\config下有文件zookeeper.properties
dataDir=F:/data
F:\kafka\config下有文件server.properties
log.dirs=F:/data
配置完毕之后
打开dos命令行界面
Win+R输入cmd
C:\Users\TEDU>F:F:\>cd F:\kafka\bin\windowsF:\kafka\bin\windows>zookeeper-server-start.bat ..\..\config\zookeeper.properties
再启动kafka
打开命令行Win+R输入cmd
C:\Users\TEDU>F:F:\>cd F:\kafka\bin\windowsF:\kafka\bin\windows>kafka-server-start.bat ..\..\config\server.properties
附录
Mac系统启动Kafka服务命令(参考):
# 进入Kafka文件夹
cd Documents/kafka_2.13-2.4.1/bin/
# 动Zookeeper服务
./zookeeper-server-start.sh -daemon ../config/zookeeper.properties
# 启动Kafka服务
./kafka-server-start.sh -daemon ../config/server.properties
Mac系统关闭Kafka服务命令(参考):
# 关闭Kafka服务
./kafka-server-stop.sh
# 启动Zookeeper服务
./zookeeper-server-stop.sh
在启动kafka时有一个常见错误
wmic不是内部或外部命令
这样的提示,需要安装wmic命令,安装方式参考
https://zhidao.baidu.com/question/295061710.html
英文
Cluster:集群
zookeeper和Kafka
zookeeper简介
zoo keeper
动物园 持有者
动物管理员
很多大数据软件都是动物命名或动物的logo
这些软件在运行前都需要进行一些配置
zookeeper是一个能够统一配置所有软件的配置信息的软件
提供了集中的配置方式,就无需找到对应软件才能配置这个软件了
后期有些软件干脆就不自带配置功能了,只能在zookeeper中配置
kafka就是只能在zookeeper中配置
ProjectDay19相关推荐
最新文章
- 一文搞懂TCP的三次握手和四次挥手
- 超美观的 Vue+Element 开源后台管理 UI
- python sort函数key_Python:s.sort([cmp[, key[, reverse]]])
- 学习需要总结。。。。
- rocketmq发送顺序消息(四)
- php发送https请求,php post 请求https接口
- 为什么开不了4g网络_为什么4G网络越来越慢?究竟是你手机出了问题还是网络原因?...
- python数据库-mongoDB的高级查询操作(55)
- sbit和sfr的定义
- Atitit 模块打包器(module bundler)的概念与使用 目录 1. 解决问题	1 1.1. 多js合并方便性能加载	1 1.2. 静态模块打包	2 1.3. 动态模块打包	2 2. 最
- Xmind2021绿色版,思维导图最佳软件
- 多媒体计算机主要有哪些基本特性,多媒体的特点主要包括哪些?
- 单片机(51) 什么是编码器?什么是译码器?
- 光盘自动播放 html,插入dvd光盘直接播放 禁用dvd菜单制作刻录dvd光盘 光盘直接放在DVD中就可以自动播放...
- SNN识别手写数字—MNIST数据集
- Android模拟器的判定
- Adobe Creative Cloud一直转圈无法进入的问题
- mesh和wifi中继的区别_一文看懂Mesh路由和无线中继的差异
- 微信小程序编译时警告:Component “pages/test2/test2“ does not have a method “ getData“ to handle event “t
- 如何开放防火墙端口?