当前位置: 首页 > news >正文

【Spring Cloud】新闻头条微服务项目:引入ElasticSearch建立文章搜索索引

8420b26844034fab91b6df661ae68671.png

个人简介: 

> 📦个人主页:赵四司机
> 🏆学习方向:JAVA后端开发 
> ⏰往期文章:SpringBoot项目整合微信支付
> 🔔博主推荐网站:牛客网 刷题|面试|找工作神器
> 📣种一棵树最好的时间是十年前,其次是现在!
> 💖喜欢的话麻烦点点关注喔,你们的支持是我的最大动力。

前言:

最近在做一个基于SpringCloud+Springboot+Docker的新闻头条微服务项目,用的是黑马的教程,现在项目开发进入了尾声,我打算通过写文章的形式进行梳理一遍,并且会将梳理过程中发现的Bug进行修复,有需要改进的地方我也会继续做出改进。这一系列的文章我将会放入微服务项目专栏中,这个项目适合刚接触微服务的人作为练手项目,假如你对这个项目感兴趣你可以订阅我的专栏进行查看,需要资料可以私信我,当然要是能给我点个小小的关注就更好了,你们的支持是我最大的动力。

 如果你想要一个可以系统学习的网站,那么我推荐的是牛客网,个人感觉用着还是不错的,页面很整洁,而且内容也很全面,语法练习,算法题练习,面试知识汇总等等都有,论坛也很活跃,传送门链接:牛客刷题神器

目录

一:需求分析

二:技术选型

1.方案对比

2.ES简介

三:ES环境搭建

1.拉取镜像

2. 创建容器

3.配置中文分词器 ik

四:代码实现

1.实现思路

2.创建映射

3.数据初始化到索引库

4.搜索功能实现

5.测试


一:需求分析

        在App端,我们可以在首页的顶部搜索栏里输入关键字进行文章的搜索,而且对于搜索结果我们会对命中的标题进行高亮展示,对于标题没命中但是文章内容命中的文章我们也要将其展示出来,并且当用户点击搜索结果中某一条文章的时候能够实现页面跳转查看文章详情的功能。

二:技术选型

1.方案对比

        跟以往不一样的是,以前我用的比较多的是用SQL语句进行模糊查询,往往也能达到不错的效果。但是这只适用于数据量比较少的情况下,而且数据库模糊查询还有一个问题,举个例子,假如我要搜索“什么是消息中间件”,这时候正好有一篇文章中包含有“消息中间件的介绍”的文字内容,但是这时候使用SQL的模糊查询是查询不到这个结果的,这显然是一个很大的弊端。

        除此之外,当数据库中的文档数达到上万条时候,采用模糊查询就已经很慢了,要是数据达到企业级的话,这样的检索速度肯定是让人受不了的。因为采用模糊查询这时候数据库并不知道那些数据包含了这个关键词,只能一条数据一条数据进行查询,而且还需要进行字符串匹配,假如我搜索“Kafka”这一关键词,就算数据库中一万条数据只有一条是包含有这个词的采用模糊查询还是会检索数据库中的所有数据,这样做显然是很费时费力的。

        而采用ES(ElasticSearch)之后能很好解决这个问题,即使是TB级的数据也能在毫秒内返回结果。那么为什么ES能具有如此高的效率呢?

2.ES简介

        Elasticsearch是一个开源的高扩展的分布式全文检索引擎,它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理PB级别的数据。 我们知道Java是面向对象的,而Elasticsearch是面向文档的,也就是说文档是所有可搜索数据的最小单元。ES的文档就像MySql中的一条记录,只是ES的文档会被序列化成json格式,保存在Elasticsearch中。

         ES是基于倒排索引的,什么意思呢,举个例子,假如海量的文档中只有文档A、B、C、D包含有“Kafka”这个关键词,假如用户搜索“Kafka”,这时候ES就会立即返回A、B、C、D这四个文档,从而避免把时间浪费在检索其他文档上面。

        由于ES对中文的分词不太友好,因此需要自己配置一个中文分词器ik。

三:ES环境搭建

1.拉取镜像

docker pull elasticsearch:7.4.0

2. 创建容器

docker run -id --name elasticsearch -d --restart=always -p 9200:9200 -p 9300:9300 -v /usr/share/elasticsearch/plugins:/usr/share/elasticsearch/plugins -e "discovery.type=single-node" elasticsearch:7.4.0

3.配置中文分词器 ik

因为在创建elasticsearch容器的时候,映射了目录,所以可以在宿主机上进行配置ik中文分词器

在去选择ik分词器的时候,需要与elasticsearch的版本好对应上。

把资料中的elasticsearch-analysis-ik-7.4.0.zip上传到服务器上,放到对应目录(plugins)解压

#切换目录
cd /usr/share/elasticsearch/plugins
#新建目录
mkdir analysis-ik
cd analysis-ik
#root根目录中拷贝文件
mv elasticsearch-analysis-ik-7.4.0.zip /usr/share/elasticsearch/plugins/analysis-ik
#解压文件
cd /usr/share/elasticsearch/plugins/analysis-ik
unzip elasticsearch-analysis-ik-7.4.0.zip

四:代码实现

1.实现思路

        为了加快检索速度,搜索关键词时候不会到数据库中直接搜索,而是到ES索引库中进行检索,因此我们首先需要创建好 索引库,然后需要对以前上传的文章创建索引,当后续有新文章要上传时候,我们就采取实时创建索引的策略。当用户输入关键词进行搜索时候,服务器就会到ES中进行检索,假如检索结果中标题含有该关键词,则将该标题中该关键词高亮进行返回,若标题不包含关键词但是内容包含关键词也将结果返回,如若不然则说明没有检索结果,流程图见下图:

2.创建映射

使用apifox添加映射

   

映射内容:

{
    "mappings":{
        "properties":{
            "id":{
                "type":"long"
            },
            "publishTime":{
                "type":"date"
            },
            "layout":{
                "type":"integer"
            },
            "images":{
                "type":"keyword",
                "index": false
            },
            "staticUrl":{
                "type":"keyword",
                "index": false
            },
            "authorId": {
                "type": "long"
            },
            "authorName": {
                "type": "text"
            },
            "title":{
                "type":"text",
                "analyzer":"ik_smart"
            },
            "content":{
                "type":"text",
                "analyzer":"ik_smart"
            }
        }
    }
}

 最后两项表示对文章标题和内容创建索引,其他字段均用于展示使用。

3.数据初始化到索引库

①在tbug-headlines-test中创建新模块es-init

  

②相关配置

server:
  port: 9999
spring:
  application:
    name: es-article

  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/headlines_article?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
    username: root
    password: 43
# 设置Mapper接口所对应的XML文件位置,如果你在Mapper接口中有自定义方法,需要进行该配置
mybatis-plus:
  mapper-locations: classpath*:mapper/*.xml
  # 设置别名包扫描路径,通过该属性可以给包中的类注册别名
  type-aliases-package: com.my.model.article.pojos


#自定义elasticsearch连接配置
elasticsearch:
  host: 49.23.192
  port: 9200

③pojo及mapper类

pojo

package com.my.es.pojo;

import lombok.Data;
import java.util.Date;

@Data
public class SearchArticleVo {

    // 文章id
    private Long id;
    // 文章标题
    private String title;
    // 文章发布时间
    private Date publishTime;
    // 文章布局
    private Integer layout;
    // 封面
    private String images;
    // 作者id
    private Long authorId;
    // 作者名词
    private String authorName;
    //静态url
    private String staticUrl;
    //文章内容
    private String content;

}

mapper 

package com.my.es.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.my.es.pojo.SearchArticleVo;
import com.my.model.article.pojos.ApArticle;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface ApArticleMapper extends BaseMapper<ApArticle> {

    List<SearchArticleVo> loadArticleList();

}

XML 

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.my.es.mapper.ApArticleMapper">

    <resultMap id="resultMap" type="com.my.es.pojo.SearchArticleVo">
        <id column="id" property="id"/>
        <result column="title" property="title"/>
        <result column="author_id" property="authorId"/>
        <result column="author_name" property="authorName"/>
        <result column="layout" property="layout"/>
        <result column="images" property="images"/>
        <result column="publish_time" property="publishTime"/>
        <result column="static_url" property="staticUrl"/>
        <result column="content" property="content"/>
    </resultMap>
    <select id="loadArticleList" resultMap="resultMap">
        SELECT
            aa.*, aacon.content
        FROM
            `ap_article` aa,
            ap_article_config aac,
            ap_article_content aacon
        WHERE
            aa.id = aac.article_id
          AND aa.id = aacon.article_id
          AND aac.is_delete != 1
          AND aac.is_down != 1

    </select>

</mapper>

④批量导入

package com.my.es;

import com.alibaba.fastjson.JSON;
import com.my.es.mapper.ApArticleMapper;
import com.my.es.pojo.SearchArticleVo;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

@SpringBootTest
@RunWith(SpringRunner.class)
public class ApArticleTest {
    @Autowired
    private ApArticleMapper apArticleMapper;

    @Autowired
    private RestHighLevelClient restHighLevelClient;
    /**
     * 注意:数据量的导入,如果数据量过大,需要分页导入
     * @throws Exception
     */
    @Test
    public void init() throws Exception {
        //1.查询所有符合条件的文章数据
        List<SearchArticleVo> searchArticleVos = apArticleMapper.loadArticleList();

        //2.批量导入到es索引库

        BulkRequest bulkRequest = new BulkRequest("app_info_article");

        for (SearchArticleVo searchArticleVo : searchArticleVos) {

            IndexRequest indexRequest = new IndexRequest().id(searchArticleVo.getId().toString())
                    .source(JSON.toJSONString(searchArticleVo), XContentType.JSON);

            //批量添加数据
            bulkRequest.add(indexRequest);

        }
        restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);

    }

}

注意这里创建了一个全局索引名称app_info_article

4.搜索功能实现

①导入tbug-headlines-search模块

  

②在tbug-headlines-service中添加依赖

<!--elasticsearch-->
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>7.4.0</version>
</dependency>
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-client</artifactId>
    <version>7.4.0</version>
</dependency>
<dependency>
    <groupId>org.elasticsearch</groupId>
    <artifactId>elasticsearch</artifactId>
    <version>7.4.0</version>
</dependency>

③nacos配置

spring:
  autoconfigure:
    exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
elasticsearch:
  host: 49.234.52.192
  port: 9200

④搜索接口定义

package com.my.search.controller.v1;

import com.my.model.common.dtos.ResponseResult;
import com.my.model.search.dtos.UserSearchDto;
import com.my.search.service.ArticleSearchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;

@RestController
@RequestMapping("/api/v1/article/search")
public class ArticleSearchController {
    @Autowired
    private ArticleSearchService articleSearchService;

    @PostMapping("/search")
    public ResponseResult search(@RequestBody UserSearchDto dto) throws IOException {
        return articleSearchService.search(dto);
    }
}

⑤dto

package com.my.model.search.dtos;

import lombok.Data;

import java.util.Date;


@Data
public class UserSearchDto {

    /**
    * 搜索关键字
    */
    String searchWords;
    /**
    * 当前页
    */
    int pageNum;
    /**
    * 分页条数
    */
    int pageSize;
    /**
    * 最小时间
    */
    Date minBehotTime;

    public int getFromIndex(){
        if(this.pageNum<1)return 0;
        if(this.pageSize<1) this.pageSize = 10;
        return this.pageSize * (pageNum-1);
    }
}

⑥业务层实现

package com.my.search.service;


import com.my.model.common.dtos.ResponseResult;
import com.my.model.search.dtos.UserSearchDto;

import java.io.IOException;

public interface ArticleSearchService {

    /**
     ES文章分页搜索
     @return
     */
    ResponseResult search(UserSearchDto userSearchDto) throws IOException;
}
package com.my.search.service.serviceImpl;

import com.alibaba.fastjson.JSON;
import com.my.model.common.dtos.ResponseResult;
import com.my.model.common.enums.AppHttpCodeEnum;
import com.my.model.search.dtos.UserSearchDto;
import com.my.model.user.pojos.ApUser;
import com.my.search.service.ApUserSearchService;
import com.my.search.service.ArticleSearchService;
import com.my.utils.thread.AppThreadLocalUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Slf4j
@Service
public class ArticleSearchServiceImpl implements ArticleSearchService {
    @Autowired
    private RestHighLevelClient restHighLevelClient;
    @Autowired
    private ApUserSearchService apUserSearchService;

    /**
     * es文章分页检索
     *
     * @param dto
     * @return
     */
    @Override
    public ResponseResult search(UserSearchDto dto) throws IOException {

        // 1.检查参数
        if (dto == null || StringUtils.isBlank(dto.getSearchWords())) {
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }
        log.info("文章查询...");

        //异步调用保存历史记录
        ApUser user = AppThreadLocalUtils.getUser();
        //用户信息不为空并且为首页搜索才进行保存
        if(user != null && dto.getFromIndex() == 0) {
            apUserSearchService.insert(dto.getSearchWords(),user.getId());
        }

        // 2.设置查询条件
        SearchRequest searchRequest = new SearchRequest("app_info_article");
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

        // 布尔查询
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();

        // 关键字的分词之后查询
        QueryStringQueryBuilder queryStringQueryBuilder = QueryBuilders.queryStringQuery
                (dto.getSearchWords()).field("title").field("content").defaultOperator(Operator.OR);
        boolQueryBuilder.must(queryStringQueryBuilder);

        // 查询小于mindate的数据
        RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("publishTime")
                .lt(dto.getMinBehotTime().getTime());
        boolQueryBuilder.filter(rangeQueryBuilder);

        // 分页查询
        searchSourceBuilder.from(0);
        searchSourceBuilder.size(dto.getPageSize());

        // 按照发布时间倒序查询
        searchSourceBuilder.sort("publishTime", SortOrder.DESC);

        // 设置高亮  title
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        highlightBuilder.field("title");
        highlightBuilder.preTags("<font style='color: red; font-size: inherit;'>");
        highlightBuilder.postTags("</font>");
        searchSourceBuilder.highlighter(highlightBuilder);


        searchSourceBuilder.query(boolQueryBuilder);
        searchRequest.source(searchSourceBuilder);
        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);


        // 3.结果封装返回

        List<Map> list = new ArrayList<>();

        SearchHit[] hits = searchResponse.getHits().getHits();
        for (SearchHit hit : hits) {
            String json = hit.getSourceAsString();
            Map map = JSON.parseObject(json, Map.class);
            // 处理高亮
            if (hit.getHighlightFields() != null && hit.getHighlightFields().size() > 0) {
                Text[] titles = hit.getHighlightFields().get("title").getFragments();
                String title = StringUtils.join(titles);
                // 高亮标题
                map.put("h_title", title);
            } else {
                // 原始标题
                map.put("h_title", map.get("title"));
            }
            list.add(map);
        }

        return ResponseResult.okResult(list);
    }
}

⑦在app网关添加如下配置

        #搜索微服务
        - id: headlines-search
          uri: lb://headlines-search
          predicates:
            - Path=/search/**
          filters:
            - StripPrefix= 1

5.测试

        启动项目进行测试,至少要启动文章微服务,用户微服务,搜索微服务,app网关微服务,app前端工程,由于我的云服务器已经过期了,所以我就不进行展示了,代码是没问题的,有问题的可以随时私信我。

下篇预告:文章自动构建索引&搜索记录&关键词联想

 友情链接: 牛客网  刷题|面试|找工作神器

相关文章:

  • 深度学习之文本分类 ----FastText
  • ITE IT6604E/AX HDMI1.4 接收器
  • openCV实践项目:拖拽虚拟方块
  • Java线程通信的简介说明
  • 对JavaBean的特点写法与实战心得详解
  • 【手写算法实现】 之 朴素贝叶斯 Naive Bayes 篇
  • Vue.js核心技术解析与uni-app跨平台实战开发学习笔记 第12章 Vue3.X新特性解析 12.12 响应式系统工具集的使用
  • java服务器端开发-servlet:202、Servlet执行过程介绍:get请求与post请求、编码相关等
  • yara 分析器
  • 数据结构(三) -- 栈
  • 神策数据发布融媒行业版,驱动媒体深度融合转型
  • 解决安装GDAL库报错问题(Windos)
  • 数据逻辑校验机制
  • Linux关于jar包的基本操作
  • 用什么软件可以提高视频批量剪辑的效率
  • JS中 map, filter, some, every, forEach, for in, for of 用法总结
  • bootstrap创建登录注册页面
  • co模块的前端实现
  • Java,console输出实时的转向GUI textbox
  • Linux链接文件
  • nginx 负载服务器优化
  • OSS Web直传 (文件图片)
  • ReactNative开发常用的三方模块
  • Sass 快速入门教程
  • springboot_database项目介绍
  • Vue--数据传输
  • 从零开始的webpack生活-0x009:FilesLoader装载文件
  • 基于Vue2全家桶的移动端AppDEMO实现
  • 解决jsp引用其他项目时出现的 cannot be resolved to a type错误
  • 开年巨制!千人千面回放技术让你“看到”Flutter用户侧问题
  • 聊聊directory traversal attack
  • 前端设计模式
  • 入门到放弃node系列之Hello Word篇
  • 腾讯优测优分享 | Android碎片化问题小结——关于闪光灯的那些事儿
  • 学习ES6 变量的解构赋值
  • 源码之下无秘密 ── 做最好的 Netty 源码分析教程
  • 主流的CSS水平和垂直居中技术大全
  • C# - 为值类型重定义相等性
  • 好程序员大数据教程Hadoop全分布安装(非HA)
  • ​Kaggle X光肺炎检测比赛第二名方案解析 | CVPR 2020 Workshop
  • !!【OpenCV学习】计算两幅图像的重叠区域
  • #etcd#安装时出错
  • #pragma once与条件编译
  • #QT(TCP网络编程-服务端)
  • $(function(){})与(function($){....})(jQuery)的区别
  • (13)Latex:基于ΤΕΧ的自动排版系统——写论文必备
  • (C)一些题4
  • (Redis使用系列) Springboot 使用Redis+Session实现Session共享 ,简单的单点登录 五
  • (ZT)薛涌:谈贫说富
  • (第一天)包装对象、作用域、创建对象
  • (附源码)springboot青少年公共卫生教育平台 毕业设计 643214
  • (一)u-boot-nand.bin的下载
  • (译)计算距离、方位和更多经纬度之间的点
  • .Net 高效开发之不可错过的实用工具
  • .NetCore实践篇:分布式监控Zipkin持久化之殇