[백업][가리사니] 스프링 데이터로 엘라스틱 서치 연동
elasticsearch, kotlin, spring

이 문서는 가리사니 개발자 포럼에 올렸던 글의 백업 파일입니다. 오래된 문서가 많아 현재 상황과 맞지 않을 수 있습니다.

서론

스프링데이터의 spring-boot-starter-data-elasticsearch 를 통해서 연동하는 방식이다. 필자는 아래 단점 때문에 좋아하지 않는 방식이다.

장점

  • 간단하다.
  • 정말 간단하다..

단점

  • 버전에 민감하며, 버전이 올라갈 때 호환되지 않는 것들이 많다.
  • 루씬계 복잡한 쿼리를 ORM에 끼워 맞추다 보니 셀릭트 작성하다? 왜이러지 이런느낌이 있다.
  • 데이터 타입등이 자동화 된다.

구현

build.gradle.kts

implementation("org.springframework.boot:spring-boot-starter-data-elasticsearch:2.3.12.RELEASE")

AnimeDocument.kt

@Document(indexName = "anissia_anime")
data class AnimeDocument (
    @Id
    @Field(store = true)
    var animeNo: Long = 0,

    @Field(index = true)
    var subject: String = "",

    @Field(index = true)
    var genres: List<String> = listOf(),

    @Field(index = true)
    var status: String = "",

    @Field(index = true)
    var translators: List<String> = listOf(),

    @Field(index = true)
    var endDate: Long = 0,
)

AnimeDocumentRepository.kt

interface AnimeDocumentRepository : ElasticsearchRepository<AnimeDocument, Long>, AnimeDocumentRepositoryCustom

interface AnimeDocumentRepositoryCustom {
    fun search(cmd: SearchAnimeDocumentCommand): Page<Long>
}

class AnimeDocumentRepositoryCustomImpl(
    private val operations: ElasticsearchOperations
): AnimeDocumentRepositoryCustom {

    var log = As.logger<AnimeDocumentRepositoryCustomImpl>()
    override fun search(cmd: SearchAnimeDocumentCommand): Page<Long> {
        val keywords = cmd.keywords
        val genres = cmd.genres
        val translators = cmd.translators
        val end = cmd.end
        val pageable = cmd.pageable
        val query = NativeQueryBuilder().withPageable(pageable)

        if (keywords.isNotEmpty()) {
            query.withQuery { q ->
                q.wildcard { it.field("subject").wildcard(keywords.joinToString("*", "*", "*")) }
            }
        }

        if (genres.isNotEmpty() || translators.isNotEmpty() || end) {
            query.withFilter { filter ->
                filter.bool { bq ->
                    bq.minimumShouldMatch("100%")
                    if (genres.isNotEmpty()) {
                        bq.filter { fs -> fs.match { it.field("genres").query(genres.joinToString(" ")).minimumShouldMatch("100%") } }
                    }
                    if (translators.isNotEmpty()) {
                        bq.filter { fs -> fs.match { it.field("translators").query(translators.joinToString(" ")) } }
                    }
                    if (end) {
                        bq.filter { fs -> fs.match { it.field("status").query("END") } }
                    }
                    bq
                }
            }
            if (end) {
                query.withSort { f -> f.field { fn -> fn.field("endDate").order(SortOrder.Desc) } }
            }
        }

        return SearchHitSupport.searchPageFor(operations.search(query.build(), AnimeDocument::class.java), pageable).map { it.content.animeNo }
    }
}