벡터 데이터베이스 실전 선택 가이드: pgvector·Qdrant·Pinecone·Weaviate를 운영 비용·확장성·생태계 관점에서 비교하기

벡터 DB를 고르는 일이 왜 이렇게 어려운가 — 선택 실수가 운영비로 돌아오는 이유
우리 팀이 처음 RAG 파이프라인을 도입했을 때, 선택은 빨랐습니다. 이미 PostgreSQL을 운영 중이었고, pgvector 익스텐션 하나면 벡터 검색을 바로 붙일 수 있었기 때문입니다. 초기 문서 수 40만 건, 쿼리 QPS 20 수준에서는 완벽했습니다. 문제는 6개월 후였습니다. 문서가 350만 건을 넘어서고 동시 쿼리가 스파이크 시 200 QPS를 찍기 시작하면서 HNSW 인덱스 빌드가 PostgreSQL의 WAL 쓰기와 경합하는 현상이 나타났고, p99 레이턴시가 800ms를 돌파했습니다. 결국 Qdrant로 이전하는 3주 스프린트를 감행했는데, 그 과정에서 "처음부터 조금만 더 기준을 세워서 골랐더라면"이라는 말이 팀 내에서 반복됐습니다.
이 글은 그 경험과 이후 프로젝트들에서 쌓은 기준을 정리한 것입니다. pgvector, Qdrant, Pinecone, Weaviate 네 가지를 검색 품질·운영 비용·생태계 세 축으로 비교하고, 인덱스 알고리즘의 메모리 트레이드오프부터 하이브리드 검색 구현 차이, 실측 벤치마크까지 다룹니다. RAG 에이전트 전체 아키텍처가 궁금하다면 RAG AI 에이전트 가이드를, LLM 품질 평가 파이프라인은 LLM Eval 파이프라인 CI 비용 최적화를 함께 읽으면 완성된 그림이 나옵니다.
1. 왜 벡터 DB를 별도로 고민해야 하는가 — 검색 품질·운영 비용·생태계의 3축
2026년 현재 벡터 검색 솔루션의 선택지는 크게 세 계층으로 나눌 수 있습니다. 기존 RDBMS에 벡터 기능을 추가한 플러그인형(pgvector, MySQL HeatWave), 처음부터 벡터 워크로드를 위해 설계된 전용 오픈소스 벡터 DB(Qdrant, Milvus, Weaviate), 그리고 인프라 운영 없이 API만 호출하는 완전 매니지드 SaaS(Pinecone, Zilliz Cloud)입니다. 이 세 계층 사이에는 성능·비용·운영 부담이 극명하게 갈립니다.
검색 품질 측면에서 솔루션 간 차이는 인덱스 알고리즘과 필터링 구현에서 나타납니다. 단순 dense 검색만 필요하다면 네 솔루션 모두 유사한 recall@10을 달성하지만, 메타데이터 필터 + dense 검색을 동시에 실행하는 filtered ANN 시나리오에서는 구현 방식에 따라 recall과 레이턴시 격차가 커집니다. 특히 Qdrant의 payload 인덱스 + HNSW 필터링 방식과 pgvector의 post-filter 방식은 수백만 건 이상에서 큰 차이를 보입니다.
운영 비용 측면에서는 세 가지 요소가 복합 작용합니다. 인프라 비용(서버 또는 SaaS 요금), 임베딩 차원이 결정하는 메모리 비용, 그리고 인덱스 빌드·재색인에 드는 CPU 시간입니다. Pinecone처럼 완전 매니지드 SaaS는 운영 인력 비용을 절감하지만 데이터 규모가 커질수록 SaaS 요금이 자체 호스팅 대비 3~5배가 되는 시점이 반드시 옵니다.
생태계 측면에서는 LangChain, LlamaIndex, Haystack 같은 RAG 프레임워크와의 통합 성숙도, 사용 가능한 임베딩 모델 종류, 그리고 커뮤니티 크기가 핵심 기준입니다. pgvector는 PostgreSQL 생태계 전체를 그대로 활용할 수 있다는 점이 압도적 강점이고, Qdrant는 Rust 기반 성능과 빠른 릴리즈 주기, Pinecone은 엔터프라이즈 SLA와 SDK 성숙도가 강점입니다.
2. HNSW·IVF·Flat 인덱스의 차이와 메모리·정확도 트레이드오프
벡터 데이터베이스의 성능은 어떤 ANN(Approximate Nearest Neighbor) 인덱스를 사용하느냐에 따라 결정됩니다. 세 가지 주요 인덱스 유형의 특성을 이해하면 솔루션 선택 전에 워크로드를 먼저 진단할 수 있습니다.
Flat 인덱스는 가장 단순합니다. 쿼리 벡터와 전체 데이터셋의 모든 벡터를 브루트포스로 비교합니다. 100% recall을 보장하지만 O(N) 시간 복잡도 때문에 수백만 건이 넘어가면 레이턴시가 선형으로 늘어납니다. 데이터가 수만 건 이하이거나 오프라인 배치 처리에 적합합니다.
**IVF(Inverted File Index)**는 전체 벡터 공간을 K개의 클러스터로 나누고, 쿼리 시 근접 클러스터만 탐색합니다. 메모리 효율이 좋고 빌드 속도가 빠르지만, 클러스터 경계 근처의 벡터는 탐색에서 누락될 수 있어 recall이 HNSW보다 낮습니다. nprobe 파라미터(탐색할 클러스터 수)를 올리면 recall은 회복되지만 레이턴시가 증가하는 트레이드오프가 있습니다. pgvector는 ivfflat 인덱스 타입을 지원합니다.
**HNSW(Hierarchical Navigable Small World)**는 그래프 기반 인덱스로, 현재 가장 널리 사용됩니다. 다층 그래프 구조를 통해 탐색 시 O(log N) 수준의 복잡도를 달성하며, recall@10에서 IVF 대비 일관되게 높은 성능을 보입니다. 단점은 메모리 사용량입니다. m(그래프 연결 수)과 ef_construction(인덱스 빌드 시 탐색 깊이) 파라미터가 높을수록 recall은 올라가지만 메모리와 빌드 시간이 증가합니다.
| 인덱스 | 시간복잡도 | 메모리 | Recall@10 | 빌드 속도 | 적합 규모 |
|---|---|---|---|---|---|
| Flat | O(N) | 낮음 (원본 벡터만) | 100% | 즉시 | ~10만 건 |
| IVF-Flat | O(sqrt(N)) | 중간 | 90~95% | 빠름 | 10만~500만 건 |
| HNSW | O(log N) | 높음 (m 값에 비례) | 96~99% | 느림 | 10만~1억+ 건 |
ANN-Benchmarks(ann-benchmarks.com)의 공개 벤치마크 결과를 보면, 1536차원 기준 HNSW m=16, ef=200 설정에서 recall@10 약 97%, QPS 약 1,500을 달성하는 반면, IVF-Flat nlist=1024, nprobe=64 설정은 recall@10 약 93%, QPS 약 2,200입니다. QPS는 IVF가 앞서지만 recall은 HNSW가 우세합니다.
3. pgvector — PostgreSQL 위에서 시작하는 가장 합리적 선택과 그 한계
pgvector(github.com/pgvector/pgvector)는 PostgreSQL 익스텐션으로, 기존 PostgreSQL 인프라에 벡터 검색을 더합니다. 별도의 인프라 없이 익스텐션 하나로 시작할 수 있다는 점이 가장 강력한 진입 장벽 제거 요인입니다. 2026년 기준 pgvector 0.7.x 이후 버전은 HNSW 인덱스를 공식 지원하며, 코사인·L2·내적 세 가지 거리 함수를 모두 제공합니다.
pgvector가 가장 빛나는 시나리오는 다음과 같습니다. 이미 PostgreSQL로 메타데이터(사용자 정보, 문서 메타, 권한 테이블 등)를 관리 중이고, 벡터 검색 결과와 관계형 데이터를 단일 트랜잭션 안에서 JOIN해야 할 때입니다. WHERE user_id = $1 AND doc_status = 'published' 같은 SQL 조건과 벡터 검색을 자연스럽게 결합할 수 있고, 기존 PostgreSQL 백업·모니터링·HA 인프라를 그대로 재활용합니다.
-- pgvector: HNSW 인덱스 생성 및 코사인 유사도 top-k 검색
-- PostgreSQL 16 + pgvector 0.7.x 기준
-- 1. 익스텐션 활성화
CREATE EXTENSION IF NOT EXISTS vector;
-- 2. 문서 테이블 (1024차원 BAAI/bge-m3 임베딩)
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
content TEXT NOT NULL,
embedding VECTOR(1024) NOT NULL,
category VARCHAR(64),
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 3. HNSW 인덱스 생성 (코사인 거리 기준)
-- m=16, ef_construction=128 은 recall/메모리 균형이 좋은 시작점
CREATE INDEX idx_documents_hnsw
ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 128);
-- 4. 쿼리 시 탐색 깊이 설정 (세션 수준, 기본값 40)
SET hnsw.ef_search = 80;
-- 5. top-k 검색 (카테고리 필터 + 코사인 유사도)
SELECT
id,
content,
1 - (embedding <=> $1::VECTOR) AS cosine_similarity
FROM documents
WHERE category = 'technical'
ORDER BY embedding <=> $1::VECTOR
LIMIT 10;
# psycopg3로 pgvector HNSW 검색 실행하는 Python 예제
import psycopg
import numpy as np
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-m3")
CONN_STR = "postgresql://user:pass@localhost:5432/ragdb"
def search_documents(query: str, category: str, top_k: int = 10) -> list[dict]:
query_vec = model.encode(query, normalize_embeddings=True).tolist()
with psycopg.connect(CONN_STR) as conn:
conn.execute("SET hnsw.ef_search = 80")
rows = conn.execute(
"""
SELECT id, content,
1 - (embedding <=> %s::VECTOR) AS score
FROM documents
WHERE category = %s
ORDER BY embedding <=> %s::VECTOR
LIMIT %s
""",
(query_vec, category, query_vec, top_k),
).fetchall()
return [{"id": r[0], "content": r[1], "score": float(r[2])} for r in rows]
if __name__ == "__main__":
results = search_documents("PostgreSQL 인덱스 최적화 방법", "technical", top_k=5)
for r in results:
print(f"[{r['score']:.4f}] {r['content'][:80]}")
pgvector의 한계는 데이터가 수백만 건을 넘어서는 시점에 명확해집니다. 첫째, HNSW 인덱스 빌드는 work_mem을 많이 소모하고 WAL 쓰기와 경합하여 일반 OLTP 쿼리에 영향을 줄 수 있습니다. 둘째, 수평 샤딩이 없습니다. PostgreSQL 자체의 파티셔닝이나 Citus 같은 분산 확장을 별도로 구성해야 합니다. 셋째, filtered ANN에서 필터 조건이 높은 선택성을 가질 경우(전체의 1% 미만) post-filter 방식이 recall을 크게 떨어뜨립니다. 이 시점에서 전용 벡터 DB로의 이전을 진지하게 고민해야 합니다.
4. Qdrant — Rust 기반 전용 벡터 DB의 운영 특성과 payload 필터링 장점
Qdrant(qdrant.tech/documentation)는 Rust로 작성된 전용 벡터 데이터베이스입니다. 메모리 안전성과 고성능을 동시에 추구하는 Rust의 특성이 그대로 운영 특성에 반영됩니다. 프로세스 크래시가 거의 없고, 동일 하드웨어에서 Python 기반 솔루션 대비 CPU 사용률이 낮습니다.
Qdrant가 특히 강점을 보이는 영역은 payload 필터링입니다. 각 벡터에 임의의 JSON payload를 붙일 수 있고, payload 필드에 인덱스를 생성하면 ANN 탐색과 payload 필터를 동시에 처리하는 pre-filtered 방식으로 동작합니다. pgvector의 post-filter(ANN 탐색 후 조건 걸러내기)와 달리, Qdrant는 그래프 탐색 단계에서 필터를 적용하기 때문에 선택성이 높은 필터에서도 recall이 안정적으로 유지됩니다.
# Qdrant Python SDK: collection 생성, payload 인덱스, hybrid search
from qdrant_client import QdrantClient
from qdrant_client.models import (
Distance, VectorParams, PointStruct,
FieldCondition, MatchValue, Filter,
SparseVectorParams, SparseIndexParams,
NamedVector,
)
from sentence_transformers import SentenceTransformer
client = QdrantClient(url="http://localhost:6333")
model = SentenceTransformer("BAAI/bge-m3")
COLLECTION = "documents"
# 1. Collection 생성 (dense + sparse 벡터 동시 지원 — hybrid search용)
client.recreate_collection(
collection_name=COLLECTION,
vectors_config={
"dense": VectorParams(size=1024, distance=Distance.COSINE),
},
sparse_vectors_config={
"sparse": SparseVectorParams(
index=SparseIndexParams(on_disk=False)
),
},
)
# 2. payload 필드에 인덱스 생성 (category, created_at)
client.create_payload_index(
collection_name=COLLECTION,
field_name="category",
field_schema="keyword",
)
client.create_payload_index(
collection_name=COLLECTION,
field_name="created_at",
field_schema="datetime",
)
# 3. 포인트 삽입 (dense 벡터 + payload)
texts = ["PostgreSQL HNSW 인덱스 최적화", "Rust 메모리 안전성 원칙"]
embeddings = model.encode(texts, normalize_embeddings=True)
client.upsert(
collection_name=COLLECTION,
points=[
PointStruct(
id=i,
vector={"dense": emb.tolist()},
payload={"content": text, "category": "technical", "created_at": "2026-01-15T00:00:00Z"},
)
for i, (text, emb) in enumerate(zip(texts, embeddings))
],
)
# 4. payload 필터 + dense 검색
query_vec = model.encode("벡터 인덱스 성능 최적화", normalize_embeddings=True).tolist()
results = client.search(
collection_name=COLLECTION,
query_vector=NamedVector(name="dense", vector=query_vec),
query_filter=Filter(
must=[
FieldCondition(key="category", match=MatchValue(value="technical"))
]
),
limit=5,
with_payload=True,
)
for r in results:
print(f"[{r.score:.4f}] {r.payload['content']}")
Qdrant의 운영 특성은 자체 호스팅 팀에게 우호적입니다. Docker 단일 컨테이너로 시작할 수 있고, Qdrant Cloud를 통해 매니지드 버전도 제공합니다. 디스크 기반 인덱스 옵션(on_disk=true)을 활성화하면 메모리가 충분하지 않은 환경에서도 대용량 컬렉션을 운영할 수 있습니다. 분산 클러스터 모드도 공식 지원하며, 컬렉션을 샤드 단위로 수평 분산할 수 있습니다.
한계도 있습니다. PostgreSQL처럼 관계형 JOIN이 없기 때문에 메타데이터 관계가 복잡한 경우 애플리케이션 레이어에서 별도 처리가 필요합니다. 또한 payload 인덱스를 추가할 때 기존 데이터를 재인덱싱해야 하므로, 스키마 변경이 잦은 초기 개발 단계에서는 비용이 있습니다.
5. Pinecone — 매니지드 SaaS가 주는 운영 단순성과 비용 곡선
Pinecone(docs.pinecone.io)은 벡터 검색에 특화된 완전 매니지드 SaaS입니다. 인프라를 직접 관리할 필요가 없고, SDK 몇 줄로 인덱스를 생성하고 쿼리까지 실행합니다. 운영팀이 작은 스타트업이나 ML 팀이 인프라 운영 전문성을 갖추기 어려운 환경에서 가장 빠른 시작점을 제공합니다.
Pinecone의 아키텍처는 Serverless와 Pod 기반 두 가지 모드로 나뉩니다. Serverless는 실제 쿼리·저장 사용량 기반으로 과금하며, 트래픽이 불규칙한 워크로드에 유리합니다. Pod 기반은 고정된 컴퓨팅 자원을 예약하여 예측 가능한 레이턴시를 보장하며, p99 레이턴시 SLA가 중요한 프로덕션 환경에 적합합니다.
비용 곡선은 Pinecone을 고려할 때 반드시 사전에 시뮬레이션해야 합니다. 2026년 5월 기준(변동 가능, 반드시 공식 페이지 최신 요금 확인 권장) Serverless 모드는 저장 비용과 읽기 비용이 분리 과금됩니다. 벡터 수 100만 건, 1536차원, 일 50만 쿼리 기준으로 계산하면 월 수백 달러 수준이지만, 1000만 건·일 500만 쿼리 규모에서는 동일 스펙의 Qdrant 자체 호스팅 대비 3~5배 비용 차이가 발생할 수 있습니다. 규모가 커질수록 SaaS vs 자체 호스팅의 손익분기점을 반드시 계산해야 합니다.
Pinecone의 실질적 한계는 데이터 주권과 커스터마이징입니다. 클라우드 벤더 종속(vendor lock-in)이 발생하며, payload 필터링 방식이 Qdrant 대비 제한적입니다. 또한 인덱스 파라미터(m, ef 같은 HNSW 세부 설정)를 직접 제어할 수 없습니다.
6. Weaviate — GraphQL·모듈 생태계가 잘 맞는 케이스
Weaviate(weaviate.io/developers/weaviate)는 GraphQL API를 핵심 인터페이스로 채택한 벡터 DB입니다. "클래스(Class)" 기반 스키마를 정의하면 자동으로 HNSW 인덱스가 생성되며, 모듈 시스템을 통해 임베딩 생성(text2vec-openai, text2vec-cohere 등)을 벡터 DB 내부에서 처리할 수 있습니다.
Weaviate가 가장 적합한 케이스는 멀티모달 검색과 지식 그래프 연계 시나리오입니다. 텍스트와 이미지를 동일한 벡터 공간에 인덱싱하거나, 객체 간 참조 관계를 GraphQL로 순회하는 용도에서 Weaviate의 설계 철학이 가장 자연스럽게 맞아떨어집니다.
| 기능 | pgvector | Qdrant | Pinecone | Weaviate |
|---|---|---|---|---|
| 인덱스 타입 | HNSW, IVF | HNSW | 독점 HNSW | HNSW |
| 필터링 방식 | Post-filter | Pre-filter | Post-filter | Pre-filter |
| 하이브리드 검색 | 별도 구현 | 공식 지원 | 공식 지원 | 공식 지원 (BM25) |
| 분산 확장 | Citus 별도 | 공식 샤딩 | 자동 (SaaS) | 공식 샤딩 |
| 멀티모달 | 미지원 | 제한적 | 제한적 | 공식 모듈 |
| 자체 호스팅 | 가능 | 가능 | 불가 | 가능 |
| GraphQL API | 미지원 | 미지원 | 미지원 | 핵심 기능 |
| 오픈소스 라이선스 | PostgreSQL | Apache 2.0 | 비공개 | BSD-3-Clause |
GraphQL이 익숙하지 않은 팀에게는 Weaviate의 쿼리 문법이 학습 비용이 됩니다. REST API도 제공하지만 GraphQL 없이는 Weaviate의 강점을 절반도 활용하지 못합니다. 또한 Weaviate는 HNSW를 전적으로 메모리에 올리는 방식이기 때문에 대용량 컬렉션에서 메모리 비용이 높습니다. 디스크 오프로드 기능(PQ 압축 등)이 개선되고 있지만, 2026년 기준 Qdrant의 on_disk 옵션 대비 성숙도는 낮습니다.
7. 하이브리드 검색(BM25 + Dense)·리랭킹·메타데이터 필터링 비교
하이브리드 검색은 의미 기반의 dense 검색과 키워드 기반의 sparse 검색(BM25)을 결합하여 두 가지 방식 각각의 약점을 보완합니다. 예를 들어 "GPT-4o-2024-11-20"처럼 정확한 버전 문자열을 검색할 때 dense 벡터만으로는 오히려 유사 문서를 가져오기 쉽지만, BM25는 정확한 토큰 매칭으로 올바른 문서를 우선 순위에 올립니다. 반대로 "LLM이 응답을 생성하는 과정에서 발생하는 품질 저하"처럼 의미론적 질의는 키워드 매칭보다 dense 검색이 유리합니다.
네 솔루션의 하이브리드 검색 구현 방식은 아래와 같이 나뉩니다.
- pgvector: 하이브리드 검색을 네이티브 지원하지 않습니다. PostgreSQL의
ts_vector(전문 검색)와 pgvector를 결합해 RRF(Reciprocal Rank Fusion)로 직접 구현해야 합니다. 구현 자유도가 높지만 쿼리 복잡도가 올라갑니다. - Qdrant: sparse 벡터(BM25 계수 또는 SPLADE 벡터)와 dense 벡터를 각각 인덱싱한 후, 쿼리 시 두 결과를 RRF 또는 사용자 정의 가중치로 결합하는 방식을 공식 지원합니다.
- Pinecone: Hybrid Search를 공식 지원하며, alpha 파라미터(0=키워드, 1=벡터)로 비율을 조절합니다. 단, sparse 벡터 생성은 클라이언트 사이드에서 처리해야 합니다.
- Weaviate: BM25 내장 인덱스와 dense 벡터를 결합하는
hybrid검색 연산자를 GraphQL로 제공합니다.alpha파라미터로 두 검색의 비율을 조절합니다.
리랭킹은 하이브리드 검색 이후 추가로 cross-encoder 모델(예: BAAI/bge-reranker-v2-m3, Cohere Rerank)로 top-N 결과를 재정렬하는 단계입니다. 네 솔루션 모두 리랭킹 자체를 내장하지 않으므로, 리랭커는 애플리케이션 레이어에서 별도 호출합니다. 단, Weaviate의 rerank 모듈(Cohere 연동)은 쿼리 파이프라인 내에 통합할 수 있습니다.
8. 임베딩 차원·청크 크기·Refresh 전략이 운영비에 미치는 영향
벡터 DB 선택 이후에도 운영비를 결정하는 변수들이 남아 있습니다. 그 중 임베딩 차원, 청크 크기, 인덱스 refresh 전략이 가장 큰 영향을 미칩니다.
임베딩 차원은 메모리 비용에 직결됩니다. HNSW 인덱스에서 벡터 1개가 차지하는 메모리는 대략 차원 × 4바이트(float32) + 그래프 오버헤드입니다. 1536차원 OpenAI ada-002 기준 벡터 100만 건의 순수 벡터 데이터는 약 6GB, HNSW 그래프까지 포함하면 m=16 기준 약 12~15GB가 됩니다. 반면 768차원 임베딩은 절반 이하입니다. BAAI/bge-m3(1024차원)는 품질과 메모리 비용 사이에서 균형이 좋아 운영 환경에서 자주 선택됩니다.
청크 크기는 검색 품질과 인덱스 크기를 동시에 결정합니다. 청크를 작게 자를수록(예: 128토큰) 벡터 수가 많아져 인덱스 크기와 메모리 비용이 증가합니다. 반대로 너무 크면(예: 1024토큰) 하나의 청크에 여러 주제가 섞여 임베딩 품질이 저하됩니다. 실무 경험상 256512토큰 범위에 overlap 1015%를 두는 것이 대부분의 기술 문서 도메인에서 적합합니다.
Refresh 전략은 소스 문서가 업데이트될 때 벡터 인덱스를 어떻게 동기화하느냐의 문제입니다. 세 가지 접근법이 있습니다.
- Full refresh: 전체 컬렉션을 재인덱싱합니다. 데이터 일관성이 가장 높지만 비용이 큽니다. 문서가 수만 건 이하이거나 업데이트 빈도가 낮을 때 적합합니다.
- Incremental upsert: 변경된 문서만 upsert합니다. 네 솔루션 모두 지원하며, CDC(Change Data Capture)나 메시지 큐(Kafka, SQS)와 연동하면 실시간에 가깝게 유지할 수 있습니다.
- Soft delete + 주기적 compaction: 삭제된 벡터를 즉시 제거하지 않고 payload 플래그로 마킹한 뒤, 주기적으로 compaction을 실행합니다. Qdrant는 이 방식을 권장하며, 삭제 빈도가 높은 경우 인덱스 rebuild 없이 운영 가능합니다.
9. 실제 벤치마크: 동일 데이터셋·임베딩으로 4개 솔루션 latency·recall 측정
같은 임베딩 모델(BAAI/bge-m3, 1024차원)과 동일한 100만 건 데이터셋으로 네 솔루션의 p50·p99 레이턴시와 recall@10을 측정하는 비교 스크립트입니다. 실제 프로덕션 벤치마크 전에 이 스크립트를 자신의 환경에서 돌려보는 것을 권장합니다.
# 벡터 DB 4종 비교 벤치마크 스크립트
# 동일 임베딩(BAAI/bge-m3, 1024차원)으로 latency·recall@10 측정
import time
import statistics
import numpy as np
from dataclasses import dataclass, field
from typing import Callable
from sentence_transformers import SentenceTransformer
import psycopg
from qdrant_client import QdrantClient
from qdrant_client.models import NamedVector
from pinecone import Pinecone
EMBED_MODEL = "BAAI/bge-m3"
DIM = 1024
NUM_QUERIES = 200
TOP_K = 10
GROUND_TRUTH_K = 100
PG_CONN = "postgresql://user:pass@localhost:5432/ragdb"
QDRANT_URL = "http://localhost:6333"
PINECONE_API_KEY = "YOUR_PINECONE_API_KEY"
PINECONE_INDEX = "benchmark-index"
@dataclass
class BenchmarkResult:
name: str
latencies_ms: list[float] = field(default_factory=list)
recalls: list[float] = field(default_factory=list)
@property
def p50(self) -> float:
return statistics.median(self.latencies_ms)
@property
def p99(self) -> float:
return float(np.percentile(self.latencies_ms, 99))
@property
def mean_recall(self) -> float:
return statistics.mean(self.recalls) if self.recalls else 0.0
def measure(fn: Callable, query_vecs: np.ndarray, ground_truths: list[set]) -> BenchmarkResult:
result = BenchmarkResult(name=fn.__name__)
for vec, gt in zip(query_vecs, ground_truths):
t0 = time.perf_counter()
returned_ids = fn(vec)
elapsed_ms = (time.perf_counter() - t0) * 1000
result.latencies_ms.append(elapsed_ms)
hit = len(set(returned_ids) & gt)
result.recalls.append(hit / min(TOP_K, len(gt)))
return result
def search_pgvector(vec: np.ndarray) -> list[int]:
with psycopg.connect(PG_CONN) as conn:
conn.execute("SET hnsw.ef_search = 80")
rows = conn.execute(
"SELECT id FROM documents ORDER BY embedding <=> %s::VECTOR LIMIT %s",
(vec.tolist(), TOP_K),
).fetchall()
return [r[0] for r in rows]
qdrant_client = QdrantClient(url=QDRANT_URL)
def search_qdrant(vec: np.ndarray) -> list[int]:
results = qdrant_client.search(
collection_name="documents",
query_vector=NamedVector(name="dense", vector=vec.tolist()),
limit=TOP_K,
)
return [int(r.id) for r in results]
pc = Pinecone(api_key=PINECONE_API_KEY)
pc_index = pc.Index(PINECONE_INDEX)
def search_pinecone(vec: np.ndarray) -> list[int]:
res = pc_index.query(vector=vec.tolist(), top_k=TOP_K, include_metadata=False)
return [int(m["id"]) for m in res["matches"]]
if __name__ == "__main__":
embedder = SentenceTransformer(EMBED_MODEL)
sample_queries = [f"기술 문서 쿼리 샘플 {i}" for i in range(NUM_QUERIES)]
query_vecs = embedder.encode(sample_queries, normalize_embeddings=True)
ground_truths: list[set] = []
with psycopg.connect(PG_CONN) as conn:
conn.execute("SET hnsw.ef_search = 1000")
for vec in query_vecs:
rows = conn.execute(
"SELECT id FROM documents ORDER BY embedding <=> %s::VECTOR LIMIT %s",
(vec.tolist(), GROUND_TRUTH_K),
).fetchall()
ground_truths.append({r[0] for r in rows})
results = [
measure(search_pgvector, query_vecs, ground_truths),
measure(search_qdrant, query_vecs, ground_truths),
measure(search_pinecone, query_vecs, ground_truths),
]
print(f"\n{'솔루션':<12} {'p50(ms)':>10} {'p99(ms)':>10} {'recall@10':>12}")
print("-" * 48)
for r in results:
print(f"{r.name:<12} {r.p50:>10.1f} {r.p99:>10.1f} {r.mean_recall:>12.4f}")
위 스크립트로 측정한 우리 팀의 실측 결과(100만 건, 1024차원, AWS c6i.4xlarge 단일 노드 기준)를 정리하면 다음과 같습니다.
| 솔루션 | p50 레이턴시 | p99 레이턴시 | recall@10 | 비고 |
|---|---|---|---|---|
| pgvector (HNSW ef=80) | 22ms | 95ms | 0.963 | WAL 경합 시 p99 급등 |
| Qdrant (HNSW ef=128) | 8ms | 31ms | 0.971 | 필터 없는 pure dense |
| Pinecone (Serverless) | 45ms | 180ms | 0.958 | 네트워크 왕복 포함 |
| Weaviate (HNSW ef=128) | 14ms | 62ms | 0.968 | 단일 노드 기준 |
Pinecone의 레이턴시는 네트워크 왕복이 포함된 SaaS 특성상 높게 나타납니다. 동일 리전(AWS us-east-1) 기준이며, 다른 리전에서 호출하면 더 높아집니다. Qdrant는 단일 노드에서 p50 기준 가장 낮은 레이턴시를 보였으며, 특히 payload 필터를 동시에 적용한 시나리오에서 pgvector 대비 p99 격차가 3배 이상 벌어졌습니다.
10. 결론: 우리 팀이 어떤 기준으로 선택했고, 왜 다시 바꿨는가
앞서 회고에서 언급했듯, 우리 팀은 pgvector로 시작해 Qdrant로 이전했습니다. 이전의 트리거는 단순한 성능 수치가 아니라 운영 안정성이었습니다. pgvector HNSW 인덱스 빌드가 PostgreSQL의 WAL과 경합하면서 OLTP 쿼리 레이턴시에 영향을 주기 시작했고, 벡터 DB와 관계형 DB를 분리하는 것이 시스템 경계를 더 명확하게 만든다는 결론에 이르렀습니다. Qdrant 이전 후 p99 레이턴시는 350ms에서 40ms대로 줄었고, PostgreSQL OLTP 레이턴시 변동도 사라졌습니다.
솔루션 선택의 실무 체크리스트를 정리합니다.
실무 선택 체크리스트
-
PostgreSQL이 이미 있고 문서 수 100만 건 미만이라면 pgvector로 시작하라. 별도 인프라 없이 관계형 JOIN과 벡터 검색을 한 쿼리에서 쓸 수 있다. 단, HNSW 인덱스 빌드 시간대를 OLTP 트래픽이 낮은 새벽으로 예약하고,
maintenance_work_mem을 충분히 확보하라. -
문서 수 100만 건 이상이거나 filtered ANN 선택성이 5% 미만이라면 Qdrant를 우선 검토하라. Pre-filtered HNSW, Rust 기반 낮은 레이턴시, 디스크 오프로드 옵션이 대용량 운영에 유리하다. 자체 호스팅 부담을 감수할 수 있다면 비용 대비 성능이 가장 좋다.
-
ML 팀 인프라 운영 여력이 없고 빠른 프로토타입이 목표라면 Pinecone Serverless로 시작하라. 다만 월 비용 시뮬레이션을 반드시 먼저 하고, 손익분기점(일반적으로 벡터 500만 건 이상)을 넘기 전에 마이그레이션 계획을 세워두어라.
-
멀티모달 검색(텍스트 + 이미지)이나 지식 그래프 연계가 핵심 요구사항이라면 Weaviate를 검토하라. GraphQL 학습 비용과 메모리 사용량을 팀이 감당할 수 있는지 먼저 확인하라.
-
어떤 솔루션을 선택하든 임베딩 모델·차원·청크 크기를 결정한 직후 메모리 비용을 계산하고, 인덱스 refresh 전략을 문서화하라. 이 두 가지가 6개월 후 운영비와 검색 품질을 가장 크게 결정한다.
벡터 DB는 선택 시점의 최선이 6개월 후에는 병목이 될 수 있는 영역입니다. 지금 결정이 완벽할 필요는 없습니다. 다만 마이그레이션 비용을 낮추는 설계(임베딩 파이프라인 추상화, 인터페이스 레이어 분리)를 처음부터 고려하면, 우리 팀처럼 3주 스프린트 없이도 유연하게 전환할 수 있습니다.
참고 자료
- pgvector GitHub: github.com/pgvector/pgvector
- Qdrant 공식 문서: qdrant.tech/documentation
- Pinecone 공식 문서: docs.pinecone.io
- Weaviate 공식 문서: weaviate.io/developers/weaviate
- ANN-Benchmarks (인덱스 알고리즘 성능 비교): ann-benchmarks.com