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 4a523ac..1cc8c53 100644 Binary files a/rag/src/api/__pycache__/main.cpython-311.pyc and b/rag/src/api/__pycache__/main.cpython-311.pyc differ 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 0000000..800a2de Binary files /dev/null and b/rag/src/api/__pycache__/term_routes.cpython-311.pyc differ diff --git a/rag/src/api/main.py b/rag/src/api/main.py index b1e989b..b5dd99b 100644 --- a/rag/src/api/main.py +++ b/rag/src/api/main.py @@ -5,6 +5,7 @@ from fastapi import FastAPI, HTTPException, Depends from fastapi.middleware.cors import CORSMiddleware from typing import List, Dict, Any import logging +import asyncio from pathlib import Path from ..models.term import ( @@ -30,10 +31,12 @@ from ..db.postgres_vector import PostgresVectorDB from ..db.azure_search import AzureAISearchDB from ..db.rag_minutes_db import RagMinutesDB from ..services.claude_service import ClaudeService +from ..services.sse_manager import sse_manager from ..utils.config import load_config, get_database_url from ..utils.embedding import EmbeddingGenerator from ..utils.text_processor import extract_nouns_as_query from ..utils.redis_cache import RedisCache +from . import term_routes # 로깅 설정 logging.basicConfig( @@ -58,6 +61,64 @@ app.add_middleware( allow_headers=["*"], ) +# SSE 라우터 등록 +app.include_router(term_routes.router) + + +# 앱 시작/종료 이벤트 핸들러 +@app.on_event("startup") +async def startup_event(): + """앱 시작 시 SSE Manager 및 EventHub Consumer 시작""" + global _eventhub_consumer_task + + # SSE Manager 시작 + await sse_manager.start() + logger.info("SSE Manager 시작 완료") + + # EventHub Consumer를 백그라운드 태스크로 시작 + _eventhub_consumer_task = asyncio.create_task(start_eventhub_consumer()) + logger.info("EventHub Consumer 백그라운드 태스크 시작 완료") + + logger.info("FastAPI 앱 시작 완료") + + +@app.on_event("shutdown") +async def shutdown_event(): + """앱 종료 시 SSE Manager 및 EventHub Consumer 정리""" + global _eventhub_consumer_task + + # EventHub Consumer 태스크 취소 + if _eventhub_consumer_task: + _eventhub_consumer_task.cancel() + try: + await _eventhub_consumer_task + except asyncio.CancelledError: + logger.info("EventHub Consumer 태스크 취소됨") + + # SSE Manager 종료 + await sse_manager.stop() + logger.info("FastAPI 앱 종료 완료") + + +async def start_eventhub_consumer(): + """EventHub Consumer 시작 함수""" + try: + from ..services.eventhub_consumer import start_consumer + + config = get_config() + rag_minutes_db = get_rag_minutes_db() + embedding_gen = get_embedding_gen() + term_db = get_term_db() + + logger.info("EventHub Consumer 시작 중...") + await start_consumer(config, rag_minutes_db, embedding_gen, term_db) + + except asyncio.CancelledError: + logger.info("EventHub Consumer 종료 요청 수신") + raise + except Exception as e: + logger.error(f"EventHub Consumer 실행 중 에러: {str(e)}", exc_info=True) + # 전역 변수 (의존성 주입용) _config = None _term_db = None @@ -66,6 +127,7 @@ _rag_minutes_db = None _embedding_gen = None _claude_service = None _redis_cache = None +_eventhub_consumer_task = None def get_config(): @@ -176,6 +238,36 @@ async def root(): } +@app.get("/health") +async def health_check(): + """헬스 체크 엔드포인트""" + global _eventhub_consumer_task + + eventhub_status = "unknown" + if _eventhub_consumer_task: + if _eventhub_consumer_task.done(): + try: + _eventhub_consumer_task.result() + eventhub_status = "stopped" + except Exception as e: + eventhub_status = f"error: {str(e)}" + else: + eventhub_status = "running" + else: + eventhub_status = "not_started" + + return { + "status": "healthy", + "sse_manager": { + "active_sessions": len(sse_manager.get_active_sessions()), + "sessions": sse_manager.get_active_sessions() + }, + "eventhub_consumer": { + "status": eventhub_status + } + } + + @app.post("/api/rag/terms/search", response_model=List[TermSearchResult]) async def search_terms( request: TermSearchRequest, diff --git a/rag/src/api/term_routes.py b/rag/src/api/term_routes.py new file mode 100644 index 0000000..5500e60 --- /dev/null +++ b/rag/src/api/term_routes.py @@ -0,0 +1,93 @@ +""" +용어 관련 API 엔드포인트 +""" +from fastapi import APIRouter, HTTPException +from sse_starlette.sse import EventSourceResponse +import asyncio +import json +import logging + +from ..services.sse_manager import sse_manager + +logger = logging.getLogger(__name__) +router = APIRouter(prefix="/api/rag/terms", tags=["terms"]) + + +@router.get("/stream/{session_id}") +async def stream_terms(session_id: str): + """ + 용어 검색 결과 SSE 스트림 + + Args: + session_id: 회의 세션 ID + + Returns: + SSE 스트림 + """ + try: + # SSE 연결 등록 + queue = sse_manager.register(session_id) + logger.info(f"용어 스트림 시작: {session_id}") + + async def event_generator(): + """SSE 이벤트 생성기""" + try: + # 연결 확인 메시지 + yield { + "event": "connected", + "data": json.dumps({"session_id": session_id, "status": "connected"}) + } + + # 메시지 수신 및 전송 + while True: + try: + # Timeout을 두어 주기적으로 heartbeat 전송 + message = await asyncio.wait_for(queue.get(), timeout=30.0) + + yield { + "event": message["event"], + "data": json.dumps(message["data"]) + } + + except asyncio.TimeoutError: + # Heartbeat 전송 + yield { + "event": "heartbeat", + "data": json.dumps({"type": "heartbeat"}) + } + + except asyncio.CancelledError: + logger.info(f"용어 스트림 취소됨: {session_id}") + except Exception as e: + logger.error(f"이벤트 생성 중 에러: {str(e)}") + finally: + # 연결 정리 + sse_manager.unregister(session_id) + logger.info(f"용어 스트림 종료: {session_id}") + + return EventSourceResponse(event_generator()) + + except ValueError as e: + raise HTTPException(status_code=429, detail=str(e)) + except Exception as e: + logger.error(f"스트림 시작 실패: {str(e)}") + raise HTTPException(status_code=500, detail="스트림 시작 실패") + + +@router.get("/stream/{session_id}/status") +async def get_stream_status(session_id: str): + """ + 스트림 연결 상태 확인 + + Args: + session_id: 회의 세션 ID + + Returns: + 연결 상태 + """ + is_connected = sse_manager.is_connected(session_id) + + return { + "session_id": session_id, + "connected": is_connected + } diff --git a/rag/src/services/__pycache__/eventhub_consumer.cpython-311.pyc b/rag/src/services/__pycache__/eventhub_consumer.cpython-311.pyc index 9555a26..b36bd70 100644 Binary files a/rag/src/services/__pycache__/eventhub_consumer.cpython-311.pyc and b/rag/src/services/__pycache__/eventhub_consumer.cpython-311.pyc differ diff --git a/rag/src/services/__pycache__/sse_manager.cpython-311.pyc b/rag/src/services/__pycache__/sse_manager.cpython-311.pyc new file mode 100644 index 0000000..6991407 Binary files /dev/null and b/rag/src/services/__pycache__/sse_manager.cpython-311.pyc differ diff --git a/rag/src/services/eventhub_consumer.py b/rag/src/services/eventhub_consumer.py index e844e25..a4c8d7f 100644 --- a/rag/src/services/eventhub_consumer.py +++ b/rag/src/services/eventhub_consumer.py @@ -5,6 +5,7 @@ Azure Event Hub Consumer 서비스 import asyncio import json import logging +import re from typing import Dict, Any, Optional, Union, List from datetime import datetime @@ -112,7 +113,13 @@ class EventHubConsumer: try: # 이벤트 데이터 파싱 event_body = event.body_as_str() - event_data = json.loads(event_body) + logger.debug(f"원본 이벤트 데이터 (처음 200자): {event_body[:200]}") + + # Java LocalDateTime 배열을 문자열로 변환하여 JSON 파싱 가능하게 변환 + converted_body = self._convert_java_datetime_arrays(event_body) + logger.debug(f"변환된 이벤트 데이터 (처음 200자): {converted_body[:200]}") + + event_data = json.loads(converted_body) event_type = event_data.get('eventType', 'unknown') logger.info(f"이벤트 수신: {event_type}") @@ -132,7 +139,7 @@ class EventHubConsumer: except json.JSONDecodeError as e: logger.error(f"이벤트 파싱 실패: {str(e)}") except Exception as e: - logger.error(f"이벤트 처리 실패: {str(e)}") + logger.error(f"이벤트 처리 실패: {str(e)}", exc_info=True) async def _on_error(self, partition_context, error): """ @@ -144,6 +151,46 @@ class EventHubConsumer: """ logger.error(f"Event Hub 에러 (Partition {partition_context.partition_id}): {str(error)}") + def _convert_java_datetime_arrays(self, json_str: str) -> 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 +
+
+ +
+

📊 용어 검색 결과

+
+
+ + + +

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

+
+
+ +

📝 로그

+
+
+

로그가 여기에 표시됩니다

+
+
+
+
+ + + +