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

系列文章

环境依赖

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

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

高亮关键字实现

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

查看这个类的源码

  1. public class ElasticsearchTemplate implements ElasticsearchOperations, ApplicationContextAware {
  2. ...
  3. }

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

代码如下:

  1. @Slf4j
  2. @Component
  3. public class HighlightBookRepositoryTest extends EsSearchApplicationTests {
  4. @Autowired
  5. private ElasticsearchTemplate elasticsearchTemplate;
  6. @Resource
  7. private ExtResultMapper extResultMapper;
  8. @Test
  9. public void testHighlightQuery() {
  10. BookQuery query = new BookQuery();
  11. query.setQueryString("穿越");
  12. // 复合查询
  13. BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
  14. // 以下为查询条件, 使用 must query 进行查询组合
  15. MultiMatchQueryBuilder matchQuery = QueryBuilders.multiMatchQuery(query.getQueryString(), "name", "intro", "author");
  16. boolQuery.must(matchQuery);
  17. PageRequest pageRequest = PageRequest.of(query.getPage() - 1, query.getSize());
  18. NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
  19. .withQuery(boolQuery)
  20. .withHighlightFields(
  21. new HighlightBuilder.Field("name").preTags("<span style=\"color:red\">").postTags("</span>"),
  22. new HighlightBuilder.Field("author").preTags("<span style=\"color:red\">").postTags("</span>"))
  23. .withPageable(pageRequest)
  24. .build();
  25. Page<Book> books = elasticsearchTemplate.queryForPage(searchQuery, Book.class, extResultMapper);
  26. books.forEach(e -> log.info("{}", e));
  27. // <span style="color:red">穿越</span>小道人
  28. }
  29. }

注意这里 的

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

这里返回的是分页对象。

查询方式和上文的差不多,只不过是是 Repository 变成了 ElasticsearchTemplate,操作方式也大同小异。

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

自定义ResultMapper

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

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

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

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

注意:这个类可以直接拷贝到你的项目中直接使用!

