무중단 임베딩 마이그레이션: 프로덕션에서 text-embedding-004에서 text-embedding-3-large로 전환

발행: (2026년 2월 20일 오후 08:30 GMT+9)
5 분 소요
원문: Dev.to

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에서 저를 찾아보세요.

0 조회
Back to Blog

관련 글

더 보기 »

따뜻한 소개

소개 여러분, 안녕하세요! 여기서 진행되는 deep tech 토론에 매료되었습니다. 커뮤니티가 번창하는 모습을 보는 것은 정말 놀랍습니다. 프로젝트 개요 저는 열정적입니다...