天天看點

第五章 建構分布式搜尋引擎

第五章 建構分布式搜尋引擎

Elasticsearch 簡介

  • 一個分布式的、Restful風格的搜尋引擎
  • 支援對各種類型的資料的檢索
  • 搜尋速度快,可以提供實時的搜尋服務
  • 便于水準擴充,每秒可以處理PB級海量資料
Elasticsearch 術語
  • 索引、類型、文檔、字段
  • ​索引​

    ​對應資料庫的一張表
  • ​文檔​

    ​對應資料庫的一張表裡面的一條資料
  • ​字段​

    ​對應資料庫的一張表裡面的一個字段
  • 類型逐漸地在es中進行廢棄
  • 叢集、節點、分片、副本

中文分詞插件

​​https://github.com/medcl/elasticsearch-analysis-ik​​

測試es

查詢es中一共有多少個索引
(GET)localhost:9200/_cat/indices?v      
第五章 建構分布式搜尋引擎
建立一條索引
第五章 建構分布式搜尋引擎
删除一條索引
第五章 建構分布式搜尋引擎
往es裡面插入一條資料
  • 送出資料,用​

    ​PUT​

    ​請求
  • test表示索引,相當于資料庫中的表名
  • _doc表示類型,在6.0版本已經逐漸被廢棄,新版本中完全廢棄,這裡使用的是6.0版本的es,是以使用​

    ​_doc​

    ​充當占位符
  • 1表示id,相當于資料庫中的id字段
第五章 建構分布式搜尋引擎
查詢資料
第五章 建構分布式搜尋引擎
删除一條資料
第五章 建構分布式搜尋引擎
分詞搜尋
  • 搜尋指定索引下的全部資料
第五章 建構分布式搜尋引擎
  • 搜尋test索引下title包含網際網路的資料,注意預設格式都是​

    ​_search?q=​

第五章 建構分布式搜尋引擎

Spring整合Elasticsearch

導入相關依賴

<!-- elasticsearch依賴 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>      

配置es

spring:
# 配置Elasticsearch
  data:
    elasticsearch:
      cluster-name: nowcoder
      # es有兩個端口:9200用于http連接配接,9300用于spring連接配接
      cluster-nodes: localhost:9300      

配置實體類

// indexName表示存儲在es中的索引名字
@Document(indexName = "discusspost", shards = 6, replicas = 3)
public class DiscussPost {

    /**
     * 文章id
     */
    @Id
    private int id;

    /**
     * 建立文章的使用者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;

    /**
     * 文章類型,0表示普通,1表示置頂
     */
    @Field(type = FieldType.Integer)
    private int type;

    /**
     * 文章狀态,0表示正常,1表示精華,2表示拉黑
     */
    @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;

}      

資料層

/**
 * ElasticsearchRepository<DiscussPost, Integer>:
 * 第一個參數表示該接口需要處理的實體類是誰
 * 第二個參數表示該實體類的主鍵的類型
 *
 * @author xiexu
 * @create 2022-07-10 18:07
 */
@Repository
public interface DiscussPostRepository extends ElasticsearchRepository<DiscussPost, Integer> {

}      

開發社群搜尋功能

搜尋服務
  • 将文章儲存至Elasticsearch伺服器。
  • 從Elasticsearch伺服器删除文章。
  • 從Elasticsearch伺服器搜尋文章。
釋出事件
  • 釋出文章時,将文章異步的送出到Elasticsearch伺服器。
  • 增加評論時,将文章異步的送出到Elasticsearch伺服器。
  • 在消費元件中增加一個方法,消費文章釋出事件。
顯示結果
  • 在控制器中處理搜尋請求,在HTML上顯示搜尋結果。
第五章 建構分布式搜尋引擎

業務層

@Service
public class ElasticsearchService {

    @Autowired
    private DiscussPostRepository discussRepository;

    @Autowired
    private ElasticsearchTemplate elasticTemplate;

    /**
     * 将文章資訊儲存到es
     *
     * @param post
     */
    public void saveDiscussPost(DiscussPost post) {
        discussRepository.save(post);
    }

    /**
     * 在es中删除對應id的文章
     *
     * @param id
     */
    public void deleteDiscussPost(int id) {
        discussRepository.deleteById(id);
    }

