Pinecone vs Weaviate vs Qdrant vs Chroma: 2026년 2월 기준 “성능/운영/비용”으로 고르는 벡터DB 선택 가이드
들어가며
RAG가 “데모는 쉬운데 운영이 어렵다”로 귀결되는 가장 큰 이유는 retrieval 계층이 병목이 되기 때문입니다. LLM 호출 비용/지연만 보다가, 막상 트래픽이 붙으면 벡터 검색 latency(P95), 필터링 비용, 인덱싱(ingest) 처리량, 메모리 압박이 전체 시스템 SLO를 결정합니다.
그래서 벡터DB 선택은 “기능 체크리스트”가 아니라, 워크로드(쓰기/읽기 비율, 필터 비중, 멀티테넌시, 하이브리드 필요 여부)를 먼저 고정하고 그에 맞춰야 합니다.
2026년 2월 관점에서 많이 쓰는 4종(Pinecone/Weaviate/Qdrant/Chroma)을 선택 가이드 + 성능 관점 포인트로 정리합니다.
(참고로, 벤치마크 수치는 환경에 따라 크게 흔들립니다. 아래에서는 ‘왜 그런 결과가 나오는가’에 초점을 둡니다.)
🔧 핵심 개념
1) 벡터 검색 성능을 좌우하는 4요소
- Index 타입(Flat vs HNSW)
- Flat: 정확하지만 O(N). 작은 데이터/테넌트에는 유리.
- HNSW: ANN 그래프 기반. 대규모에서 latency/throughput에 유리.
Weaviate는 flat→HNSW로 자동 전환하는 dynamic index를 제공(기본 threshold 10,000)합니다. (weaviate.io)
필터링 전략(“ANN 후 필터” vs “필터-aware ANN”)
실무 RAG는 거의 항상tenant_id,doc_type,created_at같은 metadata filter가 붙습니다.
Qdrant는 payload index를 별도로 만들고, 어떤 필드를 인덱싱할지/디스크에 둘지까지 튜닝할 수 있어 “필터가 많은 서비스”에서 강점이 나옵니다. (qdrant.tech)- Quantization/Compression(메모리 vs recall vs 속도 트레이드오프)
- Qdrant: scalar/binary/product quantization을 공식 지원하고, 특히 scalar는
float32 -> int8로 4x 메모리 절감 + SIMD 비교로 속도 이점이 가능합니다. (qdrant.tech) - Weaviate: PQ/SQ/RQ/BQ 등 다양한 compression 조합을 제공하며, dynamic index와 함께 “작을 땐 가볍게, 커지면 빠르게”를 설계할 수 있습니다. (docs.weaviate.io)
- (주의) Weaviate의 “default quantization”은 문서/설정 상태에 따라 오해가 생길 수 있어, 실제 배포에서는 환경변수/컬렉션 설정을 명시적으로 확인하는 습관이 필요합니다. (forum.weaviate.io)
- Qdrant: scalar/binary/product quantization을 공식 지원하고, 특히 scalar는
- 멀티테넌시 모델
- Pinecone: namespace 단위로 격리하고, “요청이 특정 namespace만 타게” 설계하여 noisy neighbor를 줄이고, 비용도 namespace 크기에 연동된다는 메시지를 강하게 가져갑니다. (docs.pinecone.io)
- Weaviate: multi-tenant + dynamic index 조합이 설계상 자연스럽습니다(작은 테넌트는 flat 유지). (weaviate.io)
2) “하이브리드 검색”이 필요한가?
- Weaviate: vector + BM25를 fusion으로 결합하는 hybrid search를 지원(가중치/전략 조정). (docs.weaviate.io)
- Pinecone: dense+sparse를 하나의 hybrid 인덱스/엔드포인트로 다루고, alpha로 keyword vs semantic 비중을 조절합니다. (pinecone.io)
결론: “RAG인데도 키워드 정확도가 중요(상품명/에러코드/약어)”하면 hybrid는 옵션이 아니라 거의 필수입니다.
💻 실전 코드
아래는 동일한 데이터(텍스트+metadata)로 4개 DB에 최소 동작 파이프라인을 맞춘 예시입니다.
(실무에서는 embedding 모델/차원/배치 크기/필터 패턴을 먼저 확정하고, 그다음 DB별 튜닝을 시작하세요.)
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# Python 3.11+
# pip install qdrant-client chromadb weaviate-client pinecone
# (Pinecone는 API Key 필요, Weaviate는 서버/클라우드 엔드포인트 필요)
from typing import List, Dict, Any
import os
TEXTS = [
"Pinecone serverless namespaces are useful for multitenancy.",
"Weaviate hybrid search fuses vector and BM25 results.",
"Qdrant payload index helps fast metadata filtering.",
"Chroma PersistentClient stores data under persist_directory.",
]
METAS = [
{"tenant_id": "t1", "source": "doc", "tag": "multitenancy"},
{"tenant_id": "t1", "source": "doc", "tag": "hybrid"},
{"tenant_id": "t2", "source": "doc", "tag": "filter"},
{"tenant_id": "t2", "source": "doc", "tag": "local"},
]
# 예제용 더미 embedding (실무에서는 OpenAI/Voyage/Cohere/bge 등으로 교체)
def embed(texts: List[str], dim: int = 8) -> List[List[float]]:
# 절대 운영에 쓰지 말 것: 단순 해시 기반
out = []
for t in texts:
v = [0.0] * dim
for i, ch in enumerate(t.encode("utf-8")):
v[i % dim] += (ch % 13) / 13.0
out.append(v)
return out
vectors = embed(TEXTS, dim=8)
def demo_qdrant():
# Qdrant: 필터가 중요하면 payload index를 "초기에" 생성 권장
from qdrant_client import QdrantClient, models
client = QdrantClient(url="http://localhost:6333")
collection = "demo"
client.recreate_collection(
collection_name=collection,
vectors_config=models.VectorParams(size=8, distance=models.Distance.COSINE),
)
# payload index: tenant_id로 필터할 거면 인덱싱
client.create_payload_index(
collection_name=collection,
field_name="tenant_id",
field_schema=models.KeywordIndexParams(),
)
points = [
models.PointStruct(id=i, vector=vectors[i], payload=METAS[i] | {"text": TEXTS[i]})
for i in range(len(TEXTS))
]
client.upsert(collection, points=points)
res = client.search(
collection_name=collection,
query_vector=embed(["hybrid search"], dim=8)[0],
query_filter=models.Filter(
must=[models.FieldCondition(key="tenant_id", match=models.MatchValue(value="t1"))]
),
limit=3,
)
return [r.payload for r in res]
def demo_chroma():
# Chroma: 로컬/프로토타이핑에 매우 편함. PersistentClient는 디렉토리에 sqlite+세그먼트 저장.
import chromadb
client = chromadb.PersistentClient(path="./chroma_data") # persist_directory
col = client.get_or_create_collection("demo")
ids = [str(i) for i in range(len(TEXTS))]
col.upsert(ids=ids, documents=TEXTS, embeddings=vectors, metadatas=METAS)
# where 필터 + 쿼리 embedding
q = embed(["payload index"], dim=8)[0]
res = col.query(query_embeddings=[q], n_results=3, where={"tenant_id": "t2"})
return res["metadatas"][0]
def demo_weaviate():
# Weaviate: hybrid(BM25+vector), dynamic index, quantization 등 옵션이 풍부
# 여기서는 "vector search + filter" 예시만 최소 형태로.
import weaviate
from weaviate.classes.config import Configure, Property, DataType
client = weaviate.connect_to_local() # 로컬 weaviate 실행 중이라고 가정
try:
client.collections.delete("Demo")
except Exception:
pass
demo = client.collections.create(
"Demo",
vectorizer_config=Configure.Vectorizer.none(),
properties=[
Property(name="text", data_type=DataType.TEXT),
Property(name="tenant_id", data_type=DataType.TEXT),
Property(name="tag", data_type=DataType.TEXT),
],
# 필요시 index/quantization/hybrid(BM25) 등을 컬렉션 단위로 설정
)
with demo.batch.dynamic() as batch:
for i in range(len(TEXTS)):
batch.add_object(
properties={"text": TEXTS[i], "tenant_id": METAS[i]["tenant_id"], "tag": METAS[i]["tag"]},
vector=vectors[i],
)
q = embed(["multitenancy"], dim=8)[0]
res = demo.query.near_vector(
near_vector=q,
limit=3,
filters=weaviate.classes.query.Filter.by_property("tenant_id").equal("t1"),
)
return [o.properties for o in res.objects]
def demo_pinecone():
# Pinecone: serverless index, namespace로 테넌트 분리하는 패턴이 강력
from pinecone import Pinecone, ServerlessSpec
pc = Pinecone(api_key=os.environ["PINECONE_API_KEY"])
index_name = "demo-vecdb-2026"
if index_name not in pc.list_indexes().names():
pc.create_index(
name=index_name,
dimension=8,
metric="cosine",
spec=ServerlessSpec(cloud="aws", region="us-east-1"),
)
index = pc.Index(index_name)
# tenant_id를 namespace로 사용 (멀티테넌시)
upserts_t1 = []
upserts_t2 = []
for i in range(len(TEXTS)):
item = (str(i), vectors[i], METAS[i] | {"text": TEXTS[i]})
(upserts_t1 if METAS[i]["tenant_id"] == "t1" else upserts_t2).append(item)
if upserts_t1:
index.upsert(vectors=upserts_t1, namespace="t1")
if upserts_t2:
index.upsert(vectors=upserts_t2, namespace="t2")
q = embed(["hybrid"], dim=8)[0]
res = index.query(vector=q, top_k=3, namespace="t1", include_metadata=True)
return [m["metadata"] for m in res["matches"]]
if __name__ == "__main__":
# 실행 환경에 따라 원하는 것만 테스트
print("Qdrant:", demo_qdrant())
print("Chroma:", demo_chroma())
# print("Weaviate:", demo_weaviate())
# print("Pinecone:", demo_pinecone())
⚡ 실전 팁
1) “성능 비교”를 할 때, 먼저 질문을 바꿔라
단순 TopK latency 비교는 의미가 약합니다. 아래 3가지를 꼭 넣어야 실전과 비슷해집니다.
- Filtered query 비중(예: 70%가
tenant_id+doc_type) - ingest 패턴(배치 upsert vs 스트리밍, update/delete 빈도)
- P95/P99 + 동시성(단일 요청 ms는 예쁘게 나오기 쉽습니다)
Qdrant는 payload index를 “나중에 만들면” 업데이트가 막힐 수 있으니, 컬렉션 생성 직후 인덱스 설계 확정이 중요합니다. (qdrant.tech)
Pinecone은 쿼리/업서트에 rate/용량 제한이 걸리면 429가 나므로, 백오프/재시도 + 배치가 필수입니다. (pinecone-poc-guide.mintlify.app)
2) 멀티테넌시: “namespace 분리”가 정답인 경우가 많다
Pinecone은 namespace 단위로 데이터가 분리되고, 읽기/쓰기 요청이 특정 namespace로만 라우팅된다는 점을 멀티테넌시 핵심 가치로 설명합니다. 이 방식은
- noisy neighbor 완화
- 테넌트 offboarding 단순화
- 비용(읽기 단위)이 “전체 스캔”보다 예측 가능 같은 장점이 있습니다. (docs.pinecone.io)
반대로 “테넌트 수가 수만이고, 각 테넌트가 매우 작다”면 Weaviate의 dynamic index(작을 때 flat, 커지면 HNSW) 전략이 운영적으로 깔끔합니다. (weaviate.io)
3) Quantization은 “메모리 절약”이 아니라 “캐시 적중률” 게임
대규모에서 성능이 무너지는 가장 흔한 이유는 CPU가 아니라 메모리/NUMA/캐시 미스입니다.
- Qdrant scalar quantization은 메모리를 4배 줄여 캐시 효율을 올리고, int8 SIMD로 비교가 빨라질 수 있습니다. (qdrant.tech)
- Weaviate도 RQ/PQ/SQ/BQ 등 압축 옵션이 있고, index 타입에 따라 가능한 조합이 다릅니다. (docs.weaviate.io)
실무 팁: “recall이 0.98에서 0.97로 떨어져도 latency가 2배 좋아진다”면, RAG 전체 품질은 오히려 좋아지는 경우가 흔합니다(리랭커/LLM이 후단에서 보정).
4) Chroma는 “로컬/제품화 전 단계”에서 빛난다
Chroma의 강점은 운영 복잡도가 아니라 DX와 로컬 영속성입니다. PersistentClient는 지정한 디렉토리에 sqlite 파일과 컬렉션 세그먼트를 저장합니다. (cookbook.chromadb.dev)
대신 10M급/고동시성/분산까지 밀어붙일 계획이면, 초반부터 Qdrant/Weaviate/Pinecone 같은 “서버/클러스터 전제” 제품으로 가는 편이 시행착오가 줄어듭니다.
🚀 마무리
정리하면, 4개 중 “누가 제일 빠르냐”보다 아래처럼 고르는 게 실패 확률이 낮습니다.
- Pinecone: 완전 관리형 + serverless 운영 모델, 멀티테넌시를 namespace로 깔끔하게 가져가고 싶을 때. (아키텍처/namespace 모델이 강한 메시지) (docs.pinecone.io)
- Weaviate: hybrid(BM25+vector) + 다양한 인덱스 전략(dynamic 포함) + 압축 옵션으로 “검색 기능 자체를 제품화”할 때. (weaviate.io)
- Qdrant: metadata filtering이 핵심이거나, payload index/온디스크 옵션/quantization 등으로 “성능 튜닝 여지”를 확보하고 싶을 때. (qdrant.tech)
- Chroma: 로컬 RAG, 프로토타이핑, 단일 노드에서 빠른 반복이 최우선일 때(영속성 구조가 단순). (cookbook.chromadb.dev)
다음 학습 추천: 1) 내 워크로드로 Filtered P95 벤치마크 스크립트를 먼저 만들고(동시성 포함),
2) Qdrant payload index 설계 / Weaviate dynamic+quantization / Pinecone namespace 전략을 각각 “한 가지 가설”로 비교해보면, 마케팅 문구가 아니라 수치로 결론이 납니다.
원하시면, (1) 데이터 크기/차원(768? 1536?), (2) 필터 패턴, (3) QPS/쓰기 비율을 알려주시면 그 조건에 맞춘 벤치마크 시나리오와 튜닝 체크리스트까지 더 구체적으로 잡아드릴게요.