728x90
반응형
개념정리
Spring에서 Redis Sorted set으로 인기검색어 순위 나타내기
redis의 Sorted set을 이용한 인기검색어 순위 구현. 색인이라던지 다른 기능없이 단순 검색어 순위 나열이다.
기존에는 검색어를 전부 db에 저장하여 가장 개수가 많은 검색어 10개를 출력하는 식으로 구현했었지만 Sorted set을 이용하여 캐시에 검색어를 저장하고, 검색을 할 때마다 검색어별로 score를 하나씩 추가하였다. 그리고 score를 기준으로 상위 10개만 출력하도록 구현하였다.
1. Redis Sorted set
1-1. Sorted set 이란?
Redis에서 제공해주는 자료구조 중 하나인 Sorted Set(또는 ZSET, 둘다 동일한 말이다)은, 이름 그대로 Set의 특성을 그대로 가지면서 추가적으로 저장된 value들의 순서도 관리해준다. 이 때 이 순서를 위해 각 value에 대해 score를 필요에 맞게 설정할 수 있으며, 이 score를 기반으로 정렬이 된다.
1-2. Sorted set 구조
- ZSET은 key/value 형태의 자료구조이고, 여기서 key는 member, value는 score 라고 부른다. 하나의 ZSET에서 member는 unique하고, member 값을 통해 시간복잡도 O(1)로 해당하는 원소에 바로 접근할 수 있다.
- score 은 부동 소수점 수만 허용되고, 이 score 값을 기준으로 ZSET 내의 각 원소들이 순서를 가지게 된다.
반응형
2. 구현하기
2-1. 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'io.lettuce:lettuce-core:6.1.5.RELEASE'
2-2 Application.java에 @EnableCaching 추가
@EnableCaching
@EnableJpaAuditing
@SpringBootApplication
public class BackendApplication {
public static void main(String[] args) {
SpringApplication.run(BackendApplication.class, args);
}
}
2-3. redisConfiguration
@Configuration
@EnableRedisRepositories
public class RedisConfiguration extends CachingConfigurerSupport {
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.host}")
private String host;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(String.class));
return redisTemplate;
}
//레디스 캐시
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext
.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext
.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager
.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory)
.cacheDefaults(redisCacheConfiguration)
.build();
}
}
2-4 API
@GetMapping("/api/search/rank")
public List<SearchRankResponseDto> searchRankList(){
return searchService.SearchRankList();
}
2-5 Service
@RequiredArgsConstructor
@Service
public class SearchService {
private final SearchRepository searchRepository;
private final SearchRepositoryInterface searchRepositoryInterface;
private final CommentRepository commentRepository;
private final BookMarkRepository bookMarkRepository;
private final RedisTemplate<String, String> redisTemplate;
// 검색을 했을 때 해당 글목록 리스트를 출력하는 메서드
@Transactional
public SearchDto.TotalResponseDto keywordSearch(SearchDto.RequestDto searchRequestDto, Pageable pageable) {
PageImpl<Post> posts = searchRepository.keywordFilter(searchRequestDto, pageable);
Double score = 0.0;
try {
// 검색을하면 해당검색어를 value에 저장하고, score를 1 준다
redisTemplate.opsForZSet().incrementScore("ranking", searchRequestDto.getKeyword().get(0),1);
} catch (Exception e) {
System.out.println(e.toString());
}
//score를 1씩 올려준다.
redisTemplate.opsForZSet().incrementScore("ranking", searchRequestDto.getKeyword().get(0), score);
List<SearchDto.ResponseDto> responseDtoList = posts.stream()
.map(s -> new SearchDto.ResponseDto(
s.getId(),
s.getUser().getNickname(),
s.getTitle(),
s.getContent(),
s.getUser().getAddress(),
s.getMyItem(),
s.getExchangeItem(),
s.getUser().getProfileImg(),
s.getImages(),
s.getBookMarks().stream()
.map(this::toBookmarkResponseDto)
.collect(Collectors.toList()),
s.getCurrentState(),
TimeConversion.timeConversion(s.getCreateAt()),
bookMarkRepository.countByPost(s).orElse(0),
commentRepository.countByPost(s).orElse(0)))
.collect(Collectors.toList());
Long postCnt = (long) responseDtoList.size();
return SearchDto.TotalResponseDto.builder()
.postCnt(postCnt)
.posts(responseDtoList)
.build();
}
private BookMarkDto.DetailResponseDto toBookmarkResponseDto(BookMark bookMark) {
return BookMarkDto.DetailResponseDto.builder()
.userId(bookMark.getUser().getId())
.build();
}
// 인기검색어 리스트 1위~10위까지
public List<SearchRankResponseDto> SearchRankList() {
String key = "ranking";
ZSetOperations<String, String> ZSetOperations = redisTemplate.opsForZSet();
Set<ZSetOperations.TypedTuple<String>> typedTuples = ZSetOperations.reverseRangeWithScores(key, 0, 9); //score순으로 10개 보여줌
return typedTuples.stream().map(SearchRankResponseDto::convertToResponseRankingDto).collect(Collectors.toList());
}
}
2-6 확인하기
검색을 하면 해당검색어의 score가 하나씩올라가고 score순으로 10개가 출력된다
redis-cli에서 검색했을 때
간단하게 구현하는 것만 적었는데 레디스에 대해 잘 몰라서 이렇게 작성하는데만 해도 시간이 엄청 걸렸다. 이후 테스트코드도 작성해보고 혹시 시간이 된다면 다 갈아 엎더라도 검색엔진을 적용해보고 싶다.
참고
728x90
반응형
'개발 하나둘셋 > Java & Spring' 카테고리의 다른 글
spring 게시판 기간조회 방법! String -> LocalDateTime 변환 (0) | 2022.04.25 |
---|---|
스프링 DI ,IOC, AOP (0) | 2022.04.11 |
SpringBoot기반 Redis Cache (0) | 2022.01.13 |
웹소켓(WebSocket)으로 채팅기능 구현하기 (0) | 2022.01.03 |
OOP의 상속 / Implements와 Extends (0) | 2021.12.17 |