搜索服务:
-
将帖子保存至Elasticsearch服务器。
-
从Elasticsearch服务器删除帖子。
-
从Elasticsearch服务器搜索帖子。
发布事件:
-
发布帖子时,将帖子异步的提交到Elasticsearch服务器。
-
增加评论时,将帖子异步的提交到Elasticsearch服务器。
-
在消费组件中增加一个方法,消费帖子发布事件。
显示结果:
在控制器中处理搜索请求,在HTML上显示搜索结果。
1.ElasticsearchService
import com.nowcoder.community.elasticsearch.DiscussPostRepository;
import com.nowcoder.community.entity.DiscussPost;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.SearchResultMapper;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.SearchQuery;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Service
public class ElasticsearchService {
@Autowired
private DiscussPostRepository discussPostRepository;
@Autowired
private ElasticsearchTemplate elasticsearchTemplate;
//添加
public void saveDiscussPost(DiscussPost discussPost){
discussPostRepository.save(discussPost);
}
//删除
public void deleteDiscussPost(DiscussPost discussPost){
discussPostRepository.delete(discussPost);
}
//查询
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();
return elasticsearchTemplate.queryForPage(searchQuery, DiscussPost.class, new SearchResultMapper() {
@Override
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();
String id = hit.getSourceAsMap().get("id").toString();
post.setId(Integer.valueOf(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) {
post.setTitle(titleField.getFragments()[0].toString());
}
HighlightField contentField = hit.getHighlightFields().get("content");
if (contentField != null) {
post.setContent(contentField.getFragments()[0].toString());
}
list.add(post);
}
return new AggregatedPageImpl(list, pageable,
hits.getTotalHits(), response.getAggregations(), response.getScrollId(), hits.getMaxScore());
}
});
}
}
2.DiscussPostController
帖子发布成功后,触发发帖事件,向elasticsearch中添加帖子
import com.nowcoder.community.entity.*;
import com.nowcoder.community.event.EventProducer;
import com.nowcoder.community.service.CommentServiceImpl;
import com.nowcoder.community.service.DiscussPostServiceImpl;
import com.nowcoder.community.service.LikeService;
import com.nowcoder.community.service.UserServiceImpl;
import com.nowcoder.community.util.CommunityConstant;
import com.nowcoder.community.util.CommunityUtil;
import com.nowcoder.community.util.HostHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@Controller
@RequestMapping("/discuss")
public class DiscussPostController implements CommunityConstant {
@Autowired
private DiscussPostServiceImpl discussPostService;
@Autowired
private HostHolder hostHolder;
@Autowired
private UserServiceImpl userService;
@Autowired
private CommentServiceImpl commentService;
@Autowired
private LikeService likeService;
@Autowired
private EventProducer eventProducer;
@PostMapping("/add")
@ResponseBody
public String addDiscussPost(String title,String content){
User user = hostHolder.getUser();
if (user==null){
return CommunityUtil.getJSONString(403,"你还没有登陆");
}
DiscussPost discussPost = new DiscussPost();
discussPost.setUserId(user.getId());
discussPost.setTitle(title);
discussPost.setContent(content);
discussPost.setCreateTime(new Date());
discussPostService.addDiscussPost(discussPost);
//触发发帖事件
Event event = new Event()
.setTopic(TOPIC_PUBLISH)
.setEntityType(ENTITY_TYPE_POST)
.setEntityId(discussPost.getId())
.setUserId(user.getId());
eventProducer.fireEvent(event);
return CommunityUtil.getJSONString(0,"发布成功");
}
@GetMapping("/detail/{discussPostId}")
public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model, Page page){
//帖子
DiscussPost post = discussPostService.selectDiscussPostById(discussPostId);
model.addAttribute("post",post);
//作者
User user = userService.selectById(post.getUserId());
model.addAttribute("user",user);
//点赞数量
long likeCount = likeService.selectEntityLikeCount(CommunityConstant.ENTITY_TYPE_POST,discussPostId);
model.addAttribute("likeCount",likeCount);
//点赞状态
int likeStatus=hostHolder.getUser()==null ? 0 : likeService.selectEntityLikeStatus(hostHolder.getUser().getId(),CommunityConstant.ENTITY_TYPE_POST,discussPostId);
model.addAttribute("likeStatus",likeStatus);
//评论分页信息
page.setPath("/discuss/detail/"+discussPostId);
page.setLimit(5);
page.setRows(post.getCommentCount());
//评论:给帖子的评论
//回复:给评论的评论
//评论列表
List<Comment> commentList = commentService.selectCommentByEntity(ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit());
//评论VO列表
List<Map<String,Object>> commentVoList=new ArrayList<>();
if (commentList!=null){
for (Comment comment : commentList) {
//评论VO
Map<String,Object> commentVo=new HashMap<>();
//评论
commentVo.put("comment",comment);
//作者
commentVo.put("user",userService.selectById(comment.getUserId()));
//点赞数量
likeCount = likeService.selectEntityLikeCount(CommunityConstant.ENTITY_TYPE_COMMENT,comment.getId());
//点赞状态
likeStatus=hostHolder.getUser()==null ? 0 : likeService.selectEntityLikeStatus(hostHolder.getUser().getId(),CommunityConstant.ENTITY_TYPE_COMMENT,comment.getId());
commentVo.put("likeCount",likeCount);
commentVo.put("likeStatus",likeStatus);
//回复列表
List<Comment> replayList = commentService.selectCommentByEntity(ENTITY_TYPE_COMMENT, comment.getId(), 0, Integer.MAX_VALUE);
//回复VO列表
List<Map<String,Object>> replayVoList=new ArrayList<>();
if (replayList!=null){
for (Comment replay : replayList) {
Map<String,Object> replayVo=new HashMap<>();
//回复
replayVo.put("replay",replay);
//作者
replayVo.put("user",userService.selectById(replay.getUserId()));
//回复目标
User target=replay.getTargetId()==0?null:userService.selectById(replay.getTargetId());
replayVo.put("target",target);
//点赞数量
likeCount = likeService.selectEntityLikeCount(CommunityConstant.ENTITY_TYPE_COMMENT,replay.getId());
//点赞状态
likeStatus=hostHolder.getUser()==null ? 0 : likeService.selectEntityLikeStatus(hostHolder.getUser().getId(),CommunityConstant.ENTITY_TYPE_COMMENT,replay.getId());
replayVo.put("likeCount",likeCount);
replayVo.put("likeStatus",likeStatus);
replayVoList.add(replayVo);
}
}
commentVo.put("replays",replayVoList);
//回复数量
int replayCount = commentService.selectCountByEntity(ENTITY_TYPE_COMMENT, comment.getId());
commentVo.put("replayCount",replayCount);
commentVoList.add(commentVo);
}
}
model.addAttribute("comments",commentVoList);
return "/site/discuss-detail";
}
}
3.CommentController
在给帖子增加评论后,触发事件,存入Elasticsearch
import com.nowcoder.community.entity.Comment;
import com.nowcoder.community.entity.DiscussPost;
import com.nowcoder.community.entity.Event;
import com.nowcoder.community.event.EventProducer;
import com.nowcoder.community.service.CommentServiceImpl;
import com.nowcoder.community.service.DiscussPostServiceImpl;
import com.nowcoder.community.util.CommunityConstant;
import com.nowcoder.community.util.HostHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.Date;
@Controller
@RequestMapping("/comment")
public class CommentController implements CommunityConstant {
@Autowired
private CommentServiceImpl commentService;
@Autowired
private HostHolder hostHolder;
@Autowired
private EventProducer eventProducer;
@Autowired
private DiscussPostServiceImpl discussPostService;
@PostMapping("/add/{discussPostId}")
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 post = discussPostService.selectDiscussPostById(comment.getEntityId());
event.setEntityUserId(post.getUserId());
}else if (comment.getEntityType()==ENTITY_TYPE_COMMENT){
Comment comment1 = commentService.selectCommentById(comment.getEntityId());
event.setEntityUserId(comment1.getUserId());
}
eventProducer.fireEvent(event);
if (comment.getEntityType()==ENTITY_TYPE_POST){
//触发发帖事件
event = new Event()
.setTopic(TOPIC_PUBLISH)
.setEntityType(ENTITY_TYPE_POST)
.setEntityId(discussPostId)
.setUserId(hostHolder.getUser().getId());
eventProducer.fireEvent(event);
}
return "redirect:/discuss/detail/"+discussPostId;
}
}
4.EventConsumer
import com.alibaba.fastjson.JSONObject;
import com.nowcoder.community.entity.DiscussPost;
import com.nowcoder.community.entity.Event;
import com.nowcoder.community.entity.Message;
import com.nowcoder.community.service.DiscussPostServiceImpl;
import com.nowcoder.community.service.ElasticsearchService;
import com.nowcoder.community.service.MessageServiceImpl;
import com.nowcoder.community.util.CommunityConstant;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class EventConsumer implements CommunityConstant {
private static final Logger logger= LoggerFactory.getLogger(EventConsumer.class);
@Autowired
private MessageServiceImpl messageService;
@Autowired
private DiscussPostServiceImpl discussPostService;
@Autowired
private ElasticsearchService elasticsearchService;
@KafkaListener(topics = {TOPIC_COMMENT,TOPIC_FOLLOW,TOPIC_LIKE})
public void handleCommentMessage(ConsumerRecord record){
if (record==null||record.value()==null){
logger.error("消息的内容为空!");
return;
}
Event event= JSONObject.parseObject(record.value().toString(),Event.class);
if (event==null){
logger.error("消息格式错误!");
return;
}
//发送站内通知
Message message=new Message();
message.setFromId(SYSTEM_USER_ID);
message.setToId(event.getEntityUserId());
message.setConversationId(event.getTopic());
message.setCreateTime(new Date());
Map<String,Object> content=new HashMap<>();
content.put("userId",event.getUserId());
content.put("entityType",event.getEntityType());
content.put("entityId",event.getEntityId());
if (!event.getData().isEmpty()){
for (Map.Entry<String, Object> entry : event.getData().entrySet()) {
content.put(entry.getKey(), entry.getValue());
}
}
message.setContent(JSONObject.toJSONString(content));
messageService.addMessage(message);
}
//消费发帖事件
@KafkaListener(topics = {TOPIC_PUBLISH})
public void handlePublishMessage(ConsumerRecord record){
if (record==null||record.value()==null){
logger.error("消息的内容为空!");
}
Event event=JSONObject.parseObject(record.value().toString(),Event.class);
if (event==null){
logger.error("消息格式错误!");
return;
}
DiscussPost post = discussPostService.selectDiscussPostById(event.getEntityId());
elasticsearchService.saveDiscussPost(post);
}
}
5.SearchController
import com.nowcoder.community.entity.DiscussPost;
import com.nowcoder.community.entity.Page;
import com.nowcoder.community.service.ElasticsearchService;
import com.nowcoder.community.service.LikeService;
import com.nowcoder.community.service.UserServiceImpl;
import com.nowcoder.community.util.CommunityConstant;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Controller
public class SearchController implements CommunityConstant {
@Autowired
private ElasticsearchService elasticsearchService;
@Autowired
private UserServiceImpl userService;
@Autowired
private LikeService likeService;
@GetMapping("/search")
public String search(String keyword, Page page, Model model){
//搜索帖子
org.springframework.data.domain.Page<DiscussPost> searchResult=
elasticsearchService.searchDiscussPost(keyword, page.getCurrentPage()-1, page.getLimit());
//聚合数据
List<Map<String,Object>> discussPosts=new ArrayList<>();
if (discussPosts!=null){
for (DiscussPost post : searchResult) {
Map<String,Object> map=new HashMap<>();
//帖子
map.put("post",post);
//作者
map.put("user",userService.selectById(post.getUserId()));
//点赞数量
map.put("likeCount",likeService.selectEntityLikeCount(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";
}
}
5.index.html
<!-- 搜索 -->
<form class="form-inline my-2 my-lg-0" 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>
6.search.html
<!-- 帖子列表 -->
<ul class="list-unstyled mt-4" th:each="map:${discussPosts}">
<li class="media pb-3 pt-3 mb-3 border-bottom">
<img th:src="${map.user.headerUrl}" class="mr-4 rounded-circle" alt="用户头像" style="width: 50px;height: 50px;">
<div class="media-body">
<h6 class="mt-0 mb-3">
<a th:href="@{|/discuss/detail/${map.post.id}|}" th:utext="${map.post.title}">备战<em>春招</em>,面试刷题跟他复习,一个月全搞定!</a>
</h6>
<div class="mb-3" th:utext="${map.post.content}">
金三银四的金三已经到了,你还沉浸在过年的喜悦中吗? 如果是,那我要让你清醒一下了:目前大部分公司已经开启了内推,正式网申也将在3月份陆续开始,金三银四,<em>春招</em>的求职黄金时期已经来啦!!! 再不准备,作为19应届生的你可能就找不到工作了。。。作为20届实习生的你可能就找不到实习了。。。 现阶段时间紧,任务重,能做到短时间内快速提升的也就只有算法了, 那么算法要怎么复习?重点在哪里?常见笔试面试算法题型和解题思路以及最优代码是怎样的? 跟左程云老师学算法,不仅能解决以上所有问题,还能在短时间内得到最大程度的提升!!!
</div>
<div class="text-muted font-size-12">
<u class="mr-3" th:utext="${map.user.username}">寒江雪</u> 发布于 <b th:text="${#dates.format(map.post.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-15 15:32:18</b>
<ul class="d-inline float-right">
<li class="d-inline ml-2">赞 <i th:text="${map.likeCount}">11</i></li>
<li class="d-inline ml-2">|</li>
<li class="d-inline ml-2">回复 <i th:text="${map.post.commentCount}">7</i></li>
</ul>
</div>
</div>
</li>
</ul>