From 584bce8d9c5fed34d4b9ca33c2e717f01f34c1ef Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Fri, 12 Sep 2025 19:46:31 +0900 Subject: [PATCH 01/15] =?UTF-8?q?=E2=9C=A8feat:=20ElasticSearch=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 ++ src/main/resources/application-es.yml | 3 +++ src/main/resources/application.yml | 3 ++- 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/application-es.yml diff --git a/build.gradle b/build.gradle index 75a1c970..d44089fb 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,7 @@ repositories { dependencies { // ============= Spring Boot 스타터 ============= implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' @@ -34,6 +35,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-cache' implementation 'org.springframework.boot:spring-boot-starter-aop' implementation 'org.springframework.retry:spring-retry' + implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' // ============= 개발 도구 ============= compileOnly 'org.projectlombok:lombok' diff --git a/src/main/resources/application-es.yml b/src/main/resources/application-es.yml new file mode 100644 index 00000000..130e747d --- /dev/null +++ b/src/main/resources/application-es.yml @@ -0,0 +1,3 @@ +spring: + elasticsearch: + uris: http://localhost:9200 \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e9e6cdf5..462a9d4b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -10,4 +10,5 @@ spring: - jwt - s3 - email - - redis \ No newline at end of file + - redis + - es \ No newline at end of file From e99519789e64f9e3082f1844b849764178f5037c Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Fri, 12 Sep 2025 19:46:45 +0900 Subject: [PATCH 02/15] =?UTF-8?q?=E2=9C=A8feat:=20ElasticSearch=EB=A5=BC?= =?UTF-8?q?=20Docker=EB=A1=9C=20pull?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- compose-dev.yml | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/compose-dev.yml b/compose-dev.yml index 7881f6e8..cdd1f99d 100644 --- a/compose-dev.yml +++ b/compose-dev.yml @@ -31,10 +31,36 @@ services: timeout: 3s retries: 10 + elasticsearch: + image: elasticsearch:8.18.6 + environment: + - discovery.type=single-node # 단일 노드 모드 설정, 다른 노드 탐색하지 않아서 실행 시간 줄어듬 + - xpack.security.enabled=false # 개발용으로 보안 기능 비활성화 + - "ES_JAVA_OPTS=-Xms512m -Xmx512m" # 메모리 사용량을 512MB로 제한 + ports: + - "9200:9200" + - "9300:9300" + volumes: + - es-data-dev:/usr/share/elasticsearch/data + networks: + - novaminds-network + + kibana: + image: kibana:8.18.6 + environment: + - ELASTICSEARCH_HOSTS=http://elasticsearch:9200 + ports: + - "5601:5601" + depends_on: + - elasticsearch + networks: + - novaminds-network + networks: novaminds-network: driver: bridge volumes: #novaminds-mysql-data: - redis_data: \ No newline at end of file + redis_data: + es-data-dev: \ No newline at end of file From 57af20e414f7b00dc38fa947e309d8eb763989f1 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Fri, 12 Sep 2025 19:47:10 +0900 Subject: [PATCH 03/15] =?UTF-8?q?=E2=9C=A8feat:=20Jpa=EC=99=80=20ElasticSe?= =?UTF-8?q?arch=EC=9D=98=20=ED=99=98=EA=B2=BD=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/novaminds/gradproj/GradprojApplication.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/novaminds/gradproj/GradprojApplication.java b/src/main/java/novaminds/gradproj/GradprojApplication.java index fa6511cc..17198a43 100644 --- a/src/main/java/novaminds/gradproj/GradprojApplication.java +++ b/src/main/java/novaminds/gradproj/GradprojApplication.java @@ -3,18 +3,26 @@ import io.awspring.cloud.autoconfigure.s3.S3AutoConfiguration; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRepositoriesAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration; +import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.retry.annotation.EnableRetry; @SpringBootApplication( exclude = { RedisRepositoriesAutoConfiguration.class, - S3AutoConfiguration.class + S3AutoConfiguration.class, + ReactiveElasticsearchRepositoriesAutoConfiguration.class } ) @EnableJpaAuditing @EnableRetry +@EnableJpaRepositories( + basePackages = "novaminds.gradproj.domain" +) +@EnableElasticsearchRepositories(basePackages = "novaminds.gradproj.global.search.repository") public class GradprojApplication { public static void main(String[] args) { From 39912299bbdaf3897f14bb7450ae8089233100fe Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Fri, 12 Sep 2025 22:15:54 +0900 Subject: [PATCH 04/15] =?UTF-8?q?=E2=9C=A8feat:=20Nori=20Analysis=20?= =?UTF-8?q?=ED=94=8C=EB=9F=AC=EA=B7=B8=EC=9D=B8=20=EC=84=A4=EC=B9=98?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=B4=20Elastic=20Search=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=EB=A5=BC=20Dockerfile=EB=A1=9C=20=EC=84=A4?= =?UTF-8?q?=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile.elasticsearch | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Dockerfile.elasticsearch diff --git a/Dockerfile.elasticsearch b/Dockerfile.elasticsearch new file mode 100644 index 00000000..d96b326a --- /dev/null +++ b/Dockerfile.elasticsearch @@ -0,0 +1,2 @@ +FROM elasticsearch:8.18.6 +RUN bin/elasticsearch-plugin install analysis-nori \ No newline at end of file From 2d1aca52f0fb7f82d44e715fdf1f811df964af81 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Fri, 12 Sep 2025 22:16:10 +0900 Subject: [PATCH 05/15] =?UTF-8?q?=E2=9C=A8feat:=20Dockerfile=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=B9=8C=EB=93=9C=EB=90=9C=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- compose-dev.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/compose-dev.yml b/compose-dev.yml index cdd1f99d..82889223 100644 --- a/compose-dev.yml +++ b/compose-dev.yml @@ -32,7 +32,9 @@ services: retries: 10 elasticsearch: - image: elasticsearch:8.18.6 + build: + context: . + dockerfile: Dockerfile.elasticsearch environment: - discovery.type=single-node # 단일 노드 모드 설정, 다른 노드 탐색하지 않아서 실행 시간 줄어듬 - xpack.security.enabled=false # 개발용으로 보안 기능 비활성화 From f037cc250b09b7903f600905bb6bde3b4114beb3 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Fri, 12 Sep 2025 22:22:59 +0900 Subject: [PATCH 06/15] =?UTF-8?q?=E2=9C=A8feat:=20Nori=20analysis=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/es-setting/nori-setting.json | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/main/resources/es-setting/nori-setting.json diff --git a/src/main/resources/es-setting/nori-setting.json b/src/main/resources/es-setting/nori-setting.json new file mode 100644 index 00000000..bfeed7b4 --- /dev/null +++ b/src/main/resources/es-setting/nori-setting.json @@ -0,0 +1,20 @@ +{ + "analysis": { + "tokenizer": {}, + "filter": { + "edge_ngram_filter": { "type": "edge_ngram", "min_gram": 1, "max_gram": 20 } + }, + "analyzer": { + "korean_nori": { + "type": "custom", + "tokenizer": "nori_tokenizer", + "filter": [ "lowercase", "nori_readingform" ] + }, + "korean_autocomplete": { + "type": "custom", + "tokenizer": "nori_tokenizer", + "filter": [ "lowercase", "nori_readingform", "edge_ngram_filter" ] + } + } + } +} \ No newline at end of file From 69a724954366855ccd0d401971594ece7f370961 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Fri, 12 Sep 2025 22:23:14 +0900 Subject: [PATCH 07/15] =?UTF-8?q?=E2=9C=A8feat:=20ElasticSearch=20?= =?UTF-8?q?=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/controller/SearchController.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/main/java/novaminds/gradproj/global/search/web/controller/SearchController.java diff --git a/src/main/java/novaminds/gradproj/global/search/web/controller/SearchController.java b/src/main/java/novaminds/gradproj/global/search/web/controller/SearchController.java new file mode 100644 index 00000000..b6670928 --- /dev/null +++ b/src/main/java/novaminds/gradproj/global/search/web/controller/SearchController.java @@ -0,0 +1,33 @@ +package novaminds.gradproj.global.search.web.controller; + +import lombok.RequiredArgsConstructor; +import novaminds.gradproj.apiPayload.ApiResponse; +import novaminds.gradproj.global.search.service.IngredientIndexingService; +import novaminds.gradproj.global.search.service.SearchQueryService; +import novaminds.gradproj.global.search.web.dto.SearchResponseDTO; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/search") +public class SearchController { + + private final IngredientIndexingService ingredientIndexingService; + private final SearchQueryService searchQueryService; + + @PostMapping("/admin/reindex") + public ApiResponse reindexIngredients() { + ingredientIndexingService.indexIngredients(); + return ApiResponse.onSuccess("인덱싱이 성공적으로 완료되었습니다."); + } + + @GetMapping("/ingredients") + public ApiResponse searchIngredients( + @RequestParam("keyword") String keyword, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size + ) { + SearchResponseDTO.IngredientSearchList searchResults = searchQueryService.searchIngredients(keyword, page, size); + return ApiResponse.onSuccess(searchResults); + } +} From e7d0573943cbd98503acf7c4aa4b4016148ef494 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Fri, 12 Sep 2025 22:23:39 +0900 Subject: [PATCH 08/15] =?UTF-8?q?=E2=9C=A8feat:=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EC=A0=84=EC=9A=A9=20=EC=BB=A8=EB=B2=84=ED=84=B0=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../search/converter/SearchConverter.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/main/java/novaminds/gradproj/global/search/converter/SearchConverter.java diff --git a/src/main/java/novaminds/gradproj/global/search/converter/SearchConverter.java b/src/main/java/novaminds/gradproj/global/search/converter/SearchConverter.java new file mode 100644 index 00000000..449f7d8b --- /dev/null +++ b/src/main/java/novaminds/gradproj/global/search/converter/SearchConverter.java @@ -0,0 +1,28 @@ +package novaminds.gradproj.global.search.converter; + +import novaminds.gradproj.global.search.IngredientDocument; +import novaminds.gradproj.global.search.web.dto.SearchResponseDTO; + +import java.util.List; + +public class SearchConverter { + + public static SearchResponseDTO.IngredientSearchResult toIngredientSearchResultDTO(IngredientDocument document) { + return SearchResponseDTO.IngredientSearchResult.builder() + .id(document.getId()) + .name(document.getIngredientName()) + .category(document.getCategoryName()) + .imageUrl(document.getImageUrl()) + .build(); + } + + public static SearchResponseDTO.IngredientSearchList toIngredientSearchListDTO(List documents) { + List results = documents.stream() + .map(SearchConverter::toIngredientSearchResultDTO) + .toList(); + + return SearchResponseDTO.IngredientSearchList.builder() + .results(results) + .build(); + } +} From 031c70a65af4d1340f8111b0fae9ec37ce46c769 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Fri, 12 Sep 2025 22:24:26 +0900 Subject: [PATCH 09/15] =?UTF-8?q?=E2=9C=A8feat:=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EC=A0=84=EC=9A=A9=20DTO=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../search/web/dto/SearchResponseDTO.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/main/java/novaminds/gradproj/global/search/web/dto/SearchResponseDTO.java diff --git a/src/main/java/novaminds/gradproj/global/search/web/dto/SearchResponseDTO.java b/src/main/java/novaminds/gradproj/global/search/web/dto/SearchResponseDTO.java new file mode 100644 index 00000000..0b937ad3 --- /dev/null +++ b/src/main/java/novaminds/gradproj/global/search/web/dto/SearchResponseDTO.java @@ -0,0 +1,24 @@ +package novaminds.gradproj.global.search.web.dto; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +public class SearchResponseDTO { + + @Getter + @Builder + public static class IngredientSearchList { + private List results; + } + + @Getter + @Builder + public static class IngredientSearchResult { + private Long id; + private String name; + private String category; + private String imageUrl; + } +} From 38a743d79f1174017087aa1a5a08f8413a245e83 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Fri, 12 Sep 2025 22:25:24 +0900 Subject: [PATCH 10/15] =?UTF-8?q?=E2=9C=A8feat:=20ElasticSearch=20?= =?UTF-8?q?=EC=A0=84=EC=9A=A9=20=EC=A0=80=EC=9E=A5=EC=86=8C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../search/repository/IngredientSearchRepository.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/main/java/novaminds/gradproj/global/search/repository/IngredientSearchRepository.java diff --git a/src/main/java/novaminds/gradproj/global/search/repository/IngredientSearchRepository.java b/src/main/java/novaminds/gradproj/global/search/repository/IngredientSearchRepository.java new file mode 100644 index 00000000..55ad7af2 --- /dev/null +++ b/src/main/java/novaminds/gradproj/global/search/repository/IngredientSearchRepository.java @@ -0,0 +1,7 @@ +package novaminds.gradproj.global.search.repository; + +import novaminds.gradproj.global.search.IngredientDocument; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; + +public interface IngredientSearchRepository extends ElasticsearchRepository { +} \ No newline at end of file From 09caa77960f8609fd22461ee1c9a87b894cac6d6 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Fri, 12 Sep 2025 22:25:38 +0900 Subject: [PATCH 11/15] =?UTF-8?q?=E2=9C=A8feat:=20ElasticSearch=EC=97=90?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5=ED=95=98=EB=8A=94=20=EC=9A=A9=EB=8F=84?= =?UTF-8?q?=EC=9D=98=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/IngredientIndexingService.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/main/java/novaminds/gradproj/global/search/service/IngredientIndexingService.java diff --git a/src/main/java/novaminds/gradproj/global/search/service/IngredientIndexingService.java b/src/main/java/novaminds/gradproj/global/search/service/IngredientIndexingService.java new file mode 100644 index 00000000..c2237f55 --- /dev/null +++ b/src/main/java/novaminds/gradproj/global/search/service/IngredientIndexingService.java @@ -0,0 +1,49 @@ +package novaminds.gradproj.global.search.service; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import novaminds.gradproj.domain.ingredient.entity.Ingredient; +import novaminds.gradproj.domain.ingredient.repository.IngredientRepository; +import novaminds.gradproj.global.search.IngredientDocument; +import novaminds.gradproj.global.search.repository.IngredientSearchRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class IngredientIndexingService { + + private final IngredientRepository ingredientRepository; // DB에서 데이터를 읽어올 JpaRepository + private final IngredientSearchRepository ingredientSearchRepository; // Elasticsearch에 데이터를 저장할 Repository + + @Transactional(readOnly = true) + public void indexIngredients() { + log.info(">>>> Ingredient 데이터 인덱싱 시작..."); + + // 1. DB에서 모든 재료 데이터를 조회 + List allIngredients = ingredientRepository.findAll(); + if (allIngredients.isEmpty()) { + log.info(">>>> 인덱싱할 재료 데이터가 없습니다."); + return; + } + + // 2. Ingredient 엔티티를 IngredientDocument로 변환 + List ingredientDocuments = allIngredients.stream() + .map(ingredient -> IngredientDocument.builder() + .id(ingredient.getId()) + .ingredientName(ingredient.getIngredientName()) + .categoryName(ingredient.getIngredientCategory().getIngredientCategoryName()) + .imageUrl(ingredient.getImageUrl()) + .build()) + .toList(); + + // 3. Elasticsearch에 변환된 데이터를 저장(인덱싱) + ingredientSearchRepository.saveAll(ingredientDocuments); + + log.info(">>>> Ingredient 데이터 인덱싱 완료 ({}개)", ingredientDocuments.size()); + } +} From 224e149dc37a6ae6cc17ac673bae7f1831674164 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Fri, 12 Sep 2025 22:25:49 +0900 Subject: [PATCH 12/15] =?UTF-8?q?=E2=9C=A8feat:=20ElasticSearch=EC=97=90?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5=ED=95=A0=20=EC=9D=B8=EB=8D=B1=EC=8A=A4=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/search/IngredientDocument.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/main/java/novaminds/gradproj/global/search/IngredientDocument.java diff --git a/src/main/java/novaminds/gradproj/global/search/IngredientDocument.java b/src/main/java/novaminds/gradproj/global/search/IngredientDocument.java new file mode 100644 index 00000000..6df1ceec --- /dev/null +++ b/src/main/java/novaminds/gradproj/global/search/IngredientDocument.java @@ -0,0 +1,35 @@ +package novaminds.gradproj.global.search; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.*; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Document(indexName = "ingredients") // ingredients 라는 이름의 인덱스(서랍장)에 저장 +@Setting(settingPath = "es-setting/nori-setting.json") // 한글 형태소 분석기 설정 추가 +public class IngredientDocument { + + @Id + private Long id; // 원본 DB의 Ingredient ID + + @MultiField( + mainField = @Field(type = FieldType.Text, name = "ingredient_name", analyzer = "korean_nori"), + otherFields = { + @InnerField(suffix = "auto", type = FieldType.Text, analyzer = "korean_autocomplete"), + @InnerField(suffix = "kw", type = FieldType.Keyword) + } + ) + private String ingredientName; + + @Field(type = FieldType.Keyword, name = "category_name") // 카테고리 이름은 정확히 일치해야 하므로 Keyword 타입 사용 + private String categoryName; + + @Field(type = FieldType.Keyword, name = "image_url", index = false) // 이미지 URL은 검색 대상이 아니므로 index=false + private String imageUrl; +} From 705eae0dda932b9b6c621968e62dfadbfead19e8 Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Fri, 12 Sep 2025 22:26:41 +0900 Subject: [PATCH 13/15] =?UTF-8?q?=E2=9C=A8feat:=20ElasticSearch=20?= =?UTF-8?q?=EC=9D=B8=EB=8D=B1=EC=8A=A4=20=EC=B4=88=EA=B8=B0=ED=99=94=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/search/EsIndexBootstrap.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/main/java/novaminds/gradproj/global/search/EsIndexBootstrap.java diff --git a/src/main/java/novaminds/gradproj/global/search/EsIndexBootstrap.java b/src/main/java/novaminds/gradproj/global/search/EsIndexBootstrap.java new file mode 100644 index 00000000..a541f4a3 --- /dev/null +++ b/src/main/java/novaminds/gradproj/global/search/EsIndexBootstrap.java @@ -0,0 +1,22 @@ +package novaminds.gradproj.global.search; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class EsIndexBootstrap { + private final ElasticsearchOperations operations; + + @EventListener(ApplicationReadyEvent.class) + public void init() { + var idx = operations.indexOps(IngredientDocument.class); + if (!idx.exists()) { + idx.create(); // @Setting(nori-setting.json) 반영 + idx.putMapping(idx.createMapping(IngredientDocument.class)); // @MultiField 매핑 반영 + } + } +} \ No newline at end of file From 7c4c5d5f0a4636ec8aceb267751d4b059b29e1aa Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Sat, 13 Sep 2025 14:57:18 +0900 Subject: [PATCH 14/15] =?UTF-8?q?=E2=9C=A8feat:=20ElasticSearch=20?= =?UTF-8?q?=ED=99=9C=EC=9A=A9=ED=95=B4=EC=84=9C=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../search/service/SearchQueryService.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/main/java/novaminds/gradproj/global/search/service/SearchQueryService.java diff --git a/src/main/java/novaminds/gradproj/global/search/service/SearchQueryService.java b/src/main/java/novaminds/gradproj/global/search/service/SearchQueryService.java new file mode 100644 index 00000000..c8843e6e --- /dev/null +++ b/src/main/java/novaminds/gradproj/global/search/service/SearchQueryService.java @@ -0,0 +1,71 @@ +package novaminds.gradproj.global.search.service; + +import lombok.RequiredArgsConstructor; +import novaminds.gradproj.global.search.IngredientDocument; +import novaminds.gradproj.global.search.converter.SearchConverter; +import novaminds.gradproj.global.search.web.dto.SearchResponseDTO; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.elasticsearch.client.elc.NativeQuery; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.SearchHit; +import org.springframework.data.elasticsearch.core.SearchHits; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class SearchQueryService { + + private final ElasticsearchOperations operations; + + public SearchResponseDTO.IngredientSearchList searchIngredients(String keyword, int page, int size) { + // 1. 검색어가 비어있으면 빈 결과 반환 + if (keyword == null || keyword.isBlank()) { + return SearchConverter.toIngredientSearchListDTO(List.of()); + } + + // 2. 다중 필드 검색 쿼리 생성 (bool + should 조합) + NativeQuery query = NativeQuery.builder() + .withQuery(q -> q + .bool(b -> b + // 2-1) Text 필드: 형태소 분석 + 퍼지 매칭 (오타 허용) + .should(s -> s + .match(m -> m + .field("ingredient_name") + .query(keyword) + .fuzziness("AUTO") // 오타 허용 + .boost(3.0f) // 가중치 3배 + ) + ) + // 2-2) 자동완성 필드: 부분 일치 검색 + .should(s -> s + .match(m -> m + .field("ingredient_name.auto") + .query(keyword) + .boost(2.0f) // 가중치 2배 + ) + ) + // 2-3) 키워드 필드: 정확한 일치 (가장 높은 점수) + .should(s -> s + .term(t -> t + .field("ingredient_name.kw") + .value(keyword) + .boost(50.0f) // 가중치 50배 + ) + ) + .minimumShouldMatch("1") // 최소 1개 조건은 만족해야 함 + ) + ) + .withPageable(PageRequest.of(page, size)) // 페이징 설정 + .build(); + + // 3. 검색 실행 및 결과 추출 + SearchHits hits = operations.search(query, IngredientDocument.class); + List docs = hits.getSearchHits().stream() + .map(SearchHit::getContent).toList(); + + // 4. DTO 변환 후 반환 + return SearchConverter.toIngredientSearchListDTO(docs); + } +} \ No newline at end of file From 244a7def0ed435229209f2733db955bffd1397ca Mon Sep 17 00:00:00 2001 From: MODUGGAGI Date: Wed, 24 Sep 2025 13:19:17 +0900 Subject: [PATCH 15/15] =?UTF-8?q?=E2=9C=A8feat:=20ElasticSearch=20?= =?UTF-8?q?=ED=99=9C=EC=9A=A9=ED=95=B4=EC=84=9C=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/search/IngredientDocument.java | 13 +++---- .../service/IngredientIndexingService.java | 2 +- .../search/service/SearchQueryService.java | 38 +++---------------- .../resources/es-setting/nori-setting.json | 11 ++---- 4 files changed, 17 insertions(+), 47 deletions(-) diff --git a/src/main/java/novaminds/gradproj/global/search/IngredientDocument.java b/src/main/java/novaminds/gradproj/global/search/IngredientDocument.java index 6df1ceec..de44e747 100644 --- a/src/main/java/novaminds/gradproj/global/search/IngredientDocument.java +++ b/src/main/java/novaminds/gradproj/global/search/IngredientDocument.java @@ -18,15 +18,14 @@ public class IngredientDocument { @Id private Long id; // 원본 DB의 Ingredient ID - @MultiField( - mainField = @Field(type = FieldType.Text, name = "ingredient_name", analyzer = "korean_nori"), - otherFields = { - @InnerField(suffix = "auto", type = FieldType.Text, analyzer = "korean_autocomplete"), - @InnerField(suffix = "kw", type = FieldType.Keyword) - } - ) + // 모든 검색을 책임질 단 하나의 메인 필드 + @Field(type = FieldType.Text, analyzer = "korean_unified_analyzer") private String ingredientName; + // 정확 일치 및 정렬을 위한 키워드 필드 + @Field(type = FieldType.Keyword) + private String ingredientNameKw; + @Field(type = FieldType.Keyword, name = "category_name") // 카테고리 이름은 정확히 일치해야 하므로 Keyword 타입 사용 private String categoryName; diff --git a/src/main/java/novaminds/gradproj/global/search/service/IngredientIndexingService.java b/src/main/java/novaminds/gradproj/global/search/service/IngredientIndexingService.java index c2237f55..85481b6a 100644 --- a/src/main/java/novaminds/gradproj/global/search/service/IngredientIndexingService.java +++ b/src/main/java/novaminds/gradproj/global/search/service/IngredientIndexingService.java @@ -1,6 +1,5 @@ package novaminds.gradproj.global.search.service; -import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import novaminds.gradproj.domain.ingredient.entity.Ingredient; @@ -36,6 +35,7 @@ public void indexIngredients() { .map(ingredient -> IngredientDocument.builder() .id(ingredient.getId()) .ingredientName(ingredient.getIngredientName()) + .ingredientNameKw(ingredient.getIngredientName()) .categoryName(ingredient.getIngredientCategory().getIngredientCategoryName()) .imageUrl(ingredient.getImageUrl()) .build()) diff --git a/src/main/java/novaminds/gradproj/global/search/service/SearchQueryService.java b/src/main/java/novaminds/gradproj/global/search/service/SearchQueryService.java index c8843e6e..a78a42bb 100644 --- a/src/main/java/novaminds/gradproj/global/search/service/SearchQueryService.java +++ b/src/main/java/novaminds/gradproj/global/search/service/SearchQueryService.java @@ -20,52 +20,26 @@ public class SearchQueryService { private final ElasticsearchOperations operations; public SearchResponseDTO.IngredientSearchList searchIngredients(String keyword, int page, int size) { - // 1. 검색어가 비어있으면 빈 결과 반환 if (keyword == null || keyword.isBlank()) { return SearchConverter.toIngredientSearchListDTO(List.of()); } - // 2. 다중 필드 검색 쿼리 생성 (bool + should 조합) NativeQuery query = NativeQuery.builder() .withQuery(q -> q - .bool(b -> b - // 2-1) Text 필드: 형태소 분석 + 퍼지 매칭 (오타 허용) - .should(s -> s - .match(m -> m - .field("ingredient_name") - .query(keyword) - .fuzziness("AUTO") // 오타 허용 - .boost(3.0f) // 가중치 3배 - ) - ) - // 2-2) 자동완성 필드: 부분 일치 검색 - .should(s -> s - .match(m -> m - .field("ingredient_name.auto") - .query(keyword) - .boost(2.0f) // 가중치 2배 - ) - ) - // 2-3) 키워드 필드: 정확한 일치 (가장 높은 점수) - .should(s -> s - .term(t -> t - .field("ingredient_name.kw") - .value(keyword) - .boost(50.0f) // 가중치 50배 - ) - ) - .minimumShouldMatch("1") // 최소 1개 조건은 만족해야 함 + .multiMatch(mm -> mm + .query(keyword) + .fields("ingredientName", "ingredientNameKw") + .fuzziness("AUTO") ) ) - .withPageable(PageRequest.of(page, size)) // 페이징 설정 + .withMinScore(2.0f) + .withPageable(PageRequest.of(page, size)) .build(); - // 3. 검색 실행 및 결과 추출 SearchHits hits = operations.search(query, IngredientDocument.class); List docs = hits.getSearchHits().stream() .map(SearchHit::getContent).toList(); - // 4. DTO 변환 후 반환 return SearchConverter.toIngredientSearchListDTO(docs); } } \ No newline at end of file diff --git a/src/main/resources/es-setting/nori-setting.json b/src/main/resources/es-setting/nori-setting.json index bfeed7b4..9f1410ae 100644 --- a/src/main/resources/es-setting/nori-setting.json +++ b/src/main/resources/es-setting/nori-setting.json @@ -1,16 +1,13 @@ { + "index": { + "max_ngram_diff": 20 + }, "analysis": { - "tokenizer": {}, "filter": { "edge_ngram_filter": { "type": "edge_ngram", "min_gram": 1, "max_gram": 20 } }, "analyzer": { - "korean_nori": { - "type": "custom", - "tokenizer": "nori_tokenizer", - "filter": [ "lowercase", "nori_readingform" ] - }, - "korean_autocomplete": { + "korean_unified_analyzer": { "type": "custom", "tokenizer": "nori_tokenizer", "filter": [ "lowercase", "nori_readingform", "edge_ngram_filter" ]