前面几篇文章详细讲解了 ElasticSearch 的搭建以及使用 SpringDataElasticSearch 来完成搜索查询,但是搜索一般都会有搜索关键字高亮的功能,今天我们把它给加上。
系列文章
- 一、
- 二、
- 三、
- 四、
- 五、
- 六、
- ...
环境依赖
本文以及后续 es 系列文章都基于 5.5.3 这个版本的 elasticsearch ,这个版本比较稳定,可以用于生产环境。
SpringDataElasticSearch 的基本使用可以看我的上一篇文章 ,本文就不再赘述。
高亮关键字实现
前文查询是通过写一个接口来继承 ElasticsearchRepository 来实现的,但是如果要实现高亮,我们就不能这样做了,我们需要使用到 ElasticsearchTemplate来完成。
查看这个类的源码
public class ElasticsearchTemplate implements ElasticsearchOperations, ApplicationContextAware { ...}
可以看到,ElasticsearchTemplate 实现了接口 ApplicationContextAware,所以这个类是被 Spring 管理的,可以在类里面直接注入使用。
代码如下:
@Slf4j@Componentpublic 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("").postTags(""), new HighlightBuilder.Field("author").preTags("").postTags("")) .withPageable(pageRequest) .build(); Pagebooks = elasticsearchTemplate.queryForPage(searchQuery, Book.class, extResultMapper); books.forEach(e -> log.info("{}", e)); // 穿越小道人 }}
注意这里 的
Pagebooks = 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 */@Componentpublic class ExtResultMapper extends AbstractResultMapper { private MappingContext , ElasticsearchPersistentProperty> mappingContext; public ExtResultMapper() { super(new DefaultEntityMapper()); } public ExtResultMapper(MappingContext , ElasticsearchPersistentProperty> mappingContext) { super(new DefaultEntityMapper()); this.mappingContext = mappingContext; } public ExtResultMapper(EntityMapper entityMapper) { super(entityMapper); } public ExtResultMapper( MappingContext , ElasticsearchPersistentProperty> mappingContext, EntityMapper entityMapper) { super(entityMapper); this.mappingContext = mappingContext; } @Override publicAggregatedPage mapResults(SearchResponse response, Class clazz, Pageable pageable) { long totalHits = response.getHits().totalHits(); List 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 (results, pageable, totalHits, response.getAggregations(), response.getScrollId()); } private void populateHighLightedFields(T result, Map 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 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 mapEntity(Collection values, Class clazz) { return mapEntity(buildJSONFromFields(values), clazz); } private String buildJSONFromFields(Collection 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 mapResult(GetResponse response, Class clazz) { T result = mapEntity(response.getSourceAsString(), clazz); if (result != null) { setPersistentEntityId(result, response.getId(), clazz); setPersistentEntityVersion(result, response.getVersion(), clazz); } return result; } @Override public LinkedList mapResults(MultiGetResponse responses, Class clazz) { LinkedList 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 void setPersistentEntityId(T result, String id, Class 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 void setPersistentEntityVersion(T result, long version, Class 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 的依赖。
commons-beanutils commons-beanutils 1.9.3
自定义 ResultMapper 写好之后,添加 @Component 注解,表示为 Spring 的一个组件,在类中进行注入使用即可。
最后
本文示例项目地址:https://github.com/Mosiki/SpringDataElasticSearchQuickStartExample
有疑问?
欢迎来信,