무중단 임베딩 마이그레이션: 프로덕션에서 text-embedding-004에서 text-embedding-3-large로 전환
Source: Dev.to
죄송합니다만, 현재 외부 웹사이트의 내용을 직접 가져올 수 없습니다. 번역을 원하시는 본문을 여기 채팅에 복사해 주시면, 요청하신 대로 한국어로 번역해 드리겠습니다.
상황
서비스: PostgreSQL의 pgvector를 이용한 RAG 검색 서비스
이전 모델: text-embedding-004 (폐기 예정)
새 모델: text-embedding-3-large (768 차원)
데이터 양: 수천 개의 임베디드 문서
제약 조건: 다운타임 제로, 데이터 손실 제로, 프로덕션 트래픽 지속 유지
단계 1: 모델을 구성 가능하게 만들기
다른 어떤 작업보다 먼저, 모델 이름을 하드코딩하는 것을 중단하세요:
# Before (hardcoded in 6 places)
response = openai.embeddings.create(
model="text-embedding-004",
input=text,
)
# After (configured once)
EMBED_MODEL = os.getenv("EMBED_MODEL", "text-embedding-3-large")
EMBED_DIMENSIONS = int(os.getenv("EMBED_DIMENSIONS", "768"))
response = openai.embeddings.create(
model=EMBED_MODEL,
input=text,
dimensions=EMBED_DIMENSIONS,
)
두 개의 환경 변수가 2일 이내 마이그레이션과 2주가 걸리는 마이그레이션 사이의 차이를 만들습니다.
단계 2: 새 열 추가 (대체하지 않음)
-- Migration: add new embedding column alongside the old one
ALTER TABLE documents
ADD COLUMN embedding_v2 vector(768);
CREATE INDEX CONCURRENTLY idx_documents_embedding_v2
ON documents USING ivfflat (embedding_v2 vector_cosine_ops)
WITH (lists = 100);
CONCURRENTLY를 사용하면 테이블을 잠그지 않고 인덱스를 생성하므로, 프로덕션 읽기가 중단되지 않고 계속됩니다.
Step 3: Batch Re‑embedding Script
import asyncio
from tqdm import tqdm
async def re_embed_batch(session, documents, batch_size=50):
"""Re‑embed documents in batches with progress tracking."""
for i in tqdm(range(0, len(documents), batch_size)):
batch = documents[i:i + batch_size]
texts = [doc.content for doc in batch]
# Batch embedding call
response = await openai.embeddings.create(
model=EMBED_MODEL,
input=texts,
dimensions=EMBED_DIMENSIONS,
)
for doc, embedding in zip(batch, response.data):
doc.embedding_v2 = embedding.embedding
await session.commit()
# Rate limiting
await asyncio.sleep(0.5)
주요 기능
- 배치 처리 – 문서를 하나씩 임베딩하지 않습니다.
- 진행 바 – 얼마나 걸리는지 확인할 수 있어야 합니다.
- 속도 제한 – 임베딩 API에는 제한이 있습니다.
- 배치당 커밋 – 10 K 문서에 대해 트랜잭션을 오래 유지하지 않습니다.
단계 4: 드라이‑런 검증
프로덕션 트래픽을 전환하기 전에:
async def validate_migration(session, sample_size=100):
"""Compare search results between old and new embeddings."""
test_queries = get_random_queries(sample_size)
overlaps = []
for query in test_queries:
old_results = await search(session, query, column="embedding")
new_results = await search(session, query, column="embedding_v2")
# Check overlap
old_ids = {r.id for r in old_results[:10]}
new_ids = {r.id for r in new_results[:10]}
overlap = len(old_ids & new_ids) / len(old_ids)
overlaps.append(overlap)
if overlap :query_vec) AS similarity
FROM documents
ORDER BY {column} :query_vec
LIMIT :top_k
"""), {"query_vec": str(query_embedding), "top_k": top_k})
return results.fetchall()
USE_V2_EMBEDDINGS=false 로 배포합니다. 모든 것이 정상적으로 작동하는지 확인하세요. true 로 전환합니다. 문제가 발생하면 즉시 다시 되돌립니다.
6단계: 정리
v2를 일주일 동안 문제 없이 실행한 후:
ALTER TABLE documents DROP COLUMN embedding;
ALTER TABLE documents RENAME COLUMN embedding_v2 TO embedding;
DROP INDEX idx_documents_embedding;
ALTER INDEX idx_documents_embedding_v2 RENAME TO idx_documents_embedding;
배운 교훈
- 항상 임베딩 제공자를 추상화하세요. 두 개의 환경 변수가 다중 파일 리팩터링을 방지했습니다.
- 저장된 벡터에 모델 버전 추적을 추가하세요. 우리는 하지 않았고, 그래야 했습니다.
- 필요하기 전에 마이그레이션 도구를 구축하세요. 배치 스크립트와 검증 도구는 재사용 가능합니다.
- 옆에 나란히 컬럼 > 제자리 교체. 롤백 이야기가 즉시 이루어집니다.
- 모든 것을 드라이런하세요. 우리의 검증으로 겹침이 낮은 세 개의 쿼리를 발견해 조사했습니다.
총 영향: 48 시간, 다운타임 제로, 데이터 손실 제로.
전체 마이그레이션 이야기는 my blog에서 읽어보세요. “Production GCP Patterns” 시리즈의 일부 — humzakt.github.io에서 저를 찾아보세요.