如何实现 Es 全文检索、高亮文本略缩处理

如何实现 Es 全文检索、高亮文本略缩处理

    • 前言
    • 技术选型
    • [JAVA 常用语法说明](#JAVA 常用语法说明)
    • 全文检索开发
    • 高亮开发
    • [Es Map 转对象使用](#Es Map 转对象使用)
    • [核心代码 Trans 接口(支持父类属性的复杂映射)](#核心代码 Trans 接口(支持父类属性的复杂映射))
    • [Trans 接口的不足](#Trans 接口的不足)
    • 真实项目落地效果

前言

最近手上在做 Es 全文检索的需求,类似于百度那种,根据关键字检索出对应的文章,然后高亮显示,特此记录一下,其实主要就是处理 Es 数据那块复杂,涉及到高亮文本替换以及高亮字段截取,还有要考虑到代码的复用性,是否可以将转换代码抽离出来,提供给不同结构的索引来使用。

技术选型

像市面上有的 Spring Data,码云上面的 GVP 项目 (EasyEs)等其他封装框架。使用起来确实很方便,但是考虑到由于开源项目的不稳定性且 Es 不同版本间语法差异比较大,决定使用原生的 Api。也就是使用 RestHighLevelClient。

JAVA 常用语法说明

查时间范围内的数据 BoolQuery 里面嵌套一个 RangeQuery 即可在RangeQuery 里面指定时间范围。BoolQuery.must() 各位理解为 Mybatis 中的 eq 方法即可,必须包含的意思。

javascript 复制代码
   RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery(articleRequest.getSortType());
        if (StringUtils.isNotEmpty(articleRequest.getBeginTime())) {
            rangeQuery.gte(articleRequest.getBeginTime());
        }
        if (StringUtils.isNotEmpty(articleRequest.getEndTime())) {
            rangeQuery.lte(articleRequest.getEndTime());
        }
        boolQuery.must(rangeQuery);

BoolQuery.should() 方法可以理解为 OR 可包含可不包含,多字段全文检索时应用 shoud。

javascript 复制代码
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
boolQuery.should(QueryBuilders.matchPhraseQuery(articleRequest.getKeys()[i], articleRequest.getKeyword()));

termsQuery 字符精确匹配

javascript 复制代码
QueryBuilders.termsQuery()

字符短句匹配,字符不会进行分词

javascript 复制代码
QueryBuilders.matchPhraseQuery()

分词匹配

javascript 复制代码
QueryBuilders.multiMatchQuery()

全文检索开发

核心代码如下

javascript 复制代码
    BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
    if (StringUtils.isNotEmpty(articleRequest.getKeyword())) {
            for (int i = 0; i < articleRequest.getKeys().length; i++) {
                //根据短句匹配
                boolQuery.should(QueryBuilders.matchPhraseQuery(articleRequest.getKeys()[i], articleRequest.getKeyword()));
            }
        }

高亮开发

里面可以指定高亮的字段,以及高亮前缀,尾缀,API的调用,直接 copy 就行

javascript 复制代码
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder()
                .highlighter(new HighlightBuilder()
                        .requireFieldMatch(false)
                        .field("author")
                        .field("title")
                        .field("body")
                        .field("attachments.filename")
                        .preTags(EsConstant.HIGHT_PREFIX)
                        .postTags(EsConstant.HIGHT_END)
                        .fragmentSize(800000)//下面这两项,如果你要高亮如文字内容等有很多字的字段,必须配置,不然会导致高亮不全,文章内容缺失等;
                        .numOfFragments(0))
                .query(boolQuery)
                .from(articleRequest.getPage() - 1)
                .size(articleRequest.getSize())

Es Map 转对象使用

由于索引结构是已 ArticleResponse 格式存储的,查询的时候也需将的得到 SourceAsMap 转换成 ArticleResponse 格式,核心逻辑我都封装到 Trans 接口了。利用反射实现的,当然也可以用其他技术实现,例如 MapStruct 在编译期间就自动生成对应的 get、set 方法,比反射效率高点,毕竟反射是运行期间的属性映射!!!!

javascript 复制代码
 SearchHits hits = restHighLevelClient.search(
                    new SearchRequest().indices(indexname).source(searchSourceBuilder)).getHits();
            for (SearchHit hit : hits) {
                result.add(new ArticleResponse().trans(hit.getSourceAsMap(),
                        hit.getHighlightFields(),
                        Collections.singletonList("attachments.filename")));

使用的话只需让 ArticleResponse 类实现 Trans 接口,即可调用里面的 trans 方法。

核心代码 Trans 接口(支持父类属性的复杂映射)

主要逻辑就是挨个拿到本身、然后递归获取父类的所有字段名称、字段类型放到一个 Map(nameTypeMap) 中,然后遍历 SourceAsMap 挨个进行字段类型匹配校验,如果是 String 类型直接进行反射填充属性。

非 String 类型,进行类型转换然后再进行属性填充。

以及高亮字段文本略缩的处理,主要就是用了下 Jsoup 中去除 Html 标签的 Api,本来想着让前端自己去找插件看能不能处理下的,无奈说处理不了,想了个取巧的方法,高亮标签我用特殊字符,然后去除所有的 html 标签后,我的特殊字符还存在,之后将特殊字符再次替换回高亮 Html 标签,这样就得到了只存在我自定义高亮 Html 标签的一段文本了,同时高亮标签里面我塞了一个 id,之后根据高亮标签中的 id 截取字符即可,即可实现文本略缩的效果,同事直呼秒啊哈哈哈哈


javascript 复制代码
/**
 * map 转对象
 * author:zzh
 */
public interface Trans<T> {
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    Class getTargetClass();

    /**
     * 逻辑写的太多了,可以搞几个抽象类抽分功能
     * @param SourceAsMap           原始数据
     * @param highlightFieldsSource 高亮数据
     * @param highLightFields       高亮字段
     */
    default Object trans(Map<String, Object> SourceAsMap, Map<String, HighlightField> highlightFieldsSource, List<String> highLightFields) throws IntrospectionException, InstantiationException, IllegalAccessException {
        Object o = getTargetClass().newInstance();
        Class tclass = getTargetClass();
        HashMap<String, Class> nameTypeMap = new HashMap<>();
        //找到父类的所有字段
        do {
            Arrays.stream(tclass.getDeclaredFields()).forEach(field -> {
                field.setAccessible(true);
                //key:字段名称,value:字段类型
                nameTypeMap.put(field.getName(), field.getType());
            });
            tclass = tclass.getSuperclass();
        } while (!tclass.equals(Object.class));
        PropertyDescriptor[] propertyDescriptors = Introspector.getBeanInfo(o.getClass()).getPropertyDescriptors();
        Arrays.stream(propertyDescriptors).forEach(propertyDescriptor -> {
            if (!"targetClass".equals(propertyDescriptor.getName()) && !Objects.isNull(SourceAsMap.get(propertyDescriptor.getName()))) {
                try {
                    Method writeMethod = propertyDescriptor.getWriteMethod();
                    if (null != writeMethod) {
                        if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
                            writeMethod.setAccessible(true);
                        }
                        Object sourceValue = SourceAsMap.get(propertyDescriptor.getName());
                        //父类以及自己所有字段类型
                        Class aClass = nameTypeMap.get(propertyDescriptor.getName());
                        //String 类型以及高亮直接赋值
                        if (sourceValue.getClass().equals(aClass)) {
                            HighlightField highlightObject = highlightFieldsSource.get(propertyDescriptor.getName());
                            //如果高亮字段是 body,为了避免高亮文本处于文章末尾搜索页显示不到的问题,因此采用截取字符串将高亮字段偏移至前面
                            if ("body".equals(propertyDescriptor.getName()) && null != highlightObject) {
                                String highlightString = highlightObject.getFragments()[0].toString();
                                //去除所有 html 标签,并将自定义高亮前缀替换 span 标签,这样就实现了只保留高亮标签的目的了
                                highlightString = Jsoup.parse(highlightString).body().text()
                                        .replaceAll(EsConstant.HIGHT_PREFIX, EsConstant.HIGHT_PREFIX_HTML)
                                        .replaceAll(EsConstant.HIGHT_END, EsConstant.HIGHT_END_HTML);
                                //高亮字段前 50 个字到文章末尾
                                highlightString = highlightString.substring((highlightString.indexOf(EsConstant.HIGHT_HTML_ID) - EsConstant.HIGHT_SIZE) < 0
                                        ? 0 : (highlightString.indexOf(EsConstant.HIGHT_HTML_ID) -  EsConstant.HIGHT_SIZE));
                                writeMethod.invoke(o, highlightObject != null ? highlightString : SourceAsMap.get(propertyDescriptor.getName()));
                            } else {
                                //非 body 的其他高亮字段正常替换高亮文本
                                writeMethod.invoke(o, highlightObject != null ? highlightObject.getFragments()[0].toString()
                                        .replaceAll(EsConstant.HIGHT_PREFIX, EsConstant.HIGHT_PREFIX_HTML)
                                        .replaceAll(EsConstant.HIGHT_END, EsConstant.HIGHT_END_HTML) : SourceAsMap.get(propertyDescriptor.getName()));

                            }

                        }
                        /**
                         * 类型不一致强转,这里可以搞个策略模式优化优化
                         */
                        else {
                            if (aClass.equals(Date.class)) {
                                Date parse = simpleDateFormat.parse(String.valueOf(SourceAsMap.get(propertyDescriptor.getName())));
                                writeMethod.invoke(o, parse);
                            }
                            if (aClass.equals(Integer.class)) {
                                writeMethod.invoke(o, Integer.valueOf(String.valueOf(SourceAsMap.get(propertyDescriptor.getName()))));
                            }
                            if (aClass.equals(Long.class)) {
                                writeMethod.invoke(o, Long.valueOf(String.valueOf(SourceAsMap.get(propertyDescriptor.getName()))));
                            }
                            if (aClass.equals(List.class)) {
                                ArrayList<Map<String, Object>> oraginSources = (ArrayList<Map<String, Object>>) SourceAsMap.get(propertyDescriptor.getName());
                                //复杂对象高亮字段映射
                                if (null != oraginSources && 0 != highlightFieldsSource.size()) {
                                    for (int i = 0; i < oraginSources.size(); i++) {
                                        for (int j = 0; j < highLightFields.size(); j++) {
                                            try {
                                                if (highlightFieldsSource.containsKey(highLightFields.get(j))) {
                                                    oraginSources.get(i).put(highLightFields.get(j).split("\\.")[1],
                                                            highlightFieldsSource.get(highLightFields.get(j)).getFragments()[j].toString()
                                                                    .replaceAll(EsConstant.HIGHT_PREFIX, EsConstant.HIGHT_PREFIX_HTML)
                                                                    .replaceAll(EsConstant.HIGHT_END, EsConstant.HIGHT_END_HTML));
                                                }
                                            } catch (Exception e) {
                                                e.printStackTrace();
                                            }
                                        }
                                    }
                                }
                                writeMethod.invoke(o, oraginSources);
                            }
                            if (aClass.equals(int.class)) {
                                writeMethod.invoke(o, Integer.parseInt(String.valueOf(SourceAsMap.get(propertyDescriptor.getName()))));
                            }
                        }
                    } else throw new RuntimeException(propertyDescriptor.getName() + "~ writeMethod is null!!!!!");
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        return o;
    }

}

Trans 接口的不足

追求极至代码解耦的人,里面的类型转换的代码可以搞个策略模式优化优化。Trans 接口定义一个就好,可以搞几个抽象类,譬如专门处理高亮文本的抽象类、不涉及到嵌套对象的抽象类转换、以及通用转换抽象类出来,写多了感觉自己再写源码了,那些搞开源项目的还有一些主流框架的源码不都是这么干的,但是需要花费一定的精力去写,笔者比较懒,下完班只想回家美美的打打游戏,追追剧,就点到为止了。

真实项目落地效果

复杂对象高亮字段替换效果

相关推荐
码爸3 分钟前
hbase merge工具
大数据·数据库·hbase
hong_zc5 分钟前
JDBC 编程
java·数据库·mysql
Flying_Fish_roe6 分钟前
MyBatis-Plus 常见问题与优化
java·tomcat·mybatis
X² 编程说10 分钟前
14.面试算法-字符串常见算法题(三)
java·数据结构·后端·算法·面试
Qspace丨轻空间23 分钟前
科技引领未来生活——“光影漫游者”展览馆应用—轻空间
大数据·人工智能·科技·娱乐
imc.1142 分钟前
初识linux(2)
java·linux·数据库
武子康43 分钟前
大数据-143 - ClickHouse 集群 SQL 超详细实践记录!
java·大数据·数据库·分布式·sql·clickhouse·flink
巧手打字通44 分钟前
解锁Java线程池:实战技巧与陷阱规避
java·性能优化·线程池
装不满的克莱因瓶1 小时前
【微服务】Eureka的自我保护机制
java·spring cloud·云原生·eureka·注册中心·服务注册
虫本初阳1 小时前
【Java】接口interface【主线学习笔记】
java·笔记·学习