# OpenRAG 기술 도입 스펙 — 아누 시스템 + InsuRo 적용

> 원본 분석: OpenRAG v0.3.0 (langflow-ai/openrag)
> 작성일: 2026-03-13
> 대상: 아누 시스템 인프라 + InsuRo 서비스

---

## 도입 기능 목록

| # | 기능 | 원본 기술 | 우리 구현 | 적용 대상 |
|---|------|----------|----------|----------|
| 1 | 문서 파싱 고도화 | Docling | Docling 직접 도입 | InsuRo(금소법), InsuWiki(약관) |
| 2 | 하이브리드 검색 | OpenSearch KNN | Supabase pgvector + 텍스트 | InsuWiki, InsuRo |
| 3 | MCP 서버 | openrag-mcp | 자체 MCP 서버 | 아누 시스템 전체 |
| 4 | Drive 커넥터 고도화 | Google Drive connector | gdrive.py 고도화 | InsuRo |
| 5 | 벡터 검색 인프라 | OpenSearch 풀스택 | Supabase pgvector 경량 | 공통 인프라 |

---

## 기능 1: Docling 문서 파싱

### 현재
- pdfplumber만 사용 (텍스트 PDF OK, 표/이미지/스캔 PDF 미지원)

### 도입 후
- PDF, 이미지, 스캔 문서, PPT, DOCX, HTML 등 다양한 포맷 파싱
- 표(table) 구조 보존, OCR 자동 처리
- 보험 약관 PDF의 복잡한 레이아웃 정확 추출

### 구현 방법
```
옵션 A: Docling 서버 (Docker)
  - docling serve 명령으로 REST API 서버 실행 (포트 5001)
  - FastAPI에서 HTTP로 호출
  - 장점: 격리, 안정성
  - 단점: Docker 리소스 추가

옵션 B: Docling 라이브러리 직접 사용
  - pip install docling
  - ai_parser.py에서 직접 import
  - 장점: 단순, 추가 서비스 불필요
  - 단점: 메모리 사용 증가
```

### 적용 파일
- `/home/jay/projects/InsuRo/server/ai_parser.py` — extract_text_from_pdf() 교체
- InsuWiki 약관 파싱 파이프라인 (향후)

### 의존성
- `pip install docling` (또는 Docker image)
- PyTorch (OCR 사용 시)

---

## 기능 2: 하이브리드 검색 (시맨틱 + 키워드)

### 현재
- InsuWiki: Supabase 텍스트 검색만 (LIKE, ilike)
- InsuRo: fcpa_config 단일 레코드 조회

### 도입 후
- 시맨틱 검색: "실비보험 갱신 시 불이익" → 의미적으로 관련된 문서 반환
- 키워드 검색: "제22조제3항" → 정확한 법조문 매칭
- 하이브리드: 시맨틱(0.7) + 키워드(0.3) 가중치 결합

### 구현 방법
```
1. Supabase에 pgvector 활성화 (이미 지원됨)
2. 임베딩 생성: Claude/OpenAI Embedding API → 1536차원 벡터
3. 테이블 설계:
   CREATE TABLE documents (
     id UUID PRIMARY KEY,
     content TEXT,
     embedding VECTOR(1536),
     metadata JSONB,
     source TEXT, -- 'insuwiki', 'insuro_fcpa', etc.
     created_at TIMESTAMPTZ
   );
   CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops);

4. 검색 쿼리:
   -- 시맨틱 검색
   SELECT * FROM documents
   ORDER BY embedding <=> query_embedding
   LIMIT 10;

   -- 하이브리드 (RRF 또는 가중치 결합)
   SELECT *,
     (0.7 * semantic_score + 0.3 * keyword_score) AS combined_score
   FROM ...
```

### 적용 파일
- 새 파일: `/home/jay/workspace/libs/vector_search.py` (공통 라이브러리)
- InsuWiki 검색 API 연동
- InsuRo 금소법 검색 기능

### 의존성
- Supabase pgvector (무료 플랜에서도 사용 가능)
- 임베딩 API (Claude CLI 또는 OpenAI)

