# 연관 회의록 조회 API 구현 문서 ## 📋 개요 **준호** (Backend Developer) 회의록 ID를 기준으로 유사한 연관 회의록을 조회하는 API를 구현했습니다. ## 🎯 구현 방식: Option A (DB 조회 후 벡터 검색) ### 선택 이유 - ✅ **정확도 우선**: full_content 기반 검색으로 높은 정확도 보장 - ✅ **확장성**: 향후 full_content 기반 추가 기능 확장 용이 - ✅ **성능 최적화**: Redis 캐싱으로 DB 조회 오버헤드 최소화 ### 처리 흐름 ``` 1. 요청 수신 ↓ 2. Redis 캐시 조회 (연관 회의록 결과) ├─ HIT → 즉시 반환 └─ MISS → 3단계로 진행 ↓ 3. Redis 캐시 조회 (회의록 full_content) ├─ HIT → 5단계로 진행 └─ MISS → 4단계로 진행 ↓ 4. DB 조회 (rag_minutes 테이블) ├─ full_content 획득 └─ Redis에 캐싱 (TTL: 30분) ↓ 5. 벡터 임베딩 생성 (Azure OpenAI) ↓ 6. 벡터 유사도 검색 (자기 자신 제외) ↓ 7. 결과 Redis 캐싱 (TTL: 1시간) ↓ 8. 응답 반환 ``` ## 🔧 구현 상세 ### 1. 데이터 모델 추가 **파일**: `rag/src/models/minutes.py` ```python class RelatedMinutesRequest(BaseModel): """연관 회의록 조회 요청""" minute_id: str # 기준 회의록 ID meeting_title: str # 회의 제목 (현재 미사용) summary: str # 회의록 요약 (현재 미사용) top_k: int = 5 # 반환할 최대 결과 수 similarity_threshold: float = 0.7 # 최소 유사도 임계값 class RelatedMinutesResponse(BaseModel): """연관 회의록 조회 응답""" minutes: RagMinutes # 회의록 정보 similarity_score: float # 유사도 점수 (0.0 ~ 1.0) ``` ### 2. DB 쿼리 함수 개선 **파일**: `rag/src/db/rag_minutes_db.py` **수정 내용**: `search_by_vector()` 함수에 `exclude_minutes_id` 파라미터 추가 ```python def search_by_vector( self, query_embedding: List[float], top_k: int = 5, similarity_threshold: float = 0.7, exclude_minutes_id: Optional[str] = None # 자기 자신 제외 ) -> List[Dict[str, Any]]: """벡터 유사도 검색""" # WHERE 절에 minutes_id != %s 조건 추가 # 자기 자신을 결과에서 제외 ``` ### 3. Redis 캐싱 유틸리티 구현 **파일**: `rag/src/utils/redis_cache.py` **주요 기능**: - `get(key)`: 캐시 조회 - `set(key, value, ttl)`: 캐시 저장 - `delete(key)`: 캐시 삭제 - `delete_pattern(pattern)`: 패턴 매칭 일괄 삭제 **특징**: - Redis 연결 실패 시 자동으로 캐싱 비활성화 (서비스 장애 방지) - JSON 직렬화/역직렬화 자동 처리 ### 4. 설정 추가 **파일**: `rag/config.yaml` ```yaml rag_minutes: search: top_k: 5 similarity_threshold: 0.7 cache: ttl: 1800 # 회의록 조회 결과: 30분 prefix: "minutes:" related_ttl: 3600 # 연관 회의록 검색 결과: 1시간 ``` ### 5. API 엔드포인트 구현 **파일**: `rag/src/api/main.py` **엔드포인트**: `POST /api/minutes/related` **Request Body**: ```json { "minute_id": "MIN-2025-001", "meeting_title": "2025 Q1 마케팅 전략 회의", "summary": "2025년 1분기 마케팅 전략 수립을 위한 회의", "top_k": 5, "similarity_threshold": 0.7 } ``` **Response**: ```json [ { "minutes": { "meeting_id": "MTG-2024-050", "title": "2024 Q4 마케팅 성과 분석", "minutes_id": "MIN-2024-050", "full_content": "...", ... }, "similarity_score": 0.85 }, ... ] ``` ## ⚡ 성능 최적화 ### 2단계 캐싱 전략 1. **회의록 조회 결과 캐싱** (TTL: 30분) - 키: `minutes:{minute_id}` - DB 조회 횟수 감소 2. **연관 회의록 검색 결과 캐싱** (TTL: 1시간) - 키: `minutes:related:{minute_id}:{top_k}:{threshold}` - 벡터 검색 및 임베딩 생성 비용 절감 ### 예상 성능 개선 | 케이스 | 처리 시간 | 비고 | |--------|----------|------| | 캐시 MISS (최초 요청) | ~2-3초 | DB 조회 + 임베딩 생성 + 벡터 검색 | | 회의록 캐시 HIT | ~1-2초 | 임베딩 생성 + 벡터 검색 | | 연관 회의록 캐시 HIT | ~50ms | 캐시에서 즉시 반환 | ## 🔍 사용 예시 ### Python (requests) ```python import requests url = "http://localhost:8000/api/minutes/related" data = { "minute_id": "MIN-2025-001", "meeting_title": "2025 Q1 마케팅 전략 회의", "summary": "2025년 1분기 마케팅 전략 수립", "top_k": 5, "similarity_threshold": 0.7 } response = requests.post(url, json=data) related_minutes = response.json() for item in related_minutes: print(f"제목: {item['minutes']['title']}") print(f"유사도: {item['similarity_score']:.2f}") print("---") ``` ### curl ```bash curl -X POST "http://localhost:8000/api/minutes/related" \ -H "Content-Type: application/json" \ -d '{ "minute_id": "MIN-2025-001", "meeting_title": "2025 Q1 마케팅 전략 회의", "summary": "2025년 1분기 마케팅 전략 수립", "top_k": 5, "similarity_threshold": 0.7 }' ``` ## 🧪 테스트 시나리오 ### 1. 정상 케이스 - ✅ 존재하는 minute_id로 요청 - ✅ 유사한 회의록 5개 반환 - ✅ 자기 자신은 결과에서 제외 ### 2. 캐싱 동작 확인 - ✅ 최초 요청: 캐시 MISS → DB 조회 - ✅ 2번째 요청: 캐시 HIT → 즉시 반환 - ✅ TTL 경과 후: 캐시 MISS → 재조회 ### 3. 에러 케이스 - ❌ 존재하지 않는 minute_id → 404 오류 - ❌ 잘못된 요청 형식 → 422 오류 ## 📊 데이터베이스 영향 ### 쿼리 패턴 ```sql -- 1. 회의록 조회 (캐시 MISS 시) SELECT * FROM rag_minutes WHERE minutes_id = 'MIN-2025-001'; -- 2. 벡터 유사도 검색 (캐시 MISS 시) SELECT *, 1 - (embedding <=> '{vector}'::vector) as similarity_score FROM rag_minutes WHERE embedding IS NOT NULL AND 1 - (embedding <=> '{vector}'::vector) >= 0.7 AND minutes_id != 'MIN-2025-001' -- 자기 자신 제외 ORDER BY embedding <=> '{vector}'::vector LIMIT 5; ``` ### 인덱스 활용 - `minutes_id`: Primary Key 인덱스 활용 - `embedding`: pgvector 인덱스 활용 (IVFFlat 또는 HNSW) ## 🔐 보안 고려사항 1. **SQL Injection 방지**: psycopg2 파라미터 바인딩 사용 2. **Rate Limiting**: 향후 추가 권장 (분당 요청 수 제한) 3. **인증/인가**: 향후 JWT 토큰 기반 인증 추가 권장 ## 🚀 향후 개선 사항 ### 1. Hybrid 검색 지원 (Option B 통합) - summary를 활용한 빠른 1차 검색 - full_content로 정밀한 2차 검색 - 적응형 임계값 조정 ### 2. 성능 모니터링 - 캐시 히트율 추적 - API 응답 시간 측정 - 벡터 검색 성능 분석 ### 3. 검색 품질 개선 - 시간 가중치 적용 (최근 회의록 우선) - 부서/팀별 필터링 옵션 - 주제별 카테고리 분류 ## 📝 파일 변경 목록 | 파일 | 변경 유형 | 설명 | |------|----------|------| | `rag/src/models/minutes.py` | 수정 | Request/Response 모델 추가 | | `rag/src/db/rag_minutes_db.py` | 수정 | exclude_minutes_id 파라미터 추가 | | `rag/src/utils/redis_cache.py` | 신규 | Redis 캐싱 유틸리티 | | `rag/src/api/main.py` | 수정 | API 엔드포인트 추가 | | `rag/config.yaml` | 수정 | rag_minutes 설정 추가 | ## ✅ 구현 완료 체크리스트 - [x] Request/Response 모델 정의 - [x] DB 쿼리 함수 개선 (자기 자신 제외) - [x] Redis 캐싱 유틸리티 구현 - [x] API 엔드포인트 구현 - [x] 2단계 캐싱 전략 적용 - [x] 설정 파일 업데이트 - [x] 문서 작성 --- **작성자**: 준호 (Backend Developer) **작성일**: 2025-10-29 **버전**: 1.0.0