일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- jwt-java
- docker
- java
- DI
- curl
- 하이브리드 데이터 모델
- ELK
- mybatis
- Nice
- C++
- elastic search
- konga
- prometeus
- Spring
- template/callback
- 메소드
- 화자분리
- fosslight
- OpenSource
- API Gateway
- roll over
- pyannote
- umc
- supabase
- monitoring
- 파이썬
- metricbeat
- kong
- devops
- 자료구조
- Today
- Total
youngseo's TECH blog
[Java|Spring|AWS] OpenSearch Java API로 검색 기능 구현 본문
과제
진행 중인 프로젝트에서 검색 기능 구현 과제를 맡게 되었다. 키워드(ex. 헤어)를 인풋으로 받아 요청하면, 키워드가 포함된 포트폴리오 리스트들을 반환받아야 한다. 단순히 MySQL의 LIKE, OR 문법을 사용하여 구현할 수 있지만, OpenSearch(ElasticSearch) 검색엔진을 사용하게 되면 역인덱싱이 가능하기에 훨씬 빠르게 검색 기록을 가져올 수 있다. ElasticSearch(OpenSearch)의 역인덱싱 로직에 대한 설명은 이전 블로그 글에서 참고할 수 있다.
AWS OpenSearch vs ElasticSearch
AWS OpenSearch는 ElasticSearch를 기반으로 만들어진 툴이다. ElasticSearch와 OpenSearch의 역사(?)를 알아보면 재미있는 내용들이 많이 나오는데, 결론적으로 설명하면 ElasticSearch는 기존에 모두가 사용할 수 있는 오픈소스 였으나, 2021년 v7.10부터 SSPL로 전환하면서 이 ElasticSearch를 마음대로 가져다가 사용하는 것이 허용되지 않게 되었다. 그 이후 AWS는 ElasticSearch의 v7.10 에 자체적인 여러 기능들을 붙여 서비스하게 되었고, 이것이 AWS OpenSearch이다.
나는 ElasticSearch가 아닌 OpenSearch를 선택해 사용하게 되었다. 물론 ElasticSearch 가 레퍼런스가 더 많고, 사용해본 경험도 있으나, 현재 배포방식이 Beanstalk 속에 jar 파일로 배포되어있기 때문에 Docker 환경에 불리하고, Docker 환경으로 바꿔 배포한다고 하더라도 무중단 배포(Rolling배포 방식) 시 EC2가 기존에 한 개라 EC2 서버를 또 새로 만드는 BlueGreen 방식으로 동작하기 때문에 Docker 컨테이너간의 동기화 이슈가 있다고 한다.. 여기에서 이 모든 문제들을 엿볼 수 있었다.
따라서 AWS ElasticBeanstalk과 호환도 잘 되는 AWS 자체 검색 엔진인 OpenSearch를 사용해보기로 하였다.
ElasticSearch 공식 문서에 실제로 위와 같이 적혀 있다. 너무 OpenSearch 질문을 많이 받아서 그런가..ㅋㅋ싶기도 하지만 나도 실제로 제대로 알아보기 전에 이 둘이 너무 헷갈렸다.. 구현방법도 다르니 혼동하지 말자 ! 특히나 OpenSearch의 경우 레퍼런스가 정말 없으므로 꼭 OpenSearch 사이트 를 같이 보면서 팩트 체크 하면서 구현하길..
구현 과정
opensearch-java User Guide 와 Java Client for Opensearch 문서 를 참고하여 구현하였다. Deprecated된 것들(ex. RestClientTransport) 도 있으니 문서를 한 번 쭉 잘 읽어보는 것을 추천한다. 또한 Springboot 버전에 따라 호환되는 FrameWork 버전들이 다르니 이 또한 확인해야 한다. 꼭..! 꼭 잘 확인하세요 Spring Data OpenSearch와 OpenSearch Client 중에 구현 방법을 고민하다가 포맷이 좀 더 직관적으로 와닿는 OpenSearch Client를 사용하였다.
build.gradle 의존성
우선 build.gradle 에 OpenSearch 관련 의존성을 추가해준다.
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.0'
id 'io.spring.dependency-management' version '1.1.5'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
}
dependencies {
//OpenSearch 의존성 추가
implementation("org.opensearch.client:opensearch-rest-client:2.11.0")
implementation("org.opensearch.client:opensearch-java:2.7.0")
implementation("jakarta.json:jakarta.json-api")
...
OpenSearchConfig
이후 OpenSearchConfig를 설정해준다. org.springframework.web.client 패키지 하위의 RestClient.Builder를 사용하여 외부 서비스(AWS OpenSearch)와의 통신을 허용해준다.
@Configuration
public class OpenSearchConfig {
@Value("${spring.elasticsearch.uris}")
private String host;
@Value("${spring.elasticsearch.username}")
private String username;
@Value("${spring.elasticsearch.password}")
private String password;
@Bean
public OpenSearchClient openSearchClient() {
BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(
AuthScope.ANY,
new UsernamePasswordCredentials(username, password)
);
RestClientBuilder builder = RestClient.builder(new HttpHost(host, 443, "https"))
.setHttpClientConfigCallback(new RestClientBuilder.HttpClientConfigCallback(){
@Override
public HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder httpClientBuilder) {
return httpClientBuilder
.setDefaultCredentialsProvider(credentialsProvider)
.setDefaultIOReactorConfig(IOReactorConfig.custom()
.setIoThreadCount(1)
.build());
}
});
RestClient restClient = builder.build();
return new OpenSearchClient(new RestClientTransport(restClient, new JacksonJsonpMapper(new ObjectMapper())));
}
}
Service 계층 로직
도메인 엔드포인트에 저장되는 양식을 참고하여 indexing/search/delete 해주는 로직을 작성하면 된다. 코드는 직관적이니까 설명 패스 하겠다
@Service
public class PortfolioSearchService {
private final OpenSearchClient openSearchClient;
private static final String indexName = ${indexName};
private final PortfolioMapper portfolioMapper;
public PortfolioSearchService(OpenSearchClient openSearchClient, PortfolioMapper portfolioMapper) {
this.openSearchClient = openSearchClient;
this.portfolioMapper = portfolioMapper;
}
//인덱스 생성
public void createIndex() {
try {
CreateIndexRequest request = CreateIndexRequest.of(builder -> builder.index(indexName));
openSearchClient.indices().create(request);
} catch (Exception e) {
e.printStackTrace();
}
}
//OpenSearch에 인덱싱(저장)
public void indexDocumentUsingDTO(Portfolio portfolio) {
PortfolioSearchDTO.Request request = portfolioMapper.entityToSearchRequest(portfolio);
try {
IndexRequest<PortfolioSearchDTO.Request> indexRequest = IndexRequest.of(builder ->
builder.index(indexName)
.id(String.valueOf(request.getId()))
.document(request)
);
IndexResponse response = openSearchClient.index(indexRequest);
System.out.println("RESPONSE: "+response);
} catch (Exception e) {
e.printStackTrace();
}
}
//OpenSearch에 덮어쓰기(수정/업데이트)
public void updateDocumentUsingDTO(Portfolio portfolio){
PortfolioSearchDTO.Request request = portfolioMapper.entityToSearchRequest(portfolio);
try {
UpdateRequest<PortfolioSearchDTO.Request, Object> updateRequest = UpdateRequest.of(builder ->
builder.index(indexName)
.id(String.valueOf(portfolio.getId()))
.doc(request)
);
UpdateResponse updateResponse = openSearchClient.update(updateRequest, PortfolioSearchDTO.Request.class);
System.out.println("Update Response: " + updateResponse.get());
} catch (Exception e) {
e.printStackTrace();
}
}
//OpenSearch에서 검색
public List<PortfolioSearchDTO.Response> search(String keyword) {
List<PortfolioSearchDTO.Response> resultList = new ArrayList<>();
try {
SearchRequest request = SearchRequest.of(searchRequest ->
searchRequest.index(indexName)
.query(query ->
query.bool(bool ->
bool.should(should ->
should.wildcard(wildcard ->
wildcard.field("{특정 entity 필드 기입}")
.value("*" + keyword + "*")
)
)
.should(should ->
should.wildcard(wildcard ->
wildcard.field("특정 entity 필드 기입")
.value("*" + keyword + "*")
)
)
.should(should ->
should.wildcard(wildcard ->
wildcard.field("특정 entity 필드 기입")
.value("*" + keyword + "*")
)
)
.should(should ->
should.wildcard(wildcard ->
wildcard.field("특정 entity 필드 기입")
.value("*" + keyword + "*")
)
)
)
)
);
SearchResponse<PortfolioSearchDTO.Request> response = openSearchClient.search(request, PortfolioSearchDTO.Request.class);
List<Hit<PortfolioSearchDTO.Request>> hits = response.hits().hits();
System.out.println("HITS: "+hits);
for (Hit<PortfolioSearchDTO.Request> hit : hits) {
resultList.add(portfolioMapper.requestToSearchResponse(hit.source()));
}
} catch (Exception e) {
e.printStackTrace();
}
return resultList;
}
//OpenSearch에서 삭제
public void deleteDocumentById(Long id) {
try {
DeleteRequest deleteRequest = DeleteRequest.of(builder ->
builder.index(indexName)
.id(String.valueOf(id))
);
DeleteResponse response = openSearchClient.delete(deleteRequest);
System.out.println("Delete Response: " + response);
} catch (Exception e) {
e.printStackTrace();
}
}
}
결과
의도하는 포트폴리오가 잘 검색되었다 !
참고
opensearch-java/USER_GUIDE.md at main · opensearch-project/opensearch-java
Java Client for OpenSearch. Contribute to opensearch-project/opensearch-java development by creating an account on GitHub.
github.com
https://opensearch.org/docs/latest/clients/java/
Java client
Java client
opensearch.org
'BackEnd > JAVA\SPRING' 카테고리의 다른 글
[Spring Security] Spring Security 이용하여 헤더로 간단히 User 구분하기 (0) | 2024.07.10 |
---|---|
[S3|AWS] S3와 CloudFront로 이미지 저장소 만들기 (0) | 2024.06.30 |
[Java/Spring] 템플릿 콜백 패턴으로 JDBC 템플릿 구현해보기 (0) | 2024.04.02 |
[SpringSecurity] Koala 프로젝트 간단한 인증처리 (0) | 2024.02.13 |
[Java|Spring] Koala 프로젝트 구현 과정 중 이슈 정리 (0) | 2024.02.01 |