用ES写了一个查询带有排序和随机效果的实例

203 阅读4分钟

本鼠鼠干JAVA也有些年头了,平常也只会写写CRUD。突然有一天,产品出了一个需求给本鼠鼠给整懵了,本想直接三连告诉他做不了,找别人吧,告辞。奈何产品好说歹说,心软了,于是就有了下面的一系列内容。第一次写文章,写这篇文章也主要是为了做个记录。

首先先说一下需求:

展示工作推荐列表,可下拉刷新,需满足以下条件  
1、数据来源:【更新时间】在一个月内的招聘信息
2、排序规则:置顶>更新时间(3.1),随机(3.2)
3、排序顺序
      3.1 与当前用户【简历-求职职位】相符的招聘信息
      3.2 按以下顺序展示:职位所属招聘子版块>招聘求职(随机推荐,在UI展示上与3.1区别开)
4、 数据上限:100条  

说实话,看到这个排序,然后还要随机,脑壳就一阵疼。一开始的想法是先把符合条件的数据分段从数据库查出来,然后再拼接100条进行返回,但是产品要求了,必须分页,而且由于该表在生产环境是经常被访问的,所以针对这表的操作在性能上是必须慎之又慎。中间就是绕了挺多弯的,后来想到这表的数据都存在ES中,那是否可以直接用ES查询来实现呢。废话不多说,先上代码:

 @Override
    public ResResult<Page<FrontJobsRecruittPageListVo>>  selectRecommandRandList(RecommandJobsQueryParam params) {
        //获取随机种子
        int seed = getSeedNum(params.getCurrent());
        Long positionId = params.getPositionId();
        if (null == positionId){
            FrontUserResume userResume = userResumeMapper.selectOne(new QueryWrapper<FrontUserResume>().eq("user_id",LoginUtil.getUserId()).last("limit 1"));
            positionId = null != userResume ?  userResume.getPositionId() : null;
        }
        if (params.getCurrent() * params.getSize() > 100){
            // 封装结果
            Page<FrontJobsRecruittPageListVo> result = new Page<>();
            result.setCurrent(params.getCurrent());
            result.setSize(params.getSize());
            result.setTotal(100);
            result.setRecords(new ArrayList<>());
            return ResUtil.yes(result);
        }
        FrontModulesCategory modulesCategory =  frontModulesCategoryMapper.selectById(positionId);
        List<FrontModulesCategory> modulesCategoryList = frontModulesCategoryMapper.selectList(
        new QueryWrapper<FrontModulesCategory>().eq("pid",modulesCategory.getPid())
                .ne("id",positionId));
        List<Long> categoryIds = modulesCategoryList.stream().map(FrontModulesCategory :: getId).collect(Collectors.toList());

        List<Long> notExitsIds = new ArrayList<>();
        notExitsIds.add(positionId);
        notExitsIds.addAll(categoryIds);

        List<FrontModulesCategory> otherCategoryList = frontModulesCategoryMapper.selectList(
                new QueryWrapper<FrontModulesCategory>().eq("types","position")
                        .likeRight("modules_code","recruit_")
                        .notIn("id",notExitsIds));
        List<Long> otherCategoryIds = otherCategoryList.stream().map(FrontModulesCategory :: getId).collect(Collectors.toList());
        List<FrontJobsRecruittPageListVo> results=new ArrayList<>();
        // 设置日期格式为 basic_date_time
        Date now = new Date();
        Calendar cal = Calendar.getInstance();
        cal.setTime(now);
        cal.add(Calendar.MONTH, -1);
        Date oneMonthAgoDate = cal.getTime();
        // 设置日期格式为 basic_date_time
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss.SSSZ");
//        sdf.setTimeZone(TimeZone.getTimeZone(DateUtils.getXTimezone())); // 确保使用 UTC 时间格式化
        String oneMonthAgo = sdf.format(oneMonthAgoDate);

        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();

        //先选择公共的查询条件
        BoolQueryBuilder boolQueryBuilder2 = boolQuery()
                .must(termQuery("flag", "1"))
                .must(wildcardQuery("modulesCode", "recruit_*"))
                .must(rangeQuery("sortTime").gte(oneMonthAgo))
                .must(termsQuery("auditStatus", AppConstant.SHOW_AUDIT_STATUS));

        //里面多写了一次公共条件,之前有试着整合公共条件,但不知道为啥根据城市查询出的结果会有问题所以就多写了
        if (params.getCitiesId() != null && !params.getCitiesId().equals("0")){
            boolQueryBuilder2.mustNot(termQuery("categoryId", positionId));
            BoolQueryBuilder boolQueryBuilder1 = boolQuery()
                    .must(termQuery("flag", "1"))
                    .must(wildcardQuery("modulesCode", "recruit_*"))
                    .must(rangeQuery("sortTime").gte(oneMonthAgo))
                    .must(termsQuery("auditStatus", AppConstant.SHOW_AUDIT_STATUS))
                    .must(termQuery("categoryId", positionId))
                    .must(termQuery("citiesIds", params.getCitiesId()));
            boolQueryBuilder.should(boolQueryBuilder1);

        }
        boolQueryBuilder.should(boolQueryBuilder2);

        //上面查询条件转换为sql为where (flag = 1 and modulesCode like 'recruit_%' and sortTime >= 时间 and auditStatus int(1,2))
        //                      or (flag = 1 and modulesCode like 'recruit_%' and sortTime >= 时间 and auditStatus int(1,2) and categoryId = 职位ID and citiesIds = 城市ID)

       //重点来了,根据符合条件的结果进行匹配得分,得分越高的排在越前面,因为查询的结果要分三块排序,精准职位匹配>同类职位匹配(随机)>同模块职位匹配的(随机)
        FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery(
                boolQueryBuilder,
                new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
                        new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                                QueryBuilders.boolQuery()
                                        .must(params.getCitiesId() != null && !params.getCitiesId().equals("0") ?
                                                QueryBuilders.termQuery("citiesIds", params.getCitiesId()) :
                                                QueryBuilders.matchAllQuery()) // 仅过滤 citiesId,不影响得分
                                        .must(QueryBuilders.termQuery("categoryId", positionId)),
                                ScoreFunctionBuilders.weightFactorFunction(99.0f)  // 提高匹配 positionId 的文档得分
                        ),
                        new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                                QueryBuilders.boolQuery().must(QueryBuilders.termsQuery("categoryId", categoryIds)),
                                ScoreFunctionBuilders.randomFunction().seed(seed).setWeight(10.0f) // 使用种子值增加随机排序
                        ),
                        new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                                QueryBuilders.boolQuery().must(QueryBuilders.termsQuery("categoryId", otherCategoryIds)),
                                ScoreFunctionBuilders.randomFunction().seed(seed) // 使用种子值增加随机排序
                        )
                }
        );
        Query searchQuery = new NativeSearchQueryBuilder()
                .withQuery(functionScoreQueryBuilder)
                .withSort(SortBuilders.scoreSort().order(SortOrder.DESC))  // 首先根据得
                .withSort(SortBuilders.fieldSort("ifTop").order(SortOrder.DESC))
                .withSort(SortBuilders.fieldSort("sortTime").order(SortOrder.DESC))
                .withPageable(PageRequest.of(params.getCurrent()-1, params.getSize()))
                .build();

        SearchHits<ModulesMessageDocument> searchHits = elasticsearchTemplate.search(searchQuery, ModulesMessageDocument.class);
        List<ModulesMessageDocument> primaryResults = searchHits.getSearchHits().stream()
                .map(hit -> hit.getContent())
                .collect(Collectors.toList());
        processDataNew(primaryResults,results,positionId,categoryIds);

        // 封装结果
        Page<FrontJobsRecruittPageListVo> result = new Page<>();
        result.setCurrent(params.getCurrent());
        result.setSize(params.getSize());
        result.setTotal(100);
        result.setRecords(results);
        return ResUtil.yes(result);
    }

