개발/그 외

[OpenSearch] Spring - Kotlin 기반의 OpenSearch Client 구현

지잉지잉 2023. 11. 3. 23:13

1. 개요

  • ElasticSearch 에 비해 AWS OpenSearch는 JAVA API Client 문서가 불친절한 느낌이다.
  • 이것 저것 해보면서 겪었던 시행 착오들을 기록하고,
  • SpringBoot - Kotlin 기반으로 AWS OpenSearch Instance에 접근하고 색인/검색 요청을 하는 JAVA API Client 설정 및 구현 방법을 정리한다.

2. Dependency

dependencies {
	...
	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:2.0.1")
    	...
 }
  • jakarta.json-api 의존성을 추가해주지 않으면,
    • java.lang.NoClassDefFoundError: jakarta/json/JsonException 에러가 난다.
    • = ES에서 가져온 JSON 기반의 객체를 파싱하는데 실패하는 것 같다.

3. OpenSearchClient

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.apache.http.HttpHost
import org.apache.http.auth.AuthScope
import org.apache.http.auth.UsernamePasswordCredentials
import org.apache.http.impl.client.BasicCredentialsProvider
import org.opensearch.client.RestClient
import org.opensearch.client.json.jackson.JacksonJsonpMapper
import org.opensearch.client.opensearch.OpenSearchClient
import org.opensearch.client.transport.rest_client.RestClientTransport
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class OpenSearchConfiguration {

    @Bean
    fun openSearchClient(): OpenSearchClient {
        val host = HttpHost("127.0.0.1", 9200, "https") // OpenSearch Domain
        val credential = BasicCredentialsProvider().apply {
            this.setCredentials(
                AuthScope(host),
                UsernamePasswordCredentials("admin", "admin") // OpenSearch Username & Password
            )
        }

        val restClient = RestClient.builder(host).setHttpClientConfigCallback {
            it.setDefaultCredentialsProvider(credential)
        }.build()

        return OpenSearchClient(RestClientTransport(restClient, JacksonJsonpMapper(jacksonObjectMapper())))
    }
}
  • Webflux를 사용한다면 OpenSearchAsyncClient로 생성하여 비동기로 OpenSearch 호출이 가능하다.
  • JacksonJsonpMapper도 jacksonObjectMapper로 만들어주자. (안하면 parsing Exception 발생)

4. Service Example

import org.opensearch.client.opensearch.OpenSearchClient
import org.opensearch.client.opensearch.core.BulkRequest
import org.opensearch.client.opensearch.core.SearchRequest
import org.opensearch.client.opensearch.core.bulk.CreateOperation
import org.opensearch.client.opensearch.indices.CreateIndexRequest
import org.springframework.stereotype.Service

@Service
class OpenSearchService(
    private val openSearchClient: OpenSearchClient
) {

    /**
     * 인덱스 생성
      */
    fun createIndex() {
        try {
            val indexName = "test-index"

            val request = CreateIndexRequest.of {
                it.index(indexName)
            }

            openSearchClient.indices().create(request)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    /**
     * bulk index(색인)
     */
    fun bulk() {
        val indexName = "test-index"
        val dummyData = listOf(
            TestIndex(
                id = "test1",
                name = "test1",
                age = 20
            ),
            TestIndex(
                id = "test2",
                name = "test2",
                age = 30
            )
        )
        try {
            val bulkRequest = BulkRequest.Builder()

            dummyData.forEach {  testIndex ->
                bulkRequest.operations { operation ->
                    operation.create { co: CreateOperation.Builder<TestIndex> ->
                        co.index(indexName).id(testIndex.id).document(testIndex)
                    }
                }
            }

            openSearchClient.bulk(bulkRequest.build())
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    /**
     * 검색
     */
    fun search(keyword: String): List<TestIndex> {
        val indexName = "test-index"
        return try {
            val request = SearchRequest.of { searchRequest ->
                searchRequest.index(indexName)
                searchRequest.query { query ->
                    query.match { matchQuery ->
                        matchQuery.field("name")
                        matchQuery.query {
                            it.stringValue(keyword)
                        }
                    }
                }
            }

            openSearchClient.search(request, TestIndex::class.java).hits().hits().mapNotNull {
                it.source()
            }
        } catch (e: Exception) {
            e.printStackTrace()
            emptyList()
        }
    }

    data class TestIndex(
        val id: String,
        val name: String,
        val age: Int,
    )
}
  • 인덱스 생성, 색인, 검색 예제
  • 간단하게 request 객체를 만들어 요청할 수 있다.