仿牛客网第六章

Posted WonderC

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了仿牛客网第六章相关的知识,希望对你有一定的参考价值。

一、Elasticsearch入门

  • Elasticsearch简介

    • 一个分布式的、Restful风格的搜索引擎。
    • 支持对各种类型的数据的检索。
    • 搜索速度快,可以提供实时的搜索服务。
    • 便于水平扩展,每秒可以处理PB级海量数据。
  • Elasticsearch术语

    • 索引、类型、文档、字段。
    • 集群、节点、分片、副本。

术语的解释

  • 索引:相当于数据库中的database 改版后作为table
  • 类型:相当于数据库中的table 不再使用
  • 文档:相当于数据库中的一行数据,数据结构为JSON
  • 字段:相当于数据库中的一列

Elasticsearch6.0以后开始逐步废除类型的概念,索引的含义中也包括了类型。

  • 集群:分布式部署,提高性能
  • 节点:集群中的每一台服务器
  • 分片:对一个索引的进一步划分存储,提高并发处理能力
  • 副本:对分片的备份,提高可用性
    image
    Elasticsearch相关链接:官网

Elasticsearch选择下载6.4.3版本和SpringBoot兼容

Elasticsearch配置

文件位置config/elasticsearch.yml

image配置环境变量

image

安装中文分词插件

github上找

image

image

解压到指定目录下

image

ik插件配置文件说明

IKAnalyzer.cfg 可以自己配置新词

imageimage

安装Postman模拟网页访问

Postman相关链接:官网

使用命令行操作Elasticsearch

1.启动ES—./bin/elasticsearch.bat

2.常用命令介绍

  • 查看节点健康状态
curl -X GET "localhost:9200/_cat/health?v"

这个命令需要下载curl相关包:参考链接

image

  • 查看节点具体信息
curl -X GET "localhost:9200/_cat/nodes?v"

image

  • 查看索引相关信息
curl -X GET "localhost:9200/_cat/indices?v"

新装的没有索引:

image

  • 创建索引
curl -X PUT "localhost:9200/test"  //test就是索引名字

image

查看状态显示yellow是因为没有分片(备份)。

image

  • 删除索引
curl -X DELETE "localhost:9200/test"  //test就是索引名字

image

使用Postman/RESTer访问ES

RESTer是火狐插件也很好用。

  • 查索引
    image
  • 建索引
    image
  • 删除索引
    image
  • 提交数据
//test:索引  _doc:固定格式  1:id号 然后在请求body中写数据
PUT localhost:9200/test/_doc/1   

image

  • 查数据
GET localhost:9200/test/_doc/1

image

  • 改数据

和添加数据一样,底层会先删除再添加

  • 删除数据
DELETE localhost:9200/test/_doc/1

image

  • 搜索功能的演示

    • 先插入一些数据
      imageimage
      image
  • 开始搜索演示
    image

搜索时会先分词然后搜索,并不一定完全匹配

image

二、Spring整合Elasticsearch

  • 引入依赖

    • spring-boot-starter-data-elasticsearch
  • 配置Elasticsearch

    • cluster-name、cluster-nodes
  • Spring Data Elasticsearch

    • ElasticsearchTemplate
    • ElasticsearchRepository
      image

导包

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-elasticsearch</artifactId>
</dependency>

配置

1.application.properties

spring:
  data:
    elasticsearch:
      cluster-name: nowcoder
      cluster-nodes: 127.0.0.1:9300

2.解决Netty冲突问题

问题原因:Redis底层使用了Netty,Elasticsearch也用了Netty,当被注册两次就会报错

image

解决思路:Elasticsearch中注册Netty前会判断有无一个参数,如果有就不注册

image这么解决:

image

3.给discussPost加注解

//indexName:索引名,type:固定_doc,shards:分片,replicas:备份
@Document(indexName = "discusspost",type = "_doc",shards =6 ,replicas = 2)
public class DiscussPost {
    @Id
    private int id;
    @Field(type = FieldType.Integer)
    private int userId;
    //analyzer:互联网校招--->建立最大的索引(就是各种拆分)
    //searchAnalyzer 拆分尽可能少的满足意图的分词器
    @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)
    //0-普通; 1-置顶;
    private int type;
    @Field(type = FieldType.Integer)
    //0-正常; 1-精华; 2-拉黑;
    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下建立子包elasticsearch,并创建DiscussPostRepository接口

image

测试一波

Elasticsearch中配置再加一个这个:

image

elasticsearch包不要放在dao包下边,会导致bean注入失败。

