-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #108 from Korea-Certified-Store/feature/fix-search…
- Loading branch information
Showing
10 changed files
with
194 additions
and
128 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,9 +46,6 @@ jobs: | |
mysql user: 'test' | ||
mysql password: ${{ secrets.DB_PASSWORD }} | ||
|
||
- name: Set up Redis | ||
uses: shogo82148/[email protected] | ||
|
||
# github action 에서 Gradle dependency 캐시 사용 | ||
- name: Cache Gradle packages | ||
uses: actions/cache@v3 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
116 changes: 77 additions & 39 deletions
116
src/main/java/com/nainga/nainga/domain/store/application/StoreService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,62 +1,100 @@ | ||
package com.nainga.nainga.domain.store.application; | ||
|
||
import com.nainga.nainga.domain.store.dao.StoreRepository; | ||
import com.nainga.nainga.global.application.RedisSortedSetService; | ||
import jakarta.annotation.PostConstruct; | ||
import lombok.RequiredArgsConstructor; | ||
import org.apache.commons.lang3.StringUtils; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.Set; | ||
import java.util.concurrent.ExecutorService; | ||
import java.util.concurrent.Executors; | ||
|
||
@Service | ||
@Transactional(readOnly = true) | ||
@RequiredArgsConstructor | ||
public class StoreService { | ||
private final StoreRepository storeRepository; | ||
private final RedisSortedSetService redisSortedSetService; | ||
private String suffix = "*"; //검색어 자동 완성 기능에서 실제 노출될 수 있는 완벽한 형태의 단어를 구분하기 위한 접미사 | ||
private int maxSize = 10; //검색어 자동 완성 기능 최대 개수 | ||
|
||
@PostConstruct | ||
public void init() { //이 Service Bean이 생성된 이후에 검색어 자동 완성 기능을 위한 데이터들을 Redis에 저장 (Redis는 인메모리 DB라 휘발성을 띄기 때문) | ||
redisSortedSetService.removeAllOfSortedSet(); | ||
saveAllSubstring(storeRepository.findAllDisplayName()); //MySQL DB에 저장된 모든 가게명을 음절 단위로 잘라 모든 Substring을 Redis에 저장해주는 로직 | ||
|
||
public List<String> autocorrect(String keyword) { //검색어 자동 완성 로직 | ||
return storeRepository.findAllBySearchKeyword(keyword); | ||
} | ||
|
||
public void saveAllSubstring(List<String> allDisplayName) { | ||
ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); //병렬 처리를 위한 스레드풀을 생성하는 과정 | ||
// private final RedisSortedSetService redisSortedSetService; | ||
// private final RedisHashService redisHashService; | ||
|
||
for (String displayName : allDisplayName) { | ||
executorService.submit(() -> { //submit 메서드를 사용해서 병렬 처리할 작업 추가 | ||
redisSortedSetService.addToSortedSet(displayName + suffix); | ||
/* | ||
Redis Hash 자료 구조를 활용한 새로운 검색어 자동 완성 로직입니다. | ||
아래 로직은 Case insensitive하게 검색될 수 있도록 구현하였습니다. | ||
지금은 MySQL이 성능이 더 좋아서 사용하지 않습니다. | ||
*/ | ||
|
||
for (int i = displayName.length(); i > 0; --i) { | ||
redisSortedSetService.addToSortedSet(displayName.substring(0, i)); | ||
} | ||
}); | ||
} | ||
executorService.shutdown(); //작업이 모두 완료되면 스레드풀을 종료 | ||
} | ||
// @PostConstruct | ||
// public void init() { //이 Service Bean이 생성된 이후에 검색어 자동 완성 기능을 위한 데이터들을 Redis에 저장 (Redis는 인메모리 DB라 휘발성을 띄기 때문) | ||
// redisHashService.removeAllOfHash(); | ||
// saveAllDisplayName(storeRepository.findAllDisplayName()); //모든 가게명을 소문자로 변환한 것을 field, 원래 가게 이름을 value로 매핑시켜서 Redis Hash에 저장 | ||
// } | ||
|
||
public List<String> autocorrect(String keyword) { //검색어 자동 완성 기능 관련 로직 | ||
Long index = redisSortedSetService.findFromSortedSet(keyword); //사용자가 입력한 검색어를 바탕으로 Redis에서 조회한 결과 매칭되는 index | ||
// public void saveAllDisplayName(List<String> allDisplayName) { | ||
// ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); //병렬 처리를 위한 스레드풀을 생성하는 과정 | ||
// | ||
// for (String displayName : allDisplayName) { | ||
// executorService.submit(() -> { //submit 메서드를 사용해서 병렬 처리할 작업 추가 | ||
// //Redis 내장 기능이 아닌 case insensitive 조회를 구현하기 위해 소문자로 변환한 field값과 원래 string인 value의 쌍을 Hash에 저장 | ||
// redisHashService.addToHash(displayName.toLowerCase(), displayName); //case insensitive하게 검색어 자동 완성 기능을 직접 구현하기 위해 소문자로 통일해서 저장 | ||
// }); | ||
// } | ||
// executorService.shutdown(); //작업이 모두 완료되면 스레드풀을 종료 | ||
// } | ||
|
||
if (index == null) { | ||
return new ArrayList<>(); //만약 사용자 검색어 바탕으로 자동 완성 검색어를 만들 수 없으면 Empty Array 리턴 | ||
} | ||
// public List<String> autocorrect(String keyword) { //검색어 자동 완성 로직 | ||
// Set<String> allValuesContainingSearchKeyword = redisHashService.findAllValuesContainingSearchKeyword(keyword); //case insensitive하게 serachKeyword를 포함하는 가게 이름 최대 10개 반환 | ||
// if(allValuesContainingSearchKeyword.isEmpty()) | ||
// return new ArrayList<>(); | ||
// else | ||
// return new ArrayList<>(allValuesContainingSearchKeyword); //자동 완성 결과가 존재하면 ArrayList로 변환하여 리턴 | ||
// } | ||
|
||
Set<String> allValuesAfterIndexFromSortedSet = redisSortedSetService.findAllValuesAfterIndexFromSortedSet(index); //사용자 검색어 이후로 정렬된 Redis 데이터들 가져오기 | ||
/* | ||
아래 주석처리 된 코드는 초기 검색어 자동 완성 로직입니다. | ||
검색어 자동 완성에 대한 요구 사항이 앞에서부터 매칭되는 글자가 아닌 Contains 개념으로 바뀌어서 주석 처리하여 임시로 남겨놓았습니다. | ||
지금은 MySQL이 성능이 더 좋기 때문에 사용하지 않는 코드입니다. | ||
*/ | ||
|
||
return allValuesAfterIndexFromSortedSet.stream() | ||
.filter(value -> value.endsWith(suffix) && value.startsWith(keyword)) | ||
.map(value -> StringUtils.removeEnd(value, suffix)) | ||
.limit(maxSize) | ||
.toList(); //자동 완성을 통해 만들어진 최대 maxSize개의 키워드들 | ||
} | ||
// private String suffix = "*"; //검색어 자동 완성 기능에서 실제 노출될 수 있는 완벽한 형태의 단어를 구분하기 위한 접미사 | ||
// private int maxSize = 10; //검색어 자동 완성 기능 최대 개수 | ||
// | ||
// @PostConstruct | ||
// public void init() { //이 Service Bean이 생성된 이후에 검색어 자동 완성 기능을 위한 데이터들을 Redis에 저장 (Redis는 인메모리 DB라 휘발성을 띄기 때문) | ||
// redisSortedSetService.removeAllOfSortedSet(); | ||
// saveAllSubstring(storeRepository.findAllDisplayName()); //MySQL DB에 저장된 모든 가게명을 음절 단위로 잘라 모든 Substring을 Redis에 저장해주는 로직 | ||
// } | ||
// | ||
// public void saveAllSubstring(List<String> allDisplayName) { | ||
// ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); //병렬 처리를 위한 스레드풀을 생성하는 과정 | ||
// | ||
// for (String displayName : allDisplayName) { | ||
// executorService.submit(() -> { //submit 메서드를 사용해서 병렬 처리할 작업 추가 | ||
// redisSortedSetService.addToSortedSet(displayName + suffix); | ||
// | ||
// for (int i = displayName.length(); i > 0; --i) { | ||
// redisSortedSetService.addToSortedSet(displayName.substring(0, i)); | ||
// } | ||
// }); | ||
// } | ||
// executorService.shutdown(); //작업이 모두 완료되면 스레드풀을 종료 | ||
// } | ||
// | ||
// public List<String> autocorrect(String keyword) { //검색어 자동 완성 기능 관련 로직 | ||
// Long index = redisSortedSetService.findFromSortedSet(keyword); //사용자가 입력한 검색어를 바탕으로 Redis에서 조회한 결과 매칭되는 index | ||
// | ||
// if (index == null) { | ||
// return new ArrayList<>(); //만약 사용자 검색어 바탕으로 자동 완성 검색어를 만들 수 없으면 Empty Array 리턴 | ||
// } | ||
// | ||
// Set<String> allValuesAfterIndexFromSortedSet = redisSortedSetService.findAllValuesAfterIndexFromSortedSet(index); //사용자 검색어 이후로 정렬된 Redis 데이터들 가져오기 | ||
// | ||
// return allValuesAfterIndexFromSortedSet.stream() | ||
// .filter(value -> value.endsWith(suffix) && value.startsWith(keyword)) | ||
// .map(value -> StringUtils.removeEnd(value, suffix)) | ||
// .limit(maxSize) | ||
// .toList(); //자동 완성을 통해 만들어진 최대 maxSize개의 키워드들 | ||
// } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
54 changes: 54 additions & 0 deletions
54
src/main/java/com/nainga/nainga/global/application/RedisHashService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
//package com.nainga.nainga.global.application; | ||
// | ||
//import org.springframework.data.redis.core.Cursor; | ||
//import org.springframework.data.redis.core.HashOperations; | ||
//import org.springframework.data.redis.core.RedisTemplate; | ||
//import org.springframework.data.redis.core.ScanOptions; | ||
//import org.springframework.stereotype.Service; | ||
// | ||
//import java.util.HashSet; | ||
//import java.util.Map; | ||
//import java.util.Set; | ||
//import java.util.TreeSet; | ||
// | ||
//@Service | ||
//public class RedisHashService { | ||
// private final HashOperations<String, String, String> hashOperations; | ||
// private final RedisTemplate<String, String> redisTemplate; | ||
// | ||
// public RedisHashService(RedisTemplate<String, String> redisTemplate) { | ||
// this.hashOperations = redisTemplate.opsForHash(); | ||
// this.redisTemplate = redisTemplate; | ||
// } | ||
// | ||
// private String key = "autocorrect"; //검색어 자동 완성을 위한 Redis 데이터 | ||
// | ||
// //Hash에 field-value 쌍을 추가하는 메서드 | ||
// public void addToHash(String field, String value) { | ||
// hashOperations.put(key, field, value); | ||
// } | ||
// | ||
// public Set<String> findAllValuesContainingSearchKeyword(String searchKeyword) { | ||
// //Redis에서는 case insensitive한 검색을 지원하는 내장 모듈이 없으므로 searchKeyword는 모두 소문자로 통일하여 검색하도록 구현 | ||
// //당연히 초기 Redis에 field를 저장할 때도 모두 소문자로 변형하여 저장했고 원본 문자열은 value에 저장! | ||
// Set<String> result = new HashSet<>(); //searchKeyword를 포함하는 원래 가게 이름들의 리스트. 최대 maxSize개까지 저장. 중복 허용하지 않고, 자동 사전순 정렬하기 위해 사용 | ||
// final int maxSize = 10; //최대 검색어 자동 완성 개수 | ||
// | ||
// ScanOptions scanOptions = ScanOptions.scanOptions().match("*" + searchKeyword + "*").build(); //searchKeyword를 포함하는지를 검사하기 위한 scanOption | ||
// Cursor<Map.Entry<String, String>> cursor = hashOperations.scan(key, scanOptions); //기존 Redis Keys 로직의 성능 이슈를 해결하기 위해 10개 단위로 끊어서 조회하는 Scan 기능 사용 | ||
// | ||
// while (cursor.hasNext()) { //끊어서 조회하다보니 while loop로 조회 | ||
// Map.Entry<String, String> entry = cursor.next(); | ||
// result.add(entry.getValue()); | ||
// | ||
// if(result.size() >= maxSize) //maxSize에 도달하면 scan 중단 | ||
// break; | ||
// } | ||
// cursor.close(); | ||
// return result; | ||
// } | ||
// | ||
// public void removeAllOfHash() { | ||
// redisTemplate.delete(key); | ||
// } | ||
//} |
68 changes: 34 additions & 34 deletions
68
src/main/java/com/nainga/nainga/global/application/RedisSortedSetService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,34 +1,34 @@ | ||
package com.nainga.nainga.global.application; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.data.redis.core.RedisCallback; | ||
import org.springframework.data.redis.core.RedisTemplate; | ||
import org.springframework.data.redis.core.ZSetOperations; | ||
import org.springframework.stereotype.Service; | ||
|
||
import java.util.List; | ||
import java.util.Set; | ||
|
||
@Service | ||
@RequiredArgsConstructor | ||
public class RedisSortedSetService { //검색어 자동 완성을 구현할 때 사용하는 Redis의 SortedSet 관련 서비스 레이어 | ||
private final RedisTemplate<String, String> redisTemplate; | ||
private String key = "autocorrect"; //검색어 자동 완성을 위한 Redis 데이터 | ||
private int score = 0; //Score는 딱히 필요 없으므로 하나로 통일 | ||
|
||
public void addToSortedSet(String value) { //Redis SortedSet에 추가 | ||
redisTemplate.opsForZSet().add(key, value, score); | ||
} | ||
|
||
public Long findFromSortedSet(String value) { //Redis SortedSet에서 Value를 찾아 인덱스를 반환 | ||
return redisTemplate.opsForZSet().rank(key, value); | ||
} | ||
|
||
public Set<String> findAllValuesAfterIndexFromSortedSet(Long index) { | ||
return redisTemplate.opsForZSet().range(key, index, index + 200); //전체를 다 불러오기 보다는 200개 정도만 가져와도 자동 완성을 구현하는 데 무리가 없으므로 200개로 rough하게 설정 | ||
} | ||
|
||
public void removeAllOfSortedSet() { | ||
redisTemplate.opsForZSet().removeRange(key, 0, -1); | ||
} | ||
} | ||
//package com.nainga.nainga.global.application; | ||
// | ||
//import lombok.RequiredArgsConstructor; | ||
//import org.springframework.data.redis.core.RedisCallback; | ||
//import org.springframework.data.redis.core.RedisTemplate; | ||
//import org.springframework.data.redis.core.ZSetOperations; | ||
//import org.springframework.stereotype.Service; | ||
// | ||
//import java.util.List; | ||
//import java.util.Set; | ||
// | ||
//@Service | ||
//@RequiredArgsConstructor | ||
//public class RedisSortedSetService { //검색어 자동 완성을 구현할 때 사용하는 Redis의 SortedSet 관련 서비스 레이어 | ||
// private final RedisTemplate<String, String> redisTemplate; | ||
// private String key = "autocorrect"; //검색어 자동 완성을 위한 Redis 데이터 | ||
// private int score = 0; //Score는 딱히 필요 없으므로 하나로 통일 | ||
// | ||
// public void addToSortedSet(String value) { //Redis SortedSet에 추가 | ||
// redisTemplate.opsForZSet().add(key, value, score); | ||
// } | ||
// | ||
// public Long findFromSortedSet(String value) { //Redis SortedSet에서 Value를 찾아 인덱스를 반환 | ||
// return redisTemplate.opsForZSet().rank(key, value); | ||
// } | ||
// | ||
// public Set<String> findAllValuesAfterIndexFromSortedSet(Long index) { | ||
// return redisTemplate.opsForZSet().range(key, index, index + 200); //전체를 다 불러오기 보다는 200개 정도만 가져와도 자동 완성을 구현하는 데 무리가 없으므로 200개로 rough하게 설정 | ||
// } | ||
// | ||
// public void removeAllOfSortedSet() { | ||
// redisTemplate.opsForZSet().removeRange(key, 0, -1); | ||
// } | ||
//} |
28 changes: 0 additions & 28 deletions
28
src/main/java/com/nainga/nainga/global/config/RedisConfig.java
This file was deleted.
Oops, something went wrong.
Submodule backend-submodule
updated
from bf2608 to 30d800
Oops, something went wrong.