聚合搜索项目记录

  • Post author:
  • Post category:其他




JunSearch开发笔记

前端源码地址:https://github.com/z-h-u-a-i/junso-frontend

后端源码地址:https://github.com/z-h-u-a-i/junso-backend



前端开发

  • 前端实现页面防抖、用url记录页面搜索状态,当用户刷新页面时,能够从url还原之前的搜索状态

    • 核心技巧:用url来控制页面状态,当用户每点击一个选项或者搜索框中输入内容就把他体现在url上,然后再通过url的参数来控制页面状态

    • 步骤:

      • 1、配置路由

        • 导包:npm i vue-router

        • 创建router文件夹并编写index.js

          import { createRouter, createWebHashHistory} from "vue-router";
          import IndexPage from "../components/IndexPage.vue";
          import LoginPage from "../components/LoginPage"
          
          const routes = [
              {
                  path: "/",
                  component: IndexPage,
              },
              {
                  path: "/login",
                  component: LoginPage,
              },
          ];
          
          const router = createRouter({
              history: createWebHashHistory(),
              routes,
          });
          
          export default router;
          
        • 在main.js中应用这个router(会自动找index.js)

          import router from "@/router";
          
          createApp(App).use(Antd).use(router).mount("#app");
          
          
      • 2、设为动态路径

          {
            path: "/:category",
            component: IndexPage,
          },
        
      • 3、当搜索时在url的query参数上加上搜索条件

        const onSearch = () => {
          router.push({
            query: searchQuery.value,
          });
        };
        
      • 4、当url中的text发生变化后(即用户点击搜索时)回写到输入框中,这样当用户刷新的时候,这个值就不会丢失

        watchEffect(() => {
          searchQuery.value = {
            ...initSearchQuery,
            text: route.query.text as string,
          };
        });
        
      • 5、当切换选项时修改url的路径为当前选项的key值

        const onTabChange = (key: string) => {
          router.push({
            path: `/${key}`,
            query: searchQuery.value,
          });
        };
        
      • 6、每次获取activeKey都从url路径中获取

        let activeKey = computed(() => {
          return route.params.category;
        });
        
  • 前端整合Axios

    • 参考文档:https://www.axios-http.cn/

      import axios from "axios";
      
      const instance = axios.create({
        //访问前端项目的端口号,会自动代理到8102
        baseURL: "http://localhost:8080/api",
        timeout: 10000,
      });
      
      // 添加响应拦截器
      instance.interceptors.response.use(
        function (response) {
          // 2xx 范围内的状态码都会触发该函数。
          const data = response.data;
          if (data.code === 0) {
            return data.data;
          }
          console.log("request error");
          return data;
        },
        function (error) {
          // 超出 2xx 范围的状态码都会触发该函数。
          return error.message;
        }
      );
      
      export default instance;
      
      
    • 小坑:如果Controller方法中不是从body中获取一个封装所有需要属性的请求对象,请求头要设置成application/x-www-form-urlencoded后端才拿得到参数

      const instance = axios.create({
          baseURL: 'http://localhost:8080/api/',
          timeout: 10000,
          headers: {'Content-Type':'application/x-www-form-urlencoded'}
      });
      



