- Published on
Pinecone 임베딩 차원 변경 무중단 마이그레이션
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
임베딩 모델을 바꾸다 보면(예: text-embedding-3-small에서 text-embedding-3-large로, 또는 자체 모델로 전환) 필연적으로 벡터 차원(dimension) 이 달라집니다. Pinecone은 인덱스 생성 시 차원이 고정되므로, 운영 중인 인덱스의 차원을 “수정”하는 방식은 불가능하고 새 인덱스를 만들고 데이터를 옮기는 마이그레이션이 필요합니다.
문제는 검색 API가 24/7로 호출되는 환경에서 “인덱스 재생성 + 재적재”를 하려면, 잘못하면 검색 품질이 흔들리거나(일부 문서 누락), 쓰기 이벤트가 유실되거나, 트래픽 급증 시 지연이 폭발합니다. 이 글은 무중단(Zero-downtime) 을 목표로 Pinecone 임베딩 차원 변경을 안전하게 수행하는 설계를 다룹니다.
왜 차원 변경이 까다로운가
Pinecone에서 차원 변경이 곧 “새 인덱스”인 이유는 간단합니다.
- 인덱스는
dimension을 기준으로 내부 자료구조와 HNSW/IVF 계열 구성(서비스 옵션에 따라 다름)을 최적화합니다. - 기존 벡터(예: 1536차원)와 신규 벡터(예: 3072차원)는 동일 공간에서 비교 자체가 불가능합니다.
- 따라서 운영 중인 인덱스에 새 차원 벡터를 섞어 넣을 수 없고, 인덱스 단위로 분리해야 합니다.
무중단 관점에서 핵심 리스크는 다음 3가지입니다.
- Write 유실: 마이그레이션 중 들어온 신규/수정 이벤트가 새 인덱스에 반영되지 않음
- Read 불일치: 어떤 요청은 구 인덱스, 어떤 요청은 신 인덱스를 보며 결과 품질이 달라짐
- 재처리 비용 폭증: 전체 재임베딩/업서트가 길어지며 백필(backfill) 작업이 운영 트래픽과 경합
이 리스크를 줄이려면 전형적으로 듀얼 인덱스 + 듀얼 라이트 + 점진적 리드 전환 패턴을 씁니다. 분산 트랜잭션이 어려운 구간이므로, 보상/재처리 관점은 Saga 사고방식이 유용합니다. 관련해서는 MSA Saga 보상 트랜잭션 설계 실전 가이드도 함께 참고하면 좋습니다.
목표 아키텍처: 듀얼 인덱스, 듀얼 라이트, 스위치
구성 요소를 역할 중심으로 나누면 다음과 같습니다.
- Index V1: 기존 차원(예: 1536) 인덱스
- Index V2: 신규 차원(예: 3072) 인덱스
- Embedding Service: 입력 텍스트를 임베딩으로 변환(모델 버전별 분기)
- Write Path: 문서 생성/수정/삭제 이벤트를 Pinecone에 반영
- Read Path: 검색 요청을 받아 쿼리 임베딩 생성 후 Pinecone query 수행
- Migration Controller: 백필 진행률, 듀얼라이트 상태, 리드 전환 비율을 제어
무중단의 핵심은 이 순서입니다.
- V2 인덱스 생성
- 백필 시작: 기존 문서를 V2 차원으로 재임베딩하여 V2에 업서트
- 듀얼 라이트 활성화: 이후 들어오는 변경 이벤트는 V1과 V2에 모두 반영
- 점진적 리드 전환: 일부 트래픽부터 V2로 query를 보내며 품질/지연 관찰
- 완전 전환: 리드를 V2로 100% 전환
- V1 정리: 듀얼 라이트 종료 후 V1 삭제(또는 롤백 대비 일정 기간 유지)
1) 새 Pinecone 인덱스(V2) 생성
인덱스 생성 시 반드시 다음을 명확히 결정해야 합니다.
dimension: 새 임베딩 차원metric:cosine또는dotproduct등(모델 권장에 맞춤)pods/replicas또는 서버리스 설정: 백필 동안의 쓰기량을 감당할 수 있는지namespace전략: 테넌트/환경 분리 방식
예시(개념 코드, SDK 버전별 API는 다를 수 있음):
from pinecone import Pinecone
pc = Pinecone(api_key="PINECONE_API_KEY")
index_name_v2 = "docs-v2"
# dimension은 새 모델에 맞춰 고정
pc.create_index(
name=index_name_v2,
dimension=3072,
metric="cosine",
spec={"serverless": {"cloud": "aws", "region": "us-east-1"}},
)
운영 팁:
- 백필은 대량 upsert를 유발하므로, 인덱스 생성 직후부터 모니터링(지연, 에러율, 제한)을 붙이세요.
- 쿼리 트래픽과 백필 트래픽을 분리할 수 있으면(별도 인덱스/별도 프로젝트/별도 서비스) 안정성이 크게 올라갑니다.
2) 백필 설계: 재임베딩 + 업서트 파이프라인
백필은 “기존 문서 전체를 새 차원으로 다시 임베딩해서 V2에 넣는 작업”입니다. 여기서 중요한 건 속도가 아니라 정확성과 재시도 가능성입니다.
백필 단위와 멱등성
- 기본 단위는
doc_id(또는chunk_id)입니다. - 업서트는 같은
id로 반복해도 최종 상태가 같도록 설계합니다(멱등). - 메타데이터에
embedding_version또는model_id를 넣어, 디버깅과 검증을 쉽게 합니다.
백필 워커 예시(Python)
아래 코드는 개념적으로 “DB에서 문서 배치를 읽고, 새 모델로 임베딩한 뒤, V2에 upsert”를 수행합니다.
import time
from typing import Iterable
BATCH = 100
def embed_v2(texts: list[str]) -> list[list[float]]:
# 새 모델 호출(예: OpenAI, 자체 모델, Bedrock 등)
# 반환 차원은 3072라고 가정
raise NotImplementedError
def fetch_docs(offset: int, limit: int) -> list[dict]:
# DB에서 문서/청크를 가져오는 부분
# 예: [{"id": "c_123", "text": "...", "meta": {...}}, ...]
raise NotImplementedError
def upsert_v2(index, vectors: list[dict], namespace: str):
index.upsert(vectors=vectors, namespace=namespace)
def backfill(index_v2, namespace: str):
offset = 0
while True:
docs = fetch_docs(offset, BATCH)
if not docs:
break
texts = [d["text"] for d in docs]
embs = embed_v2(texts)
vectors = []
for d, e in zip(docs, embs):
vectors.append({
"id": d["id"],
"values": e,
"metadata": {
**d.get("meta", {}),
"embedding_version": "v2",
},
})
# 재시도/백오프는 반드시 넣는 것을 권장
upsert_v2(index_v2, vectors, namespace)
offset += BATCH
time.sleep(0.05)
운영 팁:
- 백필은 작업 중단/재시작이 흔합니다.
offset기반보다 키셋 페이징 또는 “처리 완료 체크포인트 테이블” 방식이 안전합니다. - 임베딩 호출이 병목이면, 워커를 늘리기 전에 레이트리밋과 비용을 먼저 계산하세요.
3) 듀얼 라이트: 변경 이벤트를 V1과 V2에 동시에 반영
백필만으로는 무중단이 아닙니다. 백필이 도는 동안에도 문서는 계속 수정/삭제됩니다. 따라서 변경 이벤트를 동시에 두 인덱스에 반영해야 합니다.
이벤트 기반이 가장 안전
- DB 트리거보다는 애플리케이션 레벨에서 “문서 변경 이벤트”를 발행하고, 컨슈머가 V1/V2에 각각 반영하는 방식이 일반적입니다.
- 이때 분산 트랜잭션을 강제하려고 하기보다, 실패 시 재처리/보상으로 수렴시키는 모델이 현실적입니다. Saga 관점의 설계는 MSA Saga 보상 트랜잭션 설계 실전 가이드와 결이 같습니다.
듀얼 라이트의 일관성 전략
- 최소 보장: “둘 중 하나만 성공해도 이벤트는 ack”는 위험합니다(영구 불일치 가능).
- 권장: “둘 다 성공해야 ack, 실패 시 재시도”
- 재시도 시 멱등 키(예:
event_id)를 로그/테이블로 관리하면 중복 처리를 줄일 수 있습니다.
개념 코드:
def handle_doc_upsert(event, index_v1, index_v2):
doc_id = event["doc_id"]
text = event["text"]
meta = event.get("meta", {})
emb_v1 = embed_v1([text])[0]
emb_v2 = embed_v2([text])[0]
vec_v1 = {"id": doc_id, "values": emb_v1, "metadata": {**meta, "embedding_version": "v1"}}
vec_v2 = {"id": doc_id, "values": emb_v2, "metadata": {**meta, "embedding_version": "v2"}}
# 둘 다 성공해야 커밋
index_v1.upsert(vectors=[vec_v1], namespace=event["ns"])
index_v2.upsert(vectors=[vec_v2], namespace=event["ns"])
def handle_doc_delete(event, index_v1, index_v2):
doc_id = event["doc_id"]
ns = event["ns"]
index_v1.delete(ids=[doc_id], namespace=ns)
index_v2.delete(ids=[doc_id], namespace=ns)
운영 팁:
- 듀얼 라이트는 쓰기량이 2배가 됩니다. 백필과 겹치면 순간 쓰기폭증이 생기니, 워커 동시성/큐 컨슈머 수를 조절하세요.
- 데이터베이스도 함께 압박받을 수 있습니다. 쓰기 폭증 튜닝 관점은 MySQL 8.0 쓰기폭증 - REDO 로그·버퍼풀 튜닝에서 아이디어를 얻을 수 있습니다.
4) 점진적 리드 전환: V2로 천천히 옮기기
이제 V2에 데이터가 충분히 차고, 듀얼 라이트로 최신성도 유지된다면 리드를 전환합니다.
전환 방식 3가지
- 퍼센트 라우팅: 1% → 10% → 50% → 100%로 점진 전환
- 테넌트/사용자 기반 라우팅: 특정 고객군부터 V2로 전환
- 기능 플래그 기반: 내부 운영자/스태프 트래픽부터 전환
리드 전환 시 가장 흔한 함정
- 동일 질의라도 임베딩 모델이 바뀌면 이웃 관계가 달라져 결과가 변합니다. 이는 “오류”가 아니라 “모델 변경 효과”입니다.
- 따라서 전환 전에 오프라인 평가(골든 쿼리셋에 대한 nDCG/Recall 등)와 온라인 A/B를 같이 보세요.
쿼리 경로 예시
def search(query: str, ns: str, use_v2: bool, index_v1, index_v2):
if use_v2:
q = embed_v2([query])[0]
res = index_v2.query(vector=q, top_k=10, namespace=ns, include_metadata=True)
return res
q = embed_v1([query])[0]
res = index_v1.query(vector=q, top_k=10, namespace=ns, include_metadata=True)
return res
운영 팁:
- 전환 중에는 “V1 결과와 V2 결과를 동시에 계산해 로그로만 남기는” 섀도우 쿼리를 일부 트래픽에 적용하면, 품질 차이와 회귀를 빨리 잡을 수 있습니다.
- 다만 섀도우 쿼리는 비용이 2배이므로, 샘플링(예: 0.1%)을 권장합니다.
5) 데이터 검증: 누락과 불일치 잡는 체크리스트
무중단 마이그레이션에서 가장 무서운 건 “조용한 누락”입니다. 다음을 최소로 점검하세요.
카운트/커버리지 검증
- 문서 총량이 V1과 V2에서 동일한지(네임스페이스별)
- 최근 N시간 변경분이 V2에 반영되었는지(이벤트 로그 기반)
샘플링 기반 내용 검증
- 임의의
doc_id를 뽑아 V1/V2 모두 존재하는지 확인 - 메타데이터 필드(권한, 테넌트, 카테고리)가 동일하게 들어갔는지 확인
권한/필터 검증
Pinecone 쿼리에서 메타데이터 필터를 사용한다면, V2에서도 필터가 동일하게 작동하는지 반드시 확인해야 합니다. 필터 누락은 검색 품질 문제가 아니라 보안 사고로 이어질 수 있습니다.
6) 컷오버(완전 전환)와 롤백 전략
컷오버 절차
- 리드 라우팅을 V2로 100% 전환
- 일정 기간(예: 24~72시간) 듀얼 라이트 유지
- 안정화되면 듀얼 라이트 종료(쓰기 경로에서 V1 제거)
- V1 인덱스는 즉시 삭제하지 말고, 롤백 윈도우 동안 보관
롤백
- 리드 라우팅을 다시 V1로 되돌리는 것은 보통 쉽습니다(플래그만 되돌리면 됨).
- 문제는 “V2에만 반영된 쓰기”가 생기지 않도록 듀얼 라이트를 일정 기간 유지하는 것입니다.
- 듀얼 라이트를 종료한 뒤에는 롤백이 사실상 “또 다른 마이그레이션”이 됩니다.
7) 운영에서 자주 터지는 이슈와 대응
백필로 인한 시스템 과부하
- 임베딩 API 레이트리밋
- Pinecone upsert 제한
- 원천 DB 조회 부하
대응:
- 백필 워커 동시성 제한
- 배치 크기 조절
- 지수 백오프 재시도
- 야간/저트래픽 시간대 집중
이벤트 순서 뒤바뀜
예: update 다음에 늦게 도착한 update가 덮어써서 최신이 깨짐
대응:
- 이벤트에
updated_at또는 단조 증가 버전(version)을 포함 - upsert 시 메타데이터에 버전을 넣고, 애플리케이션 레벨에서 최신 버전만 반영
듀얼 라이트 중 일부 실패
대응:
- “둘 다 성공해야 ack” + 재시도 큐
- 최종적으로는 “V2 재동기화 잡”을 별도 운영(최근 1일치 변경분을 다시 스캔)
결론: 무중단의 본질은 ‘재처리 가능한 파이프라인’
Pinecone 임베딩 차원 변경은 단순히 인덱스 하나를 바꾸는 작업이 아니라, 데이터 파이프라인을 이중화하고 점진적으로 전환하는 운영 이벤트입니다. 성공하는 팀의 공통점은 다음 3가지를 먼저 설계합니다.
- 멱등 upsert와 재시도 가능한 백필
- 듀얼 라이트로 최신성 보장
- 점진적 리드 전환과 빠른 롤백
이 3가지만 갖추면, 차원 변경뿐 아니라 모델 교체, 메타데이터 스키마 변경, 네임스페이스 재구성 같은 작업도 같은 패턴으로 확장할 수 있습니다.