配置并连接
1 配置文件
pom依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
这里特别解释一下!
failed to load elasticsearch nodes : org.elasticsearch.client.transport.NoNodeAvailableException: None of the configured nodes are available: [{#transport#-1}{m-smEJI0TKOTPDV_eVS1hQ}{127.0.0.1}{127.0.0.1:9300}]
当初我报了这个错误,以为是我的配置问题,疯狂去找elasticsearch 的问题,最后才知道是版本问题!这是我遇见的错误和解决方法,记录一下。
原因如下:
首先我的elasticsearch版本是elasticsearch 7.x,我的SpringBoot版本是2.1.8,注意:SpringBoot是2.2.0.RELEASE才兼容elasticsearch 7.x
解决如下:
方式一:升级springboot到2.2.0及以上
方式二:降低elasticsearch版本到自己项目中对于jar的版本
在application.yml中添加
spring:
data:
elasticsearch:
cluster-name: docker-cluster
cluster-nodes: xxx.xxx.xxx.xx:9300
cluster-name 是你的集群名称
可以在你的elasticsearch的容器下的configs文件下的elasticsearch.yml可以看到
cluster-nodes 是你的路径,注意使用kibana连接elasticsearch时,它默认连接的端口时9200,而Java想去连接elasticsearch时,端口时9300!不然会报错误的。
2 启动测试
配置完这些就可以启动测试一下了。如果能够正常启动,就说明你的连接可以了。
3 创建索引测试
测试因为不需要访问,就直接在test下就可以了!
elasticsearch具体我就不解释了,想了解的可以搜一下。
3.1创建索引和映射
首先我们准备好实体类:
package com.leyou.es.pojo;
public class Item {
Long id;
String title; //标题
String category;// 分类
String brand; // 品牌
Double price; // 价格
String images; // 图片地址
}
映射
Spring Data通过注解来声明字段的映射属性,有下面的三个注解:
@Document
作用在类,标记实体类为文档对象,一般有两个属性- indexName:对应索引库名称
- type:对应在索引库中的类型
- shards:分片数量,默认5
- replicas:副本数量,默认1
@Id
作用在成员变量,标记一个字段作为id主键@Field
作用在成员变量,标记为文档的字段,并指定字段映射属性:- type:字段类型,是是枚举:FieldType
- index:是否索引,布尔类型,默认是true
- store:是否存储,布尔类型,默认是false
- analyzer:分词器名称
示例:
@AllArgsConstructor
@NoArgsConstructor
@Data
@Document(indexName = "big",type = "item", shards = 1, replicas = 0)
public class Item {
@Id
@Field(type = FieldType.Long)
private Long id;
@Field(type = FieldType.Text, analyzer = "ik_smart")
private String title; //标题
@Field(type = FieldType.Keyword)
private String category;// 分类
@Field(type = FieldType.Keyword)
private String brand; // 品牌
@Field(type = FieldType.Double)
private Double price; // 价格
@Field(type = FieldType.Keyword,index = false)
private String images; // 图片地址
}
3.2 创建索引
ElasticsearchTemplate中提供了创建索引的API:
一样,可以根据类的字节码信息(注解配置)来生成映射,或者手动编写映射
我们这里采用类的字节码信息创建索引并映射:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = EsApplication.class,webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class EsTest {
@Autowired
public ElasticsearchTemplate template;
@Test
public void testCreate(){
// 创建索引,会根据Item类的@Document注解信息来创建
template.createIndex(Item.class);
// 配置映射,会根据Item类中的id、Field等字段来自动完成映射
template.putMapping(Item.class);
}
可以在kibana中查看:
结果为
GET /big
{
"item": {
"aliases": {},
"mappings": {
"docs": {
"properties": {
"brand": {
"type": "keyword"
},
"category": {
"type": "keyword"
},
"images": {
"type": "keyword",
"index": false
},
"price": {
"type": "double"
},
"title": {
"type": "text",
"analyzer": "ik_smart"
}
}
}
},
"settings": {
"index": {
"refresh_interval": "1s",
"number_of_shards": "1",
"provided_name": "item",
"creation_date": "1525405022589",
"store": {
"type": "fs"
},
"number_of_replicas": "0",
"uuid": "4sE9SAw3Sqq1aAPz5F6OEg",
"version": {
"created": "6020499"
}
}
}
}
}
3.3 删除索引
可以根据类名或索引名删除。
示例:
@Test
public void deleteIndex() {
template.deleteIndex("big");
}
结果:
4 新增文档数据
4.1 Repository接口
Spring Data 的强大之处,就在于你不用写任何DAO处理,自动根据方法名或类的信息进行CRUD操作。只要你定义一个接口,然后继承Repository提供的一些子接口,就能具备各种基本的CRUD功能。
来看下Repository的继承关系:
看到有一个ElasticsearchCrudRepository接口:
所以,只需要定义接口,然后继承它就OK了。
4.2 新增一个对象
package com.leyou.es.demo;
@Autowired
public ItemRepository itemRepository;
@Test
public void index() {
Item item = new Item(1L, "小米手机7", " 手机",
"小米", 3499.00, "http://image.leyou.com/13123.jpg");
itemRepository.save(item);
}
去页面查询看看:
{
"took": 0,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 1,
"hits": [
{
"_index": "item",
"_type": "docs",
"_id": "1",
"_score": 1,
"_source": {
"id": 1,
"title": "小米手机7",
"category": " 手机",
"brand": "小米",
"price": 3499,
"images": "http://image.leyou.com/13123.jpg"
}
}
}
]
}
}
4.3 批量新增
代码:
@Test
public void indexList() {
List<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"));
// 接收对象集合,实现批量新增
itemRepository.saveAll(list);
}
可以去kibana看看
4.4 修改
修改和新增是同一个接口,区分的依据就是id,这一点跟我们在页面发起PUT请求是类似的。
4.5 .删除
package com.leyou.es.demo;
@Test
public void delete(){
itemRepository.deleteById(2L);
}
5. 查询
5.1 基本查询
ElasticsearchRepository提供了一些基本的查询方法:
来试试查询所有:
@Test
public void testFind(){
Iterable<Item> all=itemRepository.findAll();
for (Item item:all){
System.out.println("item="+item);
}
}
@Test
public void query(){
// 查询全部,并按照价格降序排序
Iterable<Item> items = this.itemRepository.findAll(Sort.by("price").descending());
for (Item item : items) {
System.out.println("item = " + item);
}
}
结果:
5.2.自定义方法
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}}}} |
例如,我们来按照价格区间查询,定义这样的一个方法:
package com.leyou.es.repository;
public interface ItemRepository extends ElasticsearchRepository<Item,Long>{
/**
* 根据价格区间查询
* @param begin
* @param end
* @return
*/
List<Item> findByPriceBetween(Double begin, Double end);
}
真的赞这功能!
不需要写实现类,然后我们直接去运行:
@Test
public void testFindBy(){
List<Item> items=this.itemRepository.findByPriceBetween(2000d,4000d);
for (Item item : items) {
System.out.println("item = " + item);
}
}
结果:
5.3.自定义查询
先来看最基本的match query:
package com.leyou.es.demo;
@Test
public void search(){
// 构建查询条件
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 添加基本分词查询
queryBuilder.withQuery(QueryBuilders.matchQuery("title", "小米手机"));
// 搜索,获取结果
Page<Item> items = this.itemRepository.search(queryBuilder.build());
// 总条数
long total = items.getTotalElements();
System.out.println("total = " + total);
for (Item item : items) {
System.out.println(item);
}
}
-
NativeSearchQueryBuilder:Spring提供的一个查询条件构建器,帮助构建json格式的请求体
-
QueryBuilders.matchQuery(“title”, “小米手机”):利用QueryBuilders来生成一个查询。QueryBuilders提供了大量的静态方法,用于生成各种不同类型的查询:
Page<item>
:默认是分页查询,因此返回的是一个分页的结果对象,包含属性: -
totalElements:总条数
-
totalPages:总页数
-
Iterator:迭代器,本身实现了Iterator接口,因此可直接迭代得到当前页的数据
-
其它属性:
结果:
5.4 分页查询
利用NativeSearchQueryBuilder
可以方便的实现分页:
package com.leyou.es.demo;
@Test
public void searchByPage(){
// 构建查询条件
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 添加基本分词查询
queryBuilder.withQuery(QueryBuilders.termQuery("category", "手机"));
// 分页:从0开始
int page = 0;
int size = 2;
queryBuilder.withPageable(PageRequest.of(page,size));
// 搜索,获取结果
Page<Item> items = this.itemRepository.search(queryBuilder.build());
// 总条数
System.out.println("总条数 = " + items.getTotalElements());
// 总页数
System.out.println("总页数 = " + items.getTotalPages());
// 当前页
System.out.println("当前页:" + items.getNumber());
// 每页大小
System.out.println("每页大小:" + items.getSize());
for (Item item : items) {
System.out.println(item);
}
}
结果:
5.5.排序
排序也通用通过NativeSearchQueryBuilder
完成:
package com.leyou.es.demo;
@Test
public void searchAndSort(){
// 构建查询条件
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 添加基本分词查询
queryBuilder.withQuery(QueryBuilders.termQuery("category", "手机"));
// 排序
queryBuilder.withSort(SortBuilders.fieldSort("price").order(SortOrder.ASC));
// 搜索,获取结果
Page<Item> items = this.itemRepository.search(queryBuilder.build());
// 总条数
long total = items.getTotalElements();
System.out.println("总条数 = " + total);
for (Item item : items) {
System.out.println(item);
}
}
结果:
5.6.聚合
5.6.1.聚合为桶
桶就是分组,比如这里我们按照品牌brand进行分组:
package com.leyou.es.demo;
@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,即品牌名称
System.out.println(bucket.getKeyAsString());
// 3.5、获取桶中的文档数量
System.out.println(bucket.getDocCount());
}
}
显示的结果:
关键API:
AggregationBuilders
:聚合的构建工厂类。所有聚合都由这个类来构建,看看他的静态方法:
AggregatedPage
:聚合查询的结果类。它是Page<T>
的子接口:
AggregatedPage
在Page
功能的基础上,拓展了与聚合相关的功能,它其实就是对聚合结果的一种封装,大家可以对照聚合结果的JSON结构来看。
而返回的结果都是Aggregation类型对象,不过根据字段类型不同,又有不同的子类表示
我们看下页面的查询的JSON结果与Java类的对照关系: