From 4944855210671670380d7f51ba0fc4878533b236 Mon Sep 17 00:00:00 2001 From: cyjadela Date: Wed, 29 Oct 2025 17:45:37 +0900 Subject: [PATCH 01/10] =?UTF-8?q?Chore:=20ai-python=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=ED=8F=AC=ED=8A=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ai-python/API-DOCUMENTATION.md | 8 ++++---- ai-python/README.md | 8 ++++---- ai-python/app/config.py | 2 +- ai-python/restart.sh | 32 ++++++++++++++++---------------- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/ai-python/API-DOCUMENTATION.md b/ai-python/API-DOCUMENTATION.md index 77fed86..50dd5d5 100644 --- a/ai-python/API-DOCUMENTATION.md +++ b/ai-python/API-DOCUMENTATION.md @@ -1,9 +1,9 @@ # AI Service API Documentation ## 서비스 정보 -- **Base URL**: `http://localhost:8086` -- **프로덕션 URL**: `http://{AKS-IP}:8086` (배포 후) -- **포트**: 8086 +- **Base URL**: `http://localhost:8087` +- **프로덕션 URL**: `http://{AKS-IP}:8087` (배포 후) +- **포트**: 8087 - **프로토콜**: HTTP - **CORS**: 모든 origin 허용 (개발 환경) @@ -285,7 +285,7 @@ A: Redis에 충분한 텍스트(10개 세그먼트)가 축적되어야 분석이 **요청 예시 (curl)**: ```bash -curl -X POST "http://localhost:8086/api/v1/ai/summary/generate" \ +curl -X POST "http://localhost:8087/api/v1/ai/summary/generate" \ -H "Content-Type: application/json" \ -d '{ "text": "오늘 회의에서는 프로젝트 일정과 예산에 대해 논의했습니다...", diff --git a/ai-python/README.md b/ai-python/README.md index 87fd8ad..2b8a796 100644 --- a/ai-python/README.md +++ b/ai-python/README.md @@ -56,10 +56,10 @@ python3 main.py ```bash # 헬스 체크 -curl http://localhost:8086/health +curl http://localhost:8087/health # SSE 스트림 테스트 -curl -N http://localhost:8086/api/v1/ai/suggestions/meetings/test-meeting/stream +curl -N http://localhost:8087/api/v1/ai/suggestions/meetings/test-meeting/stream ``` ## 📡 API 엔드포인트 @@ -123,7 +123,7 @@ ai-python/ | `REDIS_HOST` | Redis 호스트 | 20.249.177.114 | | `REDIS_PORT` | Redis 포트 | 6379 | | `EVENTHUB_CONNECTION_STRING` | Event Hub 연결 문자열 | (선택) | -| `PORT` | 서비스 포트 | 8086 | +| `PORT` | 서비스 포트 | 8087 | ## 🔍 동작 원리 @@ -136,7 +136,7 @@ ai-python/ ```bash # Event Hub 없이 SSE만 테스트 (Mock 데이터) -curl -N http://localhost:8086/api/v1/ai/suggestions/meetings/test-meeting/stream +curl -N http://localhost:8087/api/v1/ai/suggestions/meetings/test-meeting/stream # 5초마다 샘플 제안사항이 발행됩니다 ``` diff --git a/ai-python/app/config.py b/ai-python/app/config.py index f5a62de..ccfa815 100644 --- a/ai-python/app/config.py +++ b/ai-python/app/config.py @@ -10,7 +10,7 @@ class Settings(BaseSettings): # 서버 설정 app_name: str = "AI Service (Python)" host: str = "0.0.0.0" - port: int = 8086 + port: int = 8087 # Claude API claude_api_key: str = "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" diff --git a/ai-python/restart.sh b/ai-python/restart.sh index 78afc44..b166c1a 100755 --- a/ai-python/restart.sh +++ b/ai-python/restart.sh @@ -1,7 +1,7 @@ #!/bin/bash # AI Python 서비스 재시작 스크립트 -# 8086 포트로 깔끔하게 재시작 +# 8087 포트로 깔끔하게 재시작 echo "==================================" echo "AI Python 서비스 재시작" @@ -18,23 +18,23 @@ sleep 2 # 2. 포트 확인 echo "2️⃣ 포트 상태 확인..." -if lsof -i:8086 > /dev/null 2>&1; then - echo " ⚠️ 8086 포트가 아직 사용 중입니다." +if lsof -i:8087 > /dev/null 2>&1; then + echo " ⚠️ 8087 포트가 아직 사용 중입니다." echo " 강제 종료 시도..." - PID=$(lsof -ti:8086) + PID=$(lsof -ti:8087) if [ ! -z "$PID" ]; then kill -9 $PID sleep 2 fi fi -if lsof -i:8086 > /dev/null 2>&1; then - echo " ❌ 8086 포트를 해제할 수 없습니다." +if lsof -i:8087 > /dev/null 2>&1; then + echo " ❌ 8087 포트를 해제할 수 없습니다." echo " 시스템 재부팅 후 다시 시도하거나," echo " 다른 포트를 사용하세요." exit 1 else - echo " ✅ 8086 포트 사용 가능" + echo " ✅ 8087 포트 사용 가능" fi # 3. 가상환경 활성화 @@ -51,7 +51,7 @@ echo " ✅ 가상환경 활성화 완료" mkdir -p ../logs # 5. 서비스 시작 -echo "4️⃣ AI Python 서비스 시작 (포트: 8086)..." +echo "4️⃣ AI Python 서비스 시작 (포트: 8087)..." nohup python3 main.py > ../logs/ai-python.log 2>&1 & PID=$! @@ -76,16 +76,16 @@ else fi # 포트 확인 -if lsof -i:8086 > /dev/null 2>&1; then - echo " ✅ 8086 포트 리스닝 중" +if lsof -i:8087 > /dev/null 2>&1; then + echo " ✅ 8087 포트 리스닝 중" else - echo " ⚠️ 8086 포트 아직 준비 중..." + echo " ⚠️ 8087 포트 아직 준비 중..." fi # Health 체크 echo "7️⃣ Health Check..." sleep 2 -HEALTH=$(curl -s http://localhost:8086/health 2>/dev/null) +HEALTH=$(curl -s http://localhost:8087/health 2>/dev/null) if [ $? -eq 0 ]; then echo " ✅ Health Check 성공" @@ -103,13 +103,13 @@ echo "✅ AI Python 서비스 시작 완료" echo "==================================" echo "📊 서비스 정보:" echo " - PID: $PID" -echo " - 포트: 8086" +echo " - 포트: 8087" echo " - 로그: tail -f ../logs/ai-python.log" echo "" echo "📡 엔드포인트:" -echo " - Health: http://localhost:8086/health" -echo " - Root: http://localhost:8086/" -echo " - Swagger: http://localhost:8086/swagger-ui.html" +echo " - Health: http://localhost:8087/health" +echo " - Root: http://localhost:8087/" +echo " - Swagger: http://localhost:8087/swagger-ui.html" echo "" echo "🛑 서비스 중지: pkill -f 'python.*main.py'" echo "==================================" From 8bb91f646f616ba2402793cc909e82bd59d2ad98 Mon Sep 17 00:00:00 2001 From: djeon Date: Wed, 29 Oct 2025 21:50:16 +0900 Subject: [PATCH 02/10] =?UTF-8?q?feat:=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EC=9A=A9=EC=96=B4=EC=84=A4=EB=AA=85=20=EC=A1=B0=ED=9A=8C=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 --- rag/IMPLEMENTATION_SUMMARY.md | 441 ------------- rag/README.md | 132 ---- rag/README_RAG_MINUTES.md | 375 ----------- rag/TESTING.md | 508 --------------- rag/eventhub_guide.md | 378 ----------- rag/install-pgvector.md | 595 ------------------ rag/requirements.txt | 1 + rag/{ => scripts}/check_active_consumers.py | 0 rag/src/api/__pycache__/main.cpython-311.pyc | Bin 25544 -> 29792 bytes .../__pycache__/term_routes.cpython-311.pyc | Bin 0 -> 4392 bytes rag/src/api/main.py | 92 +++ rag/src/api/term_routes.py | 93 +++ .../eventhub_consumer.cpython-311.pyc | Bin 21390 -> 25793 bytes .../__pycache__/sse_manager.cpython-311.pyc | Bin 0 -> 8921 bytes rag/src/services/eventhub_consumer.py | 100 ++- rag/src/services/sse_manager.py | 183 ++++++ rag/start_all.sh | 47 -- rag/start_all_services.py | 180 ------ rag/start_consumer.py | 62 -- rag/test_noun_extraction.py | 37 -- rag/test_sse.html | 573 +++++++++++++++++ 21 files changed, 1038 insertions(+), 2759 deletions(-) delete mode 100644 rag/IMPLEMENTATION_SUMMARY.md delete mode 100644 rag/README.md delete mode 100644 rag/README_RAG_MINUTES.md delete mode 100644 rag/TESTING.md delete mode 100644 rag/eventhub_guide.md delete mode 100644 rag/install-pgvector.md rename rag/{ => scripts}/check_active_consumers.py (100%) create mode 100644 rag/src/api/__pycache__/term_routes.cpython-311.pyc create mode 100644 rag/src/api/term_routes.py create mode 100644 rag/src/services/__pycache__/sse_manager.cpython-311.pyc create mode 100644 rag/src/services/sse_manager.py delete mode 100644 rag/start_all.sh delete mode 100644 rag/start_all_services.py delete mode 100644 rag/start_consumer.py delete mode 100644 rag/test_noun_extraction.py create mode 100644 rag/test_sse.html diff --git a/rag/IMPLEMENTATION_SUMMARY.md b/rag/IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index 92389ee..0000000 --- a/rag/IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,441 +0,0 @@ -# Vector DB 통합 시스템 구현 완료 보고서 - -## 프로젝트 개요 - -**목표**: 용어집(Term Glossary)과 관련자료(Related Documents) 검색을 위한 Vector DB 기반 통합 시스템 개발 - -**구현 기간**: 2025년 (프로젝트 완료) - -**기술 스택**: -- **Backend**: Python 3.9+, FastAPI -- **Vector DB (용어집)**: PostgreSQL 14+ with pgvector -- **Vector DB (관련자료)**: Azure AI Search -- **AI Services**: Azure OpenAI (임베딩), Claude 3.5 Sonnet (설명 생성) -- **Cache**: Redis (설정 완료, 구현 대기) - ---- - -## 구현 완료 항목 - -### ✅ 1. 프로젝트 구조 및 의존성 설정 -- **디렉토리 구조**: - ``` - vector/ - ├── src/ - │ ├── models/ # 데이터 모델 - │ ├── db/ # 데이터베이스 레이어 - │ ├── services/ # 비즈니스 로직 - │ ├── api/ # REST API - │ └── utils/ # 유틸리티 - ├── scripts/ # 데이터 로딩 스크립트 - ├── tests/ # 테스트 코드 - ├── config.yaml # 설정 파일 - ├── requirements.txt # 의존성 - └── README.md # 문서 - ``` - -- **주요 파일**: - - `requirements.txt`: 15개 핵심 패키지 정의 - - `config.yaml`: 환경별 설정 관리 - - `.env.example`: 환경 변수 템플릿 - -### ✅ 2. 데이터 모델 및 스키마 정의 - -**용어집 모델** (`src/models/term.py`): -- `Term`: 용어 기본 정보 + 벡터 임베딩 -- `TermSearchRequest`: 검색 요청 (keyword/vector/hybrid) -- `TermSearchResult`: 검색 결과 + 관련도 점수 -- `TermExplanation`: Claude AI 생성 설명 - -**관련자료 모델** (`src/models/document.py`): -- `Document`: 문서 메타데이터 및 전체 내용 -- `DocumentChunk`: 문서 청크 (2000 토큰 단위) -- `DocumentSearchRequest`: 하이브리드 검색 요청 -- `DocumentSearchResult`: 검색 결과 + 시맨틱 점수 - -### ✅ 3. 용어집 Vector DB 구현 (PostgreSQL + pgvector) - -**구현 파일**: `src/db/postgres_vector.py` - -**핵심 기능**: -- ✅ 데이터베이스 초기화 (테이블, 인덱스 자동 생성) -- ✅ 용어 삽입/업데이트 (UPSERT) -- ✅ 키워드 검색 (ILIKE, 유사도 점수) -- ✅ 벡터 검색 (코사인 유사도) -- ✅ 카테고리별 통계 -- ✅ 평균 신뢰도 계산 - -**테이블 스키마**: -```sql -CREATE TABLE terms ( - term_id VARCHAR(255) PRIMARY KEY, - term_name VARCHAR(255) NOT NULL, - normalized_name VARCHAR(255), - category VARCHAR(100), - definition TEXT, - context TEXT, - synonyms TEXT[], - related_terms TEXT[], - document_source JSONB, - confidence_score FLOAT, - usage_count INT, - last_updated TIMESTAMP, - embedding vector(1536), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); -``` - -**인덱스**: -- B-tree: term_name, normalized_name, category -- GIN: synonyms -- IVFFlat: embedding (벡터 유사도 검색용) - -### ✅ 4. 관련자료 Vector DB 구현 (Azure AI Search) - -**구현 파일**: `src/db/azure_search.py` - -**핵심 기능**: -- ✅ 인덱스 생성 (벡터 필드 + 시맨틱 설정) -- ✅ 문서 청크 업로드 (배치 처리) -- ✅ 하이브리드 검색 (키워드 + 벡터 + 시맨틱 랭킹) -- ✅ 필터링 (폴더, 문서타입, 날짜) -- ✅ 통계 조회 (문서 수, 타입별 분포) - -**인덱스 스키마**: -- **필드**: id, document_id, document_type, title, folder, created_date, participants, keywords, agenda_id, agenda_title, chunk_index, content, content_vector, token_count -- **벡터 설정**: 1536차원, 코사인 유사도 -- **시맨틱 설정**: title + content 우선순위 - -### ✅ 5. 데이터 로딩 및 임베딩 생성 - -**용어집 로딩** (`scripts/load_terms.py`): -- ✅ JSON 파일 파싱 (terms-01.json ~ terms-04.json) -- ✅ 임베딩 생성 (용어명 + 정의 + 맥락) -- ✅ PostgreSQL 삽입 -- ✅ 통계 출력 - -**관련자료 로딩** (`scripts/load_documents.py`): -- ✅ JSON 파일 파싱 (meet-ref.json) -- ✅ 문서 청킹 (2000 토큰 단위, 문단 기준) -- ✅ 임베딩 생성 (청크별) -- ✅ Azure AI Search 업로드 -- ✅ 통계 출력 - -**임베딩 생성기** (`src/utils/embedding.py`): -- ✅ Azure OpenAI API 연동 -- ✅ 단일/배치 임베딩 생성 -- ✅ 재시도 로직 (Exponential Backoff) -- ✅ 토큰 카운팅 -- ✅ 오류 처리 - -### ✅ 6. 검색 API 및 서비스 구현 - -**FastAPI 애플리케이션** (`src/api/main.py`): - -**용어집 엔드포인트**: -- `POST /api/terms/search`: 하이브리드 검색 (keyword/vector/hybrid) -- `GET /api/terms/{term_id}`: 용어 상세 조회 -- `POST /api/terms/{term_id}/explain`: Claude AI 설명 생성 -- `GET /api/terms/stats`: 통계 조회 - -**관련자료 엔드포인트**: -- `POST /api/documents/search`: 하이브리드 검색 + 시맨틱 랭킹 -- `GET /api/documents/stats`: 통계 조회 - -**주요 기능**: -- ✅ 의존성 주입 (Database, Embedding, Claude Service) -- ✅ CORS 설정 -- ✅ 에러 핸들링 -- ✅ 로깅 -- ✅ OpenAPI 문서 자동 생성 - -### ✅ 7. Claude AI 연동 구현 - -**Claude 서비스** (`src/services/claude_service.py`): - -**구현 기능**: -- ✅ 용어 설명 생성 (2-3문장, 회의 맥락 반영) -- ✅ 유사 회의록 요약 (3문장, 환각 방지) -- ✅ 재시도 로직 (최대 3회) -- ✅ Fallback 메커니즘 -- ✅ 토큰 사용량 추적 - -**프롬프트 엔지니어링**: -- 시스템 프롬프트: 역할 정의, 출력 형식 제약 -- 사용자 프롬프트: 구조화된 정보 제공 -- 환각 방지: "실제로 다뤄진 내용만 포함" 명시 - -### ✅ 8. 테스트 및 샘플 데이터 검증 - -**테스트 코드**: -- `tests/test_api.py`: API 엔드포인트 통합 테스트 (10개 테스트 케이스) -- `tests/test_data_loading.py`: 데이터 로딩 및 임베딩 생성 검증 - -**검증 스크립트**: -- `scripts/validate_setup.py`: 설정 검증 자동화 스크립트 - - Python 버전 확인 - - 프로젝트 구조 확인 - - 의존성 패키지 확인 - - 환경 변수 확인 - - 샘플 데이터 파일 확인 - -**테스트 가이드**: -- `TESTING.md`: 상세한 테스트 절차 및 문제 해결 가이드 - ---- - -## 기술적 의사결정 - -### 1. 하이브리드 아키텍처 선택 - -**결정**: PostgreSQL + pgvector (용어집) + Azure AI Search (관련자료) - -**이유**: -- **용어집**: 소규모 데이터, 키워드 검색 중요 → PostgreSQL 적합 -- **관련자료**: 대규모 문서, 시맨틱 검색 필요 → Azure AI Search 적합 -- 각 용도에 최적화된 기술 선택으로 성능 극대화 - -### 2. 하이브리드 검색 전략 - -**용어집**: -- 키워드 검색: ILIKE 기반 유사도 계산 -- 벡터 검색: 코사인 유사도 -- 하이브리드: 가중 평균 (keyword_weight: 0.4, vector_weight: 0.6) - -**관련자료**: -- Azure AI Search의 Hybrid Search + Semantic Ranking 활용 -- 키워드 + 벡터 + L2 시맨틱 리랭킹 - -### 3. 청킹 전략 - -**기준**: 2000 토큰 단위, 문단 경계 존중 - -**장점**: -- 의미 단위 분할로 컨텍스트 보존 -- 임베딩 품질 향상 -- 검색 정확도 개선 - -### 4. 에러 처리 및 Fallback - -**임베딩 생성**: -- Exponential Backoff (최대 3회 재시도) -- Rate Limit 대응 - -**Claude AI**: -- API 실패 시 기본 정의 + 맥락 반환 -- 사용자 경험 저하 방지 - ---- - -## 주요 파일 구조 - -``` -vector/ -├── src/ -│ ├── models/ -│ │ ├── term.py # 용어집 데이터 모델 -│ │ └── document.py # 관련자료 데이터 모델 -│ ├── db/ -│ │ ├── postgres_vector.py # PostgreSQL + pgvector 구현 -│ │ └── azure_search.py # Azure AI Search 구현 -│ ├── services/ -│ │ └── claude_service.py # Claude AI 서비스 -│ ├── api/ -│ │ └── main.py # FastAPI 애플리케이션 -│ └── utils/ -│ ├── config.py # 설정 관리 -│ └── embedding.py # 임베딩 생성 -├── scripts/ -│ ├── load_terms.py # 용어집 데이터 로딩 -│ ├── load_documents.py # 관련자료 데이터 로딩 -│ └── validate_setup.py # 설정 검증 -├── tests/ -│ ├── test_api.py # API 테스트 -│ └── test_data_loading.py # 데이터 로딩 테스트 -├── config.yaml # 설정 파일 -├── requirements.txt # 의존성 -├── .env.example # 환경 변수 템플릿 -├── README.md # 프로젝트 문서 -├── TESTING.md # 테스트 가이드 -└── IMPLEMENTATION_SUMMARY.md # 본 문서 -``` - ---- - -## API 엔드포인트 요약 - -### 용어집 API - -| Method | Endpoint | 설명 | -|--------|----------|------| -| POST | `/api/terms/search` | 용어 하이브리드 검색 | -| GET | `/api/terms/{term_id}` | 용어 상세 조회 | -| POST | `/api/terms/{term_id}/explain` | Claude AI 설명 생성 | -| GET | `/api/terms/stats` | 용어 통계 | - -### 관련자료 API - -| Method | Endpoint | 설명 | -|--------|----------|------| -| POST | `/api/documents/search` | 문서 하이브리드 검색 | -| GET | `/api/documents/stats` | 문서 통계 | - ---- - -## 성능 특성 - -### 용어집 검색 -- **키워드 검색**: ~10ms (100개 용어 기준) -- **벡터 검색**: ~50ms (IVFFlat 인덱스) -- **하이브리드 검색**: ~60ms - -### 관련자료 검색 -- **하이브리드 검색**: ~100-200ms -- **시맨틱 랭킹**: +50ms - -### 임베딩 생성 -- **단일 텍스트**: ~200ms -- **배치 (50개)**: ~1-2초 - -### Claude AI 설명 -- **평균 응답 시간**: 2-5초 -- **토큰 사용량**: 500-1000 토큰 - ---- - -## 다음 단계 (권장사항) - -### 즉시 실행 가능 -1. **환경 설정**: - ```bash - python scripts/validate_setup.py - ``` - -2. **데이터 로딩**: - ```bash - python scripts/load_terms.py - python scripts/load_documents.py - ``` - -3. **API 서버 실행**: - ```bash - python -m src.api.main - # 또는 - uvicorn src.api.main:app --reload - ``` - -4. **테스트 실행**: - ```bash - pytest tests/ -v - ``` - -### 단기 개선 (1-2주) -- [ ] Redis 캐싱 활성화 (설정 완료, 구현 필요) -- [ ] API 인증/인가 추가 -- [ ] 로깅 시스템 고도화 (구조화된 로그) -- [ ] 성능 모니터링 (Prometheus/Grafana) - -### 중기 개선 (1-2개월) -- [ ] 용어 버전 관리 -- [ ] 문서 업데이트 자동화 (웹훅 또는 스케줄러) -- [ ] 사용자 피드백 기반 관련도 학습 -- [ ] A/B 테스트 프레임워크 - -### 장기 개선 (3개월+) -- [ ] 다국어 지원 (한국어/영어) -- [ ] 그래프 DB 통합 (용어 관계 시각화) -- [ ] 실시간 회의록 생성 (STT 연동) -- [ ] 지식 그래프 자동 구축 - ---- - -## 품질 메트릭 - -### 코드 커버리지 -- 데이터 모델: 100% -- DB 레이어: 90% -- API 레이어: 85% -- 서비스 레이어: 80% - -### 검색 품질 -- 용어집 정확도: 평가 필요 (사용자 피드백) -- 문서 검색 정확도: 평가 필요 (사용자 피드백) -- Claude 설명 품질: 평가 필요 (전문가 리뷰) - ---- - -## 의존성 요약 - -### 핵심 라이브러리 -- **Web Framework**: fastapi, uvicorn -- **Database**: psycopg2-binary, pgvector -- **AI Services**: openai (Azure OpenAI), anthropic (Claude) -- **Azure**: azure-search-documents, azure-core, azure-identity -- **Cache**: redis -- **Data**: pydantic, pyyaml -- **Utilities**: tenacity (retry), tiktoken (tokenizer) - -### 개발/테스트 -- pytest -- httpx (API 테스트) - ---- - -## 보안 고려사항 - -### 현재 구현 -- ✅ 환경 변수로 API 키 관리 -- ✅ .env 파일 gitignore 처리 -- ✅ SQL Injection 방지 (파라미터화된 쿼리) - -### 개선 필요 -- [ ] API 키 로테이션 자동화 -- [ ] Rate Limiting -- [ ] API 인증/인가 (JWT, OAuth2) -- [ ] 입력 검증 강화 -- [ ] HTTPS 강제 -- [ ] 감사 로그 - ---- - -## 비용 예측 (월별) - -### Azure OpenAI (임베딩) -- 모델: text-embedding-ada-002 -- 비용: $0.0001 / 1K 토큰 -- 예상: 100만 토큰/월 → **$0.10** - -### Azure AI Search -- 티어: Basic -- 비용: ~$75/월 -- 예상: **$75** - -### Claude API -- 모델: claude-3-5-sonnet -- 비용: $3 / 1M 입력 토큰, $15 / 1M 출력 토큰 -- 예상: 10만 토큰/월 → **$1-2** - -### 총 예상 비용: **~$80-85/월** - ---- - -## 결론 - -Vector DB 통합 시스템이 성공적으로 구현되었습니다. 용어집과 관련자료 검색을 위한 하이브리드 아키텍처를 채택하여 각 용도에 최적화된 성능을 제공합니다. - -**주요 성과**: -- ✅ 8개 주요 컴포넌트 완전 구현 -- ✅ 10개 REST API 엔드포인트 -- ✅ 포괄적인 테스트 스위트 -- ✅ 상세한 문서화 -- ✅ 프로덕션 준비 코드 - -**다음 단계**: -1. 환경 설정 및 검증 -2. 데이터 로딩 -3. API 서버 실행 -4. 통합 테스트 -5. 프로덕션 배포 - -모든 소스 코드와 문서는 `/Users/daewoong/home/workspace/HGZero/vector/` 디렉토리에 있습니다. diff --git a/rag/README.md b/rag/README.md deleted file mode 100644 index 3c35e6e..0000000 --- a/rag/README.md +++ /dev/null @@ -1,132 +0,0 @@ -# Vector DB 통합 시스템 - -## 개요 -회의록 작성 시스템을 위한 Vector DB 기반 용어집 및 관련자료 검색 시스템 - -## 주요 기능 -1. **용어집 (Term Glossary)** - - PostgreSQL + pgvector 기반 - - 맥락 기반 용어 설명 제공 - - Claude AI 연동 - -2. **관련자료 (Related Documents)** - - Azure AI Search 기반 (별도 인덱스) - - Hybrid Search + Semantic Ranking - - 회의록 유사도 검색 - -## 기술 스택 -- Python 3.11+ -- FastAPI (REST API) -- PostgreSQL + pgvector (용어집) -- Azure AI Search (관련자료) -- Azure OpenAI (Embedding) -- Claude 3.5 Sonnet (LLM) -- Redis (캐싱) - -## 프로젝트 구조 -``` -vector/ -├── src/ -│ ├── models/ # 데이터 모델 -│ ├── db/ # DB 연동 (PostgreSQL, Azure Search) -│ ├── services/ # 비즈니스 로직 -│ ├── api/ # REST API -│ └── utils/ # 유틸리티 (임베딩, 설정 등) -├── scripts/ # 초기화 및 데이터 로딩 스크립트 -└── tests/ # 테스트 -``` - -## 설치 및 실행 - -### 1. 환경 설정 -```bash -# .env 파일 생성 -cp .env.example .env - -# .env 파일을 열어 실제 API 키 및 데이터베이스 정보 입력 -# - POSTGRES_* (PostgreSQL 접속 정보) -# - AZURE_OPENAI_* (Azure OpenAI API 키 및 엔드포인트) -# - AZURE_SEARCH_* (Azure AI Search API 키 및 엔드포인트) -# - CLAUDE_API_KEY (Claude API 키) -``` - -### 2. 의존성 설치 -```bash -# 가상환경 생성 (권장) -python -m venv venv -source venv/bin/activate # Linux/Mac -# venv\Scripts\activate # Windows - -# 패키지 설치 -pip install -r requirements.txt -``` - -### 3. 설정 검증 -```bash -# 모든 설정이 올바른지 확인 -python scripts/validate_setup.py -``` - -### 4. 데이터 로딩 -```bash -# 용어집 데이터 로딩 (PostgreSQL 테이블 자동 생성 및 데이터 삽입) -python scripts/load_terms.py - -# 관련자료 데이터 로딩 (Azure AI Search 인덱스 생성 및 데이터 업로드) -python scripts/load_documents.py -``` - -### 5. API 서버 실행 -```bash -# 방법 1: 직접 실행 -python -m src.api.main - -# 방법 2: uvicorn 사용 (개발 모드) -uvicorn src.api.main:app --reload --host 0.0.0.0 --port 8000 -``` - -### 6. API 문서 확인 -브라우저에서 다음 주소로 접속: -- Swagger UI: http://localhost:8000/docs -- ReDoc: http://localhost:8000/redoc - -## API 엔드포인트 - -### 용어집 API -- `POST /api/terms/search` - 용어 검색 -- `GET /api/terms/{term_id}` - 용어 상세 조회 -- `POST /api/terms/{term_id}/explain` - 맥락 기반 용어 설명 (Claude AI) - -### 관련자료 API -- `POST /api/documents/search` - 관련 문서 검색 (Hybrid Search) -- `GET /api/documents/related/{meeting_id}` - 관련 회의록 추천 -- `POST /api/documents/{doc_id}/summarize` - 유사 내용 요약 (Claude AI) - -## 테스트 - -### 설정 검증 테스트 -```bash -# 환경 설정 및 의존성 확인 -python scripts/validate_setup.py -``` - -### 데이터 로딩 테스트 -```bash -# 데이터 파일 로드 및 임베딩 생성 검증 -python tests/test_data_loading.py -``` - -### API 테스트 -```bash -# API 서버가 실행 중인 상태에서: -pytest tests/test_api.py -v -``` - -자세한 테스트 가이드는 [TESTING.md](TESTING.md) 참조 - -## 문서 -- [TESTING.md](TESTING.md) - 상세한 테스트 가이드 및 문제 해결 -- [IMPLEMENTATION_SUMMARY.md](IMPLEMENTATION_SUMMARY.md) - 구현 완료 보고서 -- [용어집 구현방안](../design/구현방안-용어집.md) -- [관련자료 구현방안](../design/구현방안-관련자료.md) -- [아키텍처 최적안 결정](../design/아키텍처_최적안_결정.md) diff --git a/rag/README_RAG_MINUTES.md b/rag/README_RAG_MINUTES.md deleted file mode 100644 index 508084c..0000000 --- a/rag/README_RAG_MINUTES.md +++ /dev/null @@ -1,375 +0,0 @@ -# RAG 회의록 서비스 - -회의록 RAG(Retrieval-Augmented Generation) 서비스는 확정된 회의록을 embedding 벡터와 함께 저장하고, 유사한 회의록을 검색할 수 있는 기능을 제공합니다. - -## 아키텍처 - -``` -Meeting Service RAG Service - | | - | 1. 회의록 확정 | - | | - v | -Event Hub --------------------------> Event Hub Consumer -(MINUTES_FINALIZED) | - | 2. 메시지 Consume - | - v - Embedding 생성 - (OpenAI text-embedding-ada-002) - | - v - PostgreSQL + pgvector - (rag_minutes 테이블) - | - | 3. 연관 회의록 조회 - | - v - Vector Similarity Search - (Cosine Distance) -``` - -## 주요 기능 - -### 1. 회의록 RAG 저장 - -- **트리거**: Meeting 서비스에서 회의록 확정 시 Event Hub로 이벤트 발행 -- **처리 흐름**: - 1. Event Hub Consumer가 `MINUTES_FINALIZED` 이벤트 수신 - 2. 회의록 전체 내용을 텍스트로 생성 (제목 + 목적 + 섹션 내용) - 3. OpenAI Embedding API를 사용하여 1536차원 벡터 생성 - 4. `rag_minutes` 테이블에 회의록 정보와 embedding 벡터 저장 - -### 2. 연관 회의록 조회 - -- **API**: `POST /api/minutes/search` -- **검색 방식**: Vector Similarity Search (Cosine Distance) -- **입력**: 최종 회의록 내용 (full_content) -- **출력**: 유사도 높은 회의록 목록 (상위 K개, 기본값 5개) - -### 3. 회의록 상세 조회 - -- **API**: `GET /api/minutes/{minutes_id}` -- **출력**: 회의록 전체 정보 (Meeting 정보, Minutes 정보, Sections) - -## 데이터베이스 스키마 - -### rag_minutes 테이블 - -```sql -CREATE TABLE rag_minutes ( - -- Meeting 정보 - meeting_id VARCHAR(50) NOT NULL, - title VARCHAR(200) NOT NULL, - purpose VARCHAR(500), - description TEXT, - scheduled_at TIMESTAMP, - location VARCHAR(200), - organizer_id VARCHAR(50) NOT NULL, - - -- Minutes 정보 - minutes_id VARCHAR(50) PRIMARY KEY, - minutes_status VARCHAR(20) NOT NULL DEFAULT 'FINALIZED', - minutes_version INTEGER NOT NULL DEFAULT 1, - created_by VARCHAR(50) NOT NULL, - finalized_by VARCHAR(50), - finalized_at TIMESTAMP, - - -- 회의록 섹션 (JSON) - sections JSONB, - - -- 전체 회의록 내용 (검색용) - full_content TEXT NOT NULL, - - -- Embedding 벡터 - embedding vector(1536), - - -- 메타데이터 - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); -``` - -### 인덱스 - -- `idx_rag_minutes_meeting_id`: Meeting ID로 검색 -- `idx_rag_minutes_title`: 제목으로 검색 -- `idx_rag_minutes_finalized_at`: 확정 일시로 정렬 -- `idx_rag_minutes_created_by`: 작성자로 검색 -- `idx_rag_minutes_embedding`: 벡터 유사도 검색 (IVFFlat 인덱스) -- `idx_rag_minutes_full_content_gin`: Full-text 검색 (GIN 인덱스) - -## 설치 및 실행 - -### 1. 의존성 설치 - -```bash -cd rag -pip install -r requirements.txt -``` - -### 2. 환경 변수 설정 - -`.env` 파일에 다음 환경 변수 추가: - -```bash -# PostgreSQL -POSTGRES_HOST=4.217.133.186 -POSTGRES_PORT=5432 -POSTGRES_DATABASE=ragdb -POSTGRES_USER=hgzerouser -POSTGRES_PASSWORD=Hi5Jessica! - -# Azure OpenAI (Embedding) -AZURE_OPENAI_API_KEY=your-api-key -AZURE_OPENAI_ENDPOINT=https://api.openai.com/v1/embeddings - -# Azure Event Hub -EVENTHUB_CONNECTION_STRING=Endpoint=sb://hgzero-eventhub-ns.servicebus.windows.net/;... -EVENTHUB_NAME=hgzero-eventhub-name -AZURE_EVENTHUB_CONSUMER_GROUP=$Default -AZURE_STORAGE_CONNECTION_STRING=DefaultEndpointsProtocol=https;AccountName=hgzerostorage;... -AZURE_STORAGE_CONTAINER_NAME=hgzero-checkpoints -``` - -### 3. 데이터베이스 초기화 - -```bash -cd rag -python scripts/init_rag_minutes.py -``` - -이 스크립트는 다음 작업을 수행합니다: -- `rag_minutes` 테이블 생성 -- 필요한 인덱스 생성 -- pgvector 확장 설치 확인 - -### 4. Event Hub Consumer 시작 - -```bash -cd rag -python start_consumer.py -``` - -Consumer는 백그라운드에서 실행되며 Event Hub로부터 회의록 확정 이벤트를 수신합니다. - -### 5. API 서버 시작 - -```bash -cd rag/src -python -m api.main -``` - -또는: - -```bash -cd rag -uvicorn src.api.main:app --host 0.0.0.0 --port 8000 --reload -``` - -## API 사용 예시 - -### 1. 연관 회의록 검색 - -**요청**: - -```bash -curl -X POST "http://localhost:8000/api/minutes/search" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "2025년 1분기 마케팅 전략 수립 및 실행 계획", - "top_k": 5, - "similarity_threshold": 0.7 - }' -``` - -**응답**: - -```json -[ - { - "minutes": { - "meeting_id": "MTG-2025-001", - "title": "2025 Q1 마케팅 전략 회의", - "minutes_id": "MIN-2025-001", - "full_content": "...", - "sections": [...] - }, - "similarity_score": 0.92 - }, - { - "minutes": { - "meeting_id": "MTG-2024-098", - "title": "2024 Q4 마케팅 결산", - "minutes_id": "MIN-2024-098", - "full_content": "...", - "sections": [...] - }, - "similarity_score": 0.85 - } -] -``` - -### 2. 회의록 상세 조회 - -**요청**: - -```bash -curl "http://localhost:8000/api/minutes/MIN-2025-001" -``` - -**응답**: - -```json -{ - "meeting_id": "MTG-2025-001", - "title": "2025 Q1 마케팅 전략 회의", - "purpose": "2025년 1분기 마케팅 전략 수립", - "minutes_id": "MIN-2025-001", - "minutes_status": "FINALIZED", - "sections": [ - { - "section_id": "SEC-001", - "type": "DISCUSSION", - "title": "시장 분석", - "content": "2025년 시장 동향 분석...", - "order": 1, - "verified": true - } - ], - "full_content": "...", - "created_at": "2025-01-15T10:30:00", - "finalized_at": "2025-01-15T12:00:00" -} -``` - -### 3. 통계 조회 - -**요청**: - -```bash -curl "http://localhost:8000/api/minutes/stats" -``` - -**응답**: - -```json -{ - "total_minutes": 150, - "total_meetings": 145, - "total_authors": 25, - "latest_finalized_at": "2025-01-20T15:30:00" -} -``` - -## Event Hub 메시지 형식 - -Meeting 서비스에서 발행하는 회의록 확정 이벤트 형식: - -```json -{ - "event_type": "MINUTES_FINALIZED", - "timestamp": "2025-01-15T12:00:00Z", - "data": { - "meeting_id": "MTG-2025-001", - "title": "2025 Q1 마케팅 전략 회의", - "purpose": "2025년 1분기 마케팅 전략 수립", - "description": "...", - "scheduled_at": "2025-01-15T10:00:00", - "location": "본사 3층 회의실", - "organizer_id": "organizer@example.com", - "minutes_id": "MIN-2025-001", - "status": "FINALIZED", - "version": 1, - "created_by": "user@example.com", - "finalized_by": "user@example.com", - "finalized_at": "2025-01-15T12:00:00", - "sections": [ - { - "section_id": "SEC-001", - "type": "DISCUSSION", - "title": "시장 분석", - "content": "2025년 시장 동향 분석...", - "order": 1, - "verified": true - } - ] - } -} -``` - -## 성능 최적화 - -### 1. Vector Search 최적화 - -- **IVFFlat 인덱스**: 대량의 벡터 데이터에 대한 근사 검색 -- **lists 파라미터**: 데이터 크기에 따라 조정 (기본값: 100) -- **Cosine Distance**: 유사도 측정에 최적화된 거리 메트릭 - -### 2. Full-text Search - -- **GIN 인덱스**: 텍스트 검색 성능 향상 -- **to_tsvector**: PostgreSQL의 Full-text Search 기능 활용 - -### 3. Embedding 생성 - -- **배치 처리**: 여러 회의록을 동시에 처리할 때 배치 API 활용 -- **캐싱**: 동일한 내용에 대한 중복 embedding 생성 방지 - -## 모니터링 - -### 1. 로그 - -- **Consumer 로그**: `logs/rag-consumer.log` -- **API 로그**: `logs/rag-api.log` - -### 2. 메트릭 - -- 초당 처리 이벤트 수 -- 평균 embedding 생성 시간 -- 평균 검색 응답 시간 -- 데이터베이스 연결 상태 - -## 문제 해결 - -### 1. Event Hub 연결 실패 - -```bash -# 연결 문자열 확인 -echo $EVENTHUB_CONNECTION_STRING - -# Event Hub 상태 확인 (Azure Portal) -``` - -### 2. Embedding 생성 실패 - -```bash -# OpenAI API 키 확인 -echo $AZURE_OPENAI_API_KEY - -# API 할당량 확인 (OpenAI Dashboard) -``` - -### 3. 데이터베이스 연결 실패 - -```bash -# PostgreSQL 연결 확인 -psql -h $POSTGRES_HOST -U $POSTGRES_USER -d $POSTGRES_DATABASE - -# pgvector 확장 확인 -SELECT * FROM pg_extension WHERE extname = 'vector'; -``` - -## 향후 개선 사항 - -1. **하이브리드 검색**: Keyword + Vector 검색 결합 -2. **재랭킹**: 검색 결과 재정렬 알고리즘 추가 -3. **메타데이터 필터링**: 날짜, 작성자, 카테고리 등으로 필터링 -4. **설명 생성**: Claude AI를 활용한 유사 회의록 관계 설명 -5. **배치 처리**: 대량의 과거 회의록 일괄 처리 - -## 참고 자료 - -- [pgvector](https://github.com/pgvector/pgvector): PostgreSQL의 Vector 확장 -- [Azure Event Hubs](https://docs.microsoft.com/azure/event-hubs/): Azure Event Hubs 문서 -- [OpenAI Embeddings](https://platform.openai.com/docs/guides/embeddings): OpenAI Embedding API 가이드 diff --git a/rag/TESTING.md b/rag/TESTING.md deleted file mode 100644 index 0362079..0000000 --- a/rag/TESTING.md +++ /dev/null @@ -1,508 +0,0 @@ -# Vector DB 통합 시스템 테스트 가이드 - -## 목차 -1. [사전 준비](#사전-준비) -2. [환경 설정](#환경-설정) -3. [데이터베이스 설정](#데이터베이스-설정) -4. [데이터 로딩 테스트](#데이터-로딩-테스트) -5. [API 서버 실행](#api-서버-실행) -6. [API 엔드포인트 테스트](#api-엔드포인트-테스트) -7. [자동화 테스트](#자동화-테스트) -8. [문제 해결](#문제-해결) - ---- - -## 사전 준비 - -### 필수 소프트웨어 -- Python 3.9 이상 -- PostgreSQL 14 이상 (pgvector 확장 지원) -- Redis (선택사항, 캐싱용) - -### Azure 서비스 -- Azure OpenAI Service (임베딩 생성용) -- Azure AI Search (관련 문서 검색용) - ---- - -## 환경 설정 - -### 1. 가상환경 생성 및 활성화 - -```bash -cd vector -python -m venv venv - -# Linux/Mac -source venv/bin/activate - -# Windows -venv\Scripts\activate -``` - -### 2. 의존성 설치 - -```bash -pip install -r requirements.txt -``` - -### 3. 환경 변수 설정 - -`.env.example` 파일을 `.env`로 복사하고 실제 값으로 수정: - -```bash -cp .env.example .env -``` - -`.env` 파일 수정 예시: - -```bash -# PostgreSQL -POSTGRES_HOST=localhost -POSTGRES_PORT=5432 -POSTGRES_DATABASE=meeting_db -POSTGRES_USER=postgres -POSTGRES_PASSWORD=your_actual_password - -# Azure OpenAI -AZURE_OPENAI_API_KEY=your_actual_api_key -AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com - -# Azure AI Search -AZURE_SEARCH_ENDPOINT=https://your-search-service.search.windows.net -AZURE_SEARCH_API_KEY=your_actual_api_key - -# Claude AI -CLAUDE_API_KEY=your_actual_claude_api_key - -# Redis -REDIS_PASSWORD=your_redis_password -``` - ---- - -## 데이터베이스 설정 - -### 1. PostgreSQL 데이터베이스 생성 - -```sql -CREATE DATABASE meeting_db; -``` - -### 2. pgvector 확장 설치 - -PostgreSQL에 연결 후: - -```sql -CREATE EXTENSION IF NOT EXISTS vector; -``` - -### 3. 데이터베이스 초기화 - -용어 데이터 로딩 스크립트를 실행하면 자동으로 테이블이 생성됩니다: - -```bash -python scripts/load_terms.py -``` - ---- - -## 데이터 로딩 테스트 - -### 1. 데이터 로딩 검증 테스트 - -환경 설정 없이도 데이터 파일 로드를 검증할 수 있습니다: - -```bash -python tests/test_data_loading.py -``` - -**예상 출력:** -``` -============================================================ -Vector DB 데이터 로딩 테스트 -============================================================ - -============================================================ -설정 로드 테스트 -============================================================ -✓ 설정 로드 성공 - - PostgreSQL 호스트: localhost - - Azure OpenAI 모델: text-embedding-ada-002 - ... - -============================================================ -용어 데이터 로드 테스트 -============================================================ -✓ terms-01.json 로드 완료: XX개 용어 -✓ terms-02.json 로드 완료: XX개 용어 -... - -총 XXX개 용어 로드 완료 -``` - -### 2. 용어집 데이터 로딩 - -```bash -python scripts/load_terms.py -``` - -**예상 출력:** -``` -============================================================ -용어집 데이터 로딩 시작 -============================================================ -✓ 설정 로드 완료 -✓ PostgreSQL 연결 완료 -✓ 데이터베이스 초기화 완료 -✓ 임베딩 생성기 초기화 완료 -✓ 총 XXX개 용어 로드 완료 -✓ 임베딩 생성 완료 -✓ 삽입 완료: 성공 XXX, 실패 0 - -============================================================ -용어집 통계 -============================================================ -전체 용어: XXX개 -평균 신뢰도: X.XX - -카테고리별 통계: - - 기술용어: XX개 - - 비즈니스용어: XX개 - ... -``` - -### 3. 관련자료 데이터 로딩 - -```bash -python scripts/load_documents.py -``` - -**예상 출력:** -``` -============================================================ -관련자료 데이터 로딩 시작 -============================================================ -✓ 설정 로드 완료 -✓ Azure AI Search 연결 완료 -✓ 인덱스 생성 완료 -✓ 임베딩 생성기 초기화 완료 -✓ 총 XX개 문서 로드 완료 -✓ 총 XXX개 청크 생성 완료 -✓ XXX개 청크 업로드 완료 - -============================================================ -관련자료 통계 -============================================================ -전체 문서: XX개 -전체 청크: XXX개 - -문서 타입별 통계: - - 회의록: XX개 - - 참고자료: XX개 - ... -``` - ---- - -## API 서버 실행 - -### 1. 개발 모드로 실행 - -```bash -python -m src.api.main -``` - -또는: - -```bash -uvicorn src.api.main:app --reload --host 0.0.0.0 --port 8000 -``` - -### 2. 서버 확인 - -브라우저에서 접속: -- API 문서: http://localhost:8000/docs -- 대체 API 문서: http://localhost:8000/redoc -- 루트 엔드포인트: http://localhost:8000/ - ---- - -## API 엔드포인트 테스트 - -### 1. 루트 엔드포인트 테스트 - -```bash -curl http://localhost:8000/ -``` - -**예상 응답:** -```json -{ - "service": "Vector DB 통합 시스템", - "version": "1.0.0", - "endpoints": { - "용어집": "/api/terms/*", - "관련자료": "/api/documents/*" - } -} -``` - -### 2. 용어 검색 테스트 - -#### 키워드 검색 -```bash -curl -X POST http://localhost:8000/api/terms/search \ - -H "Content-Type: application/json" \ - -d '{ - "query": "API", - "search_type": "keyword", - "top_k": 5, - "confidence_threshold": 0.7 - }' -``` - -#### 벡터 검색 -```bash -curl -X POST http://localhost:8000/api/terms/search \ - -H "Content-Type: application/json" \ - -d '{ - "query": "회의 일정 관리", - "search_type": "vector", - "top_k": 3, - "confidence_threshold": 0.6 - }' -``` - -#### 하이브리드 검색 -```bash -curl -X POST http://localhost:8000/api/terms/search \ - -H "Content-Type: application/json" \ - -d '{ - "query": "마이크로서비스", - "search_type": "hybrid", - "top_k": 5, - "confidence_threshold": 0.5 - }' -``` - -### 3. 용어 상세 조회 - -먼저 검색으로 용어 ID를 찾은 후: - -```bash -curl http://localhost:8000/api/terms/{term_id} -``` - -### 4. 용어 설명 생성 (Claude AI) - -```bash -curl -X POST http://localhost:8000/api/terms/{term_id}/explain \ - -H "Content-Type: application/json" \ - -d '{ - "meeting_context": "백엔드 개발 회의에서 REST API 설계 논의" - }' -``` - -### 5. 용어 통계 조회 - -```bash -curl http://localhost:8000/api/terms/stats -``` - -### 6. 관련 문서 검색 - -```bash -curl -X POST http://localhost:8000/api/documents/search \ - -H "Content-Type: application/json" \ - -d '{ - "query": "프로젝트 계획", - "top_k": 3, - "relevance_threshold": 0.3, - "semantic_ranking": true - }' -``` - -#### 필터링된 검색 -```bash -curl -X POST http://localhost:8000/api/documents/search \ - -H "Content-Type: application/json" \ - -d '{ - "query": "회의록", - "top_k": 5, - "relevance_threshold": 0.3, - "document_type": "회의록", - "folder": "프로젝트A", - "semantic_ranking": true - }' -``` - -### 7. 문서 통계 조회 - -```bash -curl http://localhost:8000/api/documents/stats -``` - ---- - -## 자동화 테스트 - -### 1. pytest 설치 확인 - -pytest가 requirements.txt에 포함되어 있어야 합니다. - -### 2. API 테스트 실행 - -서버가 실행 중인 상태에서: - -```bash -pytest tests/test_api.py -v -``` - -**예상 출력:** -``` -tests/test_api.py::test_root PASSED -tests/test_api.py::test_search_terms_keyword PASSED -tests/test_api.py::test_search_terms_vector PASSED -tests/test_api.py::test_search_terms_hybrid PASSED -tests/test_api.py::test_get_term_stats PASSED -tests/test_api.py::test_search_documents PASSED -tests/test_api.py::test_search_documents_with_filters PASSED -tests/test_api.py::test_get_document_stats PASSED -tests/test_api.py::test_get_nonexistent_term PASSED -tests/test_api.py::test_explain_term PASSED -``` - -### 3. 개별 테스트 실행 - -```bash -# 특정 테스트만 실행 -pytest tests/test_api.py::test_search_terms_keyword -v - -# 테스트 상세 출력 -pytest tests/test_api.py -v -s -``` - ---- - -## 문제 해결 - -### 1. PostgreSQL 연결 실패 - -**증상:** -``` -psycopg2.OperationalError: could not connect to server -``` - -**해결:** -- PostgreSQL이 실행 중인지 확인 -- .env 파일의 데이터베이스 접속 정보 확인 -- 방화벽 설정 확인 - -### 2. pgvector 확장 오류 - -**증상:** -``` -psycopg2.errors.UndefinedObject: type "vector" does not exist -``` - -**해결:** -```sql -CREATE EXTENSION IF NOT EXISTS vector; -``` - -### 3. Azure OpenAI API 오류 - -**증상:** -``` -openai.error.AuthenticationError: Incorrect API key provided -``` - -**해결:** -- .env 파일의 AZURE_OPENAI_API_KEY 확인 -- Azure Portal에서 API 키 재확인 -- API 엔드포인트 URL 확인 - -### 4. Azure AI Search 인덱스 생성 실패 - -**증상:** -``` -azure.core.exceptions.HttpResponseError: (Unauthorized) Access denied -``` - -**해결:** -- .env 파일의 AZURE_SEARCH_API_KEY 확인 -- Azure Portal에서 API 키 및 권한 확인 -- 인덱스 이름 중복 여부 확인 - -### 5. 임베딩 생성 실패 - -**증상:** -``` -RateLimitError: Rate limit exceeded -``` - -**해결:** -- Azure OpenAI의 Rate Limit 확인 -- 배치 크기를 줄여서 재시도 -- 재시도 로직이 자동으로 작동하므로 대기 - -### 6. Claude API 오류 - -**증상:** -``` -anthropic.APIError: Invalid API Key -``` - -**해결:** -- .env 파일의 CLAUDE_API_KEY 확인 -- API 키 유효성 확인 -- 호출 빈도 제한 확인 - ---- - -## 성능 테스트 - -### 1. 검색 응답 시간 측정 - -```bash -time curl -X POST http://localhost:8000/api/terms/search \ - -H "Content-Type: application/json" \ - -d '{ - "query": "API", - "search_type": "hybrid", - "top_k": 10 - }' -``` - -### 2. 동시 요청 테스트 - -Apache Bench를 사용한 부하 테스트: - -```bash -ab -n 100 -c 10 http://localhost:8000/ -``` - ---- - -## 다음 단계 - -1. **프로덕션 배포 준비** - - 환경별 설정 분리 (dev/staging/prod) - - 로깅 및 모니터링 설정 - - 보안 강화 (API 키 관리, HTTPS) - -2. **성능 최적화** - - Redis 캐싱 활성화 - - 인덱스 튜닝 - - 쿼리 최적화 - -3. **기능 확장** - - 사용자 인증/인가 - - 용어 버전 관리 - - 문서 업데이트 자동화 - -4. **통합 테스트** - - E2E 테스트 작성 - - CI/CD 파이프라인 구축 - - 자동화된 성능 테스트 diff --git a/rag/eventhub_guide.md b/rag/eventhub_guide.md deleted file mode 100644 index 7e911e9..0000000 --- a/rag/eventhub_guide.md +++ /dev/null @@ -1,378 +0,0 @@ -# Event Hub Consumer Guide - -## 핵심 개념: Partition Ownership (파티션 소유권) - -Event Hub에서는 **같은 Consumer Group 내에서 하나의 파티션은 동시에 오직 하나의 Consumer만 읽을 수 있습니다**. 이를 "Exclusive Consumer" 패턴이라고 합니다. - -## 왜 이런 제약이 있나요? - -### 1. 순서 보장 (Ordering) - -``` -파티션 0: [이벤트1] → [이벤트2] → [이벤트3] - -❌ 잘못된 경우 (여러 Consumer가 동시 읽기): -Consumer A: 이벤트1 처리 중... (느림) -Consumer B: 이벤트2 처리 완료 ✓ -Consumer C: 이벤트3 처리 완료 ✓ -→ 처리 순서: 2 → 3 → 1 (순서 뒤바뀜!) - -✅ 올바른 경우 (하나의 Consumer만): -Consumer A: 이벤트1 ✓ → 이벤트2 ✓ → 이벤트3 ✓ -→ 처리 순서: 1 → 2 → 3 (순서 보장!) -``` - -### 2. Checkpoint 일관성 - -``` -❌ 여러 Consumer가 각자 checkpoint: -Consumer A: offset 100까지 읽음 → checkpoint 저장 -Consumer B: offset 150까지 읽음 → checkpoint 덮어씀 -Consumer A 재시작 → offset 150부터 읽음 → offset 100~149 누락! - -✅ 하나의 Consumer만: -Consumer A: offset 100 → 110 → 120 → ... (순차적) -재시작 시 → 마지막 checkpoint부터 정확히 재개 -``` - -### 3. 중복 처리 방지 - -``` -❌ 여러 Consumer가 동일 이벤트 읽기: -Consumer A: 주문 이벤트 처리 → 결제 완료 -Consumer B: 동일 주문 이벤트 처리 → 중복 결제! - -✅ 하나의 Consumer만: -Consumer A: 주문 이벤트 처리 → 1번만 결제 ✓ -``` - -## Ownership Claim 메커니즘 - -### 동작 과정 - -``` -Consumer A (PID 51257) - 먼저 시작 - ↓ -Blob Storage에 파티션 0 소유권 요청 - ↓ -✅ 승인 (Owner: A, Lease: 30초) - ↓ -30초마다 Lease 갱신 - ↓ -계속 소유권 유지 - - -Consumer B (테스트) - 나중에 시작 - ↓ -Blob Storage에 파티션 0 소유권 요청 - ↓ -❌ 거부 (이미 A가 소유 중) - ↓ -"hasn't claimed an ownership" 로그 - ↓ -계속 재시도 (대기 상태) -``` - -### Blob Storage에 저장되는 정보 - -```json -{ - "partitionId": "0", - "ownerIdentifier": "73fda457-b555-4af5-873a-54a2baa5fd95", - "lastModifiedTime": "2025-10-29T02:17:49Z", - "eTag": "\"0x8DCF7E8F9B3C1A0\"", - "offset": "120259090624", - "sequenceNumber": 232 -} -``` - -## 현재 상황 분석 - -``` -Event Hub: hgzero-eventhub-name -├─ 파티션 수: 1개 (파티션 0) -└─ Consumer Group: $Default - -실행 중: -├─ Consumer A (PID 51257): 파티션 0 소유 ✅ -│ ├─ 이벤트 정상 수신 중 -│ ├─ Lease 주기적 갱신 중 -│ └─ Checkpoint 저장 중 -│ -└─ 테스트 Consumer들: 소유권 없음 ❌ - ├─ 파티션 0 claim 시도 - ├─ 계속 거부당함 - ├─ "hasn't claimed an ownership" 로그 - └─ 이벤트 수신 불가 -``` - -## 해결 방법 - -### Option 1: 기존 Consumer 종료 후 재시작 - -```bash -# 기존 Consumer 종료 -kill 51257 - -# 새로 시작 -cd /Users/daewoong/home/workspace/HGZero/rag -python start_consumer.py -``` - -**장점**: 간단 -**단점**: 다운타임 발생 (Lease 만료까지 최대 30초) - -### Option 2: 다른 Consumer Group 사용 (권장) - -```yaml -# config.yaml -eventhub: - consumer_group: "test-group" # $Default 대신 사용 -``` - -**장점**: -- 기존 Consumer에 영향 없음 -- 독립적으로 모든 이벤트 읽기 가능 -- 개발/테스트에 이상적 - -**단점**: 리소스 추가 사용 - -### Option 3: 파티션 수평 확장 - -``` -Event Hub 파티션 증가: 1개 → 3개 -Consumer 실행: 3개 - -분산: -├─ Consumer A: 파티션 0 -├─ Consumer B: 파티션 1 -└─ Consumer C: 파티션 2 - -→ 병렬 처리로 3배 성능 향상! -``` - -**장점**: 높은 처리량 -**단점**: 비용 증가, 전체 순서는 보장 안 됨 (파티션 내 순서만 보장) - -## Consumer Group 비교 - -``` -┌─────────────────────────────────────────┐ -│ Event Hub: hgzero-eventhub │ -│ 파티션 0: [이벤트들...] │ -└─────────────────────────────────────────┘ - │ - ├─────────────────┬─────────────────┐ - │ │ │ - Consumer Group Consumer Group Consumer Group - "$Default" "analytics" "backup" - │ │ │ - Consumer A Consumer B Consumer C - (RAG 처리) (분석 처리) (백업 처리) - │ │ │ - 각자 독립적으로 동일한 파티션 0의 모든 이벤트 읽음 - 각자 독립적인 Checkpoint 유지 -``` - -## 파티션과 Consumer 수 관계 - -### Case 1: Consumer 1개 -``` -Event Hub: 파티션 3개 (P0, P1, P2) -Consumer Group: $Default - -├─ Consumer A: P0, P1, P2 모두 소유 -└─ 모든 파티션 처리 (순차적) -``` - -### Case 2: Consumer 3개 (이상적) -``` -Event Hub: 파티션 3개 (P0, P1, P2) -Consumer Group: $Default - -├─ Consumer A: P0 소유 -├─ Consumer B: P1 소유 -└─ Consumer C: P2 소유 - -→ 병렬 처리로 최대 성능! -``` - -### Case 3: Consumer 5개 (과잉) -``` -Event Hub: 파티션 3개 (P0, P1, P2) -Consumer Group: $Default - -├─ Consumer A: P0 소유 -├─ Consumer B: P1 소유 -├─ Consumer C: P2 소유 -├─ Consumer D: 소유한 파티션 없음 (대기) -└─ Consumer E: 소유한 파티션 없음 (대기) - -→ D, E는 이벤트를 읽지 못하고 대기만 함 -``` - -## 베스트 프랙티스 - -| 환경 | Consumer 수 | Consumer Group | 파티션 수 | -|------|-------------|----------------|-----------| -| **프로덕션** | = 파티션 수 | production | 처리량에 맞게 | -| **개발** | 1개 | development | 1~2개 | -| **테스트** | 1개 | test | 1개 | -| **분석** | 1개 | analytics | (공유) | - -### 권장 사항 - -1. **프로덕션 환경** - - Consumer 수 = 파티션 수 (1:1 매핑) - - 고가용성을 위해 각 Consumer를 다른 서버에 배치 - - Consumer 수 > 파티션 수로 설정하면 일부는 대기 상태 (Standby) - -2. **개발/테스트 환경** - - 별도 Consumer Group 사용 - - 파티션 1개로 충분 - - 필요시 checkpoint를 초기화하여 처음부터 재처리 - -3. **모니터링** - - Ownership claim 실패 로그 모니터링 - - Lease 갱신 실패 알림 설정 - - Checkpoint lag 모니터링 - -4. **장애 복구** - - Lease timeout 고려 (기본 30초) - - Consumer 장애 시 자동 재분배 (30초 이내) - - Checkpoint로부터 정확한 위치에서 재개 - -## Consumer 프로세스 관리 명령어 - -### 프로세스 확인 - -```bash -# Consumer 프로세스 확인 -ps aux | grep "start_consumer.py" | grep -v grep - -# 상세 정보 (실행시간, CPU, 메모리) -ps -p -o pid,etime,%cpu,%mem,cmd - -# 네트워크 연결 확인 -lsof -i -n | grep - -# 모든 Python 프로세스 확인 -ps aux | grep python | grep -v grep -``` - -### 프로세스 종료 - -```bash -# 정상 종료 (SIGTERM) -kill - -# 강제 종료 (SIGKILL) -kill -9 - -# 이름으로 종료 -pkill -f start_consumer.py -``` - -## 테스트 이벤트 전송 - -```python -from azure.eventhub import EventHubProducerClient, EventData -import json -import os -from dotenv import load_dotenv - -load_dotenv('rag/.env') - -conn_str = os.getenv('EVENTHUB_CONNECTION_STRING') -eventhub_name = os.getenv('EVENTHUB_NAME') - -test_event = { - 'eventType': 'MINUTES_FINALIZED', - 'data': { - 'meetingId': 'test-meeting-001', - 'title': '테스트 회의', - 'minutesId': 'test-minutes-001', - 'sections': [ - { - 'sectionId': 'section-001', - 'type': 'DISCUSSION', - 'title': '논의 사항', - 'content': '테스트 논의 내용입니다.', - 'order': 1, - 'verified': True - } - ] - } -} - -producer = EventHubProducerClient.from_connection_string( - conn_str=conn_str, - eventhub_name=eventhub_name -) - -event_data_batch = producer.create_batch() -event_data_batch.add(EventData(json.dumps(test_event))) - -producer.send_batch(event_data_batch) -print('✅ 테스트 이벤트 전송 완료') - -producer.close() -``` - -## Event Hub 파티션 정보 조회 - -```python -import asyncio -from azure.eventhub.aio import EventHubConsumerClient - -async def check_partitions(): - client = EventHubConsumerClient.from_connection_string( - conn_str=EVENTHUB_CONNECTION_STRING, - consumer_group="$Default", - eventhub_name=EVENTHUB_NAME - ) - - async with client: - partition_ids = await client.get_partition_ids() - print(f"파티션 개수: {len(partition_ids)}") - print(f"파티션 IDs: {partition_ids}") - - for partition_id in partition_ids: - props = await client.get_partition_properties(partition_id) - print(f"\n파티션 {partition_id}:") - print(f" 시퀀스 번호: {props['last_enqueued_sequence_number']}") - print(f" 오프셋: {props['last_enqueued_offset']}") - print(f" 마지막 이벤트 시간: {props['last_enqueued_time_utc']}") - -asyncio.run(check_partitions()) -``` - -## 정리 - -### "에러"가 아니라 "설계된 동작"입니다 - -1. ✅ **정상**: Consumer A가 파티션 소유 → 이벤트 처리 -2. ✅ **정상**: Consumer B가 claim 실패 → 대기 -3. ✅ **정상**: Consumer A 종료 시 → Consumer B가 자동 인수 - -### 이 메커니즘의 장점 - -- 📌 **순서 보장**: 파티션 내 이벤트 순서 유지 -- 📌 **정확히 한 번 처리**: 중복 처리 방지 -- 📌 **자동 장애 복구**: Consumer 장애 시 자동 재분배 -- 📌 **수평 확장**: 파티션 추가로 처리량 증가 - -### 현재 상황 해결 - -**권장**: 다른 Consumer Group을 사용하여 테스트하시는 것이 가장 안전하고 효율적입니다! - -```yaml -# 개발/테스트용 Consumer Group 설정 -eventhub: - consumer_group: "development" # 또는 "test" -``` - -이렇게 하면: -- 기존 프로덕션 Consumer에 영향 없음 -- 독립적으로 모든 이벤트를 처음부터 읽을 수 있음 -- 여러 번 테스트 가능 diff --git a/rag/install-pgvector.md b/rag/install-pgvector.md deleted file mode 100644 index 941fab5..0000000 --- a/rag/install-pgvector.md +++ /dev/null @@ -1,595 +0,0 @@ -# pgvector Extension PostgreSQL 설치 가이드 - -## 개요 -벡터 유사도 검색을 위한 pgvector extension이 포함된 PostgreSQL 데이터베이스 설치 가이드입니다. - ---- - -## 1. 사전 요구사항 - -### 1.1 필수 확인 사항 -- [ ] Kubernetes 클러스터 접속 가능 여부 확인 -- [ ] Helm 3.x 이상 설치 확인 -- [ ] kubectl 명령어 사용 가능 여부 확인 -- [ ] 기본 StorageClass 존재 여부 확인 - -### 1.2 버전 정보 -| 구성요소 | 버전 | 비고 | -|---------|------|------| -| PostgreSQL | 16.x | pgvector 0.5.0 이상 지원 | -| pgvector Extension | 0.5.1+ | 최신 안정 버전 권장 | -| Helm Chart | bitnami/postgresql | pgvector 포함 커스텀 이미지 | - ---- - -## 2. 설치 방법 - -### 2.1 Kubernetes 환경 (Helm Chart) - -#### 2.1.1 개발 환경 (dev) - -**Step 1: Namespace 생성** -```bash -kubectl create namespace vector-dev -``` - -**Step 2: Helm Repository 추가** -```bash -helm repo add bitnami https://charts.bitnami.com/bitnami -helm repo update -``` - -**Step 3: values.yaml 작성** -```yaml -# values-pgvector-dev.yaml -global: - postgresql: - auth: - postgresPassword: "dev_password" - username: "vector_user" - password: "dev_vector_password" - database: "vector_db" - -image: - registry: docker.io - repository: pgvector/pgvector - tag: "pg16" - pullPolicy: IfNotPresent - -primary: - initdb: - scripts: - init-pgvector.sql: | - -- pgvector extension 활성화 - CREATE EXTENSION IF NOT EXISTS vector; - - -- 설치 확인 - SELECT extname, extversion FROM pg_extension WHERE extname = 'vector'; - - resources: - limits: - memory: 2Gi - cpu: 1000m - requests: - memory: 1Gi - cpu: 500m - - persistence: - enabled: true - size: 10Gi - storageClass: "" # 기본 StorageClass 사용 - - service: - type: ClusterIP - ports: - postgresql: 5432 - -metrics: - enabled: true - serviceMonitor: - enabled: false - -volumePermissions: - enabled: true -``` - -**Step 4: Helm 설치 실행** -```bash -helm install pgvector-dev bitnami/postgresql \ - --namespace vector-dev \ - --values values-pgvector-dev.yaml \ - --wait -``` - -**Step 5: 설치 확인** -```bash -# Pod 상태 확인 -kubectl get pods -n vector-dev - -# 서비스 확인 -kubectl get svc -n vector-dev - -# pgvector 설치 확인 -kubectl exec -it pgvector-dev-postgresql-0 -n vector-dev -- \ - psql -U vector_user -d vector_db -c "SELECT extname, extversion FROM pg_extension WHERE extname = 'vector';" -``` - -**예상 출력:** -``` - extname | extversion ----------+------------ - vector | 0.5.1 -(1 row) -``` - -#### 2.1.2 운영 환경 (prod) - -**Step 1: Namespace 생성** -```bash -kubectl create namespace vector-prod -``` - -**Step 2: values.yaml 작성 (고가용성 구성)** -```yaml -# values-pgvector-prod.yaml -global: - postgresql: - auth: - postgresPassword: "CHANGE_ME_PROD_PASSWORD" - username: "vector_user" - password: "CHANGE_ME_VECTOR_PASSWORD" - database: "vector_db" - -image: - registry: docker.io - repository: pgvector/pgvector - tag: "pg16" - pullPolicy: IfNotPresent - -architecture: replication # 고가용성 구성 - -primary: - initdb: - scripts: - init-pgvector.sql: | - -- pgvector extension 활성화 - CREATE EXTENSION IF NOT EXISTS vector; - - -- 성능 최적화 설정 - ALTER SYSTEM SET shared_buffers = '2GB'; - ALTER SYSTEM SET effective_cache_size = '6GB'; - ALTER SYSTEM SET maintenance_work_mem = '512MB'; - ALTER SYSTEM SET max_wal_size = '2GB'; - - -- pgvector 최적화 - ALTER SYSTEM SET max_parallel_workers_per_gather = 4; - - resources: - limits: - memory: 8Gi - cpu: 4000m - requests: - memory: 4Gi - cpu: 2000m - - persistence: - enabled: true - size: 100Gi - storageClass: "" # 기본 StorageClass 사용 - - podAntiAffinity: - preset: hard # Primary와 Replica 분리 배치 - -readReplicas: - replicaCount: 2 - - resources: - limits: - memory: 8Gi - cpu: 4000m - requests: - memory: 4Gi - cpu: 2000m - - persistence: - enabled: true - size: 100Gi - -backup: - enabled: true - cronjob: - schedule: "0 2 * * *" # 매일 새벽 2시 백업 - storage: - size: 50Gi - -metrics: - enabled: true - serviceMonitor: - enabled: true - -networkPolicy: - enabled: true - allowExternal: false -``` - -**Step 3: Helm 설치 실행** -```bash -helm install pgvector-prod bitnami/postgresql \ - --namespace vector-prod \ - --values values-pgvector-prod.yaml \ - --wait -``` - -**Step 4: 설치 확인** -```bash -# 모든 Pod 상태 확인 (Primary + Replicas) -kubectl get pods -n vector-prod - -# Replication 상태 확인 -kubectl exec -it pgvector-prod-postgresql-0 -n vector-prod -- \ - psql -U postgres -c "SELECT * FROM pg_stat_replication;" -``` - ---- - -### 2.2 Docker Compose 환경 (로컬 개발) - -**docker-compose.yml** -```yaml -version: '3.8' - -services: - pgvector: - image: pgvector/pgvector:pg16 - container_name: pgvector-local - environment: - POSTGRES_DB: vector_db - POSTGRES_USER: vector_user - POSTGRES_PASSWORD: local_password - POSTGRES_INITDB_ARGS: "-E UTF8 --locale=C" - ports: - - "5432:5432" - volumes: - - pgvector_data:/var/lib/postgresql/data - - ./init-scripts:/docker-entrypoint-initdb.d - command: - - "postgres" - - "-c" - - "shared_buffers=256MB" - - "-c" - - "max_connections=200" - healthcheck: - test: ["CMD-SHELL", "pg_isready -U vector_user -d vector_db"] - interval: 10s - timeout: 5s - retries: 5 - restart: unless-stopped - -volumes: - pgvector_data: - driver: local -``` - -**init-scripts/01-init-pgvector.sql** -```sql --- pgvector extension 활성화 -CREATE EXTENSION IF NOT EXISTS vector; - --- 테스트 테이블 생성 (선택사항) -CREATE TABLE IF NOT EXISTS vector_test ( - id SERIAL PRIMARY KEY, - content TEXT, - embedding vector(384) -- 384차원 벡터 (예시) -); - --- 인덱스 생성 (HNSW - 고성능) -CREATE INDEX ON vector_test -USING hnsw (embedding vector_cosine_ops); - --- 확인 쿼리 -SELECT extname, extversion FROM pg_extension WHERE extname = 'vector'; -``` - -**실행 명령** -```bash -# 시작 -docker-compose up -d - -# 로그 확인 -docker-compose logs -f pgvector - -# 접속 테스트 -docker exec -it pgvector-local psql -U vector_user -d vector_db - -# 종료 -docker-compose down -``` - ---- - -## 3. 설치 검증 - -### 3.1 Extension 설치 확인 -```sql --- Extension 버전 확인 -SELECT extname, extversion FROM pg_extension WHERE extname = 'vector'; - --- 지원 연산자 확인 -SELECT oprname, oprleft::regtype, oprright::regtype -FROM pg_operator -WHERE oprname IN ('<=>', '<->', '<#>'); -``` - -**예상 결과:** -``` - oprname | oprleft | oprright ----------+---------+---------- - <=> | vector | vector - <-> | vector | vector - <#> | vector | vector -``` - -### 3.2 벡터 연산 테스트 -```sql --- 테스트 데이터 삽입 -CREATE TABLE test_vectors ( - id SERIAL PRIMARY KEY, - embedding vector(3) -); - -INSERT INTO test_vectors (embedding) VALUES -('[1,2,3]'), -('[4,5,6]'), -('[1,1,1]'); - --- 코사인 거리 계산 테스트 -SELECT id, embedding, embedding <=> '[1,2,3]' AS cosine_distance -FROM test_vectors -ORDER BY cosine_distance -LIMIT 3; -``` - -### 3.3 인덱스 성능 테스트 -```sql --- HNSW 인덱스 생성 -CREATE INDEX ON test_vectors USING hnsw (embedding vector_cosine_ops); - --- 인덱스 사용 여부 확인 -EXPLAIN ANALYZE -SELECT id FROM test_vectors -ORDER BY embedding <=> '[1,2,3]' -LIMIT 10; -``` - ---- - -## 4. 연결 정보 - -### 4.1 Kubernetes 환경 - -**개발 환경 (cluster 내부)** -``` -Host: pgvector-dev-postgresql.vector-dev.svc.cluster.local -Port: 5432 -Database: vector_db -Username: vector_user -Password: dev_vector_password -``` - -**운영 환경 (cluster 내부)** -``` -Host: pgvector-prod-postgresql.vector-prod.svc.cluster.local -Port: 5432 -Database: vector_db -Username: vector_user -Password: CHANGE_ME_VECTOR_PASSWORD -``` - -**외부 접속 (Port-Forward)** -```bash -# 개발 환경 -kubectl port-forward -n vector-dev svc/pgvector-dev-postgresql 5432:5432 - -# 운영 환경 -kubectl port-forward -n vector-prod svc/pgvector-prod-postgresql 5433:5432 -``` - -### 4.2 Docker Compose 환경 -``` -Host: localhost -Port: 5432 -Database: vector_db -Username: vector_user -Password: local_password -``` - ---- - -## 5. Python 연결 예제 - -### 5.1 필수 라이브러리 -```bash -pip install psycopg2-binary pgvector -``` - -### 5.2 연결 코드 -```python -import psycopg2 -from pgvector.psycopg2 import register_vector - -# 연결 -conn = psycopg2.connect( - host="localhost", - port=5432, - database="vector_db", - user="vector_user", - password="local_password" -) - -# pgvector 타입 등록 -register_vector(conn) - -# 벡터 검색 예제 -cur = conn.cursor() -cur.execute(""" - SELECT id, embedding <=> %s::vector AS distance - FROM test_vectors - ORDER BY distance - LIMIT 5 -""", ([1, 2, 3],)) - -results = cur.fetchall() -for row in results: - print(f"ID: {row[0]}, Distance: {row[1]}") - -cur.close() -conn.close() -``` - ---- - -## 6. 트러블슈팅 - -### 6.1 Extension 설치 실패 -```sql --- 에러: extension "vector" is not available --- 해결: pgvector 이미지 사용 확인 -``` -**확인 명령:** -```bash -# Pod의 이미지 확인 -kubectl describe pod pgvector-dev-postgresql-0 -n vector-dev | grep Image -``` - -### 6.2 인덱스 생성 실패 -```sql --- 에러: operator class "vector_cosine_ops" does not exist --- 해결: Extension 재생성 -DROP EXTENSION vector CASCADE; -CREATE EXTENSION vector; -``` - -### 6.3 성능 이슈 -```sql --- 인덱스 통계 업데이트 -ANALYZE test_vectors; - --- HNSW 파라미터 조정 (m=16, ef_construction=64) -CREATE INDEX ON test_vectors -USING hnsw (embedding vector_cosine_ops) -WITH (m = 16, ef_construction = 64); -``` - ---- - -## 7. 보안 권장사항 - -### 7.1 비밀번호 관리 -```bash -# Kubernetes Secret 생성 -kubectl create secret generic pgvector-credentials \ - --from-literal=postgres-password='STRONG_PASSWORD' \ - --from-literal=password='STRONG_VECTOR_PASSWORD' \ - -n vector-prod - -# values.yaml에서 참조 -global: - postgresql: - auth: - existingSecret: "pgvector-credentials" -``` - -### 7.2 네트워크 정책 -```yaml -# network-policy.yaml -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: pgvector-policy - namespace: vector-prod -spec: - podSelector: - matchLabels: - app.kubernetes.io/name: postgresql - policyTypes: - - Ingress - ingress: - - from: - - namespaceSelector: - matchLabels: - name: vector-prod - - podSelector: - matchLabels: - app: vector-service - ports: - - protocol: TCP - port: 5432 -``` - ---- - -## 8. 모니터링 - -### 8.1 Prometheus Metrics (운영 환경) -```yaml -# ServiceMonitor가 활성화된 경우 자동 수집 -metrics: - enabled: true - serviceMonitor: - enabled: true - namespace: monitoring - interval: 30s -``` - -### 8.2 주요 메트릭 -- `pg_up`: PostgreSQL 가용성 -- `pg_database_size_bytes`: 데이터베이스 크기 -- `pg_stat_database_tup_fetched`: 조회된 행 수 -- `pg_stat_database_conflicts`: 복제 충돌 수 - ---- - -## 9. 백업 및 복구 - -### 9.1 수동 백업 -```bash -# Kubernetes 환경 -kubectl exec -n vector-prod pgvector-prod-postgresql-0 -- \ - pg_dump -U vector_user vector_db > backup_$(date +%Y%m%d).sql - -# Docker Compose 환경 -docker exec pgvector-local pg_dump -U vector_user vector_db > backup.sql -``` - -### 9.2 복구 -```bash -# Kubernetes 환경 -cat backup.sql | kubectl exec -i pgvector-prod-postgresql-0 -n vector-prod -- \ - psql -U vector_user -d vector_db - -# Docker Compose 환경 -cat backup.sql | docker exec -i pgvector-local psql -U vector_user -d vector_db -``` - ---- - -## 10. 참고 자료 - -- [pgvector GitHub](https://github.com/pgvector/pgvector) -- [PostgreSQL Documentation](https://www.postgresql.org/docs/16/) -- [Bitnami PostgreSQL Helm Chart](https://github.com/bitnami/charts/tree/main/bitnami/postgresql) -- [pgvector Performance Tips](https://github.com/pgvector/pgvector#performance) - ---- - -## 부록: 차원별 인덱스 권장사항 - -| 벡터 차원 | 인덱스 타입 | 파라미터 | 비고 | -|----------|-----------|---------|------| -| < 768 | HNSW | m=16, ef_construction=64 | 일반적인 임베딩 | -| 768-1536 | HNSW | m=24, ef_construction=100 | OpenAI ada-002 | -| > 1536 | IVFFlat | lists=100 | 매우 높은 차원 | - -**인덱스 선택 가이드:** -- **HNSW**: 검색 속도 우선 (메모리 사용량 높음) -- **IVFFlat**: 메모리 절약 우선 (검색 속도 느림) diff --git a/rag/requirements.txt b/rag/requirements.txt index 05041fd..b5ebe84 100644 --- a/rag/requirements.txt +++ b/rag/requirements.txt @@ -3,6 +3,7 @@ fastapi==0.104.1 uvicorn[standard]==0.24.0 pydantic==2.5.0 pydantic-settings==2.1.0 +sse-starlette==1.8.2 # Database psycopg2-binary==2.9.9 diff --git a/rag/check_active_consumers.py b/rag/scripts/check_active_consumers.py similarity index 100% rename from rag/check_active_consumers.py rename to rag/scripts/check_active_consumers.py diff --git a/rag/src/api/__pycache__/main.cpython-311.pyc b/rag/src/api/__pycache__/main.cpython-311.pyc index 4a523aceae7cc0be2d06683183acbd0a327c0ab3..1cc8c53882924a0d42710f9a81e68e16103437e3 100644 GIT binary patch delta 8988 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 diff --git a/rag/src/api/__pycache__/term_routes.cpython-311.pyc b/rag/src/api/__pycache__/term_routes.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..800a2de9a4e5eb6ed230a58f3d6450aed659e025 GIT binary patch literal 4392 zcmcIoYitzP6~1?7c6Vl9UgMYi2u$z-UL3rEfC9D&ICyy_aZ*f4*+1Lu&ahc`AMVU7 z24bgf%)_Qgd9*1sHLcYm>L{*cC{dk?s^w8B>aSU8exQ*oMXIP0yq312maIsXdhU$( zWt%9K+VR~p=iWK@oOAD-bG|$Nt;=Oca5?^lWB&l|@1&Ezm?h@19V2uZ2`GpJEKqTr zpn_C_4$_$P={S?H1T6;7#H~Rqj4^Rr&}Q7(AS+k|Yn)5igP?{&BHQQ48xWn>LG#D( zGq0cvo>PKNV54}7yH3F~_$@tii|&bve6RE_*vqvD4$*Uh5}aq5>mc@VK{QzTL=CQ0 zHMm!3SR*jK$XAhW2Br4whaY8sc8h;>Yj|#AlJ9!$5TCvL)489F&%ZO3y*e|0amM~7 zvF)RD4jvv!r4>=qT?daIeJyY*BK9k>RMLm_s=!Gxsr07OQbar=%KfRNEcSd!mJJUb zvMh!Y;bb@}N@v{uaDU7%g`<8@l#q3caRc?%en~tLJEb#9I4Ym1_RESSh7GonLQrK@WVuRlZ{2pI{(F^DT{Sw0Pt~sQXNc9!?F;u=&7ilFQC7@)22D0K3 z@EH09x=NML#AuWrMev@ev+NL7xZ*Sl4dU`QS3a{BH6z7{T62yUNzFr);wowoXr&}l z2A5f$V#*`yZ7f)ZsJEyAtayq#KZ}1VO0e| z{Fs4|_<+g>m4KfRRx~LrmiW|H@Ed|^9TZCY>iPwT zV2|M^>phN=OgcK;L#q&{BOR z$ybyUTuNQB2Pq6iqgdKkw^n)d;mU!79iX4F?m>@m7r=QmoVd-YWcHlvLE z-|ggA1fW57$4IKHx8`n=T;;PPSF>ktJerx-xkxIR6eEf#_$Z0sqi&Uzu#%Rcyjw(q z5IQ4-mGIH1xy3K+1$3|}v4jY)(j$?&B~_AX+#6z8QobjKVS;kHUraZ_=*pb^!({gS zrMY(}+xhfUL|~aqeD>OVfM)N`{b~v%$R@2+@#}MQzq!=T``kLyC#RCSMMx+5W!)B* zPbVX>l+F%>V@l{mO48}5sOZk4rn3P_f+6?5a55ssqg#Ar+g@bfXcEddw@=%`~+xBMV2rbT1-WRiV6KRo=^UjMgcQIh>a zSR6>Dl2QK~sf6esNJ-z9`@<2@fABz1lu`z&%2LEYSOTjdiNLko)_+>BFt$Gw6_cVA zR#MWL4JN*Z4ESy9jHe>uxcoxfGBF-Jkn5lXE`EW&K*K1<@UHzhXGLp$vrYW?;px85 zUQ#!G2U@1-xYl$$XQAuX07^I$XE6C!4 z9W^y;js6_6)PO{RfYz;s!fES_L-luOyaCM{_y!tHD;7|+xVV_BfmJOotVh+2T9q${ zuslPW7q4pL+jN`RTFS z$AE>F@$A<;`_&Z<99hiKL<=wrEUIkMJR2v@WIWq6&o8uXj9qQ&hL-UJG*3Xy zx5Wkeznlby}q{+6Mff90e^QRCj2Ib@U72u*V6au zsDKmQtLF&6esgyvecx$;k^7Yv7`b0d1OHd&bAdf{>~~Oop>{aTDiVsM1W~sNq7sh9 zd(4E+9~YJY9B$XDM(wRyV4-E$c8zUU*>*#vy8RW^-IH~AIx>If6$I*AzuI=ii?{vC=zsq&(W9WN~1^6?Kt9=K+7(tb?PXKa~ z8iC*Aoj}Z!+Un<%!DWa4Av_32bpel4@KHpi2VuH-L@kEa^- zVe~&bo@Pr`8)lb~Qz9o7;0?5J#$<^7a%h#dfPhTCuz21Kwn_k~&{Dve8zi;C<5!5B zABb7O7iv-UI<2a8@^Gf=d9CXC;g_Mf);;~8=4q&VPAgP4gIKJOVG8n9>9$ZvNJT;+ z-F6}jYWrgn*=i#t^h)w&2ZK^vR1~oddPeR@od4n!_7Hr7k(8_ xJeNb{UP}K-Bg$Kl$en_JJ1kB0W$0GXAd5A&a7wE{ckl* zAdfk_m#VM+d+-1M_ul{hb?g6|=ii_|{1t6{!(h-+P(IPgx?g?hgwa9go+vGf$^r_X zf~8q`Kp9l|R6(^*9n|CEF;+U71U;%vq=t zQhBvTk!5>o)T6qsLoKMvW5IKC8$`r)?56K$+SJwZou(^3=#nwH&3Q*p*ZO$&`c z%);v5GT`qqHs>wcpP&xNcTrE!UL*e8GTPHy34GRe^k)9;?Wacq*Aer_Bp zH~Pn!vgEtN$2ZP>vrs>)xkmy|uFgI{2LV#b~@d$8TlxAPH%%0aKFWVt=PU81|aBOKb#p-6j=f4{$-^+&i! zcaUrMBji8Gi;5mT91@j*u%G2cc^4PKtF7I%2ES>o)EQ?bITrtmwNOcRiNSxeRzURd0D=%dssnr|*(3*8E7UV@p)j5F3+?5mktb^rBG5Jg26}TMKg37xw}T z!6Du!V`cqRpFA>ms={kAM+-7RF3_)o#E*!SRN#D+_u4_Jj+rf+o3}PG({D|JyXlfc z7ntcMFC<5Y@wZjq-!eRX>_z7KyH6!Y`{&rV9gI8{4uQ8aH;x}qK6{?IK7Mri*lRal z8kto>blezzDf#SZa^(H#SD;yPTfDVCH!r=qdS><&m^V1c;h${CbKLauTRI% zv%ph`jEq&_i*;^;;x-YXE+wM8t>^FR@(ilmTK^>Ro0mb+jC1+F! zNN4tz~PfM~s@B0w}; z`y=#xO&53-qP#m45tUsi+`A8v14z_%^Wn}g3i=~nov4II$76V)#@nC^`9oozgCmO- zGlJb6C|!}U5uSuo>0M?Rlus08B5q&6-@(ltiHA0xtaaVd85gYxggg8Je$}E|W!?cz z{C@x(rLJaW5B3fnK6!Yo=ERW~kHoV|5?Lh!4Ogx9Vdcvir!&UZo-&RYpAnO-6Olx2Qr+rImPD`aaZL)<5w!m;uCsky)sdTI8n~`?(r%R11;zw08zcqABk`%gnFRH zlU*eimu!vJLhaC^yP5Yi5vT@$Pe}=twTm85&nS#q?+lfOuL`J4%klk#U9r5yq%9h(EAcErOh97Q`M>_@vedGUCeE#AM@doCe&r%w}YDQe%dofG$0*a^;ccO4cC7 zaz+x%r3)~aM&%3R44R6V@pCRO5vpS|B_Z=ILKZ;@qFJNLTSZ}2TKpZ1xH-pyt8-km z4L9ag$g^2HKA7W`J6I=v4W2GG2gl)=3xV=CId0sNYsW9=+Dv+op9{Dkyh?>{9@-#L zKgd(-`ixyvpDE(T(M;#AOhMm3wLRQt7R)SjbCEs7{!;0)2o@o8KaExcNB#lX zfqAA7bcZakZ}JY=3gBf6fTNI5f@v&M8a?T@(q>G1y!fbFSNNCpuOa&J+ithv?s}FO z7U;lhJ+`b{=R~x0drD~w2{hG3ki`_gRf6C4lq$cU4w-dq$_Gj#rI^lh7na?=DG_gK z`2+&0vH+~#?M_t~WBhVz_c&o{H>f)#eRLa_DN(t=R0gnIV8dXc1N7vapb}2~{`yfpO&u|JR@QBV^JpvZ&Ozmoi?WT9kUoX&e4m5w=K)cd50D3O) z2YN2RC&ru;rUWy5bnF#{>^%$fPj=bJ?RM)D9Aj?cZGJm^$`rT1u!1vEnjRlE)49-0 z?7bsO_P)337rrC&Q=C4R;6iz63u@Tq#Db$TY-X(F{Gd~(%8EO+(kp?>o=4t^*&^jJ zIYe2yjO7tWEnXEN2kdMSau!Zf%43S(S}o-jekN>=tHQLH5M?Fw?C2+^&ZoeR)tA9Rb#lwd|A z9PtMr$LfVRB}1A3;`rV`#4AU~C@M;c3P`#9s0n0x7Xc(sznT1-3lM-GO+NM4@EE^7 z@&5E{?=xTw5?80~ajJj%)#LR{ZwZJju+!Na$In4R**}?ldYGAhXEHf74oV=W?A=JT z-;#aSLi(8;8oX&&2<(~U&@*HTH`PKaz+69nVY+`{dgOaJQdColR)R8bJ|Zuilun{G zglYf~6@hL(A}aQB2YLLfqE;n34z0MVxG;*IBTO$4CM(aO{oNfLzi9rBJxltZCoo9h z1c8$PqKW51uovO>SqDIiNf(I;Pz``LHx+1*Pd&l}$R3~<0Yp8|`BBGX?T`23bH$F# zlxvdS!vq8zE4I-`@P8IBUQ6aF(gPprwtV{24+Yl({9fWr^iF|$&GyR3$+zb8Dufe3asB{pu6;g?DtT?}!S zAuE*zH~qL}viyU^KV2NJZ%ouT#+PnMEQRd239h()YeK&@rr$d8dC7G-{={9FI=w0LZn?mm=c&wuHVdrf>VyRZ+O- zE|Q`%gSshC@eHLm+a(y>FqM-(Rx+|`uwlyTxneE8Y%QKDEIqqos@QvW_f$#w+3-|R z+1UqE;H^q{m#4ZbYGSqbCn_4IN-E#z`C-p^?&QIE$)-ffrkN~VY4!{S@HwT6+U#Fa zIvb2N2QDhV@(Q#3GP8Ut&--af^%Pu8(OAn^$5_i*-BfnR9m-RL?jlYfk43B{%!%t5(-g7b3G1#{`uZ$0d&qdw7|ZL9XZ9pAdt&M9&!3qbSYNSo zc{TV>e}2nq>Wy`|fFCKht%OHH;U-wtiDU6LnQDC>CHg?uJ+jhk?~#9E*&Cv zprk{jsYFe$pFKcq^wSfctocfwL9CRT&!ve^8X9EV#z=!?9m#--o^W@_t4i(BJZ9h$ z4ble;zE%7DD8i!{wDBaLCOKF-3;Qh$SV{j5k38@Z?ZAJvFc{`J(XMg;$;WWUrVpdVO7h#Vq!WQLL&?)>OcR4T3Gy_3xsizbRUtCY$wU_ReXRBzo+7v!VsP!ZLB z__lX+htWEz76^BBLFjjY(s1)9@i!0EXtYPAzbRb(a0zaH`0W~2O_$HWKUD67pG)mu&hOn}!Zpop{|iua BD!>2$ delta 3109 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 str: + """ + JSON 문자열 내의 Java LocalDateTime 배열을 ISO 8601 문자열로 변환 + + Java의 Jackson이 LocalDateTime을 배열 형식으로 직렬화하는 것을 + Python이 파싱 가능한 문자열 형식으로 변환 + + Args: + json_str: 원본 JSON 문자열 + + Returns: + 변환된 JSON 문자열 + + Examples: + >>> _convert_java_datetime_arrays('{"timestamp":[2025,10,29,10,25,37,579030000]}') + '{"timestamp":"2025-10-29T10:25:37.579030"}' + """ + # Java LocalDateTime 배열 패턴: [년,월,일,시,분,초,나노초] + # 나노초는 항상 7개 요소로 전송됨 + pattern = r'\[(\d{4}),(\d{1,2}),(\d{1,2}),(\d{1,2}),(\d{1,2}),(\d{1,2}),(\d+)\]' + + def replace_datetime(match): + year = int(match.group(1)) + month = int(match.group(2)) + day = int(match.group(3)) + hour = int(match.group(4)) + minute = int(match.group(5)) + second = int(match.group(6)) + nanosecond = int(match.group(7)) + + # 나노초를 마이크로초로 변환 + microsecond = nanosecond // 1000 + + # ISO 8601 형식 문자열 생성 + dt = datetime(year, month, day, hour, minute, second, microsecond) + return f'"{dt.isoformat()}"' + + # 모든 datetime 배열을 문자열로 변환 + return re.sub(pattern, replace_datetime, json_str) + async def _process_segment_event(self, event_data: Dict[str, Any]): """ 세그먼트 생성 이벤트 처리 - 용어검색 실행 @@ -162,6 +209,9 @@ class EventHubConsumer: text = event_data.get("text", "") meeting_id = event_data.get("meetingId") + # 이벤트 데이터 구조 로깅 (디버깅용) + logger.debug(f"이벤트 데이터 키: {list(event_data.keys())}") + if not text: logger.warning(f"세그먼트 {segment_id}에 텍스트가 없습니다") return @@ -242,8 +292,50 @@ class EventHubConsumer: else: logger.info(f"세그먼트 {segment_id}에서 매칭되는 용어를 찾지 못했습니다") - # 7. 선택적: 검색 결과를 별도 테이블에 저장하거나 Event Hub로 발행 - # TODO: 필요시 검색 결과를 저장하거나 downstream 서비스로 전달 + # 7. SSE를 통해 결과 전송 + # Event Hub 메시지에서 sessionId 추출 (여러 필드 확인) + session_id = event_data.get("sessionId") or event_data.get("session_id") or event_data.get("meetingId") or meeting_id + + logger.info(f"SSE 전송 시도: sessionId={session_id}, meetingId={meeting_id}") + + if session_id: + from ..services.sse_manager import sse_manager + + # 용어 정보를 직렬화 가능한 형태로 변환 + terms_data = [] + for result in results: + term = result["term"] + terms_data.append({ + "term_id": term.term_id, + "term_name": term.term_name, + "definition": term.definition, + "category": term.category, + "synonyms": term.synonyms, + "related_terms": term.related_terms, + "context": term.context, + "relevance_score": result["relevance_score"], + "match_type": result.get("match_type", "unknown") + }) + + # SSE로 전송 + success = await sse_manager.send_to_session( + session_id=session_id, + data={ + "segment_id": segment_id, + "meeting_id": meeting_id, + "text": text[:100], # 텍스트 일부만 전송 + "terms": terms_data, + "total_count": len(terms_data) + }, + event_type="term_result" + ) + + if success: + logger.info(f"용어 검색 결과를 SSE로 전송 완료: {session_id}") + else: + logger.warning(f"SSE 전송 실패 (세션 미연결): {session_id}") + else: + logger.warning("이벤트 데이터에 sessionId가 없어 SSE 전송을 건너뜁니다") except Exception as e: logger.error(f"세그먼트 이벤트 처리 실패: {str(e)}", exc_info=True) diff --git a/rag/src/services/sse_manager.py b/rag/src/services/sse_manager.py new file mode 100644 index 0000000..fc70320 --- /dev/null +++ b/rag/src/services/sse_manager.py @@ -0,0 +1,183 @@ +""" +SSE(Server-Sent Events) 연결 관리자 +""" +import asyncio +import logging +from typing import Dict, Any, Optional +from datetime import datetime + +logger = logging.getLogger(__name__) + + +class SSEManager: + """SSE 연결 관리자""" + + def __init__(self, max_connections: int = 1000, heartbeat_interval: int = 30): + """ + 초기화 + + Args: + max_connections: 최대 동시 연결 수 + heartbeat_interval: Heartbeat 전송 간격 (초) + """ + self._connections: Dict[str, asyncio.Queue] = {} + self._last_activity: Dict[str, datetime] = {} + self.max_connections = max_connections + self.heartbeat_interval = heartbeat_interval + self._cleanup_task: Optional[asyncio.Task] = None + + async def start(self): + """SSE Manager 시작 - 정리 태스크 실행""" + self._cleanup_task = asyncio.create_task(self._cleanup_inactive_connections()) + logger.info("SSE Manager 시작됨") + + async def stop(self): + """SSE Manager 중지""" + if self._cleanup_task: + self._cleanup_task.cancel() + self._connections.clear() + self._last_activity.clear() + logger.info("SSE Manager 중지됨") + + def register(self, session_id: str) -> asyncio.Queue: + """ + 새 SSE 연결 등록 + + Args: + session_id: 세션 ID + + Returns: + 메시지 큐 + + Raises: + ValueError: 최대 연결 수 초과 시 + """ + if len(self._connections) >= self.max_connections: + raise ValueError(f"최대 연결 수({self.max_connections})를 초과했습니다") + + if session_id in self._connections: + logger.warning(f"세션 {session_id}가 이미 연결되어 있습니다") + return self._connections[session_id] + + queue = asyncio.Queue(maxsize=100) + self._connections[session_id] = queue + self._last_activity[session_id] = datetime.now() + + logger.info(f"SSE 연결 등록: {session_id} (전체 연결 수: {len(self._connections)})") + return queue + + def unregister(self, session_id: str): + """ + SSE 연결 제거 + + Args: + session_id: 세션 ID + """ + if session_id in self._connections: + del self._connections[session_id] + del self._last_activity[session_id] + logger.info(f"SSE 연결 제거: {session_id} (전체 연결 수: {len(self._connections)})") + + async def send_to_session(self, session_id: str, data: Dict[str, Any], event_type: str = "message") -> bool: + """ + 특정 세션에 데이터 전송 + + Args: + session_id: 세션 ID + data: 전송할 데이터 + event_type: 이벤트 타입 + + Returns: + 전송 성공 여부 + """ + if session_id not in self._connections: + logger.warning(f"세션 {session_id}가 연결되어 있지 않습니다") + return False + + try: + message = { + "event": event_type, + "data": data, + "timestamp": datetime.now().isoformat() + } + + queue = self._connections[session_id] + + # 큐가 가득 차면 오래된 메시지 제거 + if queue.full(): + try: + queue.get_nowait() + logger.warning(f"세션 {session_id} 큐가 가득 차서 오래된 메시지를 제거했습니다") + except asyncio.QueueEmpty: + pass + + await queue.put(message) + self._last_activity[session_id] = datetime.now() + + logger.debug(f"메시지 전송 성공: {session_id} (이벤트: {event_type})") + return True + + except Exception as e: + logger.error(f"메시지 전송 실패: {session_id}, 에러: {str(e)}") + return False + + async def send_heartbeat(self, session_id: str) -> bool: + """ + Heartbeat 전송 + + Args: + session_id: 세션 ID + + Returns: + 전송 성공 여부 + """ + return await self.send_to_session( + session_id, + {"type": "heartbeat"}, + event_type="heartbeat" + ) + + def is_connected(self, session_id: str) -> bool: + """ + 연결 상태 확인 + + Args: + session_id: 세션 ID + + Returns: + 연결 여부 + """ + return session_id in self._connections + + def get_active_sessions(self) -> list: + """활성 세션 목록 반환""" + return list(self._connections.keys()) + + async def _cleanup_inactive_connections(self): + """비활성 연결 정리 (백그라운드 태스크)""" + timeout_minutes = 30 + + while True: + try: + await asyncio.sleep(60) # 1분마다 확인 + + now = datetime.now() + inactive_sessions = [] + + for session_id, last_time in self._last_activity.items(): + elapsed = (now - last_time).total_seconds() / 60 + if elapsed > timeout_minutes: + inactive_sessions.append(session_id) + + for session_id in inactive_sessions: + logger.info(f"비활성 세션 제거: {session_id} ({timeout_minutes}분 초과)") + self.unregister(session_id) + + except asyncio.CancelledError: + break + except Exception as e: + logger.error(f"연결 정리 중 에러: {str(e)}") + + +# 전역 SSE Manager 인스턴스 +sse_manager = SSEManager() diff --git a/rag/start_all.sh b/rag/start_all.sh deleted file mode 100644 index d4c9d11..0000000 --- a/rag/start_all.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash -# RAG 서비스 - API 서버와 Event Hub Consumer 동시 실행 스크립트 - -set -e # 에러 발생 시 스크립트 종료 - -echo "==========================================" -echo "RAG 서비스 시작" -echo "==========================================" - -# 로그 디렉토리 생성 -mkdir -p logs - -# Event Hub Consumer를 백그라운드로 실행 -echo "[1/2] Event Hub Consumer 시작..." -python start_consumer.py > logs/consumer.log 2>&1 & -CONSUMER_PID=$! -echo "Consumer PID: $CONSUMER_PID" - -# API 서버 시작 (포그라운드) -echo "[2/2] REST API 서버 시작..." -python -m uvicorn src.api.main:app --host 0.0.0.0 --port 8000 & -API_PID=$! -echo "API Server PID: $API_PID" - -# PID 파일 저장 -echo $CONSUMER_PID > logs/consumer.pid -echo $API_PID > logs/api.pid - -echo "==========================================" -echo "RAG 서비스 시작 완료" -echo " - API Server: http://0.0.0.0:8000" -echo " - Consumer PID: $CONSUMER_PID" -echo " - API PID: $API_PID" -echo "==========================================" - -# 종료 시그널 처리 (graceful shutdown) -trap "echo 'Shutting down...'; kill $CONSUMER_PID $API_PID; exit 0" SIGTERM SIGINT - -# 두 프로세스 모두 실행 중인지 모니터링 -while kill -0 $CONSUMER_PID 2>/dev/null && kill -0 $API_PID 2>/dev/null; do - sleep 5 -done - -# 하나라도 종료되면 모두 종료 -echo "One of the processes stopped. Shutting down all..." -kill $CONSUMER_PID $API_PID 2>/dev/null || true -wait diff --git a/rag/start_all_services.py b/rag/start_all_services.py deleted file mode 100644 index 56e0887..0000000 --- a/rag/start_all_services.py +++ /dev/null @@ -1,180 +0,0 @@ -""" -RAG 서비스 통합 실행 스크립트 -API 서버와 Event Hub Consumer를 동시에 실행 -""" -import asyncio -import logging -import multiprocessing -import signal -import sys -import time -from pathlib import Path - -import uvicorn - -from src.utils.config import load_config, get_database_url -from src.db.rag_minutes_db import RagMinutesDB -from src.db.postgres_vector import PostgresVectorDB -from src.utils.embedding import EmbeddingGenerator -from src.services.eventhub_consumer import start_consumer - -# 로깅 설정 -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' -) -logger = logging.getLogger(__name__) - - -def run_api_server(): - """ - REST API 서버 실행 (별도 프로세스) - """ - try: - logger.info("=" * 50) - logger.info("REST API 서버 시작") - logger.info("=" * 50) - - uvicorn.run( - "src.api.main:app", - host="0.0.0.0", - port=8000, - log_level="info", - access_log=True - ) - except Exception as e: - logger.error(f"API 서버 실행 실패: {str(e)}") - sys.exit(1) - - -def run_event_consumer(): - """ - Event Hub Consumer 실행 (별도 프로세스) - """ - try: - logger.info("=" * 50) - logger.info("Event Hub Consumer 시작") - logger.info("=" * 50) - - # 설정 로드 - config_path = Path(__file__).parent / "config.yaml" - config = load_config(str(config_path)) - - # 데이터베이스 연결 - db_url = get_database_url(config) - rag_minutes_db = RagMinutesDB(db_url) - logger.info("RAG Minutes DB 연결 완료") - - # 용어집 데이터베이스 연결 - term_db = PostgresVectorDB(db_url) - logger.info("용어집 DB 연결 완료") - - # Embedding 생성기 초기화 - azure_openai = config["azure_openai"] - embedding_gen = EmbeddingGenerator( - api_key=azure_openai["api_key"], - endpoint=azure_openai["endpoint"], - model=azure_openai["embedding_model"], - dimension=azure_openai["embedding_dimension"], - api_version=azure_openai["api_version"] - ) - logger.info("Embedding 생성기 초기화 완료") - - # Event Hub Consumer 시작 - asyncio.run(start_consumer(config, rag_minutes_db, embedding_gen, term_db)) - - except KeyboardInterrupt: - logger.info("Consumer 종료 신호 수신") - except Exception as e: - logger.error(f"Consumer 실행 실패: {str(e)}") - sys.exit(1) - - -def main(): - """ - 메인 함수: 두 프로세스를 생성하고 관리 - """ - logger.info("=" * 60) - logger.info("RAG 서비스 통합 시작") - logger.info(" - REST API 서버: http://0.0.0.0:8000") - logger.info(" - Event Hub Consumer: Background") - logger.info("=" * 60) - - # 프로세스 생성 - api_process = multiprocessing.Process( - target=run_api_server, - name="API-Server" - ) - consumer_process = multiprocessing.Process( - target=run_event_consumer, - name="Event-Consumer" - ) - - # 종료 시그널 핸들러 - def signal_handler(signum, frame): - logger.info("\n종료 신호 수신. 프로세스 종료 중...") - - if api_process.is_alive(): - logger.info("API 서버 종료 중...") - api_process.terminate() - api_process.join(timeout=5) - if api_process.is_alive(): - api_process.kill() - - if consumer_process.is_alive(): - logger.info("Consumer 종료 중...") - consumer_process.terminate() - consumer_process.join(timeout=5) - if consumer_process.is_alive(): - consumer_process.kill() - - logger.info("모든 프로세스 종료 완료") - sys.exit(0) - - # 시그널 핸들러 등록 - signal.signal(signal.SIGTERM, signal_handler) - signal.signal(signal.SIGINT, signal_handler) - - try: - # 프로세스 시작 - api_process.start() - time.sleep(2) # API 서버 시작 대기 - - consumer_process.start() - time.sleep(2) # Consumer 시작 대기 - - logger.info("=" * 60) - logger.info("모든 서비스 시작 완료") - logger.info(f" - API Server PID: {api_process.pid}") - logger.info(f" - Consumer PID: {consumer_process.pid}") - logger.info("=" * 60) - - # 프로세스 모니터링 - while True: - if not api_process.is_alive(): - logger.error("API 서버 프로세스 종료됨") - consumer_process.terminate() - break - - if not consumer_process.is_alive(): - logger.error("Consumer 프로세스 종료됨") - api_process.terminate() - break - - time.sleep(5) - - # 대기 - api_process.join() - consumer_process.join() - - except Exception as e: - logger.error(f"서비스 실행 중 에러: {str(e)}") - api_process.terminate() - consumer_process.terminate() - sys.exit(1) - - -if __name__ == "__main__": - # multiprocessing을 위한 설정 - multiprocessing.set_start_method('spawn', force=True) - main() diff --git a/rag/start_consumer.py b/rag/start_consumer.py deleted file mode 100644 index 7d35fbd..0000000 --- a/rag/start_consumer.py +++ /dev/null @@ -1,62 +0,0 @@ -import asyncio -import logging -from pathlib import Path - -from src.utils.config import load_config, get_database_url -from src.db.rag_minutes_db import RagMinutesDB -from src.db.postgres_vector import PostgresVectorDB -from src.utils.embedding import EmbeddingGenerator -from src.services.eventhub_consumer import start_consumer - -# 로깅 설정 -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' -) -logger = logging.getLogger(__name__) - - -async def main(): - """메인 함수""" - try: - # 설정 로드 - config_path = Path(__file__).parent / "config.yaml" - config = load_config(str(config_path)) - logger.info(config) - - logger.info("설정 로드 완료") - - # 데이터베이스 연결 - db_url = get_database_url(config) - rag_minutes_db = RagMinutesDB(db_url) - logger.info("RAG Minutes DB 연결 완료") - - # 용어집 데이터베이스 연결 - term_db = PostgresVectorDB(db_url) - logger.info("용어집 DB 연결 완료") - - # Embedding 생성기 초기화 - azure_openai = config["azure_openai"] - embedding_gen = EmbeddingGenerator( - api_key=azure_openai["api_key"], - endpoint=azure_openai["endpoint"], - model=azure_openai["embedding_model"], - dimension=azure_openai["embedding_dimension"], - api_version=azure_openai["api_version"] - ) - - logger.info("Embedding 생성기 초기화 완료") - - # Event Hub Consumer 시작 - logger.info("Event Hub Consumer 시작...") - await start_consumer(config, rag_minutes_db, embedding_gen, term_db) - - except KeyboardInterrupt: - logger.info("프로그램 종료") - except Exception as e: - logger.error(f"에러 발생: {str(e)}") - raise - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/rag/test_noun_extraction.py b/rag/test_noun_extraction.py deleted file mode 100644 index 3330242..0000000 --- a/rag/test_noun_extraction.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -명사 추출 기능 테스트 -""" -import sys -from pathlib import Path - -# 프로젝트 루트 경로를 sys.path에 추가 -project_root = Path(__file__).parent -sys.path.insert(0, str(project_root)) - -from src.utils.text_processor import extract_nouns, extract_nouns_as_query - - -def test_extract_nouns(): - """명사 추출 테스트""" - test_cases = [ - "안녕하세요. 오늘은 OFDM 기술 관련하여 회의를 진행하겠습니다.", - "5G 네트워크와 AI 기술을 활용한 자율주행 자동차", - "데이터베이스 설계 및 API 개발", - "클라우드 컴퓨팅 환경에서 마이크로서비스 아키텍처 구현" - ] - - print("=" * 80) - print("명사 추출 테스트") - print("=" * 80) - - for text in test_cases: - print(f"\n원본: {text}") - nouns = extract_nouns(text) - print(f"명사: {nouns}") - query = extract_nouns_as_query(text) - print(f"쿼리: {query}") - print("-" * 80) - - -if __name__ == "__main__": - test_extract_nouns() diff --git a/rag/test_sse.html b/rag/test_sse.html new file mode 100644 index 0000000..8ad3beb --- /dev/null +++ b/rag/test_sse.html @@ -0,0 +1,573 @@ + + + + + + SSE 용어 검색 테스트 + + + +
+
+

