# Task-510: pgvector 벡터 검색 인프라 + 임베딩 서비스 + 하이브리드 검색

## 목표
Supabase pgvector 기반 벡터 검색 인프라를 구축하고, 임베딩 생성 + 하이브리드 검색(시맨틱+키워드) API를 구현한다.
InsuRo와 InsuWiki 등 여러 프로젝트에서 공통으로 사용할 수 있는 라이브러리 형태로 만든다.

## 상세 스펙
- 전체 스펙: `/home/jay/workspace/memory/specs/openrag-adoption-spec.md` 참조 (기능 2, 5번)

## 구현 범위

### 1. Supabase DB 마이그레이션 스크립트
- 경로: `/home/jay/workspace/libs/migrations/001_pgvector_setup.sql`
- pgvector 확장 활성화
- `knowledge_documents` 테이블 생성 (id, title, content, source, source_url, metadata JSONB, created_at, updated_at)
- `knowledge_chunks` 테이블 생성 (id, document_id FK, chunk_index, content, embedding VECTOR(1536), token_count, metadata JSONB, created_at)
- IVFFlat 벡터 인덱스
- GIN 전문 검색 인덱스 (한국어)
- `hybrid_search()` SQL 함수 (시맨틱 0.7 + 키워드 0.3 가중치)
- 스펙 문서의 SQL 참조하되, 더 나은 방법이 있으면 개선

### 2. 임베딩 서비스
- 경로: `/home/jay/workspace/libs/embedding_service.py`
- OpenAI Embedding API 사용 (`text-embedding-3-small`, 1536차원)
  - API 키: `.env.keys`의 `OPENAI_API_KEY` (없으면 에러)
  - 요청 제한: 배치 처리 (한번에 최대 100개 청크)
- `get_embedding(text: str) -> list[float]` — 단일 텍스트 임베딩
- `get_embeddings_batch(texts: list[str]) -> list[list[float]]` — 배치 임베딩
- 에러 핸들링: 재시도 3회, rate limit 대응

### 3. 텍스트 청킹
- 경로: `/home/jay/workspace/libs/chunker.py`
- `chunk_text(text: str, max_tokens: int = 500, overlap: int = 50) -> list[dict]`
  - 반환: `[{"content": str, "chunk_index": int, "token_count": int}]`
- 토큰 카운팅: `tiktoken` 사용 (cl100k_base 인코더)
- 청킹 전략: 문단 경계 → 문장 경계 → 토큰 제한 순으로 분할
- 오버랩: 이전 청크 끝부분 overlap 토큰 포함 (문맥 유지)

### 4. 문서 인제스션 파이프라인
- 경로: `/home/jay/workspace/libs/ingest.py`
- `ingest_document(title, content, source, source_url=None, metadata=None) -> str`
  - 텍스트 청킹 → 배치 임베딩 → Supabase INSERT
  - 중복 확인: content 해시로 이미 인덱싱된 문서 스킵
  - 반환: document_id
- `delete_document(document_id) -> bool` — 문서+청크 삭제
- `reindex_document(document_id)` — 재인덱싱

### 5. 검색 API
- 경로: `/home/jay/workspace/libs/search.py`
- `semantic_search(query, limit=10, source_filter=None) -> list[dict]`
- `keyword_search(query, limit=10, source_filter=None) -> list[dict]`
- `hybrid_search(query, limit=10, semantic_weight=0.7, keyword_weight=0.3, source_filter=None) -> list[dict]`
- 모든 검색: 쿼리 임베딩 생성 → Supabase RPC 호출 → 결과 반환
- source_filter: 'insuwiki', 'insuro_fcpa' 등으로 소스별 필터링

### 6. FastAPI 검색 엔드포인트
- 경로: `/home/jay/projects/InsuRo/server/main.py`에 추가
- `POST /api/insuro/search` — 하이브리드 검색 (JWT 인증 필수)
  - body: `{"query": str, "source": str|null, "limit": int}`
  - response: `{"results": [{"content": str, "similarity": float, "source": str}]}`

## Supabase 연결 정보
- InsuRo Supabase URL: 환경변수 `INSURO_SUPABASE_URL` 또는 `INSURO_NEW_SUPABASE_URL`
- Service Role Key: 환경변수 `INSURO_SUPABASE_SERVICE_ROLE_KEY`
- 없으면 `.env.keys`에서 확인하고 보고서에 "어떤 환경변수가 필요한지" 명시

## 테스트
- 각 모듈별 단위 테스트 필수
- 마이그레이션 SQL 문법 검증
- 임베딩 서비스 mock 테스트
- 청킹 결과 검증 (토큰 제한 준수, 오버랩 정확성)
- 검색 결과 정렬 순서 검증

## 주의사항
- `/home/jay/workspace/libs/` 디렉토리에 공통 라이브러리로 작성 (프로젝트 독립적)
- API 키를 코드에 하드코딩 금지. 환경변수에서만 로드
- Supabase 클라이언트: `supabase-py` 패키지 사용
- tiktoken, openai 패키지 필요시 설치
- `npx pyright` 타입 체크 통과 필수
