仿牛客网社区项目(三十)搜索服务

6.5 开发社区搜索功能

搜索服务

  • 将帖子保存至Elasticsearch服务器。
    • 对贴子实体类DiscussPost用注解进行相关配置
    • 从Mybatis取数据存入
    • 在dao层创建DiscussPostRepository类,继承ElasticsearchRepository接口即可,它集成了CRUD方法
  • 从Elasticsearch服务器删除帖子。
  • 从Elasticsearch服务器搜索帖子。
    • Es可以在搜索到的词加标签,达到高亮显示
    • 利用elasticTemplate.queryForPage()查询

发布事件

  • 发布帖子时,将帖子异步的提交到Elasticsearch服务器。
    • 新建ElasticsearchService类,定义CRUD和搜索方法。
    • 在DiscussPostController类发帖时,定义和触发发帖事件(Event、eventProducer.fireEvent(event))
  • 增加评论时,将帖子异步的提交到Elasticsearch服务器。
    • 在CommentController类发表评论时,定义和触发发帖事件
  • 在消费组件中增加一个方法,消费帖子发布事件。
    • 在EventConsumer类增加消费发帖事件的方法
    • 在事件中查询帖子,存到Es服务器

显示结果

  • 在控制器中处理搜索请求,在HTML上显示搜索结果。
    • 新建SearchController类处理搜索请求
    • 此时为GET请求,keyword的传入(search?keyword=xxx)
    • 修改index.html,表单提交路径,文本框name="keyword"
    • 在search.html修改,遍历取到帖子。

1.开篇点题一个小bug