我写这么多,只是想说明为什么这个类是这样的。

  1. import com.fasterxml.jackson.core.JsonEncoding;
  2. import com.fasterxml.jackson.core.JsonFactory;
  3. import com.fasterxml.jackson.core.JsonGenerator;
  4. import org.apache.commons.beanutils.PropertyUtils;
  5. import org.elasticsearch.action.get.GetResponse;
  6. import org.elasticsearch.action.get.MultiGetItemResponse;
  7. import org.elasticsearch.action.get.MultiGetResponse;
  8. import org.elasticsearch.action.search.SearchResponse;
  9. import org.elasticsearch.common.text.Text;
  10. import org.elasticsearch.search.SearchHit;
  11. import org.elasticsearch.search.SearchHitField;
  12. import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
  13. import org.springframework.data.domain.Pageable;
  14. import org.springframework.data.elasticsearch.ElasticsearchException;
  15. import org.springframework.data.elasticsearch.annotations.Document;
  16. import org.springframework.data.elasticsearch.annotations.ScriptedField;
  17. import org.springframework.data.elasticsearch.core.AbstractResultMapper;
  18. import org.springframework.data.elasticsearch.core.DefaultEntityMapper;
  19. import org.springframework.data.elasticsearch.core.EntityMapper;
  20. import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
  21. import org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl;
  22. import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
  23. import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
  24. import org.springframework.data.mapping.context.MappingContext;
  25. import org.springframework.stereotype.Component;
  26. import org.springframework.util.Assert;
  27. import org.springframework.util.StringUtils;
  28. import java.io.ByteArrayOutputStream;
  29. import java.io.IOException;
  30. import java.lang.reflect.InvocationTargetException;
  31. import java.nio.charset.Charset;
  32. import java.util.*;
  33. /**
  34. * 类名称:ExtResultMapper
  35. * 类描述:自定义结果映射类
  36. * 创建人:WeJan
  37. * 创建时间:2018-09-13 20:47
  38. */
  39. @Component
  40. public class ExtResultMapper extends AbstractResultMapper {
  41. private MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext;
  42. public ExtResultMapper() {
  43. super(new DefaultEntityMapper());
  44. }
  45. public ExtResultMapper(MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext) {
  46. super(new DefaultEntityMapper());
  47. this.mappingContext = mappingContext;
  48. }
  49. public ExtResultMapper(EntityMapper entityMapper) {
  50. super(entityMapper);
  51. }
  52. public ExtResultMapper(
  53. MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext,
  54. EntityMapper entityMapper) {
  55. super(entityMapper);
  56. this.mappingContext = mappingContext;
  57. }
  58. @Override
  59. public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
  60. long totalHits = response.getHits().totalHits();
  61. List<T> results = new ArrayList<>();
  62. for (SearchHit hit : response.getHits()) {
  63. if (hit != null) {
  64. T result = null;
  65. if (StringUtils.hasText(hit.sourceAsString())) {
  66. result = mapEntity(hit.sourceAsString(), clazz);
  67. } else {
  68. result = mapEntity(hit.getFields().values(), clazz);
  69. }
  70. setPersistentEntityId(result, hit.getId(), clazz);
  71. setPersistentEntityVersion(result, hit.getVersion(), clazz);
  72. populateScriptFields(result, hit);
  73. // 高亮查询
  74. populateHighLightedFields(result, hit.getHighlightFields());
  75. results.add(result);
  76. }
  77. }
  78. return new AggregatedPageImpl<T>(results, pageable, totalHits, response.getAggregations(), response.getScrollId());
  79. }
  80. private <T> void populateHighLightedFields(T result, Map<String, HighlightField> highlightFields) {
  81. for (HighlightField field : highlightFields.values()) {
  82. try {
  83. PropertyUtils.setProperty(result, field.getName(), concat(field.fragments()));
  84. } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
  85. throw new ElasticsearchException("failed to set highlighted value for field: " + field.getName()
  86. + " with value: " + Arrays.toString(field.getFragments()), e);
  87. }
  88. }
  89. }
  90. private String concat(Text[] texts) {
  91. StringBuffer sb = new StringBuffer();
  92. for (Text text : texts) {
  93. sb.append(text.toString());
  94. }
  95. return sb.toString();
  96. }
  97. private <T> void populateScriptFields(T result, SearchHit hit) {
  98. if (hit.getFields() != null && !hit.getFields().isEmpty() && result != null) {
  99. for (java.lang.reflect.Field field : result.getClass().getDeclaredFields()) {
  100. ScriptedField scriptedField = field.getAnnotation(ScriptedField.class);
  101. if (scriptedField != null) {
  102. String name = scriptedField.name().isEmpty() ? field.getName() : scriptedField.name();
  103. SearchHitField searchHitField = hit.getFields().get(name);
  104. if (searchHitField != null) {
  105. field.setAccessible(true);
  106. try {
  107. field.set(result, searchHitField.getValue());
  108. } catch (IllegalArgumentException e) {
  109. throw new ElasticsearchException("failed to set scripted field: " + name + " with value: "
  110. + searchHitField.getValue(), e);
  111. } catch (IllegalAccessException e) {
  112. throw new ElasticsearchException("failed to access scripted field: " + name, e);
  113. }
  114. }
  115. }
  116. }
  117. }
  118. }
  119. private <T> T mapEntity(Collection<SearchHitField> values, Class<T> clazz) {
  120. return mapEntity(buildJSONFromFields(values), clazz);
  121. }
  122. private String buildJSONFromFields(Collection<SearchHitField> values) {
  123. JsonFactory nodeFactory = new JsonFactory();
  124. try {
  125. ByteArrayOutputStream stream = new ByteArrayOutputStream();
  126. JsonGenerator generator = nodeFactory.createGenerator(stream, JsonEncoding.UTF8);
  127. generator.writeStartObject();
  128. for (SearchHitField value : values) {
  129. if (value.getValues().size() > 1) {
  130. generator.writeArrayFieldStart(value.getName());
  131. for (Object val : value.getValues()) {
  132. generator.writeObject(val);
  133. }
  134. generator.writeEndArray();
  135. } else {
  136. generator.writeObjectField(value.getName(), value.getValue());
  137. }
  138. }
  139. generator.writeEndObject();
  140. generator.flush();
  141. return new String(stream.toByteArray(), Charset.forName("UTF-8"));
  142. } catch (IOException e) {
  143. return null;
  144. }
  145. }
  146. @Override
  147. public <T> T mapResult(GetResponse response, Class<T> clazz) {
  148. T result = mapEntity(response.getSourceAsString(), clazz);
  149. if (result != null) {
  150. setPersistentEntityId(result, response.getId(), clazz);
  151. setPersistentEntityVersion(result, response.getVersion(), clazz);
  152. }
  153. return result;
  154. }
  155. @Override
  156. public <T> LinkedList<T> mapResults(MultiGetResponse responses, Class<T> clazz) {
  157. LinkedList<T> list = new LinkedList<>();
  158. for (MultiGetItemResponse response : responses.getResponses()) {
  159. if (!response.isFailed() && response.getResponse().isExists()) {
  160. T result = mapEntity(response.getResponse().getSourceAsString(), clazz);
  161. setPersistentEntityId(result, response.getResponse().getId(), clazz);
  162. setPersistentEntityVersion(result, response.getResponse().getVersion(), clazz);
  163. list.add(result);
  164. }
  165. }
  166. return list;
  167. }
  168. private <T> void setPersistentEntityId(T result, String id, Class<T> clazz) {
  169. if (mappingContext != null && clazz.isAnnotationPresent(Document.class)) {
  170. ElasticsearchPersistentEntity<?> persistentEntity = mappingContext.getRequiredPersistentEntity(clazz);
  171. ElasticsearchPersistentProperty idProperty = persistentEntity.getIdProperty();
  172. // Only deal with String because ES generated Ids are strings !
  173. if (idProperty != null && idProperty.getType().isAssignableFrom(String.class)) {
  174. persistentEntity.getPropertyAccessor(result).setProperty(idProperty, id);
  175. }
  176. }
  177. }
  178. private <T> void setPersistentEntityVersion(T result, long version, Class<T> clazz) {
  179. if (mappingContext != null && clazz.isAnnotationPresent(Document.class)) {
  180. ElasticsearchPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(clazz);
  181. ElasticsearchPersistentProperty versionProperty = persistentEntity.getVersionProperty();
  182. // Only deal with Long because ES versions are longs !
  183. if (versionProperty != null && versionProperty.getType().isAssignableFrom(Long.class)) {
  184. // check that a version was actually returned in the response, -1 would indicate that
  185. // a search didn't request the version ids in the response, which would be an issue
  186. Assert.isTrue(version != -1, "Version in response is -1");
  187. persistentEntity.getPropertyAccessor(result).setProperty(versionProperty, version);
  188. }
  189. }
  190. }
  191. }

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

  1. <dependency>
  2. <groupId>commons-beanutils</groupId>
  3. <artifactId>commons-beanutils</artifactId>
  4. <version>1.9.3</version>
  5. </dependency>

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

