
검색 결과 품질이 갑자기 이상해졌다는 리포트를 받고 로그를 뒤져보면, 최근에 임베딩 모델을 바꾸면서 기존 벡터를 지우지 않고 새 모델 결과와 그대로 섞어 쓴 게 원인인 경우가 꽤 많습니다. 서로 다른 모델이 만든 벡터는 좌표계 자체가 다르기 때문에 같은 인덱스 안에서 유사도를 비교하면 결과가 통째로 무너집니다. 그렇다고 수백만 건을 한 번에 재색인하려면 시간과 API 비용이 만만치 않아서, 이 글에서는 컬렉션을 처음부터 다시 쌓지 않고도 모델을 교체하는 실전 전략을 정리했습니다.
임베딩 모델을 바꾸면 기존 벡터가 못 쓰게 되는 이유
가장 먼저 부딪히는 문제는 차원 수 자체가 다르다는 점입니다. 예를 들어 OpenAI의 text-embedding-ada-002는 1536차원을 출력하지만, text-embedding-3-large는 기본값이 3072차원입니다. 차원이 다르면 코사인 유사도 계산 자체가 성립하지 않으니, 이 경우엔 섞어 쓰는 것 자체가 불가능합니다.
문제는 우연히 차원이 같아도 벌어집니다. 두 모델이 똑같이 1536차원을 뽑아내더라도, 그 좌표 공간은 각 모델이 학습한 방식에 따라 완전히 다르게 배치되어 있습니다. A 모델이 “가깝다”고 판단한 두 문장이 B 모델 공간에서는 전혀 다른 위치에 놓일 수 있다는 뜻입니다. HNSW나 IVF 같은 인덱스 구조는 특정 모델의 벡터 분포를 전제로 그래프나 클러스터를 구성하기 때문에, 이질적인 벡터가 섞여 들어가면 recall이 조용히 떨어집니다. 에러가 나지 않고 그냥 결과가 미묘하게 나빠지는 게 더 무섭습니다.
전체 재색인 없이 넘어가는 네 가지 방법
실무에서 쓰는 접근은 대체로 아래 네 가지로 정리됩니다. 규모와 트래픽 패턴에 따라 맞는 방식이 다릅니다.
| 전략 | 방식 | 적합한 상황 |
|---|---|---|
| 듀얼 컬렉션(블루-그린) | 새 모델로 백필 후 별칭만 전환 | 다운타임을 아예 없애고 싶을 때 |
| 점진적 재임베딩 | 조회·수정 시점에 그 문서만 재계산 | 데이터가 너무 많아 일괄 처리가 부담될 때 |
| 버전 태깅 + 필터 검색 | 벡터마다 모델 버전을 메타데이터로 저장 | 신구 벡터를 당분간 공존시켜야 할 때 |
| 하이브리드 검색 병행 | 마이그레이션 기간 동안 BM25 등 키워드 검색을 함께 사용 | 벡터 품질 저하를 사용자가 체감하면 안 될 때 |

네 방식 모두 결국 “재임베딩 자체는 피할 수 없다”는 공통점이 있습니다. 다만 언제, 얼마나 한꺼번에 하느냐를 조절해서 서비스 중단이나 비용 폭탄을 피하는 게 핵심입니다.
듀얼 컬렉션으로 무중단 전환하기
Qdrant나 Pinecone처럼 컬렉션(또는 네임스페이스) 단위로 별칭을 걸 수 있는 벡터 DB라면, 새 컬렉션을 하나 더 만들어 백필을 끝낸 뒤 트래픽만 옮기는 방식이 가장 깔끔합니다. 아래는 흐름을 보여주는 예시 코드입니다. 실제 파라미터명은 사용 중인 클라이언트 버전 공식 문서로 꼭 확인하세요.
from openai import OpenAI
from qdrant_client import QdrantClient
from qdrant_client.models import PointStruct
client = OpenAI()
qdrant = QdrantClient(url="http://localhost:6333")
def reembed_batch(rows):
texts = [r["content"] for r in rows]
resp = client.embeddings.create(
model="text-embedding-3-large",
input=texts,
dimensions=1536, # 기존 컬렉션과 차원을 맞추고 싶을 때만 지정
)
points = [
PointStruct(id=r["id"], vector=e.embedding, payload={"embedding_ver": "v2"})
for r, e in zip(rows, resp.data)
]
qdrant.upsert(collection_name="docs_v2", points=points)
# 백필이 끝난 뒤 별칭만 새 컬렉션으로 돌립니다
qdrant.update_collection_aliases(change_aliases_operations=[
{"delete_alias": {"alias_name": "docs_live"}},
{"create_alias": {"collection_name": "docs_v2", "alias_name": "docs_live"}},
])
pgvector를 쓰는 구조라면 컬럼 자체가 고정 차원이라 기존 컬럼에 새 모델 벡터를 바로 넣을 수 없습니다. 컬럼을 하나 추가하고 배치로 채운 뒤, 검증이 끝나면 인덱스를 새로 걸고 이전 컬럼을 정리하는 순서가 안전합니다.
ALTER TABLE documents ADD COLUMN embedding_v2 vector(3072);
-- 배치 작업으로 새 임베딩 값을 채웁니다
UPDATE documents SET embedding_v2 = %s WHERE id = %s;
-- 검증 후 새 컬럼 기준으로 인덱스 생성
CREATE INDEX ON documents USING hnsw (embedding_v2 vector_cosine_ops);
점진적 재임베딩은 언제 선택해야 할까요?