---

## 기능 3: MCP 서버 (Claude Desktop/Cursor 연동)

### 현재
- 아누 시스템은 Telegram 챗봇으로만 접근 가능
- Claude Desktop에서 우리 지식 직접 참조 불가

### 도입 후
- Claude Desktop에서 "InsuWiki에서 실비보험 갱신형 검색해줘" 가능
- Cursor에서 코딩 중 우리 문서 참조 가능
- GEO 전략: AI가 우리 시스템을 직접 도구로 사용

### 구현 방법
```
MCP 서버 구조:
  - stdio 기반 통신 (Anthropic MCP 프로토콜)
  - 3개 Tool 제공:
    1. search_knowledge(query) — 하이브리드 검색
    2. get_document(id) — 문서 상세 조회
    3. list_categories() — 카테고리 목록

설정 (Claude Desktop claude_desktop_config.json):
  {
    "mcpServers": {
      "anu-knowledge": {
        "command": "python",
        "args": ["/path/to/mcp_server.py"]
      }
    }
  }
```

### 적용 파일
- 새 파일: `/home/jay/workspace/services/mcp_server.py`
- 의존성: `mcp` 패키지 (Anthropic MCP SDK)

---

## 기능 4: Google Drive 커넥터 고도화

### 현재
- InsuRo gdrive.py: 단순 업로드/폴더 생성만
- 변경 감지, 메타데이터 관리, ACL 없음

### 도입 후
- 웹훅 기반 변경 감지 (새 파일 추가 시 자동 인제스션)
- 메타데이터 추출/태깅
- 폴더 구조 동기화

### 구현 방법
```
1. Google Drive Push Notifications (Changes API)
   - watch 등록 → 변경 시 webhook 콜백
   - 새 PDF 감지 → 자동 파싱 → 벡터 인덱싱

2. 메타데이터 관리
   - 파일별 source, category, tags JSONB 저장
   - Drive 파일 설명(description)에서 메타데이터 추출

3. 동기화 상태 관리
   - sync_status 테이블: file_id, last_synced, hash
   - 변경된 파일만 재인덱싱 (incremental)
```

### 적용 파일
- `/home/jay/projects/InsuRo/server/gdrive.py` — 고도화
- 새 파일: `gdrive_sync.py` (동기화 데몬)

---

## 기능 5: Supabase pgvector 벡터 검색 인프라

### OpenSearch 대신 pgvector를 선택한 이유
- 이미 Supabase 사용 중 (추가 서비스 불필요)
- pgvector는 Supabase에 내장 (활성화만 하면 됨)
- 5개 서비스 운영 vs 기존 인프라 활용 → 리소스/비용 절감
- 우리 규모(보험 약관 수백~수천 건)에 pgvector로 충분

