Spring Data Elasticsearch

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);
        }
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