포스트

Pinecone·Weaviate·Qdrant·Chroma 벡터DB 선택 가이드: “성능”은 결국 **메모리/필터/운영모델** 싸움이다

벡터DB가 해결하는 문제는 단순합니다. 고차원 embedding(보통 768~1536-dim) 을 빠르게 kNN/ANN으로 찾고(semantic search), 여기에 필터(tenant, 권한, 카테고리, 기간) 를 얹어 RAG/추천/검색을 제품으로 만들게 해줍니다.

Pinecone·Weaviate·Qdrant·Chroma 벡터DB 선택 가이드: “성능”은 결국 **메모리/필터/운영모델** 싸움이다

들어가며

벡터DB가 해결하는 문제는 단순합니다. 고차원 embedding(보통 768~1536-dim) 을 빠르게 kNN/ANN으로 찾고(semantic search), 여기에 필터(tenant, 권한, 카테고리, 기간) 를 얹어 RAG/추천/검색을 제품으로 만들게 해줍니다.

하지만 “언제 쓰면 좋은가/안 좋은가”는 꽤 명확합니다.

  • 쓰면 좋은 경우
    • 문서/상품/이벤트가 수백만~수천만 벡터로 커지고, 질의가 kNN + 필터 + (가능하면) hybrid(BM25+sparse+dense) 로 복잡해질 때
    • 운영 중 쓰기(업서트/삭제)와 읽기(QPS)가 동시에 발생하고 tail latency가 중요한 서비스일 때
  • 쓰면 안 좋은 경우
    • 데이터가 작고(예: <100만) 업데이트가 드물며, 결국은 단순 top-k만 필요해서 Postgres+pgvector/Redis/Mongo 같은 “기존 DB 내 벡터”로 충분할 때(동기화 파이프라인 비용이 더 큼)
    • “나중에 성능 튜닝하자”라고 생각했는데, 실제로는 필터 선택도/세그먼트 구조/메모리 상주 여부가 성능을 좌우해 뒤늦게 바꾸기 어려운 경우(처음부터 선택 기준이 필요)

이 글은 “기능 나열”이 아니라, 2026년 8월 시점에서 Pinecone(서버리스 중심), Weaviate(압축/하이브리드/디스크 인덱스), Qdrant(필터/온디스크/세그먼트), Chroma(단일노드 성격)프로젝트 적용 관점으로 비교합니다.
(각 제품의 최신 문서/아키텍처/운영 특성은 인용한 공식 문서 기준입니다.1)


🔧 핵심 개념

1) “성능”을 결정하는 4가지 축

벡터DB 벤치마크가 프로젝트마다 결과가 갈리는 이유는 보통 아래 4축의 조합 때문입니다.

  1. Index 구조(HNSW vs cluster/IVF 계열 vs flat)
    • HNSW는 대체로 빠르지만 메모리 비용이 크고, 필터 선택도에 따라 효율이 크게 흔들립니다.
  2. 메모리 전략(전부 RAM vs 벡터/인덱스 온디스크 vs 캐시)
    • RAM 상주가 빠르지만 비용 폭탄. 온디스크는 NVMe 없으면 tail latency가 튈 수 있습니다(Qdrant도 “둘 다 디스크는 최후”에 가깝게 가이드).2
  3. 필터 처리 방식(필터 먼저? 후보 뽑고 필터?)
    • 실무 RAG는 “의미 유사도”만으로 끝나지 않고 권한/tenant/기간/카테고리를 거의 항상 겁니다.
    • Qdrant는 payload index를 통해 필터 성능을 강하게 밀고, “strict mode”로 비효율 패턴을 막는 방향까지 갑니다.2
  4. 운영 모델(완전관리형/서버리스 vs 자가호스팅/클러스터링)
    • Pinecone은 “서버리스 + 객체스토리지 기반”으로 아키텍처를 재편했고, pod는 레거시로 명시합니다.1

2) Pinecone: “LSM + object storage + stateless executors”의 서버리스 모델

Pinecone 최신 방향성은 명확합니다: pods는 레거시, 신규는 serverless.1
서버리스의 핵심은:

  • 벡터를 object storage의 immutable slab에 저장
  • stateless query executors가 slab을 캐시해 병렬 처리
  • 쓰기는 WAL/memtable을 거쳐 비동기로 slab 구조로 정리(LSM 트리 계열 스토리지 언급)1

