Skip to content

Commit acc71d4

Browse files
authored
Merge pull request #96 from TU-NEBULA/refactor/SCRUM-235
SCRUM 235 : 연관검색어 기능 수정
2 parents 8ca2339 + 9b41831 commit acc71d4

22 files changed

Lines changed: 628 additions & 262 deletions

build.gradle

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,21 @@ dependencies {
5151
// WebFlux
5252
implementation 'org.springframework.boot:spring-boot-starter-webflux'
5353

54-
// Elasticsearch
55-
implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
56-
implementation 'co.elastic.clients:elasticsearch-java:8.13.4'
54+
// OpenSearch 클라이언트 (Elasticsearch 의존성 제거)
55+
implementation 'org.opensearch.client:opensearch-java:2.19.0'
56+
implementation 'org.opensearch.client:opensearch-rest-client:2.19.0'
57+
implementation 'com.fasterxml.jackson.core:jackson-databind'
58+
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
5759

58-
// Prometheus & Actuator
59-
implementation 'org.springframework.boot:spring-boot-starter-actuator'
60-
implementation 'io.micrometer:micrometer-registry-prometheus'
60+
// bucket4j
61+
implementation 'com.bucket4j:bucket4j_jdk17-core:8.14.0'
62+
63+
// guava
64+
implementation 'com.google.guava:guava:32.1.3-jre'
65+
66+
// Redis
67+
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
68+
implementation 'org.springframework.boot:spring-boot-starter-cache'
6169

6270
compileOnly 'org.projectlombok:lombok'
6371
developmentOnly 'org.springframework.boot:spring-boot-devtools'
@@ -67,8 +75,6 @@ dependencies {
6775
annotationProcessor 'org.projectlombok:lombok'
6876
testImplementation 'org.springframework.boot:spring-boot-starter-test'
6977
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
70-
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.0'
71-
implementation 'org.springframework.boot:spring-boot-starter-webflux'
7278
implementation 'org.springframework.boot:spring-boot-starter-amqp'
7379
}
7480

src/main/java/com/team_nebula/nebula/domain/category/service/CategoryCommandService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public interface CategoryCommandService {
1313

1414
public CreateCategoryResponseDTO createCategory(CreateCategoryRequestDTO request, Long userId);
1515

16-
public void linkStarToCategory(Star star, String categoryName);
16+
public void linkStarToCategory(Star star, String categoryName, Long userId);
1717

1818
public String linkStarToCategoryAndGetName(Star star, String categoryName);
1919

src/main/java/com/team_nebula/nebula/domain/category/service/CategoryCommandServiceImpl.java

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,20 @@ public CreateCategoryResponseDTO createCategory(CreateCategoryRequestDTO request
5858
}
5959

6060
@Override
61-
public void linkStarToCategory(Star star, String categoryName){
61+
public void linkStarToCategory(Star star, String categoryName, Long userId){
6262
Category category = categoryRepository.findByName(categoryName)
63-
.orElseThrow(() -> new GeneralException(ErrorStatus._CATEGORY_NOT_FOUND));
63+
.orElseGet(() -> {
64+
if (categoryName.equalsIgnoreCase("basic")) {
65+
Category basicCategory = Category.builder()
66+
.name("basic")
67+
.build();
68+
69+
70+
return categoryRepository.save(basicCategory);
71+
} else {
72+
throw new GeneralException(ErrorStatus._CATEGORY_NOT_FOUND);
73+
}
74+
});
6475

6576
category.getStars().add(star);
6677
categoryRepository.save(category);
@@ -97,9 +108,6 @@ public UpdateCategoryOneResponseDTO updateCategory(UpdateCategoryOneRequestDTO r
97108

98109
@Override
99110
public DeleteCategoryResponseDTO deleteCategory(Long userId, UUID categoryId){
100-
UserNode userNode = userNodeRepository.findByUserId(userId)
101-
.orElseThrow(() -> new GeneralException(ErrorStatus._USER_NOT_FOUND));
102-
103111
Category category = categoryRepository.findById(categoryId)
104112
.orElseThrow(() -> new GeneralException(ErrorStatus._CATEGORY_NOT_FOUND));
105113

src/main/java/com/team_nebula/nebula/domain/star/api/StarV2Controller.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,19 @@
1515
import io.swagger.v3.oas.annotations.Operation;
1616
import io.swagger.v3.oas.annotations.tags.Tag;
1717
import lombok.RequiredArgsConstructor;
18+
import org.springframework.beans.factory.annotation.Autowired;
19+
import org.springframework.cache.Cache;
20+
import org.springframework.cache.CacheManager;
21+
import org.springframework.cache.annotation.Cacheable;
22+
import org.springframework.data.redis.connection.RedisConnectionFactory;
23+
import org.springframework.data.redis.core.RedisTemplate;
24+
import org.springframework.data.redis.serializer.StringRedisSerializer;
1825
import org.springframework.http.MediaType;
1926
import org.springframework.web.bind.annotation.*;
2027
import org.springframework.web.multipart.MultipartFile;
2128

22-
import java.util.Collections;
23-
import java.util.List;
24-
import java.util.UUID;
29+
import java.lang.reflect.Field;
30+
import java.util.*;
2531

2632
@RestController
2733
@RequiredArgsConstructor
@@ -84,7 +90,7 @@ public ApiResponse<List<String>> getAutoComplete(
8490
@AuthUser Long userId,
8591
@RequestParam("q") String query
8692
) {
87-
List<String> suggestions = starQueryService.getAutoComplete(query, userId);
93+
List<String> suggestions = starQueryService.getAutoComplete(query, userId, 5);
8894
return ApiResponse.onSuccess(suggestions);
8995
}
9096
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package com.team_nebula.nebula.domain.star.search.batch;
2+
3+
import org.opensearch.client.opensearch.OpenSearchClient;
4+
import org.opensearch.client.opensearch.core.BulkRequest;
5+
import org.opensearch.client.opensearch.core.BulkResponse;
6+
import org.opensearch.client.opensearch.core.bulk.IndexOperation;
7+
import com.google.common.collect.Lists;
8+
import com.team_nebula.nebula.domain.star.search.document.StarSearchDocument;
9+
import lombok.RequiredArgsConstructor;
10+
import lombok.extern.slf4j.Slf4j;
11+
import org.springframework.scheduling.annotation.Async;
12+
import org.springframework.stereotype.Service;
13+
14+
import java.util.List;
15+
import java.util.concurrent.CompletableFuture;
16+
17+
@Service
18+
@RequiredArgsConstructor
19+
@Slf4j
20+
public class ElasticsearchBatchService {
21+
22+
private final OpenSearchClient openSearchClient;
23+
24+
@Async("batchExecutor")
25+
public CompletableFuture<Void> processBatchAsync(List<StarSearchDocument> documents) {
26+
try {
27+
batchIndexDocuments(documents);
28+
return CompletableFuture.completedFuture(null);
29+
} catch (Exception e) {
30+
log.error("Async batch processing failed", e);
31+
return CompletableFuture.failedFuture(e);
32+
}
33+
}
34+
35+
public void batchIndexDocuments(List<StarSearchDocument> documents) {
36+
if (documents.isEmpty()) return;
37+
38+
try {
39+
List<List<StarSearchDocument>> batches = Lists.partition(documents, 100);
40+
for (List<StarSearchDocument> batch : batches) {
41+
processBatch(batch);
42+
Thread.sleep(50);
43+
}
44+
} catch (Exception e) {
45+
log.error("Batch indexing failed", e);
46+
}
47+
}
48+
49+
private void processBatch(List<StarSearchDocument> batch) throws Exception {
50+
BulkRequest.Builder bulkBuilder = new BulkRequest.Builder();
51+
for (StarSearchDocument doc : batch) {
52+
bulkBuilder.operations(op -> op
53+
.index(IndexOperation.of(i -> i
54+
.index("star_search")
55+
.id(doc.getId())
56+
.document(doc)
57+
))
58+
);
59+
}
60+
61+
BulkResponse response = openSearchClient.bulk(bulkBuilder.build());
62+
if (response.errors()) {
63+
log.error("Bulk indexing failed for some documents in batch");
64+
response.items().forEach(item -> {
65+
if (item.error() != null) {
66+
log.error("Failed to index document {}: {}", item.id(), item.error().reason());
67+
}
68+
});
69+
} else {
70+
log.info("Successfully indexed {} documents", batch.size());
71+
}
72+
}
73+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.team_nebula.nebula.domain.star.search.cache;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.springframework.data.redis.core.Cursor;
6+
import org.springframework.data.redis.core.RedisTemplate;
7+
import org.springframework.data.redis.core.ScanOptions;
8+
import org.springframework.stereotype.Service;
9+
10+
11+
@Service
12+
@RequiredArgsConstructor
13+
@Slf4j
14+
public class CacheInvalidationService {
15+
16+
private final RedisTemplate<String, Object> redisTemplate;
17+
18+
public void clearUserCache(Long userId) {
19+
clearCacheByPattern("autocomplete_service::*:" + userId + ":*");
20+
}
21+
22+
private void clearCacheByPattern(String pattern) {
23+
ScanOptions scanOptions = ScanOptions.scanOptions()
24+
.match(pattern)
25+
.count(100)
26+
.build();
27+
28+
try (Cursor<byte[]> cursor = redisTemplate.getConnectionFactory()
29+
.getConnection()
30+
.scan(scanOptions)) {
31+
32+
int deleteCount = 0;
33+
while (cursor.hasNext()) {
34+
byte[] key = cursor.next();
35+
redisTemplate.delete(new String(key));
36+
deleteCount++;
37+
}
38+
log.info("Cleared {} keys matching pattern '{}'", deleteCount, pattern);
39+
40+
} catch (Exception e) {
41+
log.error("Error clearing Redis cache with pattern {}", pattern, e);
42+
}
43+
}
44+
}

src/main/java/com/team_nebula/nebula/domain/star/search/document/StarSearchDocument.java

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,58 +2,28 @@
22

33
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
44
import lombok.*;
5-
import org.springframework.data.elasticsearch.annotations.Document;
6-
import org.springframework.data.elasticsearch.annotations.Field;
7-
import org.springframework.data.elasticsearch.annotations.FieldType;
8-
import org.springframework.data.annotation.Id;
95

106
import java.util.List;
117

128
@JsonIgnoreProperties(ignoreUnknown = true)
13-
@Document(indexName = "star_search")
149
@Getter
1510
@ToString
1611
@NoArgsConstructor
1712
@AllArgsConstructor
1813
@Builder
1914
public class StarSearchDocument {
2015

21-
@Id
2216
private String id;
23-
24-
@Field(type = FieldType.Long)
2517
private Long userId;
26-
27-
@Field(type = FieldType.Text, analyzer = "standard")
2818
private String allContent;
29-
30-
@Field(type = FieldType.Text, analyzer = "standard")
3119
private String title;
32-
33-
@Field(type = FieldType.Text, analyzer = "standard")
3420
private String categoryName;
35-
36-
@Field(type = FieldType.Text, analyzer = "standard")
3721
private String summaryAI;
38-
39-
@Field(type = FieldType.Text, analyzer = "standard")
4022
private String userMemo;
41-
42-
@Field(type = FieldType.Keyword)
4323
private List<String> keywords;
44-
45-
@Field(type = FieldType.Keyword, index = false)
4624
private String siteUrl;
47-
48-
@Field(type = FieldType.Integer, index = false)
4925
private Integer views;
50-
51-
@Field(type = FieldType.Keyword, index = false)
5226
private String lastAccessedAt;
53-
54-
@Field(type = FieldType.Keyword, index = false)
5527
private String thumbnailUrl;
56-
57-
@Field(type = FieldType.Keyword, index = false)
5828
private String faviconUrl;
5929
}

src/main/java/com/team_nebula/nebula/domain/star/search/event/StarDeletedEvent.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77
@AllArgsConstructor
88
public class StarDeletedEvent {
99
private final String starId;
10+
private final Long userId;
1011
}

0 commit comments

Comments
 (0)