一、前言
在實際使用中搜索結果中的關鍵詞前端通常會以特殊形式展示,比如標記為紅色使人一目了然。我們可以通過 ES 提供的高亮功能實現此效果。
二、代碼實現
前文查詢是通過一個繼承 ElasticsearchRepository 的接口實現的,但是如果要實現高亮,這種方式就滿足不了了,這里我們需要通過 ElasticsearchTemplate 來完成。
2.1 注入 ElasticsearchTemplate
① ElasticsearchTemplate 類簡介
public class ElasticsearchTemplate implements ElasticsearchOperations, ApplicationContextAware {
...省略其余部分...
}
從上述源碼中可以看到 ElasticsearchTemplate 實現了 ApplicationContextAware 接口,表明這個類是被 Spring 管理的,可以直接注入使用。
② 業務實現類注入 ElasticsearchTemplate
@Autowired
private ElasticsearchTemplate elasticsearchTemplate;
2.2 查詢對象指定高亮字段
在構建查詢對象時需要指定高亮字段,通過 withHighlightFields 方法設置。
private SearchQuery getKnowledgeSearchQuery(KnowledgeSearchParam param) {
Pageable pageable = PageRequest.of(param.getStart() / param.getSize(), param.getSize());
String knowledgeTitleFieldName = "knowledgeTitle";
String knowledgeContentFieldName = "knowledgeContent";
String preTags = "<span style=\"color:#F56C6C\">";
String postTags = "</span>";
HighlightBuilder.Field knowledgeTitleField = new HighlightBuilder.Field(knowledgeTitleFieldName).preTags(preTags).postTags(postTags);
HighlightBuilder.Field knowledgeContentField = new HighlightBuilder.Field(knowledgeContentFieldName).preTags(preTags).postTags(postTags);
BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
queryBuilder.must(QueryBuilders.termQuery("isDeleted", IsDeletedEnum.NO.getKey()));
queryBuilder.should(QueryBuilders.matchQuery(knowledgeTitleFieldName, param.getKeyword()));
queryBuilder.should(QueryBuilders.matchQuery(knowledgeContentFieldName, param.getKeyword()));
return new NativeSearchQueryBuilder()
.withPageable(pageable)
.withQuery(queryBuilder)
.withHighlightFields(knowledgeTitleField, knowledgeContentField)
.build();
}
2.3 自定義 ResultMapper
ResultMapper 是用於將 ES 文檔轉換成 Java 對象的映射類,因為 Spring Data Elasticsearch 默認的的映射類 DefaultResultMapper 不支持高亮,因此,我們需要自定義一個 ResultMapper 。
完整代碼如下:
@Slf4j
@Component
public class HighlightResultHelper implements SearchResultMapper {
private static ObjectMapper objectMapper = new ObjectMapper();
static {
objectMapper.setVisibility(JsonMethod.FIELD, JsonAutoDetect.Visibility.ANY);
objectMapper.configure(SerializationConfig.Feature.INDENT_OUTPUT, true);
objectMapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
private static final Pattern SUB_FIELD_PATTERN = Pattern.compile("\\..*");
private static final String HIGHLIGHT_FIELD_SUFFIX = "Highlight";
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
long totalHits = response.getHits().getTotalHits();
List<T> list = Lists.newArrayList();
// 獲取搜索結果
SearchHits hits = response.getHits();
for (SearchHit searchHit : hits) {
if (hits.getHits().length <= 0) {
continue;
}
// 獲取高亮字段Map
Map<String, HighlightField> highlightFields = searchHit.getHighlightFields();
// 通過jackson將json字符串轉化為對象
T item = jsonStrToObject(searchHit.getSourceAsString(), clazz);
if (Objects.isNull(item)) {
continue;
}
// 遍歷高亮字段Map,將高亮字段key轉化為原始字段名(title.pinyin -> title),拼接高亮文本並與原始字段名組裝為一個Map
Map<String, String> highlightFieldMap = Maps.newHashMap();
for (Map.Entry<String, HighlightField> highlightField : highlightFields.entrySet()) {
String key = SUB_FIELD_PATTERN.matcher(highlightField.getKey()).replaceAll(Constants.BLANK) + HIGHLIGHT_FIELD_SUFFIX;
HighlightField value = highlightField.getValue();
Text[] fragments = value.getFragments();
StringBuilder sb = new StringBuilder();
for (Text text : fragments) {
sb.append(text);
}
highlightFieldMap.put(key, sb.toString());
}
// 通過反射將高亮文本賦值到原始字段對應的高亮字段中
try {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (!field.getName().contains(HIGHLIGHT_FIELD_SUFFIX)) {
continue;
}
field.setAccessible(true);
if (highlightFieldMap.containsKey(field.getName())) {
field.set(item, highlightFieldMap.get(field.getName()));
} else {
field.set(item, searchHit.getSource().get(field.getName().replace(HIGHLIGHT_FIELD_SUFFIX, Constants.BLANK)));
}
}
} catch (Exception e) {
e.printStackTrace();
}
list.add(item);
}
return new AggregatedPageImpl<>(list, pageable, totalHits);
}
private <T> T jsonStrToObject(String json, Class<T> cls) {
try {
return objectMapper.readValue(json, cls);
} catch (IOException e) {
log.error("json cant be objectTranslate to object,{}", json);
return null;
}
}
}
2.4 獲取返回結果
① 返回對象增加高亮字段
@Data
@Document(indexName = "knowledge", type = "knowledge")
public class KnowledgeDO {
...省略其余部分...
private String knowledgeTitleHighlight;
private String knowledgeContentHighlight;
}
② 業務實現類注入 HighlightResultHelper
@Autowired
private HighlightResultHelper highlightResultHelper;
③ 獲取分頁結果由前文的 knowledgeRepository.search 改為 elasticsearchTemplate.queryForPage 實現,查詢時指定 highlightResultHelper
Page<KnowledgeDO> page = elasticsearchTemplate.queryForPage(searchQuery, KnowledgeDO.class, highlightResultHelper);
注:測試結果展示
[
{
"id": 850,
"knowledgeTitle": "小兒腺樣體肥大的孩子宜多吃什么?",
"knowledgeTitleHighlight": "小兒腺樣體肥大的孩子宜多吃什么?",
"knowledgeContent": "1、飲食中要停掉一切寒涼的食物,只吃性平、性溫的食物,如豬肉、雞肉、牛肉、鴿肉、鵪鶉、鱔魚、泥鰍、青菜、白菜、包菜、黃豆芽、土豆、韭菜、胡蘿卜(一周2次)等,夏天再增加四季豆、豇豆、黃瓜、西紅柿、藕、芹菜、花菜、各種菌類(菌類也偏涼適合夏天吃),水果吃新鮮時令的水果,5月份以后,新鮮水果上市了。可以吃草莓、桃子、葡萄、櫻桃,秋天可以吃蘋果、梨子、桔子等。\n2、每周吃2-3次紅燒鱔魚或喝鱔魚湯,鱔魚與其它魚類不同,補血、補腎、抗過敏的作用明顯,但不易上火,補而不燥。每周吃2次海蝦,一次10只左右,7歲左右的孩子可以一次半斤,海蝦就是雞尾蝦或對蝦,補腎陽的作用明顯,可以用來治療慢性扁桃體炎、慢性鼻炎、慢性咽炎,與河蝦的功效完全不一樣。",
"knowledgeContentHighlight": "1、飲食中要停掉一切寒涼的食物,只吃性平、性溫的食物,如豬肉、雞肉、牛肉、鴿肉、鵪鶉、鱔魚、泥鰍、青菜、白菜、包菜、黃豆芽、土豆、韭菜、胡蘿卜(一周2次)等,夏天再增加四季豆、豇豆、黃瓜、<span style=\"color:#F56C6C\">西紅柿</span>、藕",
"referenceCount": 0
}
]
三、結語
至此搜索結果高亮已經實現完畢,下一篇將介紹相關度排序優化。