이 구조의 실무적 의미:

  • 트래픽이 들쭉날쭉한 서비스에서 오버프로비저닝을 줄이는 방향(과금도 usage-based 성격이 강함)3
  • 대신 “내가 HNSW 파라미터를 극단적으로 튜닝해서 하드웨어를 쥐어짜겠다” 같은 타입의 최적화 여지는 제한적일 수 있습니다(관리형의 전형적 트레이드오프).

3) Weaviate: hybrid(BM25+dense) + 압축(RQ/PQ/SQ/BQ) + 디스크형(HFresh)로 “메모리 비용”을 정면돌파

Weaviate의 강점은 “검색 품질/비용”을 시스템 내에서 조합할 수 있다는 점입니다.

  • Hybrid search: BM25 + vector를 병렬 수행 후 fusion(기본은 relativeScoreFusion)4
  • Vector compression: RQ/PQ/SQ/BQ를 HNSW나 flat에 적용하고, 결과 후보를 uncompressed vector로 rescoring하여 recall을 보정5
  • 특히 RQ(8-bit)v1.32에 HNSW용으로 들어갔고, 4x 압축 + 내부 테스트 98~99% recall을 주장합니다.6
  • 더 나아가 v1.36HFresh는 “centroid만 메모리에 두고(압축 RQ), 나머지는 디스크”로 메모리 효율을 크게 가져가는 방향입니다.7

즉 Weaviate는 “HNSW RAM 폭탄”을 압축 + 디스크형 인덱스로 완화할 옵션이 비교적 풍부합니다.

4) Qdrant: 세그먼트 기반 + payload index + memmap/on_disk로 “필터 성능/운영 현실성”을 밀어붙임

Qdrant 문서를 보면 성능 철학이 드러납니다.

  • 데이터는 segment 단위로 저장되고, 각 세그먼트가 벡터/페이로드/인덱스를 따로 가짐8
  • payload index를 만들지 않으면 필터가 느려질 수 있고, 클라우드에서는 아예 비인덱스 필터를 제한하는 쪽(엄격 모드/제약)2
  • 메모리 전략은 현실적입니다: 벡터/인덱스를 디스크로 내릴 수 있지만, HNSW graph traversal이 IO를 유발할 수 있어 “둘 다 디스크는 RAM이 정말 부족할 때”라고 못 박습니다.2

실무에서는 Qdrant가 “필터를 진짜로 많이 거는 멀티테넌트/권한 기반 RAG”에서 만족도가 높은 편인데, 그 이유가 payload index/strict mode 같은 안전장치와 잘 맞습니다.

5) Chroma: “프로덕션도 가능하지만, 기본 성격은 단일 노드/내장형에 가깝다”

Chroma는 팀/환경에 따라 훌륭한 선택이 될 수 있지만, 2026년 기준으로도 문서가 강조하는 건 Single-Node 성능/제약sqlite 기반 메타데이터 저장의 용량 변동성입니다.9
즉, “분산/고QPS/엄격한 tail latency”가 우선이면 Pinecone/Weaviate/Qdrant 쪽이 자연스럽고, Chroma는:

  • 앱에 가볍게 내장하거나
  • 로컬/단일 노드로 빠르게 운영하거나
  • 파이프라인 검증 후 상위 DB로 넘어가는 전략 에 더 잘 맞습니다.

💻 실전 코드

현실적인 시나리오로 “고객지원 RAG”를 잡겠습니다.

  • 문서 chunk는 S3 같은 오브젝트 스토리지에 원문 저장
  • 벡터DB에는 vector + 최소 메타데이터(tenant_id, doc_id, chunk_id, updated_at, tags) 만 저장
  • 검색은 (1) tenant 필터 + (2) 최신 문서 우선 + (3) dense top-k 후 애플리케이션에서 rerank/답변

아래는 Qdrant를 예로 든 “운영형” 코드입니다(필터/인덱스/온디스크 옵션이 드러나서). 다른 DB를 쓰더라도 구조는 거의 동일합니다.

1) 초기 셋업 (Docker + 컬렉션 생성)

1
2
3
4
5
6
7
8
# 1) Qdrant 실행
docker run -p 6333:6333 -p 6334:6334 \
  -v "$(pwd)/qdrant_storage:/qdrant/storage" \
  qdrant/qdrant:latest

# 2) 의존성
python -m venv .venv && source .venv/bin/activate
pip install qdrant-client fastembed pydantic
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# create_collection.py
from qdrant_client import QdrantClient
from qdrant_client.models import (
    Distance, VectorParams,
    PayloadSchemaType
)

client = QdrantClient(url="http://localhost:6333")

COLLECTION = "cs_chunks_v1"
DIM = 1024  # 예: bge-m3의 설정에 맞춰 실제 dim 사용

