벡터 DB를 느리게 만드는 진짜 원인
1편에서 임베딩 생성과 기본 검색을 다뤘습니다. 실서비스 규모로 가면 정확도와 속도가 함께 무너지는 지점이 있습니다. 인덱스 전략과 하이브리드 검색이 그 해결책입니다.
인덱스 알고리즘 비교
벡터 검색의 성능은 인덱스 알고리즘 선택에서 90% 결정됩니다.
| 알고리즘 | 검색 속도 | 정확도 | 메모리 | 적합한 규모 |
|---|---|---|---|---|
| Flat (브루트포스) | 느림 | 100% | 낮음 | ~10만 건 |
| IVF | 빠름 | 95~98% | 중간 | 10만~1000만 |
| HNSW | 매우 빠름 | 97~99% | 높음 | 모든 규모 |
| ScaNN | 매우 빠름 | 97~99% | 낮음 | 1억 건 이상 |
HNSW 파라미터 튜닝
import hnswlib
import numpy as np
def build_hnsw_index(vectors: np.ndarray, dim: int) -> hnswlib.Index:
index = hnswlib.Index(space="cosine", dim=dim)
index.init_index(
max_elements=len(vectors),
ef_construction=200, # 높을수록 정확, 느린 인덱싱 (권장: 100~400)
M=16, # 연결 수. 높을수록 정확, 많은 메모리 (권장: 8~48)
)
index.add_items(vectors)
# 검색 시 ef: 높을수록 정확, 느린 쿼리 (권장: ef_construction 이상)
index.set_ef(50)
return index
# 정확도 vs 속도 트레이드오프 측정
def benchmark(index, queries, ground_truth, ef_values):
for ef in ef_values:
index.set_ef(ef)
labels, _ = index.knn_query(queries, k=10)
recall = compute_recall(labels, ground_truth)
print(f"ef={ef:3d} → recall={recall:.3f}")하이브리드 검색: 의미 + 키워드
순수 벡터 검색은 정확한 키워드·코드·고유명사에 취약합니다. BM25와 결합하면 상호보완됩니다.
from rank_bm25 import BM25Okapi
import numpy as np
class HybridSearcher:
def __init__(self, docs: list[str], embeddings: np.ndarray):
tokenized = [d.split() for d in docs]
self.bm25 = BM25Okapi(tokenized)
self.embeddings = embeddings
self.docs = docs
def search(self, query: str, query_emb: np.ndarray,
alpha: float = 0.5, top_k: int = 10) -> list:
# BM25 점수 (키워드 매칭)
bm25_scores = self.bm25.get_scores(query.split())
bm25_norm = bm25_scores / (bm25_scores.max() + 1e-9)
# 코사인 유사도 (의미 매칭)
cosine_scores = self.embeddings @ query_emb
cosine_norm = (cosine_scores + 1) / 2 # [-1,1] → [0,1]
# 가중 합산 (RRF 방식도 가능)
final = alpha * cosine_norm + (1 - alpha) * bm25_norm
top_idx = np.argsort(final)[::-1][:top_k]
return [(self.docs[i], float(final[i])) for i in top_idx]alpha 값 선택 기준
- alpha 0.7~0.9: 의미 중심 (일반 Q&A, 개념 검색)
- alpha 0.3~0.5: 키워드 중심 (코드 검색, 제품명, 날짜)
- alpha 0.5: 균형 (대부분의 RAG 유스케이스)
메타데이터 필터링으로 검색 범위 축소
# pgvector 예시 — 필터 후 벡터 검색
async def filtered_search(
query_emb: list[float],
department: str,
date_from: str,
limit: int = 10
) -> list:
return await db.fetch("""
SELECT id, title, content,
1 - (embedding <=> $1::vector) AS similarity
FROM documents
WHERE department = $2
AND created_at >= $3
AND status = 'published'
ORDER BY embedding <=> $1::vector
LIMIT $4
""", query_emb, department, date_from, limit)필터를 먼저 적용하면 검색 대상을 줄여 2~5배 속도 향상 효과가 있습니다.
임베딩 캐싱
동일 쿼리를 반복 임베딩하는 비용을 제거합니다.
import hashlib
import redis
class EmbeddingCache:
def __init__(self, ttl: int = 3600):
self.redis = redis.Redis()
self.ttl = ttl
def _key(self, text: str, model: str) -> str:
h = hashlib.sha256(f"{model}:{text}".encode()).hexdigest()[:16]
return f"emb:{h}"
async def get_or_embed(self, text: str, model: str, embed_fn) -> list[float]:
key = self._key(text, model)
cached = self.redis.get(key)
if cached:
return json.loads(cached)
emb = await embed_fn(text)
self.redis.setex(key, self.ttl, json.dumps(emb))
return emb3편에서는 멀티테넌트 격리, 증분 인덱스 갱신, 프로덕션 모니터링으로 Vector DB 운영을 완성합니다.
증상별 튜닝 의사결정표
운영 중 성능 문제가 생기면 아래 표에서 증상으로 시작하세요.
| 증상 | 유력 원인 | 우선 조치 |
|---|---|---|
| 정확도(recall)가 낮다 | ef_search가 낮음 / 하이브리드 미적용 | ef_search를 단계적으로 2배씩 올리며 recall-지연 곡선 측정 → 한계면 alpha 조정 |
| p99 지연이 튄다 | ef_search 과다 / 필터가 인덱스 뒤에 적용 | 메타데이터 사전 필터(pre-filter) 지원 여부 확인, ef_search 상한 재설정 |
| 메모리가 계속 증가 | HNSW 그래프(M 값) 과다 / 삭제 벡터 미정리 | M을 낮춘 재색인 검토, 주기적 compaction/재빌드 |
| 색인 시간이 너무 길다 | ef_construction 과다 | 정확도 요구와 함께 낮추기 — 색인 후 ef_search로 보완 가능 |
| 키워드 정확 일치가 안 잡힌다 | 순수 벡터 검색만 사용 | 하이브리드(BM25+벡터) 도입, alpha를 검색 로그로 튜닝 |
측정 없이 튜닝하지 마세요. 파라미터 변경 전후로 동일한 golden query 세트(대표 질의 50~100개)의 recall@k와 p95 지연을 기록하는 것이 유일하게 신뢰할 수 있는 방법입니다.
이 글은 AI 에이전트가 자료 조사와 1차 초안 작성을 담당하고, 사람 편집자가 사실관계·출처·톤과 맥락을 검토한 뒤 발행했습니다. 환경(OS·버전)에 따라 결과가 다를 수 있으니 적용 전 공식 문서를 함께 확인하세요. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.
댓글
첫 번째 댓글을 남겨보세요.