• 和我一起打造个简单搜索之SpringDataElasticSearch关键词高亮


    前面几篇文章详细讲解了 ElasticSearch 的搭建以及使用 SpringDataElasticSearch 来完成搜索查询,但是搜索一般都会有搜索关键字高亮的功能,今天我们把它给加上。

    系列文章

    环境依赖

    本文以及后续 es 系列文章都基于 5.5.3 这个版本的 elasticsearch ,这个版本比较稳定,可以用于生产环境。

    SpringDataElasticSearch 的基本使用可以看我的上一篇文章 和我一起打造个简单搜索之SpringDataElasticSearch入门,本文就不再赘述。

    高亮关键字实现

    前文查询是通过写一个接口来继承 ElasticsearchRepository 来实现的,但是如果要实现高亮,我们就不能这样做了,我们需要使用到 ElasticsearchTemplate来完成。

    查看这个类的源码

    public class ElasticsearchTemplate implements ElasticsearchOperations, ApplicationContextAware {
        ...
    }
    

    可以看到,ElasticsearchTemplate 实现了接口 ApplicationContextAware,所以这个类是被 Spring 管理的,可以在类里面直接注入使用。

    代码如下:

    @Slf4j
    @Component
    public class HighlightBookRepositoryTest extends EsSearchApplicationTests {
    
        @Autowired
        private ElasticsearchTemplate elasticsearchTemplate;
        @Resource
        private ExtResultMapper extResultMapper;
    
        @Test
        public void testHighlightQuery() {
            BookQuery query = new BookQuery();
            query.setQueryString("穿越");
    
            // 复合查询
            BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
    
            // 以下为查询条件, 使用 must query 进行查询组合
            MultiMatchQueryBuilder matchQuery = QueryBuilders.multiMatchQuery(query.getQueryString(), "name", "intro", "author");
            boolQuery.must(matchQuery);
    
            PageRequest pageRequest = PageRequest.of(query.getPage() - 1, query.getSize());
    
            NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
                    .withQuery(boolQuery)
                    .withHighlightFields(
                            new HighlightBuilder.Field("name").preTags("<span style="color:red">").postTags("</span>"),
                            new HighlightBuilder.Field("author").preTags("<span style="color:red">").postTags("</span>"))
                    .withPageable(pageRequest)
                    .build();
            Page<Book> books = elasticsearchTemplate.queryForPage(searchQuery, Book.class, extResultMapper);
    
            books.forEach(e -> log.info("{}", e));
            // <span style="color:red">穿越</span>小道人
        }
    }
    

    注意这里 的

     Page<Book> books = elasticsearchTemplate.queryForPage(searchQuery, Book.class, extResultMapper);
    

    这里返回的是分页对象。
    查询方式和上文的差不多,只不过是是 Repository 变成了 ElasticsearchTemplate,操作方式也大同小异。

    这里用到了 ExtResultMapper,请接着看下文。

    自定义ResultMapper

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

    复制 DefaultResultMapper 类,重命名为 ExtResultMapper,对构造方法名称修改为正确的值。

    新增一个方法,用于将高亮的内容赋值给需要转换的 Java 对象内。

    在 mapResults 方法内调用这个方法。

    注意:这个类可以直接拷贝到你的项目中直接使用!
    我写这么多,只是想说明为什么这个类是这样的。

    import com.fasterxml.jackson.core.JsonEncoding;
    import com.fasterxml.jackson.core.JsonFactory;
    import com.fasterxml.jackson.core.JsonGenerator;
    import org.apache.commons.beanutils.PropertyUtils;
    import org.elasticsearch.action.get.GetResponse;
    import org.elasticsearch.action.get.MultiGetItemResponse;
    import org.elasticsearch.action.get.MultiGetResponse;
    import org.elasticsearch.action.search.SearchResponse;
    import org.elasticsearch.common.text.Text;
    import org.elasticsearch.search.SearchHit;
    import org.elasticsearch.search.SearchHitField;
    import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
    import org.springframework.data.domain.Pageable;
    import org.springframework.data.elasticsearch.ElasticsearchException;
    import org.springframework.data.elasticsearch.annotations.Document;
    import org.springframework.data.elasticsearch.annotations.ScriptedField;
    import org.springframework.data.elasticsearch.core.AbstractResultMapper;
    import org.springframework.data.elasticsearch.core.DefaultEntityMapper;
    import org.springframework.data.elasticsearch.core.EntityMapper;
    import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
    import org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl;
    import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
    import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
    import org.springframework.data.mapping.context.MappingContext;
    import org.springframework.stereotype.Component;
    import org.springframework.util.Assert;
    import org.springframework.util.StringUtils;
    
    import java.io.ByteArrayOutputStream;
    import java.io.IOException;
    import java.lang.reflect.InvocationTargetException;
    import java.nio.charset.Charset;
    import java.util.*;
    
    /**
     * 类名称:ExtResultMapper
     * 类描述:自定义结果映射类
     * 创建人:WeJan
     * 创建时间:2018-09-13 20:47
     */
    @Component
    public class ExtResultMapper extends AbstractResultMapper {
    
        private MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext;
    
        public ExtResultMapper() {
            super(new DefaultEntityMapper());
        }
    
        public ExtResultMapper(MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext) {
            super(new DefaultEntityMapper());
            this.mappingContext = mappingContext;
        }
    
        public ExtResultMapper(EntityMapper entityMapper) {
            super(entityMapper);
        }
    
        public ExtResultMapper(
                MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext,
                EntityMapper entityMapper) {
            super(entityMapper);
            this.mappingContext = mappingContext;
        }
    
        @Override
        public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
            long totalHits = response.getHits().totalHits();
            List<T> results = new ArrayList<>();
            for (SearchHit hit : response.getHits()) {
                if (hit != null) {
                    T result = null;
                    if (StringUtils.hasText(hit.sourceAsString())) {
                        result = mapEntity(hit.sourceAsString(), clazz);
                    } else {
                        result = mapEntity(hit.getFields().values(), clazz);
                    }
                    setPersistentEntityId(result, hit.getId(), clazz);
                    setPersistentEntityVersion(result, hit.getVersion(), clazz);
                    populateScriptFields(result, hit);
                   
                   // 高亮查询
                    populateHighLightedFields(result, hit.getHighlightFields());
                    results.add(result);
                }
            }
    
            return new AggregatedPageImpl<T>(results, pageable, totalHits, response.getAggregations(), response.getScrollId());
        }
    
        private <T>  void populateHighLightedFields(T result, Map<String, HighlightField> highlightFields) {
            for (HighlightField field : highlightFields.values()) {
                try {
                    PropertyUtils.setProperty(result, field.getName(), concat(field.fragments()));
                } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
                    throw new ElasticsearchException("failed to set highlighted value for field: " + field.getName()
                            + " with value: " + Arrays.toString(field.getFragments()), e);
                }
            }
        }
    
        private String concat(Text[] texts) {
            StringBuffer sb = new StringBuffer();
            for (Text text : texts) {
                sb.append(text.toString());
            }
            return sb.toString();
        }
    
        private <T> void populateScriptFields(T result, SearchHit hit) {
            if (hit.getFields() != null && !hit.getFields().isEmpty() && result != null) {
                for (java.lang.reflect.Field field : result.getClass().getDeclaredFields()) {
                    ScriptedField scriptedField = field.getAnnotation(ScriptedField.class);
                    if (scriptedField != null) {
                        String name = scriptedField.name().isEmpty() ? field.getName() : scriptedField.name();
                        SearchHitField searchHitField = hit.getFields().get(name);
                        if (searchHitField != null) {
                            field.setAccessible(true);
                            try {
                                field.set(result, searchHitField.getValue());
                            } catch (IllegalArgumentException e) {
                                throw new ElasticsearchException("failed to set scripted field: " + name + " with value: "
                                        + searchHitField.getValue(), e);
                            } catch (IllegalAccessException e) {
                                throw new ElasticsearchException("failed to access scripted field: " + name, e);
                            }
                        }
                    }
                }
            }
        }
    
        private <T> T mapEntity(Collection<SearchHitField> values, Class<T> clazz) {
            return mapEntity(buildJSONFromFields(values), clazz);
        }
    
        private String buildJSONFromFields(Collection<SearchHitField> values) {
            JsonFactory nodeFactory = new JsonFactory();
            try {
                ByteArrayOutputStream stream = new ByteArrayOutputStream();
                JsonGenerator generator = nodeFactory.createGenerator(stream, JsonEncoding.UTF8);
                generator.writeStartObject();
                for (SearchHitField value : values) {
                    if (value.getValues().size() > 1) {
                        generator.writeArrayFieldStart(value.getName());
                        for (Object val : value.getValues()) {
                            generator.writeObject(val);
                        }
                        generator.writeEndArray();
                    } else {
                        generator.writeObjectField(value.getName(), value.getValue());
                    }
                }
                generator.writeEndObject();
                generator.flush();
                return new String(stream.toByteArray(), Charset.forName("UTF-8"));
            } catch (IOException e) {
                return null;
            }
        }
    
        @Override
        public <T> T mapResult(GetResponse response, Class<T> clazz) {
            T result = mapEntity(response.getSourceAsString(), clazz);
            if (result != null) {
                setPersistentEntityId(result, response.getId(), clazz);
                setPersistentEntityVersion(result, response.getVersion(), clazz);
            }
            return result;
        }
    
        @Override
        public <T> LinkedList<T> mapResults(MultiGetResponse responses, Class<T> clazz) {
            LinkedList<T> list = new LinkedList<>();
            for (MultiGetItemResponse response : responses.getResponses()) {
                if (!response.isFailed() && response.getResponse().isExists()) {
                    T result = mapEntity(response.getResponse().getSourceAsString(), clazz);
                    setPersistentEntityId(result, response.getResponse().getId(), clazz);
                    setPersistentEntityVersion(result, response.getResponse().getVersion(), clazz);
                    list.add(result);
                }
            }
            return list;
        }
    
        private <T> void setPersistentEntityId(T result, String id, Class<T> clazz) {
    
            if (mappingContext != null && clazz.isAnnotationPresent(Document.class)) {
    
                ElasticsearchPersistentEntity<?> persistentEntity = mappingContext.getRequiredPersistentEntity(clazz);
                ElasticsearchPersistentProperty idProperty = persistentEntity.getIdProperty();
    
                // Only deal with String because ES generated Ids are strings !
                if (idProperty != null && idProperty.getType().isAssignableFrom(String.class)) {
                    persistentEntity.getPropertyAccessor(result).setProperty(idProperty, id);
                }
    
            }
        }
    
        private <T> void setPersistentEntityVersion(T result, long version, Class<T> clazz) {
            if (mappingContext != null && clazz.isAnnotationPresent(Document.class)) {
    
                ElasticsearchPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(clazz);
                ElasticsearchPersistentProperty versionProperty = persistentEntity.getVersionProperty();
    
                // Only deal with Long because ES versions are longs !
                if (versionProperty != null && versionProperty.getType().isAssignableFrom(Long.class)) {
                    // check that a version was actually returned in the response, -1 would indicate that
                    // a search didn't request the version ids in the response, which would be an issue
                    Assert.isTrue(version != -1, "Version in response is -1");
                    persistentEntity.getPropertyAccessor(result).setProperty(versionProperty, version);
                }
            }
        }
    }
    

    注意这里使用到了 PropertyUtils ,需要引入一个 Apache 的依赖。

    <dependency>
        <groupId>commons-beanutils</groupId>
        <artifactId>commons-beanutils</artifactId>
        <version>1.9.3</version>
    </dependency>
    

    自定义 ResultMapper 写好之后,添加 @Component 注解,表示为 Spring 的一个组件,在类中进行注入使用即可。

    最后

    本文示例项目地址:https://github.com/Mosiki/SpringDataElasticSearchQuickStartExample

    有疑问?

    欢迎来信,给我写信

  • 相关阅读:
    linux_shell_入门
    Linux下安装jdk
    Linux杂记
    Linux常用命令
    Java 性能优化的五大技巧
    Java异常处理的9个最佳实践
    Java面试:投行的15个多线程和并发面试题
    敏捷持续集成详解
    gitlab系列详解
    git系列讲解
  • 原文地址:https://www.cnblogs.com/vcmq/p/9966693.html
Copyright © 2020-2023  润新知