# 컬렉션 생성: 벡터를 디스크(memmap)로 내려 RAM 사용을 줄이는 옵션(환경에 따라 조정)
client.recreate_collection(
    collection_name=COLLECTION,
    vectors_config=VectorParams(size=DIM, distance=Distance.COSINE, on_disk=True),
)

# 필터에 자주 쓰는 payload 필드는 반드시 인덱싱
client.create_payload_index(
    collection_name=COLLECTION,
    field_name="tenant_id",
    field_schema=PayloadSchemaType.KEYWORD,
)
client.create_payload_index(
    collection_name=COLLECTION,
    field_name="updated_at",
    field_schema=PayloadSchemaType.INTEGER,
)

print("OK: collection & payload indexes created")

예상 출력:

1
OK: collection & payload indexes created

2) 업서트 파이프라인 (문서 변경에 강하게)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# upsert_chunks.py
import time
from typing import Iterable
from qdrant_client import QdrantClient
from qdrant_client.models import PointStruct
from fastembed import TextEmbedding  # 로컬 임베딩(예시). 운영은 자체 임베딩 서비스 권장.

client = QdrantClient(url="http://localhost:6333")
embedder = TextEmbedding(model_name="BAAI/bge-m3")  # 환경에 따라 모델/차원 확인 필수

COLLECTION = "cs_chunks_v1"

def upsert_chunks(tenant_id: str, doc_id: str, chunks: Iterable[tuple[str, str]]):
    """
    chunks: Iterable[(chunk_id, chunk_text)]
    """
    updated_at = int(time.time())
    texts = [t for _, t in chunks]
    vectors = list(embedder.embed(texts))  # (n, dim)

    points = []
    for (chunk_id, text), vec in zip(chunks, vectors):
        points.append(
            PointStruct(
                id=f"{tenant_id}:{doc_id}:{chunk_id}",
                vector=vec,
                payload={
                    "tenant_id": tenant_id,
                    "doc_id": doc_id,
                    "chunk_id": chunk_id,
                    "updated_at": updated_at,
                    # 원문은 벡터DB에 넣지 말고 object storage key만 저장 권장
                    "blob_key": f"s3://support/{tenant_id}/{doc_id}/{chunk_id}.txt",
                },
            )
        )

    client.upsert(collection_name=COLLECTION, points=points)
    return len(points)

if __name__ == "__main__":
    n = upsert_chunks(
        tenant_id="acme",
        doc_id="refund_policy",
        chunks=[
            ("0001", "Refunds are available within 30 days of purchase..."),
            ("0002", "To request a refund, open a ticket in the portal..."),
        ],
    )
    print(f"OK: upserted={n}")

3) 검색 (필터 + 최신성 + top-k)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# search.py
from qdrant_client import QdrantClient
from qdrant_client.models import Filter, FieldCondition, MatchValue, Range
from fastembed import TextEmbedding

client = QdrantClient(url="http://localhost:6333")
embedder = TextEmbedding(model_name="BAAI/bge-m3")
COLLECTION = "cs_chunks_v1"

def search(tenant_id: str, query: str, updated_after: int | None = None, k: int = 8):
    qvec = list(embedder.embed([query]))[0]

    must = [FieldCondition(key="tenant_id", match=MatchValue(value=tenant_id))]
    if updated_after is not None:
        must.append(FieldCondition(key="updated_at", range=Range(gte=updated_after)))

    flt = Filter(must=must)

    hits = client.search(
        collection_name=COLLECTION,
        query_vector=qvec,
        limit=k,
        query_filter=flt,
        with_payload=True,
    )
    return [(h.score, h.payload["blob_key"]) for h in hits]

if __name__ == "__main__":
    for score, blob_key in search("acme", "How do I get a refund?", k=5):
        print(score, blob_key)

예상 출력(예시):

1
2
3
0.78 s3://support/acme/refund_policy/0001.txt
0.71 s3://support/acme/refund_policy/0002.txt
...

이 패턴의 핵심은 “벡터DB를 source of truth로 만들지 말고, search index로 취급”하는 것입니다. 그래야 마이그레이션/재색인이 쉬워지고, payload 폭발/중복/정합성 문제가 줄어듭니다.


⚡ 실전 팁 & 함정

Best Practice 1) “필터 필드”는 DB별로 인덱싱/전략이 다르다

  • Qdrant: payload index 없으면 필터가 급격히 비싸질 수 있고, 클라우드는 비인덱스 필터/업데이트를 제한하는 방향(strict mode)2
  • Weaviate: hybrid(BM25+dense) 자체 지원 + 최근엔 filter 전략 파라미터도 문서에 노출(acorn/sweeping 등)10
  • Pinecone: 관리형이라 “필터는 되는데, 필터 선택도에 따른 비용/지연”이 내부 구현에 묻히는 편. 대신 운영 부담이 낮음.11

