仿牛客網第六章


一、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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM