浅谈Elasticsearch之Spring Data Elasticsearch

本文基于 elasticsearch 6.8.2spring boot 2.x

核心配置

pom.xml

1
2
3
4
<dependency>
<groupId> org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

application.yml

1
2
3
4
5
spring:
data:
elasticsearch:
cluster-name: elasticsearch
cluster-nodes: 127.0.0.1:9300

实体注解

@Document

@Document 作用在类,标记实体类为文档对象。

@Document 属性 说明
indexName 对应索引库名称。
type 对应在索引库中的类型。
shards 分片数量,默认5。
replicas 副本数量,默认1。

@Id

@Id 作用在成员变量,标记一个字段作为id主键。

@Field

@Field 作用在成员变量,标记为文档的字段,可以指定字段映射属性。

一般只要指定 text | keyword 即可,其他会自动识别。

@Field 属性 说明
type 字段类型,取值是枚举:FieldType
index 是否索引,布尔类型,默认是true 。
store 是否存储,布尔类型,默认是false 。
analyzer 分词器,如:ik_max_word

注解示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package tk.gushizone.elasticsearch.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
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;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = "item", type = "docs", shards = 1, replicas = 0)
public class Item {

@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;

Double price;

@Field(type = FieldType.Keyword, index = false)
String images;
}

Template 索引操作

ElasticsearchTemplate 可以完成索引相关操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@SpringBootTest
@RunWith(SpringRunner.class)
public class ElasticsearchApplicationTest {

@Autowired
private ElasticsearchTemplate elasticsearchTemplate;

@Test
public void testIndex() {
// 删除索引
elasticsearchTemplate.deleteIndex("item");

// 创建索引
elasticsearchTemplate.createIndex(Item.class);
// 配置索引
elasticsearchTemplate.putMapping(Item.class);
}
}

Repoitory 文档操作

众所周知, Spring Data 会提供 repository接口 用于 Dao操作,仅需要根据方法名就可以完成 CURD 等操作。

*Repository.java

1
2
3
4
5
6
7
8
9
10
package tk.gushizone.elasticsearch.dao;

import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import tk.gushizone.elasticsearch.pojo.Item;

import java.util.List;

public interface ItemRepository extends ElasticsearchRepository<Item, Long> {

}

保存 & 删除

保存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Test
public void testSave() {

Item item = new Item(1L, "小米手机", " 手机",
"小米", 3499.00, "http://image.leyou.com/13123.jpg");

itemRepository.save(item);


List<Item> list = new ArrayList<>();
list.add(new Item(2L, "坚果手机", " 手机", "锤子", 3699.00, "http://image.leyou.com/123.jpg"));
list.add(new Item(3L, "华为手机", " 手机", "华为", 4499.00, "http://image.leyou.com/3.jpg"));

itemRepository.saveAll(list);


List<Item> testList = new ArrayList<>();
testList.add(new Item(1L, "小米手机7", "手机", "小米", 3299.00, "http://image.leyou.com/13123.jpg"));
testList.add(new Item(2L, "坚果手机R1", "手机", "锤子", 3699.00, "http://image.leyou.com/13123.jpg"));
testList.add(new Item(3L, "华为META10", "手机", "华为", 4499.00, "http://image.leyou.com/13123.jpg"));
testList.add(new Item(4L, "小米Mix2S", "手机", "小米", 4299.00, "http://image.leyou.com/13123.jpg"));
testList.add(new Item(5L, "荣耀V10", "手机", "华为", 2799.00, "http://image.leyou.com/13123.jpg"));

itemRepository.saveAll(testList);
}

删除

1
2
3
4
5
6
7
@Test
public void testDelete() {

itemRepository.deleteById(1L);

itemRepository.deleteAll();
}

