youngseo's TECH blog

[Java|Spring|AWS] OpenSearch Java API로 검색 기능 구현 본문

BackEnd/JAVA\SPRING

[Java|Spring|AWS] OpenSearch Java API로 검색 기능 구현

jeonyoungseo 2024. 7. 21. 18:48

과제

진행 중인 프로젝트에서 검색 기능 구현 과제를 맡게 되었다. 키워드(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 OpenSearchOpenSearch 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