本文最后更新于:2021年6月15日 晚上
前面几篇文章详细讲解了 ElasticSearch 的搭建以及使用 SpringDataElasticSearch 来完成搜索查询,但是搜索一般都会有搜索关键字高亮的功能,今天我们把它给加上。
系列文章
环境依赖 本文以及后续 es 系列文章都基于 5.5.3 这个版本的 elasticsearch ,这个版本比较稳定,可以用于生产环境。
SpringDataElasticSearch 的基本使用可以看我的上一篇文章 和我一起打造个简单搜索之SpringDataElasticSearch入门 ,本文就不再赘述。
高亮关键字实现 前文查询是通过写一个接口来继承 ElasticsearchRepository 来实现的,但是如果要实现高亮,我们就不能这样做了,我们需要使用到 ElasticsearchTemplate 来完成。
查看这个类的源码
public class ElasticsearchTemplate implements ElasticsearchOperations , ApplicationContextAware { ... }
可以看到,ElasticsearchTemplate 实现了接口 ApplicationContextAware ,所以这个类是被 Spring 管理的,可以在类里面直接注入使用。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @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(); 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)); } }
注意这里 的
Page<Book> books = elasticsearchTemplate.queryForPage(searchQuery, Book.class, extResultMapper);
这里返回的是分页对象。 查询方式和上文的差不多,只不过是是 Repository 变成了 ElasticsearchTemplate,操作方式也大同小异。
这里用到了 ExtResultMapper,请接着看下文。
自定义ResultMapper ResultMapper 是用于将 ES 文档转换成 Java 对象的映射类,因为 SpringDataElasticSearch 默认的的映射类 DefaultResultMapper 不支持高亮,因此,我们需要自己定义一个 ResultMapper。
复制 DefaultResultMapper 类,重命名为 ExtResultMapper,对构造方法名称修改为正确的值。
新增一个方法,用于将高亮的内容赋值给需要转换的 Java 对象内。
在 mapResults 方法内调用这个方法。
注意:这个类可以直接拷贝到你的项目中直接使用! 我写这么多,只是想说明为什么这个类是这样的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 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.*;@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(); 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(); if (versionProperty != null && versionProperty.getType().isAssignableFrom(Long.class)) { 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
有疑问? 欢迎来信,给我写信