벡터 데이터베이스 비교 분석 2025
Pinecone vs Qdrant vs Weaviate vs Milvus - AI 시대의 벡터 DB 선택 가이드
🔮 벡터 데이터베이스 비교 분석 2025
AI 애플리케이션을 위한 최적의 벡터 데이터베이스 선택 가이드
📊 개요
비교 대상
- Pinecone: 완전 관리형 벡터 DB
- Qdrant: 고성능 오픈소스 벡터 DB
- Weaviate: 시맨틱 검색 특화
- Milvus: 대규모 벡터 검색
- Chroma: 개발자 친화적 벡터 DB
- pgvector: PostgreSQL 확장
평가 기준
- 성능 (검색 속도)
- 확장성
- 가격
- 기능
- 사용 편의성
- 생태계 지원
📈 상세 비교표
핵심 특성 비교
| 특성 | Pinecone | Qdrant | Weaviate | Milvus | | ------------------- | -------- | ------------- | ------------- | ------------- | | 호스팅 | SaaS | 자체/클라우드 | 자체/클라우드 | 자체/클라우드 | | 오픈소스 | ❌ | ✅ | ✅ | ✅ | | 최대 벡터 수 | 수십억 | 수십억 | 수억 | 수조 | | 차원 제한 | 20,000 | 65,536 | 65,536 | 32,768 | | 실시간 업데이트 | ✅ | ✅ | ✅ | ✅ | | 하이브리드 검색 | 제한적 | ✅ | ✅ | ✅ |
성능 벤치마크
| 메트릭 | Pinecone | Qdrant | Weaviate | Milvus | | ------------------ | -------- | ---------- | -------- | -------- | | QPS (1M 벡터) | 5,000+ | 10,000+ | 3,000+ | 8,000+ | | 지연시간 (P99) | <100ms | <50ms | <100ms | <80ms | | 인덱싱 속도 | 10K/s | 50K/s | 20K/s | 100K/s | | 메모리 효율성 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
가격 비교 (월 기준)
| 규모 | Pinecone | Qdrant Cloud | Weaviate Cloud | pgvector | | --------------- | -------- | ------------ | -------------- | -------- | | 1M 벡터 | $70 | $25 | $50 | EC2 비용 | | 10M 벡터 | $700 | $250 | $500 | EC2 비용 | | 100M 벡터 | $7,000 | $2,500 | $5,000 | EC2 비용 | | Self-hosted | N/A | $0 | $0 | $0 |
💼 사용 사례별 구현
🤖 RAG (Retrieval Augmented Generation)
추천: Qdrant
# Qdrant를 사용한 RAG 구현
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
import openai
import numpy as np
from typing import List, Dict
import uuid
class RAGSystem:
def __init__(self, collection_name: str = "knowledge_base"):
# Qdrant 클라이언트 초기화
self.client = QdrantClient(
url="http://localhost:6333",
api_key="YOUR_API_KEY"
)
self.collection_name = collection_name
self.embedding_model = "text-embedding-ada-002"
# 컬렉션 생성
self._create_collection()
def _create_collection(self):
"""벡터 컬렉션 생성"""
try:
self.client.create_collection(
collection_name=self.collection_name,
vectors_config=VectorParams(
size=1536, # OpenAI 임베딩 차원
distance=Distance.COSINE
),
# 하이브리드 검색을 위한 설정
optimizers_config={
"indexing_threshold": 20000,
"memmap_threshold": 50000
},
# 페이로드 인덱싱
payload_schema={
"source": {"type": "keyword"},
"timestamp": {"type": "integer"},
"category": {"type": "keyword"}
}
)
except Exception as e:
print(f"Collection already exists: {e}")
def embed_text(self, text: str) -> List[float]:
"""텍스트를 벡터로 변환"""
response = openai.Embedding.create(
model=self.embedding_model,
input=text
)
return response['data'][0]['embedding']
def add_documents(self, documents: List[Dict]):
"""문서 추가 및 인덱싱"""
points = []
for doc in documents:
# 텍스트 임베딩
embedding = self.embed_text(doc['content'])
# 포인트 생성
point = PointStruct(
id=str(uuid.uuid4()),
vector=embedding,
payload={
"content": doc['content'],
"source": doc.get('source', 'unknown'),
"timestamp": doc.get('timestamp', 0),
"category": doc.get('category', 'general'),
"metadata": doc.get('metadata', {})
}
)
points.append(point)
# 배치 업로드
self.client.upsert(
collection_name=self.collection_name,
points=points
)
def search(self, query: str, limit: int = 5, filter_dict: Dict = None):
"""시맨틱 검색"""
# 쿼리 임베딩
query_vector = self.embed_text(query)
# 필터 조건 구성
filter_conditions = None
if filter_dict:
from qdrant_client.models import Filter, FieldCondition, MatchValue
conditions = []
for key, value in filter_dict.items():
conditions.append(
FieldCondition(
key=key,
match=MatchValue(value=value)
)
)
filter_conditions = Filter(must=conditions)
# 검색 실행
results = self.client.search(
collection_name=self.collection_name,
query_vector=query_vector,
limit=limit,
query_filter=filter_conditions,
with_payload=True,
with_vectors=False
)
return results
def hybrid_search(self, query: str, keywords: List[str], limit: int = 5):
"""하이브리드 검색 (벡터 + 키워드)"""
from qdrant_client.models import Filter, FieldCondition, MatchText
# 벡터 검색
query_vector = self.embed_text(query)
# 키워드 필터
keyword_conditions = []
for keyword in keywords:
keyword_conditions.append(
FieldCondition(
key="content",
match=MatchText(text=keyword)
)
)
filter_conditions = Filter(should=keyword_conditions)
# 하이브리드 검색 실행
results = self.client.search(
collection_name=self.collection_name,
query_vector=query_vector,
limit=limit * 2, # 더 많이 가져와서 재순위화
query_filter=filter_conditions
)
# 점수 재계산 (벡터 유사도 + 키워드 매칭)
for result in results:
keyword_score = sum(
1 for keyword in keywords
if keyword.lower() in result.payload['content'].lower()
)
result.score = result.score * 0.7 + keyword_score * 0.3
# 재정렬 후 반환
results.sort(key=lambda x: x.score, reverse=True)
return results[:limit]
def generate_answer(self, query: str, context_limit: int = 3):
"""RAG 기반 답변 생성"""
# 관련 문서 검색
search_results = self.search(query, limit=context_limit)
# 컨텍스트 구성
context = "\n\n".join([
f"Source {i+1}: {result.payload['content']}"
for i, result in enumerate(search_results)
])
# 프롬프트 구성
prompt = f"""
Based on the following context, answer the question.
If the answer is not in the context, say "I don't have enough information."
Context:
{context}
Question: {query}
Answer:
"""
# LLM으로 답변 생성
response = openai.ChatCompletion.create(
model="gpt-4",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": prompt}
],
temperature=0.7
)
return {
"answer": response.choices[0].message.content,
"sources": [
{
"content": result.payload['content'],
"source": result.payload['source'],
"score": result.score
}
for result in search_results
]
}
# 사용 예시
rag = RAGSystem()
# 문서 추가
documents = [
{
"content": "Kubernetes is an open-source container orchestration platform...",
"source": "k8s-docs",
"category": "technology"
},
{
"content": "Docker is a platform for developing, shipping, and running applications...",
"source": "docker-docs",
"category": "technology"
}
]
rag.add_documents(documents)
# RAG 질의응답
result = rag.generate_answer("What is Kubernetes used for?")
print(result['answer'])
🖼️ 이미지 검색 시스템
추천: Weaviate
# Weaviate를 사용한 멀티모달 검색
import weaviate
import weaviate.classes as wvc
from weaviate.classes.config import Configure, Property, DataType
import base64
from PIL import Image
import requests
import io
class ImageSearchSystem:
def __init__(self):
# Weaviate 클라이언트
self.client = weaviate.Client(
url="http://localhost:8080",
additional_headers={
"X-OpenAI-Api-Key": "YOUR_OPENAI_KEY"
}
)
# 스키마 생성
self._create_schema()
def _create_schema(self):
"""멀티모달 스키마 정의"""
schema = {
"classes": [{
"class": "ImageData",
"description": "Multimodal image and text data",
"vectorizer": "multi2vec-clip",
"moduleConfig": {
"multi2vec-clip": {
"imageFields": ["image"],
"textFields": ["description", "tags"]
}
},
"properties": [
{
"name": "image",
"dataType": ["blob"],
"description": "Image data"
},
{
"name": "description",
"dataType": ["text"],
"description": "Image description"
},
{
"name": "tags",
"dataType": ["text[]"],
"description": "Image tags"
},
{
"name": "url",
"dataType": ["string"],
"description": "Image URL"
},
{
"name": "metadata",
"dataType": ["object"],
"description": "Additional metadata"
}
]
}]
}
try:
self.client.schema.create(schema)
except Exception as e:
print(f"Schema already exists: {e}")
def add_image(self, image_path: str, description: str, tags: List[str]):
"""이미지 추가"""
# 이미지를 base64로 인코딩
with open(image_path, "rb") as f:
image_data = base64.b64encode(f.read()).decode()
# 이미지 메타데이터 추출
img = Image.open(image_path)
metadata = {
"width": img.width,
"height": img.height,
"format": img.format,
"mode": img.mode
}
# Weaviate에 추가
data_object = {
"image": image_data,
"description": description,
"tags": tags,
"url": image_path,
"metadata": metadata
}
self.client.data_object.create(
data_object=data_object,
class_name="ImageData"
)
def search_by_text(self, query: str, limit: int = 10):
"""텍스트로 이미지 검색"""
result = (
self.client.query
.get("ImageData", ["description", "tags", "url", "metadata"])
.with_near_text({"concepts": [query]})
.with_limit(limit)
.with_additional(["distance", "id"])
.do()
)
return result["data"]["Get"]["ImageData"]
def search_by_image(self, image_path: str, limit: int = 10):
"""이미지로 유사 이미지 검색"""
# 이미지를 base64로 인코딩
with open(image_path, "rb") as f:
image_data = base64.b64encode(f.read()).decode()
result = (
self.client.query
.get("ImageData", ["description", "tags", "url", "metadata"])
.with_near_image({"image": image_data})
.with_limit(limit)
.with_additional(["distance", "id"])
.do()
)
return result["data"]["Get"]["ImageData"]
def hybrid_search(self, text_query: str = None, image_path: str = None,
filters: Dict = None, limit: int = 10):
"""하이브리드 검색 (텍스트 + 이미지 + 필터)"""
query = self.client.query.get(
"ImageData",
["description", "tags", "url", "metadata"]
)
# 텍스트 검색 조건
if text_query:
query = query.with_near_text({"concepts": [text_query]})
# 이미지 검색 조건
if image_path:
with open(image_path, "rb") as f:
image_data = base64.b64encode(f.read()).decode()
query = query.with_near_image({"image": image_data})
# 필터 조건
if filters:
where_filter = {
"operator": "And",
"operands": []
}
for key, value in filters.items():
if isinstance(value, list):
# 태그 필터
where_filter["operands"].append({
"path": [key],
"operator": "ContainsAny",
"valueTextArray": value
})
else:
# 일반 필터
where_filter["operands"].append({
"path": [key],
"operator": "Equal",
"valueText": value
})
query = query.with_where(where_filter)
# 검색 실행
result = (
query
.with_limit(limit)
.with_additional(["distance", "id", "vector"])
.do()
)
return result["data"]["Get"]["ImageData"]
📊 실시간 추천 시스템
추천: Pinecone
# Pinecone을 사용한 실시간 추천
import pinecone
import numpy as np
from typing import List, Dict, Tuple
import redis
import json
from datetime import datetime, timedelta
class RealtimeRecommender:
def __init__(self, index_name: str = "recommendations"):
# Pinecone 초기화
pinecone.init(
api_key="YOUR_API_KEY",
environment="us-west1-gcp"
)
# 인덱스 생성 또는 연결
self.index_name = index_name
self._setup_index()
self.index = pinecone.Index(index_name)
# Redis for caching
self.redis = redis.Redis(
host='localhost',
port=6379,
decode_responses=True
)
def _setup_index(self):
"""Pinecone 인덱스 설정"""
if self.index_name not in pinecone.list_indexes():
pinecone.create_index(
name=self.index_name,
dimension=768, # 임베딩 차원
metric="cosine",
metadata_config={
"indexed": ["category", "timestamp", "popularity"]
},
replicas=2, # 고가용성
pod_type="p1.x1" # 성능 최적화
)
def add_items(self, items: List[Dict]):
"""아이템 추가 (상품, 콘텐츠 등)"""
vectors = []
for item in items:
# 특징 벡터 생성 (실제로는 ML 모델 사용)
features = self._extract_features(item)
embedding = self._generate_embedding(features)
vector = (
item['id'],
embedding.tolist(),
{
"title": item['title'],
"category": item['category'],
"price": item.get('price', 0),
"popularity": item.get('popularity', 0),
"timestamp": int(datetime.now().timestamp()),
"attributes": json.dumps(item.get('attributes', {}))
}
)
vectors.append(vector)
# 배치 업서트
self.index.upsert(vectors=vectors, batch_size=100)
def get_recommendations(self, user_id: str, limit: int = 10,
category: str = None) -> List[Dict]:
"""사용자별 추천 생성"""
# 캐시 확인
cache_key = f"recs:{user_id}:{category or 'all'}:{limit}"
cached = self.redis.get(cache_key)
if cached:
return json.loads(cached)
# 사용자 프로필 벡터 가져오기
user_vector = self._get_user_vector(user_id)
# 필터 조건
filter_dict = {}
if category:
filter_dict["category"] = {"$eq": category}
# 최신 아이템 우선
week_ago = int((datetime.now() - timedelta(days=7)).timestamp())
filter_dict["timestamp"] = {"$gte": week_ago}
# 벡터 검색
results = self.index.query(
vector=user_vector.tolist(),
top_k=limit * 2, # 더 많이 가져와서 후처리
include_metadata=True,
filter=filter_dict
)
# 후처리 및 재순위화
recommendations = self._rerank_results(
results.matches,
user_id,
limit
)
# 캐시 저장 (5분)
self.redis.setex(
cache_key,
300,
json.dumps(recommendations)
)
return recommendations
def _get_user_vector(self, user_id: str) -> np.ndarray:
"""사용자 선호도 벡터 생성"""
# 사용자 상호작용 히스토리
interactions = self._get_user_interactions(user_id)
if not interactions:
# 콜드 스타트: 평균 벡터 반환
return self._get_average_vector()
# 상호작용한 아이템들의 벡터 가져오기
item_ids = [i['item_id'] for i in interactions]
item_vectors = self.index.fetch(ids=item_ids)
# 가중 평균 계산
weights = []
vectors = []
for interaction in interactions:
item_id = interaction['item_id']
if item_id in item_vectors.vectors:
vector = item_vectors.vectors[item_id].values
# 상호작용 타입별 가중치
weight = {
'view': 1.0,
'like': 2.0,
'purchase': 3.0,
'share': 2.5
}.get(interaction['type'], 1.0)
# 시간 감쇠 적용
days_ago = (datetime.now() -
datetime.fromisoformat(interaction['timestamp'])).days
time_decay = np.exp(-days_ago / 30) # 30일 반감기
weights.append(weight * time_decay)
vectors.append(vector)
# 가중 평균
weights = np.array(weights)
vectors = np.array(vectors)
user_vector = np.average(vectors, axis=0, weights=weights)
# 정규화
user_vector = user_vector / np.linalg.norm(user_vector)
return user_vector
def _rerank_results(self, matches: List, user_id: str,
limit: int) -> List[Dict]:
"""결과 재순위화"""
recommendations = []
# 사용자가 이미 본 아이템 필터링
seen_items = self._get_seen_items(user_id)
for match in matches:
if match.id in seen_items:
continue
# 점수 조정
score = match.score
metadata = match.metadata
# 인기도 부스트
popularity_boost = metadata.get('popularity', 0) * 0.1
# 다양성 페널티 (같은 카테고리 중복 방지)
category = metadata.get('category')
category_count = sum(
1 for r in recommendations
if r['category'] == category
)
diversity_penalty = category_count * 0.05
final_score = score + popularity_boost - diversity_penalty
recommendations.append({
'id': match.id,
'title': metadata.get('title'),
'category': category,
'score': final_score,
'original_score': score,
'metadata': json.loads(metadata.get('attributes', '{}'))
})
# 최종 정렬
recommendations.sort(key=lambda x: x['score'], reverse=True)
return recommendations[:limit]
def update_interaction(self, user_id: str, item_id: str,
interaction_type: str):
"""사용자 상호작용 업데이트"""
# Redis에 저장
key = f"interactions:{user_id}"
interaction = {
'item_id': item_id,
'type': interaction_type,
'timestamp': datetime.now().isoformat()
}
self.redis.lpush(key, json.dumps(interaction))
self.redis.ltrim(key, 0, 999) # 최근 1000개만 유지
# 캐시 무효화
pattern = f"recs:{user_id}:*"
for key in self.redis.scan_iter(match=pattern):
self.redis.delete(key)
# 사용 예시
recommender = RealtimeRecommender()
# 아이템 추가
items = [
{
'id': 'item_001',
'title': 'Python Programming Book',
'category': 'books',
'price': 29.99,
'popularity': 85,
'attributes': {
'author': 'John Doe',
'pages': 450,
'level': 'intermediate'
}
}
]
recommender.add_items(items)
# 추천 생성
recommendations = recommender.get_recommendations(
user_id='user_123',
limit=10,
category='books'
)
# 상호작용 기록
recommender.update_interaction('user_123', 'item_001', 'purchase')
🏢 실제 기업 사례
Pinecone 사용
- Gong: 영업 대화 분석
- Clubhouse: 오디오 콘텐츠 추천
- Canvas: 디자인 템플릿 검색
Qdrant 사용
- Dust: AI 어시스턴트
- Picsart: 이미지 검색
- 여러 스타트업: RAG 시스템
Weaviate 사용
- Stack Overflow: 질문 검색
- Instabase: 문서 이해
- 여러 미디어 기업: 콘텐츠 검색
Milvus 사용
- eBay: 상품 추천
- Walmart: 시각적 검색
- 중국 대형 플랫폼: 대규모 검색
🔧 통합 및 도구
LangChain 통합
# 모든 주요 벡터 DB 지원
from langchain.vectorstores import Pinecone, Qdrant, Weaviate, Milvus
vectorstore = Qdrant(
client=client,
collection_name="documents",
embeddings=embeddings
)
임베딩 모델 지원
| 벡터 DB | OpenAI | Cohere | HuggingFace | Custom | | ------------ | ------ | ------ | ----------- | ------ | | Pinecone | ✅ | ✅ | ✅ | ✅ | | Qdrant | ✅ | ✅ | ✅ | ✅ | | Weaviate | ✅ | ✅ | ✅ | ✅ | | Milvus | ✅ | ✅ | ✅ | ✅ |
💰 비용 최적화 전략
비용 절감 방법
- 압축: 벡터 양자화 사용
- 필터링: 메타데이터로 검색 범위 축소
- 캐싱: 자주 요청되는 쿼리 캐시
- 배치 처리: 실시간이 필요 없는 작업은 배치로
오픈소스 대안
- 작은 규모: Chroma, LanceDB
- 중간 규모: Qdrant, Weaviate
- 대규모: Milvus + 자체 인프라
🎯 선택 가이드
Pinecone 선택 시
✅ 완전 관리형 원함
✅ 빠른 시작 필요
✅ 실시간 업데이트 중요
✅ 운영 부담 최소화
❌ 비용 민감
❌ 온프레미스 필수
Qdrant 선택 시
✅ 고성능 필요
✅ 유연한 배포 옵션
✅ 하이브리드 검색
✅ 비용 효율성
❌ 완전 관리형 필요
❌ 대규모 지원 필요
Weaviate 선택 시
✅ 시맨틱 검색 중심
✅ GraphQL 선호
✅ 멀티모달 검색
✅ 모듈식 아키텍처
❌ 단순 벡터 검색만
❌ 최고 성능 필요
Milvus 선택 시
✅ 대규모 데이터
✅ 높은 처리량
✅ GPU 가속 필요
✅ 중국 시장
❌ 간단한 설정
❌ 작은 규모
pgvector 선택 시
✅ PostgreSQL 사용 중
✅ 간단한 통합
✅ ACID 필요
✅ 작은-중간 규모
❌ 최고 성능
❌ 대규모 확장
📚 추가 리소스
공식 문서
벤치마크 및 비교
- ANN Benchmarks
- VectorDBBench
- 각 벤더의 성능 리포트
한국 커뮤니티
- AI Korea
- MLOps Korea
- Vector DB 사용자 모임
💡 핵심 조언: 벡터 데이터베이스는 "AI 애플리케이션의 메모리"입니다. 작은 프로젝트는 Chroma나 pgvector로 시작하고, 프로덕션은 Qdrant, 대규모는 Milvus를 고려하세요. 관리 부담을 줄이고 싶다면 Pinecone이 좋은 선택입니다. 중요한 것은 임베딩 품질과 검색 전략입니다.