Skip to content

[HCR-226] analysis-server-v0.0.5#25

Merged
tkv00 merged 20 commits into
mainfrom
release/HSC-251
Mar 9, 2026
Merged

[HCR-226] analysis-server-v0.0.5#25
tkv00 merged 20 commits into
mainfrom
release/HSC-251

Conversation

@tkv00

@tkv00 tkv00 commented Mar 9, 2026

Copy link
Copy Markdown
Contributor

🚀배포 목적

analysis-server-v0.0.5는 키워드 분석 배치 앱의 첫 공식 namespaced 릴리즈입니다.
기준 SHA 2eaf5f9를 대상으로 legacy line v0.0.4를 이어받아 발행하며, 분석 서버를 독립 배포 가능한 태그 체계로 전환한 뒤 처음 수행하는 배포입니다.


🏷️ Release Level

  • release:major
  • release:minor
  • release:patch

📦 배포 대상 (1개 이상)

  • deploy:api-server
  • deploy:worker
  • deploy:counseling-analytics
  • deploy:analysis-server
  • deploy:recommendation-server
  • deploy:customer-web
  • deploy:admin-web

📝주요 변경 사항


⚠️주의 사항

  • 이번 릴리즈부터 태그는 analysis-server-vX.Y.Z 형식으로 발행됨
  • legacy v0.0.4를 이어받아 첫 namespaced tag는 analysis-server-v0.0.5
  • 배포 전 Kafka/MSK 권한, DB 접속 정보, batch 실행 환경 변수를 재확인해야 함
  • 배포 후 batch 1회 실행 로그와 outbox/status 전이를 반드시 확인해야 함

✅배포 체크리스트

  • 마이그레이션/스키마 변경 여부 확인 (직접 마이그레이션 없음)
  • 환경 변수/시크릿 변경 여부 확인
  • 모니터링/알림 설정 확인
  • 배포 후 검증 시나리오 준비

🔁롤백 계획

  • 애플리케이션 롤백: 직전 정상 task/image 또는 이전 배포 SHA로 복구
  • 검증: 롤백 후 Kafka 소비, DB 연결, batch 처리 스모크 테스트 재실행
  • 담당자: Infra Team / 서비스 담당 개발자

🎫 Jira Ticket

  • Jira Ticket: HCR-251

closed #23


YeongHyeonHeo and others added 20 commits March 7, 2026 16:14
[HCR-226] 회원별 키워드 집계기(Aggregator) 구현 및 추출 파이프라인(Stage 2, 3) 정확도 개선
…ytics into feat/HSC-228

# Conflicts:
#	app/pipeline/aggregator.py
#	tests/test_e2e.py
[HSC-228] Kafka 소비 기반 SQL 분석 워커 전환
[HCR-235] 추천 api + 상품 임베딩
[HCR-244] Realtime API 분리 및 batch/docker 실행 구조 정리
@tkv00 tkv00 added 🏷️ release 릴리즈 준비/버전 태깅/릴리즈 노트/릴리즈 브랜치 작업 ☁️ area: INFRA 인프라/운영/배포 영역 🔥 priority: P0 즉시 처리 필요(서비스/데모 블로커) release:patch 버전 patch bump: X.Y.(Z+1) Infra Team deploy:analysis-server 배포 대상: analysis-web labels Mar 9, 2026
@github-actions github-actions Bot changed the title analysis-server-v0.0.5 [HCR-226] analysis-server-v0.0.5 Mar 9, 2026
@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, 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!

이 Pull Request는 분석 서버의 배포 체계를 namespaced 릴리즈로 전환하고, 핵심 분석 로직을 Kafka 기반으로 재구축하여 처리 효율성과 확장성을 대폭 개선하는 것을 목표로 합니다. 또한, 회원별 키워드 집계 기능과 키워드 추출 정확도 개선을 통해 분석 결과의 유용성을 높였으며, Docker 환경에서의 배치 및 실시간 애플리케이션 실행 모드를 명확히 분리하여 운영 안정성을 강화했습니다. 더불어, 향후 추천 시스템 구축을 위한 기반 API와 상품 임베딩 파이프라인을 추가하여 서비스 확장의 발판을 마련했습니다.

Highlights

  • 키워드 분석 배치 앱의 첫 공식 namespaced 릴리즈: 분석 서버의 배포 체계를 analysis-server-vX.Y.Z 형식의 namespaced 태그로 전환하고, 첫 공식 릴리즈인 analysis-server-v0.0.5를 발행했습니다.
  • Kafka 소비 기반 SQL 분석 워커 전환: 기존 EFS 파일 기반의 분석 워커를 Kafka 메시지 소비 기반으로 전환하여 실시간 처리 및 확장성을 개선했습니다.
  • 회원별 키워드 집계기 및 Stage 2, 3 정확도 개선: 회원별 키워드 집계 기능을 구현하고, 키워드 추출 파이프라인(Stage 2, 3)의 정확도를 개선하여 분석 품질을 향상시켰습니다.
  • 배치 런타임 및 Docker 실행 모드 정리: 배치 런타임 환경과 PostgreSQL 환경 변수 해석 로직을 개선하고, Docker 실행 모드를 realtimebatch로 명확히 분리하여 배포 및 운영 편의성을 높였습니다.
  • 추천 API 및 임베딩 파이프라인 추가: 실시간 추천 API를 추가하고, 상품 임베딩 텍스트 생성 및 벡터 임베딩을 위한 스크립트를 도입하여 추천 시스템의 기반을 마련했습니다.
Changelog
  • .dockerignore
    • Added new ignore patterns for build artifacts and e2e test data.
  • .env.example
    • Added a new example environment file with detailed configurations for runtime, database, OpenAI, and recommendation settings.
  • .gitignore
    • Expanded ignore rules to cover various development artifacts and E2E test data.
  • .husky/pre-commit
    • Added a pre-commit hook to run pnpm test.
  • Dockerfile
    • Refactored to a multi-stage Docker build, added a dedicated application user, and updated the entrypoint to support different application modes.
  • README.md
    • Updated the FastAPI server execution guide and added instructions for running the batch process.
  • app/api/deps.py
    • Removed the dependency injection factory module.
  • app/api/router.py
    • Removed the main API router.
  • app/api/v1/analyze.py
    • Removed the analysis request API endpoint.
  • app/api/v1/health.py
    • Removed the health check API endpoints.
  • app/api/v1/ops.py
    • Removed the operations/status inquiry API endpoint.
  • app/batch/main.py
    • Added a new entrypoint for one-off Kafka batch processing of analysis requests.
  • app/core/config.py
    • Extended application settings to include Kafka, PostgreSQL, OpenAI, and recommendation configurations, and improved environment file loading and database connection string resolution.
  • app/core/database.py
    • Added new database configuration and session management using SQLAlchemy and asyncpg, including pgvector registration.
  • app/infra/efs/jsonl.py
    • Removed the JSONL reader/writer utilities.
  • app/infra/efs/paths.py
    • Removed EFS path utility functions.
  • app/infra/efs/reader.py
    • Removed EFS input file readers.
  • app/infra/efs/writer.py
    • Removed EFS output file writers.
  • app/infra/postgres/analysis_repository.py
    • Added a new repository for querying analysis targets and active keywords from PostgreSQL.
  • app/infra/postgres/client.py
    • Added a new client for creating PostgreSQL connection pools.
  • app/infra/postgres/dispatch_outbox_repository.py
    • Added a new repository for marking dispatch outbox requests as acknowledged in PostgreSQL.
  • app/infra/state/lock.py
    • Removed the process-wide lock utility.
  • app/infra/state/request_registry.py
    • Removed the in-memory request registry.
  • app/main.py
    • Modified to be a backward-compatible entrypoint, delegating to the new app.realtime.main for the realtime API server.
  • app/pipeline/aggregator.py
    • Added a new class ResultAggregator to aggregate keyword analysis results by member ID.
  • app/pipeline/extractor.py
    • Modified the _add_to_automaton method to prevent duplicate registration of the same keyword ID for a given text.
  • app/pipeline/scorer.py
    • Modified the rescue_typos method to include more part-of-speech tags (ADV, X, VERB, ADJ, SCONJ) for noun chunking and added debugging comments.
  • app/realtime/api/router.py
    • Added a new API router for the realtime recommendation API.
  • app/realtime/api/v1/recommendation.py
    • Added a new endpoint /recommendations for handling product recommendation requests.
  • app/realtime/main.py
    • Added a new FastAPI application entrypoint specifically for the realtime recommendation API.
  • app/schemas/aggregate_record.py
    • Removed the aggregate record schema.
  • app/schemas/alias_record.py
    • Removed the alias record schema.
  • app/schemas/analysis_request_message.py
    • Added a new Pydantic schema for Kafka analysis request messages.
  • app/schemas/analyze_request.py
    • Removed the analysis request DTO schema.
  • app/schemas/analyze_response.py
    • Removed the analysis response DTO schema.
  • app/schemas/base.py
    • Removed the common Pydantic base schema.
  • app/schemas/counsel_record.py
    • Removed the counsel record schema.
  • app/schemas/recommendation.py
    • Added new Pydantic schemas for recommendation requests, recommended product items, and recommendation responses.
  • app/schemas/result_record.py
    • Removed the analysis result record schema.
  • app/services/analysis_outcome_service.py
    • Added a new service for building structured analysis outcome messages for Kafka publishing.
  • app/services/analyze_service.py
    • Removed the analysis orchestration service.
  • app/services/idempotency_service.py
    • Removed the idempotency service.
  • app/services/kafka_analysis_consumer_service.py
    • Added a new service for consuming Kafka analysis request messages, processing them, and interacting with PostgreSQL.
  • app/services/kafka_request_consumer_service.py
    • Added a new service for Kafka request consumption, including polling and committing messages.
  • app/services/kafka_result_publisher_service.py
    • Added a new service for publishing analysis results to Kafka.
  • app/services/recommendation_service.py
    • Added a new service for generating product recommendations using OpenAI embeddings and PostgreSQL, including reason generation.
  • app/services/sql_keyword_analysis_service.py
    • Added a new service for SQL-based keyword analysis, integrating ExactMapper, AhoCorasickExtractor, and ContextScorer.
  • docker-compose.local.yaml
    • Added a new Docker Compose file for local development, focusing on the application service.
  • docker-entrypoint.sh
    • Added a new shell script to serve as the Docker entrypoint, allowing selection between realtime and batch application modes.
  • e2e_test_efs/analysis/res/job-2026/chunk-01.chunk.json
    • Removed an e2e test chunk summary file.
  • e2e_test_efs_agg/analysis/res/job-9999/aggregated_summary.json
    • Added a new aggregated summary JSON file for e2e tests.
  • package-lock.json
    • Added a new package-lock file for Node.js dependencies.
  • package.json
    • Updated dev dependencies to include husky.
  • pnpm-lock.yaml
    • Added a new pnpm lock file.
  • requirements.txt
    • Updated Python dependencies, adding libraries for Kafka, LLM integration, and PostgreSQL database interaction.
  • scripts/embed_products.py
    • Added a new script to normalize product data, inject tag contexts, build targeting and upsell summaries, and update embedding_text in the database.
  • scripts/embed_vectors.py
    • Added a new script to generate OpenAI embeddings for product embedding_text and update embedding_vector in the database.
  • scripts/run_keyword_batch_once.py
    • Added a backward-compatible batch entrypoint script.
  • scripts/tag_strategy.csv
    • Added a new CSV file containing detailed tag strategies for product recommendations.
  • tests/test_aggregator.py
    • Added a new test for the ResultAggregator to verify member-wise keyword aggregation.
  • tests/test_e2e.py
    • Modified the end-to-end test to include aggregator validation and fix an Aho-Corasick duplicate count bug.
Activity
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 by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

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 pull request 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

  1. 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.

@tkv00 tkv00 merged commit 452896d into main Mar 9, 2026
5 of 7 checks passed

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This PR is an impressive refactoring that transitions the analysis server's architecture from EFS file-based to Kafka and PostgreSQL-based, incorporating improvements like multi-stage Docker builds, robust entrypoint scripts, and a real-time recommendation API. However, significant security concerns have been identified: a prompt injection vulnerability in the recommendation service and a lack of authentication on the new recommendation API endpoint. Additionally, some legacy code from the previous architecture remains, which could lead to runtime errors, and there are opportunities to enhance the performance and code design of the new Kafka consumer. These security issues should be addressed before deployment to production.

from typing import Dict, Any, List

from app.core.config import Settings
from app.infra.efs.paths import build_res_dir

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

이 파일에서 app.infra.efs.paths 모듈의 build_res_dir 함수를 가져오고 있습니다. 하지만 이번 PR에서 app.infra.efs/paths.py 파일이 삭제되어, 애플리케이션 실행 시 ImportError가 발생할 것입니다. 이 집계기(Aggregator)는 이전 EFS 기반 아키텍처에 의존하는 것으로 보이며, 현재의 Kafka/DB 기반 흐름과 맞지 않습니다. 이 파일과 관련 테스트(tests/test_aggregator.py)가 여전히 필요한지 확인하고, 필요하다면 EFS 의존성을 제거하거나, 필요 없다면 삭제하는 것을 고려해 주세요.

Comment thread tests/test_e2e.py
from pathlib import Path

from app.core.config import Settings
from app.services.analyze_service import AnalyzeService

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

E2E 테스트에서 app.services.analyze_serviceAnalyzeService를 가져오고 있습니다. 이 서비스는 이번 PR에서 SqlKeywordAnalysisService 등으로 대체되면서 삭제되었습니다. 따라서 이 테스트는 실행 시 ImportError로 인해 실패할 것입니다. 새로운 아키텍처에 맞게 E2E 테스트를 수정하거나, 더 이상 유효하지 않다면 삭제해야 합니다.

Comment on lines +44 to +52
prompt = f"""사용자 프로필: {profile_text}

아래 상품들을 이 프로필에 맞춰 추천했습니다. 각 상품을 왜 추천했는지 한 문장으로만 설명해주세요.
상품 목록:
{product_list}

응답은 반드시 JSON만 주세요. 다른 말 없이 예시 형식만 따르세요.
예시: {{"reasons": ["이유1", "이유2", "이유3"]}}
"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The profile_text provided by the user is directly concatenated into the LLM prompt in the _generate_recommendation_reasons function. This allows an attacker to perform prompt injection attacks to manipulate the LLM's behavior, potentially leading to the generation of malicious content or bypassing intended constraints.

Remediation: Sanitize the profile_text input and use a more robust prompt structure. Consider using system messages to define the LLM's role and constraints, and use delimiters to clearly separate user input from instructions.

Comment on lines +133 to +188
async def _process_batch(self, batch: list[AnalysisRequestMessage]) -> None:
assert self._analysis_repository is not None
assert self._outbox_repository is not None
assert self._analysis_service is not None

unique_request_ids = list(dict.fromkeys(msg.dispatch_request_id for msg in batch))
acked_request_ids = await self._outbox_repository.mark_acked_by_request_ids(unique_request_ids)
acked_count = len(acked_request_ids)

unique_pairs = list(dict.fromkeys((msg.case_id, msg.analyzer_version) for msg in batch))
case_ids = [pair[0] for pair in unique_pairs]
analyzer_versions = [pair[1] for pair in unique_pairs]
target_rows = await self._analysis_repository.find_targets_by_case_and_version(case_ids, analyzer_versions)
target_by_pair = {
(int(row["case_id"]), int(row["analyzer_version"])): row
for row in target_rows
}
missing_pairs = [pair for pair in unique_pairs if pair not in target_by_pair]

keyword_rows = await self._analysis_repository.load_active_keyword_rows()
keyword_dict_rows = [dict(row) for row in keyword_rows]
self._analysis_service.load_dictionary(keyword_dict_rows)
keyword_name_by_id: dict[int, str] = {}
for row in keyword_dict_rows:
keyword_id = int(row["business_keyword_id"])
if keyword_id not in keyword_name_by_id:
keyword_name_by_id[keyword_id] = str(row["keyword_name"])

targets = [dict(row) for row in target_rows]
mapping_rows, completed_ids, failed_items = self._analysis_service.analyze_targets(targets)
# DB write 권한은 Spring에만 있으므로 Python에서는 결과를 DB에 반영하지 않는다.
# (business_keyword_mapping_result INSERT, consultation_analysis status UPDATE 미수행)

if self._settings.kafka_log_each_message:
self._log_message_outcomes(
batch=batch,
acked_request_ids=acked_request_ids,
target_by_pair=target_by_pair,
mapping_rows=mapping_rows,
completed_ids=completed_ids,
failed_items=failed_items,
keyword_name_by_id=keyword_name_by_id,
)

logger.info(
"Kafka batch consumed. messages=%d unique_requests=%d acked=%d unique_pairs=%d loaded_targets=%d missing_pairs=%d completed=%d failed=%d mappings=%d (only-outbox-write=enabled)",
len(batch),
len(unique_request_ids),
acked_count,
len(unique_pairs),
len(target_rows),
len(missing_pairs),
len(completed_ids),
len(failed_items),
len(mapping_rows),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

_process_batch 메서드는 Kafka 메시지 배치를 처리할 때마다 데이터베이스에서 전체 키워드 사전을 다시 로드하고(load_active_keyword_rows), 분석 엔진(mapper, extractor)을 새로 빌드하고 있습니다 (load_dictionary). 장시간 실행되는 Kafka 컨슈머의 경우, 이는 매우 비효율적이며 불필요한 DB 부하와 성능 저하를 유발할 수 있습니다. 키워드 사전은 애플리케이션 시작 시 한 번만 로드하거나, 주기적으로 갱신되는 캐시 레이어를 두는 것을 강력히 권장합니다.

Comment on lines +11 to +20
@router.post("/recommendations", response_model=RecommendationResponse)
async def post_recommendations(
body: RecommendationRequest,
session: AsyncSession = Depends(get_db_session),
) -> RecommendationResponse:
return await get_recommendation(
session=session,
member_id=body.member_id,
profile_text=body.profile_text,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The /recommendations endpoint is exposed without any authentication or authorization mechanisms. This allows any unauthenticated user to call the service, which could lead to unauthorized use of the recommendation logic and potential abuse of the underlying LLM API, leading to increased costs or denial of service.

Remediation: Implement an authentication layer (e.g., JWT, API Keys) and ensure that only authorized users or internal services can access this endpoint.

Comment thread app/batch/main.py
Comment on lines +28 to +31
service._db_pool = await create_postgres_pool(settings) # noqa: SLF001
service._analysis_repository = AnalysisRepository(service._db_pool) # noqa: SLF001
service._outbox_repository = DispatchOutboxRepository(service._db_pool) # noqa: SLF001
service._analysis_service = SqlKeywordAnalysisService() # noqa: SLF001

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

KafkaAnalysisConsumerService의 의존성을 주입하기 위해 _db_pool과 같은 보호된(protected) 멤버에 직접 접근하고 있습니다. 이는 캡슐화를 위반하며 코드의 유지보수성을 떨어뜨립니다. noqa: SLF001 주석이 이를 암시하고 있습니다.

KafkaAnalysisConsumerService__init__ 메서드를 통해 의존성을 명시적으로 주입하도록 리팩터링하는 것을 제안합니다. 이렇게 하면 클래스의 역할과 책임이 더 명확해지고 테스트하기 쉬운 구조가 됩니다.

@tkv00 tkv00 deleted the release/HSC-251 branch March 20, 2026 01:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

☁️ area: INFRA 인프라/운영/배포 영역 deploy:analysis-server 배포 대상: analysis-web Infra Team 🔥 priority: P0 즉시 처리 필요(서비스/데모 블로커) release:patch 버전 patch bump: X.Y.(Z+1) 🏷️ release 릴리즈 준비/버전 태깅/릴리즈 노트/릴리즈 브랜치 작업

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants