如何实现 Es 全文检索、高亮文本略缩处理(封装工具接口极致解耦)

news2024/12/24 17:04:47

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

    • 前言
    • 技术选型
    • JAVA 常用语法说明
    • 全文检索开发
    • 高亮开发
    • Es Map 转对象使用
    • 核心代码 Trans 接口(支持父类属性的复杂映射)
    • Trans 接口可优化的点
    • 高亮全局配置类如下
    • 真实项目落地效果
    • 为什么不用 numOfFragments、fragmentSize 参数控制略缩?
    • 结语

前言

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

技术选型

像市面上有的 Spring Data,码云上面的 GVP 项目 (EasyEs)等其他封装框架。使用起来确实很方便,但是考虑到由于开源项目的不稳定性且 Es 不同版本间语法差异比较大,还有一方面是公司之前用的一直是 Es 6,后续可能会涉及到 Es 的升级改造,于是决定使用原生的 Api。也就是使用 RestHighLevelClient。

JAVA 常用语法说明

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

   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。

BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
boolQuery.should(QueryBuilders.matchPhraseQuery(articleRequest.getKeys()[i], articleRequest.getKeyword()));

termsQuery 字符精确匹配

QueryBuilders.termsQuery()

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

QueryBuilders.matchPhraseQuery()

分词匹配

QueryBuilders.multiMatchQuery()

分词匹配加高亮对应 EQ(Es 的Sql,我自己给他取的名字!!!!)

GET /articlezzh/_doc/_search
{
  "from": 0,
  "size": 20,
  "query": {
    "bool": {
      "must": [
        {
          "multi_match": {
            "query": "你的购物节",
            "fields": [
              "title","author","body"
            ]
          }
        }
      ]
    }
  },
  "highlight": {
    "pre_tags": [
      "<em style='color: red'>"
    ],
    "post_tags": [
      "</em>"
    ],
    "fields": {
      "body":{},
      "author":{}
    }
  }
}

全文检索开发

核心代码如下

    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 就行。

        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)
                        //noMatchSize
                        //返回全部内容,方便后续的截取字符串操作
                        .fragmentSize(800000)
                        .numOfFragments(0))
                //过滤数据(最少满足一个 should 条件的数据才会被展示,否则过滤)
                .query(boolQuery.minimumShouldMatch(1))
                .from(articleRequest.getPage() - 1)
                .size(articleRequest.getSize());

Es Map 转对象使用

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

 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 截取字符即可,即可实现文本略缩的效果,同事直呼秒啊哈哈哈哈

在这里插入图片描述

