-
[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/tags2. 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.javapackage 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.javapackage 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프로젝트를 참조하세요.
반응형'Spring Boot > 기타' 카테고리의 다른 글
[Spring Boot] RestClient를 활용한 HTTP 요청 (0) 2024.11.10 [Spring Boot] RestTemplate를 활용한 HTTP 요청 (0) 2024.11.10 [Spring Boot] Spring Cloud Config 연동 (0) 2024.03.07 [Spring Boot] Spring Boot 2.x에서 Spring Boot 3.x으로 버전 변경 (1) 2023.09.06 [Spring Boot] RedisJSON 연동 (0) 2023.07.17