From a88c89dc3afefd268b1092cee042482bff17b8e9 Mon Sep 17 00:00:00 2001 From: seokhee Date: Mon, 16 Dec 2024 13:55:52 +0900 Subject: [PATCH] =?UTF-8?q?[#527]=20fix:=20=EA=B2=80=EC=83=89=20=ED=82=A4?= =?UTF-8?q?=EC=9B=8C=EB=93=9C=20=EC=A1=B0=ED=95=A9=20=EC=B6=94=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() {