6.3 开发社区搜索功能

86 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第30天,点击查看活动详情

6.3 开发社区搜索功能

image-20220726131945759

业务层

发布一个帖子的时候应该1.将帖子存到Elasticsearch服务器2.删帖子我们也应该从Elasticsearch服务器删去(当然现在删帖的功能还没有实现,但是我们在开发搜索服务的时候先把从Elasticsearch服务器删除帖子的方法先准备好,以后呢可以直接调用),然后重点就是我们要在组件里提供搜索的服务去3.搜索帖子

表现层

  1. 发布帖子时,采用异步的方式将帖子提交到Elasticsearch服务器

  2. 增加评论时,帖子的评论数量就会发生变化,这个时候我们也将帖子异步地提交到Elasticsearch服务器,相当于这是修改帖子

  3. 异步的方式主要是为了提高性能,当发了帖子以后,只要把事件丢到消息队列里,我们就可以继续处理下一个类似的请求,不用等待,所以说异步可以并行的处理一些事情,这样比较好。既然是异步的话,我们在发布帖子时、增加评论时触发了这样一个事件,我们需要在消费者组件里加一个方法来消费这个事件

当把数据同步到了ES服务器以后,剩下的就是查询了,查询的时候我们要想显示出搜索结果,我们需要在controller里处理搜索请求,然后在对应的html里显示结果。

首先先解决一个之前遗留的小问题

image-20220726150038650

然后正式开发刚才所述内容

事务层(Service)

新建一个 ElasticsearchService 处理业务层

@Service
public class ElasticsearchService {

    @Autowired
    private DiscussPostRepository discussRepository;    // 往ES里存、修改、删除数据、搜索可以用到

    @Autowired
    private ElasticsearchTemplate elasticTemplate;      // 这个的搜索方法可以做到高亮显示

    // 往ES里存数据(再存一次就是修改)
    public void saveDiscussPost(DiscussPost post) {
        discussRepository.save(post);
    }

    // 从ES里删除数据
    public void deleteDiscussPost(int id) {
        discussRepository.deleteById(id);
    }

    // 提供搜索方法并高亮显示  参数1:搜索的关键字, 搜索支持分页,传入分页条件 参数2:当前要显示第几页 参数3:每页显示多少条数据
    // Page是Spring提供的,不是我们自己写的实体类
    public Page<DiscussPost> searchDiscussPost(String keyword, int current, int limit) {
        SearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.multiMatchQuery(keyword, "title", "content"))//搜索的关键词并且在哪个字段搜
                .withSort(SortBuilders.fieldSort("type").order(SortOrder.DESC))       // 排序方式:倒序
                .withSort(SortBuilders.fieldSort("score").order(SortOrder.DESC))      // 排序方式:倒序
                .withSort(SortBuilders.fieldSort("createTime").order(SortOrder.DESC)) // 排序方式:倒序
                .withPageable(PageRequest.of(current, limit))                         // 分页方式
                .withHighlightFields(    // 指定哪些字段要高亮显示,怎么高亮显示
                        new HighlightBuilder.Field("title").preTags("<em>").postTags("</em>"),  // 高亮显示
                        new HighlightBuilder.Field("content").preTags("<em>").postTags("</em>") // 高亮显示
                ).build();
        // 参数1:搜索条件      参数2:实体类型    参数3:SearchResultMapper接口(实现一个匿名内部类或者传一个实现类)
        return elasticTemplate.queryForPage(searchQuery, DiscussPost.class, new SearchResultMapper() {
            @Override               // queryForPage得到结果然后交给mapResults处理,然后通过SearchResponse参数处理
            public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> aClass, Pageable pageable) {
                SearchHits hits = response.getHits();           // 先取到这次搜索命令的数据(里面可以是多条数据)
                if (hits.getTotalHits() <= 0) {                 // 判断有没有数据
                    return null;
                }

                List<DiscussPost> list = new ArrayList<>();
                for (SearchHit hit : hits) {                    // 遍历命中的数据将其放在集合里
                    DiscussPost post = new DiscussPost();       // 将命中的数据包装到实体类中
                    // hit里面是将数据封装成了map并且里面key和value都是String类型,我们可以从中取值
                    String id = hit.getSourceAsMap().get("id").toString();
                    post.setId(Integer.valueOf(id));            // 将字符类型的数转成整数存入实体类的id属性

                    String userId = hit.getSourceAsMap().get("userId").toString();
                    post.setUserId(Integer.valueOf(userId));

                    String title = hit.getSourceAsMap().get("title").toString();
                    post.setTitle(title);

                    String content = hit.getSourceAsMap().get("content").toString();
                    post.setContent(content);

                    String status = hit.getSourceAsMap().get("status").toString();
                    post.setStatus(Integer.valueOf(status));

                    String createTime = hit.getSourceAsMap().get("createTime").toString();
                    post.setCreateTime(new Date(Long.valueOf(createTime)));

                    String commentCount = hit.getSourceAsMap().get("commentCount").toString();
                    post.setCommentCount(Integer.valueOf(commentCount));

                    // 处理高亮显示的结果
                    HighlightField titleField = hit.getHighlightFields().get("title");
                    if (titleField != null) {
                        // getFragments()返回的是一个数组,因为匹配的词条有可能是多个,我们只将第一个设置成高亮即可
                        post.setTitle(titleField.getFragments()[0].toString());
                    }

                    HighlightField contentField = hit.getHighlightFields().get("content");
                    if (contentField != null) {
                        post.setContent(contentField.getFragments()[0].toString());
                    }

                    list.add(post);
                }
                // AggregatedPageImpl   参数1:集合 参数2:方法参数pageable 参数3:一共多少条数据
                //                      参数4:     参数5:                 参数6:
                return new AggregatedPageImpl(list, pageable,
                        hits.getTotalHits(), response.getAggregations(), response.getScrollId(), hits.getMaxScore());
            }
        });
    }
}

image-20220726160645620

image-20220726160807283

image-20220726161015082

触发事件(生产者)

用异步的方式去向ES服务器当中同步数据,我们是在发布帖子增加评论这两个地方同步数据(删除帖子目前还没做),我们在这两个点触发一个发帖事件

发帖事件先定义一个常量

CommunityConstant

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

image-20220726161100517

发布帖子时:

DiscussPostController

image-20220726161242572

image-20220726161323865

发布评论

CommentController

评论帖子以后帖子的评论数量,帖子就变了,这个时候需要触发一次事件把ES里的数据覆盖掉,其实是一个修改的行为。

image-20220726161500397

接下来我们需要去做的事情就是去消费这个事件

EventConsumer

消费者

image-20220726163037105

image-20220726163132084

表现层

最后就是展现,当我发一个帖子,这个帖子能够同步到ES服务器里,那就能搜到它,下面我们做的就是展现

新建一个 SearchController

@Controller
public class SearchController implements CommunityConstant {

    @Autowired
    private ElasticsearchService elasticsearchService;      // 用于查询

    @Autowired
    private UserService userService;                        // 搜到帖子以后还要展现作者

    @Autowired
    private LikeService likeService;                        // 搜到帖子以后还要展现帖子点赞的数量

    // search?keyword=xxx
    @RequestMapping(path = "/search", method = RequestMethod.GET)
    //                                  这里的Page是我们自己写的实体类
    //  参数1:搜索的关键字    参数2:传入分页的条件(我们封装的Page接收)   参数3:用于向模板传数据
    public String search(String keyword, Page page, Model model) {
        // 搜索帖子
        // 因为和实体类冲突了,所以会自动带上包名,泛型里写DiscussPost
        org.springframework.data.domain.Page<DiscussPost> searchResult =
                // 参数1:关键词          参数2:当前是第几页(方法要求从0开始)        参数3:每页显示多少条
                elasticsearchService.searchDiscussPost(keyword, page.getCurrent() - 1, page.getLimit());
        // 聚合数据(用户名、帖子点赞数量)
        List<Map<String, Object>> discussPosts = new ArrayList<>();
        if (searchResult != null) {
            for (DiscussPost post : searchResult) {
                Map<String, Object> map = new HashMap<>();
                // 帖子
                map.put("post", post);
                // 作者
                map.put("user", userService.findUserById(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 == null ? 0 : (int) searchResult.getTotalElements()); // 总共多少条数据

        return "/site/search";
    }

}

image-20220726170106830

image-20220726170257553

最后就是处理 html 了,首先我们需要处理的是搜索框,我们可以处理 首页index.html搜索框,其他页面复用这个index.htmlheader就可以复用这个代码了,所以我们首先要处理index.html

image-20220726171549322

最后是 search.html 好显示搜索的结果

image-20220726175451653

image-20220726175550122

image-20220726175821041