仿牛客社区——开发社区搜索功能

实现功能

• 搜索服务

- 将帖子保存至Elasticsearch服务器。

- 从Elasticsearch服务器删除帖子。

- 从Elasticsearch服务器搜索帖子。

• 发布事件

- 发布帖子时,将帖子异步的提交到Elasticsearch服务器。

- 增加评论时,将帖子异步的提交到Elasticsearch服务器。

- 在消费组件中增加一个方法,消费帖子发布事件。

• 显示结果

- 在控制器中处理搜索请求,在HTML上显示搜索结果

controller

@Controller
public class SearchController implements CommunityConstant {

	@Autowired
	private ElasticsearchService elasticsearchService;

	@Autowired
	private UserService userService;

	@Autowired
	private LikeService likeService;

	@RequestMapping(value = "/search",method = RequestMethod.GET)
	public String search(String keyword, Page page, Model model){
		//搜索帖子
		org.springframework.data.domain.Page<DiscussPost> searchResult =
				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";
	}
}

service

@Service
public class ElasticsearchService {
	@Autowired
	private ElasticsearchTemplate elasticsearchTemplate;

	@Autowired
	private DiscussPostRepository discussPostRepository;

	//当新发布(修改)一条帖子时,要将帖子数据传入到elasticsearch中
	public void saveDiscussPost(DiscussPost discussPost){
		discussPostRepository.save(discussPost);
	}

	//当删除一条帖子时,需要将elasticsearch中内容同步删除
	public void deleteDiscussPost(int id){
		discussPostRepository.deleteById(id);
	}

	//实现搜索业务
	public Page<DiscussPost> searchDiscussPost(String keyword,int current,int limit){

		SearchQuery searchQuery=new NativeSearchQueryBuilder()  //构造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))  //构造分页条件:PageRequest.of(0,10):当前页(从0开始 ,每页显示10条
				.withHighlightFields(
						//是否高亮显示:对搜索的关键词加标签
						new HighlightBuilder.Field("title").preTags("<em>").postTags("</em>"),
						new HighlightBuilder.Field("content").preTags("<em>").postTags("</em>")
				).build();  //返回SearchQuery接口实现类

		return elasticsearchTemplate.queryForPage(searchQuery, DiscussPost.class, new SearchResultMapper() {
			@Override
			public <T> AggregatedPage<T> mapResults(SearchResponse searchResponse, Class<T> aClass, Pageable pageable) {
				//处理
				SearchHits hits = searchResponse.getHits(); //获取搜索的结果数据
				if(hits.totalHits<0){
					return null;  //未获取到结果直接返回
				}

				//将获取到的结果装进list中
				List<DiscussPost> list = new ArrayList<>();
				//遍历命中数据
				for(SearchHit hit:hits){
					//将命中数据吧包装到实体类中
					DiscussPost post=new DiscussPost();
					//数据形式是以json形式存在的,并将json数据封装为了map格式
					//因此将数据以map形式取出--->转为string再存入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();//原始的title
					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.totalHits
						,searchResponse.getAggregations(),searchResponse.getScrollId(),hits.getMaxScore());
			}
		});
	}

}

dao

@Repository
public interface DiscussPostRepository extends ElasticsearchRepository<DiscussPost,Integer> {
}

producer

发送帖子后,需要将帖子同步到elasticsearch中

@RequestMapping(value = "/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);

		//发布帖子后,同步到elasticsearch中
		//利用事件进行发送
		Event event=new Event()
				.setTopic(TOPIC_PUBLISH)
				.setUserId(user.getId())
				.setEntityType(ENTITY_TYPE_POST)
				.setEntityId(post.getId());

		eventProducer.fireEvent(event);

		return CommunityUtil.getJsonString(0,"发布成功!");
	}

添加评论后,需要更新elasticsearch中数据

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

		//发送评论后发送通知通知对方
		Event event=new Event()
				.setTopic(TOPIC_COMMENT)
				.setUserId(hostHolder.getUser().getId())
				.setEntityType(comment.getEntityType())
				.setEntityId(comment.getEntityId())
				.setData("postId",discussPostId); //最终发送的消息还有点击查看,要链接到帖子详情页面,需要知道帖子id

		//帖子的作者:评论的是帖子帖子--》帖子表;  评论的是评论---》评论表
		if(comment.getEntityType()==ENTITY_TYPE_POST){
			DiscussPost target = discussPostService.findDiscussPostById(comment.getEntityId());
			event.setEntityUserId(target.getUserId());
		} else if (comment.getEntityType()==ENTITY_TYPE_COMMENT) {
			Comment target = commentService.findCommentById(comment.getEntityId());
			event.setEntityUserId(target.getUserId());
		}

		//生产者发送事件
		eventProducer.fireEvent(event);

		//触发发帖事件
		if(comment.getEntityType()==ENTITY_TYPE_POST){

				event=new Event()
					.setTopic(TOPIC_PUBLISH)
					.setUserId(comment.getUserId())
					.setEntityType(ENTITY_TYPE_POST)
					.setEntityId(discussPostId);

				eventProducer.fireEvent(event);
		}


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

	}

consumer

	//消费者消费发帖事件--->同步到elasticsearch中
	@KafkaListener(topics = TOPIC_PUBLISH)
	public void handleDiscussPost(ConsumerRecord record){
		//先进行判断record是否为空:未发事件或者发送的事件为空
		if(record==null|| record.value()==null){
			logger.error("发送的消息为空!");
			return;
		}

		//事件不为空:将事件转换为Event对象
		Event event= JSONObject.parseObject(record.value().toString(),Event.class);
		//判断对象是否为空
		if(event==null){
			logger.error("消息格式错误!");
			return;
		}
		//从事件中获取帖子id
		DiscussPost post = discussPostService.findDiscussPostById(event.getEntityId());
		//将查询到的帖子同步到elasticsearch中
		elasticsearchService.saveDiscussPost(post);
	}

application.properties

#配置ElasticsearchProperties
spring.data.elasticsearch.cluster-name=(自己的cluster-name)
spring.data.elasticsearch.cluster-nodes=自己的服务器ip:9300
spring.elasticsearch.rest.uris=自己的服务器ip:9200
spring.elasticsearch.rest.username=运行elasticsearch的用户
spring.elasticsearch.rest.password=用户密码
spring.data.elasticsearch.repositories.enabled=true
#192.168.10.100:9200 (http端口
#192.168.10.100:9300 (tcp端口

html

<!-- 帖子列表 -->
				<ul class="list-unstyled mt-4">
					<li class="media pb-3 pt-3 mb-3 border-bottom" th:each="map:${discussPosts}">
						<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>

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值