最后

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

有疑问?

欢迎来信,给我写信

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

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

    网上大多通过 java 操作 es 使用的都是 TransportClient,而介绍使用 SpringDataElasticSearch 的文章相对比较少,笔者也是摸索了许久,接下来本文介绍 Spr ...

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

    用过 Solr 的朋友都知道,Solr 可以直接在配置文件中配置数据库连接从而完成索引的同步创建,但是 ElasticSearch 本身并不具备这样的功能,那如何建立索引呢?方法其实很多,可以使用 J ...

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

    elasticsearch 官方默认的分词插件,对中文分词效果不理想,它是把中文词语分成了一个一个的汉字.所以我们引入 es 插件 es-ik.同时为了提升用户体验,引入 es-pinyin 插件.本 ...

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

    我们所常见的电商搜索如京东,搜索页面都会提供各种各样的筛选条件,比如品牌.尺寸.适用季节.价格区间等,同时提供排序,比如价格排序,信誉排序,销量排序等,方便了用户去找到自己心里理想的商品. 站内搜索对 ...

  5. 和我一起打造个简单搜索之ElasticSearch入门

    本文简单介绍了使用 Rest 接口,对 es 进行操作,更深入的学习,可以参考文末部分. 环境 本文以及后续 es 系列文章都基于 5.5.3 这个版本的 elasticsearch ,这个版本比较稳 ...

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

    第五章-简单搜索 众里寻他千百度 搜索是ES的核心,本节讲解一些基本的简单的搜索. 掌握ES搜索查询的RESTful的API犹如掌握关系型数据库的SQL语句,尽管Java客户端API为我们不需要我们去 ...

  7. ElasticSearch 5学习(4)——简单搜索笔记

    空搜索: GET /_search hits: total 总数 hits 前10条数据 hits 数组中的每个结果都包含_index._type和文档的_id字段,被加入到_source字段中这意味 ...

  8. nyoj 284 坦克大战 简单搜索

    题目链接:http://acm.nyist.net/JudgeOnline/problem.php?pid=284 题意:在一个给定图中,铁墙,河流不可走,砖墙走的话,多花费时间1,问从起点到终点至少 ...

  9. 分布式搜索ElasticSearch构建集群与简单搜索实例应用

    分布式搜索ElasticSearch构建集群与简单搜索实例应用 关于ElasticSearch不介绍了,直接说应用. 分布式ElasticSearch集群构建的方法. 1.通过在程序中创建一个嵌入es ...

