Spring Data Elasticsearch
Spring Data ElasticSearch 基于 spring data API 简化 elasticSearch操作,将原始操elasticSearch的客户端API 进行封装 。
官方网站:Spring Data Elasticsearch
spring-data-Elasticsearch 使用之前,必须先确定版本,elasticsearch 对版本的要求比较高。
springboot和elasticsearch的版本对照:
1、环境搭建—依赖、配置
创建工程 elasticsearch-springdata-es
1.1引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
<version>2.3.7.RELEASE</version>
</dependency>
1.2 配置文件application.properties
# es服务地址
elasticsearch.host=127.0.0.1
# es服务端口
elasticsearch.port=9200
# 配置日志级别,开启debug日志
logging.level.com.atguigu=debug
或者如下:(谷粒商城项目)
#单机情况下
spring.elasticsearch.rest.uris=http://127.0.0.1:9200
1.3主启动
@SpringBootApplication
public class SpringDataESApplication {
public static void main(String[] args) {
SpringApplication.run(SpringDataESApplication.class,args);
}
}
1.4配置类
ElasticsearchRestTemplate基于RestHighLevelClient客户端的。需要自定义配置类,继承AbstractElasticsearchConfiguration,并实现elasticsearchClient()抽象方法,创建RestHighLevelClient对象
@ConfigurationProperties(prefix = "elasticsearch")
@Configuration
@Getter
@Setter
public class ElasticsearchConfig extends AbstractElasticsearchConfiguration {
private String host ;
private Integer port ;
//重写父类方法
@Override
public RestHighLevelClient elasticsearchClient() {
//高级客户端
RestClientBuilder builder = RestClient.builder(new HttpHost(host, port));
RestHighLevelClient restHighLevelClient = new RestHighLevelClient(builder);
return restHighLevelClient;
}
}
2、ES中的实体类
Spring Data ES中实体类可是顶大用啊。Spring Data通过注解来声明字段的映射属性,有下面的三个注解:
@Document 作用在类,标记实体类为文档对象,属性解释如下:
indexName:对应索引库名称 shards:分片数量,默认1 replicas:副本数量,默认1
@Id 作用在成员变量,标记一个字段作为id主键
@Field 作用在成员变量,标记为文档的字段,并指定字段映射属性:
type:字段类型,取值是枚举:如 FieldType.Text index:是否索引,布尔类型,默认是true store:是否存储,布尔类型,默认是false analyzer:分词器名称:如 ik_max_word
@Data @NoArgsConstructor @AllArgsConstructor @Document(indexName = "item",shards = 1, replicas = 1) public class Item { @Id private Long id; @Field(type = FieldType.Text, analyzer = "ik_max_word") 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(index = false, type = FieldType.Keyword) private String images; // 图片地址 }
运行如下测试类,控制台打印
@RunWith(SpringRunner.class) @SpringBootTest public class SpringDataESTest { @Autowired private ElasticsearchRestTemplate elasticsearchTemplate; @Test public void test0(){ System.out.println(elasticsearchTemplate); } }
3、创建索引库和映射关系
1.创建索引库
// ElasticsearchTemplate是TransportClient客户端 已过期
// ElasticsearchRestTemplate是RestHighLevel客户端
@Autowired
ElasticsearchRestTemplate restTemplate;
@Test
public void testCreate() {
// 创建索引库,会根据Item类的@Document注解信息来创建
elasticsearchTemplate.createIndex(Goods.class);
// 配置映射,会根据Item类中的id、Field等字段来自动完成映射
elasticsearchTemplate.putMapping(Goods.class);
}
但是上述代码会这样
所以就还有一种创建索引库方式
写法2(谷粒商城)
@Autowired
ElasticsearchRestTemplate elasticsearchTemplate;
@Test
public void test2(){
// 索引库操作对象
IndexOperations ops = elasticsearchTemplate.indexOps(User.class);
ops.create(); // 初始化索引库
ops.putMapping(ops.createMapping(User.class)); // 更新索引的映射
}
4、文档操作
1.添加文档
//2、存入文档
@Test
void save(){
User user = new User();
user.setAge(20);
user.setId("2");
user.setName("qiqi");
user.setBirthday(new Date());
user.setSalary(10000.0);
elasticsearchTemplate.save(user);
}
2.根据id查询文档
//查询指定id的文档
@Test
void findById(){
User user = elasticsearchTemplate.get("2", User.class);
System.out.println(user);
}
3.查询全部
NativeSearchQueryBuilder:Spring提供的一个查询条件构建器,帮助构建json格式的请求体
//4、查询所有的goods文档
@Test
void queryAll(){
//构建查询的DSL需要通过
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
// builder.withQuery() 添加查询方式
// builder.addAggregation() 聚合查询
// builder.withFields() 结果过滤
// builder.withPageable() 分页
// builder.withFilter() 过滤查询
// builder.withHighlightBuilder() 高亮查询
// builder.withSort() 排序查询
builder.withQuery(QueryBuilders.matchAllQuery());
Query query = builder.build();
//执行查询
SearchHits<Goods> search = elasticsearchTemplate.search(query, Goods.class);
search.getSearchHits().forEach(goodsSearchHit -> {
System.out.println(goodsSearchHit.getContent());
});
}
5、ElasticsearchRestTemplate综合
@Autowired
ElasticsearchRestTemplate elasticsearchTemplate;
@Test
void queryByCondition(){
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
//模糊查询:查询标题中带apple
//.must(QueryBuilders.termQuery("attr.brand.keyword" , "小米")
// boolQueryBuilder.must(QueryBuilders.matchQuery("title","小米手机"));
boolQueryBuilder.must(QueryBuilders.fuzzyQuery("title","appl")
.fuzziness(Fuzziness.TWO));
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price")
.gte(999).lte(3999));
builder.withQuery(boolQueryBuilder);
//高亮
builder.withHighlightBuilder(new HighlightBuilder()
.field("title")
.preTags("<em>").postTags("</em>"));
//分页查询: PageRequest.of(0,5) 参数1代表查询的起始索引
builder.withPageable(PageRequest.of(0,5));
//排序
builder.withSort(SortBuilders.fieldSort("price")
.order(SortOrder.DESC));
//结果过滤:查询指定的字段
builder.withFields("title","id","attr.brand","price");
//使用品牌聚合,统计每个品牌的平均价格
builder.addAggregation(AggregationBuilders.terms("brands")
.field("attr.brand.keyword")
.subAggregation(AggregationBuilders.avg("avgPrice")
.field("price"))
.subAggregation(AggregationBuilders.terms("cates")
.field("attr.category.keyword")));
Query query = builder.build();
SearchHits<Goods> searchHits = elasticsearchTemplate.search(query, Goods.class);
//解析hits结果集
for (SearchHit<Goods> searchHit : searchHits.getSearchHits()) {
Goods goods = searchHit.getContent();
//高亮的标题设置给goods
List<String> field = searchHit.getHighlightField("title");
String join = StringUtils.join(field, "");
goods.setTitle(join);
//输出查询完毕的goods内容
System.out.println(goods);
}
//解析聚合结果
Aggregations aggregations = searchHits.getAggregations();
Map<String, Aggregation> map = aggregations.asMap();
ParsedStringTerms brands = (ParsedStringTerms) map.get("brands");
// System.out.println(brands.getClass().getName());
List<? extends Terms.Bucket> buckets = brands.getBuckets();//获取所有的brand品牌桶
for (Terms.Bucket bucket : buckets) {
System.out.println("品牌: "+bucket.getKey()+" 数量:"+bucket.getDocCount());
ParsedAvg avgPrice = (ParsedAvg) bucket.getAggregations().asMap().get("avgPrice");
// System.out.println(avgPrice.getClass().getName());
System.out.println("平均价格:"+avgPrice.getValue());
ParsedStringTerms cates = (ParsedStringTerms) bucket.getAggregations().asMap().get("cates");
for (Terms.Bucket catesBucket : cates.getBuckets()) {
System.out.println("分类:"+ catesBucket.getKey()+" , 数量:"+catesBucket.getDocCount());
}
System.out.println("==================================================");
}
}
6、RestHighLevelClient 综合
如果是6.8.x推荐使用原生RestHighLevelClient客户端
RestHighLevelClient还有一个好处,就是可以输出它生成的DSL语句
//比较偏原生一些
@Autowired
RestHighLevelClient restHighLevelClient;
/**
* 指定条件的查询
* @throws IOException
*/
@Test
void queryByCondition2() throws IOException {
SearchRequest searchRequest = new SearchRequest("goods");//一定要指定索引库
//DSL语句的构建器
SearchSourceBuilder builder = new SearchSourceBuilder();
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//标题匹配查询:小米手机
boolQuery.should(QueryBuilders.matchQuery("title","小米手机").operator(Operator.OR));
boolQuery.should(QueryBuilders.fuzzyQuery("title","华为").fuzziness(Fuzziness.ONE));
//价格区间查询:1999~3999(不能影响文档评分)
boolQuery.filter(QueryBuilders.rangeQuery("price")
.gte(1999).lte(3999));
builder.query(boolQuery);
//标题高亮显示
builder.highlighter(new HighlightBuilder()
.field("title")
.preTags("<font style='color:red'>")
.postTags("</font>"));
//分页查询第一页 前5条
builder.from(0);
builder.size(5);
//结果过滤: 只查询title price id attr.category分类
builder.fetchSource(new String[]{
"title","id","price","attr.brand","attr.category"
} , null);
//按照价格降序排列
builder.sort("price",SortOrder.DESC);
//使用品牌聚合,统计每个品牌的平均价格
builder.aggregation(AggregationBuilders.terms("品牌桶")
.field("attr.brand.keyword")
.subAggregation(AggregationBuilders.avg("平均价格")
.field("price")));
searchRequest.source(builder);
//发起请求获取结果
SearchResponse search = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
//解析结果
org.elasticsearch.search.SearchHit[] hits = search.getHits().getHits();
Gson gson = new Gson();
for (org.elasticsearch.search.SearchHit hit : hits) {
// System.out.println(hit.getSourceAsString());
Goods goods = gson.fromJson(hit.getSourceAsString(), Goods.class);
HighlightField field = hit.getHighlightFields().get("title");
Text[] texts = field.getFragments();
goods.setTitle(StringUtils.join(texts, ""));
System.out.println(goods);
}
ParsedStringTerms brands = (ParsedStringTerms) search.getAggregations().asMap().get("品牌桶");
for (Terms.Bucket bucket : brands.getBuckets()) {
System.out.println(bucket.getKey()+" , "+ bucket.getDocCount());
ParsedAvg avgPrice = (ParsedAvg) bucket.getAggregations().asMap().get("平均价格");
System.out.println(avgPrice.getType()+" , "+ avgPrice.getValue());
}
}
7、Repository文档操作(推荐)
Spring Data 的另一个强大功能,是根据方法名称自动实现功能。
只要你定义一个接口,然后继承Repository提供的一些子接口,就能具备各种基本的CRUD功能。
public interface UserRepository extends ElasticsearchRepository<User, Long> { }
使用repository新增文档、删除文档、查询文档
@Autowired
UserRepository userRepository;
@Test
void testAdd(){
User user = new User();
user.setAge(20);
user.setId("1");
user.setName("zhang3");
user.setBirthday(new Date());
user.setSalary(10000.0);
this.userRepository.save(user);
}
@Test
void testDelete(){
this.userRepository.deleteById(1l);
}
@Test
void testFind(){
System.out.println(this.userRepository.findById(1l).get());
}
结果:
基本查询扩展
比如:你的方法名叫做:findByTitle,那么它就知道你是根据title查询,然后自动帮你完成,无需写实现类。不用在类上加任何的注解,但是泛型要写对
其中ElasticsearchRepository接口功能最强大。该接口的方法包括:
测试方法如下
@Test public void testFindByTitile(){ // 查询全部,并按照价格降序排序 Iterable<Item> items = itemDao.findByTitle("手机"); items.forEach(item-> System.out.println(item)); }
首先,目前索引库item中所有的文档如下:
运行后控制台打印如下:效果他不就有了吗
但是方法名称要符合一定的约定
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}}}} |
虽然基本查询和自定义方法已经很强大了,但是如果是复杂查询(模糊、通配符、词条查询等)就显得力不从心了。此时,我们只能使用原生查询。
8、重建索引
ElasticSearch的索引一旦创建,只允许添加字段,不允许改变字段。因为改变字段,需要重建倒排索引,影响内部缓存结构,性能太低。
解决办法:重建一个新的索引,并将原有索引的数据导入到新索引中。
原索引库 :student_index_v1
新索引库 :student_index_v2# 2:将student_index_v1 数据拷贝到 student_index_v2 POST _reindex { "source": { "index": "student_index_v1" }, "dest": { "index": "student_index_v2" } }
9、一些问题
User实体类中没有_class字段,反序列化时就会出错
解决办法,加上注解@JsonIgnoreProperties("_class")