@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class ElasticsearchTest {
    @Autowired
    private DiscussPostMapper discussMapper;
    @Autowired
    private DiscussPostRepository discussRepository;
    //有些功能上边的解决不了,所以引入下边的
    @Autowired
    private ElasticsearchTemplate elasticTemplate;
    @Test
    public void testInsert() {
        discussRepository.save(discussMapper.selectDiscussPostById(271));
        discussRepository.save(discussMapper.selectDiscussPostById(272));
        discussRepository.save(discussMapper.selectDiscussPostById(273));
    }
    @Test
    public void testInsertList() {
        discussRepository.saveAll(discussMapper.selectDiscussPosts(101, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(102, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(103, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(111, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(112, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(131, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(132, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(133, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(134, 0, 100));
    }
    //localhost:9200/discusspost/_doc/231
    @Test
    public void testUpdate() {
        DiscussPost post = discussMapper.selectDiscussPostById(231);
        post.setContent("我是新人,使劲灌水.");
        discussRepository.save(post);
    }
    @Test
    public void testDelete() {
        discussRepository.deleteById(231);
        //discussRepository.deleteAll();
    }
    @Test
    public void testSearchByRepository() {
        SearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.multiMatchQuery("互联网寒冬", "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(0, 10)) //分页
                .withHighlightFields(
                        new HighlightBuilder.Field("title").preTags("<em>").postTags("</em>"),
                        new HighlightBuilder.Field("content").preTags("<em>").postTags("</em>")
                ).build();
        // 底层调用:elasticTemplate.queryForPage(searchQuery, class, SearchResultMapper)
        // 底层获取得到了高亮显示的值, 但是没有返回.所以为了得到高亮显示直接用elasticTemplate.queryForPage见下面
        Page<DiscussPost> page = discussRepository.search(searchQuery);
        System.out.println(page.getTotalElements());
        System.out.println(page.getTotalPages());
        System.out.println(page.getNumber());
        System.out.println(page.getSize());
        for (DiscussPost post : page) {
            System.out.println(post);
        }
    }
    @Test
    public void testSearchByTemplate() {
        SearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.multiMatchQuery("互联网寒冬", "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(0, 10))
                .withHighlightFields(
                        new HighlightBuilder.Field("title").preTags("<em>").postTags("</em>"),
                        new HighlightBuilder.Field("content").preTags("<em>").postTags("</em>")
                ).build();
        Page<DiscussPost> page = elasticTemplate.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(); //long类型的字符串
                    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());
            }
            @Override
            public <T> T mapSearchHit(SearchHit searchHit, Class<T> aClass) {
                return null;
            }
        });
        System.out.println(page.getTotalElements());
        System.out.println(page.getTotalPages());
        System.out.println(page.getNumber());
        System.out.println(page.getSize());
        for (DiscussPost post : page) {
            System.out.println(post);
        }
    }
}

三、开发社区搜索功能

  • 搜索服务

    • 将帖子保存至Elasticsearch服务器。
    • 从Elasticsearch服务器删除帖子。
    • 从Elasticsearch服务器搜索帖子。
  • 发布事件

    • 发布帖子时,将帖子异步的提交到Elasticsearch服务器。
    • 增加评论时,将帖子异步的提交到Elasticsearch服务器。
    • 在消费组件中增加一个方法,消费帖子发布事件。
  • 显示结果

    • 在控制器中处理搜索请求,在html上显示搜索结果。

一个小问题的解决

image

ElasticsearchService

@Service
public class ElasticsearchService {
    @Autowired
    private DiscussPostRepository discussPostRepository;
    @Autowired
    private ElasticsearchTemplate elasticTemplate;//高亮显示
    //添加、修改
    public void saveDiscussPost(DiscussPost discussPost){
        discussPostRepository.save(discussPost);
    }
    //删除
    public void deleteDiscussPost(int id){
        discussPostRepository.deleteById(id);
    }
    //查询
    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 elasticTemplate.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(); //long类型的字符串
                    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());
            }
            @Override
            public <T> T mapSearchHit(SearchHit searchHit, Class<T> aClass) {
                return null;
            }
        });    
    }
}

处理DiscussPostController.addDiscussPost

image

处理CommentController.addComment

image

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());
        //往es中存数据
        elasticsearchService.saveDiscussPost(post);
    }
• 1
• 2
• 3
• 4
• 5
• 6
• 7
• 8
• 9
• 10
• 11
• 12
• 13
• 14
• 15
• 16
• 17

新写一个SearchController

@Controller
public class SearchController implements CommunityContant {
    @Autowired
    private ElasticsearchService elasticsearchService;
    @Autowired
    private UserService userService;
    @Autowired
    private LikeService likeService;
    @RequestMapping(path="/search",method = RequestMethod.GET)
    //路径 search?keyword=xxx
    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";
    }
}

处理页面index处理搜索框

image

处理页面search

image

突然发现之前的一个bug

MessageController.getNoticeList

先把message加入messageVo,不然模板判断显示不显示的时候会报null

image

以上是关于仿牛客网第六章的主要内容,如果未能解决你的问题,请参考以下文章

Java牛客项目课_仿牛客网讨论区_第三章

Java牛客项目课_仿牛客网讨论区_第二章

仿牛客网论坛项目

从零开始—仿牛客网讨论社区项目

仿牛客网第七章

仿牛客网项目总结