Conversation
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughAdds Elasticsearch and Kibana to development infra, integrates Spring Data Elasticsearch, defines an IngredientDocument and repository, bootstraps index on startup, provides indexing and search services (with Korean nori analyzer), REST endpoints for reindex/search, and profile/config for local ES. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant C as Client
participant SC as SearchController
participant SQS as SearchQueryService
participant ESOp as ElasticsearchOperations
participant ES as Elasticsearch
rect rgba(230,245,255,0.6)
note over C,SC: Ingredient Search Flow
C->>SC: GET /api/search/ingredients?keyword=...&page=&size=
alt keyword blank
SC-->>C: ApiResponse(empty results)
else keyword present
SC->>SQS: searchIngredients(keyword,page,size)
SQS->>ESOp: build NativeQuery (multi-match + fuzziness + min_score + pageable)
ESOp->>ES: _search
ES-->>ESOp: hits
ESOp-->>SQS: IngredientDocument list
SQS-->>SC: DTO (results)
SC-->>C: ApiResponse(results)
end
end
sequenceDiagram
autonumber
participant Admin as Admin Client
participant SC as SearchController
participant IIS as IngredientIndexingService
participant IR as IngredientRepository (DB)
participant ISR as IngredientSearchRepository (ES)
participant ES as Elasticsearch
rect rgba(242,255,230,0.6)
note over Admin,SC: Reindex Flow
Admin->>SC: POST /api/search/admin/reindex
SC->>IIS: indexIngredients()
IIS->>IR: findAll()
IR-->>IIS: Ingredient list
IIS->>ISR: saveAll(mapped IngredientDocuments)
ISR->>ES: bulk index
ES-->>ISR: ack
IIS-->>SC: done
SC-->>Admin: ApiResponse("인덱싱 완료")
end
sequenceDiagram
autonumber
participant App as Application
participant EIB as EsIndexBootstrap
participant ESOp as ElasticsearchOperations
participant ES as Elasticsearch
rect rgba(255,245,230,0.6)
note over App,ES: Startup Index Bootstrap
App->>EIB: ApplicationReadyEvent
EIB->>ESOp: indexOps(IngredientDocument)
ESOp->>ES: HEAD /ingredients
alt not exists
ESOp->>ES: PUT /ingredients (settings from nori-setting.json)
ES-->>ESOp: created
ESOp->>ES: PUT /ingredients/_mapping
ES-->>ESOp: acknowledged
else exists
note over EIB: No action
end
EIB-->>App: ready
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Summary of Changes
Hello @MODUGGAGI, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!
이 PR은 애플리케이션에 Elasticsearch 기반의 재료 검색 기능을 도입하여 사용자 경험을 향상시키는 것을 목표로 합니다. 기존 데이터베이스 검색의 한계를 극복하고, 한국어 형태소 분석을 통해 더욱 정확하고 효율적인 검색 결과를 제공하기 위한 인프라 및 서비스 계층의 변경사항을 포함합니다. 이를 통해 재료 검색 시 오타 보정, 자동 완성 등 고급 검색 기능을 지원할 수 있는 기반을 마련했습니다.
Highlights
- Elasticsearch 통합: 재료 검색 기능을 위해 Elasticsearch를 시스템에 통합했습니다. 이를 통해 더 빠르고 유연한 검색 기능을 제공할 수 있습니다.
- Docker Compose 환경 설정: 개발 환경에서 Elasticsearch와 Kibana를 쉽게 배포하고 관리할 수 있도록
compose-dev.yml파일에 관련 서비스를 추가했습니다. - 한글 형태소 분석기(Nori) 적용: 한국어 검색의 정확도를 높이기 위해 Elasticsearch에 Nori 형태소 분석기 플러그인을 설치하고, 이를 활용한 인덱스 설정을 추가했습니다.
- 재료 데이터 인덱싱 및 검색 API: 기존 DB의 재료 데이터를 Elasticsearch로 인덱싱하는 서비스와, 인덱싱된 데이터를 활용하여 재료를 검색하는 API 엔드포인트를 구현했습니다.
Using Gemini Code Assist
The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.
Invoking Gemini
You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.
| Feature | Command | Description |
|---|---|---|
| Code Review | /gemini review |
Performs a code review for the current pull request in its current state. |
| Pull Request Summary | /gemini summary |
Provides a summary of the current pull request in its current state. |
| Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments. |
| Help | /gemini help |
Displays a list of available commands. |
Customization
To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.
Limitations & Feedback
Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.
You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.
Footnotes
-
Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩
There was a problem hiding this comment.
Summary of Changes
Hello @MODUGGAGI, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!
This pull request introduces a new ingredient search feature powered by Elasticsearch. It sets up the necessary infrastructure, including Dockerized Elasticsearch and Kibana, integrates Spring Boot with Elasticsearch, and provides a robust search API. The implementation focuses on enhancing search relevance for Korean ingredient names through specialized analyzers and offers an administrative endpoint for index management.
Highlights
- Elasticsearch Integration: Implemented Elasticsearch for ingredient search, enabling keyword-based and paginated results with details like name, category, and image.
- Korean Language Support: Configured Elasticsearch with the Nori analyzer for advanced Korean text analysis and autocomplete functionality to improve search relevance.
- Development Environment Setup: Extended the local development environment with Docker Compose services for Elasticsearch and Kibana, including a custom Elasticsearch image with the Nori plugin and persistent storage.
- Search Index Management: Added an administrative API endpoint to manually trigger the re-indexing of ingredient data from the primary database into Elasticsearch.
Using Gemini Code Assist
The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.
Invoking Gemini
You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.
| Feature | Command | Description |
|---|---|---|
| Code Review | /gemini review |
Performs a code review for the current pull request in its current state. |
| Pull Request Summary | /gemini summary |
Provides a summary of the current pull request in its current state. |
| Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments. |
| Help | /gemini help |
Displays a list of available commands. |
Customization
To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.
Limitations & Feedback
Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.
You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.
Footnotes
-
Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩
There was a problem hiding this comment.
Codex Review: Here are some suggestions.
Reply with @codex fix comments to fix any unresolved comments.
About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you open a pull request for review, mark a draft as ready, or comment "@codex review". If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex fix this CI failure" or "@codex address that feedback".
| @PostMapping("/admin/reindex") | ||
| public ApiResponse<String> reindexIngredients() { | ||
| ingredientIndexingService.indexIngredients(); | ||
| return ApiResponse.onSuccess("인덱싱이 성공적으로 완료되었습니다."); |
There was a problem hiding this comment.
[P1] Restrict reindex endpoint to admin role
The reindex endpoint is mapped to /api/search/admin/reindex but does not enforce any role check, and SecurityConfig only guards paths starting with /admin/**. As written, any authenticated user can trigger a costly reindex operation. Add explicit authorization on this controller (e.g., @PreAuthorize("hasRole('ADMIN')")) or extend the security matcher so only administrators can call it.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Code Review
이번 PR은 재료 검색 기능에 Elasticsearch를 도입하는 중요한 변경사항을 담고 있습니다. 전반적으로 Elasticsearch 설정, 인덱싱, 검색 쿼리 구현이 잘 이루어졌습니다. 특히 한글 형태소 분석기(nori)와 다중 필드 검색을 활용하여 검색 품질을 높이려는 노력이 돋보입니다. 다만, 몇 가지 부분에서 성능, 유지보수성, 보안을 개선할 수 있는 여지가 있어 리뷰 의견을 남깁니다. 주로 대용량 데이터 처리 시 발생할 수 있는 성능 문제, 관리자 API 보안, 코드의 유지보수성 향상에 대한 제안을 포함했습니다.
| List<Ingredient> allIngredients = ingredientRepository.findAll(); | ||
| if (allIngredients.isEmpty()) { | ||
| log.info(">>>> 인덱싱할 재료 데이터가 없습니다."); | ||
| return; | ||
| } | ||
|
|
||
| // 2. Ingredient 엔티티를 IngredientDocument로 변환 | ||
| List<IngredientDocument> 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()); |
There was a problem hiding this comment.
ingredientRepository.findAll()을 사용하여 모든 재료 데이터를 한 번에 메모리로 불러오고 있습니다. 재료 데이터가 많아질 경우 OutOfMemoryError가 발생할 수 있는 잠재적인 성능 문제가 있습니다. 데이터를 일정한 크기의 배치(batch)로 나누어 처리하는 것이 안전합니다. Spring Data JPA의 페이징 기능을 활용하여 이 문제를 해결할 수 있습니다. 아래와 같이 수정하는 것을 제안합니다. (참고: org.springframework.data.domain.Page와 org.springframework.data.domain.PageRequest 임포트가 필요합니다.)
final int BATCH_SIZE = 1000;
int pageNumber = 0;
long totalIndexed = 0;
while (true) {
// 1. DB에서 배치 크기만큼 재료 데이터를 조회
org.springframework.data.domain.Page<Ingredient> page = ingredientRepository.findAll(org.springframework.data.domain.PageRequest.of(pageNumber, BATCH_SIZE));
List<Ingredient> ingredients = page.getContent();
if (ingredients.isEmpty()) {
break;
}
// 2. Ingredient 엔티티를 IngredientDocument로 변환
List<IngredientDocument> ingredientDocuments = ingredients.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);
totalIndexed += ingredients.size();
pageNumber++;
}
if (totalIndexed == 0) {
log.info(">>>> 인덱싱할 재료 데이터가 없습니다.");
return;
}
log.info(">>>> Ingredient 데이터 인덱싱 완료 ({}개)", totalIndexed);| private final IngredientIndexingService ingredientIndexingService; | ||
| private final SearchQueryService searchQueryService; | ||
|
|
||
| @PostMapping("/admin/reindex") |
| if (!idx.exists()) { | ||
| idx.create(); // @Setting(nori-setting.json) 반영 | ||
| idx.putMapping(idx.createMapping(IngredientDocument.class)); // @MultiField 매핑 반영 | ||
| } |
There was a problem hiding this comment.
현재 구현은 애플리케이션 시작 시 인덱스가 존재하지 않을 때만 생성하도록 되어 있습니다. 만약 IngredientDocument의 매핑이 변경될 경우, 기존 인덱스가 삭제되지 않아 변경사항이 반영되지 않는 문제가 발생할 수 있습니다. 개발 환경에서는 애플리케이션 재시작 시 항상 최신 매핑을 보장하기 위해 기존 인덱스를 삭제하고 다시 생성하는 것이 더 안정적일 수 있습니다. 운영 환경에서는 무중단 인덱스 업데이트를 위해 Blue-Green 배포와 유사한 인덱스 alias를 활용하는 전략을 고려해야 합니다.
if (idx.exists()) {
idx.delete();
}
idx.create(); // @Setting(nori-setting.json) 반영
idx.putMapping(idx.createMapping(IngredientDocument.class)); // @MultiField 매핑 반영| public class SearchConverter { | ||
|
|
| .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배 | ||
| ) | ||
| ) |
| @RequestParam(defaultValue = "0") int page, | ||
| @RequestParam(defaultValue = "20") int size |
There was a problem hiding this comment.
페이지네이션 파라미터인 page와 size에 대한 유효성 검사가 없습니다. 사용자가 비정상적으로 큰 size 값을 요청할 경우 서버에 과도한 부하를 줄 수 있습니다. page는 0 이상, size는 적절한 범위(예: 1~100) 내의 값만 허용하도록 유효성 검사를 추가하는 것이 좋습니다. 아래와 같이 수정하고, 컨트롤러 클래스에 @org.springframework.validation.annotation.Validated 어노테이션을 추가하는 것을 권장합니다.
| @RequestParam(defaultValue = "0") int page, | |
| @RequestParam(defaultValue = "20") int size | |
| @RequestParam(defaultValue = "0") @jakarta.validation.constraints.Min(0) int page, | |
| @RequestParam(defaultValue = "20") @jakarta.validation.constraints.Min(1) @jakarta.validation.constraints.Max(100) int size |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (28)
build.gradle (1)
38-38: Elasticsearch starter added — consider tests and timeouts.
- Add ES Testcontainers for integration tests.
- Configure client timeouts to avoid long hangs if ES is down (properties in
application-es.yml).Example properties to add:
spring: elasticsearch: connection-timeout: 2s socket-timeout: 5ssrc/main/resources/application-es.yml (2)
1-3: Confirm host depending on where the app runs.
http://localhost:9200is correct when running the app on the host. If you run the app in Docker on the same compose network, usehttp://elasticsearch:9200. Also consider adding connection/socket timeouts (see build.gradle comment).
3-3: Add a newline at EOF.- uris: http://localhost:9200 + uris: http://localhost:9200 +src/main/resources/application.yml (2)
13-14: Including theesprofile globally may force ES config in all environments.If that’s not intended, gate ES to a dev/local profile (e.g., via profile groups) instead of always including it.
Option A (quick): remove
- eshere and activateesonly when needed.Option B (groups):
spring: profiles: include: - redis group: local: redis,esRun with
SPRING_PROFILES_ACTIVE=localduring local dev.
14-14: Add a newline at EOF.- - es + - es +src/main/resources/es-setting/nori-setting.json (3)
5-5: Edge n-gram min_gram=1 can explode token count and memory.For autocomplete,
min_gram: 2(and a slightly smallermax_gram) is usually sufficient and far cheaper.- "edge_ngram_filter": { "type": "edge_ngram", "min_gram": 1, "max_gram": 20 } + "edge_ngram_filter": { "type": "edge_ngram", "min_gram": 2, "max_gram": 15 }
8-17: Ensure search_analyzer is non-ngram to avoid noisy queries.Verify your field mappings use
analyzer: korean_autocompleteandsearch_analyzer: korean_nori(e.g., on the name field). If not, autocomplete will also n-gram user queries.Example mapping snippet:
{ "mappings": { "properties": { "name": { "type": "text", "analyzer": "korean_autocomplete", "search_analyzer": "korean_nori" } } } }
3-3: Remove unused empty tokenizer block.
nori_tokenizeris built-in; the empty"tokenizer": {}section is unnecessary.- "tokenizer": {},src/main/java/novaminds/gradproj/GradprojApplication.java (1)
13-18: Excluding Reactive ES repos is likely redundant.Spring Boot won’t auto-configure reactive repositories if the reactive stack isn’t on the classpath. Safe to keep, but you can remove the exclude to reduce noise.
src/main/java/novaminds/gradproj/global/search/web/dto/SearchResponseDTO.java (2)
16-23: Consider Java records for DTOs (Java 21).Records reduce boilerplate and work well with Jackson for response-only DTOs.
Example:
public record IngredientSearchResult(Long id, String name, String category, String imageUrl) {} public record IngredientSearchList(List<IngredientSearchResult> results) {}
10-14: Avoid DTO duplication with existing IngredientResponseDTO.If shapes match, reuse or map to the existing DTO to keep the API surface consistent.
compose-dev.yml (3)
34-49: Dev-only flags acknowledged; avoid carrying into prod.
xpack.security.enabled=falseand low heap are fine for local dev. Ensure prod compose overrides these.Optional: add memlock ulimit to prevent bootstrap warnings if you later enable security:
elasticsearch: build: context: . dockerfile: Dockerfile.elasticsearch + ulimits: + memlock: + soft: -1 + hard: -1
50-59: Kibana depends_on is okay; consider readiness if you script automation.If you later add healthchecks, switch to
depends_on: condition: service_healthyso Kibana waits for ES readiness.
68-68: Add a newline at EOF.es-data-dev: +src/main/java/novaminds/gradproj/global/search/IngredientDocument.java (3)
30-31: Consider a lowercase normalizer for category exact matches.
Keywordis case-sensitive. If you need case-insensitive equality, attach a normalizer (e.g.,lowercase) defined in your settings.- @Field(type = FieldType.Keyword, name = "category_name") // 카테고리 이름은 정확히 일치해야 하므로 Keyword 타입 사용 + @Field(type = FieldType.Keyword, name = "category_name", normalizer = "lowercase")Ensure
"lowercase"normalizer is defined innori-setting.json; otherwise, add it or skip this change if strict case-sensitive matching is intended.
33-34: Disable doc_values for non-indexed imageUrl to save storage.Since
image_urlis not searchable, disabling doc_values reduces heap/disk.- @Field(type = FieldType.Keyword, name = "image_url", index = false) // 이미지 URL은 검색 대상이 아니므로 index=false + @Field(type = FieldType.Keyword, name = "image_url", index = false, docValues = false) // 검색 비대상 + doc_values 비활성화
15-15: Optional: make @setting path explicit (use classpath:)Resource found at src/main/resources/es-setting/nori-setting.json — current annotation will resolve, but using an explicit classpath: prefix is safer across runtimes.
-@Setting(settingPath = "es-setting/nori-setting.json") // 한글 형태소 분석기 설정 추가 +@Setting(settingPath = "classpath:es-setting/nori-setting.json") // 한글 형태소 분석기 설정 추가src/main/java/novaminds/gradproj/global/search/EsIndexBootstrap.java (2)
14-21: Harden startup against ES unavailability and add logging.A transient ES outage during startup will bubble an exception from the event listener and can crash the app. Wrap with try/catch and log; optionally fail-fast only on specific profiles.
@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 매핑 반영 - } + try { + var idx = operations.indexOps(IngredientDocument.class); + if (!idx.exists()) { + idx.create(); // @Setting(nori-setting.json) 반영 + idx.putMapping(idx.createMapping(IngredientDocument.class)); // @MultiField 매핑 반영 + } + } catch (Exception e) { + // TODO: replace with your logger + System.err.println("[EsIndexBootstrap] Failed to init index 'ingredients': " + e.getMessage()); + // Option A: rethrow to fail-fast on non-dev + // Option B: swallow on local/dev + } }Confirm desired behavior: fail-fast vs. best-effort on
local/devversusprod.
16-20: Plan for mapping changes using versioned indices + aliases.
putMappingwon’t reconcile incompatible mapping updates on existing indices. Adopt versioned indices (e.g.,ingredients_v{n}) and a write/search alias (ingredients) for zero-downtime reindex/migrations.I can sketch a rollout plan (create v2 -> reindex -> atomically switch alias -> delete v1) if helpful.
src/main/java/novaminds/gradproj/global/search/web/controller/SearchController.java (2)
24-32: Avoid API duplication with existing DB-backed search.You now have
/api/search/ingredients(ES) and/api/ingredients(DB). Decide the canonical endpoint or attach a feature flag/profile to prevent diverging behaviors for clients.Confirm which endpoint FE will call; we can route
/api/ingredientsto ES under a profile or toggle.
24-32: Consider returning pagination metadata.Clients typically need
total,page,size. ExtendIngredientSearchListto include these.I can draft DTO and service changes if you want.
src/main/java/novaminds/gradproj/global/search/service/SearchQueryService.java (3)
22-27: Trim and short-circuit early for blank input.Normalize whitespace to reduce accidental empty queries.
- public SearchResponseDTO.IngredientSearchList searchIngredients(String keyword, int page, int size) { - // 1. 검색어가 비어있으면 빈 결과 반환 - if (keyword == null || keyword.isBlank()) { + public SearchResponseDTO.IngredientSearchList searchIngredients(String keyword, int page, int size) { + // 1. 검색어가 비어있으면 빈 결과 반환 + if (keyword == null || (keyword = keyword.strip()).isBlank()) { return SearchConverter.toIngredientSearchListDTO(List.of()); }
29-60: Exact term oningredient_name.kwis case-sensitive; align with mapping or adjust query.If you want case-insensitive exact matches, ensure the
kwsubfield uses a lowercase normalizer (mapping change), or add a second normalized keyword subfield and target that here.Please confirm
ingredient_name.kwhas a normalizer innori-setting.json. If not, matches will be strictly case-sensitive.
29-60: Optional: improve autocomplete relevance.For the
autofield,match_phrase_prefixorsearch_as_you_typecan improve prefix matches overmatch.I can provide an alternative query using
multi_matchwithtype: "bool_prefix"if desired.src/main/java/novaminds/gradproj/global/search/converter/SearchConverter.java (1)
8-8: Make the utility class non-instantiable.Add a private constructor (or Lombok’s @UtilityClass) to prevent accidental instantiation.
public class SearchConverter { + private SearchConverter() {}src/main/java/novaminds/gradproj/global/search/service/IngredientIndexingService.java (3)
23-24: AvoidreadOnly = truewhen the method performs writes (to Elasticsearch).While JPA writes aren’t happening, the method still mutates external state. Drop
readOnlyfor clarity.- @Transactional(readOnly = true) + @Transactional public void indexIngredients() {
44-46: Wrap ES bulk save in error handling with context.Add try/catch to surface failures without losing which page failed.
- ingredientSearchRepository.saveAll(docs); + try { + ingredientSearchRepository.saveAll(docs); + } catch (Exception e) { + log.error(">>>> ES 인덱싱 실패: page={}, size={}, firstId={}", + pageable.getPageNumber(), docs.size(), + docs.isEmpty() ? null : docs.get(0).getId(), e); + throw e; + }
44-47: Plan for zero-downtime reindex using aliases.Consider indexing into a versioned index (ingredients_vN), then atomically swap an alias to avoid partial results during reindex.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (15)
Dockerfile.elasticsearch(1 hunks)build.gradle(2 hunks)compose-dev.yml(1 hunks)src/main/java/novaminds/gradproj/GradprojApplication.java(1 hunks)src/main/java/novaminds/gradproj/global/search/EsIndexBootstrap.java(1 hunks)src/main/java/novaminds/gradproj/global/search/IngredientDocument.java(1 hunks)src/main/java/novaminds/gradproj/global/search/converter/SearchConverter.java(1 hunks)src/main/java/novaminds/gradproj/global/search/repository/IngredientSearchRepository.java(1 hunks)src/main/java/novaminds/gradproj/global/search/service/IngredientIndexingService.java(1 hunks)src/main/java/novaminds/gradproj/global/search/service/SearchQueryService.java(1 hunks)src/main/java/novaminds/gradproj/global/search/web/controller/SearchController.java(1 hunks)src/main/java/novaminds/gradproj/global/search/web/dto/SearchResponseDTO.java(1 hunks)src/main/resources/application-es.yml(1 hunks)src/main/resources/application.yml(1 hunks)src/main/resources/es-setting/nori-setting.json(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: MODUGGAGI
PR: TeamNovaMinds/BE#36
File: src/main/resources/application-redis.yml:7-7
Timestamp: 2025-08-29T02:33:01.862Z
Learning: MODUGGAGI is willing to use hardcoded passwords for local development/testing phases before deployment, prioritizing development convenience over strict security practices during the development phase.
Learnt from: MODUGGAGI
PR: TeamNovaMinds/BE#34
File: src/main/java/novaminds/gradproj/config/S3Config.java:22-29
Timestamp: 2025-08-28T04:53:41.474Z
Learning: MODUGGAGI prefers explicit credential configuration through environment variables and properties files rather than using AWS default credential chain fallbacks in their Spring Boot S3Config setup.
🧬 Code graph analysis (9)
src/main/resources/application-es.yml (1)
src/test/java/novaminds/gradproj/GradprojApplicationTests.java (1)
SpringBootTest(6-13)
src/main/java/novaminds/gradproj/global/search/repository/IngredientSearchRepository.java (3)
src/main/java/novaminds/gradproj/domain/ingredient/repository/IngredientRepository.java (1)
IngredientRepository(9-16)src/main/java/novaminds/gradproj/domain/ingredient/repository/IngredientRepositoryCustom.java (2)
IngredientRepositoryCustom(7-10)searchIngredientsByKeyword(8-8)src/main/java/novaminds/gradproj/domain/ingredient/repository/IngredientRepositoryCustomImpl.java (2)
Repository(18-41)Override(24-30)
src/main/java/novaminds/gradproj/GradprojApplication.java (6)
src/main/java/novaminds/gradproj/domain/ingredient/repository/IngredientRepository.java (1)
IngredientRepository(9-16)src/main/java/novaminds/gradproj/domain/userdevice/repository/UserDeviceRepository.java (1)
UserDeviceRepository(7-9)src/main/java/novaminds/gradproj/domain/recipe/repository/RecipeRepository.java (1)
RecipeRepository(10-15)src/main/java/novaminds/gradproj/domain/notification/repository/NotificationRepository.java (1)
NotificationRepository(7-8)src/main/java/novaminds/gradproj/domain/notification/repository/NotificationSettingsRepository.java (1)
NotificationSettingsRepository(7-8)src/main/java/novaminds/gradproj/domain/member/repository/MemberRepository.java (1)
MemberRepository(14-26)
src/main/java/novaminds/gradproj/global/search/web/dto/SearchResponseDTO.java (1)
src/main/java/novaminds/gradproj/domain/ingredient/web/dto/IngredientResponseDTO.java (4)
IngredientResponseDTO(10-52)Builder(23-29)Builder(12-21)Builder(31-41)
src/main/java/novaminds/gradproj/global/search/service/SearchQueryService.java (2)
src/main/java/novaminds/gradproj/global/search/converter/SearchConverter.java (1)
SearchConverter(8-28)src/main/java/novaminds/gradproj/global/search/web/dto/SearchResponseDTO.java (1)
SearchResponseDTO(8-24)
src/main/java/novaminds/gradproj/global/search/converter/SearchConverter.java (3)
src/main/java/novaminds/gradproj/global/search/web/dto/SearchResponseDTO.java (1)
SearchResponseDTO(8-24)src/main/java/novaminds/gradproj/domain/ingredient/converter/IngredientConverter.java (4)
IngredientConverter(10-49)toIngredientListDTO(21-30)toIngredientDTO(12-19)toIngredientDetailResponse(32-40)src/main/java/novaminds/gradproj/domain/ingredient/web/dto/IngredientResponseDTO.java (2)
IngredientResponseDTO(10-52)Builder(23-29)
src/main/java/novaminds/gradproj/global/search/IngredientDocument.java (5)
src/main/java/novaminds/gradproj/domain/ingredient/entity/Ingredient.java (1)
Getter(15-63)src/main/java/novaminds/gradproj/domain/ingredient/entity/IngredientCategory.java (1)
NoArgsConstructor(27-54)src/main/java/novaminds/gradproj/domain/ingredient/web/dto/IngredientResponseDTO.java (2)
IngredientResponseDTO(10-52)Builder(12-21)src/main/java/novaminds/gradproj/domain/ingredient/repository/IngredientRepositoryCustomImpl.java (1)
Repository(18-41)src/main/java/novaminds/gradproj/domain/ingredient/converter/IngredientConverter.java (1)
IngredientConverter(10-49)
src/main/java/novaminds/gradproj/global/search/service/IngredientIndexingService.java (2)
src/main/java/novaminds/gradproj/domain/ingredient/repository/IngredientRepository.java (1)
IngredientRepository(9-16)src/main/java/novaminds/gradproj/domain/ingredient/entity/Ingredient.java (1)
Getter(15-63)
src/main/java/novaminds/gradproj/global/search/web/controller/SearchController.java (3)
src/main/java/novaminds/gradproj/global/search/web/dto/SearchResponseDTO.java (1)
SearchResponseDTO(8-24)src/main/java/novaminds/gradproj/domain/ingredient/web/controller/IngredientController.java (3)
RestController(16-42)Operation(25-32)Operation(34-41)src/main/java/novaminds/gradproj/domain/ingredient/service/query/IngredientQueryService.java (1)
Service(17-47)
🪛 YAMLlint (1.37.1)
src/main/resources/application-es.yml
[error] 3-3: no new line character at the end of file
(new-line-at-end-of-file)
compose-dev.yml
[error] 68-68: no new line character at the end of file
(new-line-at-end-of-file)
src/main/resources/application.yml
[error] 14-14: no new line character at the end of file
(new-line-at-end-of-file)
🔇 Additional comments (10)
build.gradle (1)
29-29: Validation starter addition looks good.src/main/java/novaminds/gradproj/global/search/IngredientDocument.java (2)
21-28: LGTM: MultiField mapping aligns with query usage.The main field and
auto/kwsubfields match the query fields in SearchQueryService.
18-20: Ensure id is stored in _source but not indexedAnnotate the id field so it is present in _source while preventing indexing/search and docValues (example for a Long):
@field(type = FieldType.Long, index = false, docValues = false)
private Long id;
(If id is stored as String, use FieldType.Keyword instead.) Confirm current ES search hits include id in _source; if they do not, update the mapping or explicitly add the id into _source at indexing time.Location: src/main/java/novaminds/gradproj/global/search/IngredientDocument.java (reported lines 18–20) — file not found in the repo during verification; unable to confirm mapping.
src/main/java/novaminds/gradproj/global/search/EsIndexBootstrap.java (1)
16-16: No action required — toolchain targets Java 21.build.gradle sets languageVersion = JavaLanguageVersion.of(21), so local-variable type inference ('var') is supported (Java 10+).
src/main/java/novaminds/gradproj/global/search/repository/IngredientSearchRepository.java (1)
6-7: LGTM: Repository wiring is minimal and correct.Extends
ElasticsearchRepository<IngredientDocument, Long>with correct generic types. No issues.src/main/java/novaminds/gradproj/global/search/service/SearchQueryService.java (1)
64-70: Preserve ID even if not present in _source.If IngredientDocument.id may be null (not stored in _source), getContent().getId() can be null — use SearchHit.getId() as a fallback when mapping hits to DTOs, or persist id into _source (add @field on IngredientDocument.id).
File: src/main/java/novaminds/gradproj/global/search/service/SearchQueryService.java (lines 64–70)
- Suggested mapping: id = doc.getId() != null ? doc.getId() : Long.parseLong(hit.getId()); set this id on the DTO.
src/main/java/novaminds/gradproj/global/search/converter/SearchConverter.java (2)
10-17: DTO mapping looks correct and consistent.Field-to-field mapping aligns with SearchResponseDTO and IngredientDocument.
20-22: Resolved — keep Stream#toList() (project targets Java 21).
build.gradle sets java.toolchain.languageVersion = JavaLanguageVersion.of(21), so Stream.toList() is supported; no change required.src/main/java/novaminds/gradproj/global/search/service/IngredientIndexingService.java (2)
35-41: Confirm filtering policy (e.g., only APPROVED ingredients).If search should exclude 미승인/비공개 항목, filter at query (DB) level to avoid indexing unwanted documents.
Example (outside this file):
// IngredientRepository.java @EntityGraph(attributePaths = "ingredientCategory") @Query("select i from Ingredient i where i.status = novaminds.gradproj.domain.ingredient.entity.RegistrationStatus.APPROVED") Page<Ingredient> findApprovedWithCategory(Pageable pageable);And call
findApprovedWithCategory(...)here.
15-22: Overall structure and dependencies are sound.Service wiring and logging are clear; once paging/fetch tweaks land, this should scale.
| FROM elasticsearch:8.18.6 | ||
| RUN bin/elasticsearch-plugin install analysis-nori No newline at end of file |
There was a problem hiding this comment.
Make plugin install non-interactive to prevent CI hangs.
Without --batch, elasticsearch-plugin install can block waiting for confirmation during docker build.
Apply this diff:
-FROM elasticsearch:8.18.6
-RUN bin/elasticsearch-plugin install analysis-nori
+FROM elasticsearch:8.18.6
+RUN bin/elasticsearch-plugin install --batch analysis-nori📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| FROM elasticsearch:8.18.6 | |
| RUN bin/elasticsearch-plugin install analysis-nori | |
| FROM elasticsearch:8.18.6 | |
| RUN bin/elasticsearch-plugin install --batch analysis-nori |
🤖 Prompt for AI Agents
In Dockerfile.elasticsearch around lines 1 to 2, the RUN command uses
`elasticsearch-plugin install analysis-nori` which can prompt interactively
during docker builds and hang CI; modify the RUN invocation to pass the
non-interactive flag `--batch` to `elasticsearch-plugin install` so the plugin
installs without prompting (i.e., run the install with `--batch`).
| // 1. DB에서 모든 재료 데이터를 조회 | ||
| List<Ingredient> allIngredients = ingredientRepository.findAll(); | ||
| if (allIngredients.isEmpty()) { | ||
| log.info(">>>> 인덱싱할 재료 데이터가 없습니다."); | ||
| return; | ||
| } | ||
|
|
||
| // 2. Ingredient 엔티티를 IngredientDocument로 변환 | ||
| List<IngredientDocument> 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); | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
Eliminate full-table load and N+1 on ingredientCategory; index in pages with a fetch strategy.
findAll() loads everything into memory and accessing ingredient.getIngredientCategory() (LAZY) risks N+1. Page through results and fetch the category to cut memory and queries.
- // 1. DB에서 모든 재료 데이터를 조회
- List<Ingredient> allIngredients = ingredientRepository.findAll();
- if (allIngredients.isEmpty()) {
- log.info(">>>> 인덱싱할 재료 데이터가 없습니다.");
- return;
- }
-
- // 2. Ingredient 엔티티를 IngredientDocument로 변환
- List<IngredientDocument> 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());
+ int total = 0;
+ int pageSize = 1000;
+ var pageable = org.springframework.data.domain.PageRequest.of(0, pageSize);
+ while (true) {
+ var page = ingredientRepository.findAllWithCategory(pageable); // fetch category to avoid N+1
+ if (page.isEmpty()) break;
+
+ var docs = page.getContent().stream()
+ .map(ingredient -> IngredientDocument.builder()
+ .id(ingredient.getId())
+ .ingredientName(ingredient.getIngredientName())
+ .categoryName(ingredient.getIngredientCategory().getIngredientCategoryName())
+ .imageUrl(ingredient.getImageUrl())
+ .build())
+ .toList();
+
+ ingredientSearchRepository.saveAll(docs);
+ total += docs.size();
+
+ if (!page.hasNext()) break;
+ pageable = page.nextPageable();
+ }
+
+ if (total == 0) {
+ log.info(">>>> 인덱싱할 재료 데이터가 없습니다.");
+ return;
+ }
+ log.info(">>>> Ingredient 데이터 인덱싱 완료 ({}개)", total);Outside-this-file support (repository):
// IngredientRepository.java
@EntityGraph(attributePaths = "ingredientCategory")
Page<Ingredient> findAllWithCategory(Pageable pageable);| .withPageable(PageRequest.of(page, size)) // 페이징 설정 | ||
| .build(); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Clamp pageable inputs to avoid ES heavy queries and negative values.
PageRequest.of(page, size) throws on negatives and large sizes can be harmful.
- .withPageable(PageRequest.of(page, size)) // 페이징 설정
+ .withPageable(PageRequest.of(Math.max(0, page), Math.max(1, Math.min(100, size)))) // 페이징 설정(클램프)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .withPageable(PageRequest.of(page, size)) // 페이징 설정 | |
| .build(); | |
| .withPageable(PageRequest.of(Math.max(0, page), Math.max(1, Math.min(100, size)))) // 페이징 설정(클램프) | |
| .build(); |
🤖 Prompt for AI Agents
In
src/main/java/novaminds/gradproj/global/search/service/SearchQueryService.java
around lines 60-61, PageRequest.of(page, size) can throw for negative values and
allow excessively large sizes that stress Elasticsearch; clamp inputs by
ensuring page >= 0 and size between 1 and a defined MAX_PAGE_SIZE (e.g., 100),
applying Math.max/Math.min or equivalent to compute safePage and safeSize, then
pass PageRequest.of(safePage, safeSize) so negatives are avoided and size is
capped to a reasonable maximum.
| @PostMapping("/admin/reindex") | ||
| public ApiResponse<String> reindexIngredients() { | ||
| ingredientIndexingService.indexIngredients(); | ||
| return ApiResponse.onSuccess("인덱싱이 성공적으로 완료되었습니다."); | ||
| } |
There was a problem hiding this comment.
Protect the reindex endpoint (authorization + environment gating).
Unauthenticated/unauthorized reindexing is a security and operational risk. Restrict to admins and optionally expose only on local/dev.
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.context.annotation.Profile;
...
- @PostMapping("/admin/reindex")
+ @PostMapping("/admin/reindex")
+ @PreAuthorize("hasRole('ADMIN')")
+ @Profile({"local","dev"})
public ApiResponse<String> reindexIngredients() {
ingredientIndexingService.indexIngredients();
return ApiResponse.onSuccess("인덱싱이 성공적으로 완료되었습니다.");
}If Spring Security isn’t configured, at minimum add a shared secret header check or remove this endpoint from release.
Committable suggestion skipped: line range outside the PR's diff.
| @GetMapping("/ingredients") | ||
| public ApiResponse<SearchResponseDTO.IngredientSearchList> 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); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Validate and clamp pagination parameters.
Unbounded size can hurt ES. Guard against negative page and large sizes (e.g., max 100).
- public ApiResponse<SearchResponseDTO.IngredientSearchList> searchIngredients(
- @RequestParam("keyword") String keyword,
- @RequestParam(defaultValue = "0") int page,
- @RequestParam(defaultValue = "20") int size
- ) {
- SearchResponseDTO.IngredientSearchList searchResults = searchQueryService.searchIngredients(keyword, page, size);
+ public ApiResponse<SearchResponseDTO.IngredientSearchList> searchIngredients(
+ @RequestParam("keyword") String keyword,
+ @RequestParam(defaultValue = "0") int page,
+ @RequestParam(defaultValue = "20") int size
+ ) {
+ page = Math.max(0, page);
+ size = Math.max(1, Math.min(100, size));
+ var searchResults = searchQueryService.searchIngredients(keyword, page, size);
return ApiResponse.onSuccess(searchResults);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @GetMapping("/ingredients") | |
| public ApiResponse<SearchResponseDTO.IngredientSearchList> 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); | |
| } | |
| @GetMapping("/ingredients") | |
| public ApiResponse<SearchResponseDTO.IngredientSearchList> searchIngredients( | |
| @RequestParam("keyword") String keyword, | |
| @RequestParam(defaultValue = "0") int page, | |
| @RequestParam(defaultValue = "20") int size | |
| ) { | |
| page = Math.max(0, page); | |
| size = Math.max(1, Math.min(100, size)); | |
| var searchResults = searchQueryService.searchIngredients(keyword, page, size); | |
| return ApiResponse.onSuccess(searchResults); | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/novaminds/gradproj/global/search/web/controller/SearchController.java
around lines 24-32, the pagination params are unvalidated; ensure page is
non-negative and size is clamped to a safe range (e.g., 1..100) before calling
searchQueryService. At the start of the method, normalize page = Math.max(0,
page) (or return 400 for negative if you prefer strictness) and size =
Math.max(1, Math.min(size, 100)); then pass the normalized values to
searchQueryService and return the response.
| @EnableJpaRepositories( | ||
| basePackages = "novaminds.gradproj.domain" | ||
| ) | ||
| @EnableElasticsearchRepositories(basePackages = "novaminds.gradproj.global.search.repository") |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Gate Elasticsearch repositories behind the es profile to prevent unintended startup coupling.
If ES is unavailable (CI, prod without ES), repository/bootstrapping beans may cause startup or runtime failures. Prefer enabling ES repos only when es is active.
Proposed approach (move to a dedicated config):
@Configuration
@Profile("es")
@EnableElasticsearchRepositories(basePackages = "novaminds.gradproj.global.search.repository")
class EsRepositoriesConfig {}Verification script to check ES beans are profile-scoped:
🏁 Script executed:
#!/bin/bash
rg -n -C2 '@EnableElasticsearchRepositories|EsIndexBootstrap|IngredientIndexingService|SearchQueryService' src/main/java
rg -n '@Profile\("es"\)' -C2 src/main/java/novaminds/gradproj/global/searchLength of output: 3715
Gate Elasticsearch behind the "es" profile (make ES repos and ES-only beans conditional).
@EnableElasticsearchRepositories is enabled unconditionally in src/main/java/novaminds/gradproj/GradprojApplication.java (line 25). ES-related beans are unprofiled and will attempt ES wiring at startup: src/main/java/novaminds/gradproj/global/search/EsIndexBootstrap.java (class ~line 11), SearchQueryService.java (line 18), IngredientIndexingService.java (line 18), SearchController.java (lines 15–16).
Action: move @EnableElasticsearchRepositories into a @configuration annotated with @Profile("es") and make ES bootstrap/services conditional (either annotate them with @Profile("es") or use @ConditionalOnBean(ElasticsearchOperations.class) / @ConditionalOnProperty). Example:
@Configuration
@Profile("es")
@EnableElasticsearchRepositories(basePackages = "novaminds.gradproj.global.search.repository")
class EsRepositoriesConfig {}🤖 Prompt for AI Agents
In src/main/java/novaminds/gradproj/GradprojApplication.java around lines 22–25,
@EnableElasticsearchRepositories is declared unconditionally which causes ES
wiring to be attempted even when ES is not enabled; move the annotation into a
new @Configuration class annotated with @Profile("es") (e.g., create an
EsRepositoriesConfig with @Configuration, @Profile("es") and
@EnableElasticsearchRepositories(basePackages =
"novaminds.gradproj.global.search.repository")). Also make ES-related beans
conditional: update
src/main/java/novaminds/gradproj/global/search/EsIndexBootstrap.java,
SearchQueryService.java, IngredientIndexingService.java, and
SearchController.java to be either annotated with @Profile("es") or use
@ConditionalOnBean(ElasticsearchOperations.class) /
@ConditionalOnProperty("es.enabled") so they are only created when ES is active.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (6)
src/main/resources/es-setting/nori-setting.json (2)
9-15: Split index/search analyzers to avoid n‑gramming queries.Using edge_ngram at search-time hurts relevance and can be heavy. Define a search analyzer without the n‑gram filter and wire it via
searchAnalyzerin mapping.Apply this diff:
"analyzer": { "korean_unified_analyzer": { "type": "custom", "tokenizer": "nori_tokenizer", "filter": [ "lowercase", "nori_readingform", "edge_ngram_filter" ] - } + } + , "korean_unified_search_analyzer": { + "type": "custom", + "tokenizer": "nori_tokenizer", + "filter": [ "lowercase", "nori_readingform" ] + } }
7-7: Consider raising min_gram to reduce index bloat.
min_gram: 1explodes term count and storage.2or3often balances UX and size for prefix search.- "edge_ngram_filter": { "type": "edge_ngram", "min_gram": 1, "max_gram": 20 } + "edge_ngram_filter": { "type": "edge_ngram", "min_gram": 2, "max_gram": 20 }src/main/java/novaminds/gradproj/global/search/service/SearchQueryService.java (1)
35-35: Make min_score configurable.
2.0fcan easily filter out valid results depending on analyzer tuning; prefer an application property.src/main/java/novaminds/gradproj/global/search/service/IngredientIndexingService.java (1)
44-45: Consider bulk indexing via ElasticsearchOperations for throughput.
saveAllis fine for small sets; bulk operations improve speed on large batches.src/main/java/novaminds/gradproj/global/search/IngredientDocument.java (2)
21-23: Use dedicated search analyzer (no n‑gram) for queries.Pair with the new
korean_unified_search_analyzerin settings.- @Field(type = FieldType.Text, analyzer = "korean_unified_analyzer") + @Field(type = FieldType.Text, analyzer = "korean_unified_analyzer", searchAnalyzer = "korean_unified_search_analyzer") private String ingredientName;
25-28: Optional: model keyword as a multi-field instead of a separate property.A multi-field on
ingredientName(adding a.kwinner Keyword) simplifies queries and keeps fields cohesive. Keep current approach if you prefer explicit fields.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/main/java/novaminds/gradproj/global/search/IngredientDocument.java(1 hunks)src/main/java/novaminds/gradproj/global/search/service/IngredientIndexingService.java(1 hunks)src/main/java/novaminds/gradproj/global/search/service/SearchQueryService.java(1 hunks)src/main/resources/es-setting/nori-setting.json(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: MODUGGAGI
PR: TeamNovaMinds/BE#36
File: src/main/resources/application-redis.yml:7-7
Timestamp: 2025-08-29T02:33:01.862Z
Learning: MODUGGAGI is willing to use hardcoded passwords for local development/testing phases before deployment, prioritizing development convenience over strict security practices during the development phase.
Learnt from: MODUGGAGI
PR: TeamNovaMinds/BE#34
File: src/main/java/novaminds/gradproj/config/S3Config.java:22-29
Timestamp: 2025-08-28T04:53:41.474Z
Learning: MODUGGAGI prefers explicit credential configuration through environment variables and properties files rather than using AWS default credential chain fallbacks in their Spring Boot S3Config setup.
🧬 Code graph analysis (3)
src/main/java/novaminds/gradproj/global/search/service/IngredientIndexingService.java (2)
src/main/java/novaminds/gradproj/domain/ingredient/repository/IngredientRepository.java (1)
IngredientRepository(9-16)src/main/java/novaminds/gradproj/domain/ingredient/entity/Ingredient.java (1)
Getter(15-63)
src/main/java/novaminds/gradproj/global/search/IngredientDocument.java (4)
src/main/java/novaminds/gradproj/domain/ingredient/entity/Ingredient.java (1)
Getter(15-63)src/main/java/novaminds/gradproj/domain/ingredient/web/dto/IngredientResponseDTO.java (3)
IngredientResponseDTO(10-52)Builder(12-21)Builder(23-29)src/main/java/novaminds/gradproj/domain/ingredient/entity/IngredientCategory.java (1)
NoArgsConstructor(27-54)src/main/java/novaminds/gradproj/domain/ingredient/converter/IngredientConverter.java (1)
IngredientConverter(10-49)
src/main/java/novaminds/gradproj/global/search/service/SearchQueryService.java (2)
src/main/java/novaminds/gradproj/domain/ingredient/service/query/IngredientQueryService.java (2)
Service(17-47)getIngredients(25-39)src/main/java/novaminds/gradproj/domain/ingredient/repository/IngredientRepositoryCustomImpl.java (2)
Repository(18-41)Override(24-30)
🔇 Additional comments (3)
src/main/java/novaminds/gradproj/global/search/service/SearchQueryService.java (2)
31-33: Extract field names into constants.Avoid hardcoded strings for
"ingredientName"and"ingredientNameKw"to reduce typos and ease maintenance.
27-37: Avoid multi_match on a Keyword field; use match + term and clamp pagination.
multiMatchwithingredientNameKw(Keyword) is suboptimal and may error with fuzziness. Match on the Text field and add a boosting exacttermon the Keyword field. Also clamp page/size.- NativeQuery query = NativeQuery.builder() - .withQuery(q -> q - .multiMatch(mm -> mm - .query(keyword) - .fields("ingredientName", "ingredientNameKw") - .fuzziness("AUTO") - ) - ) - .withMinScore(2.0f) - .withPageable(PageRequest.of(page, size)) - .build(); + NativeQuery query = NativeQuery.builder() + .withQuery(q -> q + .bool(b -> b + .must(m -> m.match(mm -> mm + .field("ingredientName") + .query(keyword) + .fuzziness("AUTO") + )) + .should(s -> s.term(t -> t + .field("ingredientNameKw") + .value(keyword) + )) + ) + ) + .withMinScore(2.0f) + .withPageable(PageRequest.of(Math.max(0, page), Math.max(1, Math.min(100, size)))) + .build();src/main/java/novaminds/gradproj/global/search/service/IngredientIndexingService.java (1)
22-48: Batch index to avoid OOM and N+1; also index normalized name for exact matches.
findAll()loads everything and dereferencingingredientCategoryrisks N+1. Page through with a fetch strategy and use the normalized name for the Keyword field.- @Transactional(readOnly = true) - public void indexIngredients() { - log.info(">>>> Ingredient 데이터 인덱싱 시작..."); - - // 1. DB에서 모든 재료 데이터를 조회 - List<Ingredient> allIngredients = ingredientRepository.findAll(); - if (allIngredients.isEmpty()) { - log.info(">>>> 인덱싱할 재료 데이터가 없습니다."); - return; - } - - // 2. Ingredient 엔티티를 IngredientDocument로 변환 - List<IngredientDocument> ingredientDocuments = allIngredients.stream() - .map(ingredient -> IngredientDocument.builder() - .id(ingredient.getId()) - .ingredientName(ingredient.getIngredientName()) - .ingredientNameKw(ingredient.getIngredientName()) - .categoryName(ingredient.getIngredientCategory().getIngredientCategoryName()) - .imageUrl(ingredient.getImageUrl()) - .build()) - .toList(); - - // 3. Elasticsearch에 변환된 데이터를 저장(인덱싱) - ingredientSearchRepository.saveAll(ingredientDocuments); - - log.info(">>>> Ingredient 데이터 인덱싱 완료 ({}개)", ingredientDocuments.size()); - } + @Transactional(readOnly = true) + public void indexIngredients() { + log.info(">>>> Ingredient 데이터 인덱싱 시작..."); + + int pageSize = 1000; + var pageable = org.springframework.data.domain.PageRequest.of(0, pageSize); + long total = 0; + + while (true) { + var page = ingredientRepository.findAllWithCategory(pageable); // fetch join to avoid N+1 + if (page.isEmpty()) break; + + List<IngredientDocument> docs = page.getContent().stream() + .map(ingredient -> IngredientDocument.builder() + .id(ingredient.getId()) + .ingredientName(ingredient.getIngredientName()) + .ingredientNameKw(ingredient.getIngredientNameNormalized()) + .categoryName(ingredient.getIngredientCategory().getIngredientCategoryName()) + .imageUrl(ingredient.getImageUrl()) + .build()) + .toList(); + + ingredientSearchRepository.saveAll(docs); + total += docs.size(); + + if (!page.hasNext()) break; + pageable = page.nextPageable(); + } + + if (total == 0) { + log.info(">>>> 인덱싱할 재료 데이터가 없습니다."); + return; + } + + log.info(">>>> Ingredient 데이터 인덱싱 완료 ({}개)", total); + }Outside-this-file support:
// IngredientRepository.java import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.EntityGraph; @EntityGraph(attributePaths = "ingredientCategory") Page<Ingredient> findAllWithCategory(Pageable pageable);
🚀 변경사항
재료 검색에 간단하게 ElasticSearch 적용
🔗 관련 이슈
✅ 체크리스트
Summary by CodeRabbit
New Features
Configuration
Chores