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

refactor: 많이 조회한 게시글 키워드 구조 변경 #1061

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package in.koreatech.koin.domain.community.article.model;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class SearchLogEvent {
private final String query;
private final String ipAddress;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package in.koreatech.koin.domain.community.article.model;

import java.time.LocalDateTime;

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

import in.koreatech.koin.domain.community.article.repository.ArticleSearchKeywordIpMapRepository;
import in.koreatech.koin.domain.community.article.repository.ArticleSearchKeywordRepository;
import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class SearchLogEventListener {

private final ArticleSearchKeywordRepository articleSearchKeywordRepository;
private final ArticleSearchKeywordIpMapRepository articleSearchKeywordIpMapRepository;

private static final double INITIAL_WEIGHT = 1.0;
private static final int MAX_DYNAMIC_WEIGHT_COUNT = 5;
private static final int FIXED_WEIGHT_LIMIT = 10;
private static final double FIXED_WEIGHT = 1.0 / Math.pow(2, 4);

@EventListener
public void handleSearchLogEvent(SearchLogEvent event) {
String query = event.getQuery();
String ipAddress = event.getIpAddress();

if (query == null || query.trim().isEmpty()) {
return;
}

String[] keywords = query.split("\\s+");

for (String keywordStr : keywords) {
ArticleSearchKeyword keyword = articleSearchKeywordRepository.findByKeyword(keywordStr)
.orElseGet(() -> {
ArticleSearchKeyword newKeyword = ArticleSearchKeyword.builder()
.keyword(keywordStr)
.weight(INITIAL_WEIGHT)
.lastSearchedAt(LocalDateTime.now())
.totalSearch(1)
.build();
articleSearchKeywordRepository.save(newKeyword);
return newKeyword;
});

ArticleSearchKeywordIpMap map = articleSearchKeywordIpMapRepository.findByArticleSearchKeywordAndIpAddress(
keyword, ipAddress)
.orElseGet(() -> {
ArticleSearchKeywordIpMap newMap = ArticleSearchKeywordIpMap.builder()
.articleSearchKeyword(keyword)
.ipAddress(ipAddress)
.searchCount(1)
.build();
articleSearchKeywordIpMapRepository.save(newMap);
return newMap;
});

updateKeywordWeightAndCount(keyword, map);
}
}

private void updateKeywordWeightAndCount(ArticleSearchKeyword keyword, ArticleSearchKeywordIpMap map) {
map.incrementSearchCount();
double additionalWeight = calculateWeight(map.getSearchCount());

if (map.getSearchCount() <= FIXED_WEIGHT_LIMIT) {
keyword.updateWeight(keyword.getWeight() + additionalWeight);
}
articleSearchKeywordRepository.save(keyword);
}

private double calculateWeight(int searchCount) {
if (searchCount <= MAX_DYNAMIC_WEIGHT_COUNT) {
return 1.0 / Math.pow(2, searchCount - 1);
} else if (searchCount <= FIXED_WEIGHT_LIMIT) {
return FIXED_WEIGHT;
}
return 0.0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import java.util.Optional;
import java.util.stream.Collectors;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
Expand All @@ -23,9 +24,8 @@
import in.koreatech.koin.domain.community.article.dto.HotArticleItemResponse;
import in.koreatech.koin.domain.community.article.exception.ArticleBoardMisMatchException;
import in.koreatech.koin.domain.community.article.model.Article;
import in.koreatech.koin.domain.community.article.model.ArticleSearchKeyword;
import in.koreatech.koin.domain.community.article.model.ArticleSearchKeywordIpMap;
import in.koreatech.koin.domain.community.article.model.Board;
import in.koreatech.koin.domain.community.article.model.SearchLogEvent;
import in.koreatech.koin.domain.community.article.model.redis.ArticleHit;
import in.koreatech.koin.domain.community.article.model.redis.ArticleHitUser;
import in.koreatech.koin.domain.community.article.repository.ArticleRepository;
Expand All @@ -35,7 +35,6 @@
import in.koreatech.koin.domain.community.article.repository.redis.ArticleHitRepository;
import in.koreatech.koin.domain.community.article.repository.redis.ArticleHitUserRepository;
import in.koreatech.koin.domain.community.article.repository.redis.HotArticleRepository;
import in.koreatech.koin.global.concurrent.ConcurrencyGuard;
import in.koreatech.koin.global.exception.KoinIllegalArgumentException;
import in.koreatech.koin.global.model.Criteria;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -64,6 +63,7 @@ public class ArticleService {
private final HotArticleRepository hotArticleRepository;
private final ArticleHitUserRepository articleHitUserRepository;
private final Clock clock;
private final ApplicationEventPublisher eventPublisher;

@Transactional
public ArticleResponse getArticle(Integer boardId, Integer articleId, String publicIp) {
Expand Down Expand Up @@ -136,7 +136,7 @@ public ArticlesResponse searchArticles(String query, Integer boardId, Integer pa
articles = articleRepository.findAllByBoardIdAndTitleContaining(boardId, query, pageRequest);
}

saveOrUpdateSearchLog(query, ipAddress);
eventPublisher.publishEvent(new SearchLogEvent(query, ipAddress));

return ArticlesResponse.of(articles, criteria);
}
Expand All @@ -154,90 +154,6 @@ public ArticleHotKeywordResponse getArticlesHotKeyword(Integer count) {
return ArticleHotKeywordResponse.from(topKeywords);
}

@ConcurrencyGuard(lockName = "searchLog")
private void saveOrUpdateSearchLog(String query, String ipAddress) {
if (query == null || query.trim().isEmpty()) {
return;
}

String[] keywords = query.split("\\s+");

for (String keywordStr : keywords) {
ArticleSearchKeyword keyword = articleSearchKeywordRepository.findByKeyword(keywordStr)
.orElseGet(() -> {
ArticleSearchKeyword newKeyword = ArticleSearchKeyword.builder()
.keyword(keywordStr)
.weight(1.0)
.lastSearchedAt(LocalDateTime.now())
.totalSearch(1)
.build();
articleSearchKeywordRepository.save(newKeyword);
return newKeyword;
});

ArticleSearchKeywordIpMap map = articleSearchKeywordIpMapRepository.findByArticleSearchKeywordAndIpAddress(
keyword, ipAddress)
.orElseGet(() -> {
ArticleSearchKeywordIpMap newMap = ArticleSearchKeywordIpMap.builder()
.articleSearchKeyword(keyword)
.ipAddress(ipAddress)
.searchCount(1)
.build();
articleSearchKeywordIpMapRepository.save(newMap);
return newMap;
});

updateKeywordWeightAndCount(keyword, map);
}
}

/*
* 추가될 가중치를 계산합니다. 검색 횟수(map.getSearchCount())에 따라 가중치를 다르게 부여합니다:
* - 검색 횟수가 5회 이하인 경우: 1.0 / 2^(검색 횟수 - 1)
* 예:
* 첫 번째 검색: 1.0
* 두 번째 검색: 0.5
* 세 번째 검색: 0.25
* 네 번째 검색: 0.125
* 다섯 번째 검색: 0.0625
* - 검색 횟수가 5회를 초과하면: 1.0 / 2^4 (즉, 0.0625) 고정 가중치를 부여합니다.
* - 검색 횟수가 10회를 초과할 경우: 추가 가중치를 부여하지 않습니다.
*/
private void updateKeywordWeightAndCount(ArticleSearchKeyword keyword, ArticleSearchKeywordIpMap map) {
map.incrementSearchCount();
double additionalWeight = 0.0;

if (map.getSearchCount() <= 5) {
additionalWeight = 1.0 / Math.pow(2, map.getSearchCount() - 1);
} else if (map.getSearchCount() <= 10) {
additionalWeight = 1.0 / Math.pow(2, 4);
}

if (map.getSearchCount() <= 10) {
keyword.updateWeight(keyword.getWeight() + additionalWeight);
}
articleSearchKeywordRepository.save(keyword);
}

@Transactional
public void resetWeightsAndCounts() {
LocalDateTime now = LocalDateTime.now();
LocalDateTime before = now.minusHours(6).minusMinutes(30);

List<ArticleSearchKeyword> keywordsToUpdate = articleSearchKeywordRepository.findByUpdatedAtBetween(
before, now);
List<ArticleSearchKeywordIpMap> ipMapsToUpdate = articleSearchKeywordIpMapRepository.findByUpdatedAtBetween(
before, now);

for (ArticleSearchKeyword keyword : keywordsToUpdate) {
keyword.resetWeight();
}

for (ArticleSearchKeywordIpMap ipMap : ipMapsToUpdate) {
ipMap.resetSearchCount();
}
}

@Transactional
public void updateHotArticles() {
List<ArticleHit> articleHits = articleHitRepository.findAll();
Expand Down
Loading