文章目录
1. Elasticsearch 客户端的缺点
Elasticsearch 提供的 Java 客户端有一些不太方便的地方:
- 很多地方需要拼接 JSON 字符串,这将会非常麻烦
- 需要把对象序列化为 JSON 存储
- 查询到 JSON 结果也需要反序列化为对象
因此,接下来学习 Spring 提供的套件:Spring Data Elasticsearch,来解决这些问题。
2. Spring Data Elasticsearch 简介
Spring Data Elasticsearch 是 Spring Data 项目下的一个子模块。
Spring Data 的使命是给各种数据访问提供统一的编程接口,不管是关系型数据库(如:MySQL),还是非关系数据库(如:Redis),或者是 Elasticsearch 这样的索引数据库。从而简化开发人员的代码,提高开发效率。
3. 搭建 Spring Data Elasticsearch
3.1 创建工程
-
打开 IDEA --> Create New Project
-
Spring Initializr --> Next
-
填写项目信息 --> Next
-
添加依赖 --> Next
-
填写项目位置 --> Finish
3.2 添加配置
编写配置文件 application.yaml
spring:
data:
elasticsearch:
cluster-name: elasticsearch
cluster-nodes: 192.168.222.132:9300 # elasticsearch 的连接地址
3.3 添加实体类和注解
注解
Spring Data Elasticsearch 通过注解实现文档到实体类的映射,主要有的三个注解:
- @Document:作用在类,标记实体类为文档对象,一般有四个属性
- indexName:对应索引库名称
- type:对应在索引库中的类型
- shards:分片数量,默认 5
- replicas:副本数量,默认 1
- @Id:作用在成员变量,标记一个字段作为 id 主键
- @Field:作用在成员变量,标记为文档的字段,并指定字段映射属性:
- type:字段类型,取值是 FieldType 枚举类型
- index:是否索引,布尔类型,默认是 true
- store:是否存储,布尔类型,默认是 false
- analyzer:分词器名称
测试
-
创建 pojo 包,用来编写实体类
-
编写一个实体类 Item
public class Item { Long id; String title; //标题 String category;// 分类 String brand; // 品牌 Double price; // 价格 String images; // 图片地址 // getter、setter、toString 方法省略 }
-
给实体类添加注解
@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; // 图片地址 // getter、setter、toString 方法省略 }
4. 索引操作
Spring Data Elasticsearch 提供了模板工具类 ElasticsearchTemplate,它提供了很多 Elasticsearch 操作相关的 API。
4.1 创建索引和映射
-
创建测试类
@RunWith(SpringRunner.class) @SpringBootTest(classes = ZtElasticsearchApplication.class) public class IndexTest { @Autowired ElasticsearchTemplate elasticsearchTemplate; @Test public void testCreateIndex() { // 创建索引 elasticsearchTemplate.createIndex(Item.class); // 创建映射 elasticsearchTemplate.putMapping(Item.class); } }
-
运行测试类,然后打开 kibana 查看索引,创建成功
4.2 删除索引
-
在测试类中添加方法
@Test public void testDeleteIndex() { // 删除索引 elasticsearchTemplate.deleteIndex(Item.class); }
-
运行测试类,然后打开 kibana 查看索引,删除成功
5. 文档操作
Spring Data Elasticsearch 的强大之处在于,你不用写任何 CRUD 代码,只要你定义一个接口,然后继承 ElasticsearchRepository 接口,就能具备各种基本的 CRUD 功能。
5.1 新增文档
-
创建 ItemRepository 接口,继承 ElasticsearchRepository 接口
public interface ItemRepository extends ElasticsearchRepository<Item, Long> { }
ElasticsearchRepository<T, ID> 有两个泛型参数:
- T:索引库的类型
- ID:id 的类型
-
创建测试类
@RunWith(SpringRunner.class) @SpringBootTest(classes = ZtElasticsearchApplication.class) public class DocumentTest { @Autowired private ItemRepository itemRepository; @Test public void testSave() { Item item = new Item(1L, "小米10", "手机", "小米", 3999.00, "http://image.leyou.com/123.jpg"); // 新增文档 itemRepository.save(item); } }
-
运行测试类,然后打开 kibana 查看文档,新增成功
5.2 批量新增文档
-
在测试类中添加方法
@Test public void testSaveAll() { List<Item> list = new ArrayList<>(); list.add(new Item(2L, "iphoneX", "手机", "苹果", 6000.00, "http://image.leyou.com/124.jpg")); list.add(new Item(3L, "华为mate 30", " 手机", "华为", 5000.00, "http://image.leyou.com/125.jpg")); // 批量新增文档 itemRepository.saveAll(list); }
-
运行测试类,然后打开 kibana 查看文档,批量新增成功
5.3 修改文档
修改和新增是同一个接口,这一点跟我们在页面发起 PUT 请求是类似的。
5.4 删除文档
-
在测试类中添加方法
@Test public void testDelete() { Item item = new Item(); item.setId(3L); // 删除文档 itemRepository.delete(item); }
-
运行测试类,然后打开 kibana 查看文档,删除成功
5.5 查询文档
根据 ID 查询文档
-
在测试类中添加方法
@Test public void testFindById() { // 根据 ID 查询文档 Optional<Item> item = itemRepository.findById(2L); Item item1 = item.get(); System.out.println(item); }
-
运行测试类,结果如下
Item{id=2, title='iphoneX', category='手机', brand='苹果', price=6000.0, images='http://image.leyou.com/124.jpg'}
查询所有文档
-
在测试类中添加方法
@Test public void testFindAll() { // 查询所有文档 Iterable<Item> items = itemRepository.findAll(); for (Item item : items) { System.out.println(item); } }
-
运行测试类,结果如下
Item{id=1, title='小米10', category='手机', brand='小米', price=3999.0, images='http://image.leyou.com/123.jpg'} Item{id=2, title='iphoneX', category='手机', brand='苹果', price=6000.0, images='http://image.leyou.com/124.jpg'}
5.6 自定义方法
现在我想要实现根据 title 查询文档,没有这个 API 怎么办呢?
Spring Data Elasticsearch 提供了一个强大的功能,就是根据方法名称自动实现功能,并且不需要写实现类。
当然方法名称要符合一定的约定:
Keyword | Sample |
---|---|
And | findByNameAndPrice |
Or | findByNameOrPrice |
Is | findByName |
Not | findByNameNot |
Between | findByPriceBetween |
LessThanEqual | findByPriceLessThan |
GreaterThanEqual | findByPriceGreaterThan |
Before | findByPriceBefore |
After | findByPriceAfter |
Like | findByNameLike |
StartingWith | findByNameStartingWith |
EndingWith | findByNameEndingWith |
Contains/Containing | findByNameContaining |
In | findByNameIn(Collection<String>names) |
NotIn | findByNameNotIn(Collection<String>names) |
Near | findByStoreNear |
True | findByAvailableTrue |
False | findByAvailableFalse |
OrderBy | findByAvailableTrueOrderByNameDesc |
下面实现根据 title 查询文档:
-
在 ItemRepository 接口中增加方法 findByTitle
public interface ItemRepository extends ElasticsearchRepository<Item, Long> { /** * 根据 title 查询文档 * @param title * @return */ public List<Item> findByTitle(String title); }
-
在测试类中添加方法
@Test public void testFindByTitle() { // 根据 title 查询文档 List<Item> items = itemRepository.findByTitle("小米"); for (Item item : items) { System.out.println(item); } }
-
运行测试类,结果如下
Item{id=1, title='小米10', category='手机', brand='小米', price=3999.0, images='http://image.leyou.com/123.jpg'}
6. 高级查询
前面介绍的查询文档和自定义方法已经很强大了,但要实现一些复杂的查询(比如:词条查询、模糊查询等)时,还是不够的。接下来就介绍一下更高级的查询方式。
6.1 基本查询
Spring Data Elasticsearch 为我们提供了一个对象 QueryBuilders,它提供了大量的静态方法,用于生成各种不同类型的查询对象,例如:词条、模糊、通配符等。
再将 QueryBuilder 作为参数传入 Repository 的 search 方法就可以查询了。
下面实现根据词条查询文档:
-
在测试类中添加方法
@Test public void testQueryBuilder() { // 词条查询 TermQueryBuilder queryBuilder = QueryBuilders.termQuery("category", "手机"); // 执行查询 Iterable<Item> items = itemRepository.search(queryBuilder); for (Item item : items) { System.out.println(item); } }
-
运行测试类,结果如下
Item{id=1, title='小米10', category='手机', brand='小米', price=3999.0, images='http://image.leyou.com/123.jpg'} Item{id=2, title='iphoneX', category='手机', brand='苹果', price=6000.0, images='http://image.leyou.com/124.jpg'} Item{id=4, title='小米Mix2S', category='手机', brand='小米', price=4299.0, images='http://image.leyou.com/13123.jpg'} Item{id=5, title='荣耀V10', category='手机', brand='华为', price=2799.0, images='http://image.leyou.com/13123.jpg'}
6.2 自定义查询
但 QueryBuilders 还是不够灵活,比如我希望将词条查询的结果,然后按价格升序排序,再分页,这就没办法了。Spring Data Elasticsearch 提供的一个查询条件构建器,帮我们实现复杂的自定义查询。
下面实现根据词条查询文档,然后按价格升序排序,再分页:
-
在测试类中添加方法
@Test public void testNativeSearchQueryBuilder() { // 查询条件构建器 NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder(); // 添加词条查询 nativeSearchQueryBuilder.withQuery(QueryBuilders.termQuery("category", "手机")); // 按价格升序排序 nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort("price").order(SortOrder.ASC)); // 添加分页,page = 1,注意 page 是从 0 开始的,size = 2 nativeSearchQueryBuilder.withPageable(PageRequest.of(1,2)); // 执行搜索,得到分页结果对象 Page<Item> itemPage = itemRepository.search(nativeSearchQueryBuilder.build()); // 遍历得到结果 List<Item> items = itemPage.getContent(); for (Item item : items) { System.out.println(item); } System.out.println("总页数:"+itemPage.getTotalPages()); System.out.println("总结果数:"+itemPage.getTotalElements()); }
-
运行测试类,结果如下
Item{id=4, title='小米Mix2S', category='手机', brand='小米', price=4299.0, images='http://image.leyou.com/13123.jpg'} Item{id=2, title='iphoneX', category='手机', brand='苹果', price=6000.0, images='http://image.leyou.com/124.jpg'} 总页数:2 总结果数:4
7. 聚合
接下看看在 Spring Data Elasticsearch 中是如何实现聚合的。
7.1 聚合为桶
比如这里我们以 brand 分桶,先来看看原生的 Elasticsearch 的实现:
-
在 kibana 中查询
GET item/_search { "size": 0, "aggs": { "brands": { "terms": { "field": "brand" } } } }
-
得到响应结果
{ "took": 15, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": { "total": 5, "max_score": 0, "hits": [] }, "aggregations": { "brands": { "doc_count_error_upper_bound": 0, "sum_other_doc_count": 0, "buckets": [ { "key": "华为", "doc_count": 2 }, { "key": "小米", "doc_count": 2 }, { "key": "苹果", "doc_count": 1 } ] } } }
再来看看在 Spring Data Elasticsearch 实现:
-
在测试类中添加方法
@Test public void testAggs() { // 查询条件构建器 NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder(); // 添加一个聚合,聚合类型为 terms,聚合名称为 brands,聚合字段为 brand nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("brands").field("brand")); // 不查询任何结果,相当于 "size": 0 nativeSearchQueryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{""}, null)); // 执行查询,把查询结果强转为 AggregatedPage 类型 AggregatedPage<Item> itemAggregatedPage = (AggregatedPage<Item>) itemRepository.search(nativeSearchQueryBuilder.build()); // 从结果中取出名称为 brands 的那个聚合,并转换为 StringTerm 类型 StringTerms brands = (StringTerms) itemAggregatedPage.getAggregation("brands"); // 获取桶 List<StringTerms.Bucket> buckets = brands.getBuckets(); // 遍历桶 for (StringTerms.Bucket bucket : buckets) { // 获取桶中的 key,即 brand System.out.println("key:" + bucket.getKeyAsString()); // 获取桶中的文档数量 System.out.println("doc_count:" + bucket.getDocCount()); } }
-
运行测试类,结果如下
key:华为 doc_count:2 key:小米 doc_count:2 key:苹果 doc_count:1
7.2 桶内度量
比如这里我们以 brand 分桶,再以桶内的价格平均值为度量,先来看看原生的 Elasticsearch 的实现:
-
在 kibana 中查询
GET item/_search { "size": 0, "aggs": { "brands": { "terms": { "field": "brand" }, "aggs": { "avg_price": { "avg": { "field": "price" } } } } } }
-
得到响应结果
{ "took": 23, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": { "total": 5, "max_score": 0, "hits": [] }, "aggregations": { "brands": { "doc_count_error_upper_bound": 0, "sum_other_doc_count": 0, "buckets": [ { "key": "华为", "doc_count": 2, "avg_price": { "value": 3899.5 } }, { "key": "小米", "doc_count": 2, "avg_price": { "value": 4149 } }, { "key": "苹果", "doc_count": 1, "avg_price": { "value": 6000 } } ] } } }
再来看看在 Spring Data Elasticsearch 实现:
-
在测试类中添加方法
@Test public void testAggs() { // 查询条件构建器 NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder(); // 添加一个聚合,聚合类型为 terms,聚合名称为 brands,聚合字段为 brand // 添加一个度量,度量类型为 avg,度量名称为 avg_price,度量字段 price nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("brands").field("brand").subAggregation(AggregationBuilders.avg("avg_price").field("price"))); // 不查询任何结果,相当于 "size": 0 nativeSearchQueryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{""}, null)); // 执行查询,把查询结果强转为 AggregatedPage 类型 AggregatedPage<Item> itemAggregatedPage = (AggregatedPage<Item>) itemRepository.search(nativeSearchQueryBuilder.build()); // 从结果中取出名称为 brands 的那个聚合,并转换为 StringTerm 类型 StringTerms brands = (StringTerms) itemAggregatedPage.getAggregation("brands"); // 获取桶 List<StringTerms.Bucket> buckets = brands.getBuckets(); // 遍历桶 for (StringTerms.Bucket bucket : buckets) { // 获取桶中的 key,即 brand System.out.println("key:" + bucket.getKeyAsString()); // 获取桶中的文档数量 System.out.println("doc_count:" + bucket.getDocCount()); // 获取度量结果 InternalAvg avg_price = (InternalAvg) bucket.getAggregations().asMap().get("avg_price"); System.out.println("avg_value:"+avg_price.getValue()); } }
-
运行测试类,结果如下
key:华为 doc_count:2 avg_value:3899.5 key:小米 doc_count:2 avg_value:4149.0 key:苹果 doc_count:1 avg_value:6000.0