后端开发

  • pojo文件夹结构再做更新

    在这里插入图片描述


    • entity

      就是数据库表对应的实体类

    • req

      就是请求参数的封装类

    • vo

      就是把

      entity



      userId

      啥的转为

      UserVo

      ,再带上一些需要展示上页面的数据,

      vo

      类中要有两个静态方法:

      vo



      obj



      obj



      vo
  • 查询接口小技巧:拼接

    QueryWapper

     @Override
        public QueryWrapper<Post> getQueryWrapper(PostQueryRequest postQueryRequest) {
            QueryWrapper<Post> queryWrapper = new QueryWrapper<>();
            if (postQueryRequest == null) {
                return queryWrapper;
            }
            String searchText = postQueryRequest.getSearchText();
            String sortField = postQueryRequest.getSortField();
            String sortOrder = postQueryRequest.getSortOrder();
            Long id = postQueryRequest.getId();
            String title = postQueryRequest.getTitle();
            String content = postQueryRequest.getContent();
            List<String> tagList = postQueryRequest.getTags();
            Long userId = postQueryRequest.getUserId();
            Long notId = postQueryRequest.getNotId();
            // 拼接查询条件
            if (StringUtils.isNotBlank(searchText)) {
                queryWrapper.like("title", searchText).or().like("content", searchText);
            }
            queryWrapper.like(StringUtils.isNotBlank(title), "title", title);
            queryWrapper.like(StringUtils.isNotBlank(content), "content", content);
            if (CollectionUtils.isNotEmpty(tagList)) {
                for (String tag : tagList) {
                    queryWrapper.like("tags", "\"" + tag + "\"");
                }
            }
            queryWrapper.ne(ObjectUtils.isNotEmpty(notId), "id", notId);
            queryWrapper.eq(ObjectUtils.isNotEmpty(id), "id", id);
            queryWrapper.eq(ObjectUtils.isNotEmpty(userId), "userId", userId);
            queryWrapper.eq("isDelete", false);
            queryWrapper.orderBy(SqlUtils.validSortField(sortField), sortOrder.equals(CommonConstant.SORT_ORDER_ASC),
                    sortField);
            return queryWrapper;
        }
    
  • 拼接

    QueryWrapper

    后的Controller

        @GetMapping("/list/page/vo")
        public BaseResponse<Page<PostVo>> listPostVoByPage (PostQueryRequest postQueryRequest){
            int current = postQueryRequest.getCurrent();
            long pageSize = postQueryRequest.getPageSize();
            Page<Post> page = postService.page(new Page<>(current, pageSize), postService.getQueryWrapper(postQueryRequest));
            Page<PostVo> postVoPage = new Page<>(current, pageSize, page.getTotal());
            postVoPage.setRecords(postService.getPostVO(page.getRecords()));
            return ResultUtils.success(postVoPage);
    
        }
    



数据抓取



从请求地址里获取

/**
 * 初始化帖子列表
 *
 * 实现了CommandLineRunner接口并注入到容器中后每次启动程序都会执行一次run方法
 */
//@Component
@Slf4j
public class FetchInitPostList implements CommandLineRunner {

    @Autowired
    PostService postService;

    @Override
    public void run(String... args) {
        String json = "{\"current\":1,\"pageSize\":8,\"sortField\":\"_score\",\"sortOrder\":\"descend\",\"searchText\":\"\",\"category\":\"文章\",\"reviewStatus\":1}";
        String url = "https://www.code-nav.cn/api/post/search/page/vo";
        String result = HttpRequest
                .post(url)
                .body(json) //body中传入json数据
                .execute()
                .body();//获取返回值中body里的内容
        Map<String, Object> map = JSONUtil.toBean(result, Map.class);
        JSONObject data = (JSONObject)map.get("data");
        JSONArray records = (JSONArray)data.get("records");

        List<Post> postList = new ArrayList<>();
        for (Object record : records) {
            JSONObject tempRecord = (JSONObject) record;
            Post post = new Post();
            post.setTitle(tempRecord.getStr("title"));
            post.setContent(tempRecord.getStr("content"));
            post.setTags(tempRecord.getStr("tags"));
            post.setUserid(1L);
            postList.add(post);
        }
        postService.saveBatch(postList);
    }
}



从页面结构中获取

  • 首先打开页面的检查模式定位到想要的数据,然后找到它的css选择器,根据css选择器拿到这个标签元素,然后再逐层获取到需要的数据

  • 在这里插入图片描述

  •         int current = 1;
            String url = "https://cn.bing.com/images/search?q=" + "小黑子" + "&first=" + current;
        	//获取url的整个页面
            Document doc = Jsoup.connect(url).get();
        	//通过css选择器定位获取使用了这个选择器的所有元素
            Elements elements = doc.select(".iuscp.isv");
            for (Element element : elements) {
                //也是通过css选择器定位获取元素.get(0)获取第一个元素,.attr("")获取属性名对应的值
                String attr = element.select(".inflnk").get(0).attr("aria-label");
                System.out.println(attr);
                String m = element.select(".iusc").get(0).attr("m");
            Map<String, Object> map = JSONUtil.toBean(m, Map.class);
            System.out.println(map.get("murl"));
        }
    



用到的设计模式



1.门面模式

  • 对某个功能只提供一个接口,通过传入的某个数据进行细分需要进行什么操作



2.适配器模式

  • 当调用一个API接口发现需要的跟我们自己提供的数据不一样时,就需要一个适配器来将我们的数据转为该API接口需要的数据



3.注册器模式(本质也是单例)

  • 通过一个map对象或其他类型存储好后面需要用的对象,如果需要添加就往map里面注册



Elastic Stack(一套技术栈)

官网:https://www.elastic.co/cn/

包含了数据的整合 => 提取 => 存储 => 使用,一整套!

  • beats:从各种不同类型的文件 / 应用来 采集数据 a,b,c,d,e,aa,bb,cc
  • Logstash:从多个采集器或数据源来抽取 / 转换数据,向 es 输送 aa,bb,cc
  • elasticsearch:存储、查询数据
  • kibana:可视化 es 的数据

下载 ES安装包

elasticsearch:https://www.elastic.co/guide/en/elasticsearch/reference/7.17/setup.html

kibana:https://www.elastic.co/guide/en/kibana/7.17/introduction.html

安装并启动的文档:

https://www.elastic.co/guide/en/elasticsearch/reference/7.17/zip-windows.html

  • 测试是否启动成功:

    • 访问:localhost:9200,看看是否返回es的相关信息

https://www.elastic.co/guide/en/kibana/7.17/windows.html

  • 测试是否启动成功:

    • 访问:localhost:5601

只要是一套技术,所有版本必须一致!!!此处用 7.17



ES的几种调用方式



1) restful api调用(http请求)

Get请求:http://localhost:9200

curl可以发送模拟请求:curl -X GET “localhost:9200/?pretty”

Es的启动端口

  1. 9200:给外部用户(给客户端调用)的端口
  2. 9300:给ES集群内部通信的(外部调用不了的)



2) kibana界面中的devtools



3) 客户端调用

​ go语言、java、python



ES 语法



1) DSL

  • json格式,和http请求兼容性最好(一般用这个)

  • mapping可以理解为mysql中的表结构,可以显式创建mapping,在插入数据的时候该mapping缺少某个字段也会动态改变,而不是报错

    //获取某个文档的mapping
    GET user/_mapping
    
    //显式设置mapping
    PUT user
    {
      "mappings": {
        "properties": {
          "age":    { "type": "integer" },  
          "email":  { "type": "keyword"  }, 
          "name":   { "type": "text"  }     
        }
      }
    }
    


插入数据(如果不存在这个表就建表,字段根据传入的数据制定):es里的文档就是mysql中的表

POST 文档名(表名)/_doc
{
  "title": "dog",
  "desc": "dog的描述"
}


查询数据

  • 查询某个文档的所有数据

    GET 文档名/_search
    {
      "query": {
        "match_all": { }
      },
      "sort": [
        {
          "@timestamp": "desc"
        }
      ]
    }
    
  • 根据文档中的某条数据的id查询数据

    GET 文档名/_doc/某条数据id
    


修改数据

  • POST 文档名/_doc/某条数据id
    {
      "title": "dog2",
      "desc": "dog的描述2"
    }
    


删除

  • DELETE 文档名
    



2) EQL

