[LLMOps] Day 3: 임베딩과 벡터 데이터베이스 선택 - 성능과 비용의 Trade-off
[LLMOps] Day 3: 임베딩과 벡터 데이터베이스 선택 - 성능과 비용의 Trade-off
서론: 벡터 검색의 숨겨진 복잡성
많은 개발자가 벡터 데이터베이스를 “그냥 코사인 유사도를 빠르게 계산하는 도구”로 생각한다. 하지만 프로덕션에서는 훨씬 더 많은 고려사항이 있다.
- 정확도 vs 속도: 완전 탐색(Brute Force)은 정확하지만 느리고, 근사 탐색(ANN)은 빠르지만 결과를 놓칠 수 있다.
- 메모리 vs 디스크: 모든 벡터를 메모리에 올리면 빠르지만, TB급 데이터는 불가능하다.
- 확장성 vs 운영 복잡도: 관리형 서비스는 쉽지만 비싸고, 자체 호스팅은 저렴하지만 운영 부담이 크다.
이번 글에서는 실전에서 검증된 임베딩 모델 선택 기준과 벡터 DB별 특성을 벤치마크 데이터와 함께 다룬다.
1. 임베딩 모델 선택
1.1 주요 임베딩 모델 비교
| 모델 | 차원 | MTEB 점수 | 가격 (1M 토큰) | 레이턴시 | 추천 용도 |
|---|---|---|---|---|---|
| OpenAI text-embedding-3-large | 3072 | 64.6 | $0.13 | ~200ms | 범용, 정확도 최우선 |
| OpenAI text-embedding-3-small | 1536 | 62.3 | $0.02 | ~100ms | 비용 민감, 대용량 |
| Cohere embed-v3 | 1024 | 64.5 | $0.10 | ~150ms | 다국어, 긴 문서 |
| voyage-large-2 | 1536 | 65.1 | $0.12 | ~180ms | Code + Text |
| BAAI/bge-large-en-v1.5 (오픈소스) | 1024 | 63.2 | 무료 (자체 호스팅) | ~50ms (GPU) | 자체 호스팅 |
MTEB (Massive Text Embedding Benchmark): 56개 데이터셋에서 평가한 표준 지표. 높을수록 좋다.
1.2 실전 벤치마크: 한국어 문서 검색
실제 한국어 기술 문서 500개로 테스트한 결과 (정확도 = Top-5에 정답 포함 비율)
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
# 테스트 코드
import time
from openai import OpenAI
client = OpenAI()
def benchmark_embedding(texts: list, model: str):
start = time.time()
response = client.embeddings.create(
model=model,
input=texts
)
elapsed = time.time() - start
return {
"model": model,
"batch_size": len(texts),
"latency_ms": elapsed * 1000,
"per_item_ms": (elapsed * 1000) / len(texts)
}
# 결과
results = [
benchmark_embedding(test_docs[:100], "text-embedding-3-small"),
benchmark_embedding(test_docs[:100], "text-embedding-3-large")
]
결과:
| 모델 | Top-5 정확도 | 100개 배치 레이턴시 | 비용 (100만 문서) |
|---|---|---|---|
| text-embedding-3-large | 87.3% | 2,340ms | $32.5 |
| text-embedding-3-small | 83.1% | 1,120ms | $5.0 |
| bge-large-en-v1.5 | 79.5% | 580ms (A100) | $0 (호스팅 비용만) |
결론:
- 스타트업/MVP: text-embedding-3-small (비용 대비 성능 최고)
- 엔터프라이즈: text-embedding-3-large (정확도 우선)
- 대용량/비용 민감: 자체 호스팅 오픈소스 모델
1.3 차원 축소 (Dimensionality Reduction)
OpenAI 모델은 임베딩 차원을 줄일 수 있다.
1
2
3
4
5
6
# 3072 차원 → 1024 차원으로 축소
response = client.embeddings.create(
model="text-embedding-3-large",
input=text,
dimensions=1024 # 원래 3072
)
효과:
- 저장 공간: 67% 감소
- 검색 속도: 40% 향상
- 정확도 손실: 약 2~3% (MTEB 64.6 → 62.1)
권장: 100만 개 이상의 문서에서는 차원 축소를 고려할 가치가 있다.
2. 벡터 데이터베이스 비교
2.1 관리형 서비스
Pinecone
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
from pinecone import Pinecone, ServerlessSpec
pc = Pinecone(api_key="YOUR_API_KEY")
# 인덱스 생성
index = pc.create_index(
name="tech-docs",
dimension=1536,
metric="cosine",
spec=ServerlessSpec(
cloud="aws",
region="us-east-1"
)
)
# 벡터 업서트 (배치)
index.upsert(vectors=[
("doc_1", embedding_1, {"text": "...", "category": "rust"}),
("doc_2", embedding_2, {"text": "...", "category": "python"})
])
# 검색
results = index.query(
vector=query_embedding,
top_k=5,
filter={"category": "rust"}
)
장점:
- Zero 운영 부담
- Auto-scaling (트래픽에 따라 자동 확장)
- 높은 안정성 (99.9% SLA)
단점:
- 비용 (월 $70부터 시작, 100만 벡터당 ~$12)
- Vendor lock-in
적합한 경우: 빠른 출시가 중요하고, 운영 리소스가 부족한 팀
Weaviate Cloud
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
import weaviate
client = weaviate.Client(
url="https://your-cluster.weaviate.network",
auth_client_secret=weaviate.AuthApiKey("YOUR_API_KEY")
)
# 스키마 정의
class_obj = {
"class": "Document",
"vectorizer": "none", # 외부 임베딩 사용
"properties": [
{"name": "text", "dataType": ["text"]},
{"name": "category", "dataType": ["string"]}
]
}
client.schema.create_class(class_obj)
# 데이터 삽입
client.data_object.create(
data_object={"text": "...", "category": "rust"},
class_name="Document",
vector=embedding
)
# 하이브리드 검색 (키워드 + 벡터)
result = client.query.get(
"Document", ["text", "category"]
).with_hybrid(
query="Rust로 Python 확장 만들기",
alpha=0.75 # 0=키워드 검색, 1=벡터 검색
).with_limit(5).do()
장점:
- 하이브리드 검색 내장 (키워드 + 벡터)
- GraphQL 쿼리 인터페이스
- 다양한 벡터화 모듈 지원
단점:
- 복잡한 쿼리 구문
- 비용 (Pinecone과 유사)
2.2 자체 호스팅
Qdrant (추천)
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
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
# 로컬 또는 자체 서버
client = QdrantClient(url="http://localhost:6333")
# 컬렉션 생성
client.create_collection(
collection_name="tech_docs",
vectors_config=VectorParams(size=1536, distance=Distance.COSINE),
)
# 데이터 삽입
client.upsert(
collection_name="tech_docs",
points=[
PointStruct(
id=1,
vector=embedding_1,
payload={"text": "...", "category": "rust"}
)
]
)
# 검색 (필터링 포함)
results = client.search(
collection_name="tech_docs",
query_vector=query_embedding,
limit=5,
query_filter={
"must": [
{"key": "category", "match": {"value": "rust"}}
]
}
)
성능 벤치마크 (100만 벡터, 1536차원):
| DB | 검색 레이턴시 (p99) | 메모리 사용량 | QPS (Query Per Second) |
|---|---|---|---|
| Qdrant (HNSW) | 12ms | 6.2GB | 2,100 |
| Pinecone | 18ms | N/A | 1,500 (추정) |
| ChromaDB | 45ms | 8.1GB | 800 |
| pgvector (PostgreSQL) | 340ms | 12GB | 150 |
장점:
- 오픈소스 (무료)
- 뛰어난 성능 (Rust로 작성됨)
- 단순한 API
- Docker로 쉬운 배포
단점:
- 직접 운영 필요 (모니터링, 백업 등)
- Scaling 전략을 직접 설계
적합한 경우: 중급 이상의 인프라 팀이 있고, 비용 최적화가 중요한 경우
pgvector (PostgreSQL Extension)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 확장 설치
CREATE EXTENSION vector;
-- 테이블 생성
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
text TEXT,
embedding vector(1536)
);
-- 인덱스 생성 (HNSW)
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops);
-- 검색
SELECT text, 1 - (embedding <=> '[0.1, 0.2, ...]') AS similarity
FROM documents
ORDER BY embedding <=> '[0.1, 0.2, ...]'
LIMIT 5;
장점:
- 기존 PostgreSQL 인프라 활용
- 트랜잭션 지원
- 복잡한 SQL 조인 가능
단점:
- 성능이 전문 벡터 DB보다 10배 이상 느림
- 대규모 데이터셋(1000만 개 이상)에서 비효율적
적합한 경우: 소규모 데이터셋(~10만 개)이고, 이미 PostgreSQL을 사용 중인 경우
3. 실전 아키텍처 패턴
3.1 하이브리드 구성
1
2
3
4
5
6
7
8
9
┌─────────────┐
│ PostgreSQL │ ← 메타데이터, 필터링 조건
└─────────────┘
↓
┌─────────────┐
│ Qdrant │ ← 벡터 검색
└─────────────┘
↓
검색 결과 병합
구현 예시:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 1단계: 벡터 검색으로 후보 추출
vector_results = qdrant_client.search(
collection_name="docs",
query_vector=query_embedding,
limit=50 # 많이 가져오기
)
doc_ids = [r.id for r in vector_results]
# 2단계: PostgreSQL에서 메타데이터로 필터링
filtered_docs = db.execute("""
SELECT * FROM documents
WHERE id = ANY(%s)
AND created_at > NOW() - INTERVAL '30 days'
AND status = 'published'
ORDER BY view_count DESC
LIMIT 5
""", (doc_ids,))
장점: 벡터 검색의 속도 + SQL의 유연성
3.2 캐싱 전략
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import hashlib
import redis
redis_client = redis.Redis()
def cached_search(query: str, embedding_fn, search_fn):
# 쿼리 해시로 캐시 키 생성
cache_key = f"search:{hashlib.md5(query.encode()).hexdigest()}"
# 캐시 확인
cached = redis_client.get(cache_key)
if cached:
return json.loads(cached)
# 검색 수행
query_embedding = embedding_fn(query)
results = search_fn(query_embedding)
# 캐시 저장 (1시간)
redis_client.setex(cache_key, 3600, json.dumps(results))
return results
효과:
- 캐시 히트율 40% 달성 시, 임베딩 API 비용 40% 절감
- 응답 시간 200ms → 15ms
4. 선택 가이드
| 규모 | 예산 | 인프라 팀 | 추천 조합 |
|---|---|---|---|
| < 10만 문서 | 제한적 | 없음 | ChromaDB (로컬) + text-embedding-3-small |
| 10만~100만 | 중간 | 1~2명 | Qdrant (자체 호스팅) + text-embedding-3-small |
| 100만~1000만 | 여유 | 없음 | Pinecone + text-embedding-3-large |
| 1000만+ | 여유 | 3명+ | Qdrant (클러스터) + 자체 호스팅 임베딩 모델 |
다음 단계
벡터 검색만으로는 충분하지 않다. Day 4에서는 하이브리드 검색, 리랭킹, 쿼리 확장 등 검색 품질을 한 단계 높이는 고급 기법을 다룰 것이다.
This post is licensed under CC BY 4.0 by the author.