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 QuestionRepository
          extends 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开始,所以要加1
        int pageNum = page.getNumber()+1;
        //当前页面大小
        int pageSize = page.getSize();
        //总页数 pages
        int 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 {

    @Resource
    private IQuestionService questionService;
    @PostMapping
    public 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);
        // 别忘了返回pageInfo
        return  pageInfo;
    }
}

控制器编写完成

但是在运行之前要将所有微服务项目的支持添加配置完成

完成微服务相关配置

配置网关信息

gateway模块

application.yml文件,添加新的路由信息

- id: gateway-search
  uri: lb://search-service
  predicates:
    - 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(数组)在转换为Map
    String 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\windows

F:\kafka\bin\windows>zookeeper-server-start.bat ..\..\config\zookeeper.properties

再启动kafka

打开命令行Win+R输入cmd

C:\Users\TEDU>F:

F:\>cd F:\kafka\bin\windows

F:\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中配置

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值