Skip to content
Merged
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
Expand Up @@ -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"
};

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<Page<PublicServiceListResponseDto>> searchServices(
@RequestParam(name = "searchTerm", required = false, defaultValue = "") String searchTerm,
@RequestParam(name = "page", defaultValue = "0") @Min(0) int page,
Expand All @@ -48,7 +51,18 @@ public ResponseEntity<Page<PublicServiceListResponseDto>> searchServices(
}
}

// μžλ™μ™„μ„± API
// κ²Œμ‹œκΈ€ 제λͺ© 검색(톡합검색)
@GetMapping("/posts")
public ResponseEntity<Page<PostListResponseDto>> 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<List<String>> getAutocompleteResults(
@RequestParam(name = "word") String word
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,29 +119,18 @@ private String buildSearchQuery(
// should 쑰건 (κ°€μ€‘μΉ˜κ°€ 적용된 ν‚€μ›Œλ“œ 쑰건)
List<String> 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 """
{
Expand All @@ -153,7 +142,7 @@ private String buildSearchQuery(
}

/**
* ν‚€μ›Œλ“œ 기반 검색 쑰건 생성 (단일 κ°€μ€‘μΉ˜ 적용)
* ν‚€μ›Œλ“œ 기반 검색 쑰건 생성
*/
private List<String> createKeywordMatchClauses(List<String> keywords) {
List<String> clauses = new ArrayList<>();
Expand All @@ -162,74 +151,38 @@ private List<String> createKeywordMatchClauses(List<String> 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));
}
}

return clauses;
}

/**
* 검색 쑰건 생성 헬퍼 λ©”μ„œλ“œ
*/
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<String> 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<String> mustClauses = new ArrayList<>();
private void addUserMatchBoosts(
List<String> 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: [
Expand All @@ -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: [
Expand All @@ -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: [
Expand All @@ -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<String> 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;
}
}

/**
Expand Down Expand Up @@ -323,7 +325,7 @@ private String buildMatchCountStage(List<String> keywords) {
}

/**
* 검색 κ²°κ³Ό 처리
* μΆ”μ²œ κ²°κ³Ό 처리
*/
private ServiceSearchResultDto processResults(AggregationResults<Document> results, int size) {
List<Document> resultDocs = results.getMappedResults();
Expand All @@ -341,5 +343,4 @@ private ServiceSearchResultDto processResults(AggregationResults<Document> resul
PageRequest.of(0, size)
);
}
}

}
Loading