diff --git a/src/main/java/com/hyetaekon/hyetaekon/common/config/SecurityPath.java b/src/main/java/com/hyetaekon/hyetaekon/common/config/SecurityPath.java index 3708117..3c3c519 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/common/config/SecurityPath.java +++ b/src/main/java/com/hyetaekon/hyetaekon/common/config/SecurityPath.java @@ -15,7 +15,8 @@ public class SecurityPath { "/api/services/category/*", "/api/services/detail/*", "/api/public-data/serviceList/test", - "/api/mongo/services/search", + "/api/mongo/search/services", + "/api/mongo/search/posts", "/api/mongo/services/search/autocomplete" }; diff --git a/src/main/java/com/hyetaekon/hyetaekon/publicservice/controller/mongodb/SearchInfoController.java b/src/main/java/com/hyetaekon/hyetaekon/publicservice/controller/mongodb/SearchInfoController.java index 9695bc5..c735cbb 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/publicservice/controller/mongodb/SearchInfoController.java +++ b/src/main/java/com/hyetaekon/hyetaekon/publicservice/controller/mongodb/SearchInfoController.java @@ -1,5 +1,7 @@ package com.hyetaekon.hyetaekon.publicservice.controller.mongodb; +import com.hyetaekon.hyetaekon.post.dto.PostListResponseDto; +import com.hyetaekon.hyetaekon.post.service.PostService; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Positive; @@ -18,14 +20,15 @@ @Validated @RestController -@RequestMapping("/api/mongo/services") +@RequestMapping("/api/mongo/search") @RequiredArgsConstructor public class SearchInfoController { private final ServiceSearchHandler searchService; + private final PostService postService; private final AuthenticateUser authenticateUser; // 검색 API (로그인/비로그인 통합) - @GetMapping("/search") + @GetMapping("/services") public ResponseEntity> searchServices( @RequestParam(name = "searchTerm", required = false, defaultValue = "") String searchTerm, @RequestParam(name = "page", defaultValue = "0") @Min(0) int page, @@ -48,7 +51,18 @@ public ResponseEntity> searchServices( } } - // 자동완성 API + // 게시글 제목 검색(통합검색) + @GetMapping("/posts") + public ResponseEntity> searchPosts( + @RequestParam String searchTerm, + @RequestParam(name = "page", defaultValue = "0") @Min(0) int page, + @RequestParam(name = "size", defaultValue = "9") @Positive @Max(50) int size) { + + return ResponseEntity.ok(postService.getAllPosts( + searchTerm, "createdAt", "DESC", PageRequest.of(page, size))); + } + + // 자동완성 API(서비스에 대해서만) @GetMapping("/search/autocomplete") public ResponseEntity> getAutocompleteResults( @RequestParam(name = "word") String word diff --git a/src/main/java/com/hyetaekon/hyetaekon/publicservice/repository/mongodb/MatchedServiceClient.java b/src/main/java/com/hyetaekon/hyetaekon/publicservice/repository/mongodb/MatchedServiceClient.java index e0c7241..03dad8e 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/publicservice/repository/mongodb/MatchedServiceClient.java +++ b/src/main/java/com/hyetaekon/hyetaekon/publicservice/repository/mongodb/MatchedServiceClient.java @@ -119,29 +119,18 @@ private String buildSearchQuery( // should 조건 (가중치가 적용된 키워드 조건) List shouldClauses = createKeywordMatchClauses(keywords); - // must 조건 (사용자 조건 필터링) - String mustClause = buildUserMustClause(userGender, userAge); - - // 직업 및 소득 관련 should 조건 추가 - if (StringUtils.hasText(userJob)) { - shouldClauses.add(createSearchClause("occupations", userJob, 3.0f)); - shouldClauses.add(createSearchClause("businessTypes", userJob, 3.0f)); - } - - if (StringUtils.hasText(userIncomeLevel)) { - shouldClauses.add(createSearchClause("incomeLevel", userIncomeLevel, 2.8f)); - shouldClauses.add(createSearchClause("incomeLevel", "ANY", 1.0f)); - // 소득 수준 범위에 따른 가중치 추가 - addIncomeLevelRangeBoosts(shouldClauses, userIncomeLevel); - } + // 성별과 나이 조건도 should로 변경 + addUserMatchBoosts(shouldClauses, userGender, userAge, userIncomeLevel, userJob); String shouldClausesStr = shouldClauses.isEmpty() ? "[]" : "[" + String.join(",", shouldClauses) + "]"; + // compound 쿼리 구성 (should 조건만 사용) String compoundQuery = """ compound: { - should: %s%s + should: %s, + minimumShouldMatch: 1 } - """.formatted(shouldClausesStr, mustClause); + """.formatted(shouldClausesStr); return """ { @@ -153,7 +142,7 @@ private String buildSearchQuery( } /** - * 키워드 기반 검색 조건 생성 (단일 가중치 적용) + * 키워드 기반 검색 조건 생성 */ private List createKeywordMatchClauses(List keywords) { List clauses = new ArrayList<>(); @@ -162,15 +151,15 @@ private List createKeywordMatchClauses(List keywords) { for (String keyword : keywords) { if (StringUtils.hasText(keyword)) { // 서비스명 검색 - clauses.add(createSearchClause("serviceName", keyword, 5.0f)); + clauses.add(createSearchClause("serviceName", keyword, 3.5f)); // 요약 검색 - clauses.add(createSearchClause("summaryPurpose", keyword, 4.0f)); + clauses.add(createSearchClause("summaryPurpose", keyword, 3.5f)); // 서비스 분야 검색 - clauses.add(createSearchClause("serviceCategory", keyword, 4.5f)); + clauses.add(createSearchClause("serviceCategory", keyword, 5.0f)); // 특수그룹 검색 - clauses.add(createSearchClause("specialGroup", keyword, 4.0f)); + clauses.add(createSearchClause("specialGroup", keyword, 5.0f)); // 가족유형 검색 - clauses.add(createSearchClause("familyType", keyword, 4.0f)); + clauses.add(createSearchClause("familyType", keyword, 5.0f)); } } @@ -178,58 +167,22 @@ private List createKeywordMatchClauses(List keywords) { } /** - * 검색 조건 생성 헬퍼 메서드 - */ - private String createSearchClause(String path, String query, float boost) { - return """ - {text: { - query: '%s', - path: '%s', - score: {boost: {value: %.1f}} - }}""".formatted(query, path, boost); - } - - /** - * 소득수준 범위에 따른 가중치 부여 - */ - private void addIncomeLevelRangeBoosts(List clauses, String userIncomeLevel) { - switch (userIncomeLevel) { - case "HIGH": - clauses.add(createSearchClause("incomeLevel", "MIDDLE_HIGH", 2.5f)); - clauses.add(createSearchClause("incomeLevel", "MIDDLE", 2.0f)); - clauses.add(createSearchClause("incomeLevel", "MIDDLE_LOW", 1.5f)); - clauses.add(createSearchClause("incomeLevel", "LOW", 1.0f)); - break; - case "MIDDLE_HIGH": - clauses.add(createSearchClause("incomeLevel", "MIDDLE", 2.5f)); - clauses.add(createSearchClause("incomeLevel", "MIDDLE_LOW", 2.0f)); - clauses.add(createSearchClause("incomeLevel", "LOW", 1.5f)); - break; - case "MIDDLE": - clauses.add(createSearchClause("incomeLevel", "MIDDLE_LOW", 2.0f)); - clauses.add(createSearchClause("incomeLevel", "LOW", 1.5f)); - break; - case "MIDDLE_LOW": - clauses.add(createSearchClause("incomeLevel", "LOW", 2.0f)); - break; - default: - break; - } - } - - /** - * 사용자 기본 조건(성별, 나이) 필터링 + * 사용자 정보 기반 가산점 추가 */ - private String buildUserMustClause(String userGender, Integer userAge) { - List mustClauses = new ArrayList<>(); + private void addUserMatchBoosts( + List clauses, + String userGender, + Integer userAge, + String userIncomeLevel, + String userJob) { - // 성별 필수 조건 + // 성별 조건 (should로 변경) if (StringUtils.hasText(userGender)) { String genderField = "MALE".equalsIgnoreCase(userGender) ? "targetGenderMale" : "targetGenderFemale"; - // 대상 성별이 null이거나 Y인 서비스만 포함 - mustClauses.add(""" + // 대상 성별이 null이거나 Y인 서비스에 가산점 + clauses.add(""" { compound: { should: [ @@ -241,18 +194,19 @@ private String buildUserMustClause(String userGender, Integer userAge) { { equals: {path: "%s", value: "Y"} } - ] + ], + score: {boost: {value: 5.0}} } } """.formatted(genderField, genderField)); } - // 나이 필수 조건 + // 나이 조건 (should로 변경) if (userAge != null) { int age = userAge; - // 대상 나이 범위가 null이거나 사용자 나이를 포함하는 서비스만 포함 - mustClauses.add(""" + // 대상 나이 범위가 null이거나 사용자 나이를 포함하는 서비스에 가산점 + clauses.add(""" { compound: { should: [ @@ -264,12 +218,13 @@ private String buildUserMustClause(String userGender, Integer userAge) { { range: {path: "targetAgeStart", lte: %d} } - ] + ], + score: {boost: {value: 4.5}} } } """.formatted(age)); - mustClauses.add(""" + clauses.add(""" { compound: { should: [ @@ -281,19 +236,66 @@ private String buildUserMustClause(String userGender, Integer userAge) { { range: {path: "targetAgeEnd", gte: %d} } - ] + ], + score: {boost: {value: 4.5}} } } """.formatted(age)); } - // must 조건이 없으면 빈 문자열 반환 - if (mustClauses.isEmpty()) { - return ""; + // 직업 관련 조건 추가 + if (StringUtils.hasText(userJob)) { + clauses.add(createSearchClause("occupations", userJob, 3.0f)); + clauses.add(createSearchClause("businessTypes", userJob, 3.0f)); } - // must 조건이 있으면 문자열 형식으로 반환 - return ", must: [" + String.join(",", mustClauses) + "]"; + // 소득 수준 관련 조건 추가 + if (StringUtils.hasText(userIncomeLevel)) { + clauses.add(createSearchClause("incomeLevel", userIncomeLevel, 2.8f)); + clauses.add(createSearchClause("incomeLevel", "ANY", 1.0f)); + // 소득 수준 범위에 따른 가중치 추가 + addIncomeLevelRangeBoosts(clauses, userIncomeLevel); + } + } + + /** + * 검색 조건 생성 헬퍼 메서드 + */ + private String createSearchClause(String path, String query, float boost) { + return """ + {text: { + query: '%s', + path: '%s', + score: {boost: {value: %.1f}} + }}""".formatted(query, path, boost); + } + + /** + * 소득수준 범위에 따른 가중치 부여 + */ + private void addIncomeLevelRangeBoosts(List clauses, String userIncomeLevel) { + switch (userIncomeLevel) { + case "HIGH": + clauses.add(createSearchClause("incomeLevel", "MIDDLE_HIGH", 2.5f)); + clauses.add(createSearchClause("incomeLevel", "MIDDLE", 2.0f)); + clauses.add(createSearchClause("incomeLevel", "MIDDLE_LOW", 1.5f)); + clauses.add(createSearchClause("incomeLevel", "LOW", 1.0f)); + break; + case "MIDDLE_HIGH": + clauses.add(createSearchClause("incomeLevel", "MIDDLE", 2.5f)); + clauses.add(createSearchClause("incomeLevel", "MIDDLE_LOW", 2.0f)); + clauses.add(createSearchClause("incomeLevel", "LOW", 1.5f)); + break; + case "MIDDLE": + clauses.add(createSearchClause("incomeLevel", "MIDDLE_LOW", 2.0f)); + clauses.add(createSearchClause("incomeLevel", "LOW", 1.5f)); + break; + case "MIDDLE_LOW": + clauses.add(createSearchClause("incomeLevel", "LOW", 2.0f)); + break; + default: + break; + } } /** @@ -323,7 +325,7 @@ private String buildMatchCountStage(List keywords) { } /** - * 검색 결과 처리 + * 추천 결과 처리 */ private ServiceSearchResultDto processResults(AggregationResults results, int size) { List resultDocs = results.getMappedResults(); @@ -341,5 +343,4 @@ private ServiceSearchResultDto processResults(AggregationResults resul PageRequest.of(0, size) ); } -} - +} \ No newline at end of file diff --git a/src/main/java/com/hyetaekon/hyetaekon/publicservice/repository/mongodb/ServiceSearchClient.java b/src/main/java/com/hyetaekon/hyetaekon/publicservice/repository/mongodb/ServiceSearchClient.java index e8693bd..e21f506 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/publicservice/repository/mongodb/ServiceSearchClient.java +++ b/src/main/java/com/hyetaekon/hyetaekon/publicservice/repository/mongodb/ServiceSearchClient.java @@ -72,7 +72,6 @@ public ServiceSearchResultDto search(ServiceSearchCriteriaDto criteria) { private String buildSearchQuery(ServiceSearchCriteriaDto criteria) { List shouldClauses = new ArrayList<>(); - String mustClause = buildUserMustClause(criteria); // 검색어 관련 조건 추가 if (StringUtils.hasText(criteria.getSearchTerm())) { @@ -82,28 +81,24 @@ private String buildSearchQuery(ServiceSearchCriteriaDto criteria) { // 사용자 관심사 관련 조건 추가 if (criteria.getUserInterests() != null && !criteria.getUserInterests().isEmpty()) { for (String interest : criteria.getUserInterests()) { - shouldClauses.add(createSearchClause("serviceCategory", interest, 2.5f, 0)); - shouldClauses.add(createSearchClause("specialGroup", interest, 2.5f, 0)); - shouldClauses.add(createSearchClause("familyType", interest, 2.5f, 0)); + shouldClauses.add(createSearchClause("serviceCategory", interest, 3.5f, 0)); + shouldClauses.add(createSearchClause("specialGroup", interest, 3.5f, 0)); + shouldClauses.add(createSearchClause("familyType", interest, 3.5f, 0)); } } - // 소득 수준 일치 가산점 (should 조건) + // 사용자 정보(성별, 나이, 직업, 소득) 기반 조건 추가 addUserMatchBoosts(shouldClauses, criteria); - if (StringUtils.hasText(criteria.getUserIncomeLevel())) { - shouldClauses.add(createSearchClause("incomeLevel", criteria.getUserIncomeLevel(), 2.8f, 0)); - shouldClauses.add(createSearchClause("incomeLevel", "ANY", 1.0f, 0)); - } - String shouldClausesStr = shouldClauses.isEmpty() ? "[]" : "[" + String.join(",", shouldClauses) + "]"; - // must 조건이 있으면 추가, 없으면 생략 + // compound 쿼리 구성 (should 조건만 사용) String compoundQuery = """ compound: { - should: %s%s + should: %s, + minimumShouldMatch: 1 } - """.formatted(shouldClausesStr, mustClause); + """.formatted(shouldClausesStr); return """ { @@ -147,16 +142,14 @@ private void addSearchTermClauses(List clauses, String searchTerm) { }}""".formatted(searchTerm)); } - private String buildUserMustClause(ServiceSearchCriteriaDto criteria) { - List mustClauses = new ArrayList<>(); - - // 성별 필수 조건 + private void addUserMatchBoosts(List clauses, ServiceSearchCriteriaDto criteria) { + // 성별 조건 (should로 변경) if (StringUtils.hasText(criteria.getUserGender())) { String genderField = "MALE".equalsIgnoreCase(criteria.getUserGender()) ? "targetGenderMale" : "targetGenderFemale"; - // 대상 성별이 null이거나 Y인 서비스만 포함 - mustClauses.add(""" + // 대상 성별이 null이거나 Y인 서비스에 가산점 + clauses.add(""" { compound: { should: [ @@ -168,19 +161,19 @@ private String buildUserMustClause(ServiceSearchCriteriaDto criteria) { { equals: {path: "%s", value: "Y"} } - ] + ], + score: {boost: {value: 5.0}} } } """.formatted(genderField, genderField)); - } - // 나이 필수 조건 + // 나이 조건 (should로 변경) if (criteria.getUserAge() != null) { int age = criteria.getUserAge(); - // 대상 나이 범위가 null이거나 사용자 나이를 포함하는 서비스만 포함 - mustClauses.add(""" + // 대상 나이 범위가 null이거나 사용자 나이를 포함하는 서비스에 가산점 + clauses.add(""" { compound: { should: [ @@ -192,12 +185,13 @@ private String buildUserMustClause(ServiceSearchCriteriaDto criteria) { { range: {path: "targetAgeStart", lte: %d} } - ] + ], + score: {boost: {value: 4.5}} } } """.formatted(age)); - mustClauses.add(""" + clauses.add(""" { compound: { should: [ @@ -209,23 +203,13 @@ private String buildUserMustClause(ServiceSearchCriteriaDto criteria) { { range: {path: "targetAgeEnd", gte: %d} } - ] + ], + score: {boost: {value: 4.5}} } } """.formatted(age)); - - } - - // must 조건이 없으면 빈 문자열 반환 - if (mustClauses.isEmpty()) { - return ""; } - // must 조건이 있으면 문자열 형식으로 반환 - return ", must: [" + String.join(",", mustClauses) + "]"; - } - - private void addUserMatchBoosts(List clauses, ServiceSearchCriteriaDto criteria) { // 직업 일치 가산점 if (StringUtils.hasText(criteria.getUserJob())) { String userJob = criteria.getUserJob();