- Authors
- Name
- 들어가며
- 1. 핵심 개념
- 2. 아키텍처
- 3. 컬렉션 설계 및 인덱스 전략
- 4. CRUD 및 검색
- 5. 필터링과 페이로드
- 6. RAG 파이프라인 연동
- 7. 운영 체크리스트
- 8. 흔한 실수
- 9. 요약
- 퀴즈
들어가며
LLM 기반 애플리케이션이 확산되면서 벡터 데이터베이스는 이제 선택이 아닌 필수 인프라가 되었습니다. 그중 Qdrant는 Rust로 작성되어 뛰어난 성능을 보여주고, gRPC와 REST API를 모두 지원하며, 페이로드 필터링과 멀티테넌시 등 프로덕션에 필요한 기능을 갖추고 있습니다. 이 글에서는 Qdrant를 실서비스에 도입하려는 분들을 위해 컬렉션 설계, CRUD, 필터링, RAG 연동, 운영 모니터링까지 실전 중심으로 정리합니다.
1. 핵심 개념
벡터 데이터베이스란?
벡터 데이터베이스는 고차원 벡터(임베딩)를 저장하고, **유사도 검색(Similarity Search)**을 밀리초 단위로 수행하는 전문 데이터베이스입니다. 텍스트, 이미지, 오디오 등을 임베딩 모델로 변환한 벡터를 저장한 뒤, 쿼리 벡터와 가장 가까운 벡터를 찾아 반환합니다.
Qdrant의 특징
- Rust 기반: 메모리 안전성과 높은 처리량을 동시에 달성
- 페이로드(Payload): 벡터에 JSON 메타데이터를 함께 저장하여 필터링 가능
- 멀티테넌시: 컬렉션 내에서 테넌트별 데이터 격리 지원
- 양자화(Quantization): 메모리 사용량을 최대 4배 절감
- 분산 모드: Raft 합의 프로토콜 기반 클러스터 구성
거리 메트릭 비교
| 메트릭 | 수식 | 사용처 | 범위 |
|---|---|---|---|
| Cosine | 1 - cos(a, b) | 텍스트 임베딩 (OpenAI, Cohere) | 0 ~ 2 |
| Euclid | ||a - b|| | 이미지 특징 벡터, 좌표 기반 | 0 ~ +inf |
| Dot Product | -a . b | 정규화된 임베딩, 추천 시스템 | -inf ~ +inf |
| Manhattan | sum(|a_i - b_i|) | 희소 벡터, 특수 도메인 | 0 ~ +inf |
팁: OpenAI
text-embedding-3-small/large는 정규화된 벡터를 반환하므로 Cosine과 Dot Product가 동일한 결과를 냅니다. 이 경우 Dot Product가 연산이 더 빠릅니다.
2. 아키텍처
핵심 구성 요소
- Segment: 벡터와 페이로드를 물리적으로 분리 저장하는 단위. 병렬 검색의 기본 단위
- WAL (Write-Ahead Log): 쓰기 연산의 내구성 보장. 장애 복구 시 재생
- HNSW Index: Hierarchical Navigable Small World 그래프 기반의 ANN 인덱스
- Payload Index: 메타데이터 필터링을 위한 보조 인덱스 (keyword, integer, geo 등)
클러스터 모드
클러스터 모드에서는 Shard 단위로 데이터를 분산하고, Raft 합의 프로토콜로 메타데이터 일관성을 유지합니다. 각 샤드는 여러 노드에 복제본(replica)을 둘 수 있습니다. Docker Compose로 구성하려면 환경변수 QDRANT__CLUSTER__ENABLED=true와 P2P 포트를 설정하고, 두 번째 노드부터는 --bootstrap 플래그로 첫 번째 노드를 지정합니다.
3. 컬렉션 설계 및 인덱스 전략
컬렉션 생성
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, HnswConfigDiff, OptimizersConfigDiff
from qdrant_client.models import ScalarQuantization, ScalarQuantizationConfig, ScalarType
client = QdrantClient(host="localhost", port=6333)
client.create_collection(
collection_name="documents",
vectors_config=VectorParams(
size=1536, # OpenAI text-embedding-3-small 차원
distance=Distance.COSINE,
),
hnsw_config=HnswConfigDiff(
m=16, # 그래프 연결 수
ef_construct=128, # 인덱스 빌드 시 탐색 폭
),
optimizers_config=OptimizersConfigDiff(
indexing_threshold=20000,
),
quantization_config=ScalarQuantization(
scalar=ScalarQuantizationConfig(type=ScalarType.INT8, quantile=0.99, always_ram=True),
),
)
HNSW 파라미터 가이드
| 파라미터 | 기본값 | 권장 범위 | 설명 |
|---|---|---|---|
m | 16 | 8 ~ 64 | 그래프 연결 수. 높을수록 recall 향상, 메모리 증가 |
ef_construct | 100 | 64 ~ 512 | 인덱스 빌드 품질. 높을수록 정확하지만 느림 |
ef (검색 시) | 128 | 64 ~ 512 | 검색 시 탐색 폭. recall/latency 트레이드오프 |
양자화(Quantization) 전략
# Scalar Quantization: float32 → int8 (메모리 4배 절감)
# 일반적인 텍스트 검색에 적합, recall 손실 < 1%
# Binary Quantization: float32 → 1-bit (메모리 32배 절감)
# 매우 공격적. 고차원(1536+)에서만 사용. oversampling 필요
from qdrant_client.models import BinaryQuantization, BinaryQuantizationConfig
client.update_collection(
collection_name="documents",
quantization_config=BinaryQuantization(
binary=BinaryQuantizationConfig(
always_ram=True,
),
),
)
4. CRUD 및 검색
벡터 삽입 (Upsert)
from qdrant_client.models import PointStruct
import openai
def get_embedding(text: str) -> list[float]:
resp = openai.embeddings.create(model="text-embedding-3-small", input=text)
return resp.data[0].embedding
# 단건 upsert
client.upsert(
collection_name="documents",
points=[
PointStruct(
id=1,
vector=get_embedding("Qdrant는 Rust로 작성된 벡터DB입니다."),
payload={"title": "Qdrant 소개", "category": "database", "tenant_id": "team-alpha"},
),
],
)
# 대량 upsert — 100건씩 배치 처리
BATCH_SIZE = 100
for i in range(0, len(points), BATCH_SIZE):
client.upsert(collection_name="documents", points=points[i : i + BATCH_SIZE])
유사도 검색 (Search)
from qdrant_client.models import SearchParams
results = client.search(
collection_name="documents",
query_vector=get_embedding("벡터 데이터베이스 성능 최적화"),
limit=10,
score_threshold=0.7,
search_params=SearchParams(
hnsw_ef=128, # 검색 시 ef 값 (정확도 조절)
exact=False, # True면 brute-force (정확하지만 느림)
),
with_payload=True,
with_vectors=False, # 벡터 자체는 반환하지 않음 (응답 크기 절감)
)
for result in results:
print(f"ID: {result.id}, Score: {result.score:.4f}")
print(f" Title: {result.payload['title']}")
REST API로 검색
curl -X POST "http://localhost:6333/collections/documents/points/search" \
-H "Content-Type: application/json" \
-d '{
"vector": [0.1, 0.2, ...],
"limit": 10,
"with_payload": true,
"params": {
"hnsw_ef": 128
}
}'
업데이트 및 삭제
from qdrant_client.models import PointIdsList, FilterSelector, Filter, FieldCondition, MatchValue
# 페이로드 업데이트
client.set_payload(collection_name="documents", payload={"category": "vector-database"}, points=[1, 2, 3])
# 포인트 삭제 (ID 기반)
client.delete(collection_name="documents", points_selector=PointIdsList(points=[10, 11, 12]))
# 필터 기반 삭제
client.delete(
collection_name="documents",
points_selector=FilterSelector(
filter=Filter(must=[FieldCondition(key="category", match=MatchValue(value="deprecated"))])
),
)
5. 필터링과 페이로드
페이로드 인덱스 생성
필터링 성능을 높이려면 반드시 페이로드 인덱스를 생성해야 합니다.
from qdrant_client.models import PayloadSchemaType, TextIndexParams, TokenizerType
# 키워드 인덱스 (정확 매칭)
client.create_payload_index(collection_name="documents", field_name="category", field_schema=PayloadSchemaType.KEYWORD)
# 정수 인덱스 (범위 검색)
client.create_payload_index(collection_name="documents", field_name="page_count", field_schema=PayloadSchemaType.INTEGER)
# 텍스트 인덱스 (전문 검색 — 다국어 토크나이저)
client.create_payload_index(
collection_name="documents",
field_name="content",
field_schema=TextIndexParams(type="text", tokenizer=TokenizerType.MULTILINGUAL, min_token_len=2, max_token_len=20),
)
복합 필터 검색
from qdrant_client.models import Filter, FieldCondition, MatchValue, Range
# 카테고리가 "database"이고 페이지 수가 10 이상인 문서 중 유사도 검색
results = client.search(
collection_name="documents",
query_vector=get_embedding("벡터 인덱스 최적화"),
query_filter=Filter(
must=[
FieldCondition(
key="category",
match=MatchValue(value="database"),
),
FieldCondition(
key="page_count",
range=Range(gte=10),
),
],
must_not=[
FieldCondition(
key="status",
match=MatchValue(value="archived"),
),
],
),
limit=5,
)
하이브리드 검색 (벡터 + 텍스트)
Qdrant는 Sparse Vector를 지원하여 BM25 스타일의 키워드 검색과 벡터 검색을 결합할 수 있습니다.
from qdrant_client.models import SparseVector, SparseVectorParams, Prefetch, FusionQuery, Fusion
# dense + sparse 벡터 설정으로 컬렉션 생성
client.create_collection(
collection_name="hybrid_docs",
vectors_config={"dense": VectorParams(size=1536, distance=Distance.COSINE)},
sparse_vectors_config={"sparse": SparseVectorParams()},
)
# Reciprocal Rank Fusion 기반 하이브리드 검색
results = client.query_points(
collection_name="hybrid_docs",
prefetch=[
Prefetch(query=get_embedding("벡터 검색 성능"), using="dense", limit=20),
Prefetch(query=SparseVector(indices=[1, 42, 1337], values=[0.9, 0.3, 0.7]), using="sparse", limit=20),
],
query=FusionQuery(fusion=Fusion.RRF),
limit=10,
)
6. RAG 파이프라인 연동
LangChain 연동
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_qdrant import QdrantVectorStore
from langchain.chains import RetrievalQA
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 1. 문서 분할
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", ". ", " ", ""],
)
chunks = text_splitter.split_documents(documents)
# 2. 벡터 스토어 생성
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = QdrantVectorStore.from_documents(
documents=chunks,
embedding=embeddings,
url="http://localhost:6333",
collection_name="rag_documents",
force_recreate=True,
)
# 3. RAG 체인 구성
retriever = vectorstore.as_retriever(
search_type="similarity_score_threshold",
search_kwargs={
"k": 5,
"score_threshold": 0.7,
},
)
llm = ChatOpenAI(model="gpt-4o", temperature=0)
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=retriever,
return_source_documents=True,
)
# 4. 질문 응답
response = qa_chain.invoke({"query": "Qdrant의 HNSW 인덱스 파라미터를 어떻게 튜닝하나요?"})
print(response["result"])
LlamaIndex 연동
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings
from llama_index.vector_stores.qdrant import QdrantVectorStore
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
import qdrant_client
# 1. Qdrant 클라이언트 & 벡터 스토어
qclient = qdrant_client.QdrantClient(host="localhost", port=6333)
vector_store = QdrantVectorStore(
client=qclient,
collection_name="llamaindex_docs",
)
# 2. 설정
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
Settings.llm = OpenAI(model="gpt-4o", temperature=0)
Settings.chunk_size = 512
Settings.chunk_overlap = 50
# 3. 문서 로드 및 인덱싱
documents = SimpleDirectoryReader("./data").load_data()
index = VectorStoreIndex.from_documents(
documents,
vector_store=vector_store,
)
# 4. 쿼리 엔진
query_engine = index.as_query_engine(
similarity_top_k=5,
)
response = query_engine.query("Qdrant에서 필터링과 벡터 검색을 동시에 하는 방법은?")
print(response)
7. 운영 체크리스트
모니터링
Qdrant는 Prometheus 형식의 메트릭을 /metrics 엔드포인트로 제공합니다.
# 메트릭 확인
curl http://localhost:6333/metrics
# 주요 메트릭
# qdrant_grpc_responses_total — gRPC 요청 수
# qdrant_rest_responses_total — REST 요청 수
# qdrant_collection_points_total — 컬렉션별 포인트 수
# qdrant_collection_search_latency — 검색 지연 시간
운영 체크리스트 표
| 항목 | 확인 사항 | 빈도 |
|---|---|---|
| 메모리 | RAM 사용률 80% 미만 유지 | 매일 |
| 디스크 | 스토리지 여유 공간 20% 이상 | 매일 |
| 검색 지연 | p99 latency < 100ms | 실시간 |
| 인덱스 상태 | optimizer_status = "ok" | 매시간 |
| 스냅샷 | 자동 스냅샷 정상 생성 확인 | 매일 |
| 복제본 | 모든 샤드의 replica 상태 active | 매시간 |
| WAL 크기 | WAL 디렉토리 비정상 증가 모니터링 | 매일 |
| API 오류율 | 5xx 응답 비율 < 0.1% | 실시간 |
스냅샷 및 백업
# 스냅샷 생성 및 목록 조회
client.create_snapshot(collection_name="documents")
snapshots = client.list_snapshots(collection_name="documents")
for snap in snapshots:
print(f"Name: {snap.name}, Size: {snap.size}")
# REST API로 스냅샷 다운로드
curl -o snapshot.tar "http://localhost:6333/collections/documents/snapshots/snapshot-2026-03-09.snapshot"
# 전체 스토리지 스냅샷 (모든 컬렉션 포함)
curl -X POST "http://localhost:6333/snapshots"
8. 흔한 실수
페이로드 인덱스 없이 필터 검색 사용: 페이로드 인덱스가 없으면 모든 포인트를 순차 스캔합니다. 필터를 쓰는 필드에는 반드시 인덱스를 생성하세요.
HNSW
ef값을 너무 낮게 설정: 기본값(128)보다 낮추면 recall이 급격히 떨어집니다. 성능이 충분하다면 기본값을 유지하세요.벡터 차원과 모델 불일치: 컬렉션 생성 시
size와 임베딩 모델의 차원이 다르면 upsert 시 에러가 발생합니다.text-embedding-3-small은 1536,text-embedding-3-large는 3072입니다.배치 크기를 너무 크게 설정: 한 번에 10,000건 이상을 upsert하면 메모리 스파이크가 발생할 수 있습니다. 100~500건 단위로 배치 처리하세요.
양자화 적용 후 테스트 생략: Scalar Quantization은 recall 손실이 적지만, Binary Quantization은 반드시 recall 벤치마크를 수행해야 합니다.
단일 컬렉션에 모든 데이터 저장: 서로 다른 임베딩 모델이나 차원을 사용하는 데이터를 하나의 컬렉션에 넣으면 검색 품질이 저하됩니다. 용도별로 컬렉션을 분리하세요.
스냅샷 백업 미설정: WAL만으로는 디스크 장애 시 복구가 불가합니다. 정기적인 스냅샷을 외부 스토리지(S3 등)에 보관하세요.
gRPC 대신 REST만 사용: 대량 데이터 처리 시 gRPC가 REST 대비 2~3배 빠릅니다. Python 클라이언트는
prefer_grpc=True로 설정하면 자동으로 gRPC를 사용합니다.
# gRPC 사용 권장
client = QdrantClient(
host="localhost",
port=6333,
grpc_port=6334,
prefer_grpc=True,
)
9. 요약
Qdrant는 Rust의 성능과 풍부한 기능을 겸비한 벡터 데이터베이스로, 특히 페이로드 필터링, 양자화, 하이브리드 검색 기능이 프로덕션 RAG 파이프라인에 적합합니다. 컬렉션 설계 시 HNSW 파라미터와 양자화 전략을 신중히 선택하고, 페이로드 인덱스를 적극 활용하세요. 운영 단계에서는 Prometheus 메트릭 모니터링과 정기 스냅샷을 반드시 설정하고, 트래픽 증가에 대비해 클러스터 모드와 샤딩 전략을 미리 계획해 두는 것이 중요합니다.
퀴즈
Q1: Qdrant는 어떤 프로그래밍 언어로 작성되었으며, 이로 인해 어떤 이점이 있나요?
Qdrant는 Rust로 작성되었습니다. Rust는 가비지 컬렉터 없이 메모리 안전성을 보장하므로, 높은 처리량과 낮은 지연 시간을 동시에 달성할 수 있습니다. 이는 대규모 벡터 검색 워크로드에서 특히 유리합니다.
Q2: Cosine 거리와 Dot Product 거리 메트릭의 차이점은 무엇이며, 언제 Dot Product를 선택해야 하나요?
Cosine 거리는 두 벡터의 방향 유사도를 측정하고 크기를 정규화합니다. Dot Product는 정규화 없이 내적을 계산합니다. 이미 정규화된 벡터(예: OpenAI 임베딩)를 사용할 때는 두 메트릭의 결과가 동일하므로, 연산이 더 빠른 Dot Product를 선택하는 것이 유리합니다.
Q3: HNSW 인덱스의 m 파라미터는 무엇을 의미하며, 값을 높이면 어떤 트레이드오프가 발생하나요?
m 파라미터는 무엇을 의미하며, 값을 높이면 어떤 트레이드오프가 발생하나요?m 파라미터는 HNSW 그래프에서 각 노드의 연결(이웃) 수를 결정합니다. 값을 높이면 검색 recall(정확도)이 향상되지만, 인덱스가 차지하는 메모리가 증가하고 인덱스 빌드 시간이 길어집니다. 일반적으로 8~64 범위에서 설정합니다.
Q4: Scalar Quantization과 Binary Quantization의 차이점은 무엇인가요?
Scalar Quantization은 float32를 int8로 변환하여 메모리를 약 4배 절감하며, recall 손실이 1% 미만으로 일반적인 텍스트 검색에 적합합니다. Binary Quantization은 float32를 1-bit로 변환하여 메모리를 32배 절감하지만, recall 손실이 크므로 반드시 oversampling과 벤치마크가 필요합니다. 고차원(1536+) 벡터에서만 사용을 권장합니다.
Q5: 페이로드 인덱스를 생성하지 않으면 필터 검색 시 어떤 문제가 발생하나요?
페이로드 인덱스가 없으면 Qdrant는 필터 조건을 평가하기 위해 모든 포인트의 페이로드를 순차적으로 스캔합니다. 데이터 규모가 커질수록 검색 지연이 선형적으로 증가하여 프로덕션 환경에서 심각한 성능 저하를 초래합니다.
Q6: Qdrant에서 하이브리드 검색(벡터 + 키워드)을 구현하는 방법은 무엇인가요?
Qdrant의 Sparse Vector 기능을 사용하여 BM25 스타일의 키워드 검색과 Dense 벡터 검색을 결합합니다. 컬렉션에 dense와 sparse 벡터를 함께 설정하고, Prefetch로 각 검색 결과를 가져온 뒤 Reciprocal Rank Fusion(RRF) 등의 fusion 전략으로 결과를 병합합니다.
Q7: Qdrant 클러스터 모드에서 데이터 분산과 일관성은 어떻게 관리되나요?
클러스터 모드에서 데이터는 Shard 단위로 여러 노드에 분산 저장됩니다. 메타데이터 일관성은 Raft 합의 프로토콜로 유지하며, 각 샤드는 여러 노드에 복제본(replica)을 두어 고가용성을 보장합니다.
Q8: REST API 대신 gRPC를 사용하면 어떤 이점이 있으며, Python 클라이언트에서 어떻게 설정하나요?
gRPC는 Protocol Buffers 기반의 바이너리 직렬화를 사용하여 REST(JSON) 대비 2~3배 빠른 처리 성능을 보입니다. Python 클라이언트에서는 QdrantClient 생성 시 grpc_port=6334와 prefer_grpc=True 옵션을 설정하면 자동으로 gRPC를 사용합니다.