이전글을 보면 docker위에 es와 키바나가 설치된 것 까지 진행되었다. 이번에는 spring boot환경을 세팅할 거다!
api를 생성하려면? 기존에 Entity를 생성 -> Repository 생성 -> Service를 생성 -> controller 등록한다.
그런데 우리는 지금 검색엔진을 만든다. 따라서 검색에 사용할 api라는 것인데 findById와 같은 query보다는 동적쿼리에 집중해서 알아볼 것이다.
여기서는 Criteria를 사용할 것이다.
package com.sparta.projectblue.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
import org.springframework.beans.factory.annotation.Value;
@Configuration
@EnableElasticsearchRepositories(basePackages = "org.springframework.data.elasticsearch.repository")
public class ElasticSearchConfig extends ElasticsearchConfiguration {
@Value("${elastic.host}")
private String elasticHost;
@Override
public ClientConfiguration clientConfiguration() {
return ClientConfiguration.builder().connectedTo(elasticHost).build();
}
}
properties

elastic:
host: ${ES_URI}
다른 코드들도 많아서 es 관련 부분만 편집했다.
spring밑에 es를 넣어주고
host는 환경변수로 줬다.
나는 시큐리티는 이용하지 않아서 pw는 없었다!
내가 만들 검색 api는 다음과 같다.
사실 이래도 되는 지 잘 모르겠다(일단 주먹구구식 구현했음)
1) Document 생성: 인덱스에 들어갈 데이터 형식을 정의한다.
2) Index 생성: Elasticsearch에서 데이터를 저장할 인덱스를 만든다.
3) 데이터 동기화: DB에서 Elasticsearch로 데이터를 옮기는 동기화 API를 구현한다.
4) 검색 API 구현: 관리자가 검색할 수 있도록 API를 작성한다.
차례대로 구현했다.
코드는 다음과 같다. 사실 초반에는 결제부분까지 document에 넣었는데 현재 구현된 건 예매 부분까지밖에 없다.
그래서 후에 확장 가능하면 이 부분을 확장해볼 계획이다.
(document, dto, service, controller)
package com.sparta.projectblue.domain.search.document;
import java.time.LocalDateTime;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
@Document(indexName = "user_booking_index_v4")
public class UserBookingDocument {
@Id private Long reservationId;
@Field(type = FieldType.Text)
private String userName;
@Field(type = FieldType.Long)
private Long userId;
@Field(type = FieldType.Text)
private String performanceTitle;
@Field(
type = FieldType.Date,
format = {},
pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime bookingDate; // 예매한 날짜
@Field(type = FieldType.Long)
private Long paymentAmount; // 예매한 티켓 금액
@Field(type = FieldType.Text)
private String reservationStatus; // 예매 상태
@Field(type = FieldType.Long)
private Long paymentId; // 결제id (추가구현 가능하려나)
@Field(
type = FieldType.Date,
format = {},
pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime paymentDate; // 결제 된 날짜(추가구현 가능하려나2)
public UserBookingDocument(
Long reservationId,
String userName,
Long userId,
String performanceTitle,
LocalDateTime bookingDate,
Long paymentAmount,
String reservationStatus,
Long paymentId,
LocalDateTime paymentDate) {
this.reservationId = reservationId;
this.userName = userName;
this.userId = userId;
this.performanceTitle = performanceTitle;
this.bookingDate = bookingDate;
this.paymentAmount = paymentAmount;
this.reservationStatus = reservationStatus;
this.paymentId = paymentId;
this.paymentDate = paymentDate;
}
}
package com.sparta.projectblue.domain.search.dto;
import java.time.LocalDateTime;
import com.sparta.projectblue.domain.common.enums.ReservationStatus;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class UserBookingDto {
// 동기화와 검색에 공통으로 사용되는 필드
private Long reservationId;
private String userName;
private Long userId;
private String performanceTitle;
private LocalDateTime bookingDate;
private Long paymentAmount;
private ReservationStatus reservationStatus;
private Long paymentId;
private LocalDateTime paymentDate;
// 검색에만 사용되는 필드
private LocalDateTime bookingDateStart;
private LocalDateTime bookingDateEnd;
private Long minPaymentAmount;
private Long maxPaymentAmount;
private String searchReservationStatus;
public UserBookingDto(Long reservationId, String userName, Long userId, String performanceTitle,
LocalDateTime bookingDate, Long paymentAmount, ReservationStatus reservationStatus,
Long paymentId, LocalDateTime paymentDate) {
this.reservationId = reservationId;
this.userName = userName;
this.userId = userId;
this.performanceTitle = performanceTitle;
this.bookingDate = bookingDate;
this.paymentAmount = paymentAmount;
this.reservationStatus = reservationStatus;
this.paymentId = paymentId;
this.paymentDate = paymentDate;
}
}
package com.sparta.projectblue.domain.search.service;
import java.util.List;
import java.util.stream.Collectors;
import com.sparta.projectblue.domain.reservation.repository.ReservationRepository;
import org.springframework.stereotype.Service;
import com.sparta.projectblue.domain.search.document.UserBookingDocument;
import com.sparta.projectblue.domain.search.dto.UserBookingDto;
import com.sparta.projectblue.domain.search.repository.UserBookingEsRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class UserBookingSearchService {
private final UserBookingEsRepository userBookingEsRepository;
private final ReservationRepository reservationRepository;
@Transactional
public void syncData() {
// 데이터베이스에서 필요한 데이터 조회
List<UserBookingDocument> documents = reservationRepository.findUserBookingData().stream()
.map(this::convertToDocument)
.collect(Collectors.toList());
// Elasticsearch 인덱스에 동기화
userBookingEsRepository.saveAll(documents);
}
private UserBookingDocument convertToDocument(UserBookingDto dto) {
return new UserBookingDocument(
dto.getReservationId(),
dto.getUserName(),
dto.getUserId(),
dto.getPerformanceTitle(),
dto.getBookingDate(),
dto.getPaymentAmount(),
dto.getReservationStatus().name(), // Enum을 문자열로 변환하여 저장
dto.getPaymentId(),
dto.getPaymentDate()
);
}
public List<UserBookingDocument> searchBookings(UserBookingDto searchCriteria) {
return userBookingEsRepository.searchByCriteria(searchCriteria);
}
}
// UserBookingController.java
package com.sparta.projectblue.domain.search.controller;
import java.time.LocalDateTime;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.sparta.projectblue.config.ApiResponse;
import com.sparta.projectblue.domain.search.dto.UserBookingDto;
import com.sparta.projectblue.domain.search.service.UserBookingSearchService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
@RestController
@RequestMapping("/search")
@RequiredArgsConstructor
@Tag(name = "Search", description = "검색 API")
public class UserBookingController {
private final UserBookingSearchService searchService;
@GetMapping("/admin")
@Operation(summary = "관리자 정보 검색", description = "다양한 조건을 이용해 정보를 검색합니다")
public ResponseEntity<?> searchBookings(
@RequestParam(required = false) String userName,
@RequestParam(required = false) String performanceTitle,
@RequestParam(required = false) LocalDateTime bookingDateStart,
@RequestParam(required = false) LocalDateTime bookingDateEnd,
@RequestParam(required = false) String searchReservationStatus,
@RequestParam(required = false) Long minPaymentAmount,
@RequestParam(required = false) Long maxPaymentAmount) {
// 검색 파라미터를 UserBookingDto에 매핑
UserBookingDto dto = new UserBookingDto(
null, // reservationId는 검색에 필요하지 않으므로 null로 설정
userName,
null, // userId는 검색에 필요하지 않으므로 null로 설정
performanceTitle,
null, // bookingDate는 단일 값이 아닌 범위를 사용하므로 null로 설정
null, // paymentAmount는 범위로 검색하므로 null로 설정
null, // reservationStatus는 문자열로 처리하여 searchReservationStatus에 매핑
null, // paymentId는 검색에 필요하지 않으므로 null로 설정
null, // paymentDate는 검색에 필요하지 않으므로 null로 설정
bookingDateStart,
bookingDateEnd,
minPaymentAmount,
maxPaymentAmount,
searchReservationStatus // 검색 상태 문자열
);
return ResponseEntity.ok(ApiResponse.success(searchService.searchBookings(dto)));
}
@GetMapping("/sync")
@Operation(summary = "데이터 동기화", description = "예약 정보를 Elasticsearch와 동기화합니다")
public ResponseEntity<ApiResponse<String>> syncData() {
searchService.syncData();
return ResponseEntity.ok(ApiResponse.success("데이터 동기화가 완료되었습니다."));
}
}
동기화와 검색 api 들은 전부 이렇게 구현했다.
package com.sparta.projectblue.domain.search.repository;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import com.sparta.projectblue.domain.search.document.UserBookingDocument;
public interface UserBookingEsRepository
extends ElasticsearchRepository<UserBookingDocument, Long>, UserBookingEsCustomRepository {}
package com.sparta.projectblue.domain.search.repository;
import java.util.List;
import com.sparta.projectblue.domain.search.document.UserBookingDocument;
import com.sparta.projectblue.domain.search.dto.UserBookingDto;
public interface UserBookingEsCustomRepository {
List<UserBookingDocument> searchByCriteria(UserBookingDto request);
}
package com.sparta.projectblue.domain.search.repository;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
import org.springframework.stereotype.Repository;
import com.sparta.projectblue.domain.search.document.UserBookingDocument;
import com.sparta.projectblue.domain.search.dto.UserBookingDto;
import lombok.RequiredArgsConstructor;
@Repository
@RequiredArgsConstructor
public class UserBookingEsRepositoryImpl implements UserBookingEsCustomRepository {
private final ElasticsearchOperations elasticsearchOperations;
@Override
public List<UserBookingDocument> searchByCriteria(UserBookingDto request) {
Criteria criteria = new Criteria();
// 사용자 이름 조건 추가
if (request.getUserName() != null && !request.getUserName().isEmpty()) {
criteria = criteria.and("userName").is(request.getUserName());
}
// 공연 제목 조건 추가
if (request.getPerformanceTitle() != null && !request.getPerformanceTitle().isEmpty()) {
criteria = criteria.and("performanceTitle").is(request.getPerformanceTitle());
}
// 예약 날짜 범위 조건 추가
if (request.getBookingDateStart() != null && request.getBookingDateEnd() != null) {
criteria = criteria.and("bookingDate")
.between(request.getBookingDateStart(), request.getBookingDateEnd());
} else if (request.getBookingDateStart() != null) {
criteria = criteria.and("bookingDate").greaterThanEqual(request.getBookingDateStart());
} else if (request.getBookingDateEnd() != null) {
criteria = criteria.and("bookingDate").lessThanEqual(request.getBookingDateEnd());
}
// 결제 금액 범위 조건 추가
if (request.getMinPaymentAmount() != null && request.getMaxPaymentAmount() != null) {
criteria = criteria.and("paymentAmount")
.between(request.getMinPaymentAmount(), request.getMaxPaymentAmount());
} else if (request.getMinPaymentAmount() != null) {
criteria = criteria.and("paymentAmount").greaterThanEqual(request.getMinPaymentAmount());
} else if (request.getMaxPaymentAmount() != null) {
criteria = criteria.and("paymentAmount").lessThanEqual(request.getMaxPaymentAmount());
}
// 예약 상태 조건 추가
if (request.getSearchReservationStatus() != null && !request.getSearchReservationStatus().isEmpty()) {
criteria = criteria.and("reservationStatus").is(request.getSearchReservationStatus());
}
CriteriaQuery query = new CriteriaQuery(criteria);
return elasticsearchOperations
.search(query, UserBookingDocument.class)
.getSearchHits()
.stream()
.map(hit -> hit.getContent())
.collect(Collectors.toList());
}
}
여기서 의존성 문제가 엄청 터졌다...
꼭 스프링과 es 관련 의존성 문제 확인하고 설치하시길... (나는 후반에 알기도 했고, 이미 팀원과 환경을 동일시 해놔서...방법이 없었따)
PUT /user_booking_index_v4
{
"mappings": {
"properties": {
"reservationId": {
"type": "long"
},
"userName": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"userId": {
"type": "long"
},
"performanceTitle": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"bookingDate": {
"type": "date",
"format": "yyyy-MM-dd'T'HH:mm:ss"
},
"paymentAmount": {
"type": "long"
},
"reservationStatus": {
"type": "text"
},
"paymentId": {
"type": "long"
},
"paymentDate": {
"type": "date",
"format": "yyyy-MM-dd'T'HH:mm:ss"
}
}
}
}
코드는 이러하고 다음에는 결과물을 들고오겠다.