Spring Data Elasticsearch
环境
Elasticsearch7.8.0
SpringBoot2.3.6
1 创建测试环境
1.1 添加依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.iforeverhz</groupId>
<artifactId>es-spring</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
1.2 添加配置
@Configuration
public class ElasticsearchConfig extends AbstractElasticsearchConfiguration {
@Override
public RestHighLevelClient elasticsearchClient() {
RestHighLevelClient restHighLevelClient = new RestHighLevelClient(
RestClient.builder(new HttpHost("localhost",9200))
);
return restHighLevelClient;
}
}
2 实体类及注解
@Data
@AllArgsConstructor
@NoArgsConstructor
@Document(indexName = "goods" ,shards = 3,replicas = 1 )
public class Item {
@Field(type = FieldType.Long)
@Id
Long id;
@Field(type = FieldType.Text,analyzer = "ik_max_word")
String title;
@Field(type = FieldType.Keyword)
String category;
@Field(type = FieldType.Keyword)
String brand;
@Field(type = FieldType.Double)
Double price;
@Field(index = false, type = FieldType.Keyword )
String images;
}
Spring Data通过注解来声明字段的映射属性,有下面的三个注解:
@Document 作用在类,标记实体类为文档对象,一般有两个属性
- indexName:对应索引库名称
- shards:分片数量,默认1
- replicas:副本数量,默认1
@Id 作用在成员变量,标记一个字段作为id主键@Field 作用在成员变量,标记为文档的字段,并指定字段映射属性:
- type:字段类型,取值是枚举:FieldType
- index:是否索引,布尔类型,默认是true
- store:是否存储,布尔类型,默认是false
- analyzer:分词器名称
3 创建Repository
@Repository
public interface ItemRepository extends ElasticsearchRepository<Item,Long> {
List<Item> findByPriceBetween(double price1, double price2);
}
ElasticsearchRepository继承关系
4 索引操作
4.1 创建索引
public void createIndex() {
IndexOperations indexOperations = template.indexOps(Item.class);
if (indexOperations.exists()) {
System.out.println("索引已存在");
return;
}
indexOperations.create();
System.out.println("索引创建成功");
}
4.2 删除索引
public void deleteIndex() {
IndexOperations indexOperations = template.indexOps(Item.class);
if (indexOperations.exists()) {
if (indexOperations.delete()) {
System.out.println("索引删除成功");
}else {
System.out.println("索引删除失败");
}
return;
}
System.out.println("索引不存在");
}
5 文档操作
5.1 新增文档
public void addDoc() {
Item item = new Item(1L, "小米手机7", "手机", "小米", 4299.00, "http://image.iforeverhz.com/13123.jpg");
Item save = repository.save(item);
System.out.println(save);
}
5.2 批量新增文档
public void addBulkDoc() {
List<Item> list = new ArrayList<>();
list.add(new Item(1L, "小米手机7", "手机", "小米", 3299.00, "http://image.iforeverhz.com/13123.jpg"));
list.add(new Item(2L, "坚果手机R1", "手机", "锤子", 3699.00, "http://image.iforeverhz.com/13123.jpg"));
list.add(new Item(3L, "华为META10", "手机", "华为", 4499.00, "http://image.iforeverhz.com/13123.jpg"));
list.add(new Item(4L, "小米Mix2S", "手机", "小米", 4299.00, "http://image.iforeverhz.com/13123.jpg"));
list.add(new Item(5L, "荣耀V10", "手机", "华为", 2799.00, "http://image.iforeverhz.com/13123.jpg"));
list.add(new Item(6L, "小米手机7", "手机", "小米", 3299.00, "http://image.iforeverhz.com/13123.jpg"));
list.add(new Item(7L, "坚果手机R1", "手机", "锤子", 3699.00, "http://image.iforeverhz.com/13123.jpg"));
list.add(new Item(8L, "华为META10", "手机", "华为", 4499.00, "http://image.iforeverhz.com/13123.jpg"));
list.add(new Item(9L, "小米Mix2S", "手机", "小米", 4299.00, "http://image.iforeverhz.com/13123.jpg"));
list.add(new Item(10L, "荣耀V10", "手机", "华为", 2799.00, "http://image.iforeverhz.com/13123.jpg"));
repository.saveAll(list);
}
5.3 修改文档
public void updateDoc() {
Item item = new Item(1L, "小米手机7", "手机", "小米", 5299.00, "http://image.iforeverhz.com/13123.jpg");
Item save = repository.save(item);
System.out.println(save);
}
5.4 删除文档
public void delete() {
repository.deleteById(1L);
Item item = new Item();
item.setId(2l);
repository.delete(item);
repository.deleteAll();
List<Item> list = new ArrayList<>();
list.add(new Item(1L, "小米手机7", "手机", "小米", 3299.00, "http://image.iforeverhz.com/13123.jpg"));
list.add(new Item(2L, "坚果手机R1", "手机", "锤子", 3699.00, "http://image.iforeverhz.com/13123.jpg"));
repository.deleteAll(() -> list.stream().iterator());
}
5.5 基本查询
5.5.1 查询所有
public void findAll() {
Iterable<Item> items = repository.findAll();
items.forEach(item -> System.out.println(item));
Iterable<Item> items1 = repository.findAll(Sort.by(Sort.Direction.ASC, "price"));
Iterable<Item> items2 = repository.findAll(Sort.by(Sort.Direction.DESC, "price"));
items1.forEach(System.out::println);
items2.forEach(System.out::println);
int page = 0;
int size = 3;
Page<Item> itemPage = repository.findAll(PageRequest.of(page, size));
List<Item> items4 = itemPage.getContent();
int totalPages = itemPage.getTotalPages();
long totalElements = itemPage.getTotalElements();
System.out.println(totalElements+"==="+totalPages);
items4.forEach(System.out::println);
}
5.5.2 根据id查询
public void findById() {
Optional<Item> optionalItem = repository.findById(1L);
optionalItem.ifPresent(item -> System.out.println(item));
}
5.5.3 自定义方法查询
Spring Data 的另一个强大功能,是根据方法名称自动实现功能。
比如:你的方法名叫做:findByTitle,那么它就知道你是根据title查询,然后自动帮你完成,无需写实现类。
当然,方法名称要符合一定的约定:
Keyword | Sample | Elasticsearch Query String |
---|
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" : "?"}}}} | Between | 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}}}} |
例如,我们来按照价格区间查询,定义这样的一个方法:
@Repository
public interface ItemRepository extends ElasticsearchRepository<Item,Long> {
List<Item> findByPriceBetween(double price1, double price2);
}
public void query() {
List<Item> items = repository.findByPriceBetween(2000, 3500D);
items.forEach(System.out::println);
}
5.6 高级查询
5.6.1基本查询
public void termQuery() {
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("brand", "小米");
Iterable<Item> items = repository.search(termQueryBuilder);
items.forEach(System.out::println);
}
QueryBuilders提供了大量的静态方法,用于生成各种不同类型的查询对象,例如:词条、模糊、通配符等QueryBuilder对象。
5.6.2 分页&排序&结果过滤查询
使用repository
public void testQuery() {
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id", "title", "price"}, null));
queryBuilder.withQuery(QueryBuilders.matchQuery("title", "小米手机"));
queryBuilder.withSort(SortBuilders.fieldSort("price").order(SortOrder.DESC));
queryBuilder.withPageable(PageRequest.of(0, 2));
Page<Item> items = repository.search(queryBuilder.build());
long totalElements = items.getTotalElements();
int totalPages = items.getTotalPages();
System.out.println("总条数:" + totalElements);
System.out.println("总页数:" + totalPages);
items.forEach(item -> System.out.println(item));
}
使用template
public void testQuery1() {
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id", "title", "price"}, null));
queryBuilder.withQuery(QueryBuilders.matchQuery("title", "小米手机"));
queryBuilder.withSort(SortBuilders.fieldSort("price").order(SortOrder.DESC));
queryBuilder.withPageable(PageRequest.of(0, 2));
SearchHits<Item> searchHits = template.search(queryBuilder.build(), Item.class);
long totalHits = searchHits.getTotalHits();
List<Item> itemList = searchHits.get()
.map(SearchHit::getContent)
.collect(Collectors.toList());
System.out.println(totalHits);
itemList.forEach(System.out::println);
}
NativeSearchQueryBuilder:Spring提供的一个查询条件构建器,帮助构建json格式的请求体
5.6.3 聚合查询
5.6.3.1 聚合为桶
不推荐使用repository
public void testAgg(){
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{""}, null));
queryBuilder.addAggregation(
AggregationBuilders.terms("brands").field("brand"));
AggregatedPage<Item> aggPage = (AggregatedPage<Item>) repository.search(queryBuilder.build());
ParsedStringTerms agg = (ParsedStringTerms) aggPage.getAggregation("brands");
List<? extends Terms.Bucket> buckets = agg.getBuckets();
for (Terms.Bucket bucket : buckets) {
System.out.println(bucket.getKeyAsString()+"==="+bucket.getDocCount());
}
}
推荐使用template
public void testAgg() {
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{}, null));
queryBuilder.addAggregation(AggregationBuilders.terms("brands").field("brand"));
SearchHits<Item> searchHits = template.search(queryBuilder.build(), Item.class);
Aggregations aggregations = searchHits.getAggregations();
Terms agg = (Terms) aggregations.get("brands");
for (Terms.Bucket bucket : agg.getBuckets()) {
String key = bucket.getKeyAsString();
long count = bucket.getDocCount();
System.out.println(key + "==" + count);
}
}
结果
5.6.3.2 嵌套聚合
根据品牌求价格平均值
public void testAgg() {
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{}, null));
queryBuilder.addAggregation(
AggregationBuilders.terms("brands").field("brand")
.subAggregation(AggregationBuilders.avg("priceAvg").field("price"))
);
SearchHits<Item> searchHits = template.search(queryBuilder.build(), Item.class);
Aggregations aggregations = searchHits.getAggregations();
Terms agg = (Terms) aggregations.get("brands");
for (Terms.Bucket bucket : agg.getBuckets()) {
String key = bucket.getKeyAsString();
long count = bucket.getDocCount();
ParsedAvg priceAvg = bucket.getAggregations().get("priceAvg");
System.out.println(key + "==" + count + "==" + priceAvg.getValue());
}
}
根据品牌求价格总合
public void testAgg() {
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{}, null));
queryBuilder.addAggregation(
AggregationBuilders.terms("brands").field("brand")
.subAggregation(AggregationBuilders.avg("priceAvg").field("price"))
.subAggregation(AggregationBuilders.sum("priceSum").field("price"))
);
SearchHits<Item> searchHits = template.search(queryBuilder.build(), Item.class);
Aggregations aggregations = searchHits.getAggregations();
Terms agg = (Terms) aggregations.get("brands");
for (Terms.Bucket bucket : agg.getBuckets()) {
String key = bucket.getKeyAsString();
long count = bucket.getDocCount();
ParsedAvg priceAvg = bucket.getAggregations().get("priceAvg");
ParsedSum priceSum = bucket.getAggregations().get("priceSum");
System.out.println(key + "==" + count +"=="+priceSum.getValue()+"=="+ priceAvg.getValue());
}
}
结果
|