在discusspostmapper.xml中的insertDiscussPost 增加keyproperty=“id”就是自增id 我用的mybtisplus 都自带自增id,在实体类上注释一些就ok,

 <insert id="insertDiscussPost" parameterType="DiscussPost" keyProperty="id">
        insert into discuss_post(<include refid="insertFields"></include>)
        values(#{userId},#{title},#{content},#{type},#{status},#{createTime},#{commentCount},#{score})
 </insert>

2.先在Discusspostmapper中新增几个方法用于查询数据,然后由es使用,

int selectDiscussPostRows(@Param("userId") int userId);
    List<DiscussPost> selectDiscussPosts(@Param("userId") int userId,@Param("offset") int offset,@Param("limit") int limit);

    int insertDiscussPost(DiscussPost discussPost);

    DiscussPost selectDiscussPostById(int id);

3.写对应的xml

<select id="selectDiscussPostRows" resultType="int">
    select count(id)
    from discuss_post
    where status != 2
    <if test="userId!=0">
        and user_id = #{userId}
    </if>
</select>

<select id="selectDiscussPostById" resultType="DiscussPost">
    select <include refid="selectFields"></include>
    from discuss_post
    where id = #{id}
</select>


<insert id="insertDiscussPost" parameterType="DiscussPost" keyProperty="id">
    insert into discuss_post(<include refid="insertFields"></include>)
    values(#{userId},#{title},#{content},#{type},#{status},#{createTime},#{commentCount},#{score})
</insert>

4.编写elasticsearchService 这里开始就不同了,我使用的是新版的ES,原先的都废除了

@Service
public class ElasticsearchService {

    @Autowired
    private DiscussPostRepository discussRepository;

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    public void saveDiscussPost(DiscussPost post) {
        discussRepository.save(post);
    }

    public void deleteDiscussPost(int id) {
        discussRepository.deleteById(id);
    }

    public SearchResult searcherDiscussPost(String keyword, int current, int limit) throws IOException {
        SearchRequest searchRequest = new SearchRequest("discusspost");//discusspost是索引名,就是表名

        //高亮
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        highlightBuilder.field("title");
        highlightBuilder.field("content");
        highlightBuilder.requireFieldMatch(false);
        highlightBuilder.preTags("<span style='color:red'>");
        highlightBuilder.postTags("</span>");

        //构建搜索条件
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder()
                .query(QueryBuilders.multiMatchQuery(keyword, "title", "content"))
                .sort(SortBuilders.fieldSort("type").order(SortOrder.DESC))
                .sort(SortBuilders.fieldSort("score").order(SortOrder.DESC))
                .sort(SortBuilders.fieldSort("createTime").order(SortOrder.DESC))
                .from(current)// 指定从哪条开始查询
                .size(limit)// 需要查出的总记录条数
                .highlighter(highlightBuilder);//高亮

        searchRequest.source(searchSourceBuilder);
        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

        List<DiscussPost> list = new LinkedList<>();
        long total = searchResponse.getHits().getTotalHits().value;
        for (SearchHit hit : searchResponse.getHits().getHits()) {
            DiscussPost discussPost = JSONObject.parseObject(hit.getSourceAsString(), DiscussPost.class);

            // 处理高亮显示的结果
            HighlightField titleField = hit.getHighlightFields().get("title");
            if (titleField != null) {
                discussPost.setTitle(titleField.getFragments()[0].toString());
            }
            HighlightField contentField = hit.getHighlightFields().get("content");
            if (contentField != null) {
                discussPost.setContent(contentField.getFragments()[0].toString());
            }
            System.out.println(discussPost);
            list.add(discussPost);
        }
        return new SearchResult(list, total);
    }

5.对静态变量处理一下

/**
 * 主题: 发帖
 */
String TOPIC_PUBLISH = "publish";

6.对DiscusspostContoller的add做一些修改,流程是这样发布帖子-> 发送帖子id到队列-> 队列处理 放入es中

所以我们先把消息发到队列中,发出一个事件

@RequestMapping(path = "/add", method = RequestMethod.POST)
@ResponseBody
public String addDiscussPost(String title, String content) {
    User user = hostHolder.getUser();
    if (user == null) {
        return CommunityUtil.getJSONString(403, "你还没有登录哦!");
    }

    DiscussPost post = new DiscussPost();
    post.setUserId(user.getId());
    post.setTitle(title);
    post.setContent(content);
    post.setCreateTime(new Date());
    discussPostService.addDiscussPost(post);
    //触发es事件
    Event event = new Event()
            .setTopic(TOPIC_PUBLISH)//这个名字就是routingKey 用的Topic实际上意义routingKey
            .setUserId(user.getId())
            .setEntityType(ENTITY_TYPE_POST)
            .setEntityId(post.getId());
    eventProduce.send(event);
    
    return CommunityUtil.getJSONString(0, "发布成功!");
}

7.我们还没对队列处理 在ttlConfig中声明一下队列

@Configuration
public class TtlQueueConfig {
    //交换机名字
    public static final String X_EXCHANGE = "X";
    //队列A 处理follow消息
    public static final String QUEUE_A = "QA";
    //队列B 处理Comment消息
    public static final String QUEUE_B = "QB";
    //队列C 处理like消息
    public static final String QUEUE_C = "QC";
    //队列D 处理publish消息
    public static final String QUEUE_D = "QD";
    //队列D 处理delet消息
    public static final String QUEUE_E = "QE";



    // 声明 xExchange
    @Bean("xExchange")
    public DirectExchange xExchange(){
        return new DirectExchange(X_EXCHANGE);
    }

    //声明队列 A ttl 为 10s 并绑定到对应的死信交换机
    @Bean("queueA")
    public Queue queueA(){
//        Map<String, Object> args = new HashMap<>(3);
//        //声明当前队列绑定的死信交换机
//        args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
//        //声明当前队列的死信路由 key
//        args.put("x-dead-letter-routing-key", "YD");
//        //声明队列的 TTL
//        args.put("x-message-ttl", 10000);
//        return QueueBuilder.durable(QUEUE_A).withArguments(args).build();
        return QueueBuilder.durable(QUEUE_A).build();
    }
    // 声明队列 A 绑定 X 交换机
    @Bean
    public Binding queueaBindingX(@Qualifier("queueA") Queue queueA,
                                  @Qualifier("xExchange") DirectExchange xExchange){
        return BindingBuilder.bind(queueA).to(xExchange).with("follow");
    }
    //声明队列 B ttl 为 40s 并绑定到对应的死信交换机
    @Bean("queueB")
    public Queue queueB(){
//        Map<String, Object> args = new HashMap<>(3);
//        //声明当前队列绑定的死信交换机
//        args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
//        //声明当前队列的死信路由 key
//        args.put("x-dead-letter-routing-key", "YD");
//        //声明队列的 TTL
//        args.put("x-message-ttl", 40000);
//        return QueueBuilder.durable(QUEUE_B).withArguments(args).build();
        return QueueBuilder.durable(QUEUE_B).build();
    }
    //声明队列 B 绑定 X 交换机
    @Bean
    public Binding queuebBindingX(@Qualifier("queueB") Queue queueB,
                                  @Qualifier("xExchange") DirectExchange xExchange){
        return BindingBuilder.bind(queueB).to(xExchange).with("comment");
    }

    //生命队列
    @Bean("queueC")
    public Queue queueC(){
        return QueueBuilder.durable(QUEUE_C).build();
    }
    //声明队列 B 绑定 X 交换机
    @Bean
    public Binding queuecBindingX(@Qualifier("queueC") Queue queueC,
                                  @Qualifier("xExchange") DirectExchange xExchange){
        return BindingBuilder.bind(queueC).to(xExchange).with("like");
    }


    //声明队列 QD
    @Bean("queueD")
    public Queue queueD(){
        return QueueBuilder.durable(QUEUE_D).build();
    }
    //声明队列 QD 绑定关系
    @Bean
    public Binding deadLetterBindingQD(@Qualifier("queueD") Queue queueD,
                                        @Qualifier("xExchange") DirectExchange xExchange){
        return BindingBuilder.bind(queueD).to(xExchange).with("publish");
    }

    //声明队列 QD
    @Bean("queueE")
    public Queue queueE(){
        return QueueBuilder.durable(QUEUE_E).build();
    }
    //声明队列 QD 绑定关系
    @Bean
    public Binding deadLetterBindingQE(@Qualifier("queueE") Queue queueD,
                                        @Qualifier("xExchange") DirectExchange xExchange){
        return BindingBuilder.bind(queueD).to(xExchange).with("delete");
    }
}

8.声明完之后 对队列的消费者做处理,也就是EventConsumer

//publish队列
@RabbitListener(queues = "QD")
public void receivePublish(String jsonString, Channel channel){
    if(  jsonString==null||jsonString.length()==0){
        log.error("消息为空");
    }
    Event event = JSONObject.parseObject(jsonString, Event.class);
    if (event==null){
        log.error("类型转换失败");
        return;
    }
    //查帖子 加到
    DiscussPost post = discussPostService.getDiscussPostById(event.getEntityId());
    elasticsearchService.saveDiscussPost(post);
}

9.对帖子的评论之后,触发一次添加帖子,目的是更新帖子的评论数量状况,我觉得是后来的热榜需要评论数量做标准,所以更新

@RequestMapping(path = "/add/{discussPostId}", method = RequestMethod.POST)
public String addComment(@PathVariable("discussPostId") int discussPostId, Comment comment) {
    comment.setUserId(hostHolder.getUser().getId());
    comment.setStatus(0);
    comment.setCreateTime(new Date());
    commentService.addComment(comment);

    // 触发评论事件
    Event event = new Event()
            .setTopic(TOPIC_COMMENT)
            .setUserId(hostHolder.getUser().getId())
            .setEntityType(comment.getEntityType())
            .setEntityId(comment.getEntityId())
            .setData("postId", discussPostId);
    if (comment.getEntityType() == ENTITY_TYPE_POST) {
        DiscussPost target = discussPostService.getDiscussPostById(comment.getEntityId());
        event.setEntityUserId(target.getUserId());
    } else if (comment.getEntityType() == ENTITY_TYPE_COMMENT) {
        Comment target = commentService.findCommentById(comment.getEntityId());
        event.setEntityUserId(target.getUserId());
    }
    eventProduce.send(event);
    //先判断一下 是否为帖子的评论再去加到es
    if(comment.getEntityType()==ENTITY_TYPE_POST){
        event = new Event()
                .setTopic(TOPIC_PUBLISH)
                .setUserId(comment.getUserId())
                .setEntityType(ENTITY_TYPE_POST)
                .setEntityId(discussPostId);
        eventProduce.send(event);
    }

    return "redirect:/discuss/detail/" + discussPostId;
}

10.创建elasticsearchController用于响应服务

@Controller
public class ElasticSearchController implements CommunityConstant {
    private static final Logger LOGGER = LoggerFactory.getLogger(ElasticSearchController.class);
    @Autowired
    ElasticsearchService elasticSearchService;
    @Autowired
    UserService userService;
    @Autowired
    LikeService likeService;
    
    @GetMapping("search")
    public String search(String keyword, NewPage page, Model model) {
        try {
            //(page.getCurrent() - 1)*1每10个一页,第一次查询的10条可以跳过了,所以*10
            //看一下对应的方法
            SearchResult searchResult = elasticSearchService.searcherDiscussPost
                (keyword, (page.getCurrent() - 1)*10, page.getLimit());
            List<Map<String,Object>> discussPosts = new ArrayList<>();
            List<DiscussPost> list = searchResult.getList();
            if(list != null) {
                for (DiscussPost post : list) {
                    Map<String,Object> map = new HashMap<>();
                    //帖子 和 作者
                    map.put("post",post);
                    map.put("user",userService.findUserById(Integer.valueOf(post.getUserId())));
                    // 点赞数目
                    map.put("likeCount",likeService.findEntityLikeCount(ENTITY_TYPE_POST, post.getId()));

                    discussPosts.add(map);
                }
            }
            model.addAttribute("discussPosts",discussPosts);
            model.addAttribute("keyword",keyword);
            //分页信息
            page.setPath("/search?keyword=" + keyword);
            page.setRows(searchResult.getTotal() == 0 ? 0 : (int) searchResult.getTotal());
        } catch (IOException e) {
            LOGGER.error("系统出错,没有数据:" + e.getMessage());
        }
        return "/site/search";
    }

}

对静态资源处理 index页面有一个搜索框,处理一下其他都可以复用

<!-- 搜索 -->
<form class="form-inline my-2 my-lg-0" method="get" th:action="@{/search}">
   <input class="form-control mr-sm-2" type="search" aria-label="Search" name="keyword" th:value="${keyword}"/>
   <button class="btn btn-outline-light my-2 my-sm-0" type="submit">搜索</button>
</form>

还有一个Search.html页面 不想粘贴了浪费地方

posted @ 2022-05-03 19:27  卷皇  阅读(316)  评论(0编辑  收藏  举报