和我一起打造个简单搜索之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

有疑问?

欢迎来信,给我写信

原文地址:https://www.cnblogs.com/vcmq/p/9966693.html

时间: 2024-10-29 08:16:38

和我一起打造个简单搜索之SpringDataElasticSearch关键词高亮的相关文章

和我一起打造个简单搜索之SpringDataElasticSearch入门

网上大多通过 java 操作 es 使用的都是 TransportClient,而介绍使用 SpringDataElasticSearch 的文章相对比较少,笔者也是摸索了许久,接下来本文介绍 SpringDataElasticSearch 的 api 使用,更加方便的进行查询. 系列文章 一.和我一起打造个简单搜索之ElasticSearch集群搭建 二.和我一起打造个简单搜索之ElasticSearch入门 三.和我一起打造个简单搜索之IK分词以及拼音分词 四.和我一起打造个简单搜索之Log

和我一起打造个简单搜索之Logstash实时同步建立索引

用过 Solr 的朋友都知道,Solr 可以直接在配置文件中配置数据库连接从而完成索引的同步创建,但是 ElasticSearch 本身并不具备这样的功能,那如何建立索引呢?方法其实很多,可以使用 Java API 的方式建立索引,也可以通过 Logstash 的插件 logstash-input-jdbc 完成,今天来探讨下如何使用 logstash-input-jdbc 完成全量同步以及增量同步. 环境 本文以及后续 es 系列文章都基于 5.5.3 这个版本的 elasticsearch

和我一起打造个简单搜索之ElasticSearch集群搭建

我们所常见的电商搜索如京东,搜索页面都会提供各种各样的筛选条件,比如品牌.尺寸.适用季节.价格区间等,同时提供排序,比如价格排序,信誉排序,销量排序等,方便了用户去找到自己心里理想的商品. 站内搜索对于一个网站几乎是标配,只是搜索的强大与否的区别,有的网站只支持关键词模糊搜索,而淘宝,京东提供了精细的筛选条件,同时支持拼音搜索等更方便的搜索方式. 由于笔者在一家做网络文学的公司工作,所以实现就是以小说为商品的搜索,具体可以参考起点网小说的搜索. 如图所示,起点网的搜索提供了关键词搜索和排序条件以

和我一起打造个简单搜索之IK分词以及拼音分词

elasticsearch 官方默认的分词插件,对中文分词效果不理想,它是把中文词语分成了一个一个的汉字.所以我们引入 es 插件 es-ik.同时为了提升用户体验,引入 es-pinyin 插件.本文介绍这两个 es 插件的安装. 环境 本文以及后续 es 系列文章都基于 5.5.3 这个版本的 elasticsearch ,这个版本比较稳定,可以用于生产环境. ik 分词器 和 pinyin 分词器在 github 仓库可以找到,注意版本与 elasticsearch 的版本需要对应,本文使

参与过三次搜索引擎发展转折的百度,为什么要回归“简单搜索”

相信在搜索引擎技术产品化的二十余年间,我们早已经习惯了搜索引擎的商业化.不过在最近的数博会上,李彦宏却展示了一款名为"简单搜索"的语音交互搜索App,并且声明简单搜索永无广告. 相信很多人也曾想象过这样一款产品--通过语音发问,得出最简单的答案.不过可能大多数人没有想到,推出这款产品的是百度.很长一段时间内,百度的搜索引擎商业化一直为人诟病.但这次百度率先推出永无广告的语音交互搜索,无疑是在搜索引擎的技术和产品形式上都进行了重要创新. 当我们在质疑大企业的创新能力时常常会忽视一点,在技

《ElasticSearch6.x实战教程》之简单搜索、Java客户端(上)

第五章-简单搜索 众里寻他千百度 搜索是ES的核心,本节讲解一些基本的简单的搜索. 掌握ES搜索查询的RESTful的API犹如掌握关系型数据库的SQL语句,尽管Java客户端API为我们不需要我们去实际编写RESTful的API,但在生产环境中,免不了在线上执行查询语句做数据统计供产品经理等使用. 数据准备 首先创建一个名为user的Index,并创建一个student的Type,Mapping映射一共有如下几个字段: 创建名为user的Index PUT http://localhost:9

专题一、简单搜索 - Virtual Judge

很久以前刷完了Virtual Judge上的简单搜索专题,现总结如下: POJ 1321 由于题目的数据范围比较小,可以直接dfs暴力.读入时记录每个空位的位置,保存在pX[]以及pY[]数组中.暴力的时候统计当前处理第几个空格以及当前处理到了第几行即可. #include <iostream> #include <memory.h> using namespace std; const int MAX = 128; long long ans; int N, K, nCnt; b

[.NET] 一步步打造一个简单的 MVC 电商网站 - BooksStore(二)

一步步打造一个简单的 MVC 电商网站 - BooksStore(二) 本系列的 GitHub地址:https://github.com/liqingwen2015/Wen.BooksStore 前:<一步步打造一个简单的 MVC 电商网站 - BooksStore(一)> 简介 上一次我们尝试了:创建项目架构.创建域模型实体.创建单元测试.创建控制器与视图.创建分页和加入样式,而这一节我们会完成两个功能,分类导航与购物车. 主要功能与知识点如下: 分类.产品浏览.购物车.结算.CRUD(增删

java 模拟简单搜索

Java 模拟简单搜索 实体类 package org.dennisit.entity; /** * * * @version : 1.0 * * @author : 苏若年 <a href="mailto:[email protected]">发送邮件</a> * * @since : 1.0 创建时间: 2013-4-8 下午04:51:03 * * @function: TODO * */ public class Medicine { private I