Skip to content

Commit

Permalink
[KAN-148] 백엔드 > 검색 페이지네이션 성능을 높이기 위해 search after 기능 추가 (#86)
Browse files Browse the repository at this point in the history
sinkyoungdeok authored Jun 2, 2024
1 parent 3c49133 commit 1fe411b
Showing 5 changed files with 108 additions and 11 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
@@ -82,7 +82,7 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$coroutineVersion")

// Es
implementation("com.jillesvangurp:search-client:2.1.29")
implementation("com.jillesvangurp:search-client:2.2.0")

// Test
testImplementation("org.springframework.boot:spring-boot-starter-test")
Original file line number Diff line number Diff line change
@@ -39,7 +39,7 @@ class GetRestaurantService(
null
}

val restaurants = restaurantEsRepository.searchRestaurants(
val (restaurants, nextCursor) = restaurantEsRepository.searchRestaurants(
request,
pageable,
restaurantIds,
@@ -63,7 +63,8 @@ class GetRestaurantService(
sortedRestaurantProjections.map { it.toDto() },
pageable,
sortedRestaurantProjections.size.toLong()
)
),
nextCursor
)
}

Original file line number Diff line number Diff line change
@@ -34,20 +34,26 @@ data class GetRestaurantsRequest(
@ApiModelProperty(value = "경도(거리순 정렬 할 때 사용)", example = "126.123456", required = false)
val longitude: Double?,
@ApiModelProperty(value = "위도(거리순 정렬 할 때 사용)", example = "37.123456", required = false)
val latitude: Double?
val latitude: Double?,

@ApiModelProperty(value = "페이지 커서(리스트 검색에서만 사용)", example = "['1', '2']", required = false)
val cursor: List<Double>?
)

enum class Sort {
BASIC,
CLOSELY_DESC,
RATING_DESC,
REVIEW_COUNT_DESC,
LIKE_COUNT_DESC
LIKE_COUNT_DESC,
ID_ASC
}

data class GetRestaurantsResponse(
@Schema(description = "식당 리스트")
val restaurants: Page<RestaurantDto>
val restaurants: Page<RestaurantDto>,
@Schema(description = "다음 페이지")
val nextCursor: List<Double>?
)

data class GetRestaurantResponse(
Original file line number Diff line number Diff line change
@@ -17,6 +17,8 @@ import com.restaurant.be.restaurant.presentation.controller.dto.GetRestaurantsRe
import com.restaurant.be.restaurant.presentation.controller.dto.Sort
import com.restaurant.be.restaurant.repository.dto.RestaurantEsDocument
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonPrimitive
import org.springframework.data.domain.Pageable
import org.springframework.stereotype.Repository

@@ -32,7 +34,7 @@ class RestaurantEsRepository(
pageable: Pageable,
restaurantIds: List<Long>?,
like: Boolean?
): List<RestaurantEsDocument> {
): Pair<List<RestaurantEsDocument>, List<Double>?> {
val dsl = SearchDSL()
val termQueries: MutableList<ESQuery> = mutableListOf()

@@ -128,9 +130,12 @@ class RestaurantEsRepository(
}

val result = runBlocking {
client.search(
val res = client.search(
target = searchIndex,
block = {
if (request.cursor != null) {
searchAfter = request.cursor
}
query = bool {
filter(
termQueries
@@ -164,7 +169,7 @@ class RestaurantEsRepository(
}
sort {
when (request.customSort) {
Sort.BASIC -> null
Sort.BASIC -> add("_score", SortOrder.DESC)
Sort.CLOSELY_DESC -> add("_geo_distance", SortOrder.ASC) {
this["location"] = mapOf(
"lat" to request.latitude,
@@ -177,13 +182,24 @@ class RestaurantEsRepository(
Sort.RATING_DESC -> add("rating_avg", SortOrder.DESC)
Sort.REVIEW_COUNT_DESC -> add("review_count", SortOrder.DESC)
Sort.LIKE_COUNT_DESC -> add("like_count", SortOrder.DESC)
Sort.ID_ASC -> null
}

add("id", SortOrder.ASC)
}
}
},
size = pageable.pageSize,
from = pageable.offset.toInt()
).parseHits<RestaurantEsDocument>()
from = if (request.cursor != null) null else pageable.offset.toInt(),
trackTotalHits = true
)

val nextCursor: List<Double>? = res.hits?.hits?.lastOrNull()?.sort?.mapNotNull {
jsonElement ->
jsonElement.jsonPrimitive.contentOrNull?.toDouble()
}

Pair(res.parseHits<RestaurantEsDocument>(), nextCursor)
}

return result
Original file line number Diff line number Diff line change
@@ -2059,6 +2059,80 @@ class GetRestaurantControllerTest(
}
}

describe("#get restaurants search after test") {
it("when search after should return next page") {
// given
val restaurantEntity1 = RestaurantUtil.generateRestaurantEntity(
name = "목구멍 율전점1"
)
restaurantRepository.save(restaurantEntity1)
val restaurantDocument1 = RestaurantUtil.generateRestaurantDocument(
id = restaurantEntity1.id,
name = "목구멍 율전점1"
)
elasticsearchTemplate.save(restaurantDocument1)

val restaurantEntity2 = RestaurantUtil.generateRestaurantEntity(
name = "목구멍 율전점2"
)
restaurantRepository.save(restaurantEntity2)
val restaurantDocument2 = RestaurantUtil.generateRestaurantDocument(
id = restaurantEntity2.id,
name = "목구멍 율전점2"
)
elasticsearchTemplate.save(restaurantDocument2)
elasticsearchTemplate.indexOps(RestaurantDocument::class.java).refresh()

// when
val result = mockMvc.perform(
get(restaurantUrl)
.param("size", "1")
.param("customSort", "ID_ASC")
)
.also {
println(it.andReturn().response.contentAsString)
}
.andExpect(status().isOk)
.andExpect(jsonPath("$.result").value("SUCCESS"))
.andExpect(jsonPath("$.data.restaurants.content[0].name").value("목구멍 율전점1"))
.andReturn()

val responseContent = result.response.getContentAsString(Charset.forName("UTF-8"))
val responseType =
object : TypeReference<CommonResponse<GetRestaurantsResponse>>() {}
val firstPageResult: CommonResponse<GetRestaurantsResponse> =
objectMapper.readValue(
responseContent,
responseType
)

// when
val result2 = mockMvc.perform(
get(restaurantUrl)
.param("size", "1")
.param("customSort", "ID_ASC")
.param("cursor", firstPageResult.data!!.nextCursor?.joinToString(","))
)
.also {
println(it.andReturn().response.contentAsString)
}
.andExpect(status().isOk)
.andExpect(jsonPath("$.result").value("SUCCESS"))
.andExpect(jsonPath("$.data.restaurants.content[0].name").value("목구멍 율전점2"))
.andReturn()

val responseContent2 = result2.response.getContentAsString(Charset.forName("UTF-8"))
val secondPageResult: CommonResponse<GetRestaurantsResponse> =
objectMapper.readValue(
responseContent2,
responseType
)

// then
secondPageResult.data!!.restaurants.content[0].name shouldBe "목구멍 율전점2"
}
}

describe("#get restaurant test") {
it("when restaurant exist should return restaurant") {
// given

0 comments on commit 1fe411b

Please sign in to comment.