### 구현 방법
```sql
-- 1. pgvector 활성화
CREATE EXTENSION IF NOT EXISTS vector;

-- 2. 문서 테이블
CREATE TABLE knowledge_documents (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  title TEXT NOT NULL,
  content TEXT NOT NULL,
  content_chunks JSONB, -- 청킹된 텍스트 배열
  embedding VECTOR(1536),
  source TEXT NOT NULL, -- 'insuwiki', 'insuro_fcpa', 'insuro_product'
  source_url TEXT,
  metadata JSONB DEFAULT '{}',
  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now()
);

-- 3. 청크 테이블 (문서를 여러 청크로 분리)
CREATE TABLE knowledge_chunks (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  document_id UUID REFERENCES knowledge_documents(id) ON DELETE CASCADE,
  chunk_index INT NOT NULL,
  content TEXT NOT NULL,
  embedding VECTOR(1536),
  token_count INT,
  metadata JSONB DEFAULT '{}',
  created_at TIMESTAMPTZ DEFAULT now()
);

-- 4. 벡터 인덱스
CREATE INDEX ON knowledge_chunks USING ivfflat (embedding vector_cosine_ops)
  WITH (lists = 100);

-- 5. 전문 검색 인덱스
CREATE INDEX ON knowledge_chunks USING gin (to_tsvector('korean', content));

-- 6. 하이브리드 검색 함수
CREATE OR REPLACE FUNCTION hybrid_search(
  query_text TEXT,
  query_embedding VECTOR(1536),
  match_count INT DEFAULT 10,
  semantic_weight FLOAT DEFAULT 0.7,
  keyword_weight FLOAT DEFAULT 0.3
) RETURNS TABLE (
  chunk_id UUID,
  document_id UUID,
  content TEXT,
  similarity FLOAT,
  combined_score FLOAT
) AS $$
  WITH semantic AS (
    SELECT id, document_id, content,
      1 - (embedding <=> query_embedding) AS score
    FROM knowledge_chunks
    ORDER BY embedding <=> query_embedding
    LIMIT match_count * 2
  ),
  keyword AS (
    SELECT id, document_id, content,
      ts_rank(to_tsvector('korean', content), plainto_tsquery('korean', query_text)) AS score
    FROM knowledge_chunks
    WHERE to_tsvector('korean', content) @@ plainto_tsquery('korean', query_text)
    LIMIT match_count * 2
  )
  SELECT
    COALESCE(s.id, k.id) AS chunk_id,
    COALESCE(s.document_id, k.document_id) AS document_id,
    COALESCE(s.content, k.content) AS content,
    COALESCE(s.score, 0) AS similarity,
    (semantic_weight * COALESCE(s.score, 0) + keyword_weight * COALESCE(k.score, 0)) AS combined_score
  FROM semantic s
  FULL OUTER JOIN keyword k ON s.id = k.id
  ORDER BY combined_score DESC
  LIMIT match_count;
$$ LANGUAGE sql;
```

### 임베딩 생성 파이프라인
```python
# embedding_service.py
import subprocess, json

def get_embedding(text: str) -> list[float]:
    """Claude CLI로 임베딩 생성 (또는 OpenAI API)"""
    # 옵션 A: OpenAI Embedding API (저렴, 빠름)
    # 옵션 B: sentence-transformers 로컬 모델
    # 옵션 C: Supabase Edge Function에서 처리
    pass

def chunk_text(text: str, max_tokens: int = 500) -> list[str]:
    """텍스트를 토큰 제한 기반으로 청킹"""
    pass

def ingest_document(title, content, source):
    """문서 인제스션: 청킹 → 임베딩 → DB 저장"""
    chunks = chunk_text(content)
    for i, chunk in enumerate(chunks):
        embedding = get_embedding(chunk)
        # Supabase INSERT
    pass
```

---

## Phase 계획

### Phase 1: 인프라 기반 (pgvector + 임베딩)
- Supabase pgvector 활성화 + 테이블/함수 생성
- 임베딩 서비스 구현 (embedding_service.py)
- 청킹 로직 구현
- 기본 검색 API (시맨틱 + 키워드 + 하이브리드)

### Phase 2: Docling 문서 파싱
- Docling 설치 및 통합
- InsuRo ai_parser.py를 Docling 기반으로 교체
- 보험 약관 PDF 파싱 테스트

### Phase 3: InsuRo 통합 + Drive 커넥터
- 금소법 PDF → 벡터 인덱싱 자동화
- 하이브리드 검색 API → InsuRo 프론트 연동
- Drive 변경 감지 + 자동 인제스션

### Phase 4: MCP 서버
- MCP 서버 구현 (3개 Tool)
- Claude Desktop/Cursor 연동 테스트
- 문서화

---

## InsuRo 적용 시나리오

1. **금소법 PDF 업로드** → Docling 파싱 → 청킹 → 임베딩 → pgvector 저장
2. **AI 콘텐츠 작성 시** → "이 문구가 금소법에 위반되나?" → 하이브리드 검색으로 관련 조항 찾기
3. **약관 검색** → "암보험 면책 조항" → 시맨틱 검색으로 의미 기반 결과
4. **Drive 자동 동기화** → 새 약관 PDF 업로드 시 자동 인덱싱
