hgzero/rag/docs/api-related-minutes.md

7.6 KiB

연관 회의록 조회 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

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 파라미터 추가

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

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:

{
  "minute_id": "MIN-2025-001",
  "meeting_title": "2025 Q1 마케팅 전략 회의",
  "summary": "2025년 1분기 마케팅 전략 수립을 위한 회의",
  "top_k": 5,
  "similarity_threshold": 0.7
}

Response:

[
  {
    "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)

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

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 오류

📊 데이터베이스 영향

쿼리 패턴

-- 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 설정 추가

구현 완료 체크리스트

  • Request/Response 모델 정의
  • DB 쿼리 함수 개선 (자기 자신 제외)
  • Redis 캐싱 유틸리티 구현
  • API 엔드포인트 구현
  • 2단계 캐싱 전략 적용
  • 설정 파일 업데이트
  • 문서 작성

작성자: 준호 (Backend Developer) 작성일: 2025-10-29 버전: 1.0.0