Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fix] 검색 조건 수정 #523

Merged
merged 15 commits into from
Dec 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/main/java/com/jeju/nanaland/domain/common/dto/SearchDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.jeju.nanaland.domain.common.dto;

import com.querydsl.core.annotations.QueryProjection;
import java.time.LocalDateTime;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

@Data
@SuperBuilder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class SearchDto {

private Long id;
private String title;
private ImageFileDto firstImage;
private Long matchedCount;
private LocalDateTime createdAt;

@QueryProjection
public SearchDto(Long id, String title, String originUrl, String thumbnailUrl,
Long matchedCount, LocalDateTime createdAt) {
this.id = id;
this.title = title;
this.firstImage = new ImageFileDto(originUrl, thumbnailUrl);
this.matchedCount = matchedCount;
this.createdAt = createdAt;
}

public void addMatchedCount(Long count) {
this.matchedCount += count;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.jeju.nanaland.domain.experience.dto;

import com.jeju.nanaland.domain.common.dto.SearchDto;
import com.querydsl.core.annotations.QueryProjection;
import java.time.LocalDateTime;
import lombok.AccessLevel;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

@Data
@SuperBuilder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ExperienceSearchDto extends SearchDto {

@QueryProjection
public ExperienceSearchDto(Long id, String title, String originUrl, String thumbnailUrl,
Long matchedCount,
LocalDateTime createdAt) {
super(id, title, originUrl, thumbnailUrl, matchedCount, createdAt);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.jeju.nanaland.domain.common.dto.PostPreviewDto;
import com.jeju.nanaland.domain.experience.dto.ExperienceCompositeDto;
import com.jeju.nanaland.domain.experience.dto.ExperienceResponse.ExperienceThumbnail;
import com.jeju.nanaland.domain.experience.dto.ExperienceSearchDto;
import com.jeju.nanaland.domain.experience.entity.enums.ExperienceType;
import com.jeju.nanaland.domain.experience.entity.enums.ExperienceTypeKeyword;
import com.jeju.nanaland.domain.review.dto.ReviewResponse.SearchPostForReviewDto;
Expand All @@ -20,9 +21,6 @@ public interface ExperienceRepositoryCustom {

ExperienceCompositeDto findCompositeDtoByIdWithPessimisticLock(Long id, Language language);

Page<ExperienceCompositeDto> searchCompositeDtoByKeyword(String keyword, Language language,
Pageable pageable);

Page<ExperienceThumbnail> findExperienceThumbnails(Language language,
ExperienceType experienceType, List<ExperienceTypeKeyword> keywordFilterList,
List<AddressTag> addressTags, Pageable pageable);
Expand All @@ -44,5 +42,9 @@ PopularPostPreviewDto findRandomPopularPostPreviewDtoByLanguage(Language languag

PopularPostPreviewDto findPostPreviewDtoByLanguageAndId(Language language, Long postId);

Page<ExperienceSearchDto> findSearchDtoByKeywordsUnion(List<String> keywords, Language language,
Pageable pageable);

Page<ExperienceSearchDto> findSearchDtoByKeywordsIntersect(List<String> keywords,
Language language, Pageable pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,43 @@
import com.jeju.nanaland.domain.common.dto.QPostPreviewDto;
import com.jeju.nanaland.domain.experience.dto.ExperienceCompositeDto;
import com.jeju.nanaland.domain.experience.dto.ExperienceResponse.ExperienceThumbnail;
import com.jeju.nanaland.domain.experience.dto.ExperienceSearchDto;
import com.jeju.nanaland.domain.experience.dto.QExperienceCompositeDto;
import com.jeju.nanaland.domain.experience.dto.QExperienceResponse_ExperienceThumbnail;
import com.jeju.nanaland.domain.experience.dto.QExperienceSearchDto;
import com.jeju.nanaland.domain.experience.entity.enums.ExperienceType;
import com.jeju.nanaland.domain.experience.entity.enums.ExperienceTypeKeyword;
import com.jeju.nanaland.domain.hashtag.entity.QKeyword;
import com.jeju.nanaland.domain.review.dto.QReviewResponse_SearchPostForReviewDto;
import com.jeju.nanaland.domain.review.dto.ReviewResponse.SearchPostForReviewDto;
import com.querydsl.core.Tuple;
import com.querydsl.core.group.GroupBy;
import com.querydsl.core.types.Expression;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.CaseBuilder;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.core.types.dsl.StringExpression;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.LockModeType;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.support.PageableExecutionUtils;

@RequiredArgsConstructor
public class ExperienceRepositoryImpl implements ExperienceRepositoryCustom {

private static final Logger log = LoggerFactory.getLogger(ExperienceRepositoryImpl.class);
private final JPAQueryFactory queryFactory;

@Override
Expand Down Expand Up @@ -100,54 +112,129 @@ public ExperienceCompositeDto findCompositeDtoByIdWithPessimisticLock(Long id,
}

@Override
public Page<ExperienceCompositeDto> searchCompositeDtoByKeyword(String keyword, Language language,
Pageable pageable) {
public Page<ExperienceSearchDto> findSearchDtoByKeywordsUnion(List<String> keywords,
Language language, Pageable pageable) {

List<Long> idListContainAllHashtags = getIdListContainAllHashtags(keyword, language);
// experience_id를 가진 게시물의 해시태그가 검색어 키워드 중 몇개를 포함하는지 계산
List<Tuple> keywordMatchQuery = queryFactory
.select(experience.id, experience.id.count())
.from(experience)
.leftJoin(hashtag)
.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))
.groupBy(experience.id)
.fetch();

List<ExperienceCompositeDto> resultDto = queryFactory
.select(new QExperienceCompositeDto(
Map<Long, Long> keywordMatchMap = keywordMatchQuery.stream()
.collect(Collectors.toMap(
tuple -> tuple.get(experience.id), // key: experience_id
tuple -> tuple.get(experience.id.count()) // value: 매칭된 키워드 개수
));

List<ExperienceSearchDto> resultDto = queryFactory
.select(new QExperienceSearchDto(
experience.id,
experienceTrans.title,
imageFile.originUrl,
imageFile.thumbnailUrl,
experience.contact,
experience.homepage,
experienceTrans.language,
experienceTrans.title,
experienceTrans.content,
experienceTrans.address,
experienceTrans.addressTag,
experienceTrans.intro,
experienceTrans.details,
experienceTrans.time,
experienceTrans.amenity,
experienceTrans.fee
countMatchingWithKeyword(keywords), // 제목, 내용, 지역정보와 매칭되는 키워드 개수
experience.createdAt
))
.from(experience)
.leftJoin(experience.firstImageFile, imageFile)
.leftJoin(experience.experienceTrans, experienceTrans)
.on(experienceTrans.language.eq(language))
.where(experienceTrans.title.contains(keyword)
.or(experienceTrans.addressTag.contains(keyword))
.or(experienceTrans.content.contains(keyword))
.or(experience.id.in(idListContainAllHashtags)))
.orderBy(experienceTrans.createdAt.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();

JPAQuery<Long> countQuery = queryFactory
.select(experience.count())
// 해시태그 값을 matchedCount에 더해줌
for (ExperienceSearchDto experienceSearchDto : resultDto) {
Long id = experienceSearchDto.getId();
experienceSearchDto.addMatchedCount(keywordMatchMap.getOrDefault(id, 0L));
}
// matchedCount가 0이라면 검색결과에서 제거
resultDto = resultDto.stream()
.filter(experienceSearchDto -> experienceSearchDto.getMatchedCount() > 0)
.toList();

// 매칭된 키워드 수 내림차순, 생성날짜 내림차순 정렬
List<ExperienceSearchDto> resultList = new ArrayList<>(resultDto);
resultList.sort(Comparator
.comparing(ExperienceSearchDto::getMatchedCount,
Comparator.nullsLast(Comparator.reverseOrder()))
.thenComparing(ExperienceSearchDto::getCreatedAt,
Comparator.nullsLast(Comparator.reverseOrder())));

// 페이징 처리
int startIdx = pageable.getPageSize() * pageable.getPageNumber();
int endIdx = Math.min(startIdx + pageable.getPageSize(), resultList.size());
List<ExperienceSearchDto> finalList = resultList.subList(startIdx, endIdx);
final Long total = Long.valueOf(resultDto.size());

return PageableExecutionUtils.getPage(finalList, pageable, () -> total);
}

@Override
public Page<ExperienceSearchDto> findSearchDtoByKeywordsIntersect(List<String> keywords,
Language language, Pageable pageable) {

// experience_id를 가진 게시물의 해시태그가 검색어 키워드 중 몇개를 포함하는지 계산
List<Tuple> keywordMatchQuery = queryFactory
.select(experience.id, experience.id.count())
.from(experience)
.leftJoin(hashtag)
.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))
.groupBy(experience.id)
.fetch();

Map<Long, Long> keywordMatchMap = keywordMatchQuery.stream()
.collect(Collectors.toMap(
tuple -> tuple.get(experience.id), // key: experience_id
tuple -> tuple.get(experience.id.count()) // value: 매칭된 키워드 개수
));

List<ExperienceSearchDto> resultDto = queryFactory
.select(new QExperienceSearchDto(
experience.id,
experienceTrans.title,
imageFile.originUrl,
imageFile.thumbnailUrl,
countMatchingWithKeyword(keywords), // 제목, 내용, 지역정보와 매칭되는 키워드 개수
experience.createdAt
))
.from(experience)
.leftJoin(experience.firstImageFile, imageFile)
.leftJoin(experience.experienceTrans, experienceTrans)
.on(experienceTrans.language.eq(language))
.where(experienceTrans.title.contains(keyword)
.or(experienceTrans.addressTag.contains(keyword))
.or(experienceTrans.content.contains(keyword))
.or(experience.id.in(idListContainAllHashtags)));
.fetch();

return PageableExecutionUtils.getPage(resultDto, pageable, countQuery::fetchOne);
// 해시태그 값을 matchedCount에 더해줌
for (ExperienceSearchDto experienceSearchDto : resultDto) {
Long id = experienceSearchDto.getId();
experienceSearchDto.addMatchedCount(keywordMatchMap.getOrDefault(id, 0L));
}
// matchedCount가 키워드 개수와 다르다면 검색결과에서 제거
resultDto = resultDto.stream()
.filter(experienceSearchDto -> experienceSearchDto.getMatchedCount() >= keywords.size())
.toList();

// 생성날짜 내림차순 정렬
List<ExperienceSearchDto> resultList = new ArrayList<>(resultDto);
resultList.sort(Comparator
.comparing(ExperienceSearchDto::getCreatedAt,
Comparator.nullsLast(Comparator.reverseOrder())));

// 페이징 처리
int startIdx = pageable.getPageSize() * pageable.getPageNumber();
int endIdx = Math.min(startIdx + pageable.getPageSize(), resultList.size());
List<ExperienceSearchDto> finalList = resultList.subList(startIdx, endIdx);
final Long total = Long.valueOf(resultDto.size());

return PageableExecutionUtils.getPage(finalList, pageable, () -> total);
}

@Override
Expand Down Expand Up @@ -326,17 +413,31 @@ public PopularPostPreviewDto findPostPreviewDtoByLanguageAndId(Language language
.fetchOne();
}

private List<Long> getIdListContainAllHashtags(String keyword, Language language) {
private List<Long> 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.in(splitKeyword(keyword)))
.where(hashtag.keyword.content.toLowerCase().trim().in(keywords))
.groupBy(experience.id)
.having(experience.id.count().eq(splitKeyword(keyword).stream().count()))
.having(experience.id.count().eq(splitKeyword(keywords).stream().count()))
.fetch();
}

private List<Long> getIdListContainAllHashtags(List<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(keywords.stream().count()))
.fetch();
}

Expand Down Expand Up @@ -367,4 +468,35 @@ private BooleanExpression keywordCondition(List<ExperienceTypeKeyword> keywordFi
return experienceKeyword.experienceTypeKeyword.in(keywordFilterList);
}
}

private Expression<Long> countMatchingWithKeyword(List<String> keywords) {
return Expressions.asNumber(0L)
.add(countMatchingConditionWithKeyword(experienceTrans.title.toLowerCase().trim(), keywords,
0))
.add(countMatchingConditionWithKeyword(experienceTrans.addressTag.toLowerCase().trim(),
keywords, 0))
.add(countMatchingConditionWithKeyword(experienceTrans.content, keywords, 0));
}

private Expression<Integer> countMatchingConditionWithKeyword(StringExpression condition,
List<String> keywords, int idx) {
if (idx == keywords.size()) {
return Expressions.asNumber(0);
}

return new CaseBuilder()
.when(condition.contains(keywords.get(idx)))
.then(1)
.otherwise(0)
.add(countMatchingConditionWithKeyword(condition, keywords, idx + 1));
}

private BooleanExpression containsAllKeywords(StringExpression condition, List<String> keywords) {
BooleanExpression expression = null;
for (String keyword : keywords) {
BooleanExpression containsKeyword = condition.contains(keyword);
expression = (expression == null) ? containsKeyword : expression.and(containsKeyword);
}
return expression;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.jeju.nanaland.domain.festival.dto;

import com.jeju.nanaland.domain.common.dto.SearchDto;
import com.querydsl.core.annotations.QueryProjection;
import java.time.LocalDateTime;
import lombok.AccessLevel;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

@Data
@SuperBuilder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class FestivalSearchDto extends SearchDto {

@QueryProjection
public FestivalSearchDto(Long id, String title, String originUrl, String thumbnailUrl,
Long matchedCount,
LocalDateTime createdAt) {
super(id, title, originUrl, thumbnailUrl, matchedCount, createdAt);
}
}
Loading