专门查询ECS文档(标准指标文档)的数据语法,更加规范,但只适用于特殊场景

  • https://www.elastic.co/guide/en/elasticsearch/reference/7.17/eql.html

  • POST my_event/_doc
    {
      "title": "鱼333333皮",
      "@timestamp": "2099-05-06T16:21:15.000Z",
      "event": {
        "original": "192.0.2.42 - - [06/May/2099:16:21:15 +0000] \"GET /images/bg.jpg HTTP/1.0\" 200 24736"
      }
    }
    
    
    GET my_event/_eql/search
    {
      "query": """
        any where 1 == 1
      """
    }
    



3) SQL

学习成本低,但是可能需要插件支持、性能较差

  • https://www.elastic.co/guide/en/elasticsearch/reference/7.17/sql-getting-started.html

  • POST /_sql?format=txt
    {
      "query": "SELECT * FROM post where title like '%鱼皮%'"
    }
    



分词器


内置分词器

:https://www.elastic.co/guide/en/elasticsearch/reference/7.17/analysis-analyzers.html

  • 空格分词器:whitespace,结果 The、quick、brown、fox.
POST _analyze
{
  "analyzer": "keyword",
  "text":     "The quick brown fox."
}
  • 标准分词规则,结果:is、this、deja、vu
POST _analyze
{
  "tokenizer": "standard",
  "filter":  [ "lowercase", "asciifolding" ],
  "text":      "Is this déja vu?",
}
    
  • 关键词分词器:就是不分词,整句话当作专业术语
POST _analyze
{
  "analyzer": "keyword",
  "text":     "The quick brown fox."
}
  • IK 分词器(ES 插件)

    中文友好:https://github.com/medcl/elasticsearch-analysis-ik

    下载地址:https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.17.7(注意版本一致)

思考:怎么样让 ik 按自己的想法分词?

回答:自定义词典

ik_smart 和 ik_max_word 的区别?举例:“小黑子”

ik_smart 是智能分词,尽量选择最像一个词的拆分方式,比如“小”、“黑子”

ik_max_word 尽可能地分词,可以包括组合词,比如 “小黑”、“黑子”

打分机制

有 3 条内容:

1、张三是狗

2、张三是小黑子

3、我是小黑子

用户搜索:

张三,第一条分数最高,因为第一条匹配了关键词,而且更短(匹配比例更大)

张三小黑子 => 张三、小、黑子 => 2 > 3 > 1

参考文章:https://liyupi.blog.csdn.net/article/details/119176943

官方参考文章:https://www.elastic.co/guide/en/elasticsearch/guide/master/controlling-relevance.html



应用到项目中



1、在Elastic Search中创建一个文档来对应数据库中的表

ES Mapping:

id(可以不放到字段设置里)

ES 中,尽量存放需要用户筛选(搜索)的数据

aliases:别名(为了后续方便数据迁移)

字段类型是 text,这个字段是可被分词的、可模糊查询的;而如果是 keyword,只能完全匹配、精确查询。

analyzer(存储时生效的分词器):用 ik_max_word,拆的更碎、索引更多,更有可能被搜出来

search_analyzer(查询时生效的分词器):用 ik_smart,更偏向于用户想搜的分词

如果想要让 text 类型的分词字段也支持精确查询,可以创建 keyword 类型的子字段

PUT post_v1
{
  "aliases": {
    "post": {}
  },
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "content": {
        "type": "text",
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "tags": {
        "type": "keyword"
      },
      "userId": {
        "type": "long"
      },
      "createTime": {
        "type": "date"
      },
      "updateTime": {
        "type": "date"
      },
      "isDelete": {
        "type": "integer"
      }
    }
  }
}



2、引入依赖(注意依赖版本是否与装的elastic search相匹配)

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



