前言: 在用户搜索指定的数据的时候,为了提升用户的使用体验,让用户更有区分度的选择合适的内容,可以将搜索内容中关键字的高亮显示。
过程比较简单
注意: 博主这里选择使用spring-boot-starter-data-elasticsearch
SpringBoot框架帮我们自动配置了一套合适的,简易使用的elasticSearch的相关Api;这样可以减少我们的学习成本 (因为ElasticSearch版本迭代速度很快,之前主流的 High level client JavaApi被淘汰了,7.15版本后是一套新的javaApi,又要学新的东西,比较耗费时间)
<!-- elasticsearch-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
2.创建ES实体类:封装通过ES查询返回的数据,同时也可以作为添加至ES中的对象;类似于MySQL,可以通过实体类对MySQL查询出来的数据做处理,也可以将前端传递过来的数据封装成实体类,实现MySQL中数据的新增;
import cn.hutool.extra.pinyin.PinyinUtil;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import com.yupi.springbootinit.model.entity.Post;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import lombok.Data;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
/**
* 帖子 ES 包装类
**/
@Document(indexName = "post")
@Data
public class PostEsDTO implements Serializable {
private static final String DATE_TIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
/**
* id
*/
@Id //文档id
private Long id;
/**
* 标题
*/
private String title;
/**
* 内容
*/
private String content;
/**
* 标签列表
*/
private List<String> tags;
/* *
* 搜索建议
*/
private List<String> titleSuggestion;
/**
* 点赞数
*/
// private Integer thumbNum;
/**
* 收藏数
*/
// private Integer favourNum;
/**
* 创建用户 id
*/
private Long userId;
/**
* 创建时间
*/
@Field(index = false, store = true, type = FieldType.Date, format = {}, pattern = DATE_TIME_PATTERN)
private Date createTime;
/**
* 更新时间
*/
@Field(index = false, store = true, type = FieldType.Date, format = {}, pattern = DATE_TIME_PATTERN)
private Date updateTime;
/**
* 是否删除
*/
private Integer isDelete;
private static final long serialVersionUID = 1L;
private static final Gson GSON = new Gson();
/**
* 对象转包装类
*
* @param post
* @return
*/
public static PostEsDTO objToDto(Post post) {
if (post == null) {
return null;
}
PostEsDTO postEsDTO = new PostEsDTO();
BeanUtils.copyProperties(post, postEsDTO);
String title = postEsDTO.getTitle();
List<String> titleSuggestion = new ArrayList<>() {{
add(title);
add(PinyinUtil.getPinyin(title).replace(" ", ""));
}};
postEsDTO.setTitleSuggestion(titleSuggestion);
String tagsStr = post.getTags();
if (StringUtils.isNotBlank(tagsStr)) {
postEsDTO.setTags(GSON.fromJson(tagsStr, new TypeToken<List<String>>() {
}.getType()));
}
return postEsDTO;
}
/**
* 包装类转对象
*
* @param postEsDTO
* @return
*/
public static Post dtoToObj(PostEsDTO postEsDTO) {
if (postEsDTO == null) {
return null;
}
Post post = new Post();
BeanUtils.copyProperties(postEsDTO, post);
List<String> tagList = postEsDTO.getTags();
if (CollectionUtils.isNotEmpty(tagList)) {
post.setTags(GSON.toJson(tagList));
}
return post;
}
}
3.创建工具类,用于操作ES; 类似于操作MySQL的Mapper接口;可以对ES文档中指定索引实现增删改查操作(如果ES中已经有数据可忽略,没有数据需要使用该工具类向es中添加数据)
import com.yupi.springbootinit.model.dto.post.PostEsDTO;
import java.util.List;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
/**
* 帖子 ES 操作
*
*/
public interface PostEsDao extends ElasticsearchRepository<PostEsDTO, Long> {
}
4.在业务层中引入ElasticsearchRestTemplate类的对象,这是spring-boot-data-starter-elasticsearch这个依赖中已经封装好的;直接在业务层中通过 @Resource 自动装填即可; 由于我的业务层中有很多代码,太冗余了,就直接展示通过es查询数据并将数据高亮处理的方法
@Service
@Slf4j
public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements PostService {
@Resource
private ElasticSearchRestTemplate elasticsearchRestTemplate;
@Override
public Page<Post> searchFromEs(PostQueryRequest postQueryRequest) {
Long id = postQueryRequest.getId();
Long notId = postQueryRequest.getNotId();
String searchText = postQueryRequest.getSearchText();
String title = postQueryRequest.getTitle();
String content = postQueryRequest.getContent();
List<String> tagList = postQueryRequest.getTags();
List<String> orTagList = postQueryRequest.getOrTags();
Long userId = postQueryRequest.getUserId();
// es 起始页为 0
long current = postQueryRequest.getCurrent() - 1;
long pageSize = postQueryRequest.getPageSize();
// 指定排序字段
String sortField = postQueryRequest.getSortField();
// 指定排序规则(降序或升序)
String sortOrder = postQueryRequest.getSortOrder();
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 过滤
boolQueryBuilder.filter(QueryBuilders.termQuery("isDelete", 0));
if (id != null) {
boolQueryBuilder.filter(QueryBuilders.termQuery("id", id)); // term 在es中表示精确查询
}
if (notId != null) {
boolQueryBuilder.mustNot(QueryBuilders.termQuery("id", notId));
}
if (userId != null) {
boolQueryBuilder.filter(QueryBuilders.termQuery("userId", userId));
}
// 必须包含所有标签
if (CollectionUtils.isNotEmpty(tagList)) {
for (String tag : tagList) {
boolQueryBuilder.filter(QueryBuilders.termQuery("tags", tag));
}
}
// 包含任何一个标签即可
if (CollectionUtils.isNotEmpty(orTagList)) {
BoolQueryBuilder orTagBoolQueryBuilder = QueryBuilders.boolQuery();
for (String tag : orTagList) {
orTagBoolQueryBuilder.should(QueryBuilders.termQuery("tags", tag));
}
// 至少匹配到一个标签即可
orTagBoolQueryBuilder.minimumShouldMatch(1);
boolQueryBuilder.filter(orTagBoolQueryBuilder);
}
// 按关键词检索
if (StringUtils.isNotBlank(searchText)) {
boolQueryBuilder.should(QueryBuilders.matchQuery("title", searchText));
boolQueryBuilder.should(QueryBuilders.matchQuery("content", searchText));
boolQueryBuilder.minimumShouldMatch(1);
}
// 按标题检索
if (StringUtils.isNotBlank(title)) {
boolQueryBuilder.should(QueryBuilders.matchQuery("title", title));
boolQueryBuilder.minimumShouldMatch(1);
}
// 按内容检索
if (StringUtils.isNotBlank(content)) {
boolQueryBuilder.should(QueryBuilders.matchQuery("content", content));
boolQueryBuilder.minimumShouldMatch(1);
}
// 排序
SortBuilder<?> sortBuilder = SortBuilders.scoreSort();
if (StringUtils.isNotBlank(sortField)) {
sortBuilder = SortBuilders.fieldSort(sortField);
sortBuilder.order(CommonConstant.SORT_ORDER_ASC.equals(sortOrder) ? SortOrder.ASC : SortOrder.DESC);
}
// 分页
PageRequest pageRequest = PageRequest.of((int) current, (int) pageSize);
// 构造查询
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(boolQueryBuilder)
.withPageable(pageRequest)
.withSorts(sortBuilder)
.withHighlightFields( // 高亮字段显示
new HighlightBuilder.Field("content").preTags("<em style='color:red'>").postTags("</em>"),
new HighlightBuilder.Field("title").preTags("<em style='color:red'>").postTags("</em>")
).build();
SearchHits<PostEsDTO> searchHits = elasticsearchRestTemplate.search(searchQuery, PostEsDTO.class);
Page<Post> page = new Page<>();
page.setTotal(searchHits.getTotalHits());
List<Post> resourceList = new ArrayList<>();
// 对es查询出来的结果进行封装处理,作用是返回给前端
if (searchHits.hasSearchHits()) {
List<SearchHit<PostEsDTO>> searchHitList = searchHits.getSearchHits();
// 取出需要高亮的字段,封装到map中,key为id,value 为高亮字段值;
Map<Long, String> postIdTitleHighlightMap = new HashMap<>();
Map<Long, String> postIdContentHighlightMap = new HashMap<>();
searchHitList.stream().forEach(searchHit -> {
Long postId = searchHit.getContent().getId();
List<String> titleHighlightList = searchHit.getHighlightField("title");
if (titleHighlightList.size() != 0) {
String titleHighlight = titleHighlightList.get(0);
if (StringUtils.isNotBlank(titleHighlight)) {
postIdTitleHighlightMap.put(postId, titleHighlight);
}
}
List<String> contentHighlightList = searchHit.getHighlightField("content");
if (contentHighlightList.size() != 0) {
String contentHighlight = contentHighlightList.get(0);
if (StringUtils.isNotBlank(contentHighlight)) {
postIdContentHighlightMap.put(postId, contentHighlight);
}
}
});
List<Long> postIdList = searchHitList.stream().map(searchHit -> searchHit.getContent().getId())
.collect(Collectors.toList());
List<Post> postList = baseMapper.selectBatchIds(postIdList);
if (postList != null) {
Map<Long, List<Post>> idPostMap = postList.stream().collect(Collectors.groupingBy(Post::getId));
postIdList.forEach(postId -> {
if (idPostMap.containsKey(postId)) {
Post post = idPostMap.get(postId).get(0);
String titleField = postIdTitleHighlightMap.get(postId);
if (StringUtils.isNotBlank(titleField)) {
post.setTitle(titleField);
}
String contentField =postIdContentHighlightMap.get(postId);
if(StringUtils.isNotBlank(contentField)){
post.setContent(contentField);
}
resourceList.add(post);
} else {
// 从 es 清空 db 已物理删除的数据
String delete = elasticsearchRestTemplate.delete(String.valueOf(postId), PostEsDTO.class);
log.info("delete post {}", delete);
}
});
}
}
page.setRecords(resourceList);
return page;
}
}
是不是很懵😵,其实只有几个字段是需要我们关注的!!!
- 需要高亮处理,就需要在查询条件中指定高亮字段
// 在查询条件中添加高亮字段
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(boolQueryBuilder)
.withPageable(pageRequest)
.withSorts(sortBuilder)
.withHighlightFields( // 高亮字段显示
new HighlightBuilder.Field("content").preTags("<em style='color:red'>").postTags("</em>"),
new HighlightBuilder.Field("title").preTags("<em style='color:red'>").postTags("</em>")
).build();
// 将ES中的查询结果封装成 PostEsDTO 实体类;
SearchHits<PostEsDTO> searchHits = elasticsearchRestTemplate.search(searchQuery, PostEsDTO.class);
- 然后就是对查询结果做处理
// 取出需要高亮的字段,封装到map中,key为id,value 为高亮字段值;
Map<Long, String> postIdTitleHighlightMap = new HashMap<>();
Map<Long, String> postIdContentHighlightMap = new HashMap<>();
searchHitList.stream().forEach(searchHit -> {
// 取id
Long postId = searchHit.getContent().getId();
// 取高亮字段title
List<String> titleHighlightList = searchHit.getHighlightField("title");
if (titleHighlightList.size() != 0) {
// get(0)的目的是每段话 取第一个关键字作为高亮字段即可,
String titleHighlight = titleHighlightList.get(0);
// 判断是否为空
if (StringUtils.isNotBlank(titleHighlight)) {
postIdTitleHighlightMap.put(postId, titleHighlight);
}
}
// 去高亮字段content
List<String> contentHighlightList = searchHit.getHighlightField("content");
if (contentHighlightList.size() != 0) {
String contentHighlight = contentHighlightList.get(0);
if (StringUtils.isNotBlank(contentHighlight)) {
postIdContentHighlightMap.put(postId, contentHighlight);
}
}
});
- 最后将存放高亮字段的map中对应的值设置到返回的实体类中(对于ES查询的数据其实并不是数据库中的完整数据,比如用户id字段,在MySQL的帖子表 post 中存在,但不一定要上传到es上去,因为作为搜索方压根不知道你的用户id是啥)所以我们必须收集到es查询出来的所有数据id; 再通过这些id在MySQL中将所对应的全部字段找到,这时我们只要通过比对map中的key和实体类的id是否一致,然后将高亮字段值设置到实体类中进行返回即可。
// 这是从ES中查询到符合条件的数据的id集合
List<Long> postIdList = searchHitList.stream().map(searchHit -> searchHit.getContent().getId())
.collect(Collectors.toList());
// 利用集合存放从MySQL数据库根据id查询出来的数据
List<Post> postList = baseMapper.selectBatchIds(postIdList);
if (postList != null) {
// 通过Collectors.groupingBy方法对id进行分组,得到的结果类似{key:id,value:postList}但postList中只对存在一个post
Map<Long, List<Post>> idPostMap = postList.stream().collect(Collectors.groupingBy(Post::getId));
// 遍历id集合
postIdList.forEach(postId -> {
// 数据库中包含这条id的数据,同时es也查出来了,就可以设置高亮字段了
if (idPostMap.containsKey(postId)) {
Post post = idPostMap.get(postId).get(0);
String titleField = postIdTitleHighlightMap.get(postId);
if (StringUtils.isNotBlank(titleField)) {
// 设置title字段为高亮
post.setTitle(titleField);
}
String contentField =postIdContentHighlightMap.get(postId);
if(StringUtils.isNotBlank(contentField)){
// 设置content字段为高亮
post.setContent(contentField);
}
// resouceList是返回结果的集合
resourceList.add(post);
}
大致过程完毕!!!
debug一下:检查返回给前端的结果,发现title,content字段中都多了 <em color="red"> </em>标签
在前端中利用一个p标签,带上 v-html=item.title即可
最后前端高亮字段展示
如果有小伙伴还是不懂的话,可以私信我,或者评论区留下评论,我会及时解答疑问; 最后希望我们能多多沟通,共同进步!!!