前言
这一章节袁老师将带领同学们来学习Spring Data Elasticsearch高级操作相关的内容。我们继续来探索SDE是如何将原始操作Elasticsearch的客户端API进行封装的,以及通过Spring Data Elasticsearch如何来操作ES。准备好了吗?我们继续来探索ES的内容。
一. 索引数据CRUD操作
SDE的索引数据CRUD操作并没有封装在ElasticsearchTemplate类中,封装在ElasticsearchRepository这个接口中。
在com.yx.respository包下自定义ProductRepository接口,并继承ElasticsearchRespository接口。
package com.yx.respository;
import com.yx.pojo.Product;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
public interface ProductRepository extends ElasticsearchRepository<Product, Long> {
}
1.创建索引数据
创建索引数据时,有单个创建和批量创建之分。
1.先来看单个创建。在SpringDataESTests类中定义addDocument()方法。
@Autowired
private ProductRepository productRepository;
@Test
public void addDocument() {
Product product = new Product(1L, "小米手机Mix", "手机", "小米", 2899.00, "http://image.yx.com/12479122.jpg");
// 添加索引数据
productRepository.save(product);
}
2.再来看批量创建。在SpringDataESTests类中定义addDocuments()方法。
@Test
public void addDocuments() {
// 准备文档数据
List<Product> list = new ArrayList<>();
// 添加数据
list.add(new Product(2L, "坚果手机R1", "手机", "锤子", 3699.00, "http://image.yx.com/12479122.jpg"));
list.add(new Product(3L, "华为META20", "手机", "华为", 4499.00, "http://image.yx.com/12479122.jpg"));
list.add(new Product(4L, "小米Pro", "手机", "小米", 4299.00, "http://image.yx.com/12479122.jpg"));
list.add(new Product(5L, "荣耀V20", "手机", "华为", 2799.00, "http://image.yx.com/12479122.jpg"));
// 添加索引数据
productRepository.saveAll(list);
}
2.查询索引数据
2.1.根据id查询数据
ElasticsearchRepository接口中封装了根据id查询的findById(ID var1)方法。
1.在SpringDataESTests类中定义findById()方法。
@Test
public void findById() {
Optional<Product> optional = productRepository.findById(1L);
Product defaultProduct = new Product();
defaultProduct.setTitle("默认商品数据");
// orElse(T other)方法:如果Optional对象中封装的泛型为null,则将orElse()方法的参数作为返回值
Product product = optional.orElse(defaultProduct);
System.err.println(product);
}
2.运行findById()方法,输出结果见下:
Product(id=1, title=小米手机Mix, category=手机, brand=小米, price=2899.0, images=http://image.yx.com/12479122.jpg)
2.2.查询所有数据
ElasticsearchRepository接口中封装了查询所有数据的findAll()方法。
1.在SpringDataESTests类中定义findAll()方法。
@Test
public void findAll() {
Iterable<Product> list = productRepository.findAll();
list.forEach(System.err::println);
}
2.运行findAll()方法,输出结果见下:
Product(id=2, title=坚果手机R1, category=手机, brand=锤子, price=3699.0, images=http://image.yx.com/12479122.jpg)
Product(id=4, title=小米Pro, category=手机, brand=小米, price=4299.0, images=http://image.yx.com/12479122.jpg)
Product(id=5, title=荣耀V20, category=手机, brand=华为, price=2799.0, images=http://image.yx.com/12479122.jpg)
Product(id=1, title=小米手机Mix, category=手机, brand=小米, price=2899.0, images=http://image.yx.com/12479122.jpg)
Product(id=3, title=华为META20, category=手机, brand=华为, price=4499.0, images=http://image.yx.com/12479122.jpg)
3.自定义方法查询
3.1.存储库查询关键字
ElasticsearchRepository提供的查询方法有限,但是它却提供了非常强大的自定义查询功能。只要遵循Spring Data Elasticsearch提供的语法,我们可以任意定义方法声明。
关键字 | 示例 | Elasticsearch查询字符串 |
And | findByNameAndPrice | {"bool" : {"must" : [ {"field" : {"name" : "?"}}, {"field" : {"price" : "?"}} ]}} |
Or | findByNameOrPrice | {"bool" : {"should" : [ {"field" : {"name" : "?"}}, {"field" : {"price" : "?"}} ]}} |
Is | findByName | {"bool" : {"must" : {"field" : {"name" : "?"}}}} |
Not | findByNameNot | {"bool" : {"must_not" : {"field" : {"name" : "?"}}}} |
Betweend | findByPriceBetween | {"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : ?,"include_lower" : true,"include_upper" : true}}}}} |
LessThanEqual | findByPriceLessThan | {"bool" : {"must" : {"range" : {"price" : {"from" : null,"to" : ?,"include_lower" : true,"include_upper" : true}}}}} |
GreaterThanEqual | findByPriceGreaterThan | {"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : null,"include_lower" : true,"include_upper" : true}}}}} |
Before | findByPriceBefore | {"bool" : {"must" : {"range" : {"price" : {"from" : null,"to" : ?,"include_lower" : true,"include_upper" : true}}}}} |
After | findByPriceAfter | {"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : null,"include_lower" : true,"include_upper" : true}}}}} |
Like | findByNameLike | {"bool" : {"must" : {"field" : {"name" : {"query" : "? *","analyze_wildcard" : true}}}}} |
StartingWith | findByNameStartingWith | {"bool" : {"must" : {"field" : {"name" : {"query" : "? *","analyze_wildcard" : true}}}}} |
EndingWith | findByNameEndingWith | {"bool" : {"must" : {"field" : {"name" : {"query" : "*?","analyze_wildcard" : true}}}}} |
Contains/Containing | findByNameContaining | {"bool" : {"must" : {"field" : {"name" : {"query" : "? ","analyze_wildcard" : true}}}}} |
In | findByNameIn(Collection<String>names) | {"bool" : {"must" : {"bool" : {"should" : [ {"field" : {"name" : "?"}}, {"field" : {"name" : "?"}} ]}}}} |
NotIn | findByNameNotIn(Collection<String>names) | {"bool" : {"must_not" : {"bool" : {"should" : {"field" : {"name" : "?"}}}}}} |
Near | findByStoreNear | Not Supported Yet ! |
True | findByAvailableTrue | {"bool" : {"must" : {"field" : {"available" : true}}}} |
False | findByAvailableFalse | {"bool" : {"must" : {"field" : {"available" : false}}}} |
OrderBy | findByAvailableTrueOrderByNameDesc | {"sort" : [{ "name" : {"order" : "desc"} }],"bool" : {"must" : {"field" : {"available" : true}}}} |
3.2.自定义方法查询案例
1.在ProductRepository接口中定义根据商品的价格区间查询商品数据的findByPriceBetween()方法。
/**
* 根据商品的价格区间查询商品数据
* @param from 开始价格
* @param to 结束价格
* @return 符合条件的商品数据
*/
List<Product> findByPriceBetween(Double from, Double to);
2.无需写实现,SDE会自动帮我们实现该方法,我们只需要用即可。在SpringDataESTests类中定义findByPriceBetween()方法。
@Test
public void findByPriceBetween() {
List<Product> products = productRepository.findByPriceBetween(3000d, 5000d);
products.forEach(System.err::println);
}
3.运行findByPriceBetween()方法,输出结果见下:
Product(id=2, title=坚果手机R1, category= 手机, brand=锤子, price=3699.0, images=http://image.yx.com/12479122.jpg)
Product(id=4, title=小米Pro, category= 手机, brand=小米, price=4299.0, images=http://image.yx.com/12479122.jpg)
Product(id=3, title=华为META20, category= 手机, brand=华为, price=4499.0, images=http://image.yx.com/12479122.jpg)
二. 原生查询
1.原生查询介绍
如果觉得上述接口依然不符合你的需求,SDE也支持原生查询。这个时候还是使用ElasticsearchTemplate,而查询条件的构建是通过一个名为NativeSearchQueryBuilder的类来完成的,不过这个类的底层还是使用的原生API中的QueryBuilders、AggregationBuilders、HighlightBuilders等工具来实现的。
2.原生查询案例
需求描述:
- 查询title中包含“手机”的商品;
- 且以价格升序进行排序;
- 进行分页查询:每页展示2条数据,查询第1页;
- 最后对查询结果进行聚合分析:获取品牌及个数。
1.在SpringDataESTests类中定义nativeQuery()方法。
@Test
public void nativeQuery() {
/* 1 查询结果 */
// 1.1 原生查询构建器
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 1.2 source过滤
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[0], new String[0]));
// 1.3 搜索条件
queryBuilder.withQuery(QueryBuilders.matchQuery("title", "手机"));
// 1.4 分页及排序条件
queryBuilder.withPageable(PageRequest.of(0, 2, Sort.by(Sort.Direction.ASC, "price")));
// 1.5 高亮显示
// TODO
// 1.6 聚合
queryBuilder.addAggregation(AggregationBuilders.terms("brandAgg").field("brand"));
// 1.7 构建查询条件,并且查询
AggregatedPage<Product> result = template.queryForPage(queryBuilder.build(), Product.class);
/* 2 解析结果 */
// 2.1 分页结果
long total = result.getTotalElements();
int totalPages = result.getTotalPages();
List<Product> list = result.getContent();
System.out.println("总条数:" + total);
System.out.println("总页数:" + totalPages);
System.out.println("数据:" + list);
// 2.2 聚合结果
Aggregations aggregations = result.getAggregations();
// 导包org.elasticsearch.search.aggregations.bucket.terms.Terms
Terms terms = aggregations.get("brandAgg");
terms.getBuckets().forEach(b -> {
System.out.println("品牌:" + b.getKeyAsString());
System.out.println("count:" + b.getDocCount());
});
}
注意:上述查询不支持高亮结果。
2.运行nativeQuery()方法,输出结果见下:
总条数:2
总页数:1
数据:[Product(id=1, title=小米手机Mix, category=手机, brand=小米, price=2899.0, images=http://image.yx.com/12479122.jpg), Product(id=2, title=坚果手机R1, category=手机, brand=锤子, price=3699.0, images=http://image.yx.com/12479122.jpg)]
品牌:小米
count:1
品牌:锤子
count:1
三. 高亮显示
1.高亮显示需求
需求描述:查询title中包含“小米手机”的商品,并将title中对应的分词通过<span style='color:red'></span>标签进行高亮修饰。
2.高亮显示实现
1.在com.yx.mapper包下自定义搜索结果映射ProductSearchResultMapper类。
package com.yx.mapper;
import com.google.gson.Gson;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.SearchResultMapper;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/** 自定义查询结果映射,用于处理高亮显示 */
public class ProductSearchResultMapper implements SearchResultMapper {
/**
* 完成查询结果映射。将_source取出,然后放入高亮的数据
*/
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse searchResponse, Class<T> aClass, Pageable pageable) {
// 记录总条数
long totalHits = searchResponse.getHits().getTotalHits();
// 记录列表(泛型) - 构建Aggregate使用
List<T> list = new ArrayList<>();
// 获取搜索结果(真正的的记录)
SearchHits hits = searchResponse.getHits();
for (SearchHit hit : hits) {
if (hits.getHits().length <= 0) {
return null;
}
// 将原本的JSON对象转换成Map对象
Map<String, Object> map = hit.getSourceAsMap();
// 获取高亮的字段Map
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
for (Map.Entry<String, HighlightField> highlightField : highlightFields.entrySet()) {
// 获取高亮的Key
String key = highlightField.getKey();
// 获取高亮的Value
HighlightField value = highlightField.getValue();
// 实际fragments[0]就是高亮的结果,无需遍历拼接
Text[] fragments = value.getFragments();
// 因为高亮的字段必然存在于Map中,就是key值
map.put(key, fragments[0].toString());
}
// 把Map转换成对象
Gson gson = new Gson();
T item = gson.fromJson(gson.toJson(map), aClass);
list.add(item);
}
// 返回的是带分页的结果
return new AggregatedPageImpl<>(list, pageable, totalHits);
}
}
2.高亮实现。重构nativeQuery()方法,使用自定义查询结果映射,来实现高亮显示。
@Test
public void nativeQuery() {
/* 1 查询结果 */
// 1.1 原生查询构建器
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 1.2 source过滤
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[0], new String[0]));
// 1.3 搜索条件
queryBuilder.withQuery(QueryBuilders.matchQuery("title", "手机"));
// 1.4 分页及排序条件
queryBuilder.withPageable(PageRequest.of(0, 2, Sort.by(Sort.Direction.ASC, "price")));
// 1.5 高亮显示
HighlightBuilder.Field field = new HighlightBuilder.Field("title");
field.preTags("<span style='color:red'>");
field.postTags("</span>");
queryBuilder.withHighlightFields(field);
// 1.6 聚合
queryBuilder.addAggregation(AggregationBuilders.terms("brandAgg").field("brand"));
// 1.7 构建查询条件,并且查询
// AggregatedPage<Product> result = template.queryForPage(queryBuilder.build(), Product.class);
AggregatedPage<Product> result = template.queryForPage(queryBuilder.build(), Product.class, new ProductSearchResultMapper());
/* 2 解析结果 */
// 2.1 分页结果
long total = result.getTotalElements();
int totalPages = result.getTotalPages();
List<Product> list = result.getContent();
System.out.println("总条数:" + total);
System.out.println("总页数:" + totalPages);
System.out.println("数据:" + list);
// 2.2 聚合结果
/*
Aggregations aggregations = result.getAggregations();
// 导包org.elasticsearch.search.aggregations.bucket.terms.Terms
Terms terms = aggregations.get("brandAgg");
terms.getBuckets().forEach(b -> {
System.out.println("品牌:" + b.getKeyAsString());
System.out.println("count:" + b.getDocCount());
});
*/
}
3.运行nativeQuery()测试方法后,输出结果见下。
总条数:2
总页数:1
数据:[Product(id=1, title=小米<span style='color:red'>手机</span>Mix, category=手机, brand=小米, price=2899.0, images=http://image.yx.com/12479122.jpg), Product(id=2, title=坚果<span style='color:red'>手机</span>R1, category=手机, brand=锤子, price=3699.0, images=http://image.yx.com/12479122.jpg)]
四. 结语
Spring Data Elasticsearch基于spring data API大大简化了Elasticsearch的操作,从而简化开发人员的代码,提高开发效率。然后我们给大家介绍了使用Spring Data对Elasticsearch进行了增、删、改、查操作。
同学们,关于Elasticsearch的所有内容袁老师就给大家介绍到这里。还有很多前沿技术等着大家去探索,“路漫漫其修远昔,吾将上下而求索”。最后,祝愿各位小伙伴未来学业有成,一切如意!
今天的内容就分享到这里吧。关注「袁庭新」,干货天天都不断!