3、创建elastic search中文档对应的pojo类

  1. @id:指定该属性为数据的id,es会将这个id的值同步到es的_id中(es中,_ 开头的字段标识系统默认属性
@Document(indexName = "post")
@Data
public class PostEsDTO implements Serializable {

    private static final String DATE_TIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";

    /**
     * id
     */
    @Id
    private Long id;

    /**
     * 标题
     */
    private String title;

    /**
     * 内容
     */
    private String content;

    /**
     * 标签列表
     */
    private List<String> tags;

    /**
     * 创建用户 id
     */
    private Long userId;

    /**
     * 创建时间
     */
    @Field(index = false, store = true, type = FieldType.Date, format = {}, pattern = DATE_TIME_PATTERN)
    private Date createTime;

    /**
     * 更新时间
     */
    @Field(index = false, store = true, type = FieldType.Date, format = {}, pattern = DATE_TIME_PATTERN)
    private Date updateTime;

    /**
     * 是否删除
     */
    private Integer isDelete;

    private static final long serialVersionUID = 1L;

    /**
     * 对象转包装类
     *
     * @param post
     * @return
     */
    public static PostEsDTO objToDto(Post post) {
        if (post == null) {
            return null;
        }
        PostEsDTO postEsDTO = new PostEsDTO();
        BeanUtils.copyProperties(post, postEsDTO);
        String tagsStr = post.getTags();
        if (StringUtils.isNotBlank(tagsStr)) {
            postEsDTO.setTags(JSONUtil.toBean(tagsStr, List.class));
        }
        return postEsDTO;
    }

    /**
     * 包装类转对象
     *
     * @param postEsDTO
     * @return
     */
    public static Post dtoToObj(PostEsDTO postEsDTO) {
        if (postEsDTO == null) {
            return null;
        }
        Post post = new Post();
        BeanUtils.copyProperties(postEsDTO, post);
        List<String> tagList = postEsDTO.getTags();
        if (CollectionUtils.isNotEmpty(tagList)) {
            post.setTags(JSONUtil.toJsonStr(tagList));
        }
        return post;
    }
}
  1. 创建操作es文档的类

    1. 继承

      ElasticsearchRepository<pojo, ID>

      ,其中就会提供一些操作es的方法,也可以根据方法名自动生成操作数据的方法,继承了这个类以后这个类的对象会自动注入到

      IOC

      容器,不需要注解啥的,这个对象更适合做一些简单增删改查操作和定制化操作,返回结果精简

      public interface PostEsDao extends ElasticsearchRepository<PostEsDTO, Long> {
      	//只需要根据它的规则写方法名即可
          List<PostEsDTO> findByUserId(Long userId);
      }
      
      
    2. 通过直接引入

      ElasticsearchRestTemplate

      对象,这个对象里有很多复杂的操作,返回结果多、复杂

          @Resource
          private ElasticsearchRestTemplate elasticsearchRestTemplate;
      



4、实现mysql与es数据同步

需要实现两种同步:

全量同步

(首次启动时)、

增量同步

(有新数据时)


实现方式:

  1. 定时任务,比如1分钟1次,找到MySQL中过去几分钟内(至少是定时周期的2倍)发生改变的数据,然后更新到ES。

    优点:简单易懂、占用资源少、不用引入第三方中间件

    缺点:有时间差


    应用场景:数据短时间内不同步影响不大、或者数据几乎不发生修改

  2. 双写:写数据的时候,必须也去写ES;更新删除数据库同理。(事务:建议先保证MySQL写成功,如果ES写失败了,可以通过定时任务+日志+告警进行检测和修复(补偿))

  3. 用Logstash数据同步管道(一般要配合kafka消息队列+beats采集器):

  4. 订阅数据库流水的方式canal

    在这里插入图片描述



1、定时任务实现

全量同步:

/**
 * 全量同步的定时方法,只执行一次
 *
 * 实现了CommandLineRunner接口并注入到容器中后每次启动程序都会执行一次run方法
 */
@Component
@Slf4j
public class FullSyncPostToEs implements CommandLineRunner {

    @Resource
    private PostEsDao postEsDao;

    @Resource
    private PostService postService;

    @Override
    public void run(String... args) throws Exception {
        List<Post> postList = postService.list();
        if (CollectionUtil.isEmpty(postList)) {
            return;
        }

        if (CollectionUtil.isNotEmpty(postList)) {
            List<PostEsDTO> postEsDTOList = postList.stream().map(PostEsDTO::objToDto).collect(Collectors.toList());

            int total = postEsDTOList.size();
            log.info("FullSyncPostToEs start, total {}", total);
            postEsDao.saveAll(postEsDTOList);
            log.info("FullSyncPostToEs start, end {}", total);
        }
    }
}

增量同步:

/**
 * 增量同步的定时任务
 *
 * 先将类标注@EnableScheduling,然后在循环任务的方法上用 @Scheduled(fixedRate = 60 * 1000)控制它1分组执行一次
 */
@Component
@Slf4j
@EnableScheduling
public class IncSyncPostToEs {

    @Resource
    private PostEsDao postEsDao;

    @Resource
    private PostMapper postMapper;

    /**
     * 每分钟执行一次
     */
    @Scheduled(fixedRate = 60 * 1000)
    public void run() {
        //获取前五分钟更新的数据,将这个时间设置为执行周期的3-5倍,避免前面几次更新失败,造成数据丢失
        //es中如果数据插入时id一样会覆盖,所以不用担心冲突问题
        Date fiveMinutesAgoDate = new Date(new Date().getTime() - 5 * 60 * 1000L);
        List<Post> postList = postMapper.listPostByUpdateTime(fiveMinutesAgoDate);
        
        List<PostEsDTO> postEsDTOList = postList.stream().map(PostEsDTO::objToDto).collect(Collectors.toList());
        
        int total = postEsDTOList.size();
        log.info("IncSyncPostToEs start, total {}", total);
        postEsDao.saveAll(postEsDTOList);
        log.info("IncSyncPostToEs start, end {}", total);
    }
}



5、编写查询该es文档的DSL

https://www.elastic.co/guide/en/elasticsearch/reference/7.17/query-filter-context.html

https://www.elastic.co/guide/en/elasticsearch/reference/7.17/query-dsl-bool-query.html

示例:

GET post/_search
{
  "query": { 
    "bool": { // 组合条件
      "must": [ // 必须都满足
        { "match": { "title":   "鱼皮"        }}, // match 模糊查询
        { "match": { "content":   "知识星球"        }}
      ],
      "filter": [  //会筛选(跟must一样,必须都满足),但不参与最终评分
        { "term":  { "status": "published" }}, // term 精确查询
        { "range": { "publish_date": { "gte": "2015-01-01" }}} // range 范围查询
      ]
    }
  }
}

当前es文档的查询dsl:

GET post/_search
{
  "query": {
    "bool": {
      "should": [ //其中的几个符合即可,至于到底几个要根据下面的minimum_should_match设置的值来确定
        {
          "match": {
            "title": "张三哈哈哈哈"
          }
        },
        {
          "match": {
            "content": "傻逼"
          }
        }
      ],
      "filter": [
        {
          "term": {
            "isDelete": 0
          }
        },
        {
          "term": {
            "tags": "文章"
          }
        },
        {
          "term": {
            "tags": "Java"
          }
        }
      ],
      "minimum_should_match": 1 //should中至少有几个符合匹配条件
    }
  },
  "from": 0,
  "size": 5,
  "sort": [
    {
      "_score": {
        "order": "desc"
      }
    },
    {
      "updateTime": {
        "order": "desc"
      }
    }
  ]
}



6、根据查询DSL编写Java的查询es语句

    public Page<Post> getFromEs(PostQueryRequest postQueryRequest) {
        if (postQueryRequest == null) {
            return null;
        }
        String searchText = postQueryRequest.getSearchText();
        String title = postQueryRequest.getTitle();
        String content = postQueryRequest.getContent();
        List<String> tags = postQueryRequest.getTags(); //必须全部满足的标签
        List<String> orTags = postQueryRequest.getOrTags(); //只满足其中一个就行的标签
        String sortField = postQueryRequest.getSortField();
        String sortOrder = postQueryRequest.getSortOrder();
        int current = postQueryRequest.getCurrent();
        long pageSize = postQueryRequest.getPageSize();

        BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();

        //设置should
        if (StringUtils.isNotBlank(title)) {
            queryBuilder.should(QueryBuilders.matchQuery("title", title));
        }

        if (StringUtils.isNotBlank(content)) {
            queryBuilder.should(QueryBuilders.matchQuery("content", content));
        }

        if (StringUtils.isNotBlank(searchText)) {
            queryBuilder.should(QueryBuilders.matchQuery("title", searchText));
            queryBuilder.should(QueryBuilders.matchQuery("content", searchText));
        }

        if (StringUtils.isAllBlank(title, content, searchText)) {
            //如果上面几个查询条件都为空就查询出全部数据
            queryBuilder.should(QueryBuilders.matchAllQuery());
        }
        queryBuilder.minimumShouldMatch(1);//设置should中最少有一个匹配条件

        //设置filter
        queryBuilder.filter(QueryBuilders.termQuery("isDelete", 0));

        if (CollectionUtil.isNotEmpty(tags)) {
            //每一个值设置一个term,就必需列表里所有的标签数据中都存在才能把这条数据查出来
            for (String tag : tags) {
                queryBuilder.filter(QueryBuilders.termQuery("tags", tag));
            }
        }

        if (CollectionUtil.isNotEmpty(orTags)) {
            //terms:只要数据中的tags有一个值存在与查询条件中的tags里,就会被查出来
            queryBuilder.filter(QueryBuilders.termsQuery("tags", orTags));
        }

        //设置分页(es起始页为0!!!!!跟mysql不一样)
        PageRequest pageRequest = PageRequest.of(current - 1, (int) pageSize);


        //设置排序
        //先根据es生成的分数排序
        SortBuilder<?> sortBuilderForScore = SortBuilders.fieldSort("_score");
        sortBuilderForScore.order(SortOrder.DESC);
        //再根据传入的字段进行自定义排序
        SortBuilder<?> sortBuilderForSortField = SortBuilders.scoreSort();
        if (StringUtils.isNotBlank(sortField)) {
            sortBuilderForSortField = SortBuilders.fieldSort(sortField);
            sortBuilderForSortField.order(CommonConstant.SORT_ORDER_ASC.equals(sortOrder) ? SortOrder.ASC : SortOrder.DESC);
        }

        // 构造查询
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(queryBuilder)
                .withPageable(pageRequest).withSorts(sortBuilderForScore, sortBuilderForSortField).build();
        //执行查询,查出来的数据就存在这
        SearchHits<PostEsDTO> searchHits = elasticsearchRestTemplate.search(searchQuery, PostEsDTO.class);
        List<Post> resultList = new ArrayList<>();

        if (searchHits.hasSearchHits()) {
            //查询数据库
            List<SearchHit<PostEsDTO>> searchHitList = searchHits.getSearchHits();
            List<Long> idList = searchHitList.stream().map(searchHit -> searchHit.getContent().getId()).collect(Collectors.toList());
            //继承的ServiceImpl<PostMapper, Post>中有baseMapper这个成员变量
            List<Post> postList = baseMapper.selectBatchIds(idList);
            Map<Long, Post> postMap = postList.stream().collect(Collectors.toMap(Post::getId, post -> post));
            for (Long id : idList) {
                if (postMap.containsKey(id)) {
                    //数据库中存在该数据,加入结果集
                    resultList.add(postMap.get(id));
                } else {
                    //数据库中不存在该数据,从es中删除
                    String delete = elasticsearchRestTemplate.delete(id.toString(), PostEsDTO.class);
                    log.info("delete post {}", delete);
                }
            }
        }

        Page<Post> page = new Page<>(current, pageSize, searchHits.getTotalHits());
        page.setRecords(resultList);
        return page;
    }
}



