徐州网站建设公司哪家好,免费建站,商丘网红宋飞,东莞百姓网免费发布信息网ElasticSearch nested 字段多关键字搜索#xff0c;高亮全部匹配关键字的处理
环境介绍
ElasticSearch 版本号: 6.7.0
需求说明
用户会传入多个关键字去ES查询ElasticSearch nested 字段 的多个字段#xff0c;要求在返回的结果中被搜索的字段需要高亮所有匹配的关键字。…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 MapString, IntAccumulator accNestedFieldMap new HashMap();// 无需嵌套高亮的字段集合private SetString noNestedHighlightFields new HashSet();// 关键词分组列表private ListPageSearchKeywordGroupParameter keywordGroupList ;// 是否开启高亮显示private boolean isHighlight false;// 主查询构建器private BoolQueryBuilder mainQueryBuilder;//存储嵌套字段及其高亮构建器private MapNestedQueryBuilder,InnerHitBuilder nestedQueryBuilderHighlightMap new HashMap();/*** 构造方法* param keywordGroupList 搜索关键字组* param isHighlight 是否高亮*/public EsQueryBuilder(ListPageSearchKeywordGroupParameter 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来处理NestedQueryBuildernestedQueryBuilder 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 ListEsQueryFieldEnum 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 ListEsQueryFieldEnum 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());
//分页搜索
ListEsAllInfoDTO 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(ListString 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(ListString 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 endIndex0?Math.max(beginIndex - (endIndex emEndLen),0):beginIndex;// 查找em后的/em位置endIndex str.indexOf(emEnd,beginIndexemBeginLen);if(endIndex-1){// 如果找不到结束标签则跳出循环break;}// 计算被包围的子字符串长度int emSubLength endIndex - beginIndex - emBeginLen;// 更新累计偏移量,跳过未增强的字符串cumulativeOffset cumulativeOffset noEmLen;// 将被包围的子字符串在标志数组中对应的元素设置为truefor(int i0;iemSubLength;i){emFlags[cumulativeOffset i] true;}// 更新累计偏移量为处理下一个em做准备cumulativeOffset cumulativeOffset emSubLength;// 计算下一个em标签的起始位置beginIndex endIndex emEndLen;// 继续查找下一个embeginIndex 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, CollectionString 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(CollectionString 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(nestedElenull || highlightElenull){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(findSourceObjnull){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 i0;ikeyNames.length-2;i){String keyName keyNames[i1];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);}