Vector DB 마이그레이션

Pinecone에서 FAISS·Chroma로

LHG 2026-06-04

CONTENTS

  1. 핵심 용어와 이전 이유
  2. FAISS와 Chroma 포지셔닝
  3. 마이그레이션 절차와 검증
  4. 운영 단계의 트레이드오프
  5. 선택 가이드와 결론

이 글에서 쓰는 용어

처음 보는 사람도 이해되게 먼저 풀어둔다

  • ANN(근사 최근접 이웃): 정확도를 조금 포기하고 검색 속도를 크게 높이는 방식
  • HNSW: 그래프 구조로 벡터를 연결해 빠르게 탐색하는 인덱스 알고리즘
  • cosine·L2: 두 벡터가 얼마나 비슷한지 재는 방법. 미리 맞춰두지 않으면 결과가 틀어진다
  • recall@k: 상위 k개 결과 중 정답과 겹치는 비율. 이전 후 정확도를 재는 기준
  • RAG: 문서를 검색해서 그 내용을 참고해 답변을 만드는 방식

1. 왜 Pinecone을 떠나는가

편한 관리형 서비스에서 직접 운영하는 오픈소스로 옮기는 이유는 네 가지다

비용과 통제권, 두 가지가 발단이다

벡터가 천만 건을 넘어가면서 보이던 것이 달라진다

비용

  • 저장량과 읽기·쓰기 빈도에 비례해 청구
  • 벡터가 늘수록 곡선이 선형 이상으로 꺾인다

데이터 주권

  • 임베딩을 외부 SaaS에 영구 저장해도 되는지 보안 검토에서 나온다
  • 규제 환경일수록 인덱스를 직접 통제하고 싶어한다

세밀한 제어

  • efSearch, IVF-PQ 압축 같은 저수준 튜닝이 막혀 있다
  • 정확도 한계에 부딪힌 팀에는 답답한 벽이 된다

벤더 락인 회피

  • 임베딩 모델이 바뀔 때마다 인덱스를 다시 만든다
  • 인프라가 외부 종속이면 모델 교체가 느려진다

2. FAISS와 Chroma 포지셔닝

같은 층위의 도구가 아니다. 하나는 라이브러리, 하나는 데이터베이스다

라이브러리 대 데이터베이스

이 차이가 선택을 가른다

FAISS

  • Meta AI가 만든 ANN 검색 라이브러리
  • 인덱스를 메모리에 만들고 직접 검색하는 API 묶음
  • 문자열 ID와 메타데이터를 모른다

Chroma

  • 임베딩 워크로드에 특화된 데이터베이스
  • 컬렉션, 메타데이터 필터, 영속화를 기본 제공
  • 내부적으로 HNSWlib과 SQLite를 쓴다

선택 기준은 단순하게 잡는다

운영 인력 부족, 메타데이터 필터 자주 필요 → Chroma
인덱스를 직접 튜닝, 메모리·속도 극한까지 → FAISS
  • 둘 다 단일 노드 중심 도구다
  • 수억 벡터를 넘어가면 Milvus, Qdrant, Weaviate 같은 분산 솔루션이 다음 후보다

3. 마이그레이션 절차 4단계

가장 먼저 할 일은 지금 인덱스의 정확한 모양을 아는 것이다

절차 흐름

1단계
현재 인덱스 파악
→
2단계
벡터 추출
→
3단계
목표 DB 적재
→
4단계
검증
  • 차원 수, 거리 측정법, 메타데이터 스키마를 먼저 맞춰야 한다
  • cosine을 쓰던 인덱스를 L2 거리로 옮기면 결과가 완전히 달라진다

1단계, 지금 인덱스 파악하기

from pinecone import Pinecone

pc = Pinecone(api_key=PINECONE_API_KEY)
index = pc.Index("rag-prod")

stats = index.describe_index_stats()
print(stats["dimension"], stats["total_vector_count"])
for ns, info in stats["namespaces"].items():
    print(ns, info["vector_count"])

여기서 나온 dimension이 새 인덱스의 차원이 된다.

2단계, 벡터를 추출할 때 지켜야 할 것

천만 벡터를 옮기다 보면 네트워크 단절은 반드시 일어난다

  • Pinecone은 전체 벡터를 한 번에 내려받는 API가 없다. ID를 먼저 확보해야 fetch가 가능하다
  • 애플리케이션 DB에 원본 ID가 있으면 그 목록으로, 없으면 목록 조회 API로 페이지네이션한다
  • 추출은 반드시 멱등하게 짠다. 배치 단위로 파일에 적고 이미 적힌 배치는 건너뛴다

3단계, FAISS로 적재하기

import faiss, numpy as np

