From 5d9843c24eba140eed430b5bbf61856e4c841f9f Mon Sep 17 00:00:00 2001 From: hseoky desktop Date: Sun, 1 Dec 2024 15:02:03 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[#527]=20fix:=20=EC=9D=B4=EC=83=89=EC=B2=B4?= =?UTF-8?q?=ED=97=98=20=EA=B2=80=EC=83=89=20=ED=83=80=EC=9E=85=20=EB=B3=84?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ExperienceRepositoryCustom.java | 8 +-- .../repository/ExperienceRepositoryImpl.java | 19 ++++-- .../search/controller/SearchController.java | 6 +- .../domain/search/dto/SearchResponse.java | 7 ++- .../domain/search/service/SearchService.java | 26 ++++---- .../repository/ExperienceRepositoryTest.java | 59 ++++++++++++++++--- 6 files changed, 94 insertions(+), 31 deletions(-) diff --git a/src/main/java/com/jeju/nanaland/domain/experience/repository/ExperienceRepositoryCustom.java b/src/main/java/com/jeju/nanaland/domain/experience/repository/ExperienceRepositoryCustom.java index 15da749d..5ce4700b 100644 --- a/src/main/java/com/jeju/nanaland/domain/experience/repository/ExperienceRepositoryCustom.java +++ b/src/main/java/com/jeju/nanaland/domain/experience/repository/ExperienceRepositoryCustom.java @@ -42,9 +42,9 @@ PopularPostPreviewDto findRandomPopularPostPreviewDtoByLanguage(Language languag PopularPostPreviewDto findPostPreviewDtoByLanguageAndId(Language language, Long postId); - Page findSearchDtoByKeywordsUnion(List keywords, Language language, - Pageable pageable); + Page findSearchDtoByKeywordsUnion(ExperienceType experienceType, + List keywords, Language language, Pageable pageable); - Page findSearchDtoByKeywordsIntersect(List keywords, - Language language, Pageable pageable); + Page findSearchDtoByKeywordsIntersect(ExperienceType experienceType, + List keywords, Language language, Pageable pageable); } diff --git a/src/main/java/com/jeju/nanaland/domain/experience/repository/ExperienceRepositoryImpl.java b/src/main/java/com/jeju/nanaland/domain/experience/repository/ExperienceRepositoryImpl.java index a2e13746..3b9aee08 100644 --- a/src/main/java/com/jeju/nanaland/domain/experience/repository/ExperienceRepositoryImpl.java +++ b/src/main/java/com/jeju/nanaland/domain/experience/repository/ExperienceRepositoryImpl.java @@ -112,8 +112,8 @@ public ExperienceCompositeDto findCompositeDtoByIdWithPessimisticLock(Long id, } @Override - public Page findSearchDtoByKeywordsUnion(List keywords, - Language language, Pageable pageable) { + public Page findSearchDtoByKeywordsUnion(ExperienceType experienceType, + List keywords, Language language, Pageable pageable) { // experience_id를 가진 게시물의 해시태그가 검색어 키워드 중 몇개를 포함하는지 계산 List keywordMatchQuery = queryFactory @@ -123,7 +123,9 @@ public Page findSearchDtoByKeywordsUnion(List keywo .on(hashtag.post.id.eq(experience.id) .and(hashtag.language.eq(language))) .innerJoin(hashtag.keyword, QKeyword.keyword) - .where(QKeyword.keyword.content.toLowerCase().trim().in(keywords)) + .where( + experience.experienceType.eq(experienceType), + QKeyword.keyword.content.toLowerCase().trim().in(keywords)) .groupBy(experience.id) .fetch(); @@ -146,6 +148,7 @@ public Page findSearchDtoByKeywordsUnion(List keywo .leftJoin(experience.firstImageFile, imageFile) .leftJoin(experience.experienceTrans, experienceTrans) .on(experienceTrans.language.eq(language)) + .where(experience.experienceType.eq(experienceType)) .fetch(); // 해시태그 값을 matchedCount에 더해줌 @@ -176,8 +179,9 @@ public Page findSearchDtoByKeywordsUnion(List keywo } @Override - public Page findSearchDtoByKeywordsIntersect(List keywords, - Language language, Pageable pageable) { + public Page findSearchDtoByKeywordsIntersect( + ExperienceType experienceType, List keywords, Language language, + Pageable pageable) { // experience_id를 가진 게시물의 해시태그가 검색어 키워드 중 몇개를 포함하는지 계산 List keywordMatchQuery = queryFactory @@ -187,7 +191,9 @@ public Page findSearchDtoByKeywordsIntersect(List k .on(hashtag.post.id.eq(experience.id) .and(hashtag.language.eq(language))) .innerJoin(hashtag.keyword, QKeyword.keyword) - .where(QKeyword.keyword.content.toLowerCase().trim().in(keywords)) + .where( + experience.experienceType.eq(experienceType), + QKeyword.keyword.content.toLowerCase().trim().in(keywords)) .groupBy(experience.id) .fetch(); @@ -210,6 +216,7 @@ public Page findSearchDtoByKeywordsIntersect(List k .leftJoin(experience.firstImageFile, imageFile) .leftJoin(experience.experienceTrans, experienceTrans) .on(experienceTrans.language.eq(language)) + .where(experience.experienceType.eq(experienceType)) .fetch(); // 해시태그 값을 matchedCount에 더해줌 diff --git a/src/main/java/com/jeju/nanaland/domain/search/controller/SearchController.java b/src/main/java/com/jeju/nanaland/domain/search/controller/SearchController.java index a8e94545..0dfc5861 100644 --- a/src/main/java/com/jeju/nanaland/domain/search/controller/SearchController.java +++ b/src/main/java/com/jeju/nanaland/domain/search/controller/SearchController.java @@ -2,6 +2,7 @@ import static com.jeju.nanaland.global.exception.SuccessCode.SEARCH_SUCCESS; +import com.jeju.nanaland.domain.experience.entity.enums.ExperienceType; import com.jeju.nanaland.domain.member.dto.MemberResponse.MemberInfoDto; import com.jeju.nanaland.domain.search.dto.SearchResponse; import com.jeju.nanaland.domain.search.dto.SearchResponse.AllCategoryDto; @@ -95,12 +96,13 @@ public BaseResponse searchFestival( @GetMapping("/experience") public BaseResponse searchExperience( @AuthMember MemberInfoDto memberInfoDto, + @RequestParam ExperienceType experienceType, @NotNull String keyword, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "12") int size) { - ResultDto result = searchService.searchExperience(memberInfoDto, keyword, page, - size); + ResultDto result = searchService.searchExperience(memberInfoDto, experienceType, keyword, + page, size); return BaseResponse.success(SEARCH_SUCCESS, result); } diff --git a/src/main/java/com/jeju/nanaland/domain/search/dto/SearchResponse.java b/src/main/java/com/jeju/nanaland/domain/search/dto/SearchResponse.java index ed60ef83..038addc7 100644 --- a/src/main/java/com/jeju/nanaland/domain/search/dto/SearchResponse.java +++ b/src/main/java/com/jeju/nanaland/domain/search/dto/SearchResponse.java @@ -19,8 +19,11 @@ public static class AllCategoryDto { @Schema(description = "7대자연 조회 결과") private ResultDto nature; - @Schema(description = "이색체험 조회 결과") - private ResultDto experience; + @Schema(description = "액티비티 조회 결과") + private ResultDto activity; + + @Schema(description = "문화예술 조회 결과") + private ResultDto cultureAndArts; @Schema(description = "전통시장 조회 결과") private ResultDto market; diff --git a/src/main/java/com/jeju/nanaland/domain/search/service/SearchService.java b/src/main/java/com/jeju/nanaland/domain/search/service/SearchService.java index 826fcf7e..799b5153 100644 --- a/src/main/java/com/jeju/nanaland/domain/search/service/SearchService.java +++ b/src/main/java/com/jeju/nanaland/domain/search/service/SearchService.java @@ -11,6 +11,7 @@ import com.jeju.nanaland.domain.common.data.Language; import com.jeju.nanaland.domain.common.dto.CompositeDto; import com.jeju.nanaland.domain.experience.dto.ExperienceSearchDto; +import com.jeju.nanaland.domain.experience.entity.enums.ExperienceType; import com.jeju.nanaland.domain.experience.repository.ExperienceRepository; import com.jeju.nanaland.domain.favorite.service.MemberFavoriteService; import com.jeju.nanaland.domain.festival.dto.FestivalSearchDto; @@ -81,8 +82,11 @@ public SearchResponse.AllCategoryDto searchAll(MemberInfoDto memberInfoDto, Stri () -> searchFestival(memberInfoDto, keyword, page, size)); CompletableFuture marketFuture = CompletableFuture.supplyAsync( () -> searchMarket(memberInfoDto, keyword, page, size)); - CompletableFuture experienceFuture = CompletableFuture.supplyAsync( - () -> searchExperience(memberInfoDto, keyword, page, size)); + CompletableFuture activityFuture = CompletableFuture.supplyAsync( + () -> searchExperience(memberInfoDto, ExperienceType.ACTIVITY, keyword, page, size)); + CompletableFuture cultureAndArtsFuture = CompletableFuture.supplyAsync( + () -> searchExperience(memberInfoDto, ExperienceType.CULTURE_AND_ARTS, keyword, page, + size)); CompletableFuture restaurantFuture = CompletableFuture.supplyAsync( () -> searchRestaurant(memberInfoDto, keyword, page, size)); CompletableFuture nanaFuture = CompletableFuture.supplyAsync( @@ -93,7 +97,8 @@ public SearchResponse.AllCategoryDto searchAll(MemberInfoDto memberInfoDto, Stri natureFuture, festivalFuture, marketFuture, - experienceFuture, + activityFuture, + cultureAndArtsFuture, restaurantFuture, nanaFuture ).join(); @@ -103,7 +108,8 @@ public SearchResponse.AllCategoryDto searchAll(MemberInfoDto memberInfoDto, Stri .nature(natureFuture.join()) .festival(festivalFuture.join()) .market(marketFuture.join()) - .experience(experienceFuture.join()) + .activity(activityFuture.join()) + .cultureAndArts(cultureAndArtsFuture.join()) .restaurant(restaurantFuture.join()) .nana(nanaFuture.join()) .build(); @@ -223,8 +229,8 @@ public SearchResponse.ResultDto searchFestival(MemberInfoDto memberInfoDto, Stri * @param size 페이지 크기 * @return 이색체험 검색 결과 */ - public SearchResponse.ResultDto searchExperience(MemberInfoDto memberInfoDto, String keyword, - int page, int size) { + public SearchResponse.ResultDto searchExperience(MemberInfoDto memberInfoDto, + ExperienceType experienceType, String keyword, int page, int size) { Language language = memberInfoDto.getLanguage(); Member member = memberInfoDto.getMember(); @@ -236,13 +242,13 @@ public SearchResponse.ResultDto searchExperience(MemberInfoDto memberInfoDto, St Page resultPage; // 공백으로 구분한 키워드가 4개 이하라면 Union 검색 if (normalizedKeywords.size() <= 4) { - resultPage = experienceRepository.findSearchDtoByKeywordsUnion(normalizedKeywords, - language, pageable); + resultPage = experienceRepository.findSearchDtoByKeywordsUnion(experienceType, + normalizedKeywords, language, pageable); } // 4개보다 많다면 Intersect 검색 else { - resultPage = experienceRepository.findSearchDtoByKeywordsIntersect(normalizedKeywords, - language, pageable); + resultPage = experienceRepository.findSearchDtoByKeywordsIntersect(experienceType, + normalizedKeywords, language, pageable); } List favoriteIds = memberFavoriteService.getFavoritePostIdsWithMember(member); diff --git a/src/test/java/com/jeju/nanaland/domain/experience/repository/ExperienceRepositoryTest.java b/src/test/java/com/jeju/nanaland/domain/experience/repository/ExperienceRepositoryTest.java index 84a26e31..dd2e26ba 100644 --- a/src/test/java/com/jeju/nanaland/domain/experience/repository/ExperienceRepositoryTest.java +++ b/src/test/java/com/jeju/nanaland/domain/experience/repository/ExperienceRepositoryTest.java @@ -137,7 +137,8 @@ void getExperienceTypeKeywordSetTest() { @ParameterizedTest @EnumSource(value = Language.class) - void findSearchDtoByKeywordsUnionTest(Language language) { + @DisplayName("액티비티 Union 검색") + void findSearchDtoByKeywordsUnionActivityTest(Language language) { // given Pageable pageable = PageRequest.of(0, 10); List experiences1 = @@ -149,17 +150,39 @@ void findSearchDtoByKeywordsUnionTest(Language language) { // when Page resultDto = experienceRepository.findSearchDtoByKeywordsUnion( - List.of("keyword2", "keyword3"), language, pageable); + ExperienceType.ACTIVITY, List.of("keyword2", "keyword3"), language, pageable); // then - assertThat(resultDto.getTotalElements()).isEqualTo(5); + assertThat(resultDto.getTotalElements()).isEqualTo(2); + assertThat(resultDto.getContent().get(0).getMatchedCount()).isEqualTo(1); + } + + @ParameterizedTest + @EnumSource(value = Language.class) + @DisplayName("문화예술 Union 검색") + void findSearchDtoByKeywordsUnionCultureAndArtsTest(Language language) { + // given + Pageable pageable = PageRequest.of(0, 10); + List experiences1 = + getActivityList(language, List.of(LAND_LEISURE, WATER_LEISURE), "제주시", 2); + initHashtags(experiences1, List.of("keyword1", "kEyWoRd2"), language); + List experiences2 = + getCultureAndArtsList(language, List.of(EXHIBITION, MUSEUM, ART_MUSEUM), "서귀포시", 3); + initHashtags(experiences2, List.of("keyword2", "kEyWoRd3"), language); + + // when + Page resultDto = experienceRepository.findSearchDtoByKeywordsUnion( + ExperienceType.CULTURE_AND_ARTS, List.of("keyword2", "keyword3"), language, pageable); + + // then + assertThat(resultDto.getTotalElements()).isEqualTo(3); assertThat(resultDto.getContent().get(0).getMatchedCount()).isEqualTo(2); - assertThat(resultDto.getContent().get(3).getMatchedCount()).isEqualTo(1); } @ParameterizedTest @EnumSource(value = Language.class) - void findSearchDtoByKeywordsIntersectTest(Language language) { + @DisplayName("액티비티 Union 검색") + void findSearchDtoByKeywordsIntersectActivityTest(Language language) { // given Pageable pageable = PageRequest.of(0, 10); List keywords = List.of("keyword1", "keyword2", "keyword3", "keyword4", "keyword5"); @@ -168,16 +191,38 @@ void findSearchDtoByKeywordsIntersectTest(Language language) { initHashtags(experiences1, keywords, language); List experiences2 = getCultureAndArtsList(language, List.of(EXHIBITION, MUSEUM, ART_MUSEUM), "서귀포시", 3); - initHashtags(experiences2, List.of("keyword1", "kEyWoRd2"), language); + initHashtags(experiences2, keywords, language); // when Page resultDto = experienceRepository.findSearchDtoByKeywordsIntersect( - keywords, language, pageable); + ExperienceType.ACTIVITY, keywords, language, pageable); // then assertThat(resultDto.getTotalElements()).isEqualTo(2); } + @ParameterizedTest + @EnumSource(value = Language.class) + @DisplayName("문화예술 Union 검색") + void findSearchDtoByKeywordsIntersectCultureAndArtsTest(Language language) { + // given + Pageable pageable = PageRequest.of(0, 10); + List keywords = List.of("keyword1", "keyword2", "keyword3", "keyword4", "keyword5"); + List experiences1 = + getActivityList(language, List.of(LAND_LEISURE, WATER_LEISURE), "제주시", 2); + initHashtags(experiences1, keywords, language); + List experiences2 = + getCultureAndArtsList(language, List.of(EXHIBITION, MUSEUM, ART_MUSEUM), "서귀포시", 3); + initHashtags(experiences2, keywords, language); + + // when + Page resultDto = experienceRepository.findSearchDtoByKeywordsIntersect( + ExperienceType.CULTURE_AND_ARTS, keywords, language, pageable); + + // then + assertThat(resultDto.getTotalElements()).isEqualTo(3); + } + private List getActivityList(Language language, List keywordList, String addressTag, int size) { List experienceList = new ArrayList<>(); From d9e0c5b96c40efa1709b12dced0c58eae0279c96 Mon Sep 17 00:00:00 2001 From: seokhee Date: Mon, 16 Dec 2024 12:58:32 +0900 Subject: [PATCH 2/4] =?UTF-8?q?[#527]=20fix:=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=ED=82=A4=EC=9B=8C=EB=93=9C=20=EC=A1=B0=ED=95=A9=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/search/service/SearchService.java | 25 ++++++++++++++ .../search/service/SearchServiceTest.java | 33 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/src/main/java/com/jeju/nanaland/domain/search/service/SearchService.java b/src/main/java/com/jeju/nanaland/domain/search/service/SearchService.java index 799b5153..befe6132 100644 --- a/src/main/java/com/jeju/nanaland/domain/search/service/SearchService.java +++ b/src/main/java/com/jeju/nanaland/domain/search/service/SearchService.java @@ -584,4 +584,29 @@ private List getTopSearchVolumeList() { Set topSearchVolumes = zSetOperations.reverseRange(SEARCH_VOLUME_KEY, 0, 3); return topSearchVolumes != null ? new ArrayList<>(topSearchVolumes) : new ArrayList<>(); } + + /** + * 검색으로 들어온 키워드 조합 예를 들어 [jeju city restaurant]가 인자로 들어오면 [jeju, city, restaurant, jejucity, + * jejucityrestaurant, cityrestaurant]를 반환 + * + * @param keywords 공백으로 구분된 사용자의 검색어 + * @return 조합된 사용자의 검색어 + */ + private List combinationUserKeywords(List keywords) { + if (keywords.size() == 1) { + return keywords; + } + + List combinedKeywords = new ArrayList<>(keywords); + for (int i = 0; i < keywords.size() - 1; i++) { + StringBuilder combinedKeyword = new StringBuilder(); + combinedKeyword.append(keywords.get(i)); + for (int j = i + 1; j < keywords.size(); j++) { + combinedKeyword.append(keywords.get(j)); + combinedKeywords.add(combinedKeyword.toString()); + } + } + + return combinedKeywords; + } } diff --git a/src/test/java/com/jeju/nanaland/domain/search/service/SearchServiceTest.java b/src/test/java/com/jeju/nanaland/domain/search/service/SearchServiceTest.java index 14c9a248..2a436112 100644 --- a/src/test/java/com/jeju/nanaland/domain/search/service/SearchServiceTest.java +++ b/src/test/java/com/jeju/nanaland/domain/search/service/SearchServiceTest.java @@ -1,5 +1,6 @@ package com.jeju.nanaland.domain.search.service; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; import com.jeju.nanaland.domain.experience.repository.ExperienceRepository; @@ -10,7 +11,11 @@ import com.jeju.nanaland.domain.nature.repository.NatureRepository; import com.jeju.nanaland.domain.restaurant.repository.RestaurantRepository; import com.jeju.nanaland.global.config.RedisConfig; +import java.util.ArrayList; +import java.util.List; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; @@ -54,4 +59,32 @@ public void setup() { when(redisTemplate.opsForZSet()).thenReturn(zSetOperations); } + @Test + @DisplayName("검색어 조합 테스트") + void combinationUserKeywordsTest() { + // given + List keywords = List.of("jeju", "city", "restaurant"); + + // when + List combinedKeywords = new ArrayList<>(keywords); + for (int i = 0; i < keywords.size() - 1; i++) { + StringBuilder combinedKeyword = new StringBuilder(); + combinedKeyword.append(keywords.get(i)); + for (int j = i + 1; j < keywords.size(); j++) { + combinedKeyword.append(keywords.get(j)); + combinedKeywords.add(combinedKeyword.toString()); + } + } + + // then + assertThat(combinedKeywords).containsExactly( + "jeju", + "city", + "restaurant", + "jejucity", + "jejucityrestaurant", + "cityrestaurant" + ); + } + } \ No newline at end of file From a88c89dc3afefd268b1092cee042482bff17b8e9 Mon Sep 17 00:00:00 2001 From: seokhee Date: Mon, 16 Dec 2024 13:55:52 +0900 Subject: [PATCH 3/4] =?UTF-8?q?[#527]=20fix:=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=ED=82=A4=EC=9B=8C=EB=93=9C=20=EC=A1=B0=ED=95=A9=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/ExperienceRepositoryImpl.java | 70 +++++++--------- .../repository/FestivalRepositoryImpl.java | 18 ++++- .../repository/MarketRepositoryImpl.java | 18 ++++- .../nana/repository/NanaRepositoryImpl.java | 16 +++- .../repository/NatureRepositoryImpl.java | 18 ++++- .../repository/RestaurantRepositoryImpl.java | 21 ++++- .../domain/search/service/SearchService.java | 81 +++++++++++++------ .../search/service/SearchServiceTest.java | 24 ++++++ 8 files changed, 185 insertions(+), 81 deletions(-) diff --git a/src/main/java/com/jeju/nanaland/domain/experience/repository/ExperienceRepositoryImpl.java b/src/main/java/com/jeju/nanaland/domain/experience/repository/ExperienceRepositoryImpl.java index 3b9aee08..fb136d61 100644 --- a/src/main/java/com/jeju/nanaland/domain/experience/repository/ExperienceRepositoryImpl.java +++ b/src/main/java/com/jeju/nanaland/domain/experience/repository/ExperienceRepositoryImpl.java @@ -420,34 +420,6 @@ public PopularPostPreviewDto findPostPreviewDtoByLanguageAndId(Language language .fetchOne(); } - private List getIdListContainAllHashtags(String keywords, Language language) { - return queryFactory - .select(experience.id) - .from(experience) - .leftJoin(hashtag) - .on(hashtag.post.id.eq(experience.id) - .and(hashtag.category.eq(Category.EXPERIENCE)) - .and(hashtag.language.eq(language))) - .where(hashtag.keyword.content.toLowerCase().trim().in(keywords)) - .groupBy(experience.id) - .having(experience.id.count().eq(splitKeyword(keywords).stream().count())) - .fetch(); - } - - private List getIdListContainAllHashtags(List keywords, Language language) { - return queryFactory - .select(experience.id) - .from(experience) - .leftJoin(hashtag) - .on(hashtag.post.id.eq(experience.id) - .and(hashtag.category.eq(Category.EXPERIENCE)) - .and(hashtag.language.eq(language))) - .where(hashtag.keyword.content.toLowerCase().trim().in(keywords)) - .groupBy(experience.id) - .having(experience.id.count().eq(keywords.stream().count())) - .fetch(); - } - private List splitKeyword(String keyword) { String[] tokens = keyword.split("\\s+"); List tokenList = new ArrayList<>(); @@ -476,15 +448,42 @@ private BooleanExpression keywordCondition(List keywordFi } } + /** + * 공백 제거, 소문자화, '-'와 '_' 제거 + * + * @param stringExpression 조건절 컬럼 + * @return 정규화된 컬럼 + */ + private StringExpression normalizeStringExpression(StringExpression stringExpression) { + return Expressions.stringTemplate( + "replace(replace({0}, '-', ''), '_', '')", + stringExpression.toLowerCase().trim()); + } + + /** + * 제목, 주소태그, 내용과 일치하는 키워드 개수 카운팅 + * + * @param keywords 키워드 + * @return 키워드를 포함하는 조건 개수 + */ private Expression countMatchingWithKeyword(List keywords) { return Expressions.asNumber(0L) - .add(countMatchingConditionWithKeyword(experienceTrans.title.toLowerCase().trim(), keywords, - 0)) - .add(countMatchingConditionWithKeyword(experienceTrans.addressTag.toLowerCase().trim(), + .add(countMatchingConditionWithKeyword(normalizeStringExpression(experienceTrans.title), keywords, 0)) + .add( + countMatchingConditionWithKeyword(normalizeStringExpression(experienceTrans.addressTag), + keywords, 0)) .add(countMatchingConditionWithKeyword(experienceTrans.content, keywords, 0)); } + /** + * 조건이 키워드를 포함하는지 검사 + * + * @param condition 테이블 컬럼 + * @param keywords 유저 키워드 리스트 + * @param idx 키워드 인덱스 + * @return + */ private Expression countMatchingConditionWithKeyword(StringExpression condition, List keywords, int idx) { if (idx == keywords.size()) { @@ -497,13 +496,4 @@ private Expression countMatchingConditionWithKeyword(StringExpression c .otherwise(0) .add(countMatchingConditionWithKeyword(condition, keywords, idx + 1)); } - - private BooleanExpression containsAllKeywords(StringExpression condition, List keywords) { - BooleanExpression expression = null; - for (String keyword : keywords) { - BooleanExpression containsKeyword = condition.contains(keyword); - expression = (expression == null) ? containsKeyword : expression.and(containsKeyword); - } - return expression; - } } \ No newline at end of file diff --git a/src/main/java/com/jeju/nanaland/domain/festival/repository/FestivalRepositoryImpl.java b/src/main/java/com/jeju/nanaland/domain/festival/repository/FestivalRepositoryImpl.java index 09ce448e..cb5945cb 100644 --- a/src/main/java/com/jeju/nanaland/domain/festival/repository/FestivalRepositoryImpl.java +++ b/src/main/java/com/jeju/nanaland/domain/festival/repository/FestivalRepositoryImpl.java @@ -534,6 +534,18 @@ private BooleanExpression addressTagCondition(Language language, List countMatchingWithKeyword(List keywords) { return Expressions.asNumber(0L) - .add(countMatchingConditionWithKeyword(festivalTrans.title.toLowerCase().trim(), keywords, - 0)) - .add(countMatchingConditionWithKeyword(festivalTrans.addressTag.toLowerCase().trim(), + .add(countMatchingConditionWithKeyword(normalizeStringExpression(festivalTrans.title), + keywords, 0)) + .add(countMatchingConditionWithKeyword(normalizeStringExpression(festivalTrans.addressTag), keywords, 0)) .add(countMatchingConditionWithKeyword(festivalTrans.content, keywords, 0)); } diff --git a/src/main/java/com/jeju/nanaland/domain/market/repository/MarketRepositoryImpl.java b/src/main/java/com/jeju/nanaland/domain/market/repository/MarketRepositoryImpl.java index 747f72b7..72c55cbb 100644 --- a/src/main/java/com/jeju/nanaland/domain/market/repository/MarketRepositoryImpl.java +++ b/src/main/java/com/jeju/nanaland/domain/market/repository/MarketRepositoryImpl.java @@ -387,6 +387,18 @@ private BooleanExpression addressTagCondition(Language language, List countMatchingWithKeyword(List keywords) { return Expressions.asNumber(0L) - .add(countMatchingConditionWithKeyword(marketTrans.title.toLowerCase().trim(), keywords, - 0)) - .add(countMatchingConditionWithKeyword(marketTrans.addressTag.toLowerCase().trim(), + .add(countMatchingConditionWithKeyword(normalizeStringExpression(marketTrans.title), + keywords, 0)) + .add(countMatchingConditionWithKeyword(normalizeStringExpression(marketTrans.addressTag), keywords, 0)) .add(countMatchingConditionWithKeyword(marketTrans.content, keywords, 0)); } diff --git a/src/main/java/com/jeju/nanaland/domain/nana/repository/NanaRepositoryImpl.java b/src/main/java/com/jeju/nanaland/domain/nana/repository/NanaRepositoryImpl.java index c9c70282..e11a769b 100644 --- a/src/main/java/com/jeju/nanaland/domain/nana/repository/NanaRepositoryImpl.java +++ b/src/main/java/com/jeju/nanaland/domain/nana/repository/NanaRepositoryImpl.java @@ -385,6 +385,18 @@ private List splitKeyword(String keyword) { return tokenList; } + /** + * 공백 제거, 소문자화, '-'와 '_' 제거 + * + * @param stringExpression 조건절 컬럼 + * @return 정규화된 컬럼 + */ + private StringExpression normalizeStringExpression(StringExpression stringExpression) { + return Expressions.stringTemplate( + "replace(replace({0}, '-', ''), '_', '')", + stringExpression.toLowerCase().trim()); + } + /** * 제목, 주소태그, 내용과 일치하는 키워드 개수 카운팅 * @@ -393,8 +405,8 @@ private List splitKeyword(String keyword) { */ private Expression getMaxMatchingCountWithKeyword(List keywords) { return Expressions.asNumber(0L) - .add(countMatchingConditionWithKeyword(nanaTitle.heading.toLowerCase().trim(), keywords, - 0)) + .add(countMatchingConditionWithKeyword(normalizeStringExpression(nanaTitle.heading), + keywords, 0)) .add(countMatchingConditionWithKeyword(nanaContent.content, keywords, 0)) .max(); } diff --git a/src/main/java/com/jeju/nanaland/domain/nature/repository/NatureRepositoryImpl.java b/src/main/java/com/jeju/nanaland/domain/nature/repository/NatureRepositoryImpl.java index bacaf5c8..a485439b 100644 --- a/src/main/java/com/jeju/nanaland/domain/nature/repository/NatureRepositoryImpl.java +++ b/src/main/java/com/jeju/nanaland/domain/nature/repository/NatureRepositoryImpl.java @@ -439,6 +439,18 @@ private List splitKeyword(String keyword) { return tokenList; } + /** + * 공백 제거, 소문자화, '-'와 '_' 제거 + * + * @param stringExpression 조건절 컬럼 + * @return 정규화된 컬럼 + */ + private StringExpression normalizeStringExpression(StringExpression stringExpression) { + return Expressions.stringTemplate( + "replace(replace({0}, '-', ''), '_', '')", + stringExpression.toLowerCase().trim()); + } + /** * 제목, 주소태그, 내용과 일치하는 키워드 개수 카운팅 * @@ -447,9 +459,9 @@ private List splitKeyword(String keyword) { */ private Expression countMatchingWithKeyword(List keywords) { return Expressions.asNumber(0L) - .add(countMatchingConditionWithKeyword(natureTrans.title.toLowerCase().trim(), keywords, - 0)) - .add(countMatchingConditionWithKeyword(natureTrans.addressTag.toLowerCase().trim(), + .add(countMatchingConditionWithKeyword(normalizeStringExpression(natureTrans.title), + keywords, 0)) + .add(countMatchingConditionWithKeyword(normalizeStringExpression(natureTrans.addressTag), keywords, 0)) .add(countMatchingConditionWithKeyword(natureTrans.content, keywords, 0)); } diff --git a/src/main/java/com/jeju/nanaland/domain/restaurant/repository/RestaurantRepositoryImpl.java b/src/main/java/com/jeju/nanaland/domain/restaurant/repository/RestaurantRepositoryImpl.java index 09760f93..36cbfa8a 100644 --- a/src/main/java/com/jeju/nanaland/domain/restaurant/repository/RestaurantRepositoryImpl.java +++ b/src/main/java/com/jeju/nanaland/domain/restaurant/repository/RestaurantRepositoryImpl.java @@ -494,6 +494,18 @@ public List findAllIds() { .fetch(); } + /** + * 공백 제거, 소문자화, '-'와 '_' 제거 + * + * @param stringExpression 조건절 컬럼 + * @return 정규화된 컬럼 + */ + private StringExpression normalizeStringExpression(StringExpression stringExpression) { + return Expressions.stringTemplate( + "replace(replace({0}, '-', ''), '_', '')", + stringExpression.toLowerCase().trim()); + } + /** * 제목, 주소태그, 내용과 일치하는 키워드 개수 카운팅 * @@ -502,10 +514,11 @@ public List findAllIds() { */ private Expression countMatchingWithKeyword(List keywords) { return Expressions.asNumber(0L) - .add(countMatchingConditionWithKeyword(restaurantTrans.title.toLowerCase().trim(), keywords, - 0)) - .add(countMatchingConditionWithKeyword(restaurantTrans.addressTag.toLowerCase().trim(), + .add(countMatchingConditionWithKeyword(normalizeStringExpression(restaurantTrans.title), keywords, 0)) + .add( + countMatchingConditionWithKeyword(normalizeStringExpression(restaurantTrans.addressTag), + keywords, 0)) .add(countMatchingConditionWithKeyword(restaurantTrans.content, keywords, 0)); } @@ -515,7 +528,7 @@ private Expression countMatchingWithKeyword(List keywords) { * @param condition 테이블 컬럼 * @param keywords 유저 키워드 리스트 * @param idx 키워드 인덱스 - * @return + * @return 매칭된 수 */ private Expression countMatchingConditionWithKeyword(StringExpression condition, List keywords, int idx) { diff --git a/src/main/java/com/jeju/nanaland/domain/search/service/SearchService.java b/src/main/java/com/jeju/nanaland/domain/search/service/SearchService.java index befe6132..2c6d3207 100644 --- a/src/main/java/com/jeju/nanaland/domain/search/service/SearchService.java +++ b/src/main/java/com/jeju/nanaland/domain/search/service/SearchService.java @@ -130,14 +130,16 @@ public SearchResponse.ResultDto searchNature(MemberInfoDto memberInfoDto, String Language language = memberInfoDto.getLanguage(); Member member = memberInfoDto.getMember(); Pageable pageable = PageRequest.of(page, size); - List normalizedKeywords = Arrays.stream(keyword.split("\\s+")) // 공백기준 분할 - .map(String::toLowerCase) // 소문자로 - .toList(); + + // 사용자 검색어 정규화 + List normalizedKeywords = normalizeKeyword(keyword); Page resultPage; // 공백으로 구분한 키워드가 4개 이하라면 Union 검색 if (normalizedKeywords.size() <= 4) { - resultPage = natureRepository.findSearchDtoByKeywordsUnion(normalizedKeywords, + // 검색어 조합 + List combinedKeywords = combineUserKeywords(normalizedKeywords); + resultPage = natureRepository.findSearchDtoByKeywordsUnion(combinedKeywords, language, pageable); } // 4개보다 많다면 Intersect 검색 @@ -182,14 +184,16 @@ public SearchResponse.ResultDto searchFestival(MemberInfoDto memberInfoDto, Stri Language language = memberInfoDto.getLanguage(); Member member = memberInfoDto.getMember(); Pageable pageable = PageRequest.of(page, size); - List normalizedKeywords = Arrays.stream(keyword.split("\\s+")) // 공백기준 분할 - .map(String::toLowerCase) // 소문자로 - .toList(); + + // 사용자 검색어 정규화 + List normalizedKeywords = normalizeKeyword(keyword); Page resultPage; // 공백으로 구분한 키워드가 4개 이하라면 Union 검색 if (normalizedKeywords.size() <= 4) { - resultPage = festivalRepository.findSearchDtoByKeywordsUnion(normalizedKeywords, + // 검색어 조합 + List combinedKeywords = combineUserKeywords(normalizedKeywords); + resultPage = festivalRepository.findSearchDtoByKeywordsUnion(combinedKeywords, language, pageable); } // 4개보다 많다면 Intersect 검색 @@ -235,15 +239,17 @@ public SearchResponse.ResultDto searchExperience(MemberInfoDto memberInfoDto, Language language = memberInfoDto.getLanguage(); Member member = memberInfoDto.getMember(); Pageable pageable = PageRequest.of(page, size); - List normalizedKeywords = Arrays.stream(keyword.split("\\s+")) // 공백기준 분할 - .map(String::toLowerCase) // 소문자로 - .toList(); + + // 사용자 검색어 정규화 + List normalizedKeywords = normalizeKeyword(keyword); Page resultPage; // 공백으로 구분한 키워드가 4개 이하라면 Union 검색 if (normalizedKeywords.size() <= 4) { + // 검색어 조합 + List combinedKeywords = combineUserKeywords(normalizedKeywords); resultPage = experienceRepository.findSearchDtoByKeywordsUnion(experienceType, - normalizedKeywords, language, pageable); + combinedKeywords, language, pageable); } // 4개보다 많다면 Intersect 검색 else { @@ -287,14 +293,16 @@ public SearchResponse.ResultDto searchMarket(MemberInfoDto memberInfoDto, String Language language = memberInfoDto.getLanguage(); Member member = memberInfoDto.getMember(); Pageable pageable = PageRequest.of(page, size); - List normalizedKeywords = Arrays.stream(keyword.split("\\s+")) // 공백기준 분할 - .map(String::toLowerCase) // 소문자로 - .toList(); + + // 사용자 검색어 정규화 + List normalizedKeywords = normalizeKeyword(keyword); Page resultPage; // 공백으로 구분한 키워드가 4개 이하라면 Union 검색 if (normalizedKeywords.size() <= 4) { - resultPage = marketRepository.findSearchDtoByKeywordsUnion(normalizedKeywords, + // 검색어 조합 + List combinedKeywords = combineUserKeywords(normalizedKeywords); + resultPage = marketRepository.findSearchDtoByKeywordsUnion(combinedKeywords, language, pageable); } // 4개보다 많다면 Intersect 검색 @@ -338,14 +346,16 @@ public SearchResponse.ResultDto searchRestaurant(MemberInfoDto memberInfoDto, St Language language = memberInfoDto.getLanguage(); Member member = memberInfoDto.getMember(); Pageable pageable = PageRequest.of(page, size); - List normalizedKeywords = Arrays.stream(keyword.split("\\s+")) // 공백기준 분할 - .map(String::toLowerCase) // 소문자로 - .toList(); + + // 사용자 검색어 정규화 + List normalizedKeywords = normalizeKeyword(keyword); Page resultPage; // 공백으로 구분한 키워드가 4개 이하라면 Union 검색 if (normalizedKeywords.size() <= 4) { - resultPage = restaurantRepository.findSearchDtoByKeywordsUnion(normalizedKeywords, + // 검색어 조합 + List combinedKeywords = combineUserKeywords(normalizedKeywords); + resultPage = restaurantRepository.findSearchDtoByKeywordsUnion(combinedKeywords, language, pageable); } // 4개보다 많다면 Intersect 검색 @@ -391,14 +401,16 @@ public SearchResponse.ResultDto searchNana(MemberInfoDto memberInfoDto, String k Language language = memberInfoDto.getLanguage(); Member member = memberInfoDto.getMember(); Pageable pageable = PageRequest.of(page, size); - List normalizedKeywords = Arrays.stream(keyword.split("\\s+")) // 공백기준 분할 - .map(String::toLowerCase) // 소문자로 - .toList(); + + // 사용자 검색어 정규화 + List normalizedKeywords = normalizeKeyword(keyword); Page resultPage; // 공백으로 구분한 키워드가 4개 이하라면 Union 검색 if (normalizedKeywords.size() <= 4) { - resultPage = nanaRepository.findSearchDtoByKeywordsUnion(normalizedKeywords, + // 검색어 조합 + List combinedKeywords = combineUserKeywords(normalizedKeywords); + resultPage = nanaRepository.findSearchDtoByKeywordsUnion(combinedKeywords, language, pageable); } // 4개보다 많다면 Intersect 검색 @@ -585,14 +597,31 @@ private List getTopSearchVolumeList() { return topSearchVolumes != null ? new ArrayList<>(topSearchVolumes) : new ArrayList<>(); } + /** + * 사용자 검색어 정규화 검색어를 공백으로 구분하고 '-', '_' 제거, 모든 문자를 소문자로 변환 + * + * @param keyword 사용자 검색어 + * @return 공백으로 구분되고 정규화한 검색어 리스트 + */ + List normalizeKeyword(String keyword) { + return Arrays.stream(keyword.split("\\s+")) // 공백기준 분할 + .map(splittedKeyword -> splittedKeyword + .replace("-", "") // 하이픈 제거 + .replace("_", "") // 언더스코어 제거 + .toLowerCase() // 소문자로 + ) + .toList(); + } + + /** * 검색으로 들어온 키워드 조합 예를 들어 [jeju city restaurant]가 인자로 들어오면 [jeju, city, restaurant, jejucity, * jejucityrestaurant, cityrestaurant]를 반환 * - * @param keywords 공백으로 구분된 사용자의 검색어 + * @param keywords 사용자의 검색어 리스트 * @return 조합된 사용자의 검색어 */ - private List combinationUserKeywords(List keywords) { + private List combineUserKeywords(List keywords) { if (keywords.size() == 1) { return keywords; } diff --git a/src/test/java/com/jeju/nanaland/domain/search/service/SearchServiceTest.java b/src/test/java/com/jeju/nanaland/domain/search/service/SearchServiceTest.java index 2a436112..39f74f18 100644 --- a/src/test/java/com/jeju/nanaland/domain/search/service/SearchServiceTest.java +++ b/src/test/java/com/jeju/nanaland/domain/search/service/SearchServiceTest.java @@ -12,6 +12,7 @@ import com.jeju.nanaland.domain.restaurant.repository.RestaurantRepository; import com.jeju.nanaland.global.config.RedisConfig; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -59,6 +60,29 @@ public void setup() { when(redisTemplate.opsForZSet()).thenReturn(zSetOperations); } + @Test + @DisplayName("검색어 정규화 테스트") + void normalizeKeywordTest() { + // given + String keyword = "JEJU Jeju-city Korean_Restaurant"; + + // when + List normalizedKeyword = Arrays.stream(keyword.split("\\s+")) // 공백기준 분할 + .map(splittedKeyword -> splittedKeyword + .replace("-", "") // 하이픈 제거 + .replace("_", "") // 언더스코어 제거 + .toLowerCase() // 소문자로 + ) + .toList(); + + // then + assertThat(normalizedKeyword).containsExactly( + "jeju", + "jejucity", + "koreanrestaurant" + ); + } + @Test @DisplayName("검색어 조합 테스트") void combinationUserKeywordsTest() { From 9ee1fd70aac0a9789c025056fa763cdcd9da9b09 Mon Sep 17 00:00:00 2001 From: seokhee Date: Mon, 16 Dec 2024 14:00:43 +0900 Subject: [PATCH 4/4] =?UTF-8?q?[#527]=20fix:=20searchServiceTest=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nanaland/domain/search/service/SearchServiceTest.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/test/java/com/jeju/nanaland/domain/search/service/SearchServiceTest.java b/src/test/java/com/jeju/nanaland/domain/search/service/SearchServiceTest.java index 39f74f18..e15d5445 100644 --- a/src/test/java/com/jeju/nanaland/domain/search/service/SearchServiceTest.java +++ b/src/test/java/com/jeju/nanaland/domain/search/service/SearchServiceTest.java @@ -1,7 +1,6 @@ package com.jeju.nanaland.domain.search.service; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.when; import com.jeju.nanaland.domain.experience.repository.ExperienceRepository; import com.jeju.nanaland.domain.favorite.service.MemberFavoriteService; @@ -14,7 +13,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -54,12 +52,6 @@ class SearchServiceTest { @Mock private ZSetOperations zSetOperations; // ZSetOperations mock - @BeforeEach - public void setup() { - // opsForZSet() 호출 시 ZSetOperations mock을 반환하도록 설정 - when(redisTemplate.opsForZSet()).thenReturn(zSetOperations); - } - @Test @DisplayName("검색어 정규화 테스트") void normalizeKeywordTest() {