随机推荐

  1. vue 总结

    VUE总结 双花括号{{}} 01.index.hmlt main.js 内存的数据可以更改 v-model 双休数据绑定 代码: <!DOCTYPE html> <html lan ...

  2. Nodejs初识随笔

    Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境. Node.js 使用了一个事件驱动.非阻塞式 I/O 的模型,使其轻量又高效. Node.js 单线程运行,不 ...

  3. gdb调试技巧 找到php执行进程当前执行的代码

    假设线上有一段php脚本,突然在某天出问题了,不处理但是进程没有退出.这种情况可能是异常休眠或者是有段死循环代码,但是我们怎么定位呢,我们这个时候最想知道的应该是这个脚本在此刻在做什么吧.这个是gdb ...

  4. RIDE 接口自动化请求体参数中文时报错:“UnicodeDecodeError: 'ascii' codec can't decode byte 0xd7 in position 9......”

    在进行robotframework  接口自动化,在请求体参数中输入中文会报以下错误: UnicodeDecodeError: 'ascii' codec can't decode byte 0xd7 ...

  5. 64位Redhat系统应用(c++代码)搭建-使用informix和g++编译

    这篇博客很有必要写下来,记录我在一个比较原生的Linux系统上搭建一套应用所遇到的各种问题和各种坑. 关于这套应用,算是我离职前的一个项目,不完成的话没有办法交差,同时,这个项目也比较紧,合作行一直在 ...

  6. 图片利用 new Image()预加载原理 和懒加载的实现原理

    二:预加载和懒加载的区别 预加载与懒加载,我们经常经常用到,这些技术不仅仅限于图片加载,我们今天讨论的是图片加载: 图片预加载:顾名思义,图片预加载就是在网页全部加载之前,提前加载图片.当用户需要查看 ...

  7. 基于Zxing的二维码的二维码扫描之横屏扫描

    最近项目条码扫描要改为横屏,网上所搜了一下,然后发现我写的需要改动几行代码就可以了,还是很给力的. 如未查看之前的代码,请移步: 基于Zxing的二维码生成和二维码扫描 修改下面写代码就可以实现横屏条 ...

  8. 初识XMind基本操作

    花了一些时间来学习了XMind,梳理了一下学习基础部分的内容,分为输入文字,添加分支,超级链接或附件,以及美化操作四个部分.

  9. python抢火车票 短信通知

    # -*- coding: utf-8 -*- from splinter.browser import Browser from time import sleep import traceback ...

  10. Linux 第五天

    网络命令 1)write 给在线用户发信息(需按Crtl+D保存结束,w命令可看在线用户) 语法:write 用户名 2)wall 发广播信息 英文原意:write all 语法:wall 信息 3) ...