开发社区搜索功能
1.dao层
discusspost-mapper.xml中的insertDiscussPost这一行添加keyProperty属性,插入数据Mybatis会将生成的主键放入实体类中。
<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.service层
添加ElasticsearchService类,EventConsumer类中添加方法。
package com.gerrard.community.service;
import com.alibaba.fastjson.JSONObject;
import com.gerrard.community.dao.elasticsearch.DiscussPostRepository;
import com.gerrard.community.entity.DiscussPost;
import org.apache.lucene.search.TotalHits;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
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.beans.factory.annotation.Qualifier;
import org.springframework.data.domain.Page;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@Service
public class ElasticsearchService {
@Autowired
private DiscussPostRepository discussPostRepository;
// @Autowired
// private ElasticsearchTemplate elasticsearchTemplate;
@Qualifier("client")
@Autowired
private RestHighLevelClient restHighLevelClient;
public void saveDiscussPost(DiscussPost post){
discussPostRepository.save(post);
}
public void deleteDiscussPost(int id){
discussPostRepository.deleteById(id);
}
public Map<String,Object> searchDiscussPost(String keyword, int current, int limit) throws Exception{
Map<String, Object> result = new HashMap<>();
SearchRequest searchRequest = new SearchRequest("discusspost");//discusspost是索引名,就是表名
//高亮
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("title");
highlightBuilder.field("content");
highlightBuilder.requireFieldMatch(false);
highlightBuilder.preTags("<em>");
highlightBuilder.postTags("</em>");
//构建搜索条件
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);
//获取查询到的总个数
TotalHits totalHits=searchResponse.getHits().getTotalHits();
int count=(int)totalHits.value;
result.put("count",count);
List<DiscussPost> list = new LinkedList<>();
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);
}
result.put("data",list);
// return new AggregatedPageImpl()
return result;
}
}
3.event包
EventConsumer类:
//消费发帖事件
@KafkaListener(topics={TOPIC_PUBLISH})
public void handlePublishMessage(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;
}
DiscussPost post = discussPostService.findDiscussPostById(event.getEntityId());
elasticsearchService.saveDiscussPost(post);
}
4.controller层
(1)添加SearchController类,DiscussPostController类中对addDiscussPost方法进行重构。
package com.gerrard.community.controller;
import com.gerrard.community.entity.DiscussPost;
import com.gerrard.community.entity.Page;
import com.gerrard.community.service.ElasticsearchService;
import com.gerrard.community.service.LikeService;
import com.gerrard.community.service.UserService;
import com.gerrard.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.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
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 UserService userService;
@Autowired
private LikeService likeService;
// search?keyword=xxx
@RequestMapping(path = "/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";
}
}
用户在页面输入查询关键词,点击搜索按钮时,转到控制器的search方法,在Es中查询帖子,再做数据增强,设置分页信息,并返回search页面。
(2)DiscussPostController类:
@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);
// 触发发帖事件
Event event = new Event()
.setTopic(TOPIC_PUBLISH)
.setUserId(user.getId())
.setEntityType(ENTITY_TYPE_POST)
.setEntityId(post.getId()); //这里keyProperty="id"生效
eventProducer.fireEvent(event);
// 报错的情况,将来统一处理.
return CommunityUtil.getJSONString(0, "发布成功!");
}
添加帖子时,同时触发发帖事件,消费者消费发帖事件,将此帖子保存在Es中。
keyProperty="id"生效:将post实体类插入MySQL数据库后,要获取post实体类的id信息,设置发帖事件的entityId。keyProperty="id"生效后,MyBatis会将插入到MySQL数据库中生成的id返回至DiscussPost实体。
5.view层
修改index.html,链到search.html:
<!-- 搜索 -->
<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:
修改重点: