Creating Spring Data Elasticsearch with Kotlin
Overview
This article creates an Elasticsearch client with Kotlin and Spring Data Elasticsearch.
This project requires Elasticsearch to be installed before it can run. If it is not installed, install it first by following Install Elastic Search.
Create the Project
Create a Spring Boot starter project with the following curl command.
curl https://start.spring.io/starter.tgz \
-d bootVersion=2.5.5 \
-d dependencies=web,data-elasticsearch \
-d baseDir=spring-data-elasticsearch \
-d groupId=com.devkuma \
-d artifactId=spring-data-elasticsearch \
-d packageName=com.devkuma.elasticsearch \
-d applicationName=ElasticsearchApplication \
-d packaging=jar \
-d language=kotlin \
-d javaVersion=11 \
-d type=gradle-project | tar -xzvf -
Check the Generated Project
Project File Structure
If the project was generated correctly, the file structure is as follows.
.
├── HELP.md
├── build.gradle.kts
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
└── src
├── main
│ ├── kotlin
│ │ └── com
│ │ └── devkuma
│ │ └── elasticsearch
│ │ └── ElasticsearchApplication.kt
│ └── resources
│ └── application.properties
└── test
└── kotlin
└── com
└── devkuma
└── elasticsearch
└── ElasticsearchApplicationTests.kt
Build Script
/build.gradle.kts
// ... omitted ...
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-elasticsearch")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
// ... omitted ...
You can see that the Spring Data Elasticsearch library (spring-boot-starter-data-elasticsearch) is included in the dependencies.
Elasticsearch Configuration
Add Application-Related Configuration
/src/main/kotlin/com/devkuma/elasticsearch/classElasticsearchApplication.kt
package com.devkuma.elasticsearch
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.boot.runApplication
@SpringBootApplication
@ConfigurationPropertiesScan
class ElasticsearchApplication
fun main(args: Array<String>) {
runApplication<ElasticsearchApplication>(*args)
}
Add @ConfigurationPropertiesScan to the class so configuration properties can be loaded from a file.
Add Application Configuration
Add Elasticsearch-related settings to the application configuration file (application.yml).
When the initial project is created, there should be an application.properties file. You can simply change its extension to .yml.
/src/main/resources/application.yml
elasticsearch:
host: localhost
port: 9200
logging:
level:
org.springframework: INFO
org.devkuma: DEBUG
As the setting names make clear, elasticsearch contains Elasticsearch settings, and logging contains log-related settings.
Create the Properties Configuration Object
/src/main/kotlin/com/devkuma/elasticsearch/config/ElasticsearchProperties.kt
package com.devkuma.elasticsearch.config
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.ConstructorBinding
@ConstructorBinding
@ConfigurationProperties(prefix = "elasticsearch")
class ElasticsearchProperties(
private val host: String,
private val port: Integer
) {
fun getHostAndPort(): String {
return "$host:$port"
}
}
The getHostAndPort function was added so the host and port can be retrieved together.
Create the Elasticsearch Configuration Source File
/src/main/kotlin/com/devkuma/elasticsearch/config/ElasticsearchConfig.kt
package com.devkuma.elasticsearch.config
import org.elasticsearch.client.RestHighLevelClient
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.elasticsearch.client.ClientConfiguration
import org.springframework.data.elasticsearch.client.RestClients
import org.springframework.data.elasticsearch.config.AbstractElasticsearchConfiguration
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories
@Configuration
@EnableElasticsearchRepositories
class ElasticsearchConfig(
var elasticsearchProperties: ElasticsearchProperties
) : AbstractElasticsearchConfiguration() {
override fun elasticsearchClient(): RestHighLevelClient {
val clientConfiguration: ClientConfiguration = ClientConfiguration.builder()
.connectedTo(elasticsearchProperties.getHostAndPort())
.build();
return RestClients.create(clientConfiguration).rest();
}
}
Looking at the source, it extends AbstractElasticsearchConfiguration and receives the ElasticsearchProperties configured above.
Create the Elasticsearch Repository
Now create a repository that connects directly to Elasticsearch.
Create the Document Object
First, create the document object that receives data.
/src/main/kotlin/com/devkuma/elasticsearch/document/Phone.kt
package com.devkuma.elasticsearch.document
import org.springframework.data.annotation.Id
import org.springframework.data.elasticsearch.annotations.Document
@Document(indexName = "phone")
class Phone(
@Id
var id: Int,
val number: String,
val author: String
)
This example creates phone number information. The index name is set with @Document.
Create the Repository Object
Create the repository object.
/src/main/kotlin/com/devkuma/elasticsearch/repository/ElasticsearchRepository.kt
package com.devkuma.elasticsearch.repository
import com.devkuma.elasticsearch.document.Phone
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository
interface ElasticsearchRepository: ElasticsearchRepository<Phone, Int>
Looking at the source file, you can see that it extends ElasticsearchRepository.
Create Test Code for the Repository
Write code that can test the repository object.
/src/test/kotlin/com/devkuma/elasticsearch/repository/ElasticsearchRepositoryTests.kt
package com.devkuma.elasticsearch.repository
import com.devkuma.elasticsearch.document.Phone
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
class ElasticsearchRepositoryTests() {
@Autowired
private lateinit var elasticsearchRepository: ElasticsearchRepository
@Test
@DisplayName("저장")
fun save() {
val phone: Phone = Phone(1, "010-0000-0000", "devkuma")
elasticsearchRepository.save(phone)
}
@Test
@DisplayName("조회")
fun findById() {
// 저장
val savedPhone: Phone = Phone(1, "010-0000-0000", "devkuma")
elasticsearchRepository.save(savedPhone)
// 조회
val searchedPhone = elasticsearchRepository.findById(1).orElseGet(null)
// 테스트
assertNotNull(searchedPhone)
assertEquals(savedPhone.id, searchedPhone.id)
assertEquals(savedPhone.number, searchedPhone.number)
assertEquals(savedPhone.author, searchedPhone.author)
}
}
Run it and check that all tests pass without failure.
Create the Elasticsearch Service
This time, create a service so data can be read from the repository.
Create the Service Object
Create the service object.
/src/main/kotlin/com/devkuma/elasticsearch/service/ElasticsearchService.kt
package com.devkuma.elasticsearch.service
import com.devkuma.elasticsearch.document.Phone
import com.devkuma.elasticsearch.repository.ElasticsearchRepository
import org.springframework.stereotype.Service
@Service
class ElasticsearchService(
private val elasticsearchRepository: ElasticsearchRepository
) {
fun save(phone: Phone): Phone {
return elasticsearchRepository.save(phone)
}
fun findById(id: Int): Phone {
return elasticsearchRepository.findById(id).orElseThrow()
}
fun delete(id: Int) {
val phone = elasticsearchRepository.findById(id).orElseThrow()
elasticsearchRepository.delete(phone)
}
}
Create Test Code for the Service
Write code that can test the service object.
/src/test/kotlin/com/devkuma/elasticsearch/service/ElasticsearchServiceTests.kt
package com.devkuma.elasticsearch.service
import com.devkuma.elasticsearch.document.Phone
import com.devkuma.elasticsearch.repository.ElasticsearchRepository
import org.junit.jupiter.api.*
import org.mockito.ArgumentMatchers.any
import org.mockito.BDDMockito.given
import org.mockito.InjectMocks
import org.mockito.Mock
import org.springframework.boot.test.context.SpringBootTest
import java.util.*
@SpringBootTest
class ElasticsearchServiceTests {
@Mock
private lateinit var elasticsearchRepository: ElasticsearchRepository
@InjectMocks
private lateinit var elasticsearchService: ElasticsearchService
@BeforeEach
fun setUp() {
elasticsearchService = ElasticsearchService(elasticsearchRepository)
}
@Test
fun save() {
// given
val phone = Phone(1, "010-0000-0000", "devkuma")
given(elasticsearchRepository.save(any())).willReturn(phone)
// when
val savedPhone = elasticsearchService.save(phone)
// then
Assertions.assertAll(
{ Assertions.assertNotNull(savedPhone) },
{ Assertions.assertEquals(savedPhone.id, phone.id) },
{ Assertions.assertEquals(savedPhone.number, phone.number) },
{ Assertions.assertEquals(savedPhone.author, phone.author) }
)
}
@Test
fun findById() {
// given
val phone = Phone(1, "010-0000-0000", "devkuma")
given(elasticsearchRepository.findById(any())).willReturn(Optional.of(phone))
// 조회
val searchedPhone = elasticsearchService.findById(1)
// then
Assertions.assertAll(
{ Assertions.assertNotNull(searchedPhone) },
{ Assertions.assertEquals(phone.id, searchedPhone.id) },
{ Assertions.assertEquals(phone.number, searchedPhone.number) },
{ Assertions.assertEquals(phone.author, searchedPhone.author) }
)
}
@Test
fun delete() {
// given
val phone = Phone(1, "010-0000-0000", "devkuma")
given(elasticsearchRepository.findById(any())).willReturn(Optional.of(phone))
// given
assertDoesNotThrow { elasticsearchService.delete(1) }
}
}
Run it and check that all tests pass without failure.
Create the Elasticsearch Controller
Finally, create a controller that calls the service.
Create the Controller File
/src/main/kotlin/com/devkuma/elasticsearch/controller/ElasticsearchController.kt
package com.devkuma.elasticsearch.controller
import com.devkuma.elasticsearch.document.Phone
import com.devkuma.elasticsearch.service.ElasticsearchService
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import java.net.URI
@RestController
@RequestMapping("/phones")
class ElasticsearchController(
private val elasticsearchService: ElasticsearchService
) {
@GetMapping("/{id}")
fun findById(@PathVariable id: Int): ResponseEntity<Phone> {
return ResponseEntity.ok().body(elasticsearchService.findById(id))
}
@PostMapping
fun add(@RequestBody phone: Phone): ResponseEntity<Phone> {
elasticsearchService.add(phone);
return ResponseEntity.created(URI.create("/phones/${phone.id}")).build()
}
@DeleteMapping("/{id}")
fun delete(@PathVariable id: Int): ResponseEntity<Object> {
elasticsearchService.delete(id)
return ResponseEntity.noContent().build()
}
}
Create Test Code for the Controller
/src/test/kotlin/com/devkuma/elasticsearch/controller/ElasticsearchControllerTests.kt
package com.devkuma.elasticsearch.controller
import com.devkuma.elasticsearch.document.Phone
import com.devkuma.elasticsearch.service.ElasticsearchService
import com.fasterxml.jackson.databind.ObjectMapper
import org.junit.jupiter.api.Test
import org.mockito.ArgumentMatchers.*
import org.mockito.BDDMockito.given
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.http.MediaType.APPLICATION_JSON
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*
import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
@WebMvcTest(ElasticsearchController::class)
class ElasticsearchControllerTests {
@Autowired
private lateinit var mockMvc: MockMvc
@MockBean
private lateinit var elasticsearchService: ElasticsearchService
val phone = Phone(1, "010-0000-0000", "devkuma")
@Test
fun findById() {
// given
given(elasticsearchService.findById(anyInt())).willReturn(phone)
// when
var resultActions = mockMvc.perform(get("/phones/{id}", phone.id))
.andDo(print())
// then
resultActions
.andExpect(status().is2xxSuccessful)
.andExpect(jsonPath("$.id").value(phone.id))
.andExpect(jsonPath("$.number").value(phone.number))
.andExpect(jsonPath("$.author").value(phone.author))
.andDo(print())
}
@Test
fun add() {
// given ma")
given(elasticsearchService.add(phone)).willReturn(phone)
// when
var resultActions = mockMvc.perform(post("/phones")
.contentType(APPLICATION_JSON)
.content(ObjectMapper().writeValueAsString(phone)))
.andDo(print())
// then
resultActions
.andExpect(status().is2xxSuccessful)
.andExpect(redirectedUrlTemplate("/phones/{id}", phone.id))
.andDo(print())
}
@Test
fun delete() {
// when
var resultActions = mockMvc.perform(delete("/phones/{id}", phone.id))
.andDo(print())
// then
resultActions
.andExpect(status().is2xxSuccessful)
.andDo(print())
}
}
Run this as well and check that all tests pass without failure.
Test It with curl Commands
After creating the project, test whether it actually works with curl commands.
Add:
% curl -X POST http://localhost:8080/phones \
-H "Content-Type: application/json" \
-d '{"id":1,"number":"010-0000-0000","author":"devkuma"}'
Delete:
% curl -XGET http://localhost:8080/phones/1
{"id":1,"number":"010-0000-0000","author":"devkuma"}%
Query:
% curl -XDELETE http://localhost:8080/phones/1
Etc.
- This post was written with reference to the following sites.
- All source code above is available on GitHub.