ElasticSearch nested 字段多关键字搜索,高亮全部匹配关键字的处理
ElasticSearch nested
字段多关键字搜索,高亮全部匹配关键字的处理
环境介绍
ElasticSearch
版本号: 6.7.0
需求说明
用户会传入多个关键字去ES查询ElasticSearch nested
字段 的多个字段,要求在返回的结果中被搜索的字段需要高亮所有匹配的关键字。例如同时通过上海
和策划
关键字,再 工作经历
的列表中的工作内容
和公司名称
中搜索。如果有人员的工作经历
中这两个关键字上海
和策划
都可以匹配到,那么返回的结果中同时高亮这上海
和策划
关键字
分析调研
基础的ElasticSearch nested
字段的高亮实现搜可以参考https://blog.csdn.net/weixin_48990070/article/details/120342597 这篇笔记。
问题点1
对于同一个nested
字段支持在一个nested Query
用不同的关键字来搜索,但对于should
查询只会高亮其中匹配的一个关键字,而不是全部。引入如果多关键字直接是任意满足的关系,则之后高亮匹配的其中的一个关键字,这个与不满足需求。
问题点2
那就把关键字拆分为多个nested Query
,一个关键字对应一个nested Query
。但这个方法一样可以搜索,但对于同一个nested
字段的nested Query
默认的inner_hits
属性只能出现在一个nested Query
中,不允许同一个nested
字段的不同nested Query
都指定inner_hits
,如果一定要这么做,那么就会得到一个查询错误的提示,提示如下:
"reason": {
"type": "illegal_argument_exception",
"reason": "[inner_hits] already contains an entry for key [trackRecordList]"
}
如果只在一个关键字的nested Query
指定inner_hits
,那么最终的高亮结果只会有该nested Query
的高亮,还是不满足要求。
问题点3
通过AI询问得知inner_hits
有个name
属性可以解决问题点2
的情况,可以通过设置inner_hits
不同的name
属性值来达到对同一个nested
字段用不同nested Query
来做多关键字的高亮效果,但是这样里又出现了两个新的问题。
1、inner_hits
有个name
属性值不能重复,否则一样出问题点2
的错误提示。
2、高亮 结果是按照inner_hits
有个name
属性值分组展示的,不像非nested
会给一个最终多个关键字都高亮的结果。
转换下问题就是:
1、要根据关键字自动生成不重复inner_hits
有个name
属性值
2、对于同字段的高亮结果,要做高亮内容的合并。
因此只要解决了上面两个问题,就可以完成业务的需求了。
最终解决方案
问题1解决方案
在将查询参数转换为ES Query语句的处理中,用Map来缓存每个nested
字段的当前有几个nested Query
,通过累计数量,来自动生成每个nested Query
中的inner_hits
有个name
属性名,例如名称为 nested
字段名+“-”+自增序号
因此就不能再使用静态方法来构建查询语句了,得用构建器了,下面就是构建器的部分实现
public class EsQueryBuilder {
// 存储嵌套字段及其累计值的映射
private Map<String, IntAccumulator> accNestedFieldMap = new HashMap<>();
// 无需嵌套高亮的字段集合
private Set<String> noNestedHighlightFields = new HashSet<>();
// 关键词分组列表
private List<PageSearchKeywordGroupParameter> keywordGroupList ;
// 是否开启高亮显示
private boolean isHighlight = false;
// 主查询构建器
private BoolQueryBuilder mainQueryBuilder;
//存储嵌套字段及其高亮构建器
private Map<NestedQueryBuilder,InnerHitBuilder> nestedQueryBuilderHighlightMap = new HashMap<>();
/**
* 构造方法
* @param keywordGroupList 搜索关键字组
* @param isHighlight 是否高亮
*/
public EsQueryBuilder(List<PageSearchKeywordGroupParameter> keywordGroupList,boolean isHighlight) {
this.keywordGroupList = keywordGroupList;
this.isHighlight = isHighlight;
this.mainQueryBuilder = new BoolQueryBuilder();
//补充嵌套字段初始累加器
EsQueryFieldEnum.getNestedFieldList().forEach(item->{
accNestedFieldMap.put(item.getFieldConfig().getMainField(),new IntAccumulator(0));
});
}
/**
* 向当前的查询构建器中添加条件。这个方法会遍历关键字组列表(keywordGroupList)中的每一个项目,
* 并根据是否标记为排除条件,将关键字添加到查询的必须条件(must)或者必须不条件(mustNot)中。
* @return EsQueryBuilder 返回当前的查询构建器实例,允许链式调用。
*/
public EsQueryBuilder addCondition() {
keywordGroupList.forEach(item->{
// 只处理非空关键字的项目
if(StringUtils.isNotBlank(item.getKeyword())){
// 根据是否为排除条件,选择添加到must或mustNot中
if(BooleanUtils.isTrue(item.getIsExclude())){
mainQueryBuilder.mustNot(buildQueryBuilder(item));
}else{
mainQueryBuilder.must(buildQueryBuilder(item));
}
}
});
return this;
}
/**
* 为所有内容添加高亮显示条件的查询构建器。
* 该方法遍历关键字组列表,对非排除条件的关键字进行全文搜索设置,并根据关键字是否为排除条件,添加相应的查询条件。
* @return EsQueryBuilder 当前查询构建器实例,支持链式调用。
*/
public EsQueryBuilder addConditionForAllContentHighlight() {
// 遍历关键字组列表,过滤掉设置为排除条件的关键字,对剩余的关键字进行全文搜索设置
keywordGroupList.stream()
// 过滤掉设置为排除条件的关键字
.filter(item->BooleanUtils.isNotTrue(item.getIsExclude()))
.peek(item->{
// 设置搜索类型为全文搜索,清空子类型设置
item.setSearchType(EsQueryTypeEnum.ALL.value());
item.setSearchSubType(null);
})
.forEach(item->{
// 根据关键字是否为排除条件,添加相应的查询条件
if(StringUtils.isNotBlank(item.getKeyword())){
if(BooleanUtils.isTrue(item.getIsExclude())){
// 如果是排除条件,则添加到must not查询条件中
mainQueryBuilder.mustNot(buildQueryBuilder(item));
}else{
// 如果不是排除条件,则添加到must查询条件中
mainQueryBuilder.must(buildQueryBuilder(item));
}
}
});
return this;
}
/**
* 嵌套字段高亮处理
**/
private void highlightNestedQuery() {
if(!nestedQueryBuilderHighlightMap.isEmpty()){
nestedQueryBuilderHighlightMap.forEach(NestedQueryBuilder::innerHit);
}
}
/**
* 为查询添加过滤条件。
* 这个方法允许用户指定一个过滤条件,并将其应用到当前的查询构建器中。
* @param queryBuilder 过滤条件的查询构建器。这是一个已经构建好的查询条件,将作为过滤条件添加到主查询中。
* @return 返回当前的EsQueryBuilder实例,允许链式调用。
*/
public EsQueryBuilder filterCondition(QueryBuilder queryBuilder) {
// 为主查询添加过滤条件
mainQueryBuilder.filter(queryBuilder);
return this;
}
/**
* 构建查询条件
* @return org.elasticsearch.search.builder.SearchSourceBuilder
*/
public SearchSourceBuilder build() {
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//查询条件
searchSourceBuilder.query(mainQueryBuilder);
return searchSourceBuilder;
}
/**
* 构建查询条件(支持高亮)
* @return org.elasticsearch.search.builder.SearchSourceBuilder
*/
public SearchSourceBuilder buildWithHighlight() {
SearchSourceBuilder searchSourceBuilder = build();
if(isHighlight){
//补充非嵌套字段的高亮
highlightNestedQuery();
//非嵌套高亮
searchSourceBuilder.highlighter(EsHighlightUtils.buildNotNestHighlightBuilder(noNestedHighlightFields));
}
return searchSourceBuilder;
}
/**
* 构建查询构建器。
* 该方法根据传入的参数生成一个对应的查询条件构建器,主要用于处理专家页面的搜索关键词分组参数。
* @param parameter 搜索参数,包含需要搜索的关键词和其他搜索条件。
* @return 返回构建好的查询条件构建器对象。
*/
private QueryBuilder buildQueryBuilder(PageSearchKeywordGroupParameter parameter){
// 初始化一个布尔类型的查询条件构建器,用于后续添加各种查询条件
BoolQueryBuilder keywordQueryBuilder = new BoolQueryBuilder();
// 根据参数生成对应的查询类型枚举,用于确定如何构建查询条件
EsQueryTypeEnum queryTypeEnum = generateQueryTypeEnum(parameter);
// 调用查询类型枚举中定义的添加条件处理器,处理当前搜索参数,并将其添加到查询条件构建器中
queryTypeEnum.getAddConditionHandler().handle(this,keywordQueryBuilder,parameter.getKeyword());
return keywordQueryBuilder;
}
/**
* 根据关键词和字段枚举生成查询条件。
* @param keyword 关键词,用于构建查询条件。
* @param fieldEnum 字段枚举,包含字段配置信息,用于指定要查询的字段。
* @param highlight 是否高亮处理。
* @return org.elasticsearch.index.query.QueryBuilder 查询构建器,用于构建Elasticsearch的查询语句。
*/
private QueryBuilder generateCondition(String keyword, EsQueryFieldEnum fieldEnum, boolean highlight){
EsQueryFieldConfigDTO fieldConfigDTO = fieldEnum.getFieldConfig();
// 构建基于关键词的基本查询条件
BoolQueryBuilder boolQueryBuilder = EsQueryBuilderUtils.generateFieldQueryBuilder(keyword,
true, fieldConfigDTO.getSearchFieldList());
if(BooleanUtils.isTrue(fieldConfigDTO.getIsNested())){
// 如果是嵌套类型字段,则使用NestedQueryBuilder来处理
NestedQueryBuilder
nestedQueryBuilder = new NestedQueryBuilder(fieldConfigDTO.getMainField(), boolQueryBuilder, ScoreMode.Avg);
if(highlight && isHighlight){
// 如果需要高亮显示,则为嵌套类型字段设置高亮处理
String innerHitName = generateInnerHitName(fieldEnum);
InnerHitBuilder innerHitBuilder = EsHighlightUtils.buildNestHighlightBuilder(innerHitName,
fieldConfigDTO.getSearchFieldList());
nestedQueryBuilderHighlightMap.put(nestedQueryBuilder,innerHitBuilder);
}
return nestedQueryBuilder;
}else{
// 对于非嵌套类型字段,处理高亮显示的逻辑
if(highlight && isHighlight){
// 收集非嵌套类型的高亮字段
noNestedHighlightFields.addAll(fieldConfigDTO.getSearchFieldList());
}
return boolQueryBuilder;
}
}
/**
* 生成嵌套查询的innerHit名称
* @param fieldEnum
* @return java.lang.String
**/
private String generateInnerHitName(EsQueryFieldEnum fieldEnum){
IntAccumulator accumulator = accNestedFieldMap.get(fieldEnum.getFieldConfig().getMainField());
accumulator.accumulate(1);
return fieldEnum.getFieldConfig().getMainField()+"-"+accumulator.getValue();
}
/**
* 向查询构建器中添加公司名称条件。
* @param esQueryBuilder ES查询构建器,用于生成特定的ES查询条件。
* @param keywordQueryBuilder 关键词查询构建器,用于组合不同的查询条件。
* @param keyword 用户输入的关键词,用于匹配公司名称。
*/
public static void addCompanyNameCondition(EsQueryBuilder esQueryBuilder,BoolQueryBuilder keywordQueryBuilder,
String keyword) {
// 根据关键词和字段类型(当前公司名称),生成查询条件,并添加到关键词查询构建器中
keywordQueryBuilder.should(esQueryBuilder.generateCondition(keyword,EsQueryFieldEnum.CURRENT_COMPANY,true));
// 根据关键词和字段类型(履历中的公司名称),生成查询条件,并添加到关键词查询构建器中
keywordQueryBuilder.should(esQueryBuilder.generateCondition(keyword,EsQueryFieldEnum.TRACK_RECORD_COMPANY,true));
}
}
其他相关代码:
定义一个适用Lambda表达式的接口
/**
* Es 搜索条件处理器
*/
@FunctionalInterface
public interface IEsQueryConditionHandler {
/**
* 处理Es搜索条件
* @param esQueryBuilder
* @param keywordQueryBuilder
* @param keyword
* @return void
*/
void handle(EsQueryBuilder esQueryBuilder, BoolQueryBuilder keywordQueryBuilder,
String keyword);
}
定义搜索字段的枚举
/**
* 专家库ES查询字段枚举
*/
public enum EsQueryFieldEnum {
/**
* 当前公司
*/
CURRENT_COMPANY(10,"当前公司", EsQueryFieldConfigDTO.builder()
.mainField("companyInfo")
.isNested(false)
.searchFieldList(List.of("companyInfo.companyName"))
.build()),
/**
* 工作经历公司
*/
TRACK_RECORD_COMPANY(20,"工作经历公司", EsQueryFieldConfigDTO.builder()
.mainField("trackRecordList")
.isNested(true)
.searchFieldList(List.of("trackRecordList.companyName","trackRecordList.companyOtherName"))
.build()),
;
/**
* 嵌套字段列表
*/
private static final List<EsQueryFieldEnum> NESTED_FIELD_LIST = Stream.of(EsQueryFieldEnum.values())
.filter(item->item.fieldConfig.getIsNested()).collect(Collectors.toList());
EsQueryFieldEnum(Integer value, String description,
EsQueryFieldConfigDTO fieldConfig){
this.value =value;
this.description = description;
this.fieldConfig = fieldConfig;
}
private final Integer value;
private final String description;
private final EsQueryFieldConfigDTO fieldConfig;
public Integer value() {
return this.value;
}
public String getDescription() {
return this.description;
}
public EsQueryFieldConfigDTO getFieldConfig() {
return fieldConfig;
}
/**
* 获取嵌套字段列表
*/
public static List<EsQueryFieldEnum> getNestedFieldList() {
return NESTED_FIELD_LIST;
}
}
定义搜索类型的枚举
/**
* ES查询类型枚举
**/
public enum EsQueryTypeEnum {
/**
* 公司
*/
COMPANY(20,"公司", EsQueryBuilder::addCompanyNameCondition),
;
EsQueryTypeEnum(Integer value, String description,
IEsQueryConditionHandler addConditionHandler){
this.value =value;
this.description = description;
this.addConditionHandler = addConditionHandler;
}
private final Integer value;
private final String description;
private final IEsQueryConditionHandler addConditionHandler;
public Integer value() {
return this.value;
}
public String getDescription() {
return this.description;
}
public IEsQueryConditionHandler getAddConditionHandler() {
return addConditionHandler;
}
public static EsQueryTypeEnum resolve(Integer statusCode) {
for (EsQueryTypeEnum status : values()) {
if (status.value.equals(statusCode)) {
return status;
}
}
return null;
}
}
使用方法
SearchSourceBuilder searchSourceBuilder = queryBuilder
// 增加关键字查询条件条件
.addCondition()
// 组合条件过滤
.filterCondition(EsQueryHandler.getAdvancedSearchQueryBuilder(searchParameter))
//生成查询语句
.build();
// 获取总条数
Integer total = EsService.countBySearch(searchSourceBuilder);
//重新生成高亮查询语句
searchSourceBuilder = queryBuilder.buildWithHighlight();
//补充排序规则
EsQueryHandler.setSearchSortRule(searchSourceBuilder,searchParameter.getSortType());
// 从第几页开始
searchSourceBuilder.from(searchParameter.getOffset());
// 每页显示多少条
searchSourceBuilder.size(searchParameter.getLimit());
//分页搜索
List<EsAllInfoDTO> allInfoList = EsService.listByPageSearch(searchSourceBuilder);
问题2解决方案
合并高亮的处理,这个问题实际就是:对于一个字符串a
,存在多个字符串a1
,a2
,a3
,并且a1
,a2
,a3
再过滤掉<em>
和</em>
字符后是相同的字符串。现在需要将字符串a
,a1
,a2
,a3
合并为一个字符串fa
。合并后的字符串需要满足:
1、fa
过滤掉<em>
和</em>
字符后同a
相同
2、所有在a1
,a2
,a3
被<em>
和</em>
包围的子字符串,在fa
同样被<em>
和</em>
包围
另外要保证一个点是原始的字符串a
不能本身就有<em>
或</em>
这些字符串,这个可以通过对数据源头进行过滤就可以了。比如使用Jsonp
过滤。
合并高亮字符串的具体的实现算法如下:
/**
* Es高亮工具类
*/
public class EsHighlightUtils {
public static final String emBegin = "<em>";
public static final String emEnd = "</em>";
private static final String emRegex = "(?i)<em>|</em>";
private static final int emBeginLen = emBegin.length();
private static final int emEndLen = emEnd.length();
/**
* 将字符串数组中的字符串合并,并在特定位置添加增强标签(<em></em>)。
* @param stringList 字符串数组,数组中所有字符串如果去除"<em>" 和"</em>"后必定是相同的字符串。
* @return 合并后的字符串,增强了指定的字符串片段。
*/
public static String mergeStrWithEmTags(List<String> stringList) {
// 移除原始字符串中的所有em标签,获取干净的源字符串
String sourceStr = stringList.get(0).replaceAll(emRegex, "");
// 使用StringBuilder来操作源字符串,以便高效地添加em标签
StringBuilder sourceBuilder = new StringBuilder(sourceStr);
// 初始化一个布尔数组,用于标记哪些字符需要增强
boolean[] emFlags = new boolean[sourceStr.length()];
// 填充布尔数组,标记需要增强的字符位置
fillEmFlags(stringList, emFlags);
// 根据标记,在相应位置添加em标签
addEmFlags(sourceBuilder, emFlags);
return sourceBuilder.toString();
}
/**
* 为给定的字符串数组中的每个字符串设置强调标志数组。
* 该方法会查找每个字符串中所有"<em>"开头和"</em>"结尾的包围结构,
* 并将这些包围结构在原字符串中的对应部分在标志数组中设置为true。
* @param stringList 字符串数组,包含需要处理的字符串。
* @param emFlags 增强标志数组,与字符串数组对应,用于标记特定部分。
*/
private static void fillEmFlags(List<String> stringList, boolean[] emFlags) {
// 遍历字符串数组,为每个字符串设置强调标志
for(int j = 0; j< stringList.size(); j++){
String str = stringList.get(j);
// 查找每个字符串中"<em>"的起始位置
int beginIndex = str.indexOf(emBegin);
int cumulativeOffset = 0;
int noEmLen = 0;
int endIndex = 0;
while(beginIndex != -1){
//计算没有增强的字符串长度
noEmLen = endIndex>0?Math.max(beginIndex - (endIndex + emEndLen),0):beginIndex;
// 查找"<em>"后的"</em>"位置
endIndex = str.indexOf(emEnd,beginIndex+emBeginLen);
if(endIndex==-1){
// 如果找不到结束标签,则跳出循环
break;
}
// 计算被包围的子字符串长度
int emSubLength = endIndex - beginIndex - emBeginLen;
// 更新累计偏移量,跳过未增强的字符串
cumulativeOffset = cumulativeOffset+ noEmLen;
// 将被包围的子字符串在标志数组中对应的元素设置为true
for(int i=0;i<emSubLength;i++){
emFlags[cumulativeOffset + i] = true;
}
// 更新累计偏移量,为处理下一个"<em>"做准备
cumulativeOffset = cumulativeOffset + emSubLength;
// 计算下一个"<em>"标签的起始位置
beginIndex = endIndex + emEndLen;
// 继续查找下一个"<em>"
beginIndex = str.indexOf(emBegin,beginIndex);
}
}
}
/**
* 向源字符串中插入增强标签。
* 根据给定的增强标志数组(emFlags),在源字符串(sourceBuilder)中插入开始(emBegin)和结束(emEnd)标签。
* 当emFlags中的元素为true时,表示字符串的这个位置需要被增强
* @param sourceBuilder 被插入标签的源字符串的StringBuilder对象。
* @param emFlags 增强标志数组,true表示字符串的这个位置需要被增强。
*/
private static void addEmFlags(StringBuilder sourceBuilder, boolean[] emFlags) {
// 初始化是否开始插入标签的标志和累计偏移量
boolean startEm = false;
int cumulativeOffset = 0 ;
// 遍历增强标志数组,根据标志插入相应的标签
for (boolean emFlag : emFlags) {
if (emFlag) {
// 当前位置需要插入开始标签
if (!startEm) {
// 第一次需要插入开始标签,进行插入操作并更新累计偏移量
startEm = true;
sourceBuilder.insert(cumulativeOffset, emBegin);
cumulativeOffset += emBeginLen;
}
// 无论是否第一次,只要需要插入开始标签,累计偏移量就需要增加
cumulativeOffset++;
} else {
// 当前位置需要插入结束标签
if (startEm) {
// 已经开始插入标签,进行插入操作并更新累计偏移量
sourceBuilder.insert(cumulativeOffset, emEnd);
cumulativeOffset += emEndLen;
}
// 标记不再插入开始标签
startEm = false;
// 累计偏移量增加
cumulativeOffset++;
}
}
// 如果遍历结束时正在插入开始标签,插入结束标签
if(startEm){
sourceBuilder.insert(cumulativeOffset,emEnd);
}
}
/**
* 构建嵌套的高亮 InnerHitBuilder
* @param name
* @param fields
* @return org.elasticsearch.index.query.InnerHitBuilder
*/
public static InnerHitBuilder buildNestHighlightBuilder(String name, Collection<String> fields) {
if(CollectionUtils.isEmpty(fields)){
return null;
}
InnerHitBuilder innerHitBuilder = StringUtils.isBlank(name)?new InnerHitBuilder():new InnerHitBuilder(name);
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.preTags(emBegin).postTags(emEnd);
//设置高亮的方法
highlightBuilder.highlighterType("plain");
//设置分段的数量不做限制
highlightBuilder.numOfFragments(0);
for(String field:fields){
highlightBuilder.field(field);
}
innerHitBuilder.setHighlightBuilder(highlightBuilder);
return innerHitBuilder;
}
/**
* 构建非嵌套的高亮 HighlightBuilder
* @param fields
* @return org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder
*/
public static HighlightBuilder buildNotNestHighlightBuilder(Collection<String> fields) {
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.preTags(emBegin).postTags(emEnd);
//设置高亮的方法
highlightBuilder.highlighterType("plain");
//设置分段的数量不做限制
highlightBuilder.numOfFragments(0);
for(String field:fields){
highlightBuilder.field(field);
}
return highlightBuilder;
}
}
修改https://blog.csdn.net/weixin_48990070/article/details/120342597 这篇笔记中的替换高亮处理的代码,思路为每次只合并找到的第一个高亮内容,将它和当前的原始内容合并,并将合并后的内容替换掉原始内容。重复这个动作知道所有高亮的内容都被合并到当前的原始内容中。
/**
* 替换嵌套高亮的值
* @param sourceObj
* @param nestedEle
* @param highlightEle
* @return void
*/
private void replaceInnerHighlightValue(JsonObject sourceObj, JsonElement nestedEle, JsonElement highlightEle){
if(nestedEle==null || highlightEle==null){
return ;
}
//获取源对象中的嵌套字段名称
JsonObject nestedObj= nestedEle.getAsJsonObject();
String innerFieldName = nestedObj.get("field").getAsString();
//获取当前对象匹配的源对象中的偏移位置
int innerFieldOffset = nestedObj.get("offset").getAsInt();
//获取源对象
JsonObject findSourceObj = GsonUtils.getJsonObjectForArray(sourceObj,innerFieldName,innerFieldOffset);
if(findSourceObj==null){
return ;
}
//替换高亮的部分
log.debug("高亮的部分:{}",highlightEle);
JsonObject highlightObj = highlightEle.getAsJsonObject();
highlightObj.entrySet().forEach((h)->{
//合并高亮字段对应的原值
String highlightValue = h.getValue().getAsString();
JsonObject currentSourceObj = findSourceObj;
String[] keyNames = StringUtils.split(h.getKey(),".");
//循环到倒数第二层,获取待替换字段值对象
for(int i=0;i<keyNames.length-2;i++){
String keyName = keyNames[i+1];
currentSourceObj = currentSourceObj.get(keyName).getAsJsonObject();
}
//获取最后一层的字段名称
String lastFieldName = keyNames[keyNames.length-1];
//获取高亮字段对应的原值
String sourceValue = currentSourceObj.get(lastFieldName).getAsString();
//合并原值和高亮增强的值
String mergedValue = EsHighlightUtils.mergeStrWithEmTags(List.of(sourceValue, highlightValue));
//替换最后一层对象的指定字段的值
GsonUtils.replaceFieldValue(currentSourceObj,
lastFieldName, mergedValue);
});
log.debug("替换后的高亮的部分{}",findSourceObj);
}
原文地址:https://blog.csdn.net/weixin_48990070/article/details/137790211
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!