目前市面上有两类客户端
一类是TransportClient 为代表的ES原生客户端,不能执行原生dsl语句必须使用它的Java api方法。
另外一种是以Rest Api为主的missing client,最典型的就是jest。 这种客户端可以直接使用dsl语句拼成的字符串,直接传给服务端,然后返回json字符串再解析。
两种方式各有优劣,但是最近elasticsearch官网,宣布计划在7.0以后的版本中废除TransportClient。以RestClient为主。
由于原生的Elasticsearch客户端API非常麻烦。所以这里直接学习Spring提供的套件:Spring Data Elasticsearch。
spring-data-Elasticsearch 使用之前,必须先确定版本,elasticsearch 对版本的要求比较高。
4.1. 创建module
在gmall工程下创建一个模块:
引入依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> <exclusions> <exclusion> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> </exclusion> <exclusion> <groupId>org.elasticsearch.client</groupId> <artifactId>transport</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> <version>6.8.1</version> </dependency> <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>transport</artifactId> <version>6.8.1</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.10</version> </dependency>
在application.properties中添加配置
spring.elasticsearch.rest.uris=http://172.16.116.100:9200 # 集群情况下 spring.elasticsearch.rest.uris[0]=http://172.16.116.100:9200 spring.elasticsearch.rest.uris[1]=http://172.16.116.100:9200
4.2. 实体类
@Data @AllArgsConstructor @NoArgsConstructor @Document(indexName = "user", type = "info", shards = 3, replicas = 2) public class User { @Id private Long id; @Field(type = FieldType.Text, analyzer = "ik_max_word") private String name; @Field(type = FieldType.Integer) private Integer age; @Field(type = FieldType.Keyword) private String password; }
Spring Data通过注解来声明字段的映射属性,有下面的三个注解:
-
@Document
作用在类,标记实体类为文档对象,一般有四个属性-
indexName:对应索引库名称
-
type:对应在索引库中的类型
-
shards:分片数量,默认5
-
replicas:副本数量,默认1
-
-
@Id
作用在成员变量,标记一个字段作为id主键 -
@Field
作用在成员变量,标记为文档的字段,并指定字段映射属性:-
type:字段类型,取值是枚举:FieldType
-
index:是否索引,布尔类型,默认是true
-
store:是否存储,布尔类型,默认是false
-
analyzer:分词器名称:ik_max_word
-
4.3. 创建索引及映射
@SpringBootTest class EsDemoApplicationTests { // ElasticsearchTemplate是TransportClient客户端 // ElasticsearchRestTemplate是RestHighLevel客户端 @Autowired ElasticsearchRestTemplate restTemplate; @Test void contextLoads() { // 创建索引 this.restTemplate.createIndex(User.class); // 创建映射 this.restTemplate.putMapping(User.class); // 删除索引 // this.restTemplate.deleteIndex("user"); } }
4.4. Repository文档操作
Spring Data 的强大之处,就在于你不用写任何DAO处理,自动根据方法名或类的信息进行CRUD操作。只要你定义一个接口,然后继承Repository提供的一些子接口,就能具备各种基本的CRUD功能。
其中ElasticsearchRepository接口功能最强大。该接口的方法包括:
4.4.1. 新增
@Autowired UserRepository userRepository; @Test void testAdd(){ this.userRepository.save(new User(1l, "zhang3", 20, "123456")); }
修改和新增是同一个接口,区分的依据就是id,这一点跟我们在页面发起PUT请求是类似的。
4.4.2. 删除
@Test void testDelete(){ this.userRepository.deleteById(1l); }
4.5. 查询
4.5.1. 基本查询
查询一个:
@Test void testFind(){ System.out.println(this.userRepository.findById(1l).get()); }
4.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}}}} |
准备一组数据:
@Test void testAddAll(){ List<User> users = new ArrayList<>(); users.add(new User(1l, "柳岩", 18, "123456")); users.add(new User(2l, "范冰冰", 19, "123456")); users.add(new User(3l, "李冰冰", 20, "123456")); users.add(new User(4l, "锋哥", 21, "123456")); users.add(new User(5l, "小鹿", 22, "123456")); users.add(new User(6l, "韩红", 23, "123456")); this.userRepository.saveAll(users); }
在UserRepository中定义一个方法:
第一种写法:
public interface UserRepository extends ElasticsearchRepository<User, Long> { /** * 根据年龄区间查询 * @param age1 * @param age2 * @return */ List<User> findByAgeBetween(Integer age1, Integer age2); }
测试:
@Test void testFindByAgeBetween(){ System.out.println(this.userRepository.findByAgeBetween(20, 30)); }
第二种写法:
@Query("{\n" + " \"range\": {\n" + " \"age\": {\n" + " \"gte\": \"?0\",\n" + " \"lte\": \"?1\"\n" + " }\n" + " }\n" + " }") List<User> findByQuery(Integer age1, Integer age2);
测试:
@Test void testFindByQuery(){ System.out.println(this.userRepository.findByQuery(20, 30)); }
4.5.3. 自定义查询
@Test void testNative(){ // 初始化自定义查询对象 NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder(); // 构建查询 queryBuilder.withQuery(QueryBuilders.matchQuery("name", "冰冰")); // 排序 queryBuilder.withSort(SortBuilders.fieldSort("age").order(SortOrder.ASC)); // 分页 queryBuilder.withPageable(PageRequest.of(0, 2)); // 高亮 queryBuilder.withHighlightBuilder(new HighlightBuilder().field("name").preTags("<em>").postTags("</em>")); // 执行查询,获取分页结果集 Page<User> userPage = this.userRepository.search(queryBuilder.build()); // 总页数 System.out.println(userPage.getTotalPages()); // 总记录数 System.out.println(userPage.getTotalElements()); // 当前页数据 System.out.println(userPage.getContent()); }
NativeSearchQueryBuilder:Spring提供的一个查询条件构建器,帮助构建json格式的请求体
Page<item>
:默认是分页查询,因此返回的是一个分页的结果对象,包含属性:
-
totalElements:总条数
-
totalPages:总页数
-
Iterator:迭代器,本身实现了Iterator接口,因此可直接迭代得到当前页的数据