mirror of
https://github.com/hwanny1128/HGZero.git
synced 2025-12-06 16:06:23 +00:00
Merge branch 'main' of https://github.com/hwanny1128/HGZero
This commit is contained in:
commit
5c4f186290
7
.github/config/deploy_env_vars_rag_dev
vendored
Normal file
7
.github/config/deploy_env_vars_rag_dev
vendored
Normal file
@ -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
|
||||
7
.github/config/deploy_env_vars_rag_prod
vendored
Normal file
7
.github/config/deploy_env_vars_rag_prod
vendored
Normal file
@ -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
|
||||
7
.github/config/deploy_env_vars_rag_staging
vendored
Normal file
7
.github/config/deploy_env_vars_rag_staging
vendored
Normal file
@ -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
|
||||
213
.github/workflows/rag-cicd_ArgoCD.yaml
vendored
Normal file
213
.github/workflows/rag-cicd_ArgoCD.yaml
vendored
Normal file
@ -0,0 +1,213 @@
|
||||
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: 'true'
|
||||
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 || 'true' }}
|
||||
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: 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-back/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가 자동으로 배포합니다."
|
||||
@ -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": "오늘 회의에서는 프로젝트 일정과 예산에 대해 논의했습니다...",
|
||||
|
||||
@ -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 "=================================="
|
||||
|
||||
57
deployment/container/Dockerfile-rag
Normal file
57
deployment/container/Dockerfile-rag
Normal file
@ -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"]
|
||||
@ -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/` 디렉토리에 있습니다.
|
||||
132
rag/README.md
132
rag/README.md
@ -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)
|
||||
@ -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 가이드
|
||||
508
rag/TESTING.md
508
rag/TESTING.md
@ -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 파이프라인 구축
|
||||
- 자동화된 성능 테스트
|
||||
@ -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 <PID> -o pid,etime,%cpu,%mem,cmd
|
||||
|
||||
# 네트워크 연결 확인
|
||||
lsof -i -n | grep <PID>
|
||||
|
||||
# 모든 Python 프로세스 확인
|
||||
ps aux | grep python | grep -v grep
|
||||
```
|
||||
|
||||
### 프로세스 종료
|
||||
|
||||
```bash
|
||||
# 정상 종료 (SIGTERM)
|
||||
kill <PID>
|
||||
|
||||
# 강제 종료 (SIGKILL)
|
||||
kill -9 <PID>
|
||||
|
||||
# 이름으로 종료
|
||||
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에 영향 없음
|
||||
- 독립적으로 모든 이벤트를 처음부터 읽을 수 있음
|
||||
- 여러 번 테스트 가능
|
||||
@ -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**: 메모리 절약 우선 (검색 속도 느림)
|
||||
@ -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
|
||||
|
||||
Binary file not shown.
BIN
rag/src/api/__pycache__/term_routes.cpython-311.pyc
Normal file
BIN
rag/src/api/__pycache__/term_routes.cpython-311.pyc
Normal file
Binary file not shown.
@ -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,
|
||||
|
||||
93
rag/src/api/term_routes.py
Normal file
93
rag/src/api/term_routes.py
Normal file
@ -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
|
||||
}
|
||||
Binary file not shown.
BIN
rag/src/services/__pycache__/sse_manager.cpython-311.pyc
Normal file
BIN
rag/src/services/__pycache__/sse_manager.cpython-311.pyc
Normal file
Binary file not shown.
@ -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)
|
||||
|
||||
183
rag/src/services/sse_manager.py
Normal file
183
rag/src/services/sse_manager.py
Normal file
@ -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()
|
||||
@ -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
|
||||
@ -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()
|
||||
@ -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())
|
||||
@ -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()
|
||||
573
rag/test_sse.html
Normal file
573
rag/test_sse.html
Normal file
@ -0,0 +1,573 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SSE 용어 검색 테스트</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.controls {
|
||||
padding: 30px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.input-group input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
flex: 1;
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-connect {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-connect:hover {
|
||||
background: #5568d3;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-disconnect {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-disconnect:hover {
|
||||
background: #da190b;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(244, 67, 54, 0.4);
|
||||
}
|
||||
|
||||
.btn-clear {
|
||||
background: #9e9e9e;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-clear:hover {
|
||||
background: #757575;
|
||||
}
|
||||
|
||||
.btn-check {
|
||||
background: #2196f3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-check:hover {
|
||||
background: #0d47a1;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(33, 150, 243, 0.4);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 20px 30px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-indicator.connected {
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
.status-indicator.disconnected {
|
||||
background: #f44336;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.results {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 8px;
|
||||
background: #f8f9fa;
|
||||
border-left: 4px solid #667eea;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.result-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.result-time {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.result-text {
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border-radius: 5px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.terms {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.term-card {
|
||||
padding: 15px;
|
||||
background: white;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #e0e0e0;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.term-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.term-name {
|
||||
font-weight: 600;
|
||||
color: #667eea;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.term-definition {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.term-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.term-category {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.term-score {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-state svg {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin-bottom: 20px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 5px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.log-connected {
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.log-term-result {
|
||||
background: #e3f2fd;
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.log-heartbeat {
|
||||
background: #f3e5f5;
|
||||
color: #7b1fa2;
|
||||
}
|
||||
|
||||
.log-error {
|
||||
background: #ffebee;
|
||||
color: #c62828;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🔍 SSE 용어 검색 테스트</h1>
|
||||
<p>실시간 용어 검색 결과를 확인하세요</p>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="input-group">
|
||||
<label for="serverUrl">RAG 서버 URL</label>
|
||||
<input type="text" id="serverUrl" value="http://localhost:8000" placeholder="http://localhost:8000">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="sessionId">세션 ID (Meeting ID)</label>
|
||||
<input type="text" id="sessionId" value="meeting-124" placeholder="meeting-124">
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button class="btn-connect" onclick="connect()">🔌 연결</button>
|
||||
<button class="btn-disconnect" onclick="disconnect()" disabled>🔌 연결 해제</button>
|
||||
<button class="btn-check" onclick="checkStatus()">🔍 연결 상태 확인</button>
|
||||
<button class="btn-clear" onclick="clearResults()">🗑️ 결과 지우기</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status">
|
||||
<div class="status-item">
|
||||
<span class="status-indicator disconnected" id="statusIndicator"></span>
|
||||
<span id="statusText">연결 안됨</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span>수신: <strong id="messageCount">0</strong>개</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<h2 style="margin-bottom: 20px;">📊 용어 검색 결과</h2>
|
||||
<div class="results" id="results">
|
||||
<div class="empty-state">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
||||
</svg>
|
||||
<p>연결 후 용어 검색 결과가 여기에 표시됩니다</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 style="margin: 30px 0 20px 0;">📝 로그</h2>
|
||||
<div class="results" id="logs" style="max-height: 200px;">
|
||||
<div class="empty-state" style="padding: 30px;">
|
||||
<p>로그가 여기에 표시됩니다</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let eventSource = null;
|
||||
let messageCount = 0;
|
||||
|
||||
function connect() {
|
||||
const serverUrl = document.getElementById('serverUrl').value;
|
||||
const sessionId = document.getElementById('sessionId').value;
|
||||
|
||||
if (!serverUrl || !sessionId) {
|
||||
alert('서버 URL과 세션 ID를 입력해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `${serverUrl}/api/rag/terms/stream/${sessionId}`;
|
||||
|
||||
addLog('연결 시도 중...', 'connected');
|
||||
|
||||
eventSource = new EventSource(url);
|
||||
|
||||
// 연결 성공
|
||||
eventSource.addEventListener('connected', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
updateStatus(true);
|
||||
addLog(`연결 성공: ${JSON.stringify(data)}`, 'connected');
|
||||
});
|
||||
|
||||
// 용어 검색 결과 수신
|
||||
eventSource.addEventListener('term_result', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
messageCount++;
|
||||
document.getElementById('messageCount').textContent = messageCount;
|
||||
addResult(data);
|
||||
addLog(`용어 결과 수신: ${data.total_count}개`, 'term-result');
|
||||
});
|
||||
|
||||
// Heartbeat
|
||||
eventSource.addEventListener('heartbeat', (e) => {
|
||||
addLog('Heartbeat 수신', 'heartbeat');
|
||||
});
|
||||
|
||||
// 에러 처리
|
||||
eventSource.onerror = (error) => {
|
||||
console.error('SSE 에러:', error);
|
||||
addLog('연결 에러 발생', 'error');
|
||||
updateStatus(false);
|
||||
};
|
||||
|
||||
// UI 업데이트
|
||||
document.querySelector('.btn-connect').disabled = true;
|
||||
document.querySelector('.btn-disconnect').disabled = false;
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
updateStatus(false);
|
||||
addLog('연결 해제됨', 'error');
|
||||
}
|
||||
|
||||
document.querySelector('.btn-connect').disabled = false;
|
||||
document.querySelector('.btn-disconnect').disabled = true;
|
||||
}
|
||||
|
||||
function updateStatus(connected) {
|
||||
const indicator = document.getElementById('statusIndicator');
|
||||
const text = document.getElementById('statusText');
|
||||
|
||||
if (connected) {
|
||||
indicator.className = 'status-indicator connected';
|
||||
text.textContent = '연결됨';
|
||||
} else {
|
||||
indicator.className = 'status-indicator disconnected';
|
||||
text.textContent = '연결 안됨';
|
||||
}
|
||||
}
|
||||
|
||||
function addResult(data) {
|
||||
const resultsDiv = document.getElementById('results');
|
||||
|
||||
// 빈 상태 제거
|
||||
const emptyState = resultsDiv.querySelector('.empty-state');
|
||||
if (emptyState) {
|
||||
emptyState.remove();
|
||||
}
|
||||
|
||||
const resultItem = document.createElement('div');
|
||||
resultItem.className = 'result-item';
|
||||
|
||||
const now = new Date().toLocaleTimeString('ko-KR');
|
||||
|
||||
let termsHtml = '';
|
||||
if (data.terms && data.terms.length > 0) {
|
||||
termsHtml = '<div class="terms">';
|
||||
data.terms.forEach(term => {
|
||||
termsHtml += `
|
||||
<div class="term-card">
|
||||
<div class="term-name">${term.term_name}</div>
|
||||
<div class="term-definition">${term.definition}</div>
|
||||
<div class="term-meta">
|
||||
<span class="term-category">${term.category}</span>
|
||||
<span class="term-score">점수: ${term.relevance_score.toFixed(3)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
termsHtml += '</div>';
|
||||
} else {
|
||||
termsHtml = '<p style="color: #999;">매칭되는 용어가 없습니다</p>';
|
||||
}
|
||||
|
||||
resultItem.innerHTML = `
|
||||
<div class="result-header">
|
||||
<strong>세그먼트: ${data.segment_id}</strong>
|
||||
<span class="result-time">${now}</span>
|
||||
</div>
|
||||
<div class="result-text">"${data.text}..."</div>
|
||||
${termsHtml}
|
||||
`;
|
||||
|
||||
resultsDiv.insertBefore(resultItem, resultsDiv.firstChild);
|
||||
}
|
||||
|
||||
function addLog(message, type) {
|
||||
const logsDiv = document.getElementById('logs');
|
||||
|
||||
// 빈 상태 제거
|
||||
const emptyState = logsDiv.querySelector('.empty-state');
|
||||
if (emptyState) {
|
||||
emptyState.remove();
|
||||
}
|
||||
|
||||
const logItem = document.createElement('div');
|
||||
logItem.className = `log-item log-${type}`;
|
||||
|
||||
const now = new Date().toLocaleTimeString('ko-KR');
|
||||
logItem.textContent = `[${now}] ${message}`;
|
||||
|
||||
logsDiv.insertBefore(logItem, logsDiv.firstChild);
|
||||
|
||||
// 최대 50개까지만 유지
|
||||
while (logsDiv.children.length > 50) {
|
||||
logsDiv.removeChild(logsDiv.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkStatus() {
|
||||
const serverUrl = document.getElementById('serverUrl').value;
|
||||
const sessionId = document.getElementById('sessionId').value;
|
||||
|
||||
if (!serverUrl || !sessionId) {
|
||||
alert('서버 URL과 세션 ID를 입력해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
addLog('연결 상태 확인 중...', 'connected');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${serverUrl}/api/rag/terms/stream/${sessionId}/status`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.connected) {
|
||||
addLog(`✅ 연결 상태: 연결됨 (세션 ID: ${data.session_id})`, 'connected');
|
||||
updateStatus(true);
|
||||
} else {
|
||||
addLog(`❌ 연결 상태: 연결 안됨 (세션 ID: ${data.session_id})`, 'error');
|
||||
updateStatus(false);
|
||||
}
|
||||
} catch (error) {
|
||||
addLog(`❌ 상태 확인 실패: ${error.message}`, 'error');
|
||||
console.error('상태 확인 에러:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function clearResults() {
|
||||
const resultsDiv = document.getElementById('results');
|
||||
resultsDiv.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
||||
</svg>
|
||||
<p>연결 후 용어 검색 결과가 여기에 표시됩니다</p>
|
||||
</div>
|
||||
`;
|
||||
messageCount = 0;
|
||||
document.getElementById('messageCount').textContent = '0';
|
||||
}
|
||||
|
||||
// 페이지 이탈 시 연결 해제
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -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"
|
||||
));
|
||||
|
||||
// 자격 증명 허용
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user