仿牛客网社区项目(三十)搜索服务
6.5 开发社区搜索功能
搜索服务
- 将帖子保存至Elasticsearch服务器。
- 对贴子实体类DiscussPost用注解进行相关配置
- 从Mybatis取数据存入
- 在dao层创建DiscussPostRepository类,继承ElasticsearchRepository接口即可,它集成了CRUD方法
- 从Elasticsearch服务器删除帖子。
- 从Elasticsearch服务器搜索帖子。
- Es可以在搜索到的词加标签,达到高亮显示
- 利用elasticTemplate.queryForPage()查询
发布事件
- 发布帖子时,将帖子异步的提交到Elasticsearch服务器。
- 新建ElasticsearchService类,定义CRUD和搜索方法。
- 在DiscussPostController类发帖时,定义和触发发帖事件(Event、eventProducer.fireEvent(event))
- 增加评论时,将帖子异步的提交到Elasticsearch服务器。
- 在CommentController类发表评论时,定义和触发发帖事件
- 在消费组件中增加一个方法,消费帖子发布事件。
- 在EventConsumer类增加消费发帖事件的方法
- 在事件中查询帖子,存到Es服务器
显示结果
- 在控制器中处理搜索请求,在HTML上显示搜索结果。
- 新建SearchController类处理搜索请求
- 此时为GET请求,keyword的传入(search?keyword=xxx)
- 修改index.html,表单提交路径,文本框name="keyword"
- 在search.html修改,遍历取到帖子。
1.开篇点题一个小bug
在discusspostmapper.xml中的insertDiscussPost 增加keyproperty=“id”就是自增id 我用的mybtisplus 都自带自增id,在实体类上注释一些就ok,
<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.先在Discusspostmapper中新增几个方法用于查询数据,然后由es使用,
int selectDiscussPostRows(@Param("userId") int userId);
List<DiscussPost> selectDiscussPosts(@Param("userId") int userId,@Param("offset") int offset,@Param("limit") int limit);
int insertDiscussPost(DiscussPost discussPost);
DiscussPost selectDiscussPostById(int id);
3.写对应的xml
<select id="selectDiscussPostRows" resultType="int">
select count(id)
from discuss_post
where status != 2
<if test="userId!=0">
and user_id = #{userId}
</if>
</select>
<select id="selectDiscussPostById" resultType="DiscussPost">
select <include refid="selectFields"></include>
from discuss_post
where id = #{id}
</select>
<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>
4.编写elasticsearchService 这里开始就不同了,我使用的是新版的ES,原先的都废除了
@Service
public class ElasticsearchService {
@Autowired
private DiscussPostRepository discussRepository;
@Autowired
private RestHighLevelClient restHighLevelClient;
public void saveDiscussPost(DiscussPost post) {
discussRepository.save(post);
}
public void deleteDiscussPost(int id) {
discussRepository.deleteById(id);
}
public SearchResult searcherDiscussPost(String keyword, int current, int limit) throws IOException {
SearchRequest searchRequest = new SearchRequest("discusspost");//discusspost是索引名,就是表名
//高亮
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("title");
highlightBuilder.field("content");
highlightBuilder.requireFieldMatch(false);
highlightBuilder.preTags("<span style='color:red'>");
highlightBuilder.postTags("</span>");
//构建搜索条件
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);
List<DiscussPost> list = new LinkedList<>();
long total = searchResponse.getHits().getTotalHits().value;
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);
}
return new SearchResult(list, total);
}
5.对静态变量处理一下
/**
* 主题: 发帖
*/
String TOPIC_PUBLISH = "publish";
6.对DiscusspostContoller的add做一些修改,流程是这样发布帖子-> 发送帖子id到队列-> 队列处理 放入es中
所以我们先把消息发到队列中,发出一个事件
@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);
//触发es事件
Event event = new Event()
.setTopic(TOPIC_PUBLISH)//这个名字就是routingKey 用的Topic实际上意义routingKey
.setUserId(user.getId())
.setEntityType(ENTITY_TYPE_POST)
.setEntityId(post.getId());
eventProduce.send(event);
return CommunityUtil.getJSONString(0, "发布成功!");
}
7.我们还没对队列处理 在ttlConfig中声明一下队列
@Configuration
public class TtlQueueConfig {
//交换机名字
public static final String X_EXCHANGE = "X";
//队列A 处理follow消息
public static final String QUEUE_A = "QA";
//队列B 处理Comment消息
public static final String QUEUE_B = "QB";
//队列C 处理like消息
public static final String QUEUE_C = "QC";
//队列D 处理publish消息
public static final String QUEUE_D = "QD";
//队列D 处理delet消息
public static final String QUEUE_E = "QE";
// 声明 xExchange
@Bean("xExchange")
public DirectExchange xExchange(){
return new DirectExchange(X_EXCHANGE);
}
//声明队列 A ttl 为 10s 并绑定到对应的死信交换机
@Bean("queueA")
public Queue queueA(){
// Map<String, Object> args = new HashMap<>(3);
// //声明当前队列绑定的死信交换机
// args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
// //声明当前队列的死信路由 key
// args.put("x-dead-letter-routing-key", "YD");
// //声明队列的 TTL
// args.put("x-message-ttl", 10000);
// return QueueBuilder.durable(QUEUE_A).withArguments(args).build();
return QueueBuilder.durable(QUEUE_A).build();
}
// 声明队列 A 绑定 X 交换机
@Bean
public Binding queueaBindingX(@Qualifier("queueA") Queue queueA,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueA).to(xExchange).with("follow");
}
//声明队列 B ttl 为 40s 并绑定到对应的死信交换机
@Bean("queueB")
public Queue queueB(){
// Map<String, Object> args = new HashMap<>(3);
// //声明当前队列绑定的死信交换机
// args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
// //声明当前队列的死信路由 key
// args.put("x-dead-letter-routing-key", "YD");
// //声明队列的 TTL
// args.put("x-message-ttl", 40000);
// return QueueBuilder.durable(QUEUE_B).withArguments(args).build();
return QueueBuilder.durable(QUEUE_B).build();
}
//声明队列 B 绑定 X 交换机
@Bean
public Binding queuebBindingX(@Qualifier("queueB") Queue queueB,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueB).to(xExchange).with("comment");
}
//生命队列
@Bean("queueC")
public Queue queueC(){
return QueueBuilder.durable(QUEUE_C).build();
}
//声明队列 B 绑定 X 交换机
@Bean
public Binding queuecBindingX(@Qualifier("queueC") Queue queueC,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueC).to(xExchange).with("like");
}
//声明队列 QD
@Bean("queueD")
public Queue queueD(){
return QueueBuilder.durable(QUEUE_D).build();
}
//声明队列 QD 绑定关系
@Bean
public Binding deadLetterBindingQD(@Qualifier("queueD") Queue queueD,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueD).to(xExchange).with("publish");
}
//声明队列 QD
@Bean("queueE")
public Queue queueE(){
return QueueBuilder.durable(QUEUE_E).build();
}
//声明队列 QD 绑定关系
@Bean
public Binding deadLetterBindingQE(@Qualifier("queueE") Queue queueD,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueD).to(xExchange).with("delete");
}
}
8.声明完之后 对队列的消费者做处理,也就是EventConsumer
//publish队列
@RabbitListener(queues = "QD")
public void receivePublish(String jsonString, Channel channel){
if( jsonString==null||jsonString.length()==0){
log.error("消息为空");
}
Event event = JSONObject.parseObject(jsonString, Event.class);
if (event==null){
log.error("类型转换失败");
return;
}
//查帖子 加到
DiscussPost post = discussPostService.getDiscussPostById(event.getEntityId());
elasticsearchService.saveDiscussPost(post);
}
9.对帖子的评论之后,触发一次添加帖子,目的是更新帖子的评论数量状况,我觉得是后来的热榜需要评论数量做标准,所以更新
@RequestMapping(path = "/add/{discussPostId}", method = RequestMethod.POST)
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 target = discussPostService.getDiscussPostById(comment.getEntityId());
event.setEntityUserId(target.getUserId());
} else if (comment.getEntityType() == ENTITY_TYPE_COMMENT) {
Comment target = commentService.findCommentById(comment.getEntityId());
event.setEntityUserId(target.getUserId());
}
eventProduce.send(event);
//先判断一下 是否为帖子的评论再去加到es
if(comment.getEntityType()==ENTITY_TYPE_POST){
event = new Event()
.setTopic(TOPIC_PUBLISH)
.setUserId(comment.getUserId())
.setEntityType(ENTITY_TYPE_POST)
.setEntityId(discussPostId);
eventProduce.send(event);
}
return "redirect:/discuss/detail/" + discussPostId;
}
10.创建elasticsearchController用于响应服务
@Controller
public class ElasticSearchController implements CommunityConstant {
private static final Logger LOGGER = LoggerFactory.getLogger(ElasticSearchController.class);
@Autowired
ElasticsearchService elasticSearchService;
@Autowired
UserService userService;
@Autowired
LikeService likeService;
@GetMapping("search")
public String search(String keyword, NewPage page, Model model) {
try {
//(page.getCurrent() - 1)*1每10个一页,第一次查询的10条可以跳过了,所以*10
//看一下对应的方法
SearchResult searchResult = elasticSearchService.searcherDiscussPost
(keyword, (page.getCurrent() - 1)*10, page.getLimit());
List<Map<String,Object>> discussPosts = new ArrayList<>();
List<DiscussPost> list = searchResult.getList();
if(list != null) {
for (DiscussPost post : list) {
Map<String,Object> map = new HashMap<>();
//帖子 和 作者
map.put("post",post);
map.put("user",userService.findUserById(Integer.valueOf(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.getTotal() == 0 ? 0 : (int) searchResult.getTotal());
} catch (IOException e) {
LOGGER.error("系统出错,没有数据:" + e.getMessage());
}
return "/site/search";
}
}
对静态资源处理 index页面有一个搜索框,处理一下其他都可以复用
<!-- 搜索 -->
<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页面 不想粘贴了浪费地方