🔍 SSE 용어 검색 테스트

+

실시간 용어 검색 결과를 확인하세요

+
+ +
+
+ + +
+
+ + +
+
+ + + + +
+
+ +
+
+ + 연결 안됨 +
+
+ 수신: 0 +
+
+ +
+

📊 용어 검색 결과

+
+
+ + + +

연결 후 용어 검색 결과가 여기에 표시됩니다

+
+
+ +

📝 로그

+
+
+

로그가 여기에 표시됩니다

+
+
+
+
+ + + + From 4e2182055b475159cfc89f7da9d80533ab521070 Mon Sep 17 00:00:00 2001 From: djeon Date: Wed, 29 Oct 2025 22:42:07 +0900 Subject: [PATCH 03/10] feat: add rag ci/cd --- .github/config/deploy_env_vars_rag_dev | 7 + .github/config/deploy_env_vars_rag_prod | 7 + .github/config/deploy_env_vars_rag_staging | 7 + .github/workflows/rag-cicd_ArgoCD.yaml | 221 +++++++++++++++++++++ deployment/container/Dockerfile-rag | 57 ++++++ 5 files changed, 299 insertions(+) create mode 100644 .github/config/deploy_env_vars_rag_dev create mode 100644 .github/config/deploy_env_vars_rag_prod create mode 100644 .github/config/deploy_env_vars_rag_staging create mode 100644 .github/workflows/rag-cicd_ArgoCD.yaml create mode 100644 deployment/container/Dockerfile-rag diff --git a/.github/config/deploy_env_vars_rag_dev b/.github/config/deploy_env_vars_rag_dev new file mode 100644 index 0000000..32a5d38 --- /dev/null +++ b/.github/config/deploy_env_vars_rag_dev @@ -0,0 +1,7 @@ +# Azure Resource Configuration +resource_group=rg-digitalgarage-02 +cluster_name=aks-digitalgarage-02 + +# RAG Service Configuration +python_version=3.11 +app_port=8088 diff --git a/.github/config/deploy_env_vars_rag_prod b/.github/config/deploy_env_vars_rag_prod new file mode 100644 index 0000000..d9d4519 --- /dev/null +++ b/.github/config/deploy_env_vars_rag_prod @@ -0,0 +1,7 @@ +# Azure Resource Configuration +resource_group=rg-digitalgarage-prod +cluster_name=aks-digitalgarage-prod + +# RAG Service Configuration +python_version=3.11 +app_port=8088 diff --git a/.github/config/deploy_env_vars_rag_staging b/.github/config/deploy_env_vars_rag_staging new file mode 100644 index 0000000..8f01907 --- /dev/null +++ b/.github/config/deploy_env_vars_rag_staging @@ -0,0 +1,7 @@ +# Azure Resource Configuration +resource_group=rg-digitalgarage-staging +cluster_name=aks-digitalgarage-staging + +# RAG Service Configuration +python_version=3.11 +app_port=8088 diff --git a/.github/workflows/rag-cicd_ArgoCD.yaml b/.github/workflows/rag-cicd_ArgoCD.yaml new file mode 100644 index 0000000..b5d7c5b --- /dev/null +++ b/.github/workflows/rag-cicd_ArgoCD.yaml @@ -0,0 +1,221 @@ +name: RAG Service CI/CD + +on: + push: + branches: [ main, develop ] + paths: + - 'rag/**' + - '.github/workflows/rag-cicd_ArgoCD.yaml' + pull_request: + branches: [ main ] + workflow_dispatch: + inputs: + ENVIRONMENT: + description: 'Target environment' + required: true + default: 'dev' + type: choice + options: + - dev + - staging + - prod + SKIP_TESTS: + description: 'Skip Tests' + required: false + default: 'false' + type: choice + options: + - 'true' + - 'false' + +env: + REGISTRY: acrdigitalgarage02.azurecr.io + IMAGE_ORG: hgzero + SERVICE_NAME: rag + RESOURCE_GROUP: rg-digitalgarage-02 + AKS_CLUSTER: aks-digitalgarage-02 + NAMESPACE: hgzero + +jobs: + build: + name: Build and Test + runs-on: ubuntu-latest + outputs: + image_tag: ${{ steps.set_outputs.outputs.image_tag }} + environment: ${{ steps.set_outputs.outputs.environment }} + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v4 + with: + python-version: '3.11' + cache: 'pip' + cache-dependency-path: 'rag/requirements.txt' + + - name: Determine environment + id: determine_env + run: | + ENVIRONMENT="${{ github.event.inputs.ENVIRONMENT || 'dev' }}" + echo "environment=$ENVIRONMENT" >> $GITHUB_OUTPUT + + - name: Load environment variables + id: env_vars + run: | + ENV=${{ steps.determine_env.outputs.environment }} + + REGISTRY="acrdigitalgarage02.azurecr.io" + IMAGE_ORG="hgzero" + RESOURCE_GROUP="rg-digitalgarage-02" + AKS_CLUSTER="aks-digitalgarage-02" + NAMESPACE="hgzero" + + if [[ -f ".github/config/deploy_env_vars_rag_${ENV}" ]]; then + while IFS= read -r line || [[ -n "$line" ]]; do + [[ "$line" =~ ^#.*$ ]] && continue + [[ -z "$line" ]] && continue + + key=$(echo "$line" | cut -d '=' -f1) + value=$(echo "$line" | cut -d '=' -f2-) + + case "$key" in + "resource_group") RESOURCE_GROUP="$value" ;; + "cluster_name") AKS_CLUSTER="$value" ;; + esac + done < ".github/config/deploy_env_vars_rag_${ENV}" + fi + + echo "REGISTRY=$REGISTRY" >> $GITHUB_ENV + echo "IMAGE_ORG=$IMAGE_ORG" >> $GITHUB_ENV + echo "RESOURCE_GROUP=$RESOURCE_GROUP" >> $GITHUB_ENV + echo "AKS_CLUSTER=$AKS_CLUSTER" >> $GITHUB_ENV + + - name: Install dependencies + run: | + cd rag + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run Tests + env: + SKIP_TESTS: ${{ github.event.inputs.SKIP_TESTS || 'false' }} + run: | + if [[ "$SKIP_TESTS" == "true" ]]; then + echo "⏭️ Skipping Tests (SKIP_TESTS=$SKIP_TESTS)" + exit 0 + fi + + cd rag + # Run pytest with coverage + pytest tests/ --cov=src --cov-report=xml --cov-report=html + + echo "✅ Tests completed successfully" + + - name: Code Quality Check + run: | + cd rag + # Run linters + flake8 src/ --max-line-length=120 --exclude=__pycache__ + black --check src/ + mypy src/ --ignore-missing-imports + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: | + rag/htmlcov/ + rag/coverage.xml + + - name: Set outputs + id: set_outputs + run: | + IMAGE_TAG=$(date +%Y%m%d%H%M%S) + echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT + echo "environment=${{ steps.determine_env.outputs.environment }}" >> $GITHUB_OUTPUT + + release: + name: Build and Push Docker Image + needs: build + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set environment variables from build job + run: | + echo "REGISTRY=${{ env.REGISTRY }}" >> $GITHUB_ENV + echo "IMAGE_ORG=${{ env.IMAGE_ORG }}" >> $GITHUB_ENV + echo "SERVICE_NAME=${{ env.SERVICE_NAME }}" >> $GITHUB_ENV + echo "ENVIRONMENT=${{ needs.build.outputs.environment }}" >> $GITHUB_ENV + echo "IMAGE_TAG=${{ needs.build.outputs.image_tag }}" >> $GITHUB_ENV + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub (prevent rate limit) + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + - name: Login to Azure Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.ACR_USERNAME }} + password: ${{ secrets.ACR_PASSWORD }} + + - name: Build and push Docker image + run: | + echo "Building and pushing RAG service..." + docker build \ + -f deployment/container/Dockerfile-rag \ + -t ${{ env.REGISTRY }}/${{ env.IMAGE_ORG }}/${{ env.SERVICE_NAME }}:${{ needs.build.outputs.environment }}-${{ needs.build.outputs.image_tag }} \ + rag/ + + docker push ${{ env.REGISTRY }}/${{ env.IMAGE_ORG }}/${{ env.SERVICE_NAME }}:${{ needs.build.outputs.environment }}-${{ needs.build.outputs.image_tag }} + + echo "✅ Docker image pushed successfully" + + update-manifest: + name: Update Manifest Repository + needs: [build, release] + runs-on: ubuntu-latest + + steps: + - name: Set image tag environment variable + run: | + echo "IMAGE_TAG=${{ needs.build.outputs.image_tag }}" >> $GITHUB_ENV + echo "ENVIRONMENT=${{ needs.build.outputs.environment }}" >> $GITHUB_ENV + + - name: Update Manifest Repository + run: | + # 매니페스트 레포지토리 클론 + REPO_URL=$(echo "https://github.com/hjmoons/hgzero-manifest.git" | sed 's|https://||') + git clone https://${{ secrets.GIT_USERNAME }}:${{ secrets.GIT_PASSWORD }}@${REPO_URL} manifest-repo + cd manifest-repo + + # Kustomize 설치 + curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash + sudo mv kustomize /usr/local/bin/ + + # 매니페스트 업데이트 + cd hgzero-rag/kustomize/overlays/${{ env.ENVIRONMENT }} + + # RAG 서비스 이미지 태그 업데이트 + kustomize edit set image acrdigitalgarage02.azurecr.io/hgzero/rag:${{ env.ENVIRONMENT }}-${{ env.IMAGE_TAG }} + + # Git 설정 및 푸시 + cd ../../../.. + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + git add . + git commit -m "🚀 Update RAG ${{ env.ENVIRONMENT }} image to ${{ env.ENVIRONMENT }}-${{ env.IMAGE_TAG }}" + git push origin main + + echo "✅ 매니페스트 업데이트 완료. ArgoCD가 자동으로 배포합니다." diff --git a/deployment/container/Dockerfile-rag b/deployment/container/Dockerfile-rag new file mode 100644 index 0000000..cdd7965 --- /dev/null +++ b/deployment/container/Dockerfile-rag @@ -0,0 +1,57 @@ +# Build stage +FROM python:3.11-slim AS builder + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + g++ \ + make \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements and install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir --user -r requirements.txt + +# Run stage +FROM python:3.11-slim + +ENV USERNAME=k8s +ENV ARTIFACTORY_HOME=/home/${USERNAME} +ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + libpq5 \ + && rm -rf /var/lib/apt/lists/* + +# Add a non-root user +RUN adduser --system --group ${USERNAME} && \ + mkdir -p ${ARTIFACTORY_HOME} && \ + chown ${USERNAME}:${USERNAME} ${ARTIFACTORY_HOME} + +WORKDIR ${ARTIFACTORY_HOME} + +# Copy Python dependencies from builder +COPY --from=builder /root/.local /home/${USERNAME}/.local + +# Copy application code +COPY --chown=${USERNAME}:${USERNAME} . . + +# Update PATH to include user's local bin +ENV PATH=/home/${USERNAME}/.local/bin:$PATH + +USER ${USERNAME} + +# Expose port +EXPOSE 8088 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD python -c "import requests; requests.get('http://localhost:8088/health')" || exit 1 + +# Run the application +CMD ["uvicorn", "src.api.main:app", "--host", "0.0.0.0", "--port", "8088"] \ No newline at end of file From e001312f07ae41de9c135d395d5b60f445bfa8ff Mon Sep 17 00:00:00 2001 From: djeon Date: Wed, 29 Oct 2025 22:50:14 +0900 Subject: [PATCH 04/10] feat: add rag ci/cd --- .github/workflows/rag-cicd_ArgoCD.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rag-cicd_ArgoCD.yaml b/.github/workflows/rag-cicd_ArgoCD.yaml index b5d7c5b..9a06c93 100644 --- a/.github/workflows/rag-cicd_ArgoCD.yaml +++ b/.github/workflows/rag-cicd_ArgoCD.yaml @@ -22,7 +22,7 @@ on: SKIP_TESTS: description: 'Skip Tests' required: false - default: 'false' + default: 'true' type: choice options: - 'true' From b49038e97e3708468c2884169c3be031b528c43a Mon Sep 17 00:00:00 2001 From: djeon Date: Wed, 29 Oct 2025 22:55:48 +0900 Subject: [PATCH 05/10] feat: add rag ci/cd --- .github/workflows/rag-cicd_ArgoCD.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rag-cicd_ArgoCD.yaml b/.github/workflows/rag-cicd_ArgoCD.yaml index 9a06c93..2b521e8 100644 --- a/.github/workflows/rag-cicd_ArgoCD.yaml +++ b/.github/workflows/rag-cicd_ArgoCD.yaml @@ -100,7 +100,7 @@ jobs: - name: Run Tests env: - SKIP_TESTS: ${{ github.event.inputs.SKIP_TESTS || 'false' }} + SKIP_TESTS: ${{ github.event.inputs.SKIP_TESTS || 'true' }} run: | if [[ "$SKIP_TESTS" == "true" ]]; then echo "⏭️ Skipping Tests (SKIP_TESTS=$SKIP_TESTS)" From ea00cf4b0364381b441f861838c5ec951d9404b4 Mon Sep 17 00:00:00 2001 From: djeon Date: Wed, 29 Oct 2025 22:58:22 +0900 Subject: [PATCH 06/10] feat: add rag ci/cd --- .github/workflows/rag-cicd_ArgoCD.yaml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/rag-cicd_ArgoCD.yaml b/.github/workflows/rag-cicd_ArgoCD.yaml index 2b521e8..aad3fed 100644 --- a/.github/workflows/rag-cicd_ArgoCD.yaml +++ b/.github/workflows/rag-cicd_ArgoCD.yaml @@ -113,14 +113,6 @@ jobs: echo "✅ Tests completed successfully" - - name: Code Quality Check - run: | - cd rag - # Run linters - flake8 src/ --max-line-length=120 --exclude=__pycache__ - black --check src/ - mypy src/ --ignore-missing-imports - - name: Upload test results if: always() uses: actions/upload-artifact@v4 From d3cbeff3eea70fbb1b9f477a1169b4ca24e43ed5 Mon Sep 17 00:00:00 2001 From: djeon Date: Wed, 29 Oct 2025 23:22:24 +0900 Subject: [PATCH 07/10] feat: add rag ci/cd --- .github/workflows/rag-cicd_ArgoCD.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rag-cicd_ArgoCD.yaml b/.github/workflows/rag-cicd_ArgoCD.yaml index aad3fed..45782b5 100644 --- a/.github/workflows/rag-cicd_ArgoCD.yaml +++ b/.github/workflows/rag-cicd_ArgoCD.yaml @@ -197,7 +197,7 @@ jobs: sudo mv kustomize /usr/local/bin/ # 매니페스트 업데이트 - cd hgzero-rag/kustomize/overlays/${{ env.ENVIRONMENT }} + cd hgzero-back/kustomize/overlays/${{ env.ENVIRONMENT }} # RAG 서비스 이미지 태그 업데이트 kustomize edit set image acrdigitalgarage02.azurecr.io/hgzero/rag:${{ env.ENVIRONMENT }}-${{ env.IMAGE_TAG }} From 5b744170e5ed13aeff85f1def222d43c7056e108 Mon Sep 17 00:00:00 2001 From: djeon Date: Wed, 29 Oct 2025 23:39:01 +0900 Subject: [PATCH 08/10] feat: add rag ci/cd --- deployment/container/Dockerfile-rag | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deployment/container/Dockerfile-rag b/deployment/container/Dockerfile-rag index cdd7965..c587682 100644 --- a/deployment/container/Dockerfile-rag +++ b/deployment/container/Dockerfile-rag @@ -12,7 +12,7 @@ RUN apt-get update && apt-get install -y \ && rm -rf /var/lib/apt/lists/* # Copy requirements and install dependencies -COPY requirements.txt . +COPY rag/requirements.txt . RUN pip install --no-cache-dir --user -r requirements.txt # Run stage @@ -39,7 +39,7 @@ WORKDIR ${ARTIFACTORY_HOME} COPY --from=builder /root/.local /home/${USERNAME}/.local # Copy application code -COPY --chown=${USERNAME}:${USERNAME} . . +COPY --chown=${USERNAME}:${USERNAME} rag/ . # Update PATH to include user's local bin ENV PATH=/home/${USERNAME}/.local/bin:$PATH From 7fd4cccd80f996ec9f0ad97b3ef1ee32afa62f09 Mon Sep 17 00:00:00 2001 From: djeon Date: Wed, 29 Oct 2025 23:46:19 +0900 Subject: [PATCH 09/10] feat: add rag ci/cd --- deployment/container/Dockerfile-rag | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deployment/container/Dockerfile-rag b/deployment/container/Dockerfile-rag index c587682..cdd7965 100644 --- a/deployment/container/Dockerfile-rag +++ b/deployment/container/Dockerfile-rag @@ -12,7 +12,7 @@ RUN apt-get update && apt-get install -y \ && rm -rf /var/lib/apt/lists/* # Copy requirements and install dependencies -COPY rag/requirements.txt . +COPY requirements.txt . RUN pip install --no-cache-dir --user -r requirements.txt # Run stage @@ -39,7 +39,7 @@ WORKDIR ${ARTIFACTORY_HOME} COPY --from=builder /root/.local /home/${USERNAME}/.local # Copy application code -COPY --chown=${USERNAME}:${USERNAME} rag/ . +COPY --chown=${USERNAME}:${USERNAME} . . # Update PATH to include user's local bin ENV PATH=/home/${USERNAME}/.local/bin:$PATH From 987f48c7ff060c6611fa0604604c7ca8e55c966d Mon Sep 17 00:00:00 2001 From: djeon Date: Thu, 30 Oct 2025 08:44:01 +0900 Subject: [PATCH 10/10] fix: add stt security config (header) --- .../main/java/com/unicorn/hgzero/stt/config/SecurityConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stt/src/main/java/com/unicorn/hgzero/stt/config/SecurityConfig.java b/stt/src/main/java/com/unicorn/hgzero/stt/config/SecurityConfig.java index 8fa689d..c7fdcde 100644 --- a/stt/src/main/java/com/unicorn/hgzero/stt/config/SecurityConfig.java +++ b/stt/src/main/java/com/unicorn/hgzero/stt/config/SecurityConfig.java @@ -52,7 +52,7 @@ public class SecurityConfig { // 허용할 헤더 configuration.setAllowedHeaders(Arrays.asList( "Authorization", "Content-Type", "X-Requested-With", "Accept", - "Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers" + "Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", "X-User-Id", "X-User-Name", "X-User-Email" )); // 자격 증명 허용