携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第30天,点击查看活动详情
6.3 开发社区搜索功能
业务层:
发布一个帖子的时候应该1.将帖子存到Elasticsearch服务器,2.删帖子我们也应该从Elasticsearch服务器删去(当然现在删帖的功能还没有实现,但是我们在开发搜索服务的时候先把从Elasticsearch服务器删除帖子的方法先准备好,以后呢可以直接调用),然后重点就是我们要在组件里提供搜索的服务去3.搜索帖子。
表现层:
-
发布帖子时,采用异步的方式将帖子提交到Elasticsearch服务器
-
增加评论时,帖子的评论数量就会发生变化,这个时候我们也将帖子异步地提交到Elasticsearch服务器,相当于这是修改帖子
-
异步的方式主要是为了提高性能,当发了帖子以后,只要把事件丢到消息队列里,我们就可以继续处理下一个类似的请求,不用等待,所以说异步可以并行的处理一些事情,这样比较好。既然是异步的话,我们在发布帖子时、增加评论时触发了这样一个事件,我们需要在消费者组件里加一个方法来消费这个事件
当把数据同步到了ES服务器以后,剩下的就是查询了,查询的时候我们要想显示出搜索结果,我们需要在controller里处理搜索请求,然后在对应的html里显示结果。
首先先解决一个之前遗留的小问题
然后正式开发刚才所述内容
事务层(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());
}
});
}
}
触发事件(生产者)
用异步的方式去向ES服务器当中同步数据,我们是在发布帖子和增加评论这两个地方同步数据(删除帖子目前还没做),我们在这两个点触发一个发帖事件,
发帖事件先定义一个常量
CommunityConstant:
/**
* 主题: 发帖
*/
String TOPIC_PUBLISH = "publish";
发布帖子时:
DiscussPostController
发布评论时
CommentController
评论帖子以后帖子的评论数量,帖子就变了,这个时候需要触发一次事件把ES里的数据覆盖掉,其实是一个修改的行为。
接下来我们需要去做的事情就是去消费这个事件
EventConsumer
消费者
表现层
最后就是展现,当我发一个帖子,这个帖子能够同步到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";
}
}
最后就是处理 html 了,首先我们需要处理的是搜索框,我们可以处理 首页index.html 的搜索框,其他页面复用这个index.html的header就可以复用这个代码了,所以我们首先要处理index.html
最后是 search.html 好显示搜索的结果