0. 서론
지난 1편에서는 Elasticsearch와 Kibana를 Docker 환경에서 설치하고 기본적인 설정을 진행해봤습니다. 이번 2편에서는 본격적으로 Spring Boot와 Elasticsearch를 활용한 검색 서비스를 구현해보려고 합니다.
이를 위해 Spring Data Elasticsearch를 활용해 티켓 정보를 저장하고 검색하는 기능을 구현해보겠습니다.
1. 개발환경
프로젝트에서 사용된 환경은 아래와 같습니다.
- JAVA 17
- SpringBoot 3.2
- Elasticsearch 8.6.0
2. Elasticsearch 활용한 검색서비스 만들기
Spring data Elasticsearch 의존성 추가하기
Spring Data Elasticsearch는 Spring과 Elasticsearch를 쉽게 연동할 수 있도록 도와주고, JPA처럼 Repository를 활용해 간단히 데이터를 저장하고 검색할 수 있도록 도와주는 라이브러리입니다.
Spring Data Elasticsearch를 사용하기 위해 Gradle 의존성을 추가해줍니다.
implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
Elasticsearch 연결을 위한 Config Class 만들기
Spring Data Elasticsearch는 ElasticsearchClient를 통해 Elasticsearch의 노드 또는 클러스터에 연결되는데 해당 연결을 위한 Config 파일을 만들어줍니다.
@Configuration
@EnableElasticsearchRepositories
public class ElasticsearchConfig extends ElasticsearchConfiguration {
@Value("${spring.elasticsearch.uris}")
private String host;
@Override
public ClientConfiguration clientConfiguration() {
return ClientConfiguration.builder()
.connectedTo(host)
.build();
}
}
@EnableElasticsearchRepositories : Elasticsearch Repository 활성화를 위한 어노테이션
TicketDocument 만들기
Elasticsearch 인덱스와 연결하기 위한 TicketDocument 클래스를 만들어줍니다.
package kr.doridos.ticketservice.ticket.entity;
import jakarta.persistence.Id;
import kr.doridos.ticketservice.category.entity.Category;
import kr.doridos.ticketservice.place.entity.Place;
import lombok.*;
import org.springframework.data.elasticsearch.annotations.*;
import java.time.LocalDateTime;
@Getter
@Document(indexName = "tickets", createIndex = true)
@Setting(settingPath = "elasticsearch/ticket-setting.json")
@Mapping(mappingPath = "elasticsearch/ticket-mapping.json")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class TicketDocument {
@Id
@Field(type = FieldType.Long)
private Long id;
@Field(type = FieldType.Text)
private String title;
@Field(type = FieldType.Text)
private String content;
@Field(type = FieldType.Text)
private String runningTime;
@Field(type = FieldType.Date, format = {DateFormat.date_hour_minute_second_millis, DateFormat.epoch_millis})
private LocalDateTime startDate;
@Field(type = FieldType.Date, format = {DateFormat.date_hour_minute_second_millis, DateFormat.epoch_millis})
private LocalDateTime endDate;
@Field(type = FieldType.Text)
private String place;
@Field(type = FieldType.Keyword)
private String category;
@Builder
public TicketDocument(Long id, String title, String content, String runningTime, LocalDateTime startDate, LocalDateTime endDate, String place, String category) {
this.id = id;
this.title = title;
this.content = content;
this.runningTime = runningTime;
this.startDate = startDate;
this.endDate = endDate;
this.place = place;
this.category = category;
}
public static TicketDocument from(Ticket ticket, Category category, Place place) {
return TicketDocument.builder()
.id(ticket.getId())
.title(ticket.getTitle())
.content(ticket.getContent())
.runningTime(ticket.getRunningTime())
.startDate(ticket.getStartDate())
.endDate(ticket.getEndDate())
.place(place.getName())
.category(category.getName())
.build();
}
}
@Document(indexName = "tickets") : tickets라는 엘라스틱서치 인덱스에 해당 클래스가 매핑됨
- createIndex = true(기본값) 엘라스틱서치에 해당 인덱스가 존재하지 않을 경우 자동으로 생성 false 설정시 엘라스틱서치에 인덱스를 만들어둬야 합니다.
@Field : 필드의 데이터 타입 지정
@Setting(settingPath = "elasticsearch/ticket-setting.json") : 인덱스의 분석기, 토크나이저 설정
@Mapping(mappingPath = "elasticsearch/ticket-setting.json") : 필드의 설정
(json파일을 만들고 설정할 경우 해당 json파일을 통해 인덱스 설정이 되고, 이 때 json 파일은 resource 아래에 위치해야합니다!)
ticket-setting.json
{
"analysis": {
"analyzer": {
"korean": {
"type": "nori"
}
}
}
}
ticket-mapping.json
{
"properties": {
"id": {
"type": "long"
},
"title": {
"type": "text",
"analyzer": "korean"
},
"content": {
"type": "text",
"analyzer": "korean"
},
"runningTime": {
"type": "text"
},
"startDate": {
"type": "date",
"format": "uuuu-MM-dd'T'HH:mm:ss.SSS||epoch_millis"
},
"endDate": {
"type": "date",
"format": "uuuu-MM-dd'T'HH:mm:ss.SSS||epoch_millis"
},
"place": {
"type": "text",
"analyzer": "korean"
},
"category": {
"type": "keyword"
}
}
}
ElasticsearchRepository
public interface TicketElasticSearchRepository extends ElasticsearchRepository<TicketDocument, Long> {
}
ElasticsearchRepository는 기본적인 CRUD를 제공하고, 메서드 이름 기반 쿼리 생성을 지원합니다. 해당 기능을 사용하기 위해 레포지토리를 만들어줍니다.
이후 해당 레파지토리를 통해 티켓을 새로 등록할 때 엘라스틱서치에도 데이터가 저장될 수 있도록 코드를 추가해줍니다.
public Long createTicket(final TicketCreateRequest request, final UserInfo userInfo) {
validateUserType(userInfo.getUserType());
validateEndIsNotBeforeOpen(request.getOpenDate(), request.getEndDate());
final Category category = categoryRepository.findById(request.getCategoryId())
.orElseThrow(() -> new CategoryNotFoundException(ErrorCode.CATEGORY_NOT_FOUND));
final Place place = placeRepository.findById(request.getPlaceId())
.orElseThrow(() -> new PlaceNotFoundException(ErrorCode.PLACE_NOT_FOUND));
final Ticket ticket = request.toEntity(place, userInfo, category);
ticketRepository.save(ticket);
ticketElasticsearchRepository.save(TicketDocument.from(ticket, category, place));
return ticket.getId();
}
3. 키바나를 통해 데이터가 엘라스틱서치에 잘 저장되었는지 확인하기
다음으로는 엘라스틱서치에 실제로 데이터가 제대로 저장되었는지 확인해보도록 하겠습니다.
포스트맨을 통해 티켓을 생성한 후 Kibana에 접속해줍니다.
이후 Analytics -> Discover -> Create Data View 순으로 이동해줍니다.
Create data view 버튼을 누르면 tickets 인덱스가 생성되있는 것을 확인할 수 있습니다.
그 후 name, index pattern(생성된 인덱스를 입력) Timestamp field를 설정한 후 view를 생성하면
엘라스틱서치에 저장된 데이터들을 확인할 수 있습니다.
4. API를 통한 테스트
간단한 API를 통해 엘라스틱서치에 저장된 데이터를 검색해보겠습니다.
TicketSearchService.class
@Service
@Transactional(readOnly = true)
public class TicketSearchService {
private final TicketElasticsearchRepository ticketElasticsearchRepository;
public TicketSearchService(final TicketElasticsearchRepository ticketElasticsearchRepository) {
this.ticketElasticsearchRepository = ticketElasticsearchRepository;
}
public List<TicketDocument> searchTicketsByKeyword(String keyword) {
return ticketElasticsearchRepository.findByTitle(keyword);
}
}
TicketElasticsearchRepository 타이틀을 검색하는 메서드 추가
public interface TicketElasticsearchRepository extends ElasticsearchRepository<TicketDocument, Long> {
List<TicketDocument> findByTitle(String title);
}
메서드 이름을 기반으로 스프링이 쿼리를 자동으로 생성해 편리한 사용이 가능합니다.
TicketSearchController.class
@RestController
@RequestMapping("/tickets/search")
@RequiredArgsConstructor
public class TicketSearchController {
private final TicketSearchService ticketSearchService;
@GetMapping
public ResponseEntity<List<TicketDocument>> ticketSearch(@RequestParam("keyword") String keyword) {
return ResponseEntity.ok(ticketSearchService.searchTicketsByKeyword(keyword));
}
}
해당 API를 통해 플레이오프라는 타이틀이 포함된 티켓을 검색하면 아래와 같은 결과를 확인할 수 있습니다.
치열한으로 검색했을 경우에도 데이터를 잘 가져오는 것을 확인할 수 있습니다.
5. 마무리 및 다음편 예고
지금까지 티켓 데이터를 저장하고 검색하는 기능을 구현하며, Kibana를 통해 데이터를 확인하는 과정까지 진행해봤습니다.
사실 원래는 조건별 검색 및 실시간 검색어 순위 기능까지 한 번에 정리하려 했지만, 글이 너무 길어지는 관계로 다음 편에서 자세히 다뤄보겠습니다...!
👉 3편에서는 보다 정교한 검색 기능을 추가하고, 실시간 검색어 순위 기능을 구현해보겠습니다. 🚀
참고
https://docs.spring.io/spring-data/elasticsearch/reference/elasticsearch.html
Elasticsearch Support :: Spring Data Elasticsearch
For most data-oriented tasks, you can use the [Reactive]ElasticsearchTemplate or the Repository support, both of which use the rich object-mapping functionality. Spring Data Elasticsearch uses consistent naming conventions on objects in various APIs to tho
docs.spring.io
https://tecoble.techcourse.co.kr/post/2021-10-19-elasticsearch/
Spring Data Elasticsearch 설정 및 검색 기능 구현
실습 Repository에서 코드를 확인할 수 있습니다. 1. Elasticsearch Elasticsearch는 Apache Lucene 기반의 Java 오픈소스 분산형 RESTful…
tecoble.techcourse.co.kr
'Backend > 프로젝트' 카테고리의 다른 글
Elasticsearch 활용한 검색 서비스 만들기 1편 (Docker로 Elasticsearch + Kibana 구축) (0) | 2025.03.10 |
---|---|
무중단 배포 적용하기 (0) | 2025.01.01 |
[Spring] 커버링 인덱스를 통한 페이징 성능 개선하기 (0) | 2024.12.12 |
[Spring] Redis 캐시 적용하기 (0) | 2024.10.31 |
nGrinder Docker 설치 및 사용방법 (0) | 2024.10.25 |