    /**
     * 根據關鍵字搜尋文章
     *
     * @param keyword 關鍵字
     * @param current 目前顯示第幾頁,從0開始
     * @param limit   每頁顯示多少條資料
     * @return
     */
    public Page<DiscussPost> searchDiscussPost(String keyword, int current, int limit) {
        SearchQuery searchQuery = new NativeSearchQueryBuilder()
                // 隻在title和content字段進行搜尋
                .withQuery(QueryBuilders.multiMatchQuery(keyword, "title", "content"))
                // 優先根據type字段進行倒序排序
                .withSort(SortBuilders.fieldSort("type").order(SortOrder.DESC))
                // 然後根據score字段進行倒序排序
                .withSort(SortBuilders.fieldSort("score").order(SortOrder.DESC))
                // 最後根據createTime字段進行倒序排序
                .withSort(SortBuilders.fieldSort("createTime").order(SortOrder.DESC))
                // 從第幾頁開始,每頁顯示多少條資料
                .withPageable(PageRequest.of(current, limit))
                // 高亮顯示字段
                .withHighlightFields(
                        // 對title字段進行高亮顯示,在前端頁面顯示<em>title資訊</em>
                        new HighlightBuilder.Field("title").preTags("<em>").postTags("</em>"),
                        // 對content字段進行高亮顯示,在前端頁面顯示<em>content資訊</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();
                // 如果沒查到資料就直接傳回null
                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));

                    // 這是原始的title,并不是高亮顯示的title,高亮顯示的下面單獨處理
                    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());
                    }

                    // 擷取與content有關的高亮顯示的内容
                    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());
            }
        });

        return page;
    }

}      

控制層

/**
     * 釋出文章
     *
     * @param title
     * @param content
     * @return
     */
    @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)
                // 事件的觸發人
                .setUserId(user.getId())
                // 事件發生的實體類型
                .setEntityType(ENTITY_TYPE_POST)
                // 實體id
                .setEntityId(post.getId());
        // 釋出事件
        eventProducer.fireEvent(event);

        // 報錯的情況以後統一處理
        return CommunityUtil.getJSONString(0, "釋出成功!");
    }      
// 添加評論
    @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())
                // 文章id
                .setData("postId", discussPostId);

        if (comment.getEntityType() == ENTITY_TYPE_POST) { // 如果評論的是文章
            DiscussPost target = discussPostService.findDiscusspostById(comment.getEntityId());
            event.setEntityUserId(target.getUserId());
            // 計算文章分數
            String redisKey = RedisKeyUtil.getPostScoreKey();
            redisTemplate.opsForSet().add(redisKey, discussPostId);
        } else if (comment.getEntityType() == ENTITY_TYPE_COMMENT) { // 評論的是評論
            Comment target = commentService.findCommentById(comment.getEntityId());
            event.setEntityUserId(target.getUserId());
        }

        // 釋出事件
        eventProducer.fireEvent(event);

        // 隻有評論的是文章,才觸發發帖事件
        if (comment.getEntityType() == ENTITY_TYPE_POST) {
            // 觸發發帖事件,把新釋出的文章存到es中
            event = new Event()
                    // 事件主題
                    .setTopic(TOPIC_PUBLISH)
                    // 事件的觸發人
                    .setUserId(comment.getUserId())
                    // 事件發生的實體類型
                    .setEntityType(ENTITY_TYPE_POST)
                    // 實體id
                    .setEntityId(discussPostId);
            // 釋出事件
            eventProducer.fireEvent(event);
        }

        // 重定向到文章詳情頁
        return "redirect:/discuss/detail/" + discussPostId;
    }      

消費事件類

// 消費者
@Component
public class EventConsumer implements CommunityConstant {

    private static final Logger logger = LoggerFactory.getLogger(EventConsumer.class);

    @Autowired
    private MessageService messageService;

    @Autowired
    private DiscussPostService discussPostService;

    @Autowired
    private ElasticsearchService elasticsearchService;

    // 消費發帖事件
    @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;
        }

        // 根據文章id查詢文章資訊
        DiscussPost post = discussPostService.findDiscusspostById(event.getEntityId());
        // 将文章資訊添加到es中
        elasticsearchService.saveDiscussPost(post);
    }

}      

控制層

@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);
                // 查詢搜尋到的資料對應的使用者資訊
                User user = userService.findUserById(post.getUserId());
                map.put("user", user);
                // 查詢搜尋到的資料對應的文章點贊數
                long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, post.getId());
                map.put("likeCount", likeCount);

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

}      

繼續閱讀