压力测试

官方文档:https://jmeter.apache.org/

找到jar包:apache-jmeter–5.5 apache-jmeter-5.51 bin\ ApacheJMeter.jar 启动

  1. 配置线程组

    在这里插入图片描述

  2. 配置通用请求头

    在这里插入图片描述

    在这里插入图片描述

  3. 配置默认请求信息

    在这里插入图片描述

    在这里插入图片描述

  4. 设置单个请求(选择HTTP请求)

    在这里插入图片描述

    在这里插入图片描述

  5. 设置响应断言

    在这里插入图片描述

    在这里插入图片描述

  6. 设置结果树

    在这里插入图片描述

    在这里插入图片描述

  7. 设置聚合报告


在这里插入图片描述

在这里插入图片描述

  • 90%分位:90%的用户发送请求到服务器做出响应花的时间都在这个时间内
  • 99%分位:99%的用户都在这个响应时间内
  • 99%分位:99%的用户都在这个响应时间内
  • 吞吐量:每秒处理的请求数



部署



安装mysql,开启3306端口



安装es

  1. 下载并解压es

    wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.17.9-linux-x86_64.tar.gz 
    
    tar -xzf elasticsearch-7.17.9-linux-x86_64.tar.gz 
    
  2. 需要重新创建一个用户来执行

    ./elasticsearch &

    加个&是后台执行

    useradd es
    
    passwd es
    
  3. 安装ik分词器

    wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.17.6/elasticsearch-analysis-ik-7.17.6.zip
    
    unzip elasticsearch-analysis-ik-7.17.6.zip
    
    #如果ik与es的小版本对不上
    vim plugin-descriptor.properties
    把 version 改为 你的版本
    把 elasticsearch.version 改为 你的版本
    
  4. 使用http的方式向es中创建mapping

    curl -XPUT "http://localhost:9200/post_v1" -H 'Content-Type: application/json' -d'
    {
      "aliases": {
        "post": {}
      },
      "mappings": {
        "properties": {
          "title": {
            "type": "text",
            "analyzer": "ik_max_word",
            "search_analyzer": "ik_smart",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "content": {
            "type": "text",
            "analyzer": "ik_max_word",
            "search_analyzer": "ik_smart",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "tags": {
            "type": "keyword"
          },
          "userId": {
            "type": "long"
          },
          "createTime": {
            "type": "date"
          },
          "updateTime": {
            "type": "date"
          },
          "isDelete": {
            "type": "integer"
          }
        }
      }
    }'
    



打包Java后端为jar并允许

先把代码上传到代码仓库github

然后拉下来,在文件目录内运行这些语句,也可以在本地打包好再把jar包传过去,但是更新没这么方便

下载Java8
yum install -y java-1.8.0-openjdk*

下载maven
curl -o apache-maven-3.8.5-bin.tar.gz https://dlcdn.apache.org/maven/maven-3/3.8.5/binaries/apache-maven-3.8.5-bin.tar.gz

打包构建,跳过测试
git clone xxx 下载代码

打包构建,跳过测试
mvn package -DskipTests

加个 nohup     &后 台运行
nohup java -jar ./junso-backend-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod &



运行vue前端

配置nginx

vim /etc/profile
export PATH=$PATH:/www/server/nginx/sbin

先把代码上传到代码仓库github

然后拉下来,在文件目录内运行这些语句

npm install

npm i --save ant-design-vue@next

npm install axios

npm run build
 
nohup npm run serve & exit



问题:nohup npm run serve & 运行前端后,关闭当前窗口shell连接后前端进程就退出了

需要配置nohup的环境变量

查询nohup位置:复制这个位置
which nohup

修改环境变量
vim .bash_profile
在export之前加上  PATH=$PATH:$HOME/bin:/复制的地址




问题:es后台启动后自己kill了

修改jvm.options配置内存参数(jvm.options在es安装目录的config下)
这个就是直接修改JVM分配给elasticSearch的内存
最好将这两项参数修改成下面的参数设置:


修改之前
##-Xms4g
##-Xmx4g
修改之后
-Xms256m
-Xmx256m



版权声明:本文为weixin_63742275原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。