diff --git a/src/main/java/site/campingon/campingon/camp/controller/mongodb/SearchInfoController.java b/src/main/java/site/campingon/campingon/camp/controller/mongodb/SearchInfoController.java index 797335ad..19c180aa 100644 --- a/src/main/java/site/campingon/campingon/camp/controller/mongodb/SearchInfoController.java +++ b/src/main/java/site/campingon/campingon/camp/controller/mongodb/SearchInfoController.java @@ -18,7 +18,8 @@ import site.campingon.campingon.camp.service.mongodb.SearchInfoService; import site.campingon.campingon.common.jwt.CustomUserDetails; import site.campingon.campingon.common.util.AuthenticateUser; -import site.campingon.campingon.user.service.UserService; + +import java.util.List; @Slf4j @@ -60,4 +61,13 @@ public ResponseEntity> getMatchedCamps( userDetails.getName(), userDetails.getId(), PageRequest.of(page, size)) ); } + + // 검색어 자동완성 + @GetMapping("/autocomplete") + public ResponseEntity> getAutocompleteResults( + @RequestParam(name = "word") String word + ) { + return ResponseEntity.ok(searchInfoService.getAutocompleteResults(word)); + } + } diff --git a/src/main/java/site/campingon/campingon/camp/repository/mongodb/MongoSearchClient.java b/src/main/java/site/campingon/campingon/camp/repository/mongodb/MongoSearchClient.java index fe93b4e8..e54a7097 100644 --- a/src/main/java/site/campingon/campingon/camp/repository/mongodb/MongoSearchClient.java +++ b/src/main/java/site/campingon/campingon/camp/repository/mongodb/MongoSearchClient.java @@ -21,7 +21,8 @@ @RequiredArgsConstructor public class MongoSearchClient { private final MongoTemplate mongoTemplate; - private static final String INDEX_NAME = "searchIndex"; + private static final String SEARCH_INDEX = "searchIndex"; + private static final String AUTOCOMPLETE_INDEX = "autocompleteIndex"; private static final String COLLECTION_NAME = "search_info"; private static final String PROJECT_STAGE = "{" + @@ -39,9 +40,17 @@ public SearchResultDto searchWithUserPreferences(String searchTerm, List String mustClause = ""; if (StringUtils.hasText(city)) { List cityVariants = getCityVariants(city); - List phrases = cityVariants.stream() + List phrases = new ArrayList<>(); + + // city 검색을 위한 phrases + phrases.addAll(cityVariants.stream() .map(variant -> "{phrase: {query: '" + variant + "', path: 'address.city'}}") - .collect(Collectors.toList()); + .collect(Collectors.toList())); + + // state에서 제주시 추가 검색 + if (city.equals("제주특별자치도")) { + phrases.add("{phrase: {query: '제주시', path: 'address.state'}}"); + } mustClause = ", must: [{compound: {should: [" + String.join(",", phrases) + "]}}]"; } @@ -56,7 +65,7 @@ public SearchResultDto searchWithUserPreferences(String searchTerm, List "%s" + // must clause를 조건부로 추가 "}" + "}}", - INDEX_NAME, + SEARCH_INDEX, createShouldClauses(searchTerm, userKeywords), mustClause ); @@ -96,6 +105,9 @@ private String createShouldClauses(String searchTerm, List userKeywords) clauses.add("{text: {query: '" + searchTerm + "', path: 'address.state', score: {boost: {value: 3}}, fuzzy: {maxEdits: 1}}}"); clauses.add("{text: {query: '" + searchTerm + "', path: 'address.city', score: {boost: {value: 2}}}}"); clauses.add("{text: {query: '" + searchTerm + "', path: 'intro', score: {boost: {value: 2}}}}"); + + + //clauses.add("{regex: {query: '" + searchTerm + "', path: 'name', allowAnalyzedField: true, score: {boost: {value: 3.5}}}}"); } if (userKeywords != null && !userKeywords.isEmpty()) { @@ -170,13 +182,56 @@ else if (city.endsWith("도")) { variants.add(base); // 경기 } - // 특별자치도 케이스 (제주) + // 특별자치도 케이스 (제주, 강원) else if (city.endsWith("특별자치도")) { String base = city.replace("특별자치도", ""); - variants.add(base); // 제주 - variants.add(base + "도"); // 제주도 + variants.add(base); // 제주 + variants.add(base + "도"); // 제주도 + /*variants.add(base + "시"); // 제주시 + variants.add(base + "특별시"); // 제주특별시 + variants.add(base + "특별자치시"); // 제주특별자치시*/ } return variants; } + + + // 검색어 자동완성 + public List getAutocompleteResults(String word) { + String searchQuery = String.format( + "{$search: {" + + "index: '%s'," + + "autocomplete: {" + + "query: '%s'," + + "path: 'name'," + + "fuzzy: {maxEdits: 1}" + + "}" + + "}}", + AUTOCOMPLETE_INDEX, + word + ); + + String projectStage = "{$project: {name: 1}}"; // 이름만 + String limitStage = "{$limit: 8}"; // 자동 검색란은 6개까지만 + + AggregationOperation searchOperation = context -> Document.parse(searchQuery); + AggregationOperation projectOperation = context -> Document.parse(projectStage); + AggregationOperation limitOperation = context -> Document.parse(limitStage); + + List results = mongoTemplate.aggregate( + Aggregation.newAggregation( + searchOperation, + projectOperation, + limitOperation + ), + COLLECTION_NAME, + Document.class + ).getMappedResults(); + + return results.stream() + .map(doc -> doc.getString("name")) + .distinct() + .collect(Collectors.toList()); + + } } diff --git a/src/main/java/site/campingon/campingon/camp/service/CampService.java b/src/main/java/site/campingon/campingon/camp/service/CampService.java index 8d51a78d..517bbbb8 100644 --- a/src/main/java/site/campingon/campingon/camp/service/CampService.java +++ b/src/main/java/site/campingon/campingon/camp/service/CampService.java @@ -28,12 +28,9 @@ public class CampService { private final CampRepository campRepository; - private final MongoSearchClient searchInfoRepositoryImpl; - private final UserKeywordRepository userKeywordRepository; private final BookmarkRepository bookMarkRepository; private final CampMapper campMapper; - // 인기 캠핑장 조회 public Page getPopularCamps(Long userId, Pageable pageable) { return campRepository.findPopularCamps(pageable) diff --git a/src/main/java/site/campingon/campingon/camp/service/mongodb/SearchInfoService.java b/src/main/java/site/campingon/campingon/camp/service/mongodb/SearchInfoService.java index 08c44645..3353b55b 100644 --- a/src/main/java/site/campingon/campingon/camp/service/mongodb/SearchInfoService.java +++ b/src/main/java/site/campingon/campingon/camp/service/mongodb/SearchInfoService.java @@ -58,6 +58,7 @@ public Page searchExactMatchBySearchTermAndUserKeyword( if (userId != 0L) { dto.setMarked(bookmarkRepository.existsByCampIdAndUserId(searchInfo.getCampId(), userId)); } + return dto; }) .collect(Collectors.toList()); @@ -97,4 +98,12 @@ public Page getMatchedCampsByKeywords( return new PageImpl<>(dtoList, pageable, searchResult.getTotal()); } + + // 검색어 자동완성 + public List getAutocompleteResults(String word) { + if (!StringUtils.hasText(word) || word.length() < 3) { + return new ArrayList<>(); + } + return mongoSearchClient.getAutocompleteResults(word); + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8b28be9f..d3ddce7d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -2,6 +2,7 @@ spring: data: mongodb: uri: mongodb+srv://${MONGODB_USERNAME}:${MONGODB_PASSWORD}@${MONGODB_URL}/${MONGODB_NAME}?retryWrites=true&w=majority&appName=CampingOn + auto-index-creation: true redis: host: campingon-redis port: 6379