总结-9 Elasticsearch之帖子搜索

elasticsearch

安装好elasticsearch之后,我们可以通过postman访问9200端口查看一些信息,下面给出一些基本的命令

// 查看节点状态
localhost:9200/_cat/indices?v
// 向test索引中加入id为3的数据,在body中设置json格式数据
localhost:9200/test/_doc/3
// 删除test索引中id为1的数据
localhost:9200/test/_doc/1
// 在body中实现多条件查询
{
	"query":{
		"multi_match":{
			"query":"互联网",
			"fields":["title","content"]
		}
	}
}

配置

首先我们在pom.xml中引入相关依赖,但是每次引入都会报错,经过多次查询资料,发现是SSL证书的问题,具体可以参考博客

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-elasticsearch -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

然后我们需要在application.properties中配置节点名称和路径,在SpringBoot中我们使用的是9300端口

# ElasticSearchProperties
spring.data.elasticsearch.cluster-name=nowcoder
spring.data.elasticsearch.cluster-nodes=127.0.0.1:9300

这里要注意,elasticsearch和redis的网络部分都是基于netty实现的,因此我们要做一些配置避免发送冲突,具体就是在App启动的地方做一个配置(具体需要百度下。。。)

// 在构造器之前
@PostConstruct
public void init(){
	// 解决netty启动冲突问题
	// Netty4Utils.setAvailableProcessors()
	System.setProperty("es.set.netty.runtime.available.processors","false");
}

entity

在Springboot中我们使用elasticsearch,实现要对能搜索的实体进行配置,使用@Document将实体和索引(也就是数据库中的表)相联系,使用@Id设置索引中哪个字段为id,@Field来设置索引中的字段。
@Document中indexName表示索引名字,type表示分隔符,shards表示分片,replicas表示副本数量。
@Field中type表示数据类型,analyzer表示解析器,ik_max_word表示将一句话拆分成尽可能多的词语,ik_smart表示符合尝试的解析器

@Document(indexName = "discusspost",type = "_doc",shards = 6, replicas = 3)
public class DiscussPost {

    @Id
    private int id;

    @Field(type = FieldType.Integer)
    private int userId;

    // analyzer 解析器 对一句话尽可能拆分成多个词条 聪明的拆分。。
    @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
    private String title;

    @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
    private String content;

    @Field(type = FieldType.Integer)
    private int type;

    @Field(type = FieldType.Integer)
    private int status;

    @Field(type = FieldType.Date)
    private Date createTime;

    @Field(type = FieldType.Integer)
    private int commentCount;

    @Field(type = FieldType.Double)
    private double score;

dao

有了对实体中字段的注解,在dao层我们只要继承ElasticsearchRepository,并指明实体类型和ID类型即可

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

}

service

在service层,我们主要实现三个方法,分别是保存数据、删除数据和查询数据,其中保存和删除我们直接使用dao层的Repository即可,调用save和deleteById

public void saveDiscussPost(DiscussPost post){
    discussPostRepository.save(post);
}

public void deleteDiscussPost(int id){
    discussPostRepository.deleteById(id);
}

而搜索相对复杂一点。首先,我们要传入关键字、当前页和限制条数来构造搜索条件。

SearchQuery searchQuery = new NativeSearchQueryBuilder()
        // 构造多字段查询
        .withQuery(QueryBuilders.multiMatchQuery(keyword,"title","content"))
        // 设置排序字段为type,score,createTime 倒序
        .withSort(SortBuilders.fieldSort("type").order(SortOrder.DESC))
        .withSort(SortBuilders.fieldSort("score").order(SortOrder.DESC))
        .withSort(SortBuilders.fieldSort("createTime").order(SortOrder.DESC))
        // 设置分页 page表示当前页
        .withPageable(PageRequest.of(current,limit))
        // 设置高亮的前标签和后标签
        .withHighlightFields(
                new HighlightBuilder.Field("title").preTags("<em>").postTags("</em>"),
                new HighlightBuilder.Field("content").preTags("<em>").postTags("</em>")
        ).build();

有了搜索条件之后,我们就需要使用elasticsearchTemplate进行搜索了,通过使用queryForPage方法,queryForPage方法的返回类型是org.springframework.data.domain.Page<DiscussPost>,因此我们需要传入实体类.class和搜索条件,然后匿名实现一个SearchResultMapper()方法,用于对elasticsearchTemplate得到的结果进行相应的处理,然后再返回。
在匿名类中我们使用SearchResponse来接收elasticsearchTemplate返回的结果,在该方法中我们首先得到全部数据,如果数据不为空的话,我们遍历所有数据,对于每一条数据通过hit.getSourceAsMap().get(“xxx”)得到信息实例化一个post对象,然后得到高亮数据,通过hit.getHighlightFields().get(“title”)得到title的,content的也同理,然后如果高亮数据不为空,那么我们将添加了em标签的数据设置为post的title和content。最后我们使用一个AggregatedPageImpl的构造方法传入list和其他固定参数对结果进行返回。

// elasticsearchTemplate得到结果交给SearchResultMapper处理之后再返回
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.getTotalHits() <= 0){
            return null;
        }

        List<DiscussPost> list = new ArrayList<>();
        for (SearchHit hit : hits){
            DiscussPost post = new DiscussPost();
            // 得到每个被遍历数据的信息,然后设置post
            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));

            // 处理高亮结果
            // 得到title字段的高亮数据
            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.setTitle(contentField.getFragments()[0].toString());
            }
            list.add(post);
        }
        // 对返回数据进行初始化,传入list,其他都是固定的(目前)
        return new AggregatedPageImpl(list,pageable,
                hits.getTotalHits(), searchResponse.getAggregations(),
                searchResponse.getScrollId(), hits.getMaxScore());
    }
});

controller

在controller层我们首先处理发布帖子和修改帖子之后将帖子加入到elasticsearch服务器中的处理,这里我们使用消息队列来进行实现,同样的对于一个事件我们需要指定主题,触发事件的人和实体类型、实体id,然后使用eventProducer.fireEvent(event)发布事件。具体我们要在发布帖子和用户对帖子进行评论的时候加入事件触发。

// 触发发帖事件
Event event = new Event()
        .setTopic(TOPIC_PUBLISH)
        .setUserId(user.getId())
        .setEntityType(ENTITY_TYPE_POST)
        .setEntityId(discussPost.getId());
eventProducer.fireEvent(event);


if (comment.getEntityType() == ENTITY_TYPE_POST) {
    // 触发发帖事件
    event = new Event()
            .setTopic(TOPIC_PUBLISH)
            .setUserId(hostHolder.getUser().getId())
            .setEntityType(ENTITY_TYPE_POST)
            .setEntityId(discussPostId);
    eventProducer.fireEvent(event);
}

同时,对于该类事件,我们需要加入一个新的消费者进行消费。
同样地通过设置主题,判断消息内容和格式,然后调用dao加入数据即可。

// 消费发帖事件
@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);
}

最后就是对搜索结果进行处理的controller了
首先通过service得到搜索结果,然后同之间一样,根据帖子的作者id得到作者user实例,根据帖子id查询点赞数量,然后聚合到map中,再将map传入list中,最后将list传给model

@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";
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值