1.添加对es精准查询,去除去条件操作
2.添加搜索时对条件就行拆分操作
This commit is contained in:
@@ -3,6 +3,7 @@ package com.ruoyi.cms.controller.app;
|
||||
import com.ruoyi.cms.domain.ESJobDocument;
|
||||
import com.ruoyi.cms.domain.Job;
|
||||
import com.ruoyi.cms.domain.JobApply;
|
||||
import com.ruoyi.cms.domain.ik.JobSearchCondition;
|
||||
import com.ruoyi.cms.domain.query.ESJobSearch;
|
||||
import com.ruoyi.cms.service.*;
|
||||
import com.ruoyi.cms.util.RoleUtils;
|
||||
@@ -17,6 +18,7 @@ import com.ruoyi.common.utils.DateUtils;
|
||||
import com.ruoyi.common.utils.SM4Utils;
|
||||
import com.ruoyi.common.utils.SiteSecurityUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.bean.BeanUtils;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import io.swagger.annotations.ApiParam;
|
||||
@@ -50,6 +52,8 @@ public class AppJobController extends BaseController
|
||||
private SensitiveWordChecker sensitiveWordChecker;
|
||||
@Autowired
|
||||
private IJobApplyService jobApplyService;
|
||||
@Autowired
|
||||
private JobNaturalSearchService jobNaturalSearchService;
|
||||
|
||||
/**
|
||||
* 查询岗位列表
|
||||
@@ -58,6 +62,10 @@ public class AppJobController extends BaseController
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(ESJobSearch job)
|
||||
{
|
||||
if(!StringUtils.isBlank(job.getJobTitle())){
|
||||
JobSearchCondition parserDictDTO=jobNaturalSearchService.parseNaturalText(job.getJobTitle());
|
||||
BeanUtils.copyBeanProp(job, parserDictDTO);
|
||||
}
|
||||
EsPageInfo<ESJobDocument> list = jobService.appList(job);
|
||||
return getTableDataInfo2(list,job);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.ruoyi.cms.controller.cms;
|
||||
|
||||
import com.ruoyi.cms.domain.ik.JobSearchCondition;
|
||||
import com.ruoyi.cms.service.JobNaturalSearchService;
|
||||
import com.ruoyi.common.annotation.Anonymous;
|
||||
import org.elasticsearch.index.query.BoolQueryBuilder;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@RestController
|
||||
public class JobSearchController {
|
||||
|
||||
@Resource
|
||||
private JobNaturalSearchService jobNaturalSearchService;
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('job:naturalSearch')")
|
||||
@GetMapping("/job/naturalSearch")
|
||||
// @Anonymous
|
||||
public Object naturalSearch(@RequestParam String keyword) {
|
||||
// 1、解析自然语句
|
||||
JobSearchCondition condition = jobNaturalSearchService.parseNaturalText(keyword);
|
||||
// 2、组装ES查询条件
|
||||
BoolQueryBuilder boolQuery = jobNaturalSearchService.buildEsQuery(condition);
|
||||
|
||||
System.out.println("解析后的条件:" + condition);
|
||||
System.out.println("构建的ES查询DSL:" + boolQuery);
|
||||
|
||||
// TODO 此处执行ES搜索,传入boolQuery 查询职位数据
|
||||
return condition;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.ruoyi.cms.domain.ik;
|
||||
|
||||
import lombok.Data;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
public class JobSearchCondition {
|
||||
/** 岗位名称 */
|
||||
private String jobTitle;
|
||||
/** 工作地点文本(原始区县名称) */
|
||||
private String address;
|
||||
/** 区划编码(业务编码) */
|
||||
private String jobLocationAreaCode;
|
||||
|
||||
/** 最低年限(数字区间range) */
|
||||
private Integer workYearMin;
|
||||
/** 最高年限(数字区间range) */
|
||||
private Integer workYearMax;
|
||||
/** 工作经验字典编码 0~7 */
|
||||
private String experience;
|
||||
|
||||
/** 最低薪资(元) */
|
||||
private Long minSalary;
|
||||
/** 最高薪资(元) */
|
||||
private Long maxSalary;
|
||||
|
||||
/** 学历编码 -1 ~ 6 */
|
||||
private String education;
|
||||
/** 匹配到的学历原始文本(本科/大专...用于过滤岗位名称) */
|
||||
private String educationText;
|
||||
/** 匹配到的工作经验原始文本(3-5年/应届毕业生...用于过滤岗位名称) */
|
||||
private String workExpText;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.ruoyi.cms.domain.ik;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Data
|
||||
public class ParserDictDTO {
|
||||
/** 区县名称->编码 */
|
||||
private Map<String, Integer> areaMap;
|
||||
/** 工作经验文本->编码 */
|
||||
private Map<String, Integer> workExpMap;
|
||||
/** 学历文本->编码 */
|
||||
private Map<String, Integer> educationMap;
|
||||
//岗位列表
|
||||
private Set<String> standardJobSet;
|
||||
|
||||
private Map<String,String> jobIndexMap;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.ruoyi.cms.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import com.ruoyi.common.core.domain.TreeSelect;
|
||||
import com.ruoyi.common.core.domain.entity.JobTitle;
|
||||
@@ -62,4 +63,6 @@ public interface IJobTitleService
|
||||
void importJobTitle();
|
||||
|
||||
List<JobTitle> levelOne();
|
||||
|
||||
Set<String> jobTitleNameSets();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.ruoyi.cms.service;
|
||||
|
||||
import com.ruoyi.cms.domain.BussinessDictData;
|
||||
import com.ruoyi.cms.domain.ik.JobSearchCondition;
|
||||
import com.ruoyi.cms.domain.ik.ParserDictDTO;
|
||||
import com.ruoyi.cms.util.DictUtils;
|
||||
import com.ruoyi.cms.util.ik.EsAnalyzeUtil;
|
||||
import com.ruoyi.cms.util.ik.TextConditionParserUtil;
|
||||
import org.elasticsearch.index.query.BoolQueryBuilder;
|
||||
import org.elasticsearch.index.query.QueryBuilders;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class JobNaturalSearchService {
|
||||
|
||||
@Autowired
|
||||
private EsAnalyzeUtil esAnalyzeUtil;
|
||||
@Autowired
|
||||
private IJobTitleService jobTitleService;
|
||||
|
||||
/**
|
||||
* 自然语言解析入口
|
||||
*/
|
||||
public JobSearchCondition parseNaturalText(String inputText) {
|
||||
// 1、ES分词
|
||||
List<EsAnalyzeUtil.TokenItem> tokenList = esAnalyzeUtil.analyze(inputText, "ik_max_word");
|
||||
|
||||
// 2、【从Redis加载三份词典】
|
||||
List<BussinessDictData> areaDictList = DictUtils.getDictCache("area");
|
||||
List<BussinessDictData> expDictList = DictUtils.getDictCache("experience");
|
||||
List<BussinessDictData> eduDictList = DictUtils.getDictCache("education");
|
||||
Set<String> standardJobSet = jobTitleService.jobTitleNameSets();
|
||||
Map<String,String> jobIndexMap = new HashMap<>(standardJobSet.size());
|
||||
for(String name : standardJobSet){
|
||||
jobIndexMap.put(name,name);
|
||||
}
|
||||
|
||||
ParserDictDTO dictDTO = new ParserDictDTO();
|
||||
dictDTO.setAreaMap(convertDictListToMap(areaDictList));
|
||||
dictDTO.setWorkExpMap(convertDictListToMap(expDictList));
|
||||
dictDTO.setEducationMap(convertDictListToMap(eduDictList));
|
||||
dictDTO.setStandardJobSet(standardJobSet);
|
||||
dictDTO.setJobIndexMap(jobIndexMap);
|
||||
|
||||
// 3、传入词典执行解析
|
||||
return TextConditionParserUtil.parse(inputText, tokenList, dictDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建ES查询BoolQuery
|
||||
*/
|
||||
public BoolQueryBuilder buildEsQuery(JobSearchCondition condition) {
|
||||
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
|
||||
|
||||
if (condition.getJobTitle() != null && condition.getJobTitle().trim().isEmpty()) {
|
||||
boolQuery.must(QueryBuilders.matchQuery("jobName", condition.getJobTitle()));
|
||||
}
|
||||
if (condition.getJobLocationAreaCode() != null) {
|
||||
boolQuery.filter(QueryBuilders.termQuery("areaCode", condition.getJobLocationAreaCode()));
|
||||
}
|
||||
if (condition.getExperience() != null) {
|
||||
boolQuery.filter(QueryBuilders.termQuery("workExpCode", condition.getExperience()));
|
||||
}
|
||||
if (condition.getEducation() != null) {
|
||||
boolQuery.filter(QueryBuilders.termQuery("educationCode", condition.getEducation()));
|
||||
}
|
||||
if (condition.getWorkYearMin() != null) {
|
||||
boolQuery.filter(QueryBuilders.rangeQuery("workYear").gte(condition.getWorkYearMin()));
|
||||
}
|
||||
if (condition.getWorkYearMax() != null) {
|
||||
boolQuery.filter(QueryBuilders.rangeQuery("workYear").lte(condition.getWorkYearMax()));
|
||||
}
|
||||
if (condition.getMinSalary() != null) {
|
||||
boolQuery.filter(QueryBuilders.rangeQuery("salaryMin").gte(condition.getMinSalary()));
|
||||
}
|
||||
if (condition.getMaxSalary() != null) {
|
||||
boolQuery.filter(QueryBuilders.rangeQuery("salaryMax").lte(condition.getMaxSalary()));
|
||||
}
|
||||
return boolQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典转换
|
||||
* @param dictDataList
|
||||
* @return
|
||||
*/
|
||||
private Map<String, Integer> convertDictListToMap(List<BussinessDictData> dictDataList) {
|
||||
if (dictDataList == null || dictDataList.isEmpty()) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
return dictDataList.stream()
|
||||
.filter(item -> item.getDictLabel() != null && item.getDictValue() != null)
|
||||
.collect(Collectors.toMap(
|
||||
BussinessDictData::getDictLabel,
|
||||
item -> {
|
||||
try {
|
||||
return Integer.parseInt(item.getDictValue());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
(oldVal, newVal) -> oldVal
|
||||
))
|
||||
.entrySet().stream()
|
||||
.filter(entry -> entry.getValue() != null)
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
}
|
||||
}
|
||||
@@ -264,50 +264,50 @@ public class ESJobSearchImpl implements IESJobSearchService
|
||||
|
||||
List<ESJobDocument> esJobDocuments = esJobDocumentMapper.selectList(wrapper);
|
||||
|
||||
if (!isCompanyUser &&esJobDocuments.size() < esJobSearch.getPageSize()) {
|
||||
// 定义要逐步放宽的搜索条件字段
|
||||
List<Runnable> relaxConditions = new ArrayList<>();
|
||||
relaxConditions.add(() -> newSearch.setArea(null));
|
||||
relaxConditions.add(() -> newSearch.setExperience(null));
|
||||
relaxConditions.add(() -> newSearch.setMaxSalary(null));
|
||||
relaxConditions.add(() -> newSearch.setMinSalary(null));
|
||||
relaxConditions.add(() -> newSearch.setEducation(null));
|
||||
relaxConditions.add(()-> newSearch.setJobTitle(null));
|
||||
// 保存所有查询到的文档
|
||||
List<ESJobDocument> allDocuments = new ArrayList<>(esJobDocuments);
|
||||
|
||||
// 记录已经放宽的条件数量
|
||||
int relaxedConditions = 0;
|
||||
|
||||
// 继续放宽条件直到满足分页或所有条件都已放宽
|
||||
while (allDocuments.size() < esJobSearch.getPageSize()
|
||||
&& relaxedConditions < relaxConditions.size()) {
|
||||
// 放宽下一个条件
|
||||
relaxConditions.get(relaxedConditions).run();
|
||||
relaxedConditions++;
|
||||
|
||||
// 查询新增的文档(不包含之前已经查询到的)
|
||||
wrapper = getWrapper(newSearch, jobIds);
|
||||
wrapper.limit(esJobSearch.getPageSize() - allDocuments.size());
|
||||
if (!allDocuments.isEmpty()) {
|
||||
// 排除已经查询到的文档ID
|
||||
Set<String> existingIds = allDocuments.stream()
|
||||
.map(ESJobDocument::getId)
|
||||
.collect(Collectors.toSet());
|
||||
wrapper.not().in(ESJobDocument::getId, existingIds);
|
||||
}
|
||||
|
||||
List<ESJobDocument> newDocuments = esJobDocumentMapper.selectList(wrapper);
|
||||
allDocuments.addAll(newDocuments);
|
||||
}
|
||||
|
||||
// 如果总数超过分页大小,截取所需数量
|
||||
if (allDocuments.size() > esJobSearch.getPageSize()) {
|
||||
esJobDocuments = allDocuments.subList(0, esJobSearch.getPageSize());
|
||||
} else {
|
||||
esJobDocuments = allDocuments;
|
||||
}
|
||||
}
|
||||
// if (!isCompanyUser &&esJobDocuments.size() < esJobSearch.getPageSize()) {
|
||||
// // 定义要逐步放宽的搜索条件字段
|
||||
// List<Runnable> relaxConditions = new ArrayList<>();
|
||||
// relaxConditions.add(() -> newSearch.setArea(null));
|
||||
// relaxConditions.add(() -> newSearch.setExperience(null));
|
||||
// relaxConditions.add(() -> newSearch.setMaxSalary(null));
|
||||
// relaxConditions.add(() -> newSearch.setMinSalary(null));
|
||||
// relaxConditions.add(() -> newSearch.setEducation(null));
|
||||
// relaxConditions.add(()-> newSearch.setJobTitle(null));
|
||||
// // 保存所有查询到的文档
|
||||
// List<ESJobDocument> allDocuments = new ArrayList<>(esJobDocuments);
|
||||
//
|
||||
// // 记录已经放宽的条件数量
|
||||
// int relaxedConditions = 0;
|
||||
//
|
||||
// // 继续放宽条件直到满足分页或所有条件都已放宽
|
||||
// while (allDocuments.size() < esJobSearch.getPageSize()
|
||||
// && relaxedConditions < relaxConditions.size()) {
|
||||
// // 放宽下一个条件
|
||||
// relaxConditions.get(relaxedConditions).run();
|
||||
// relaxedConditions++;
|
||||
//
|
||||
// // 查询新增的文档(不包含之前已经查询到的)
|
||||
// wrapper = getWrapper(newSearch, jobIds);
|
||||
// wrapper.limit(esJobSearch.getPageSize() - allDocuments.size());
|
||||
// if (!allDocuments.isEmpty()) {
|
||||
// // 排除已经查询到的文档ID
|
||||
// Set<String> existingIds = allDocuments.stream()
|
||||
// .map(ESJobDocument::getId)
|
||||
// .collect(Collectors.toSet());
|
||||
// wrapper.not().in(ESJobDocument::getId, existingIds);
|
||||
// }
|
||||
//
|
||||
// List<ESJobDocument> newDocuments = esJobDocumentMapper.selectList(wrapper);
|
||||
// allDocuments.addAll(newDocuments);
|
||||
// }
|
||||
//
|
||||
// // 如果总数超过分页大小,截取所需数量
|
||||
// if (allDocuments.size() > esJobSearch.getPageSize()) {
|
||||
// esJobDocuments = allDocuments.subList(0, esJobSearch.getPageSize());
|
||||
// } else {
|
||||
// esJobDocuments = allDocuments;
|
||||
// }
|
||||
// }
|
||||
|
||||
return esJobDocuments;
|
||||
}
|
||||
@@ -920,50 +920,50 @@ public class ESJobSearchImpl implements IESJobSearchService
|
||||
wrapper.limit(esJobSearch.getPageSize());
|
||||
List<ESJobDocument> esJobDocuments = esJobDocumentMapper.selectList(wrapper);
|
||||
|
||||
if (esJobDocuments.size() < esJobSearch.getPageSize()) {
|
||||
// 定义要逐步放宽的搜索条件字段
|
||||
List<Runnable> relaxConditions = new ArrayList<>();
|
||||
relaxConditions.add(() -> newSearch.setArea(null));
|
||||
relaxConditions.add(() -> newSearch.setExperience(null));
|
||||
relaxConditions.add(() -> newSearch.setMaxSalary(null));
|
||||
relaxConditions.add(() -> newSearch.setMinSalary(null));
|
||||
relaxConditions.add(() -> newSearch.setEducation(null));
|
||||
relaxConditions.add(()-> newSearch.setJobTitle(null));
|
||||
// 保存所有查询到的文档
|
||||
List<ESJobDocument> allDocuments = new ArrayList<>(esJobDocuments);
|
||||
|
||||
// 记录已经放宽的条件数量
|
||||
int relaxedConditions = 0;
|
||||
|
||||
// 继续放宽条件直到满足分页或所有条件都已放宽
|
||||
while (allDocuments.size() < esJobSearch.getPageSize()
|
||||
&& relaxedConditions < relaxConditions.size()) {
|
||||
// 放宽下一个条件
|
||||
relaxConditions.get(relaxedConditions).run();
|
||||
relaxedConditions++;
|
||||
|
||||
// 查询新增的文档(不包含之前已经查询到的)
|
||||
wrapper = getWrapper(newSearch, jobIds);
|
||||
wrapper.limit(esJobSearch.getPageSize() - allDocuments.size());
|
||||
if (!allDocuments.isEmpty()) {
|
||||
// 排除已经查询到的文档ID
|
||||
Set<String> existingIds = allDocuments.stream()
|
||||
.map(ESJobDocument::getId)
|
||||
.collect(Collectors.toSet());
|
||||
wrapper.not().in(ESJobDocument::getId, existingIds);
|
||||
}
|
||||
|
||||
List<ESJobDocument> newDocuments = esJobDocumentMapper.selectList(wrapper);
|
||||
allDocuments.addAll(newDocuments);
|
||||
}
|
||||
|
||||
// 如果总数超过分页大小,截取所需数量
|
||||
if (allDocuments.size() > esJobSearch.getPageSize()) {
|
||||
esJobDocuments = allDocuments.subList(0, esJobSearch.getPageSize());
|
||||
} else {
|
||||
esJobDocuments = allDocuments;
|
||||
}
|
||||
}
|
||||
// if (esJobDocuments.size() < esJobSearch.getPageSize()) {
|
||||
// // 定义要逐步放宽的搜索条件字段
|
||||
// List<Runnable> relaxConditions = new ArrayList<>();
|
||||
// relaxConditions.add(() -> newSearch.setArea(null));
|
||||
// relaxConditions.add(() -> newSearch.setExperience(null));
|
||||
// relaxConditions.add(() -> newSearch.setMaxSalary(null));
|
||||
// relaxConditions.add(() -> newSearch.setMinSalary(null));
|
||||
// relaxConditions.add(() -> newSearch.setEducation(null));
|
||||
// relaxConditions.add(()-> newSearch.setJobTitle(null));
|
||||
// // 保存所有查询到的文档
|
||||
// List<ESJobDocument> allDocuments = new ArrayList<>(esJobDocuments);
|
||||
//
|
||||
// // 记录已经放宽的条件数量
|
||||
// int relaxedConditions = 0;
|
||||
//
|
||||
// // 继续放宽条件直到满足分页或所有条件都已放宽
|
||||
// while (allDocuments.size() < esJobSearch.getPageSize()
|
||||
// && relaxedConditions < relaxConditions.size()) {
|
||||
// // 放宽下一个条件
|
||||
// relaxConditions.get(relaxedConditions).run();
|
||||
// relaxedConditions++;
|
||||
//
|
||||
// // 查询新增的文档(不包含之前已经查询到的)
|
||||
// wrapper = getWrapper(newSearch, jobIds);
|
||||
// wrapper.limit(esJobSearch.getPageSize() - allDocuments.size());
|
||||
// if (!allDocuments.isEmpty()) {
|
||||
// // 排除已经查询到的文档ID
|
||||
// Set<String> existingIds = allDocuments.stream()
|
||||
// .map(ESJobDocument::getId)
|
||||
// .collect(Collectors.toSet());
|
||||
// wrapper.not().in(ESJobDocument::getId, existingIds);
|
||||
// }
|
||||
//
|
||||
// List<ESJobDocument> newDocuments = esJobDocumentMapper.selectList(wrapper);
|
||||
// allDocuments.addAll(newDocuments);
|
||||
// }
|
||||
//
|
||||
// // 如果总数超过分页大小,截取所需数量
|
||||
// if (allDocuments.size() > esJobSearch.getPageSize()) {
|
||||
// esJobDocuments = allDocuments.subList(0, esJobSearch.getPageSize());
|
||||
// } else {
|
||||
// esJobDocuments = allDocuments;
|
||||
// }
|
||||
// }
|
||||
|
||||
return esJobDocuments;
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import java.util.stream.Collectors;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.ruoyi.cms.constant.CommonConstant;
|
||||
import com.ruoyi.cms.domain.SubwayLine;
|
||||
import com.ruoyi.cms.util.JobTitleUtils;
|
||||
import com.ruoyi.common.constant.CacheConstants;
|
||||
import com.ruoyi.common.core.domain.TreeSelect;
|
||||
import com.ruoyi.common.core.redis.RedisCache;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
@@ -30,6 +30,8 @@ public class JobTitleServiceImpl extends ServiceImpl<JobTitleMapper,JobTitle> im
|
||||
{
|
||||
@Autowired
|
||||
private JobTitleMapper jobTitleMapper;
|
||||
@Autowired
|
||||
private RedisCache redisCache;
|
||||
|
||||
/**
|
||||
* 查询岗位
|
||||
@@ -228,4 +230,19 @@ public class JobTitleServiceImpl extends ServiceImpl<JobTitleMapper,JobTitle> im
|
||||
public List<JobTitle> levelOne() {
|
||||
return jobTitleMapper.selectList(Wrappers.<JobTitle>lambdaQuery().eq(JobTitle::getParentId, 0l));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> jobTitleNameSets(){
|
||||
Set<String> labelSet = redisCache.getCacheObject(CacheConstants.JOB_LABEL_REDIS_KEY);
|
||||
if(labelSet == null || labelSet.isEmpty()){
|
||||
List<JobTitle> list = jobTitleMapper.selectJobTitleList(new JobTitle());
|
||||
labelSet = list.stream()
|
||||
.map(JobTitle::getJobName)
|
||||
.filter(label -> label != null && !label.trim().isEmpty())
|
||||
.map(String::trim)
|
||||
.collect(Collectors.toSet());
|
||||
redisCache.setCacheObject(CacheConstants.JOB_LABEL_REDIS_KEY, labelSet);
|
||||
}
|
||||
return labelSet;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.ruoyi.cms.util.ik;
|
||||
|
||||
import org.elasticsearch.client.RequestOptions;
|
||||
import org.elasticsearch.client.RestHighLevelClient;
|
||||
import org.elasticsearch.client.indices.AnalyzeRequest;
|
||||
import org.elasticsearch.client.indices.AnalyzeResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
import javax.annotation.Resource;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
public class EsAnalyzeUtil {
|
||||
|
||||
@Resource
|
||||
private RestHighLevelClient restHighLevelClient;
|
||||
|
||||
/**
|
||||
* 获取分词Token,携带偏移量
|
||||
* @param text 原文
|
||||
* @param analyzer ik_max_word / ik_smart
|
||||
*/
|
||||
public List<TokenItem> analyze(String text, String analyzer) {
|
||||
AnalyzeRequest request = AnalyzeRequest.withGlobalAnalyzer(analyzer, text);
|
||||
try {
|
||||
AnalyzeResponse response = restHighLevelClient.indices().analyze(request, RequestOptions.DEFAULT);
|
||||
return response.getTokens().stream()
|
||||
.map(token -> {
|
||||
TokenItem item = new TokenItem();
|
||||
item.term = token.getTerm();
|
||||
item.startOffset = token.getStartOffset();
|
||||
item.endOffset = token.getEndOffset();
|
||||
return item;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("调用ES分词接口异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class TokenItem {
|
||||
private String term;
|
||||
private int startOffset;
|
||||
private int endOffset;
|
||||
|
||||
public String getLexemeText() {
|
||||
return term;
|
||||
}
|
||||
|
||||
public String getTerm() {
|
||||
return term;
|
||||
}
|
||||
|
||||
public int getStartOffset() {
|
||||
return startOffset;
|
||||
}
|
||||
|
||||
public int getEndOffset() {
|
||||
return endOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
package com.ruoyi.cms.util.ik;
|
||||
|
||||
import com.ruoyi.cms.domain.ik.JobSearchCondition;
|
||||
import com.ruoyi.cms.domain.ik.ParserDictDTO;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
public class TextConditionParserUtil {
|
||||
|
||||
// ====================== 正则规则【不变】 ======================
|
||||
private static final Pattern YEAR_PATTERN = Pattern.compile("(\\d+)\\s*-\\s*(\\d+)年|(\\d+)年以上|(\\d+)年以内");
|
||||
/** 区间 3000~6000|3k-6k|3千~6千 */
|
||||
private static final Pattern P_SALARY_RANGE = Pattern.compile(
|
||||
"(\\d+)\\s*[-~~]\\s*(\\d+)|(\\d+)k\\s*[-~~]\\s*(\\d+)k|(\\d+)千\\s*[-~~]\\s*(\\d+)千"
|
||||
);
|
||||
/** 不低于 / 以上 >= */
|
||||
private static final Pattern P_SALARY_GE = Pattern.compile(
|
||||
"(月薪|薪资|工资)不低于\\s*(\\d+)(k|千)?|(\\d+)(k|千)?\\s*(以上)"
|
||||
);
|
||||
/** 不高于 / 以下 <= */
|
||||
private static final Pattern P_SALARY_LE = Pattern.compile(
|
||||
"(月薪|薪资|工资)不高于\\s*(\\d+)(k|千)?|(\\d+)(k|千)?\\s*(以下)"
|
||||
);
|
||||
/** 精准薪资:月薪5000、工资5000、薪资5000 */
|
||||
private static final Pattern P_SALARY_NUM = Pattern.compile(
|
||||
"(月薪|薪资|工资)\\s*(\\d+)(k|千)?"
|
||||
);
|
||||
|
||||
/** 精准中文薪资:月薪五千、薪资一万【独立正则,不和阿拉伯混合!重点修复】 */
|
||||
private static final Pattern P_SALARY_CHINESE = Pattern.compile(
|
||||
"(月薪|薪资|工资)\\s*(一|二|三|四|五|六|七|八|九|十)(千|万)"
|
||||
);
|
||||
|
||||
//中文数字映射表
|
||||
private static Long getChineseNum(String numChar, String unit) {
|
||||
int num;
|
||||
switch (numChar) {
|
||||
case "一": num = 1; break;
|
||||
case "二": num = 2; break;
|
||||
case "三": num = 3; break;
|
||||
case "四": num = 4; break;
|
||||
case "五": num = 5; break;
|
||||
case "六": num = 6; break;
|
||||
case "七": num = 7; break;
|
||||
case "八": num = 8; break;
|
||||
case "九": num = 9; break;
|
||||
case "十": num = 10; break;
|
||||
default: return null;
|
||||
}
|
||||
Long base = (long) num;
|
||||
if ("千".equals(unit)) {
|
||||
base = base * 1000L;
|
||||
} else if ("万".equals(unit)) {
|
||||
base = base * 10000L;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
// 匹配单独年限:3年、5年工作经验
|
||||
private static final Pattern SINGLE_YEAR_PATTERN = Pattern.compile("(\\d+)年");
|
||||
|
||||
/**
|
||||
* 解析入口,词典由上层Service传入(来自Redis)
|
||||
* @param input 用户输入原文
|
||||
* @param tokenList ES分词结果
|
||||
* @param dictDTO 字典数据(Redis加载)
|
||||
*/
|
||||
public static JobSearchCondition parse(String input,
|
||||
List<EsAnalyzeUtil.TokenItem> tokenList,
|
||||
ParserDictDTO dictDTO) {
|
||||
if (input == null || input.trim().isEmpty()) {
|
||||
return new JobSearchCondition();
|
||||
}
|
||||
if(dictDTO == null){
|
||||
throw new RuntimeException("解析字典不能为空");
|
||||
}
|
||||
input = input.trim();
|
||||
JobSearchCondition condition = new JobSearchCondition();
|
||||
|
||||
// 1.薪资、年限区间抽取
|
||||
extractYearRange(input, condition);
|
||||
extractSalaryRange(input, condition);
|
||||
|
||||
// 2.区县识别【长词优先】
|
||||
extractAreaCode(input, condition, dictDTO.getAreaMap());
|
||||
// 3.工作经验识别【长词优先】
|
||||
extractWorkExpCode(input, condition, dictDTO.getWorkExpMap());
|
||||
if (condition.getExperience() == null) {
|
||||
Matcher singleYearMatcher = SINGLE_YEAR_PATTERN.matcher(input);
|
||||
if (singleYearMatcher.find()) {
|
||||
int year = Integer.parseInt(singleYearMatcher.group(1));
|
||||
Integer expCode = convertYearToExpCode(year);
|
||||
condition.setExperience(String.valueOf(expCode));
|
||||
// 设置文本用于过滤jobName
|
||||
condition.setWorkExpText(singleYearMatcher.group());
|
||||
}
|
||||
}
|
||||
// 4.学历识别【长词优先】
|
||||
extractEducationCode(input, condition, dictDTO.getEducationMap());
|
||||
|
||||
// 5.拼接岗位名称,过滤已识别维度词汇
|
||||
List<String> matchJobList= extractStandardJobName(input, tokenList,dictDTO.getStandardJobSet(),dictDTO.getJobIndexMap());
|
||||
if (matchJobList != null && !matchJobList.isEmpty()) {
|
||||
condition.setJobTitle(String.join(",", matchJobList));
|
||||
} else {
|
||||
condition.setJobTitle(input);
|
||||
}
|
||||
return condition;
|
||||
}
|
||||
|
||||
private static void extractAreaCode(String text, JobSearchCondition condition, Map<String, Integer> areaMap) {
|
||||
List<Map.Entry<String, Integer>> entryList = new ArrayList<>(areaMap.entrySet());
|
||||
entryList.sort((o1, o2) -> Integer.compare(o2.getKey().length(), o1.getKey().length()));
|
||||
for (Map.Entry<String, Integer> entry : entryList) {
|
||||
String areaName = entry.getKey();
|
||||
if (text.contains(areaName)) {
|
||||
condition.setAddress(areaName);
|
||||
condition.setJobLocationAreaCode(String.valueOf(entry.getValue()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void extractWorkExpCode(String text, JobSearchCondition condition, Map<String, Integer> expMap) {
|
||||
List<Map.Entry<String, Integer>> entryList = new ArrayList<>(expMap.entrySet());
|
||||
entryList.sort((o1, o2) -> Integer.compare(o2.getKey().length(), o1.getKey().length()));
|
||||
for (Map.Entry<String, Integer> entry : entryList) {
|
||||
String key = entry.getKey();
|
||||
if (text.contains(key)) {
|
||||
condition.setExperience(String.valueOf(entry.getValue()));
|
||||
// 新增
|
||||
condition.setWorkExpText(key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void extractEducationCode(String text, JobSearchCondition condition, Map<String, Integer> eduMap) {
|
||||
List<Map.Entry<String, Integer>> entryList = new ArrayList<>(eduMap.entrySet());
|
||||
entryList.sort((o1, o2) -> Integer.compare(o2.getKey().length(), o1.getKey().length()));
|
||||
for (Map.Entry<String, Integer> entry : entryList) {
|
||||
String key = entry.getKey();
|
||||
if (text.contains(key)) {
|
||||
condition.setEducation(String.valueOf(entry.getValue()));
|
||||
condition.setEducationText(key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void extractYearRange(String text, JobSearchCondition condition) {
|
||||
Matcher matcher = YEAR_PATTERN.matcher(text);
|
||||
if (matcher.find()) {
|
||||
if (matcher.group(1) != null && matcher.group(2) != null) {
|
||||
condition.setWorkYearMin(Integer.parseInt(matcher.group(1)));
|
||||
condition.setWorkYearMax(Integer.parseInt(matcher.group(2)));
|
||||
} else if (matcher.group(3) != null) {
|
||||
condition.setWorkYearMin(Integer.parseInt(matcher.group(3)));
|
||||
} else if (matcher.group(4) != null) {
|
||||
condition.setWorkYearMax(Integer.parseInt(matcher.group(4)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void extractSalaryRange(String text, JobSearchCondition condition) {
|
||||
text = text.trim();
|
||||
Matcher m;
|
||||
|
||||
// 1.优先匹配区间
|
||||
m = P_SALARY_RANGE.matcher(text);
|
||||
if (m.find()) {
|
||||
if (m.group(1) != null && m.group(2) != null) {
|
||||
condition.setMinSalary(Long.parseLong(m.group(1)));
|
||||
condition.setMaxSalary(Long.parseLong(m.group(2)));
|
||||
} else if (m.group(3) != null && m.group(4) != null) {
|
||||
// 3k~6k
|
||||
Long num1 = Long.parseLong(m.group(3)) * 1000L;
|
||||
Long num2 = Long.parseLong(m.group(4)) * 1000L;
|
||||
condition.setMinSalary(num1);
|
||||
condition.setMaxSalary(num2);
|
||||
} else if (m.group(5) != null && m.group(6) != null) {
|
||||
Long num1 = Long.parseLong(m.group(5)) * 1000L;
|
||||
Long num2 = Long.parseLong(m.group(6)) * 1000L;
|
||||
condition.setMinSalary(num1);
|
||||
condition.setMaxSalary(num2);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 2.匹配 >=
|
||||
m = P_SALARY_GE.matcher(text);
|
||||
if (m.find()) {
|
||||
String numStr = null;
|
||||
if (m.group(2) != null) numStr = m.group(2);
|
||||
if (m.group(4) != null) numStr = m.group(4);
|
||||
if (numStr != null) {
|
||||
// 字符串转Long
|
||||
Long num = Long.parseLong(numStr);
|
||||
// 判断单位 k / 千,乘以1000
|
||||
if ("k".equals(m.group(3)) || "千".equals(m.group(3))
|
||||
|| "k".equals(m.group(5)) || "千".equals(m.group(5))) {
|
||||
num = num * 1000L;
|
||||
}
|
||||
condition.setMinSalary(num);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 3.匹配 <=
|
||||
m = P_SALARY_LE.matcher(text);
|
||||
if (m.find()) {
|
||||
String numStr = null;
|
||||
if (m.group(2) != null) numStr = m.group(2);
|
||||
if (m.group(4) != null) numStr = m.group(4);
|
||||
if (numStr != null) {
|
||||
Long num = Long.parseLong(numStr);
|
||||
if ("k".equals(m.group(3)) || "千".equals(m.group(3))
|
||||
|| "k".equals(m.group(5)) || "千".equals(m.group(5))) {
|
||||
num = num * 1000L;
|
||||
}
|
||||
condition.setMaxSalary(num);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 4.阿拉伯数字薪资
|
||||
m = P_SALARY_NUM.matcher(text);
|
||||
if (m.find()) {
|
||||
Long num = safeParseLong(m.group(2));
|
||||
if (num != null) {
|
||||
if ("k".equals(m.group(3)) || "千".equals(m.group(3))) {
|
||||
num = num * 1000L;
|
||||
}
|
||||
condition.setMinSalary(num);
|
||||
//condition.setMaxSalary(num);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 5.中文薪资【月薪五千】单独匹配
|
||||
m = P_SALARY_CHINESE.matcher(text);
|
||||
if (m.find()) {
|
||||
Long num = getChineseNum(m.group(2), m.group(3));
|
||||
condition.setMinSalary(num);
|
||||
//condition.setMaxSalary(num);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据工作年限数字,换算成 workExpCode
|
||||
*/
|
||||
private static Integer convertYearToExpCode(int year) {
|
||||
if (year < 1) {
|
||||
return 3;
|
||||
} else if (year <= 3) {
|
||||
return 4;
|
||||
} else if (year <= 5) {
|
||||
return 5;
|
||||
} else if (year <= 10) {
|
||||
return 6;
|
||||
} else {
|
||||
return 7;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 正向匹配国标岗位名称(长词优先)
|
||||
* @param input 用户原始输入文本
|
||||
* @param standardJobSet 国标岗位集合
|
||||
* @return 匹配到的岗位名称,无匹配返回null
|
||||
*/
|
||||
private static List<String> extractStandardJobName(String input,List<EsAnalyzeUtil.TokenItem> tokenList, Set<String> standardJobSet, Map<String,String> jobIndexMap) {
|
||||
if (standardJobSet == null || standardJobSet.isEmpty() || jobIndexMap == null || jobIndexMap.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String text = input.replace("/", "");
|
||||
List<String> jobList = new ArrayList<>(standardJobSet);
|
||||
// 长名称优先,优先匹配更长岗位,避免短词根优先命中
|
||||
jobList.sort((s1, s2) -> Integer.compare(s2.length(), s1.length()));
|
||||
final int MAX_MATCH_SIZE = 5;
|
||||
|
||||
for (String jobName : jobList) {
|
||||
if (text.contains(jobName)) {
|
||||
return Collections.singletonList(jobName);
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> matchSet = new HashSet<>();
|
||||
if (tokenList != null && !tokenList.isEmpty()) {
|
||||
for (EsAnalyzeUtil.TokenItem token : tokenList) {
|
||||
String word = token.getLexemeText().trim();
|
||||
if (word.length() < 2) {
|
||||
continue;
|
||||
}
|
||||
for (String jobName : jobList) {
|
||||
if (jobName.contains(word)) {
|
||||
matchSet.add(jobName);
|
||||
if (matchSet.size() >= MAX_MATCH_SIZE) {
|
||||
return new ArrayList<>(matchSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchSet.isEmpty()) {
|
||||
return new ArrayList<>(matchSet);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isFilterWord(String word, JobSearchCondition condition) {
|
||||
// 基础过滤词
|
||||
if (word.matches("\\d+") || new ArrayList<>(Arrays.asList(
|
||||
"k", "千", "年", "月薪", "薪资", "工资",
|
||||
"以上", "以下", "的", "不低于", "不高于",
|
||||
"学历", "工作经验"
|
||||
)).contains(word)) {
|
||||
return true;
|
||||
}
|
||||
// 过滤区县
|
||||
if (condition.getAddress() != null && condition.getAddress().contains(word)) {
|
||||
return true;
|
||||
}
|
||||
// 过滤学历文本(本科、大专...)
|
||||
if (condition.getEducationText() != null && condition.getEducationText().contains(word)) {
|
||||
return true;
|
||||
}
|
||||
// 过滤经验文本(1-3年、应届毕业生...)
|
||||
if (condition.getWorkExpText() != null && condition.getWorkExpText().contains(word)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Long safeParseLong(String str) {
|
||||
if (str == null || str.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(str);
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,4 +54,9 @@ public class CacheConstants
|
||||
* 刷新token
|
||||
*/
|
||||
public static final String LOGIN_SITE_REFRESH_KEY = "login_site_user:refresh_token:";
|
||||
|
||||
/**
|
||||
* 国标岗位名称
|
||||
*/
|
||||
public static final String JOB_LABEL_REDIS_KEY = "dict:job_title:label_list";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user