Spring Data Elasticsearch之Repository

1,405 阅读5分钟

本文源码 github.com/tzjavadmg/e…

UserRepositoryTest

Spring Data Elasticsearch版本

Elasticsearch 8.5.2 对应Spring Data Elasticsearch 5.0.x版本和Spring Boot 3.0.x版

image.png

Spring boot 配置说明

Spring Data Elasticsearch配置说明: 官方说明

配置项说明默认值
spring.data.elasticsearch.repositories.enabled是否启用 Elasticsearch Repository启用
spring.elasticsearch.uris逗号分隔的Elasticsearch实例列表http://localhost:9200
spring.elasticsearch.username身份验证的用户名---
spring.elasticsearch.password身份验证的密码---
spring.elasticsearch.connection-timeout连接超时时间1s
spring.elasticsearch.socket-timeout与 Elasticsearch 通信时使用的套接字超时30s
spring.elasticsearch.path-prefix发送到 Elasticsearch 的每个请求的路径的前缀---
spring.elasticsearch.socket-keep-alive是否开启 socket keep alivefalse
spring.elasticsearch.restclient.sniffer.delay-after-failure失败后安排的嗅探执行延迟1m
spring.elasticsearch.restclient.sniffer.interval连续普通嗅探执行之间的间隔5m

Spring boot工程

添加maven依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.0.0</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
    </dependency>
</dependencies>

application.yml配置

spring:
  elasticsearch:
    uris: http://localhost:9200,http://localhost:9210,http://localhost:9220

SpringBootApplication

添加注解:EnableElasticsearchRepositories

@SpringBootApplication
@EnableElasticsearchRepositories(basePackages = "com.codyzeng.esample.dao")
public class EsampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(EsampleApplication.class, args);
    }
}

Repository代码示例

创建实例类

@Document(indexName = "user",createIndex = true)
@Setting(shards = 3, replicas = 1,refreshInterval="1ms")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {

    @Id
    private Long id;

    @Field(type = FieldType.Keyword)
    private String username;

    @Field(type = FieldType.Integer)
    private Integer age;

    @Field(type = FieldType.Date,format={DateFormat.basic_date, DateFormat.year_month_day})
    private LocalDate birthday;

    @Field(type = FieldType.Keyword)
    private String province;

    @Field(type = FieldType.Keyword)
    private String city;

    @Field(type = FieldType.Keyword)
    private String district;

    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String address;

    @GeoPointField
    private GeoPoint location;

    @Field(index = false, type = FieldType.Keyword)
    private String photo;

    @Field(type = FieldType.Text, analyzer = "ik_smart")
    private String about;
}

实体注解说明

实体级

  • @Document:实体对象注解
indexName 索引名称。可以包含SPEL表达式,比如"log-#{T(java.time.LocalDate).now().toString()}"
createIndex 是否应用初始化时,自动创建索引
  • @Setting:索引配置
shards = 3 索引分片数量
replicas = 1 分片副本数量
refreshInterval="1ms" 索引写入时刷盘间隔,刷盘后才可读

字段级

  • @Id:标识主键ID,设置了后Elasticsearch不会自动生成文档ID,直接使用该字段的值作为文档ID.
  • @Transient:默认情况下,所有字段都映射到文档,此注释排除该字段。
  • @GeoPointField:将字段标记为geo_point数据类型。如果字段是GeoPoint类的实例,则可以省略
  • @Field:映射文档字段
name: 字段名称,如果未设置,则使用 Java 字段名称。
type: 字段类型,可以是Text, Keyword, Long, Integer, Short, Byte, Double, Float, Half_Float, Scaled_Float, Date, Date_Nanos, Boolean, Binary, Integer_Range, Float_Range, Long_Range, Double_Range, Date_Range, Ip_Range, Object之一, 嵌套, Ip, TokenCount, Percolator, Flattened, Search_As_You_Type。请参阅Elasticsearch 映射类型。如果未指定字段类型,则默认为FieldType.Auto. 这意味着,没有为该属性写入映射条目,并且 Elasticsearch 将在存储该属性的第一个数据时动态添加一个映射条目(查看 Elasticsearch 文档以了解动态映射规则)。
format:内置日期格式。
pattern:自定义日期格式。
store: 标志原始字段值是否应存储在 Elasticsearch 中,默认值为false。
analyzer:写入时分词分析器
searchAnalyzer:查询时分词分析器
normalizer:查询时规范化

编写Repository

支持按名称规则解析和自定义查询两种方式。自定义查询使用@Query注解,使用json查询语法。

public interface UserRepository extends ElasticsearchRepository<User, Long> {

