Skip to content

필사 모드: Qdrant 벡터DB 운영 및 사용 방법 총정리 — 컬렉션 설계부터 RAG 연동까지

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며

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

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

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. 흔한 실수

1. **페이로드 인덱스 없이 필터 검색 사용**: 페이로드 인덱스가 없으면 모든 포인트를 순차 스캔합니다. 필터를 쓰는 필드에는 반드시 인덱스를 생성하세요.

2. **HNSW `ef` 값을 너무 낮게 설정**: 기본값(128)보다 낮추면 recall이 급격히 떨어집니다. 성능이 충분하다면 기본값을 유지하세요.

3. **벡터 차원과 모델 불일치**: 컬렉션 생성 시 `size`와 임베딩 모델의 차원이 다르면 upsert 시 에러가 발생합니다. `text-embedding-3-small`은 1536, `text-embedding-3-large`는 3072입니다.

4. **배치 크기를 너무 크게 설정**: 한 번에 10,000건 이상을 upsert하면 메모리 스파이크가 발생할 수 있습니다. 100~500건 단위로 배치 처리하세요.

5. **양자화 적용 후 테스트 생략**: Scalar Quantization은 recall 손실이 적지만, Binary Quantization은 반드시 recall 벤치마크를 수행해야 합니다.

6. **단일 컬렉션에 모든 데이터 저장**: 서로 다른 임베딩 모델이나 차원을 사용하는 데이터를 하나의 컬렉션에 넣으면 검색 품질이 저하됩니다. 용도별로 컬렉션을 분리하세요.

7. **스냅샷 백업 미설정**: WAL만으로는 디스크 장애 시 복구가 불가합니다. 정기적인 스냅샷을 외부 스토리지(S3 등)에 보관하세요.

8. **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 메트릭 모니터링과 정기 스냅샷을 반드시 설정하고, 트래픽 증가에 대비해 클러스터 모드와 샤딩 전략을 미리 계획해 두는 것이 중요합니다.

퀴즈

Qdrant는 Rust로 작성되었습니다. Rust는 가비지 컬렉터 없이 메모리 안전성을 보장하므로, 높은

처리량과 낮은 지연 시간을 동시에 달성할 수 있습니다. 이는 대규모 벡터 검색 워크로드에서 특히

유리합니다.

Q2: Cosine 거리와 Dot Product 거리 메트릭의 차이점은 무엇이며, 언제 Dot Product를 선택해야

하나요?

Cosine 거리는 두 벡터의 방향 유사도를 측정하고 크기를 정규화합니다. Dot Product는 정규화 없이

내적을 계산합니다. 이미 정규화된 벡터(예: OpenAI 임베딩)를 사용할 때는 두 메트릭의 결과가

동일하므로, 연산이 더 빠른 Dot Product를 선택하는 것이 유리합니다.

Q3: HNSW 인덱스의 `m` 파라미터는 무엇을 의미하며, 값을 높이면 어떤 트레이드오프가 발생하나요?

`m` 파라미터는 HNSW 그래프에서 각 노드의 연결(이웃) 수를 결정합니다. 값을 높이면 검색

recall(정확도)이 향상되지만, 인덱스가 차지하는 메모리가 증가하고 인덱스 빌드 시간이 길어집니다.

일반적으로 8~64 범위에서 설정합니다.

Scalar Quantization은 float32를 int8로 변환하여 메모리를 약 4배 절감하며, recall 손실이 1%

미만으로 일반적인 텍스트 검색에 적합합니다. Binary Quantization은 float32를 1-bit로 변환하여

메모리를 32배 절감하지만, recall 손실이 크므로 반드시 oversampling과 벤치마크가 필요합니다.

고차원(1536+) 벡터에서만 사용을 권장합니다.

페이로드 인덱스가 없으면 Qdrant는 필터 조건을 평가하기 위해 모든 포인트의 페이로드를 순차적으로

스캔합니다. 데이터 규모가 커질수록 검색 지연이 선형적으로 증가하여 프로덕션 환경에서 심각한 성능

저하를 초래합니다.

Qdrant의 Sparse Vector 기능을 사용하여 BM25 스타일의 키워드 검색과 Dense 벡터 검색을 결합합니다.

컬렉션에 dense와 sparse 벡터를 함께 설정하고, Prefetch로 각 검색 결과를 가져온 뒤 Reciprocal Rank

Fusion(RRF) 등의 fusion 전략으로 결과를 병합합니다.

클러스터 모드에서 데이터는 Shard 단위로 여러 노드에 분산 저장됩니다. 메타데이터 일관성은 Raft 합의

프로토콜로 유지하며, 각 샤드는 여러 노드에 복제본(replica)을 두어 고가용성을 보장합니다.

Q8: REST API 대신 gRPC를 사용하면 어떤 이점이 있으며, Python 클라이언트에서 어떻게 설정하나요?

gRPC는 Protocol Buffers 기반의 바이너리 직렬화를 사용하여 REST(JSON) 대비 2~3배 빠른 처리 성능을

보입니다. Python 클라이언트에서는 QdrantClient 생성 시 `grpc_port=6334`와 `prefer_grpc=True`

옵션을 설정하면 자동으로 gRPC를 사용합니다.

현재 단락 (1/272)

LLM 기반 애플리케이션이 확산되면서 벡터 데이터베이스는 이제 선택이 아닌 필수 인프라가 되었습니다. 그중 **Qdrant**는 Rust로 작성되어 뛰어난 성능을 보여주고, gRP...

작성 글자: 0원문 글자: 11,205작성 단락: 0/272