Spring Data Elasticsearch
Spring Data Elasticsearch为文档的存储,查询,排序和统计提供了一个高度抽象的模板。使用Spring Data ElasticSearch来操作Elasticsearch,可以较大程度的减少我们的代码量,提高我们的开发效率。
要使用Elasticsearch我们需要引入如下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
<version>2.1.7.RELEASE</version>
</dependency>
还需要在配置文件中增加如下配置
spring:
elasticsearch:
rest:
# elasticsearch server的地址
uris: 192.168.0.102:9200
# 连接超时时间
connection-timeout: 6s
# 访问超时时间
read-timeout: 10s
1. 定义文档映射实体类
类比于MyBatis-Plus可以定义实体类去映射数据库中的表中的数据,使用Spring Data Elasticsearch时,我们也可以通过定义一个实体类映射ES索引中的文档。
@Data
@Document(indexName = "goods", shards = 1, replicas = 0)
public class Goods {
// 商品Id skuId _id
@Id
private Long id;
@Field(type = FieldType.Keyword, index = false)
private String defaultImg;
// elasticsearch 中能分词的字段,这个字段数据类型必须是 text!keyword 不分词!
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String title;
@Field(type = FieldType.Double)
private Double price;
@Field(type = FieldType.Long)
private Long tmId;
@Field(type = FieldType.Keyword)
private String tmName;
@Field(type = FieldType.Keyword)
private String tmLogoUrl;
@Field(type = FieldType.Long)
private Long firstLevelCategoryId;
@Field(type = FieldType.Keyword)
private String firstLevelCategoryName;
@Field(type = FieldType.Long)
private Long secondLevelCategoryId;
@Field(type = FieldType.Keyword)
private String secondLevelCategoryName;
@Field(type = FieldType.Long)
private Long thirdLevelCategoryId;
@Field(type = FieldType.Keyword)
private String thirdLevelCategoryName;
// 商品的热度! 我们将商品被用户点查看的次数越多,则说明热度就越高!
@Field(type = FieldType.Long)
private Long hotScore = 0L;
// 平台属性集合对象
// Nested 支持嵌套查询
@Field(type = FieldType.Nested)
private List<SearchAttr> attrs;
}
/*
该类映射nested平台属性
*/
@Data
public class SearchAttr {
// 平台属性Id
@Field(type = FieldType.Long)
private Long attrId;
// 平台属性值名称
@Field(type = FieldType.Keyword)
private String attrValue;
// 平台属性名
@Field(type = FieldType.Keyword)
private String attrName;
}
在Goods类上,通过添加@Document注解,我们将Goods类映射的文档所属的索引:
- @Document注解的indexName属性,用来定义实体类所映射的文档所属的目标索引名称
- @Document的shards属性,表示目标索引的住分片数量
- @Document的replicas属性,表示每个主分片所拥有的副本分片的数量
在Goods类的成员变量Id上通过添加@Id注解指定,Id成员变量映射到Goods索引中文档的id字段,同时也映射到文档的唯一表示_id字段。
在Goods类的其他成员变量上,通过添加@Field注解,定义成员变量和文档字段的映射关系:
- 默认同名成员变量,映射到文档中的同名字段(也可以由@Field注解的name属性显示指定)
- 通过@Field注解的type属性指定文档中同名字段的数据类型
- 通过@Field注解的analyzer属性,指定成员变量所映射的文档字段所使用的分词器
2. Repository
类比于Mybatis-Plus中定义BaseMaper子接口即可对单表做增删改查的操作,Spring Data Elastisearch中我们可以通过定义ElasticsearchRepository
子接口,迅速实现对索引中的文档数据的增删改查,以及通过自定义方法,实现自定义查询。
public interface GoodsRepository extends ElasticsearchRepository<Goods,Long> {
}
ElasticsearchRepository
接口需要接收两个泛型,第一个泛型即映射实体类,第二个泛型是在实体类中加了@Id注解的成员变量的数据类型,即映射到文档唯一标识_id字段的成员变量类型。
一旦我们定义好了ElasticsearchRepository
的子接口,马上就可以实现对goods索引中文档的增删改查功能
// 注入repository对象
@Autowired
private GoodsRepository goodsRepository;
// 保存单个文档对象
Goods good = ....
goodsRepository.save(good);
// 批量保存多个文档对象
List<Goods> goods = ...
goodsRepository.save(goods);
// 根据id查询
goodsRepository.findById(id);
// 根据id删除
goodsRepository.deleteById(id);
同时,还需要注意一点,一旦定义好了ElasticsearchRepository
接口,而且被SpringBoot启动类扫描到,那么在应用启动的时候,如果ElasticsearchRepository子接口所访问的索引在ES中不存在,Spring Data Elasticsearch会在ES中自动创建索引,并根据映射实体类定义索引的映射。
但是,大多数时候,我们可能需要对索引中的文档数据做自定义查询,此时仅仅使用ElasticsearchRepository
接口中继承的方法无法满足我们的需求。此时就需要在自己的Repository接口中,通过自定义方法来实现各种自定义查询。
- 利用@Query注解自定义查询脚本
/*
1. 通过Query注解定义具体的查询字符串(也可以替换为其他查询)
2. 字符串中的?0是固定格式,表示第0个参数的占位符,在实际查询时会被方法的第一个参数值title的值替换, 如果有多个参数,依次类推即可
3. List<Goods>
// 模糊查询
@Query("{\"fuzzy\": {\"title\": \"?0\"}}")
// 范围查询
@Query("{\"range\": {\"price\": {\"gte\": ?0, \"lte\": ?1}}}")
// 前缀查询
@Query("{\"prefix\": {\"title\": \"?0\"}}")
*/
@Query(
"{ " +
"\"match\": {\n" +
" \"title\": \"?0\"\n" +
"}" +
"}"
)
List<Goods> matchSearch(String title);
这里的@Query注解中,只需要包含我们查询脚本中"query"{}里面的内容即可,比如上面的@Query注解所表示的查询等价于
GET goods/_search
{
"query": {
"match": {
"title": 具体待查询的参数值
}
}
}
@Autowired
ProductRepository productRepository;
@Test
public void testMatchSearch() {
// 在调用的时候传递查询的参数值
List<Goods> list = goodsRepository.matchSearch("荣耀手机");
System.out.println(list);
}
- 利用@Query注解结合分页参数,实现分页查询
/*
1. 针对一个查询结果,返回对应的一页数据
2. Pageable参数是当想要获取分页数据的时候,必须携带的参数,表示分页信息
比如,查询第多少页数据,每页多少条数据等,该参数不会用来替换我们的@Query字符串中的参数
3. 返回的结果是一个包含一页文档数据的Page对象
*/
@Query(
" {" +
" \"match\": {\n" +
" \"title\": \"?0\"\n" +
" }" +
"}"
)
Page<Goods> testSearchPage(String title, Pageable pageable);
// 注入repository对象
@Autowired
private GoodsRepository goodsRepository;
/*
测试分页查询
*/
@Test
public void testSearchPage() {
// 创建表示分页信息的Pageable对象
// 表示查询第几页数据,这里一定要注意,页数是从0开始算的
int page = 0;
// 每页假设10个文档
int pageCount = 10;
// 调用Sort方法得到Sort对象,一个Sort对象表示
Sort sort = Sort.by(Sort.Direction.ASC, "price");
// PageRequest 是 Pageable接口子类对象
PageRequest pageInfo = PageRequest.of(page, pageCount,sort);
// 这里的Page对象可以被看做是List
Page<Goods> pageResult = goodsRepository.testSearchPage("小米手机", pageInfo);
// 遍历集合,从每个SearchHit对象中取出文档对象,
// 如果需要返回可以在遍历的时候将其,放入一个List中返回
pageResult.forEach( goods -> {
// 访问查询到的一条文档
// ...
});
// 获取满足条件的总的文档数量
long totalElements = pageResult.getTotalElements();
}
- @Query注解 + @Highlight + 分页参数实现高亮,分页自定义查询
@Query(
" { \"match\": {\n" +
" \"title\": \"?0\"\n" +
" }" +
"}"
)
@Highlight(
fields = {
@HighlightField(name = "title",
parameters = @HighlightParameters(
preTags = "<font color='red'>", postTags = "</font>"))
}
)
List<SearchHit<Goods>> testHighlight(String title, Pageable pageable);
@Test
public void testHighlight() {
// 分页参数
PageRequest of = PageRequest.of(0, 10);
// 调用Repository方法获取搜索结果
List<SearchHit<Goods>> result = goodsRepository.testHighlight("小米手机", of);
// 结果集
List<Goods> itemDocuments = new ArrayList<>();
result.forEach(hit -> {
// 获取目标文档
Goods content = hit.getContent();
// 获取高亮字段title对应的高亮字符串
List<String> title = hit.getHighlightField("title");
// 在文档对象中,用高亮字符串替换掉原来的值
content.setTitle(title.get(0));
// 加入结果集
itemDocuments.add(content);
});
System.out.println(itemDocuments.size());
}
虽然,testHighlight
方法既实现了分页查询,又实现了高亮查询,但是有一个缺陷就是,该方法无法获取到满足查询条件的总的文档数量,它只会返回满足条件的一页文档数据。不知道满足条件的文档总数,前端就无法完成分页。
所以,很明显Repository好用,但是具有一定的局限性,如果面对比较复杂的查询,此时就只能使用Spring Data Elasticsearch提供的另外一个工具ElasticsearchRestTemplate
了。
3. ElasticsearchRestTemplate
3.1 查询相关特性
3.1.1 过滤
使用BoolQuery
进行过滤:
BoolQueryBuilder qb = QueryBuilders.boolQuery();
qb.filter(QueryBuilders.termQuery("price", 199)); // 过滤条件
3.1.2 排序
使用SortBuilders
构建排序条件:
SortBuilder sort = SortBuilders.fieldSort("price").order(SortOrder.ASC);
3.1.3 自定义分词器
在字段上使用analyzer
属性指定分词器:
@Field(analyzer = "ik_max_word")
private String title;
3.2 高级查询
构造自定义分页,高亮,nested以及聚合查询,并发起请求
@Autowired
ElasticsearchRestTemplate restTemplate;
@Autowired
GoodsConverter goodsConverter;
@Test
public void testRestTemplate() {
// 该Builder包含所有搜索请求的参数
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 获取bool查询Builder
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 构造bool查询中match查询
MatchQueryBuilder matchQuery = QueryBuilders.matchQuery("title", "小米手机");
// 将该查询加入bool查询must中
boolQueryBuilder.must(matchQuery);
TermQueryBuilder subQueryForAttrNested = QueryBuilders.termQuery("attrs.attrValue", "8G");
// 构造nested查询
NestedQueryBuilder attrsNestedQuery = QueryBuilders.nestedQuery("attrs", subQueryForAttrNested, ScoreMode.None);
// 将nested查询作为一个过滤条件
boolQueryBuilder.filter(attrsNestedQuery);
// 将整个bool查询添加到NativeSearchQueryBuilder
queryBuilder.withQuery(boolQueryBuilder);
// 构造分页参数
//PageRequest price = PageRequest.of(0, 10, Sort.by(Sort.Order.desc("price")));
PageRequest price = PageRequest.of(0, 10);
// 向NativeSearchQueryBuilder添加分页参数
queryBuilder.withPageable(price);
// 按照指定字段值排序
FieldSortBuilder priceSortBuilder = SortBuilders.fieldSort("price").order(SortOrder.ASC);
queryBuilder.withSort(priceSortBuilder);
// 构造高亮参数
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("title").preTags("<font color='red'>").postTags("</font>");
// 向NativeSearchQueryBuilder添加高亮参数
queryBuilder.withHighlightBuilder(highlightBuilder);
// 设置品牌聚合(平台属性等的聚合也是相同的方式)
TermsAggregationBuilder termsAggregationBuilder = AggregationBuilders.terms("tmIdAgg").field("tmId")
.subAggregation(AggregationBuilders.terms("tmNameAgg").field("tmName"))
.subAggregation(AggregationBuilders.terms("tmLogoUrlAgg").field("tmLogUrl"));
// 向NativeSearchQueryBuilder添加聚合参数
queryBuilder.addAggregation(termsAggregationBuilder);
// 结果集过滤,只包含原始文档的id,defaultImg,title,price
queryBuilder.withFields("id", "defaultImg", "title", "price");
// 使用ElasticsearchRestTemplate发起搜索请求
NativeSearchQuery build = queryBuilder.build();
SearchHits<Goods> search = restTemplate.search(build, Goods.class);
//封装所有的查询数据
SearchResponseDTO searchResponseDTO = new SearchResponseDTO();
// 获取满足条件的总文档数量
long totalHits = search.getTotalHits();
// 设置查询到的总文档条数
searchResponseDTO.setTotal(totalHits);
// 获取包含所有命中文档的SearchHit对象
List<SearchHit<Goods>> searchHits = search.getSearchHits();
// 处理搜索到的结果集即SearchHit<Goods>集合, 并使用高亮字符串替换
List<GoodsDTO> goodsList = searchHits.stream().map(hit -> {
// 获取命中的文档
Goods content = hit.getContent();
//获取高亮字段
List<String> title = hit.getHighlightField("title");
// 用高亮字段替换
content.setTitle(title.get(0));
// 将Goods对象转化为GoodsDTO对象
GoodsDTO goodsDTO = goodsConverter.goodsPO2DTO(content);
return goodsDTO;
}).collect(Collectors.toList());
// 设置查询到的结果列表
searchResponseDTO.setGoodsList(goodsList);
// 从品牌聚合中获取品牌集合
// 根据id获取品牌id terms聚合结果
Terms terms = search.getAggregations().get("tmIdAgg");
List<SearchResponseTmDTO> trademarkList = terms.getBuckets().stream().map(tmIdBucket -> {
// 封装品牌数据
SearchResponseTmDTO searchResponseTmDTO = new SearchResponseTmDTO();
String tmIdStr = tmIdBucket.getKeyAsString();
// 获取品牌id
Long tmId = Long.parseLong(tmIdStr);
// 设置品牌id
searchResponseTmDTO.setTmId(tmId);
// 获取品牌名称聚合(子聚合)
Terms tmNameAgg = tmIdBucket.getAggregations().get("tmNameAgg");
// 通过聚合桶的名称获取品牌名称
String tmName = tmNameAgg.getBuckets().get(0).getKeyAsString();
// 设置品牌名称
searchResponseTmDTO.setTmName(tmName);
// 获取品牌logo聚合(子聚合)
Terms tmLogoUrlAgg = tmIdBucket.getAggregations().get("tmLogoUrlAgg");
// 通过聚合桶的名称获取品牌名称
String tmLogoUrl = tmLogoUrlAgg.getBuckets().get(0).getKeyAsString();
// 设置品牌名称
searchResponseTmDTO.setTmLogoUrl(tmLogoUrl);
return searchResponseTmDTO;
}).collect(Collectors.toList());
// 设置聚合品牌数据
searchResponseDTO.setTrademarkList(trademarkList);
// .....
}
4. 索引管理
4.1 创建索引
可以通过ElasticsearchRestTemplate
的createIndex
方法创建索引:
// 创建索引,使用实体类进行映射
restTemplate.createIndex(Goods.class);
// 自定义索引设置
restTemplate.createIndex(Goods.class, c -> c
.settings(s -> s
.put("index.number_of_shards", 3) // 指定主分片数
.put("index.number_of_replicas", 2) // 指定副本分片数
)
.mapping(m -> m
.put("dynamic", false) // 禁用动态映射
)
);
4.2 检索索引
使用getIndex
方法获取索引信息:
GetIndexResponse response = restTemplate.getIndex(Goods.class);
Map<String, Object> settings = response.getSettings();
Map<String, Object> mappings = response.getMappings();
4.3 修改映射
使用putMapping
方法更新索引字段映射:
// 新增一个text字段
restTemplate.putMapping(Goods.class, m -> m.textField("newField"));
4.4 删除索引
使用deleteIndex
删除索引:
restTemplate.deleteIndex(Goods.class);
5. 异常处理
使用@ExceptionHandler
注解处理Elasticsearch异常:文章来源:https://www.toymoban.com/news/detail-627788.html
@ExceptionHandler(ElasticsearchException.class)
public Response handleError(Exception e) {
// 处理异常逻辑
return Response.status(500).build();
}
6. 性能优化
- 加载批量文档,使用 bulk 或 batch 方式
- 使用 scroll api 避免深分页问题
- 控制请求缓存大小,不要缓存过多数据
- 定期优化索引,提高查询性能
7. 应用案例
以电商网站的商品搜索为例:文章来源地址https://www.toymoban.com/news/detail-627788.html
- 定义商品索引、文档映射实体
- 构建查询,实现精确匹配、分词匹配、过滤、聚合等复杂查询
- 使用高级功能如Suggest完成自动补全
- 可视化分析商品热门趋势、用户行为数据
8. Spring Data Elasticsearch优势
- 简化开发,不需要了解 ES 客户端细节
- 查询方式灵活,支持自定义复杂 DSL 语句
- 提供索引管理、错误处理等功能
- 易于测试和扩展
- 与 Spring Boot 无缝集成,开发效率高
到了这里,关于Spring Data Elasticsearch - 在Spring应用中操作Elasticsearch数据库的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!