• 从零搭建 ES 搜索服务(五)搜索结果高亮


    一、前言

    在实际使用中搜索结果中的关键词前端通常会以特殊形式展示,比如标记为红色使人一目了然。我们可以通过 ES 提供的高亮功能实现此效果。


    二、代码实现

    前文查询是通过一个继承 ElasticsearchRepository 的接口实现的,但是如果要实现高亮,这种方式就满足不了了,这里我们需要通过 ElasticsearchTemplate 来完成。

    2.1 注入 ElasticsearchTemplate

    ① ElasticsearchTemplate 类简介

    public class ElasticsearchTemplate implements ElasticsearchOperations, ApplicationContextAware {
        ...省略其余部分...
    }
    

    从上述源码中可以看到 ElasticsearchTemplate 实现了 ApplicationContextAware 接口,表明这个类是被 Spring 管理的,可以直接注入使用。

    ② 业务实现类注入 ElasticsearchTemplate

    @Autowired
    private ElasticsearchTemplate elasticsearchTemplate;
    

    2.2 查询对象指定高亮字段

    在构建查询对象时需要指定高亮字段,通过 withHighlightFields 方法设置。

    private SearchQuery getKnowledgeSearchQuery(KnowledgeSearchParam param) {
        Pageable pageable = PageRequest.of(param.getStart() / param.getSize(), param.getSize());
        String knowledgeTitleFieldName = "knowledgeTitle";
        String knowledgeContentFieldName = "knowledgeContent";
        String preTags = "<span style="color:#F56C6C">";
        String postTags = "</span>";
        HighlightBuilder.Field knowledgeTitleField = new HighlightBuilder.Field(knowledgeTitleFieldName).preTags(preTags).postTags(postTags);
        HighlightBuilder.Field knowledgeContentField = new HighlightBuilder.Field(knowledgeContentFieldName).preTags(preTags).postTags(postTags);
        BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
        queryBuilder.must(QueryBuilders.termQuery("isDeleted", IsDeletedEnum.NO.getKey()));
        queryBuilder.should(QueryBuilders.matchQuery(knowledgeTitleFieldName, param.getKeyword()));
        queryBuilder.should(QueryBuilders.matchQuery(knowledgeContentFieldName, param.getKeyword()));
        return new NativeSearchQueryBuilder()
                .withPageable(pageable)
                .withQuery(queryBuilder)
                .withHighlightFields(knowledgeTitleField, knowledgeContentField)
                .build();
    }
    

    2.3 自定义 ResultMapper

    ResultMapper 是用于将 ES 文档转换成 Java 对象的映射类,因为 Spring Data Elasticsearch 默认的的映射类 DefaultResultMapper 不支持高亮,因此,我们需要自定义一个 ResultMapper 。

    完整代码如下:

    @Slf4j
    @Component
    public class HighlightResultHelper implements SearchResultMapper {
    
        private static ObjectMapper objectMapper = new ObjectMapper();
    
        static {
            objectMapper.setVisibility(JsonMethod.FIELD, JsonAutoDetect.Visibility.ANY);
            objectMapper.configure(SerializationConfig.Feature.INDENT_OUTPUT, true);
            objectMapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        }
    
        private static final Pattern SUB_FIELD_PATTERN = Pattern.compile("\..*");
    
        private static final String HIGHLIGHT_FIELD_SUFFIX = "Highlight";
    
        @Override
        public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
            long totalHits = response.getHits().getTotalHits();
            List<T> list = Lists.newArrayList();
            // 获取搜索结果
            SearchHits hits = response.getHits();
            for (SearchHit searchHit : hits) {
                if (hits.getHits().length <= 0) {
                    continue;
                }
                // 获取高亮字段Map
                Map<String, HighlightField> highlightFields = searchHit.getHighlightFields();
                // 通过jackson将json字符串转化为对象
                T item = jsonStrToObject(searchHit.getSourceAsString(), clazz);
                if (Objects.isNull(item)) {
                    continue;
                }
                // 遍历高亮字段Map,将高亮字段key转化为原始字段名(title.pinyin -> title),拼接高亮文本并与原始字段名组装为一个Map
                Map<String, String> highlightFieldMap = Maps.newHashMap();
                for (Map.Entry<String, HighlightField> highlightField : highlightFields.entrySet()) {
                    String key = SUB_FIELD_PATTERN.matcher(highlightField.getKey()).replaceAll(Constants.BLANK) + HIGHLIGHT_FIELD_SUFFIX;
                    HighlightField value = highlightField.getValue();
                    Text[] fragments = value.getFragments();
                    StringBuilder sb = new StringBuilder();
                    for (Text text : fragments) {
                        sb.append(text);
                    }
                    highlightFieldMap.put(key, sb.toString());
                }
                // 通过反射将高亮文本赋值到原始字段对应的高亮字段中
                try {
                    Field[] fields = clazz.getDeclaredFields();
                    for (Field field : fields) {
                        if (!field.getName().contains(HIGHLIGHT_FIELD_SUFFIX)) {
                            continue;
                        }
                        field.setAccessible(true);
                        if (highlightFieldMap.containsKey(field.getName())) {
                            field.set(item, highlightFieldMap.get(field.getName()));
                        } else {
                            field.set(item, searchHit.getSource().get(field.getName().replace(HIGHLIGHT_FIELD_SUFFIX, Constants.BLANK)));
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
                list.add(item);
            }
            return new AggregatedPageImpl<>(list, pageable, totalHits);
        }
    
        private <T> T jsonStrToObject(String json, Class<T> cls) {
            try {
                return objectMapper.readValue(json, cls);
            } catch (IOException e) {
                log.error("json cant be objectTranslate to object,{}", json);
                return null;
            }
        }
    }
    

    2.4 获取返回结果

    ① 返回对象增加高亮字段

    @Data
    @Document(indexName = "knowledge", type = "knowledge")
    public class KnowledgeDO {
    
        ...省略其余部分...
        
        private String knowledgeTitleHighlight;
        
        private String knowledgeContentHighlight;
    }
    

    ② 业务实现类注入 HighlightResultHelper

    @Autowired
    private HighlightResultHelper highlightResultHelper;
    

    ③ 获取分页结果由前文的 knowledgeRepository.search 改为 elasticsearchTemplate.queryForPage 实现,查询时指定 highlightResultHelper

    Page<KnowledgeDO> page = elasticsearchTemplate.queryForPage(searchQuery, KnowledgeDO.class, highlightResultHelper);
    

    注:测试结果展示

    [
        {
            "id": 850,
            "knowledgeTitle": "小儿腺样体肥大的孩子宜多吃什么?",
            "knowledgeTitleHighlight": "小儿腺样体肥大的孩子宜多吃什么?",
            "knowledgeContent": "1、饮食中要停掉一切寒凉的食物,只吃性平、性温的食物,如猪肉、鸡肉、牛肉、鸽肉、鹌鹑、鳝鱼、泥鳅、青菜、白菜、包菜、黄豆芽、土豆、韭菜、胡萝卜(一周2次)等,夏天再增加四季豆、豇豆、黄瓜、西红柿、藕、芹菜、花菜、各种菌类(菌类也偏凉适合夏天吃),水果吃新鲜时令的水果,5月份以后,新鲜水果上市了。可以吃草莓、桃子、葡萄、樱桃,秋天可以吃苹果、梨子、桔子等。
    2、每周吃2-3次红烧鳝鱼或喝鳝鱼汤,鳝鱼与其它鱼类不同,补血、补肾、抗过敏的作用明显,但不易上火,补而不燥。每周吃2次海虾,一次10只左右,7岁左右的孩子可以一次半斤,海虾就是鸡尾虾或对虾,补肾阳的作用明显,可以用来治疗慢性扁桃体炎、慢性鼻炎、慢性咽炎,与河虾的功效完全不一样。",
            "knowledgeContentHighlight": "1、饮食中要停掉一切寒凉的食物,只吃性平、性温的食物,如猪肉、鸡肉、牛肉、鸽肉、鹌鹑、鳝鱼、泥鳅、青菜、白菜、包菜、黄豆芽、土豆、韭菜、胡萝卜(一周2次)等,夏天再增加四季豆、豇豆、黄瓜、<span style="color:#F56C6C">西红柿</span>、藕",
            "referenceCount": 0
        }
    ]
    

    三、结语

    至此搜索结果高亮已经实现完毕,下一篇将介绍相关度排序优化。

  • 相关阅读:
    BOM和DOM
    前端CSS
    前端HTML
    索引 创建用户和授权 锁 事务
    多表查询 Naricat pymysql
    外键关联的修改 级联 修改表行记录的操作
    表的基础数据类型 MySQL的mod设置 表的约束
    数据库初识及操作命令
    LINUX 下LAMP之源码环境部署
    Nginx负载均衡配置实例详解【原】
  • 原文地址:https://www.cnblogs.com/orzlin/p/10496867.html
Copyright © 2020-2023  润新知