基础查询

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testFind() {

Optional<Item> item = itemRepository.findById(1L);
log.warn("item : {}", item);

Iterable<Item> list = itemRepository.findAll(Sort.by("price").descending());
log.warn("list : {}", Lists.newArrayList(list));

List<Item> item1 = itemRepository.findByTitle("小米");
log.warn("item1 : {}", item1);
}
1
2
3
item : Optional[Item(id=1, title=小米手机7, category=手机, brand=小米, price=3299.0, images=http://image.leyou.com/13123.jpg)]
list : [Item(id=3, title=华为META10, category=手机, brand=华为, price=4499.0, images=http://image.leyou.com/13123.jpg), Item(id=4, title=小米Mix2S, category=手机, brand=小米, price=4299.0, images=http://image.leyou.com/13123.jpg), Item(id=2, title=坚果手机R1, category=手机, brand=锤
item1 : [Item(id=1, title=小米手机7, category=手机, brand=小米, price=3299.0, images=http://image.leyou.com/13123.jpg), Item(id=4, title=小米Mix2S, category=手机, brand=小米, price=4299.0, images=http://image.leyou.com/13123.jpg)]

匹配查询

1
2
3
4
5
6
7
@Test
public void testQuery() {

MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("title", "手机");
Iterable<Item> items = itemRepository.search(matchQueryBuilder);
log.warn("Iterable<Item> : {}", Lists.newArrayList(items));
}
1
Iterable<Item> : [Item(id=1, title=小米手机7, category=手机, brand=小米, price=3299.0, images=http://image.leyou.com/13123.jpg), Item(id=2, title=坚果手机R1, category=手机, brand=锤子, price=3699.0, images=http://image.leyou.com/13123.jpg)]

自定义查询

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testNativeQuery() {

NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
queryBuilder.withQuery(QueryBuilders.matchQuery("title", "手机"));

Page<Item> itemPage = itemRepository.search(queryBuilder.build());
System.out.println(itemPage.getTotalPages());
System.out.println(itemPage.getTotalElements());
itemPage.getContent().forEach(System.out::println);
}
1
2
3
totalPages : 1, 
totalElements : 2,
content : [Item(id=1, title=小米手机7, category=手机, brand=小米, price=3299.0, images=http://image.leyou.com/13123.jpg), Item(id=2, title=坚果手机R1, category=手机, brand=锤子, price=3699.0, images=http://image.leyou.com/13123.jpg)]

分页查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testPage() {

NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
queryBuilder.withQuery(QueryBuilders.matchQuery("category", "手机"))
.withPageable(PageRequest.of(1, 2))
.withSort(SortBuilders.fieldSort("price").order(SortOrder.DESC));

Page<Item> itemPage = itemRepository.search(queryBuilder.build());
log.warn("totalPages : {}, totalElements : {}, content : {}",
itemPage.getTotalPages(),
itemPage.getTotalElements(),
itemPage.getContent());
}
1
2
3
totalPages : 3, 
totalElements : 5,
content : [Item(id=2, title=坚果手机R1, category=手机, brand=锤子, price=3699.0, images=http://image.leyou.com/13123.jpg), Item(id=1, title=小米手机7, category=手机, brand=小米, price=3299.0, images=http://image.leyou.com/13123.jpg)]

聚合查询

简单聚合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void testAgg(){
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 不查询任何结果
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{""}, null));
// 1、添加一个新的聚合,聚合类型为terms,聚合名称为brands,聚合字段为brand
queryBuilder.addAggregation(
AggregationBuilders.terms("brands").field("brand"));
// 2、查询,需要把结果强转为AggregatedPage类型
AggregatedPage<Item> aggPage = (AggregatedPage<Item>) this.itemRepository.search(queryBuilder.build());

// 3、解析
// 3.1、从结果中取出名为brands的那个聚合,
// 因为是利用String类型字段来进行的term聚合,所以结果要强转为StringTerm类型
StringTerms agg = (StringTerms) aggPage.getAggregation("brands");
// 3.2、获取桶
List<StringTerms.Bucket> buckets = agg.getBuckets();
// 3.3、遍历
for (StringTerms.Bucket bucket : buckets) {
// 3.4、获取桶中的key,即品牌名称 3.5、获取桶中的文档数量
System.out.println(bucket.getKeyAsString() + ",共" + bucket.getDocCount() + "台");
}

}
1
2
3
华为,共2台
小米,共2台
锤子,共1台

嵌套聚合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Test
public void testSubAgg(){
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 不查询任何结果
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{""}, null));
// 1、添加一个新的聚合,聚合类型为terms,聚合名称为brands,聚合字段为brand
queryBuilder.addAggregation(
AggregationBuilders.terms("brands").field("brand")
.subAggregation(AggregationBuilders.avg("priceAvg").field("price")) // 在品牌聚合桶内进行嵌套聚合,求平均值
);
// 2、查询,需要把结果强转为AggregatedPage类型
AggregatedPage<Item> aggPage = (AggregatedPage<Item>) this.itemRepository.search(queryBuilder.build());

// 3、解析
// 3.1、从结果中取出名为brands的那个聚合,
// 因为是利用String类型字段来进行的term聚合,所以结果要强转为StringTerm类型
StringTerms agg = (StringTerms) aggPage.getAggregation("brands");
// 3.2、获取桶
List<StringTerms.Bucket> buckets = agg.getBuckets();
// 3.3、遍历
for (StringTerms.Bucket bucket : buckets) {
// 3.4、获取桶中的key,即品牌名称 3.5、获取桶中的文档数量
System.out.println(bucket.getKeyAsString() + ",共" + bucket.getDocCount() + "台");

// 3.6.获取子聚合结果:
InternalAvg avg = (InternalAvg) bucket.getAggregations().asMap().get("priceAvg");
System.out.println("平均售价:" + avg.getValue());
}

}
1
2
3
华为, 共2台, 平均售价 : 3649.0
小米, 共2台, 平均售价 : 3799.0
锤子, 共1台, 平均售价 : 3699.0