Elasticsearch提供的Java客户端不太方便:
- 很多地方需要拼接json字符串;
- 需要自己把对象序列化为json存储;
- 查询到结果也需要自己反序列化为对象;
所以不适用原生的Java客户端,而使用Spring提供的Spring Data Elasticsearch
简介
Spring Data Elasticsearch是Spring Data项目下的一个子模块。
官网:http://projects.spring.io/spring-data/
Spring Data的使命是微数据访问提供熟悉且一致基于Spring的编程模型,同时仍保留底层数据存储的特殊特性;
Spring Data的使命是给各种数据访问提供统一的编程接口,不断试关系型数据库还是菲关系型数据库,或者类似于Elasticsearch这样的索引数据库,从而简化开发人员的代码,提高开发效率。
Spring Data Elasticsearch的特征
- 支持Spring 的基于
@Configuration
的java配置方式,或者xml配置方式; - 提供了用于操作ES的便捷工具类
ElasticsearchTemplate
,包括实现文档到POJO之间的自动智能映射; - 利用Spring的数据转换服务实现的功能丰富的对象映射;
- 基于注解的元数据映射方式,而且可扩展以支持更多不同的数据格式;
- 根据持久层接口自动生成对应实现方法,无需人工编写基本操作代码;
创建一个Demo工程
添加依赖
<?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.1.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>es-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>es-demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</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</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
实体类
@Data
public class Item {
Long id;
String title; //标题
String category; //分类
String brand; //品牌
Double price; //价格
String images; //图片地址
}
映射
SpringData 通过注解来声明字段的映射属性
-
@Document
作用在类,标记实体类为文档对象,一般有两个属性- indexName:对应的索引库名;
- type:对应在索引库中的类型;
- shards:分片数量,默认为5;
- replicas:副本数量,默认1
-
@Id
:作用在成员变量,标记一个字段为id主键; -
@Field
:作用在成员变量,标记为文档的字段,并指定字段映射属性:- type:字段类型,取值为枚举FieldType
- index:是否为索引,布尔类型,默认为true;
- store:是都存储,布尔类型,默认为false;
- analyzer:分词器名称
通过注解建立映射
@Data
@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; //品牌
@Field(type = FieldType.Double)
Double price; //价格
@Field(type = FieldType.Keyword,index = false)
String images; //图片地址
}
Template索引操作
创建索引和映射
@RunWith(SpringRunner.class)
@SpringBootTest(classes = EsDemoApplication.class)
public class indexTest {
@Autowired
private ElasticsearchTemplate template;
@Test
public void testCreate(){
//创建索引,会根据Item类的@Document注解信息来创建
template.createIndex(Item.class);
//配置映射,会根据Item类中的id,Field等字段来自动完成映射
template.putMapping(Item.class);
}
}
删除索引
可以根据类名或索引名删除;
Repository文档操作
Spring Data的强大之处在于不用写任何DAO处理,自动根据方法名或类的的信息进行CRUD,只要定义一个接口,然后继承Repository提供的一些子接口,就具备各种基本的CRUD功能;
创建接口并继承ElasticsearchRepository
public interface ItemRepository extends ElasticsearchRepository<Item,Long> {
}
新增文档
@Test
public void createDocument(){
Item item = new Item(1L, "小米手机7", " 手机",
"小米", 3499.00, "http://image.leyou.com/13123.jpg");
repository.save(item);
}
查看是否添加成功
批量新增
@Test
public void insertIndexList(){
ArrayList<Item> list = new ArrayList<>();
list.add(new Item(1L, "小米手机7", "手机", "小米", 3299.00, "http://image.leyou.com/13123.jpg"));
list.add(new Item(2L, "坚果手机R1", "手机", "锤子", 3699.00, "http://image.leyou.com/13123.jpg"));
list.add(new Item(3L, "华为META10", "手机", "华为", 4499.00, "http://image.leyou.com/13123.jpg"));
list.add(new Item(4L, "小米Mix2S", "手机", "小米", 4299.00, "http://image.leyou.com/13123.jpg"));
list.add(new Item(5L, "荣耀V10", "手机", "华为", 2799.00, "http://image.leyou.com/13123.jpg"));
// 接收对象集合,实现批量新增
repository.saveAll(list);
}
结果
{
"took": 10,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 5,
"max_score": 1,
"hits": [
{
"_index": "item",
"_type": "docs",
"_id": "1",
"_score": 1,
"_source": {
"id": 1,
"title": "小米手机7",
"category": "手机",
"brand": "小米",
"price": 3299,
"images": "http://image.leyou.com/13123.jpg"
}
},
{
"_index": "item",
"_type": "docs",
"_id": "2",
"_score": 1,
"_source": {
"id": 2,
"title": "坚果手机R1",
"category": "手机",
"brand": "锤子",
"price": 3699,
"images": "http://image.leyou.com/13123.jpg"
}
},
{
"_index": "item",
"_type": "docs",
"_id": "3",
"_score": 1,
"_source": {
"id": 3,
"title": "华为META10",
"category": "手机",
"brand": "华为",
"price": 4499,
"images": "http://image.leyou.com/13123.jpg"
}
},
{
"_index": "item",
"_type": "docs",
"_id": "4",
"_score": 1,
"_source": {
"id": 4,
"title": "小米Mix2S",
"category": "手机",
"brand": "小米",
"price": 4299,
"images": "http://image.leyou.com/13123.jpg"
}
},
{
"_index": "item",
"_type": "docs",
"_id": "5",
"_score": 1,
"_source": {
"id": 5,
"title": "荣耀V10",
"category": "手机",
"brand": "华为",
"price": 2799,
"images": "http://image.leyou.com/13123.jpg"
}
}
]
}
}
修改文档
修改和新增是同一个接口,区分的依据就是id,这一点跟在页面发起PUT请求时类似的;
基本查询
@Test
public void testFind(){
//查询所有,并根据价格降序
Iterable<Item> items = this.repository.findAll(Sort.by(Sort.Direction.DESC, "price"));
items.forEach(item -> System.out.println(item));
}
自定义方法
SpringData的另一个强大的功能是根据方法名称自动实现功能。比如方法名为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}}}} |
我们来按照价格区间查询,定义一个方法
public interface ItemRepository extends ElasticsearchRepository<Item,Long> {
List<Item> findByPriceBetween(double start,double end);
}
@Test
public void queryItemByPrice(){
List<Item> items = this.repository.findByPriceBetween(2000d, 3500d);
items.forEach(item -> System.out.println(item));
}
分页查询
利用NativeSearchQueryBuilder
可以方便的实现分页
@Test
public void testNativeQuery(){
//构建查询条件
NativeSearchQueryBuilder queryBuilder= new NativeSearchQueryBuilder();
//添加基本的分词查询
queryBuilder.withQuery(QueryBuilders.termQuery("category","手机"));
//添加分页参数
int page=0;
int size=3;
//设置分页参数
queryBuilder.withPageable(PageRequest.of(page,size));
//执行搜索,获取结果
Page<Item> items = this.repository.search(queryBuilder.build());
//打印总条数
System.out.println(items.getTotalElements());
//打印总页数
System.out.println(items.getTotalPages());
//每页大小
System.out.println(items.getSize());
//当前页
System.out.println(items.getNumber());
items.forEach(System.out::println);
}
排序
排序也可以通过NativeSearchQueryBuilder来完成
@Test
public void testSort(){
//构建查询条件
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//添加基本分词查询
queryBuilder.withQuery(QueryBuilders.termQuery("category","手机"));
queryBuilder.withSort(SortBuilders.fieldSort("price").order(SortOrder.DESC));
//指定搜索结果
Page<Item> items = this.repository.search(queryBuilder.build());
System.out.println(items.getTotalElements());
items.forEach(System.out::println);
}
聚合
聚合为桶
@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、查询,需要把结果强转为AggregatePage类型
AggregatedPage<Item> aggPage = (AggregatedPage <Item>)this.repository.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) {
//获取桶中的key,即品牌名称
System.out.println(bucket.getKeyAsString());
//获取桶中的文档数量
System.out.println(bucket.getDocCount());
}
}
关键API
AggregationBuilders
:聚合的构建工厂类,所有聚合都由这个类来构建;
查询JSON结果和Java类的对照关系
嵌套聚合,求平均值
@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.repository.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) {
//获取桶中的key,即品牌名称
System.out.println(bucket.getKeyAsString()+",共"+bucket.getDocCount()+"台");
//获取子聚合结果
InternalAvg priceAvg =(InternalAvg) bucket.getAggregations().asMap().get("priceAvg");
System.out.println("平均售价:"+priceAvg.getValue());
}
}