/**
 * 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 (EsConstant.HIGHT_FILED.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 if (EsConstant.HIGHT_FILED.equals(propertyDescriptor.getName()) && null == highlightObject) {
                                //非高亮的 body 字段,也去除下 Html 标签
                                writeMethod.invoke(o, Jsoup.parse(String.valueOf(SourceAsMap.get(propertyDescriptor.getName()))).body().text());
                            } 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)) {
                                //获取指定属性的 List
                                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 接口可优化的点

  1. 优化点一:策略模式扩展多数据类型转换那部分的代码,这里就不贴出来了。
  2. 优化点二:支持多高亮字段略缩处理,也就是将这行代码改成 List 集合判断,但是我感觉没必要,一个字段略缩就够用了。
EsConstant.HIGHT_FILED.equals(propertyDescriptor.getName())
  1. 配置文件也可以用策略模式,或者利用 Spi 机制动态加载。(针对项目要开源的情况下说的),这块的内容可以看我以往写过的 Spi 文章。
  2. 可以搞个文章内容里面涉及到关键字的地方就略缩一下,举个例子,查(地瓜)的时候,返回的数据是这样的( …地瓜…地瓜),而现在的效果是(地瓜…),希望别让我改!!!!我觉得我现在这样也够用了。
  3. 代码逻辑再次解耦,可以利用抽象类的性质,按照功能细分职责,毕竟很多框架源码就是这么干的,一堆的抽象类封装通用逻辑

高亮全局配置类如下

为了方便后期维护,将用到的配置封装了一下,大家可自行替换用 Nacos 配置中心也好,还是用枚举类也好,修改一下代码即可

public class EsConstant {
    //高亮前缀唯一 id,可自行定义
    public static String HIGHT_PREFIX = "zzhSatat";
    //高亮尾缀唯一 id,可自行定义
    public static String HIGHT_END = "zzhEnd";
    //高亮尾缀
    public static String HIGHT_END_HTML = "</span>";
    //高亮标签 id,可自行定义
    public static String HIGHT_HTML_ID = "zzh";
    //截取高亮字段前字符串长度
    public static int HIGHT_SIZE = 50;
    //高亮前缀
    public static String HIGHT_PREFIX_HTML = "<span style='color:red',id='" + HIGHT_HTML_ID + "'>";
    //略缩字段
    public static String HIGHT_FILED = "body";
}

真实项目落地效果

在这里插入图片描述
复杂对象高亮字段替换效果在这里插入图片描述

为什么不用 numOfFragments、fragmentSize 参数控制略缩?

数据库中的文章内容直接存的 Html 页面,用这俩参数截取字符串的话,截取到的文本会含残缺的 Html 标签,效果直接 Pass,当然对于纯文本类型的字段可以用这俩个参数进行控制,不用写截取字符串的逻辑!

结语

🌸🌸勿忘初心,鼎力前行🌸🌸

🌸🌸如果您觉得文章对您有帮助的话
🌸🌸不妨点个免费的赞或者关注
🌸🌸这将成为我前进的最大动力

🌸🌸微信公众号刚刚起步,后续创作更多精品内容提供给大家
在这里插入图片描述

🌸🌸有任何技术问题,欢迎加我微信交流
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1098655.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据结构与算法课后题-第五章(哈夫曼树和哈夫曼编码)

文章目录 选择题1选择题2选择题3选择题4选择题5选择题6选择题7应用题7 选择题1 选择题2 选择题3 需要深究 选择题4 选择题5 选择题6 选择题7 应用题7

【LeetCode刷题(数组and排序)】:存在重复元素

给你一个整数数组 nums 。如果任一值在数组中出现 至少两次 &#xff0c;返回 true &#xff1b;如果数组中每个元素互不相同&#xff0c;返回 false 示例 1&#xff1a; 输入&#xff1a;nums [1,2,3,1] 输出&#xff1a;true 示例 2&#xff1a; 输入&#xff1a;nums [1,2…

framework通信机制—LiveData使用方法及原理

LiveData是一种可观察的数据存储器类。与常规的可观察类不同&#xff0c;LiveData 具有生命周期感知能力&#xff0c;意指它遵循其他应用组件&#xff08;如 activity、fragment 或 service&#xff09;的生命周期。这种感知能力可确保 LiveData 仅更新处于活跃生命周期状态的应…

自我介绍思考

1.引导面试官有重点的看你简历 2.在引导部分暗示他我是最适合这个岗位的 面试官在考察什么&#xff1f; a.你的表述是否一致b.考察你的语言表达能力&#xff0c;逻辑思维能力&#xff0c;总结概括能力c.考察你对现场的把控能力d.对时间的把控能力 怎么做&#xff1f; 1.写逐…

uniapp中全局页面挂载组件(H5)

前言 我们已经学习了 uniapp中全局页面挂载组件&#xff08;小程序&#xff09; 有些小伙伴问在H5怎么做那让我们试一试 直接上代码 //引用组件 import dialog from ./index.vue; //我这里要把小程序的方法和h5方法写一起所以用了混入 import mixins from ./mixins.js //使用…

OJ项目【登录】——验证码、失败登录多次账户冻结、用户密码加密,我是如何实现的?

目录 前言 1、验证码 1.1、引入pom 1.2、前端核心代码 1.3、后端核心代码 2、账户冻结 2.1、思路&#xff1a; 2.2、核心代码示例&#xff1a; 3、密码加密——加盐算法 3.1、思路&#xff1a; 3.2、代码实现示例&#xff1a; 4、小结&#xff1a;展示我的项目 4…

[牛客习题]“幸运的袋子”

习题链接&#xff1a;幸运的袋子_牛客题霸_牛客网 题目分析 由题意可知&#xff1a;“幸运的袋子”的概念是——小球的数值之和大于小球的数值之积。 假如现在有5个小球&#xff1a;1&#xff0c;1&#xff0c;3&#xff0c;5&#xff0c;7&#xff0c;并将他们编号a0~a4.我们…

Vue项目路由加前缀

Vue项目路由加前缀 vue-cli3.0配置 1&#xff09;静态资源前缀 vue.config.js /module.exports 配置 publicPath&#xff1a;“/前缀” 2&#xff09;路由前缀 route/index.js export default new Router({ base:”/前缀" , }) 参考文章&#xff1a;https://blog.csd…

【Java学习之道】网络编程的基本概念

引言 这一章我们将一同进入网络编程的世界。在开始学习网络编程之前&#xff0c;我们需要先了解一些基本概念。那么&#xff0c;我们就从“什么是网络编程”这个问题开始吧。 一、网络编程的基本概念 1.1 什么是网络编程 网络编程&#xff0c;顾名思义&#xff0c;就是利用…

netca_crypto.dll找不到怎么修复?详细解决办法和注意事项

当你在使用计算机时&#xff0c;突然出现了一个错误提示&#xff1a;“netca_crypto.dll 找不到”。不知道该如何解决这个问题&#xff1f;其实要解决是非常的简单的&#xff0c;今天我们将为你提供几种修复 netca_crypto.dll 找不到的解决方法和一些注意事项。在深入探讨修复方…

普通螺纹基本牙型尺寸及拧紧力矩.exe

一、概要 本软件功能主要是通过输入螺纹原始三角形高度P,螺栓规格(公称直径)d,材料的屈服应力σs,计算出公称应力截面积As、外螺纹小径d1、外螺纹小径d2、拧紧力矩T等参数。 开发本软件的原因主要有以下几点: 提高设计效率:通过这款软件,工程师可以快速计算螺纹的基本牙…

转行网络安全是否可行?

一、前言 其实很多的IT大佬之前也不是专门学计算机的&#xff0c;都是后期转行的。而且大学学什么专业&#xff0c;对后期的工作真的没有太大关系&#xff0c;这也是现在高校的教育现状。有80%的学生都是通过临时抱佛脚&#xff0c;考前冲刺拿到毕业证书的。下面就带大家详细分…

2023年淘宝天猫双11红包领取活时间什么时候开始领天猫淘宝双十一红包优惠券?

2023年淘宝天猫双11红包领取活动开始与结束时间 2023年10月24日20:00开始领取至11月11日23:59结束&#xff1b; 2023年淘宝天猫双11红包活动使用开始与结束时间 第一波&#xff1a;2023年10月31日20:00开始使用至11月3日23:59 第二波&#xff1a;2023年11月10日20:00开始使用…

服务安全-应用协议rsync未授权ssh漏洞复现

目录 服务攻防-应用协议rsync&ssh漏洞复现漏洞复现配置不当-未授权访问-rsync文件备份OpenSSH 用户名枚举漏洞libssh身份验证绕过漏洞 服务攻防-应用协议rsync&ssh漏洞复现 漏洞复现 配置不当-未授权访问-rsync文件备份 rsync默认端口&#xff1a;873 rsync是Linux下…

代码随想录Day20 回溯算法 LeetCode77 组合问题

以下内容更详细解释来自于:代码随想录 (programmercarl.com) 1.回溯算法理论基础 回溯法也叫回溯搜索法,是搜索法的一种,我们之前在二叉树中也经常使用到回溯来解决问题,其实有递归就有回溯,有的时候回溯隐藏在递归之下,我们不容易发觉,今天我们来详细介绍一下什么是回溯,它能…

深圳寄包裹到德国

深圳&#xff0c;作为全球最发达的城市之一&#xff0c;以其高效的物流服务在全球范围内享有盛名。如果你正在寻找一种方式将包裹从深圳寄送到德国&#xff0c;那么本文将为你提供详细的步骤和建议。 第一步&#xff1a;了解国际邮寄的基本信息 首先&#xff0c;你需要了解包裹…

10.16课上

煎饼排序 第一步找剩余数组里的最大值&#xff0c;然后从头到这里翻转一次&#xff0c;这样最大值就到了开头&#xff0c;再把开头从当前结尾翻转一次&#xff0c;就把当前的最大值翻转到了最后 class Solution { public:vector<int> pancakeSort(vector<int>&am…

吃瓜教程-模型的评估与选择

在训练集上的误差称为训练误差&#xff08;training error&#xff09;或经验误差&#xff08;empirical error&#xff09;。在测试集上的误差称为测试误差&#xff08;test error&#xff09;。学习器在所有新样本上的误差称为泛化误差&#xff08;generalization error&…

Apipost一键压测已支持导入CSV文件

最近更新中Apipost对UI页面进行了一些调整&#xff0c;另外一键压测功能支持参数化&#xff01;本篇文章将详细介绍这些改动&#xff01; API调试页面的细节改动 在请求区填入请求参数或脚本时会有相应的标识 如在Query中填入多个参数时上方会展示数量 在预、后执行脚本中写…

Vue3.0 项目结构及组件

main.js文件 // vue中main.js的作用 // main.js是项目的入口文件&#xff0c;项目中所有的页面都会加载main.js,所以main.js,主要有三个作用&#xff1a; // 1.实例化Vue。 // 2.放置项目中经常会用到的插件和CSS样式。例如&#xff1a; 网络请求插件:axios和vue-resource、图…