# Task-510b: pgvector Python 모듈 구현 (이어서)

## 목표
이전 세션에서 SQL 마이그레이션 + 테스트 파일이 작성됨. 남은 Python 구현체를 완성한다.

## 이미 완료된 것 (건드리지 말 것)
- ✅ `/home/jay/workspace/libs/migrations/001_pgvector_setup.sql` — 완성
- ✅ `/home/jay/workspace/libs/tests/test_chunker.py` — 10개 테스트
- ✅ `/home/jay/workspace/libs/tests/test_embedding_service.py` — 12개 테스트

## 남은 구현

### 1. chunker.py (필수)
- 경로: `/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 인코더)
- 청킹 전략: 문단 경계(`\n\n`) → 문장 경계(`. `) → 토큰 제한 순으로 분할
- 오버랩: 이전 청크 끝부분 overlap 토큰 포함 (문맥 유지)
- **테스트 통과 필수**: `cd /home/jay/workspace/libs && python -m pytest tests/test_chunker.py -v`

### 2. embedding_service.py (필수)
- 경로: `/home/jay/workspace/libs/embedding_service.py`
- OpenAI Embedding API 사용 (`text-embedding-3-small`, 1536차원)
- 필요 import: `import openai`, `import time`
- API 키: `os.environ["OPENAI_API_KEY"]` (없으면 ValueError)
- `get_embedding(text: str) -> list[float]` — 단일 텍스트 임베딩
- `get_embeddings_batch(texts: list[str]) -> list[list[float]]` — 배치 임베딩 (100개씩)
- 에러 핸들링: 재시도 3회 (exponential backoff: 1, 2, 4초)
- rate limit: `openai.RateLimitError` 발생 시 동일 backoff 로직
- **테스트 통과 필수**: `cd /home/jay/workspace/libs && python -m pytest tests/test_embedding_service.py -v`

### 3. ingest.py (필수)
- 경로: `/home/jay/workspace/libs/ingest.py`
- `ingest_document(title, content, source, source_url=None, metadata=None) -> str`
  - 텍스트 청킹(chunker) → 배치 임베딩(embedding_service) → Supabase INSERT
  - 중복 확인: SHA-256 content_hash로 이미 인덱싱된 문서 스킵
  - 반환: document_id (UUID 문자열)
- `delete_document(document_id) -> bool` — 문서+청크 삭제
- `reindex_document(document_id)` — 기존 청크 삭제 후 재인덱싱
- Supabase 연결: 환경변수 `INSURO_NEW_SUPABASE_URL` 또는 `INSURO_SUPABASE_URL`, `INSURO_NEW_SERVICE_ROLE_KEY`
- 단위 테스트 작성: `/home/jay/workspace/libs/tests/test_ingest.py`

### 4. search.py (필수)
- 경로: `/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]`
- 모든 검색: 쿼리 임베딩 생성(embedding_service) → Supabase RPC 호출(`hybrid_search` SQL 함수) → 결과 반환
- source_filter: 'insuwiki', 'insuro_fcpa' 등으로 소스별 필터링
- Supabase 연결: ingest.py와 동일한 환경변수
- 단위 테스트 작성: `/home/jay/workspace/libs/tests/test_search.py`

### 5. __init__.py
- 경로: `/home/jay/workspace/libs/__init__.py`
- 주요 모듈의 public 함수를 re-export

### 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, "title": str}]}`
- sys.path에 `/home/jay/workspace/libs` 추가 필요

## 의존성 설치
```bash
pip install tiktoken openai supabase
```
(이미 설치되어 있으면 스킵)

## QC
- 모든 테스트 통과: `cd /home/jay/workspace/libs && python -m pytest tests/ -v`
- pyright 타입 체크: `cd /home/jay/workspace/libs && npx pyright *.py`

## 주의사항
- API 키를 코드에 하드코딩 금지
- Supabase 클라이언트: `supabase-py` 패키지 사용
- 이미 있는 SQL, 테스트 파일 수정 금지
- 테스트가 이미 정의한 인터페이스를 정확히 따를 것