결론: 우리 서비스의 핵심 쿼리(tenant, ACL, time range, category)가 무엇인지 먼저 쓰고, “그 필터가 인덱스 친화적인가?”로 제품을 고르세요.

Best Practice 2) 메모리 비용을 ‘압축/온디스크/서버리스’ 중 무엇으로 해결할지 결정

  • Weaviate는 RQ(8-bit) 같은 압축이 강력하고, HFresh처럼 디스크 중심 인덱스도 제공합니다.5
  • Qdrant는 memmap/on_disk로 내려갈 수 있지만, HNSW/graph traversal이 IO를 타면 tail latency가 튈 수 있어 “둘 다 디스크”는 신중해야 합니다.2
  • Pinecone은 object storage + 캐시/executor 구조로 “운영/탄력성”을 아키텍처로 해결하는 타입입니다.1

Best Practice 3) “쓰기 패턴”을 과소평가하지 말 것 (업데이트/삭제/세그먼트)

실서비스는 문서가 계속 바뀌고, chunk가 교체됩니다. 이때:

  • 세그먼트/컴팩션/옵티마이저가 있는 DB는 인덱스 빌드 전/후 성능이 다르게 보일 수 있습니다(Qdrant는 옵티마이저가 HNSW를 만드는 타이밍을 명시).2
  • Weaviate는 async indexing 옵션을 제공해 ingest 시 대기 시간을 줄일 수 있습니다.12

흔한 함정 1) “원문을 payload에 다 넣기”

  • payload가 커지면 저장/전송/캐시 비용이 커지고, 필터 인덱스/세그먼트 병합에서도 발목을 잡습니다.
  • 권장: payload는 포인터 + 최소 메타데이터, 원문은 object storage/문서DB에 둔 뒤 hit만 resolve.

흔한 함정 2) “Hybrid를 켰다고 품질이 자동으로 좋아지지 않는다”

Weaviate는 hybrid를 시스템적으로 잘 지원하지만(병렬 실행 + fusion)4, 실무 품질은:

  • chunking
  • sparse/dense 가중치
  • reranker 적용 여부
  • 쿼리 타입(제품명/오탈자/약어) 에 의해 결정됩니다. Hybrid는 만능이 아니라 장르 확장 도구에 가깝습니다.

비용/성능/안정성 트레이드오프 한 줄 요약

  • Pinecone: 운영 최소화 + 탄력적 과금/스케일 ↔ 내부 튜닝/아키텍처 통제가 제한적1
  • Weaviate: 품질(하이브리드) + 메모리(압축/디스크 인덱스) 옵션 풍부 ↔ 설정/운영 파라미터가 많아 “아는 만큼 보임”5
  • Qdrant: 필터/멀티테넌시 현실성 + 온디스크/strict mode로 운영 안전장치 ↔ 세그먼트/인덱싱 타이밍에 따른 성능 변동을 이해해야 함2
  • Chroma: 가볍고 빠른 적용(단일 노드) ↔ 규모/분산/엄격한 성능 SLO가 목표면 한계가 빨리 올 수 있음9

🚀 마무리

핵심은 “누가 더 빠르냐”가 아니라, 내 쿼리의 병목이 어디냐입니다.

  • 트래픽이 스파이크 치고, 운영 인력이 적고, 빨리 안전하게 가고 싶다 → Pinecone(serverless)1
  • BM25+dense hybrid가 기본이고, 메모리 비용이 커서 압축/디스크 인덱스까지 옵션으로 잡고 싶다 → Weaviate(RQ/HFresh 포함)4
  • tenant/ACL/메타 필터가 빡세고, 온프렘/클라우드 모두 고려하며 “필터가 곧 성능”인 서비스다 → Qdrant(payload index + on_disk 전략)2
  • 단일 노드/내장형에 가깝게 빠르게 적용하고, 이후 교체도 염두에 둔다 → Chroma9

다음 학습 추천(바로 실무로 이어지는 순서): 1) “우리 서비스 top-3 쿼리”를 문서로 고정(필터 선택도 포함)
2) cold/warm cache, 업데이트/삭제 churn을 포함한 리플레이 벤치 작성
3) Weaviate의 RQ/HFresh 또는 Qdrant의 on_disk+payload index 같은 “비용 절감 옵션”을 실제 SLO에 맞춰 실험

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.