0. 개요
스프링부트로 엘라스틱서치에 접근해서 쿼리를 날려보겠습니다.
프로젝트 구조는 아래와 같습니다.
└─es
│
└─src
└─main
├─java
│ └─com
│ └─example
│ └─es_springboot
│ │ EsSpringBootApplication.java
│ │
│ ├─config
│ │ AbstractElasticsearchConfiguration.java
│ │ ElasticSearchConfig.java
│ │
│ ├─controller
│ │ │ UserController.java
│ │ │
│ │ └─dto
│ │ ├─port_in
│ │ │ RequestSearchConditionDto.java
│ │ │ RequestUserSaveDto.java
│ │ │
│ │ └─port_out
│ │ ResponseUserDto.java
│ │
│ ├─domain
│ │ └─es
│ │ UserDocument.java
│ │
│ ├─repository
│ │ └─es
│ │ UserSearchCriteriaQueryRepository.java
│ │ UserSearchNativeQueryRepository.java
│ │ UserSearchRepository.java
│ │
│ └─service
│ UserService.java
│
└─resources
│ application.properties
│
└─elastic
es-mapping.json
es-setting.json
💡 설명을 위해 DTO와 같은 불필요한 부분을 제거할 것입니다. 자세한 프로젝트와 코드 정보를 알고 싶다면 아래에 있는 github 레포지토리 주소로 가시면 보실 수 있습니다.
💡 https://github.com/kang-seongbeom/ES-SpringBoot
💡 이 글은 https://ksb-dev.tistory.com/320 와 연관됩니다.
build.gradle에 아래의 의존성을 추가하셔야 합니다.
implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
1. 리소스 파일 설정하기
먼저 리소스 파일을 설정해 보겠습니다.
1.1 application.properties
엘라스틱서치의 로그를 날길 수 있도록 설정합니다.
logging.level.org.springframework.data.elasticsearch.client.WIRE=TRACE
1.2 es-mapping.json
해당 프로젝트가 시작될 때 엘라스틱 서치에 생성할 인덱스를 정의합니다.
{
"properties": {
"id": {
"type": "keyword"
},
"name": {
"type": "keyword"
},
"age": {
"type": "long"
},
"korean": {
"type": "text",
"analyzer": "ksb_custom_korean_analyzer"
},
"english": {
"type": "text",
"search_analyzer": "ksb_custom_english_analyzer"
},
"created": {
"type": "date",
"format": "uuuu-MM-dd'T'HH:mm:ss"
}
}
}
💡 기존에 해당 인덱스가 없는 경우에만 생성합니다.
1.3 es-setting.json
인덱스가 사용할 Analizer를 정의합니다.
{
"analysis": {
"analyzer": {
"ksb_custom_korean_analyzer": {
"type": "custom",
"tokenizer": "ksb_custom_korean_tokenizer",
"filter": [
"ksb_custom_ngram_filter",
"ksb_custom_korean_stop_filter"
]
},
"ksb_custom_english_analyzer": {
"type": "custom",
"tokenizer": "whitespace",
"filter": [
"lowercase",
"snowball",
"ksb_custom_english_stop_filter",
"ksb_custom_english_synonym_filter",
"unique"
]
}
},
"tokenizer": {
"ksb_custom_korean_tokenizer": {
"type": "nori_tokenizer",
"decompound_mode": "mixed"
}
},
"filter": {
"ksb_custom_ngram_filter": {
"type": "ngram",
"min_gram": 2,
"max_gram": 5
},
"ksb_custom_korean_stop_filter": {
"type": "nori_part_of_speech",
"stoptags": [
"E",
"IC"
]
},
"ksb_custom_english_stop_filter": {
"type": "stop",
"stopwords_path": "settings/stop/english.txt"
},
"ksb_custom_english_synonym_filter": {
"type": "synonym",
"synonyms_path": "settings/synonym/english.txt",
"updateable": true
}
}
},
"max_ngram_diff": "10"
}
2. Config
엘라스틱서치와의 통신에 필요한 설정정보를 정의합니다.
2.1 AbstractElasticsearchConfiguration
public abstract class AbstractElasticsearchConfiguration
extends ElasticsearchConfigurationSupport {
@Bean
public abstract RestHighLevelClient elasticsearchClient();
// repository에서 불러오는 ElasticsearchOperations 빈이 이 것입니다.
@Bean(name = { "elasticsearchOperations", "elasticsearchTemplate" })
public ElasticsearchOperations elasticsearchOperations(ElasticsearchConverter elasticsearchConverter,
RestHighLevelClient elasticsearchClient) {
ElasticsearchRestTemplate template = new ElasticsearchRestTemplate(elasticsearchClient, elasticsearchConverter);
template.setRefreshPolicy(refreshPolicy());
return template;
}
}
2.2 ElasticSearchConfig
2.1에서 정의한 AbstractElasticsearchConfiguration을 상속받습니다.
@Configuration
@EnableElasticsearchRepositories
public class ElasticSearchConfig extends AbstractElasticsearchConfiguration {
@Override
public RestHighLevelClient elasticsearchClient() {
ClientConfiguration clientConfiguration = ClientConfiguration.builder()
.connectedTo("localhost:9200")
.build();
return RestClients.create(clientConfiguration).rest();
}
}
3. Domain
3.1 UserDocument
1.2에서 정의한 정보를 매핑해 인덱스 생성에 사용될 수 있게 합니다.
@Document(indexName = "user_index")
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
@Getter
@Setting(settingPath = "elastic/es-setting.json")
@Mapping(mappingPath = "elastic/es-mapping.json")
public class UserDocument {
private Long id;
private String name;
private Long age;
private String korean;
private String english;
@Field(type = FieldType.Date, format = DateFormat.custom, pattern = "uuuu-MM-dd'T'HH:mm:ss")
private LocalDateTime created;
public static UserDocument of(RequestUserSaveDto dto) {
return UserDocument.builder()
.id(dto.getId())
.name(dto.getName())
.age(dto.getAge())
.korean(dto.getKorean())
.english(dto.getEnglish())
.created(LocalDateTime.now())
.build();
}
}
4. Repository
4.1 UserSeachRepository
ElasticsearchRepositoy는 JPA 기반으로 만들어졌습니다. 때문에, 네이밍 전략을 사용할 수 있습니다.
public interface UserSearchRepository extends ElasticsearchRepository<UserDocument, Long> {
List<UserDocument> findByName(String name);
List<UserDocument> findByKorean(String korean);
List<UserDocument> findByEnglish(String english);
}
4.2 UserSearchCriteriaQueryRepository
Criteria 기반으로 쿼리를 날리 수 있는 동적 쿼리입니다.
@Repository
@RequiredArgsConstructor
public class UserSearchCriteriaQueryRepository {
private final ElasticsearchOperations operations;
public List<UserDocument> findByCriteriaCondition(RequestSearchConditionDto dto) {
CriteriaQuery query = createConditionCriteriaQuery(dto);
SearchHits<UserDocument> search = operations.search(query, UserDocument.class);
return search
.stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
}
private CriteriaQuery createConditionCriteriaQuery(RequestSearchConditionDto dto){
CriteriaQuery query = new CriteriaQuery(new Criteria());
if(dto == null){
return query;
}
if(dto.getId() != null){
query.addCriteria(Criteria.where("id").is(dto.getId()));
}
if(dto.getAge() != null && dto.getAge() > 0){
query.addCriteria(Criteria.where("age").is(dto.getAge()));
}
if(StringUtils.hasText(dto.getName())){
query.addCriteria(Criteria.where("name").is(dto.getName()));
}
if(StringUtils.hasText(dto.getKorean())){
query.addCriteria(Criteria.where("korean").is(dto.getKorean()));
}
if(StringUtils.hasText(dto.getEnglish())){
query.addCriteria(Criteria.where("english").is(dto.getEnglish()));
}
return query;
}
}
4.3 UserSearchNativeQueryRepository
엘라스틱서치 Native기반의 동적 쿼리입니다.
@Repository
@RequiredArgsConstructor
public class UserSearchNativeQueryRepository {
private final ElasticsearchOperations operations;
public List<UserDocument> findByNativeCondition(RequestSearchConditionDto dto) {
Query query = createConditionNativeQuery(dto);
SearchHits<UserDocument> search = operations.search(query, UserDocument.class);
return search
.stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
}
private NativeSearchQuery createConditionNativeQuery(RequestSearchConditionDto dto) {
NativeSearchQueryBuilder query = new NativeSearchQueryBuilder();
// BoolQueryBuilder mustBoolQueryBuilder = boolQuery();
BoolQueryBuilder shouldBoolQueryBuilder = boolQuery();
BoolQueryBuilder filterBoolQueryBuilder = boolQuery();
if(dto == null){
return query.build();
}
if(dto.getId() != null){
filterBoolQueryBuilder.filter(matchQuery("id", dto.getId()));
}
if(dto.getAge() != null && dto.getAge() > 0){
shouldBoolQueryBuilder.should(matchQuery("age", dto.getAge()));
}
if(StringUtils.hasText(dto.getName())){
shouldBoolQueryBuilder.should(matchQuery("name", dto.getName()));
}
if(StringUtils.hasText(dto.getKorean())){
shouldBoolQueryBuilder.should(matchQuery("korean", dto.getKorean()));
}
if(StringUtils.hasText(dto.getEnglish())){
shouldBoolQueryBuilder.should(matchQuery("english", dto.getEnglish()));
}
query.withQuery(shouldBoolQueryBuilder)
.withFilter(filterBoolQueryBuilder)
.withSorts(SortBuilders.fieldSort("age")
.order(SortOrder.DESC));
return query.build();
}
}
5. Service
5.1 UserService
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserService {
private final UserSearchRepository repository;
private final UserSearchCriteriaQueryRepository criteriaQueryRepository;
private final UserSearchNativeQueryRepository nativeQueryRepository;
public void saveAll(List<RequestUserSaveDto> info) {
List<UserDocument> users = info.stream()
.map(UserDocument::of)
.collect(Collectors.toList());
repository.saveAll(users);
}
public List<ResponseUserDto> findByName(String name) {
return transInfo(repository.findByName(name));
}
public List<ResponseUserDto> findByKorean(String korean) {
return transInfo(repository.findByKorean(korean));
}
public List<ResponseUserDto> findByEnglish(String english) {
return transInfo(repository.findByEnglish(english));
}
public List<ResponseUserDto> findByCriteriaCondition(RequestSearchConditionDto dto){
return transInfo(criteriaQueryRepository.findByCriteriaCondition(dto));
}
public List<ResponseUserDto> findByNativeCondition(RequestSearchConditionDto dto){
return transInfo(nativeQueryRepository.findByNativeCondition(dto));
}
private List<ResponseUserDto> transInfo(List<UserDocument> users) {
return users.stream()
.map(ResponseUserDto::of)
.collect(Collectors.toList());
}
}
6. Controller
6.1 UserController
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService service;
@PostMapping("/save/users")
public ResponseEntity<Void> saveAll(@RequestBody Map<String, List<RequestUserSaveDto>> users){
service.saveAll(users.getOrDefault("users", List.of()));
return ResponseEntity.ok().build();
}
@GetMapping("/name")
ResponseEntity<List<ResponseUserDto>> searchByName(@RequestParam String name){
return ResponseEntity.ok(service.findByName(name));
}
@GetMapping("/korean")
ResponseEntity<List<ResponseUserDto>> searchKoren(@RequestParam String korean){
return ResponseEntity.ok(service.findByKorean(korean));
}
@GetMapping("/english")
ResponseEntity<List<ResponseUserDto>> searchEnglish(@RequestParam String english){
return ResponseEntity.ok(service.findByEnglish(english));
}
@PostMapping("/criteria/condition")
ResponseEntity<List<ResponseUserDto>> searchCriteriaCondition(@RequestBody RequestSearchConditionDto dto){
return ResponseEntity.ok(service.findByCriteriaCondition(dto));
}
@PostMapping("/native/condition")
ResponseEntity<List<ResponseUserDto>> searchNativeCondition(@RequestBody RequestSearchConditionDto dto){
return ResponseEntity.ok(service.findByNativeCondition(dto));
}
}
7. SpringBootApplication
7.1 EsSpringBootApplication
스프링부트 프로젝트 생성시 기본으로 만들어지는 클래스입니다.
해당 클래스에 @EnableJpaRepositories가 붙어있는 것을 보실 수 있는데, 이에 대한 설명은
https://ksb-dev.tistory.com/314 에서 확인할 수 있습니다.
@EnableJpaRepositories(excludeFilters = @ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = UserSearchRepository.class))
@SpringBootApplication
public class EsSpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(EsSpringBootApplication.class, args);
}
}
'엘라스틱 서치' 카테고리의 다른 글
Spring Boot Jpa 네이티브 쿼리로 엘라스틱서치 쿼리 구현하기 (0) | 2024.05.26 |
---|---|
뉴스 데이터를 위한 엘라스틱 서치 쿼리 모음 (0) | 2024.05.26 |
ELK, Mysql, Kafka 구축 및 연동 (0) | 2023.06.02 |
스프링부트 테스트와 엘라스틱서치 테스트 컨테이너 (0) | 2023.05.27 |
엘라스틱서치 검색 Query DSL (0) | 2023.05.18 |