    //统计城市某个区域总人数
    long countByDistrict(String district);
    //按城市区域搜索
    List<User> findByDistrictOrderByIdDesc(String district);
    //按地址搜索
    List<User> findByAddress(String address);
    //按区域或地址搜索,两个条件满足一个即可
    List<User> findByDistrictOrAddress(String district,String address);
    //按区域和地址搜索,两个条件都必须满足
    List<User> findByDistrictAndAddress(String district,String address);
    //按年龄区间搜索
    List<User> findByAgeBetween(int min,int max);
    //按地址分页搜索
    Page<User> findByAddress(String address, Pageable pageable);
    //按简介搜索
    @Query(""" 
        {"match": {"about": {"query": "?0"}}}
        """)
    Stream<User> findByAbout(String about);
    //多条件组合搜索
    @Query("""
            {"bool":{"must":[{"match":{"city":{"query": "?0"}}},{"match":{"sex":{"query": "?1"}}},{"range":{"age":{"gte":?2,"lte":?3}}}]}}
            """)
    Page<User> search(String city, String sex, Integer minAge, Integer maxAge, Pageable pageable);
}

编写单元测试

@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@Slf4j
class UserRepositoryTest {

    @Resource
    private UserRepository userRepository;

    @Test
    @DisplayName("插入或更新单个文档")
    @Order(1)
    void createUser() {
        userRepository.save(User.builder()
                .id(99L)
                .name("老六")
                .age(33)
                .province("上海")
                .city("上海")
                .district("浦东新区")
                .address("上海市浦东新区花园石桥路28弄1-8号-汤臣一品")
                .location(new GeoPoint(31.238794, 121.508506))
                .about("槟榔妹真好玩啊")
                .build());
    }

    @Test
    @DisplayName("批量插入或更新文档")
    @Order(2)
    void bulkCreateUser() {
        List<User> users = UserDataInitializer.loadUserData();
        userRepository.saveAll(users);
    }

    @Test
    @DisplayName("分页查询")
    @Order(3)
    void pageQuery() {
        //九号线星中路地铁站 121.375569,31.163862
        Sort sort = Sort.by(new GeoDistanceOrder("location", new GeoPoint(31.163862, 121.375569))).ascending();
        Page<User> userPage = userRepository.findByAddress("闵行", PageRequest.of(0, 5, sort));
        userPage.getContent().forEach(e -> log.info(e.toString()));
    }

    @Test
    @DisplayName("相似度查询")
    @Order(4)
    void searchSimilar() {
        User user = User.builder().id(7L).build();
        String[] fields = new String[]{"district"};
        Page<User> users = userRepository.searchSimilar(user, fields, PageRequest.of(0, 10));
        users.getContent().forEach(e -> log.info(e.toString()));
    }

    @Test
    @DisplayName("城市区域统计")
    @Order(4)
    void countByDistrict() {
        long count = userRepository.countByDistrict("浦东新区");
        Assertions.assertEquals(count, 2);

        count = userRepository.countByDistrict("浦东");
        Assertions.assertEquals(count, 0);
    }

    @Test
    @DisplayName("城市区域查询")
    @Order(4)
    void findByDistrict() {
        List<User> users = userRepository.findByDistrictOrderByIdDesc("闵行区");
        Assertions.assertEquals(users.size(), 4);
        Assertions.assertEquals(users.get(0).getId(), 7);
    }

    @Test
    @DisplayName("地址查询")
    @Order(4)
    void findByAddress() {
        List<User> users = userRepository.findByAddress("浦东");
        Assertions.assertEquals(users.size(), 3);

        users = userRepository.findByAddress("古北壹号");
        Assertions.assertEquals(users.size(), 1);

        users = userRepository.findByAddress("汤臣一品");
        Assertions.assertEquals(users.size(), 3);
    }

    @Test
    @DisplayName("按城市区域或地址查询")
    @Order(4)
    void findByDistrictOrAddress() {
        List<User> users = userRepository.findByDistrictAndAddress("浦东", "浦东");
        Assertions.assertEquals(users.size(), 0);

        users = userRepository.findByDistrictOrAddress("浦东", "浦东");
        Assertions.assertEquals(users.size(), 2);

    }

    @Test
    @DisplayName("按城市区域或地址查询")
    @Order(4)
    void findByAgeBetween() {
        List<User> users = userRepository.findByAgeBetween(50, 90);
        Assertions.assertEquals(users.size(), 1);
        // 18,22,25,31
        users = userRepository.findByAgeBetween(18, 31);
        Assertions.assertEquals(users.size(), 4);

    }

