见UserRepositoryTest类
Spring Data Elasticsearch版本
Elasticsearch 8.5.2 对应Spring Data Elasticsearch 5.0.x版本和Spring Boot 3.0.x版
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 alive | false |
| 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来实现地理位置查询。
| 关键词 | 示例 | 查询字符串 |
|---|---|---|
And | findByNameAndPrice | { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] } }, { "query_string" : { "query" : "?", "fields" : [ "price" ] } } ] } }} |
Or | findByNameOrPrice | { "query" : { "bool" : { "should" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] } }, { "query_string" : { "query" : "?", "fields" : [ "price" ] } } ] } }} |
Is | findByName | { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] } } ] } }} |
Not | findByNameNot | { "query" : { "bool" : { "must_not" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] } } ] } }} |
Between | findByPriceBetween | { "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : ?, "to" : ?, "include_lower" : true, "include_upper" : true } } } ] } }} |
LessThan | findByPriceLessThan | { "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : null, "to" : ?, "include_lower" : true, "include_upper" : false } } } ] } }} |
LessThanEqual | findByPriceLessThanEqual | { "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : null, "to" : ?, "include_lower" : true, "include_upper" : true } } } ] } }} |
GreaterThan | findByPriceGreaterThan | { "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : ?, "to" : null, "include_lower" : false, "include_upper" : true } } } ] } }} |
GreaterThanEqual | findByPriceGreaterThan | { "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : ?, "to" : null, "include_lower" : true, "include_upper" : true } } } ] } }} |
Before | findByPriceBefore | { "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : null, "to" : ?, "include_lower" : true, "include_upper" : true } } } ] } }} |
After | findByPriceAfter | { "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : ?, "to" : null, "include_lower" : true, "include_upper" : true } } } ] } }} |
Like | findByNameLike | { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "?*", "fields" : [ "name" ] }, "analyze_wildcard": true } ] } }} |
StartingWith | findByNameStartingWith | { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "?*", "fields" : [ "name" ] }, "analyze_wildcard": true } ] } }} |
EndingWith | findByNameEndingWith | { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "*?", "fields" : [ "name" ] }, "analyze_wildcard": true } ] } }} |
Contains/Containing | findByNameContaining | { "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" : ["?","?"]}} ] } } ] } }} |
In | findByNameIn(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" : ["?","?"]}} ] } } ] } }} |
NotIn | findByNameNotIn(Collection<String>names) | {"query": {"bool": {"must": [{"query_string": {"query": "NOT("?" "?")", "fields": ["name"]}}]}}} |
True | findByAvailableTrue | { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "true", "fields" : [ "available" ] } } ] } }} |
False | findByAvailableFalse | { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "false", "fields" : [ "available" ] } } ] } }} |
OrderBy | findByAvailableTrueOrderByNameDesc | { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "true", "fields" : [ "available" ] } } ] } }, "sort":[{"name":{"order":"desc"}}] } |
Exists | findByNameExists | {"query":{"bool":{"must":[{"exists":{"field":"name"}}]}}} |
IsNull | findByNameIsNull | {"query":{"bool":{"must_not":[{"exists":{"field":"name"}}]}}} |
IsNotNull | findByNameIsNotNull | {"query":{"bool":{"must":[{"exists":{"field":"name"}}]}}} |
IsEmpty | findByNameIsEmpty | {"query":{"bool":{"must":[{"bool":{"must":[{"exists":{"field":"name"}}],"must_not":[{"wildcard":{"name":{"wildcard":"*"}}}]}}]}}} |
IsNotEmpty | findByNameIsNotEmpty | {"query":{"bool":{"must":[{"wildcard":{"name":{"wildcard":"*"}}}]}}} |