ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring Boot] OpenSearch 연동
    Spring Boot/기타 2024. 7. 12. 23:27
    반응형

    1. Docker를 사용하여 OpenSearch 설치 및 실행

    1_1. OpenSearch 설치
    Docker를 사용하여 로컬에 Opensearch를 설치하세요.

    docker pull opensearchproject/opensearch:1.3.

     

    1_2. OpenSearch 실행
    OpenSearch Docker 이미지를 다음의 명령어를 실행하여 로컬에서 실행하세요.

    docker run -p 9200:9200 -p 9600:9600 -e "discovery.type=single-node" -e DISABLE_SECURITY_PLUGIN=true opensearchproject/opensearch:1.3.17


    1_3. OpenSearch 실행 상태 확인
    OpenSearch가 정상적으로 실행되고 있는지 확인할 수 있습니다. 아래의 명령어를 실행하여 OpenSearch 인스턴스, 노드 및 플러그인 상태를 확인하세요.

    curl -XGET http://localhost:9200 -u 'admin:admin' --insecure
    curl -XGET http://localhost:9200/_cat/nodes?v -u 'admin:admin' --insecure
    curl -XGET http://localhost:9200/_cat/plugins?v -u 'admin:admin' --insecure

     

    Docker Image - https://opensearch.org/docs/1.0/opensearch/install/docker
    Docker Hub - https://hub.docker.com/r/opensearchproject/opensearch/tags

     

    2. Spring Boot와 OpenSearch를 연동

    2_1. gradle 설정

    • org.opensearch.client:opensearch-java:2.11.1는 OpenSearch의 Java Client 라이브러리로 OpenSearch에 접근하여 색인, 검색, 삭제 등의 작업을 수행
    • org.apache.httpcomponents.client5:httpclient5:5.3.1는 HttpClient 라이브러리로 Java에서 HTTP 프로토콜을 기반으로 서버와 통신할 때 사용

    build.gradle

    plugins {
        id 'java'
        id 'org.springframework.boot' version '3.3.1'
        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 {
    
        // spring boot
        implementation 'org.springframework.boot:spring-boot-starter-web'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
        testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    
        // opensearch
        implementation 'org.opensearch.client:opensearch-java:2.11.1'
    
        // httpclient5
        implementation 'org.apache.httpcomponents.client5:httpclient5:5.3.1'
    
        // lombok
        compileOnly 'org.projectlombok:lombok'
        annotationProcessor 'org.projectlombok:lombok'
        testCompileOnly 'org.projectlombok:lombok'
        testAnnotationProcessor 'org.projectlombok:lombok'
    
        // apache common
        implementation 'org.apache.commons:commons-lang3:3.11'
    }
    
    tasks.named('test') {
        useJUnitPlatform()
    }


    2_2. config 설정 
    OpenSearch 서버와 통신할 때 필요한 설정을 하고 OpenSearchClient 객체를 생성하세요.

    OpenSearchConfig.java

    package com.example.opensearch.config;
    
    import org.apache.hc.client5.http.auth.AuthScope;
    import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
    import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
    import org.apache.hc.core5.http.HttpHost;
    import org.opensearch.client.opensearch.OpenSearchClient;
    import org.opensearch.client.transport.OpenSearchTransport;
    import org.opensearch.client.transport.httpclient5.ApacheHttpClient5TransportBuilder;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class OpenSearchConfig {
    
      private static final String SCHEME = "http";
      private static final String HOST = "localhost";
      private static final int PORT = 9200;
      private static final String USERNAME = "admin";
      private static final String PASSWORD = "admin";
    
      /**
       * OpenSearchClient Bean 설정
       *
       * @return OpenSearchClient
       */
      @Bean
      public OpenSearchClient openSearchClient() {
    
        final HttpHost httpHost = new HttpHost(SCHEME, HOST, PORT);
    
        // 인증 정보를 설정
        BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
        credentialsProvider.setCredentials(new AuthScope(httpHost),
            new UsernamePasswordCredentials(USERNAME, PASSWORD.toCharArray()));
    
        // OpenSearch와 통신하기 위한 OpenSearchTransport 객체를 생성
        final OpenSearchTransport transport =
            ApacheHttpClient5TransportBuilder.builder(httpHost)
                .setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder
                    .setDefaultCredentialsProvider(credentialsProvider)).build();
    
        return new OpenSearchClient(transport);
      }
    }


    2_3. OpenSearch 서버와의 Index 및 Document CRUD 샘플
    OpenSearchClient 객체를 사용하여 OpenSearch 서버와 통신하는 각각 메서드의 요청 및 응답을 확인하세요.
    SampleService.java

    package com.example.opensearch.sample.service;
    
    import com.example.opensearch.sample.service.dto.SampleDto;
    import java.util.List;
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.opensearch.client.opensearch.OpenSearchClient;
    import org.opensearch.client.opensearch._types.FieldValue;
    import org.opensearch.client.opensearch._types.InlineScript;
    import org.opensearch.client.opensearch.core.DeleteResponse;
    import org.opensearch.client.opensearch.core.IndexRequest;
    import org.opensearch.client.opensearch.core.IndexResponse;
    import org.opensearch.client.opensearch.core.SearchRequest;
    import org.opensearch.client.opensearch.core.SearchResponse;
    import org.opensearch.client.opensearch.core.UpdateByQueryRequest;
    import org.opensearch.client.opensearch.core.UpdateByQueryResponse;
    import org.opensearch.client.opensearch.indices.CreateIndexRequest;
    import org.opensearch.client.opensearch.indices.CreateIndexRequest.Builder;
    import org.opensearch.client.opensearch.indices.CreateIndexResponse;
    import org.opensearch.client.opensearch.indices.DeleteIndexRequest;
    import org.opensearch.client.opensearch.indices.DeleteIndexResponse;
    import org.opensearch.client.opensearch.indices.ExistsRequest;
    import org.opensearch.client.transport.endpoints.BooleanResponse;
    import org.springframework.stereotype.Service;
    
    @Slf4j
    @RequiredArgsConstructor
    @Service
    public class SampleService {
    
      private final OpenSearchClient openSearchClient;
    
      /**
       * Index 생성
       *
       * @param indexName 생성할 인덱스명
       * @return CreateIndexResponse Index 생성에 대한 응답을 담고 있는 객체
       */
      public CreateIndexResponse createIndex(String indexName) {
    
        CreateIndexResponse createIndexResponse = null;
    
        try {
    
          CreateIndexRequest createIndexRequest = new Builder()
              .index(indexName).build();
          createIndexResponse = openSearchClient.indices().create(createIndexRequest);
    
        } catch (Exception e) {
          log.error("createIndex indexName : [{}]", indexName, e);
        }
    
        return createIndexResponse;
      }
    
      /**
       * Index 존재 여부 조회
       *
       * @param indexName 조회할 인덱스명
       * @return BooleanResponse 인덱스 여부 응답을 담고 있는 객체
       */
      public BooleanResponse existIndex(String indexName) {
    
        BooleanResponse booleanResponse = null;
        try {
          ExistsRequest existsRequest = ExistsRequest.of(
              r -> r.index(indexName)
          );
          booleanResponse = openSearchClient.indices().exists(existsRequest);
        } catch (Exception e) {
          log.error("existIndex indexName : [{}]", indexName, e);
        }
    
        return booleanResponse;
      }
    
      /**
       * Index 삭제
       *
       * @param indexName 삭제할 인덱스명
       * @return DeleteIndexResponse Index 삭제에 대한 응답을 담고 있는 객체
       */
      public DeleteIndexResponse deleteIndex(String indexName) {
    
        DeleteIndexResponse deleteIndexResponse = null;
    
        try {
          DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest.Builder()
              .index(indexName).build();
          deleteIndexResponse = openSearchClient.indices().delete(deleteIndexRequest);
        } catch (Exception e) {
          log.error("deleteIndex indexName : [{}]", indexName, e);
        }
    
        return deleteIndexResponse;
      }
    
      /**
       * Document 저장
       *
       * @param indexName      저장할 Document의 인덱스명
       * @param sampleDocument 저장할 Document 정보를 담고 있는 SampleDto.Document 객체
       * @return IndexResponse Document 저장에 대한 결과를 담고 있는 객체
       */
      public IndexResponse insertDocument(String indexName, SampleDto.Document sampleDocument) {
    
        IndexResponse indexResponse = null;
    
        try {
          IndexRequest<SampleDto.Document> indexRequest = new IndexRequest.Builder<SampleDto.Document>()
              .index(indexName)
              .id(sampleDocument.getId())
              .document(sampleDocument).build();
          indexResponse = openSearchClient.index(indexRequest);
        } catch (Exception e) {
          log.error("insertDocument indexName : [{}], sampleDocument : [{}]"
              , indexName, sampleDocument.toString(), e);
        }
    
        return indexResponse;
      }
    
      /**
       * Document 조회
       *
       * @param indexName 조회할 Document의 인덱스명
       * @return Document 조회에 대한 결과를 담고 있는 객체
       */
      public SearchResponse<SampleDto.Document> searchDocument(String indexName) {
        SearchResponse<SampleDto.Document> searchResponse = null;
        try {
          SearchRequest searchRequest = SearchRequest.of(r -> r
              .index(indexName)
          );
    
          searchResponse =
              openSearchClient.search(searchRequest, SampleDto.Document.class);
    
    
        } catch (Exception e) {
          log.error("searchDocument indexName : [{}]", indexName, e);
        }
        return searchResponse;
      }
    
      /**
       * Document 수정
       *
       * @param indexName      수정할 Document의 인덱스명
       * @param sampleDocument 수정할 Document 정보를 담고 있는 SampleDto.Document 객체
       * @return UpdateByQueryResponse Document 수정에 대한 결과를 담고 있는 객체
       */
      public UpdateByQueryResponse updateDocument(String indexName, SampleDto.Document sampleDocument) {
    
        UpdateByQueryResponse updateByQueryResponse = null;
    
        try {
          UpdateByQueryRequest updateByQueryRequest = UpdateByQueryRequest.of(r -> r
              .index(indexName)
              // Document의 수정 조건 id
              .query(q -> q
                  .terms(t -> t
                      .field("id")
                      .terms(v -> v
                          .value(List.of(FieldValue.of(sampleDocument.getId()))))
                  ))
              // Document에서 수정할 내용 firstName, lastName
              .script(s -> s
                  .inline(InlineScript.of(is -> is
                      .lang("painless")
                      .source(
                          "ctx._source.firstName = '" + sampleDocument.getFirstName() + "'; "
                              + "ctx._source.lastName = '" + sampleDocument.getLastName() + "';"))
                  )
              )
          );
    
          updateByQueryResponse = openSearchClient.updateByQuery(updateByQueryRequest);
    
        } catch (Exception e) {
          log.error("updateDocument indexName : [{}], sampleDocument : [{}]",
              indexName, sampleDocument.toString(), e);
        }
    
        return updateByQueryResponse;
      }
    
      /**
       * Document 삭제
       *
       * @param indexName      삭제할 Document가 있는 인덱스명
       * @param sampleDocument 삭제할 Document 정보를 담고 있는 SampleDto.Document 객체
       * @return DeleteResponse Document 삭제에 대한 결과를 담고 있는 객체
       */
      public DeleteResponse deleteDocument(String indexName, SampleDto.Document sampleDocument) {
    
        DeleteResponse deleteResponse = null;
    
        try {
          deleteResponse = openSearchClient
              .delete(d -> d.index(indexName).id(sampleDocument.getId()));
        } catch (Exception e) {
          log.error("deleteDocument indexName : [{}], sampleDocument : [{}]"
              , sampleDocument.toString(), e);
        }
    
        return deleteResponse;
      }
    }

     

    SampleDto.java

    package com.example.opensearch.sample.service.dto;
    
    import lombok.AllArgsConstructor;
    import lombok.Builder;
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    import lombok.ToString;
    
    public class SampleDto {
    
      @Getter
      @Builder
      @NoArgsConstructor
      @AllArgsConstructor
      @ToString
      public static class Document {
    
        private String id;
        private String firstName;
        private String lastName;
      }
    }


    SampleServiceTest.java

    package com.example.opensearch.sample.service;
    
    import static org.junit.jupiter.api.Assertions.assertAll;
    import static org.junit.jupiter.api.Assertions.assertEquals;
    import static org.junit.jupiter.api.Assertions.assertTrue;
    
    import com.example.opensearch.sample.service.dto.SampleDto;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang3.ObjectUtils;
    import org.junit.jupiter.api.BeforeAll;
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.MethodOrderer;
    import org.junit.jupiter.api.Nested;
    import org.junit.jupiter.api.Order;
    import org.junit.jupiter.api.Test;
    import org.junit.jupiter.api.TestMethodOrder;
    import org.opensearch.client.opensearch._types.Result;
    import org.opensearch.client.opensearch.core.DeleteResponse;
    import org.opensearch.client.opensearch.core.IndexResponse;
    import org.opensearch.client.opensearch.core.SearchResponse;
    import org.opensearch.client.opensearch.core.UpdateByQueryResponse;
    import org.opensearch.client.opensearch.indices.CreateIndexResponse;
    import org.opensearch.client.opensearch.indices.DeleteIndexResponse;
    import org.opensearch.client.transport.endpoints.BooleanResponse;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    @Slf4j
    @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
    @SpringBootTest
    class SampleServiceTest {
    
      @Autowired
      SampleService sampleService;
    
      @Autowired
      ObjectMapper objectMapper;
    
      @DisplayName("Index 샘플 테스트")
      @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
      @Nested
      class TestIndex {
    
        static String indexName;
    
        @BeforeAll
        static void setUp() {
          indexName = "sample-index";
        }
    
        @Order(1)
        @DisplayName("createIndex_인덱스 생성")
        @Test
        void testCreateIndex() {
    
          // Given & When
          CreateIndexResponse createIndexResponse = sampleService.createIndex(indexName);
          log.debug("createIndexResponse.index : [{}], createIndexResponse.acknowledged : [{}]"
              , createIndexResponse.index(), createIndexResponse.acknowledged());
    
          // Then
          assertAll(
              () -> assertTrue(ObjectUtils.isNotEmpty(createIndexResponse)),
              () -> assertTrue(ObjectUtils.isNotEmpty(createIndexResponse.index())),
              () -> assertTrue(createIndexResponse.acknowledged())
          );
        }
    
        @Order(2)
        @DisplayName("existIndex_인덱스 존재 여부 조회")
        @Test
        void testExistIndex() {
    
          // Given & When
          BooleanResponse booleanResponse = sampleService.existIndex(indexName);
          log.debug("booleanResponse : [{}]", booleanResponse);
    
          // Then
          assertAll(
              () -> assertTrue(ObjectUtils.isNotEmpty(booleanResponse)),
              () -> assertTrue(booleanResponse.value())
          );
        }
    
        @Order(3)
        @DisplayName("deleteIndex_인덱스 삭제")
        @Test
        void testDeleteIndex() {
    
          // Given & When
          DeleteIndexResponse deleteIndexResponse = sampleService.deleteIndex(indexName);
          log.debug("deleteIndexResponse.acknowledged : [{}]", deleteIndexResponse.acknowledged());
    
          // Then
          assertAll(
              () -> assertTrue(ObjectUtils.isNotEmpty(deleteIndexResponse)),
              () -> assertTrue(deleteIndexResponse.acknowledged())
          );
        }
      }
    
      @DisplayName("Index 및 Document 샘플 테스트")
      @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
      @Nested
      class TestIndexAndDocument {
    
        static String indexName;
        static String id;
        static String originalFirstName;
        static String originalLastName;
        static String updatedFirstName;
        static String updatedLastName;
    
        @BeforeAll
        static void setUpAll() {
          indexName = "sample-index";
          id = "01";
          originalFirstName = "Original FirstName";
          originalLastName = "Original LastName";
          updatedFirstName = "Updated FirstName";
          updatedLastName = "Updated LastName";
        }
    
        @Order(1)
        @DisplayName("createIndex_인덱스 생성")
        @Test
        void testCreateIndex() {
    
          // Given & When
          CreateIndexResponse createIndexResponse = sampleService.createIndex(indexName);
          log.debug("createIndexResponse.index : [{}], createIndexResponse.acknowledged : [{}]"
              , createIndexResponse.index(), createIndexResponse.acknowledged());
    
          // Then
          assertAll(
              () -> assertTrue(ObjectUtils.isNotEmpty(createIndexResponse)),
              () -> assertTrue(ObjectUtils.isNotEmpty(createIndexResponse.index())),
              () -> assertTrue(createIndexResponse.acknowledged())
          );
        }
    
        @Order(2)
        @DisplayName("insertDocument_Document 저장")
        @Test
        void testInsertDocument() throws Exception {
    
          // Given
          SampleDto.Document sampleDocument = SampleDto.Document.builder()
              .id(id)
              .firstName(originalFirstName)
              .lastName(originalFirstName)
              .build();
    
          // When
          IndexResponse indexResponse = sampleService.insertDocument(
              indexName, sampleDocument);
          log.debug("indexResponse.result : [{}]", indexResponse.result());
    
          Thread.sleep(1000);
    
          // Then
          assertAll(
              () -> assertTrue(ObjectUtils.isNotEmpty(indexResponse)),
              () -> assertEquals(Result.Created, indexResponse.result())
          );
        }
    
        @Order(3)
        @DisplayName("searchDocument_Document 조회")
        @Test
        void testSearchDocument() {
    
          // Given & When
          SearchResponse<SampleDto.Document> searchResponse = sampleService.searchDocument(indexName);
          log.debug("searchResponse.hits().hits().size() : [{}]", searchResponse.hits().hits().size());
    
          // Then
          assertAll(
              () -> assertTrue(ObjectUtils.isNotEmpty(searchResponse)),
              () -> assertTrue(ObjectUtils.isNotEmpty(searchResponse.hits().hits().size() > 0)),
              () -> assertEquals(id, searchResponse.hits().hits().get(0).id())
          );
        }
    
        @Order(4)
        @DisplayName("updateDocument_Document 수정")
        @Test
        void testUpdateDocument() {
    
          // Given
          SampleDto.Document sampleDocument = SampleDto.Document.builder()
              .id(id)
              .firstName(updatedFirstName)
              .lastName(updatedLastName)
              .build();
    
          // When
          UpdateByQueryResponse updateByQueryResponse = sampleService.updateDocument(
              indexName, sampleDocument);
          log.debug("updateByQueryResponse : [{}]", updateByQueryResponse);
    
          // Then
          assertAll(
              () -> assertTrue(ObjectUtils.isNotEmpty(updateByQueryResponse)),
              () -> assertEquals(1L, updateByQueryResponse.updated())
          );
        }
    
        @Order(5)
        @DisplayName("deleteDocument_Document 삭제")
        @Test
        void testDeleteDocument() {
    
          // Given
          SampleDto.Document sampleDocument = SampleDto.Document.builder()
              .id(id)
              .build();
    
          // When
          DeleteResponse deleteResponse = sampleService.deleteDocument(
              indexName, sampleDocument);
          log.debug("deleteResponse.result : [{}]", deleteResponse.result());
    
          // Then
          assertAll(
              () -> assertTrue(ObjectUtils.isNotEmpty(deleteResponse)),
              () -> assertEquals(Result.Deleted, deleteResponse.result())
          );
        }
    
        @Order(6)
        @DisplayName("deleteIndex_인덱스 삭제")
        @Test
        void testDeleteIndex() {
    
          // Given & When
          DeleteIndexResponse deleteIndexResponse = sampleService.deleteIndex(indexName);
          log.debug("deleteIndexResponse.acknowledged : [{}]", deleteIndexResponse.acknowledged());
    
          // Then
          assertAll(
              () -> assertTrue(ObjectUtils.isNotEmpty(deleteIndexResponse)),
              () -> assertTrue(deleteIndexResponse.acknowledged())
          );
        }
      }
    }

     

    2_4. 단위 테스트 결과
    TestIndex 결과


    TestIndexAndDocument 결과

     

     

    OpenSearch Java Client - https://opensearch.org/docs/latest/clients/java/

     

    소스 코드는 Github Repository - https://github.com/tychejin1218/springboot-opensearch프로젝트를 참조하세요.

     

    반응형

    댓글

Designed by Tistory.