1.添加对es精准查询,去除去条件操作

2.添加搜索时对条件就行拆分操作
This commit is contained in:
sh
2026-07-17 17:07:58 +08:00
parent 06b38dddbf
commit 0cd29d6e5f
11 changed files with 737 additions and 89 deletions

View File

@@ -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);
}

View File

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

View File

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

View File

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

View File

@@ -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();
}

View File

@@ -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));
}
}

View File

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

View File

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

View File

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

View File

@@ -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~60003k-6k3千~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;
}
}
}

View File

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