    @Test
    @DisplayName("按英雄简介搜索")
    @Order(4)
    void findByAbout() {
        Stream<User> userStream = userRepository.findByAbout("普攻");
        userStream.forEach(user -> System.out.println(JSON.toJSONString(user)));
    }

    @Test
    @DisplayName("多条件组合搜索")
    @Order(4)
    void search() {
        Page<User> userPage = userRepository.search("上海", "女", 18, 27, PageRequest.of(0, 10));
        userPage.getContent().forEach(user -> System.out.println(JSON.toJSONString(user)));
    }
}

方法名称关键字

以下是ElasticsearchRepository目前支持方法名称关键字,可惜还不支持within,所以没办法实现地理位置查询。不过可以使用ElasticsearchOperations来实现地理位置查询。

关键词示例查询字符串
AndfindByNameAndPrice{ "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] } }, { "query_string" : { "query" : "?", "fields" : [ "price" ] } } ] } }}
OrfindByNameOrPrice{ "query" : { "bool" : { "should" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] } }, { "query_string" : { "query" : "?", "fields" : [ "price" ] } } ] } }}
IsfindByName{ "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] } } ] } }}
NotfindByNameNot{ "query" : { "bool" : { "must_not" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] } } ] } }}
BetweenfindByPriceBetween{ "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : ?, "to" : ?, "include_lower" : true, "include_upper" : true } } } ] } }}
LessThanfindByPriceLessThan{ "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : null, "to" : ?, "include_lower" : true, "include_upper" : false } } } ] } }}
LessThanEqualfindByPriceLessThanEqual{ "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : null, "to" : ?, "include_lower" : true, "include_upper" : true } } } ] } }}
GreaterThanfindByPriceGreaterThan{ "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : ?, "to" : null, "include_lower" : false, "include_upper" : true } } } ] } }}
GreaterThanEqualfindByPriceGreaterThan{ "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : ?, "to" : null, "include_lower" : true, "include_upper" : true } } } ] } }}
BeforefindByPriceBefore{ "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : null, "to" : ?, "include_lower" : true, "include_upper" : true } } } ] } }}
AfterfindByPriceAfter{ "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : ?, "to" : null, "include_lower" : true, "include_upper" : true } } } ] } }}
LikefindByNameLike{ "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "?*", "fields" : [ "name" ] }, "analyze_wildcard": true } ] } }}
StartingWithfindByNameStartingWith{ "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "?*", "fields" : [ "name" ] }, "analyze_wildcard": true } ] } }}
EndingWithfindByNameEndingWith{ "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "*?", "fields" : [ "name" ] }, "analyze_wildcard": true } ] } }}
Contains/ContainingfindByNameContaining{ "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "*?*", "fields" : [ "name" ] }, "analyze_wildcard": true } ] } }}
In(当注释为 FieldType.Keyword 时)findByNameIn(Collection<String>names){ "query" : { "bool" : { "must" : [ {"bool" : {"must" : [ {"terms" : {"name" : ["?","?"]}} ] } } ] } }}
InfindByNameIn(Collection<String>names){ "query": {"bool": {"must": [{"query_string":{"query": ""?" "?"", "fields": ["name"]}}]}}}
NotIn(当注释为 FieldType.Keyword 时)findByNameNotIn(Collection<String>names){ "query" : { "bool" : { "must" : [ {"bool" : {"must_not" : [ {"terms" : {"name" : ["?","?"]}} ] } } ] } }}
NotInfindByNameNotIn(Collection<String>names){"query": {"bool": {"must": [{"query_string": {"query": "NOT("?" "?")", "fields": ["name"]}}]}}}
TruefindByAvailableTrue{ "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "true", "fields" : [ "available" ] } } ] } }}
FalsefindByAvailableFalse{ "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "false", "fields" : [ "available" ] } } ] } }}
OrderByfindByAvailableTrueOrderByNameDesc{ "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "true", "fields" : [ "available" ] } } ] } }, "sort":[{"name":{"order":"desc"}}] }
ExistsfindByNameExists{"query":{"bool":{"must":[{"exists":{"field":"name"}}]}}}
IsNullfindByNameIsNull{"query":{"bool":{"must_not":[{"exists":{"field":"name"}}]}}}
IsNotNullfindByNameIsNotNull{"query":{"bool":{"must":[{"exists":{"field":"name"}}]}}}
IsEmptyfindByNameIsEmpty{"query":{"bool":{"must":[{"bool":{"must":[{"exists":{"field":"name"}}],"must_not":[{"wildcard":{"name":{"wildcard":"*"}}}]}}]}}}
IsNotEmptyfindByNameIsNotEmpty{"query":{"bool":{"must":[{"wildcard":{"name":{"wildcard":"*"}}}]}}}