D = 1536
index = faiss.IndexFlatIP(D)  # cosine을 흉내내려면 정규화 + 내적

vecs = np.asarray(vecs, dtype="float32")
faiss.normalize_L2(vecs)

index = faiss.IndexIDMap2(index)  # 문자열 ID를 모르므로 정수로 매핑
index.add_with_ids(vecs, np.asarray([hash_id(i) for i in ids], dtype="int64"))
faiss.write_index(index, "rag-prod.faiss")

FAISS 적재에서 항상 걸리는 두 가지

문자열 ID를 모른다

  • IndexIDMap2로 정수 ID를 매핑한다
  • 원래 문자열 ID는 별도 파일에 보관한다

메타데이터를 모른다

  • 필터링이 필요하면 결과 ID를 받아 후처리한다
  • 또는 메타별로 인덱스를 미리 쪼갠다

3단계, Chroma로 적재하기

import chromadb

client = chromadb.PersistentClient(path="./chroma-rag-prod")
col = client.get_or_create_collection(
    name="rag-prod",
    metadata={"hnsw:space": "cosine"},  # 거리 측정법을 반드시 명시
)

for ids, vecs, metas, docs in iter_parquet_batches("export/rag-prod"):
    col.add(ids=ids, embeddings=vecs, metadatas=metas, documents=docs)

hnsw:space를 안 쓰면 기본값 L2가 적용돼 결과가 통째로 어긋난다.

4. 검증, recall@k 측정

옮기고 나면 반드시 옛 인덱스와 새 인덱스의 결과를 겹쳐본다

같은 질문을 두 인덱스에 동시에 던진다

FAISS Flat recall@10 ≈ 1.0 · HNSW·IVF는 0.92~0.97
  • 500~1000개 정도의 질문 세트면 충분하다
  • top-k가 겹치는 비율(recall@k)과 순위 상관계수를 함께 본다
  • 이 수치가 실제 답변 품질에 어떻게 반영되는지 별도 평가셋으로 한 번 더 확인한다

recall@k 계산

def recall_at_k(pinecone_ids, new_ids, k=10):
    p = set(pinecone_ids[:k])
    n = set(new_ids[:k])
    return len(p & n) / k

scores = []
for q in load_query_samples(1000):
    pc_top = [m.id for m in index.query(vector=q, top_k=10).matches]
    new_top = chroma_query(col, q, k=10)
    scores.append(recall_at_k(pc_top, new_top))

print(f"mean recall@10 = {sum(scores)/len(scores):.3f}")

5. 운영 단계의 트레이드오프

이전이 끝나도 일은 시작도 안 한 셈이다

메모리와 동시성, 새로 떠안는 문제

FAISS, 메모리

  • 1536차원 벡터 천만 개는 약 60GB
  • 압축하면 줄지만 recall도 같이 떨어진다

Chroma, 동시성

  • SQLite 기반이라 여러 프로세스 동시 쓰기에 약하다
  • 트래픽이 늘면 서버 모드로 분리해야 한다

재인덱싱은 무중단 듀얼 인덱스로

임베딩 모델을 바꾸면 인덱스를 통째로 다시 만들어야 한다

신규 데이터
v1·v2에 동시 적재
→
읽기
v1에서 v2로 점진 이동
  • Pinecone이 자동으로 처리해주던 부분을 직접 만들어야 한다
  • 백업, 복제도 같은 이유로 운영 코드가 늘어난다

최소한 봐야 할 관측 지표 4가지

  • 검색 응답 시간(query latency), p99 100ms 이내를 기준으로
  • recall@k, 기준 인덱스 대비 0.95 이상 유지
  • 인덱스 크기, RAM의 70퍼센트 이하로 관리
  • 쓰기 큐 적체, 1분 이내 소진되는지 확인

6. 선택 가이드와 결론

비용만 보고 옮기면 운영 비용이 절감액을 넘는 함정에 빠진다

규모별 선택 가이드

  • 수백만 벡터 이하, 단일 노드로 충분: Chroma가 가장 빠른 이전 경로다
  • 수천만 벡터, 검색 속도가 핵심: FAISS와 메타데이터 사이드카가 합리적이다
  • 수억 벡터, 멀티 테넌시, 고가용성: Milvus·Qdrant·Weaviate 같은 분산 솔루션을 검토한다
  • POC 단계에서 recall, latency, 운영 인력 비용을 모두 재고 결정한다

정리

  • 비용 절감만 보고 옮기면 운영 비용이 절감액을 넘는 함정에 빠지기 쉽다
  • 이전 전에는 recall과 latency를 반드시 측정하고 이전 후에는 관측 지표를 바로 세운다