문서 수가 너무 많아서 한 번에 재계산하면 임베딩 API 요금이 부담스러운 경우, 조회되거나 수정되는 문서만 그때그때 새 모델로 다시 계산하는 방식을 씁니다. 사용자가 자주 찾는 인기 문서부터 자연스럽게 새 벡터로 교체되고, 거의 안 보는 롱테일 문서는 뒤로 밀리는 구조입니다.
이 방식을 쓰려면 검색 시점에 신구 벡터가 섞이지 않도록 반드시 버전 태그로 필터링해야 합니다. 벡터마다 embedding_ver 같은 메타데이터를 붙여두고, 쿼리도 같은 버전끼리만 비교하도록 필터를 걸어야 앞서 말한 좌표계 불일치 문제를 피할 수 있습니다.
이 방법이 안 통하는 경우 — 놓치기 쉬운 트레이드오프
점진적 재임베딩은 편하지만 함정이 하나 있습니다. 트래픽이 거의 없는 문서는 영원히 구버전 벡터에 머물 수 있습니다. 몇 달, 몇 년이 지나도 재계산될 기회가 없다면 결국 별도의 정리 배치 작업을 한 번은 돌려야 하는 순간이 옵니다.
듀얼 컬렉션 방식도 공짜는 아닙니다. 백필이 끝날 때까지 스토리지를 두 배로 쓰고, 문서 수가 많으면 재임베딩 API 비용과 처리 시간이 그대로 청구서에 찍힙니다. 또한 코사인에서 내적 방식으로 거리 척도(distance metric) 자체가 바뀌는 모델 교체라면, 벡터 값뿐 아니라 인덱스 설정도 함께 바꿔야 해서 단순 별칭 전환만으로는 끝나지 않습니다. 검색 품질 저하가 사용자에게 바로 보이는 서비스라면, 마이그레이션 기간만이라도 BM25 같은 키워드 검색을 병행해 안전망을 두는 편이 안전합니다.
지금 쓰는 벡터 DB에서 별칭·필터 기능부터 확인해 보세요
정리하면 임베딩 모델을 바꾸면 기존 벡터는 그대로 재사용할 수 없고, 결국 재임베딩은 피할 수 없는 과정입니다. 다만 전체를 한 번에 재색인할지, 듀얼 컬렉션으로 무중단 전환할지, 아니면 점진적으로 교체할지는 데이터 규모와 트래픽 패턴에 맞춰 고를 수 있습니다. 벡터의 정확한 수학적 정의와 좌표 공간 개념이 헷갈린다면 벡터 공간 문서를 먼저 보고, 차원 축소나 모델별 스펙은 OpenAI 임베딩 공식 가이드에서 최신 값을 직접 확인하는 걸 추천합니다.
바로 실행할 수 있는 다음 단계는 이렇습니다. 먼저 지금 쓰는 벡터 DB가 컬렉션 별칭이나 네임스페이스 전환을 지원하는지 문서에서 확인하고, 안 되면 메타데이터 필터로 신구 버전을 구분하는 구조부터 설계해 보세요. 그 다음 트래픽 상위 문서군을 골라 소규모로 듀얼 컬렉션 전환을 테스트해 보면, 전체 재색인 없이도 안전하게 모델을 옮길 수 있는지 감이 잡힙니다.
