Joyuno's Dev Blog

Pinecone vs Weaviate vs Qdrant vs Chroma: 2026년 2월 기준 “성능/운영/비용”으로 고르는 벡터DB 선택 가이드

AI RAG

들어가며

RAG가 “데모는 쉬운데 운영이 어렵다”로 귀결되는 가장 큰 이유는 retrieval 계층이 병목이 되기 때문입니다. LLM 호출 비용/지연만 보다가, 막상 트래픽이 붙으면 벡터 검색 latency(P95), 필터링 비용, 인덱싱(ingest) 처리량, 메모리 압박이 전체 시스템 SLO를 결정합니다.
그래서 벡터DB 선택은 “기능 체크리스트”가 아니라, 워크로드(쓰기/읽기 비율, 필터 비중, 멀티테넌시, 하이브리드 필요 여부)를 먼저 고정하고 그에 맞춰야 합니다.

2026년 2월 관점에서 많이 쓰는 4종(Pinecone/Weaviate/Qdrant/Chroma)을 선택 가이드 + 성능 관점 포인트로 정리합니다.
(참고로, 벤치마크 수치는 환경에 따라 크게 흔들립니다. 아래에서는 ‘왜 그런 결과가 나오는가’에 초점을 둡니다.)


🔧 핵심 개념

1) 벡터 검색 성능을 좌우하는 4요소

  1. Index 타입(Flat vs HNSW)
    • Flat: 정확하지만 O(N). 작은 데이터/테넌트에는 유리.
    • HNSW: ANN 그래프 기반. 대규모에서 latency/throughput에 유리.
      Weaviate는 flat→HNSW로 자동 전환하는 dynamic index를 제공(기본 threshold 10,000)합니다. (weaviate.io)
  2. 필터링 전략(“ANN 후 필터” vs “필터-aware ANN”)
    실무 RAG는 거의 항상 tenant_id, doc_type, created_at 같은 metadata filter가 붙습니다.
    Qdrant는 payload index를 별도로 만들고, 어떤 필드를 인덱싱할지/디스크에 둘지까지 튜닝할 수 있어 “필터가 많은 서비스”에서 강점이 나옵니다. (qdrant.tech)

  3. 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)
  4. 멀티테넌시 모델
    • 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별 튜닝을 시작하세요.)

# 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/쓰기 비율을 알려주시면 그 조건에 맞춘 벤치마크 시나리오와 튜닝 체크리스트까지 더 구체적으로 잡아드릴게요.