由上面可看出,先设置查询条件,然后再根据匹配结果的得分来进行排序。由于需要确保大部分设备看到的数据是不一样的,所以每次请求进来,判断如果是第一页,则重新生成一个随机种子,以下是生成种子的方法

/**
 * 获得随机种子,在ES查询的时候使用,确保每台设备查询的数据是随机的
 **/
public Integer getSeedNum(Integer current){
    Integer code;
    String key = String.format(AppConstant.RECOMMAND_JOBS_KEY,LoginUtil.getUserId());
    //判断如果是第一页,则重新生成一个随机种子
    if (null != redisTemplate.opsForValue().get(key) && current != 1){
        code = Integer.parseInt(redisTemplate.opsForValue().get(key));
    }else{
        Random random = new Random();
        code = random.nextInt(9000) + 1000;
        redisTemplate.opsForValue().set(key,code+"",1, TimeUnit.DAYS);
    }
    return code;
}

最后则是对结果数据进行整理,倒是没什么好看的,随手贴上来了。

public void processDataNew(List<ModulesMessageDocument> documentList,List<FrontJobsRecruittPageListVo> resultList,Long positionId , List<Long>positionIdList){
        if(CollectionUtil.isEmpty(documentList)){
            return;
        }

        List<ModulesMessageDocument> matchedDocuments = documentList.stream()
                .filter(modulesMessageDocument -> positionId.equals(modulesMessageDocument.getCategoryId()))
                .sorted(Comparator.comparing(ModulesMessageDocument::getIfTop)
                        .thenComparing(ModulesMessageDocument::getSortTime).reversed()).collect(Collectors.toList());
        // 取出子模块的元素
        List<ModulesMessageDocument> ifTopOneDocuments = documentList.stream()
                .filter(modulesMessageDocument ->positionIdList.contains(modulesMessageDocument.getCategoryId()))
                .sorted(Comparator.comparing(ModulesMessageDocument::getIfTop).reversed()
                        .thenComparing(ModulesMessageDocument::getSortTime).reversed())
                .collect(Collectors.toList());

        // 取出其它招聘的元素
        positionIdList.add(positionId);
        List<ModulesMessageDocument> randomDocuments = documentList.stream()
                .filter(modulesMessageDocument ->!positionIdList.contains(modulesMessageDocument.getCategoryId()) )
                .collect(Collectors.toList());
//        Collections.shuffle(randomDocuments);
       //  合并所有结果
        matchedDocuments.addAll(ifTopOneDocuments);
        matchedDocuments.addAll(randomDocuments);

        List<FrontJobsRecruittPageListVo> parallelProcessed = matchedDocuments.parallelStream().map(document -> {
            FrontJobsRecruittPageListVo vo=new FrontJobsRecruittPageListVo();
            BeanUtils.copyProperties(document,vo);
            vo.setCitiesIdChineseText(frontUsCitiesService.getCitiesChineseNameJoinStr(document.getCitiesIds()," "));
            if(ObjectUtil.isNotNull(document.getCategoryId())){
                vo.setPositionId(document.getCategoryId());
                FrontModulesCategory frontModulesCategory = frontModulesCategoryService.getModulesCategoryById(document.getCategoryId());
                FrontModulesCategory parentCategory = frontModulesCategoryService.getModulesCategoryById(frontModulesCategory.getPid());
                if(parentCategory != null){
                    vo.setPositionIdText(parentCategory.getName()+"-"+frontModulesCategory.getName());
                }else{
                    vo.setPositionIdText(frontModulesCategory.getName());
                }

            }
            vo.setMoney(com.wf.user.common.util.StringUtils.getMoney(document.getMoney()));

            JSONObject sourceData= JSON.parseObject(document.getSourceData());
            if (null != sourceData.getString("ifTax")) {
                FrontModulesCategory ifTax = frontModulesCategoryService.getModulesCategoryById(sourceData.getString("ifTax"));
                if (null != ifTax && ifTax.getId() != null) {
                    if (AppConstant.IF_TAX_ID.contains(ifTax.getId().toString())) {
                        vo.setIsShowTaxText(false);
                    } else {
                        vo.setIsShowTaxText(true);
                    }
                    vo.setIfTaxText(ifTax.getName());
                }
            } else {
                vo.setIsShowTaxText(false);
            }
            if (null != sourceData.getInteger("authType")) {
                vo.setAuthType(sourceData.getInteger("authType"));
            }

            vo.setIfTop(ObjectUtil.isNull(document.getIfTop())? false :document.getIfTop());
            vo.setRecommandType(positionId.equals(document.getCategoryId()) ? 0:1);
            return vo;
        }).collect(Collectors.toList());
        resultList.addAll(parallelProcessed);
        List<Long> ids  = resultList.stream().map(FrontJobsRecruittPageListVo ::getId).collect(Collectors.toList());
        List<FrontJobsRecruitt> jobsRecruittList = baseMapper.selectList( new QueryWrapper<FrontJobsRecruitt>()
                .in("id", ids));
        for (FrontJobsRecruittPageListVo recruittPageListVo : resultList){
           for (FrontJobsRecruitt jobs : jobsRecruittList){
               if (jobs.getId().equals(recruittPageListVo.getId())){
                   recruittPageListVo.setAuthType(jobs.getAuthType());
                   FrontModulesCategory fmc2 = frontModulesCategoryService.getModulesCategoryById(jobs.getWorkWay());
                   recruittPageListVo.setWorkWayText(fmc2.getName());
               }
           }
        }

    }

以上就是所有内容了,感觉在代码上还是有挺多优化的空间,如果有比较好的建议,可以在评论区说出来