Spring Data Elasticsearch为文档的存储,查询,排序等操作提供了一个高度抽象的模板。使用Spring Data ElasticSearch来操作Elasticsearch,可以较大程度的减少我们的代码量,提高我们的开发效率。
数据准备
在使用Spring Data Elasticsearch之前,我们先准备一些数据:
PUT serve_provider_info
{
"mappings" : {
"properties" : {
"acceptNum" : {
"type" : "integer"
},
"city" : {
"type" : "text",
"analyzer": "ik_max_word",
"search_analyzer":"ik_smart"
},
"id" : {
"type" : "long"
},
"location" : {
"type" : "geo_point"
},
"pickUp" : {
"type" : "integer"
},
"serveItemIds" : {
"type" : "long"
},
"serveTimes" : {
"type" : "long"
},
"settingStatus" : {
"type" : "long"
}
}
}
}
该索引用来存储家政服务从业者的信息,包括:
- acceptance_num:家政服务工作人员当前的接单数量
- city: 家政服务工作人员所在城市的名称
- location: 家政服务工作人员的接单地点
- pick_up:家政服务工作人员是否开启了接单
- serve_item_ids: 家政服务工作人员的服务项id
- serve_times: 家政服务工作人员的服务的服务时间集合,形如2025111209
- setting_status: 家政服务工作人员是否完成了实名认证
我们准备一下6条文档数据:
# 武昌区
POST /serve_provider_info/_doc/420001
{
"acceptNum": 2,
"city": "武汉武昌",
"id": 420001,
"location": "30.5931,114.3054",
"pickUp": 1,
"serveItemIds": [101, 102, 105],
"serveTimes": [2025111911, 2025111914, 2025111917],
"settingStatus": 1
}
# 上海黄埔区
POST /serve_provider_info/_doc/310001
{
"acceptNum": 7,
"city": "上海黄埔",
"id": 310001,
"location": "31.2304,121.4737",
"pickUp": 1,
"serveItemIds": [101, 103, 107],
"serveTimes": [2025111912, 2025111915],
"settingStatus": 1
}
# 武汉汉阳区
POST /serve_provider_info/_doc/420002
{
"acceptNum": 10,
"city": "武汉汉阳",
"id": 420002,
"location": "30.5350,114.3528",
"pickUp": 0,
"serveItemIds": [102, 104],
"serveTimes": [2025111913, 2025111916],
"settingStatus": 0
}
# 广州天河区
POST /serve_provider_info/_doc/440001
{
"acceptNum": 3,
"city": "广州天河",
"id": 440001,
"location": "23.1291,113.2644",
"pickUp": 1,
"serveItemIds": [101, 102, 103, 105],
"serveTimes": [2025111911, 2025111914, 2025111918],
"settingStatus": 1
}
# 深圳福田区
POST /serve_provider_info/_doc/440300
{
"acceptNum": 9,
"city": "深圳福田",
"id": 440300,
"location": "22.5431,114.0579",
"pickUp": 1,
"serveItemIds": [103, 106],
"serveTimes": [2025111912, 2025111919],
"settingStatus": 1
}
# 武汉市洪山区
POST /serve_provider_info/_doc/420003
{
"acceptNum": 8,
"city": "武汉洪山",
"id": 420003,
"location": "30.5728,114.2794",
"pickUp": 1,
"serveItemIds": [101, 104, 105],
"serveTimes": [2025111913, 2025111915],
"settingStatus": 1
}
开发环境准备
引入依赖
<!--spring-data-elasticsearch-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
在配置文件中添加配置
spring:
elasticsearch:
# es server地址
uris: http://192.168.153.170:9200
# 连接超时时间
connection-timeout: 6s
# 访问超时时间
socket-timeout: 10s
定义实体类
@Document(indexName = "serve_provider_info")
@Data
public class ServeProviderInfo {
@Id
private Long id;
@Field(type = FieldType.Integer)
private Integer acceptNum;
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String city;
@GeoPointField
private String location;
@Field(type = FieldType.Integer)
private Integer pickUp;
@Field(type = FieldType.Long)
private List<Long> serveItemIds;
@Field(type = FieldType.Integer)
private List<Long> serveTimes;
@Field(type = FieldType.Long)
private Long settingStatus;
}
在ServeProviderInfo类上,通过添加@Document注解,我们将ServeProviderInfo类映射的文档所属的索引,@Document注解的index属性,用来定义实体类所映射的文档所属的目标索引名称
在ServeProviderInfo类的成员变量Id上通过添加@Id注解指定,Id成员变量映射到ServeProviderInfo索引中文档的id字段,同时也映射到文档的唯一表示_id字段。
在ServeProviderInfo类的其他成员变量上,通过添加@Field注解,定义成员变量和文档字段的映射关系:
- 默认同名成员变量,映射到文档中的同名字段(也可以由@Field注解的name属性显示指定)
- 通过@Field注解的type属性指定文档中同名字段的数据类型
- 通过@Field注解的analyzer属性,指定成员变量所映射的文档字段所使用的的分词器,searchAnalyzer表示查询时对关键字所使用的分词器
框架使用
Spring Data Elasticsearch主要有两种使用方式,一种是使用Repository接口,另外一种是面对复杂场景时使用的ElasticserchRestTemplate对象
Repository接口
类比于Mybatis-Plus中定义BaseMaper子接口即可对单表做增删改查的操作,Spring Data Elastisearch中我们可以通过定义ElasticsearchRepository子接口,迅速实现对索引中的文档数据的增删改查,以及通过自定义方法,实现自定义查询。
public interface ServeProviderInfoRepository extends ElasticsearchRepository<ServeProviderInfo, Long> {
}
ElasticsearchRepository接口需要接收两个泛型:
- 第一个泛型即映射实体类
- 第二个泛型是在实体类中加了@Id注解的成员变量的数据类型,即映射到文档唯一标识_id字段的成员变量类型。
旦我们定义好了ElasticsearchRepository的子接口,马上就可以实现对goods索引中文档的增删改查功能
// 注入repository对象
@Autowired
private ServeProviderInfoRepository providerRepository;
// 保存单个文档对象
ServeProviderInfo providerInfo = ....
providerRepository.save(providerInfo);
// 批量保存多个文档对象
List<ServeProviderInfo> providerInfoList = ...
goodsRepository.saveAll(providerInfoList);
// 根据id查询
providerRepository.findById(id);
// 根据id删除
providerRepository.deleteById(id);
同时,我们还需要注意一点,一旦我们定义好了ElasticsearchRepository接口,而且被SpringBoot启动类扫描到,那么在应用启动的时候,如果ElasticsearchRepository子接口所访问的索引在ES中不存在,Spring Data Elasticsearch会在ES中自动创建索引,并根据映射实体类定义索引的映射。
虽然Repository接口中提供了一些基本的增删改查方法,但是大多数时候,我们可能需要对索引中的文档数据做自定义查询,此时仅仅使用ElasticsearchRepository接口中继承的方法无法满足我们的需求。此时我们就需要在自己的Repository接口中,通过自定义方法来实现各种自定义查询。
简单自定义查询
/*
1. 通过Query注解定义具体的查询字符串(也可以替换为其他查询)
2. 字符串中的?0是固定格式,表示第0个参数的占位符,在实际查询时会被方法的第一个参数值title的值替换,如果有多个参数,依次类推即可
3. List<ServeProviderInfo>为查询出的多天文档封装得到的对象
*/
@Query(
"{ " +
"\"match\": {\n" +
" \"city\": \"?0\"\n" +
"}" +
"}"
)
List<ServeProviderInfo> matchSearch(String city);
这里的@Query注解中,只需要包含我们查询脚本中”query”{} 里面的内容即可,比如上面的@Query注解所表示的查询等价于
GET serve_provider_info/_search
{
"query": {
"match": {
"city": "武汉"
}
}
}
@Autowired
ServeProviderInfoRepository providerRepository;
@Test
public void testMatchSearch() {
// 在调用的时候传递查询的参数值
List<ServeProviderInfo> list = providerRepository.matchSearch("武汉");
System.out.println(list);
}
分页自定查询
我们还可以结合利用@Query注解结合分页参数,实现分页查询
/*
1. 针对一个查询结果,返回对应的一页数据
2. Pageable参数是当想要获取分页数据的时候,必须携带的参数,表示分页信息
比如,查询第多少页数据,每页多少条数据等,该参数不会用来替换我们的@Query字符串中的参数
3. 返回的结果是一个包含一页文档数据的Page对象
*/
@Query(
" {" +
" \"match\": {\n" +
" \"city\": \"?0\"\n" +
" }" +
"}"
)
Page<ServeProviderInfo> testSearchPage(String city, Pageable pageable);
// 注入repository对象
@Autowired
ServeProviderInfoRepository providerRepository;
/*
测试分页查询
*/
@Test
public void testSearchPage() {
// 创建表示分页信息的Pageable对象
// 表示查询第几页数据,这里一定要注意,页数是从0开始算的
int page = 0;
// 每页假设10个文档
int pageCount = 10;
// 调用Sort方法得到Sort对象,一个Sort对象表示
Sort sort = Sort.by(Sort.Direction.ASC, "acceptNum");
// PageRequest 是 Pageable接口子类对象
PageRequest pageInfo = PageRequest.of(page, pageCount,sort);
// 这里的Page对象可以被看做是List
Page<ServeProviderInfo> pageResult = serveProviderInfoRepository.testSearchPage("武汉", pageInfo);
// 遍历集合,从每个SearchHit对象中取出文档对象,
// 如果需要返回可以在遍历的时候将其,放入一个List中返回
pageResult.forEach( providerInfo -> {
// 访问查询到的一条文档
// ...
});
// 获取满足条件的总的文档数量
long totalElements = pageResult.getTotalElements();
}
但是,如果我想要完成按照距离某个中心点的位置对文档进行排序呢?此时Repository接口就不够用了。
ElasticsearchOperations
所以,Repository只适用于比较简单的场景,如果面对比较复杂的场景我们就必须使用ElasticsearchOperations对象了。完成如下查询和排序:
- 查询 1)在武汉接单,2)已完成实名认证 3)处于正常接单状态 4)能提供id为103的服务 5)服务时间不能为2025111912 6)接单数小于10单的家政工作人员
- 且根据其于(30.5931,114.3054)距离的远近排序
- 且查询结果中只需要包含:id,accept_num即可
GET serve_provider_info/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"city": "武汉"
}
},
{
"term": {
"serveItemIds": {
"value": "102"
}
}
}
],
"must_not": [
{
"term": {
"serveTimes": {
"value": 2025111912
}
}
},
{
"range": {
"acceptNum": {
"gte": 10
}
}
}
],
"filter": [
{
"term": {
"pickUp": 1
}
},
{
"term": {
"settingStatus": 1
}
}
]
}
},
"sort": [
{
"_geo_distance": {
"location": "30.5931,114.3054",
"order": "asc",
"unit": "km",
"distance_type": "arc"
}
}
]
}
String city = "武汉";
Long serveItemId = 102L;
Long serveTime = 2025111912L;
Integer acceptNum = 10;
Integer pickUp = 1;
Integer settingStatus = 1;
// 构造整个查询条件
Query query = Query.of(q -> q.bool(bool -> bool
// 构造must条件: 查询city是否是指定city
.must(must -> must
.match(match -> match
.field("city")
.query(city)))
// 构造must条件: 查询技能列表serveItemIds中是否包含指定技能
.must(must -> must
.term(term -> term
.field("serveItemIds")
.value(serveItemId)))
// 构造must_not条件: 已经接单的服务时间中不能包含指定的服务时间
.mustNot(mustNot -> mustNot
.term(term -> term
.field("serveTimes")
.value(serveTime)))
// 构造must_not条件: 已经接单的数量不得超过指定的数量
.mustNot(mustNot -> mustNot
.range(range -> range
.field("acceptNum")
.gte(JsonData.of(acceptNum))))
// 构造filter条件: 已开启接单状态
.filter(filter -> filter
.term(term -> term
.field("pickUp")
.value(pickUp)))
// 构造filter条件: 已经做完了实名认证
.filter(filter -> filter
.term(term -> term
.field("settingStatus")
.value(settingStatus)))));
// 构造距离排序
Sort sort = Sort.by(new GeoDistanceOrder("location", new GeoPoint(30.5931, 114.3054))
.with(Sort.Direction.ASC)
.withUnit("km"));
// 构造NativeQuery对象代表整个查询请求
NativeQuery searchQuery = NativeQuery.builder()
// 设置查询
.withQuery(query)
// 设置排序
.withSort(sort)
// 设置分页
.withPageable(PageRequest.of(0, 2))
// 返回的文档只包含id,acceptNum字段值
.withSourceFilter(new FetchSourceFilter(new String[]{"id", "acceptNum"}, null))
.build();
SearchHits<ServeProviderInfo> searchHits =
// 发起查询请求
elasticsearchOperations.search(searchQuery, ServeProviderInfo.class);
// 获取查询到的总的文档条数
System.out.println(searchHits.getTotalHits());
// 遍历查询到的每条文档数据
for (SearchHit<ServeProviderInfo> hit : searchHits.getSearchHits()) {
// 输出文档的原始值(封装成了ServeProviderInfo)
System.out.println(hit.getContent());
// 获取距离中心点的排序值(就是距离中心点的距离)
Object distance = hit.getSortValues().isEmpty() ? null : hit.getSortValues().get(0);
System.out.println("distance: " + distance);
}