Qdrant 프로덕션 배포 — 컬렉션 설계·HNSW 튜닝·필터 최적화
페이로드 인덱스가 없으면 Qdrant는 필터 쿼리마다 전체를 훑습니다. 컬렉션 설계·HNSW ef 튜닝·필터 인덱스 선택 기준을 벡터 100만 개 기준 메모리 계산과 함께 정리했습니다.
목차
- 개요
- 컬렉션 설계 원칙
- HNSW 파라미터 이해와 튜닝
- 필터 인덱스 최적화
- 메모리 계산과 용량 계획
- 운영 환경 적용 시 고려사항
- 맺음말
개요
문제 배경
페이로드 인덱스를 선언하지 않으면 Qdrant는 필터 쿼리마다 전체 컬렉션을 브루트포스 스캔합니다. 컬렉션 설계·HNSW ef 튜닝·필터 인덱스 선택 기준을 벡터 100만 개 기준 메모리 수치와 함께 다룹니다. Qdrant는 Rust로 구현된 고성능 벡터 검색 엔진으로, 기본 설정만으로도 로컬 개발 환경에서는 만족스러운 결과를 냅니다. 그러나 실제 서비스 환경에서 벡터 수백만 개, 복잡한 메타데이터 필터, 동시 요청 수십 건을 처리하는 순간부터 이야기가 달라집니다. 응답 지연이 갑자기 수백 밀리초로 늘어나거나, 메모리 사용량이 예측보다 두 배 이상 치솟는 현상이 나타납니다.
이 글은 Qdrant를 프로덕션에서 안정적으로 운영하기 위한 세 가지 핵심 영역인 컬렉션 설계, HNSW 인덱스 튜닝, 필터 인덱스 최적화를 다룹니다. 벡터 100만 개 기준 메모리 계산, 파라미터별 성능 트레이드오프, 운영 중 자주 만나는 함정을 함께 살펴봅니다.
기존 방식의 한계
많은 팀이 Qdrant 공식 퀵스타트 예제를 그대로 복사해 프로덕션에 올립니다. 이 예제는 의도적으로 단순화되어 있기 때문에, 프로덕션 환경에서 반드시 조정이 필요한 파라미터들이 기본값으로 설정된 채 방치됩니다. 예를 들어 ef_construct의 기본값인 100은 인덱스 품질과 구축 속도 사이에서 중간값을 선택한 것이지, 특정 쿼리 부하 패턴에 최적화된 수치가 아닙니다. 페이로드 인덱스 없이 카테고리나 날짜 범위로 필터링하면 Qdrant는 전체 벡터에 대해 브루트포스 스캔을 수행하며, 이는 데이터가 늘어날수록 선형적으로 지연이 증가하는 원인이 됩니다.
더 근본적인 문제는 벡터 저장 방식입니다. 기본 설정은 모든 벡터를 메모리에 올리는 Vectors 타입으로 동작합니다. OpenAI text-embedding-3-large 기준 벡터 차원이 3072이고, float32를 사용한다면 벡터 하나당 약 12KB입니다. 100만 개면 약 12GB, 1000만 개면 120GB가 벡터 데이터만으로 필요합니다. 양자화나 온디스크 저장 전략 없이는 비용이 감당하기 어려운 수준으로 치솟습니다.
프로덕션으로 가는 길에는 기본값 설정이 만들어낸 함정들이 여럿 있습니다. 인덱스 재설계, 필터 전략, 메모리 최적화 세 방향 중 어느 하나라도 놓치면 병목이 남습니다.
컬렉션 설계 원칙
벡터 설정과 거리 메트릭 선택
컬렉션은 Qdrant에서 데이터의 최상위 단위입니다. 컬렉션을 생성할 때 결정한 벡터 차원, 거리 메트릭, 저장 방식은 이후에 변경이 어렵거나 비용이 큽니다. 가장 먼저 고려해야 할 것은 임베딩 모델이 권장하는 거리 메트릭입니다.
OpenAI 임베딩은 벡터가 이미 정규화되어 있어 코사인 유사도와 내적(Dot Product)이 동일하게 동작합니다. 이 경우 Dot을 사용하는 것이 Cosine보다 계산 비용이 낮습니다. Cosine은 내부적으로 정규화 연산을 한 번 더 수행하기 때문입니다. 반면 sentence-transformers의 일부 모델처럼 정규화가 보장되지 않는 경우에는 Cosine이 더 정확한 결과를 냅니다. Euclid는 이미지 검색처럼 절대 거리가 의미를 가질 때 사용하며, 일반적인 텍스트 유사도 검색에는 적합하지 않습니다.
벡터 차원도 트레이드오프가 있습니다. text-embedding-3-large는 최대 3072차원이지만, OpenAI는 Matryoshka Representation Learning을 적용해 차원을 256이나 512로 줄여도 품질 손실이 크지 않다고 밝히고 있습니다. 차원을 절반으로 줄이면 메모리 사용량도 절반이 됩니다. 실제 프로젝트에서는 256차원에서 시작해 검색 품질(recall@10)을 측정하며 점진적으로 차원을 늘리는 방식이 효율적입니다.
| 거리 메트릭 | 정규화 전제 | 계산 비용 | 권장 사용처 |
|---|---|---|---|
Dot |
필요 | 낮음 | OpenAI, Cohere 임베딩 |
Cosine |
불필요 | 중간 | 정규화 미보장 모델 |
Euclid |
불필요 | 중간 | 이미지, 절대 거리 검색 |
Manhattan |
불필요 | 중간 | 희소 벡터, 이상 탐지 |
페이로드 스키마와 명시적 인덱스 선언
Qdrant에서 페이로드(payload)는 각 벡터 포인트에 붙는 메타데이터입니다. 관계형 데이터베이스의 행에 딸린 컬럼과 유사하지만, 스키마를 강제하지 않아 어떤 JSON이든 저장할 수 있습니다. 문제는 이 유연성이 성능 함정으로 이어진다는 점입니다. 인덱스가 없는 페이로드 필드로 필터링하면 Qdrant는 전체 컬렉션을 순회하면서 조건을 평가합니다.
명시적 페이로드 인덱스 선언은 컬렉션 생성 직후에 하는 것이 좋습니다. 나중에 추가할 수도 있지만, 인덱스 구축 중에는 해당 컬렉션의 쓰기 처리량이 낮아질 수 있습니다. 필드 타입을 정확히 선언해야 인덱스가 최적으로 동작합니다. 숫자를 문자열로 저장하고 인덱스를 integer로 선언하면 쿼리 결과가 비어 오는 현상이 발생합니다.
페이로드 인덱스 타입이 쿼리 성능을 결정합니다. keyword는 카테고리나 태그처럼 정확한 값 매칭에 쓰이고, integer·float는 범위 필터에, datetime은 기간 조건에 최적화되어 있습니다.
저장 전략: 메모리, MMAP, 온디스크
Qdrant는 벡터 저장 방식을 세 가지로 나눕니다. in_memory는 모든 데이터를 RAM에 올립니다. mmap은 운영체제의 메모리 맵 파일을 사용해 자주 접근하는 데이터는 페이지 캐시에 유지하고 나머지는 디스크에서 읽어옵니다. on_disk는 HNSW 그래프 자체를 디스크에 저장해 메모리 사용량을 최소화합니다.
프로덕션 환경에서는 벡터와 페이로드를 분리해서 설정하는 것이 일반적입니다. 벡터는 크기가 크기 때문에 mmap으로 설정하고, 페이로드는 상대적으로 작으면서 필터 평가에 자주 접근되므로 in_memory로 유지하는 패턴이 효율적입니다. HNSW 그래프는 인덱스 탐색에 핵심 역할을 하므로 가능하면 메모리에 올리는 것이 응답 지연 측면에서 유리합니다.
벡터는 mmap, 페이로드는 메모리, HNSW 그래프는 메모리. 이 조합이 비용 대비 지연을 가장 균형 있게 맞춥니다.
HNSW 파라미터 이해와 튜닝
HNSW 알고리즘의 구조
HNSW(Hierarchical Navigable Small World)는 Qdrant가 기본으로 사용하는 근사 최근접 이웃(ANN) 인덱스 알고리즘입니다. 계층적 그래프 구조로 동작하는데, 최상위 레이어에는 적은 수의 노드가 성긴 연결을 이루고, 하위 레이어로 내려갈수록 노드가 많아지며 밀집됩니다. 검색 시에는 최상위 레이어에서 시작해 빠르게 후보 영역을 좁히고, 하위 레이어에서 정밀하게 탐색합니다.
이 구조에서 세 가지 파라미터가 성능을 결정합니다. m은 각 노드가 유지하는 최대 양방향 연결 수입니다. ef_construct는 인덱스를 구축할 때 탐색하는 후보 집합의 크기입니다. ef(또는 hnsw_ef)는 검색 시 탐색하는 후보 집합의 크기입니다.
HNSW는 최상위 레이어에서 빠르게 진입점을 찾고, 레이어 0에서 ef 크기만큼의 후보를 탐색해 최종 결과를 냅니다.
m과 ef_construct 설정 기준
m 값은 그래프의 연결 밀도를 결정합니다. 기본값은 16이며, 이 값을 높이면 검색 정확도(recall)가 올라가지만 메모리 사용량과 인덱스 구축 시간이 늘어납니다. 경험적으로 m=16은 범용적인 텍스트 검색에서 충분한 recall을 제공하고, 고정밀도가 필요한 추천 시스템이나 유사 이미지 검색에서는 m=32나 m=64를 고려합니다. 반대로 m=8은 메모리 제약이 극단적으로 엄격한 환경에서 recall 손실을 감수하고 사용합니다.
ef_construct는 인덱스를 한 번만 구축하기 때문에 서비스 중 성능에 직접 영향을 주지 않습니다. 그러나 이 값이 낮으면 인덱스 품질이 떨어져 이후 검색 시 recall이 낮아집니다. 기본값 100은 대부분의 경우 적절하지만, 데이터가 특정 클러스터에 밀집해 있거나 차원이 1000을 넘는 고차원 벡터를 다룰 때는 200 이상으로 올려 인덱스 품질을 확보하는 것이 좋습니다. 인덱스 구축은 배치 작업으로 한 번 수행되므로, 시간이 더 걸려도 품질을 우선시하는 것이 일반적인 선택입니다.
| 파라미터 | 기본값 | 높이면 | 낮추면 | 권장 조정 |
|---|---|---|---|---|
m |
16 | recall↑, 메모리↑ | recall↓, 메모리↓ | 범용 16, 고정밀 32 |
ef_construct |
100 | 인덱스 품질↑, 구축 시간↑ | 인덱스 품질↓, 구축 시간↓ | 고차원 200+ |
ef (검색) |
128 | recall↑, 지연↑ | recall↓, 지연↓ | 목표 recall에 맞게 조정 |
max_indexing_threads |
0(전체) | 구축 속도↑ | 구축 속도↓ | 운영 중 1-2로 제한 |
ef(검색)와 recall 목표 사이의 균형
검색 시 사용하는 ef 값은 쿼리마다 동적으로 변경할 수 있습니다. 이 점이 m이나 ef_construct와 근본적으로 다릅니다. 컬렉션 생성 시 기본 ef를 설정하더라도, 개별 쿼리에서 더 높은 값을 요청하면 그 요청에만 적용됩니다. 이 특성을 활용하면 일반 검색은 낮은 ef로 빠르게 처리하고, 정밀도가 중요한 재순위(reranking) 후보 수집 단계에서만 높은 ef를 사용하는 2단계 검색 전략을 구현할 수 있습니다.
recall과 지연 사이의 실제 수치는 데이터 분포에 따라 크게 달라집니다. 동일한 ef=128이라도 균일하게 분포된 임의 벡터에서는 높은 recall을 기대할 수 있지만, 특정 도메인에 편중된 클러스터 구조의 데이터에서는 목표치를 크게 밑돌 수 있습니다. 실측값은 데이터 분포에 따라 크게 달라지므로, 실제 서비스 데이터의 일부를 샘플로 추출해 recall을 직접 측정하고, 목표치(보통 0.95 이상)를 달성하는 최소 ef 값을 찾아 설정하는 과정이 필수입니다.
필터 인덱스 최적화
필터 검색의 두 가지 경로
Qdrant에서 필터를 동반한 벡터 검색은 내부적으로 두 경로 중 하나를 선택합니다. 첫 번째는 HNSW 우선 경로로, 먼저 HNSW 그래프를 탐색해 최근접 후보를 수집하고 이후 페이로드 조건으로 필터링합니다. 두 번째는 필터 우선 경로로, 페이로드 인덱스를 통해 조건에 맞는 포인트 집합을 먼저 구성한 뒤 그 안에서 벡터 유사도를 계산합니다.
어떤 경로를 선택할지는 Qdrant가 내부 카디널리티 추정을 통해 자동으로 결정합니다. 필터 조건을 통과하는 비율이 아주 작으면 필터 우선, 크면 HNSW 우선으로 동작합니다. 전환 기준은 Qdrant 내부 설정과 버전에 따라 다르므로, 수치를 외우기보다 페이로드 인덱스를 반드시 두는 것이 중요합니다. 이 판단이 올바르게 이루어지려면 페이로드 인덱스가 존재해야 합니다. 인덱스가 없으면 카디널리티 추정이 불가능하고, Qdrant는 항상 HNSW 우선 경로를 선택해 필터 후 남은 후보가 적을 때 recall이 급격히 떨어지는 문제가 생깁니다.
페이로드 인덱스가 없으면 Qdrant는 카디널리티를 추정할 수 없어 항상 HNSW 우선 경로를 택하고, 선택적 필터에서 recall이 떨어집니다.
페이로드 인덱스 타입 선택
Qdrant는 페이로드 인덱스를 필드 타입에 맞게 선언해야 합니다. keyword 타입은 역인덱스(inverted index) 구조로 저장되어 정확한 값 매칭과 다중 값(배열) 필드 모두에서 효율적으로 동작합니다. 카테고리, 태그, 상태 코드처럼 값의 종류가 제한적인 필드에 적합합니다.
integer와 float는 B-트리 기반 인덱스를 사용합니다. gt, gte, lt, lte 같은 범위 쿼리가 O(log n)으로 처리됩니다. 타임스탬프를 Unix epoch 정수로 저장하고 integer 인덱스를 선언하면 기간 필터가 효율적으로 동작합니다. datetime 타입은 ISO 8601 문자열을 직접 비교할 수 있어 편리하지만, 타임존 처리를 주의해야 합니다.
text 타입은 BM25 기반 전문 검색(full-text search)을 지원합니다. 단순 키워드 포함 여부 확인에 한정되며, 벡터 검색과 결합해 하이브리드 검색을 구성할 때 활용합니다. 그러나 text 인덱스는 다른 타입보다 구축 시간이 길고 메모리를 많이 사용하므로, 실제로 전문 검색 기능이 필요한 필드에만 적용합니다.
모든 필드에 인덱스를 붙이는 것은 오히려 역효과입니다. 쓰기 처리량이 떨어지고 메모리 사용량이 증가합니다. 실제로 필터 조건으로 사용하는 필드만 인덱스를 선언하세요.
복합 필터와 인덱스 전략
실제 쿼리는 대부분 여러 조건을 조합합니다. must, should, must_not을 중첩한 복합 필터에서 어떤 순서로 조건을 평가하는지가 성능에 영향을 줍니다. Qdrant는 복합 필터에서 가장 선택적인(cardinality가 낮은) 조건을 먼저 평가해 후보 집합을 좁히는 최적화를 적용합니다. 이 최적화가 제대로 동작하려면 각 조건에 해당하는 필드에 인덱스가 있어야 합니다.
예를 들어 tenant_id (수백 개의 값)와 status (3개의 값: active, inactive, deleted)를 복합 필터로 사용한다면, Qdrant는 먼저 tenant_id로 후보를 좁히고 이후 status를 평가합니다. 두 필드 모두 keyword 인덱스가 있어야 이 최적화가 작동합니다.
멀티 테넌트 서비스에서는 tenant_id가 모든 쿼리에 필수 조건으로 들어가는 경우가 많습니다. 이 경우 Qdrant의 named vectors와 sparse vectors를 조합하거나, 테넌트별로 컬렉션을 분리하는 방식도 고려할 수 있습니다. 컬렉션 분리는 데이터 격리와 관리 편의성을 높이지만 컬렉션 수가 수천 개를 넘으면 Qdrant의 메모리 오버헤드가 커지므로 테넌트 규모에 따라 전략을 선택해야 합니다.
메모리 계산과 용량 계획
벡터 메모리 사용량 공식
Qdrant 클러스터를 프로비저닝하기 전에 필요한 메모리를 정확히 계산하는 것이 중요합니다. 벡터 메모리는 아래 공식으로 추산합니다.
벡터 원본: 포인트 수 × 차원 × 4바이트(float32)
HNSW 그래프: 포인트 수 × m × 8바이트 × 2 (양방향 연결)
페이로드: 필드 수와 값 크기에 따라 가변
OpenAI text-embedding-3-small(1536차원)으로 100만 개 벡터를 m=16으로 인덱싱한다면:
- 벡터 원본: 1,000,000 × 1,536 × 4 = 6 GB
- HNSW 그래프: 1,000,000 × 16 × 8 × 2 = 256 MB
- 합계: 약 6.3 GB (페이로드 별도)
같은 조건에서 m=32로 올리면 HNSW 그래프가 512 MB로 늘어나지만 전체 메모리에서 차지하는 비율은 작습니다. 메모리 부담의 대부분은 벡터 원본에서 옵니다.
100만 포인트에서 벡터 원본이 메모리의 대부분을 차지하며, 스칼라 양자화를 적용하면 메모리를 75% 줄일 수 있습니다.
양자화 전략과 recall 트레이드오프
Qdrant는 세 가지 양자화 방식을 지원합니다. **스칼라 양자화(Scalar Quantization)**는 float32(4바이트)를 int8(1바이트)로 압축해 메모리를 약 75% 줄입니다. **프로덕트 양자화(Product Quantization)**는 벡터를 여러 서브벡터로 나눠 각각을 코드북으로 압축하며 압축률이 더 높지만 recall 손실도 큽니다. **바이너리 양자화(Binary Quantization)**는 각 차원을 1비트로 압축해 메모리를 96% 줄일 수 있지만, recall 유지를 위해 반드시 원본 벡터를 보조 스코어링에 사용하는 rescore: true 옵션을 함께 설정해야 합니다.
실제 적용 시 가장 균형 잡힌 선택은 스칼라 양자화입니다. OpenAI 임베딩처럼 차원이 큰 벡터에서는 스칼라 양자화의 recall 손실이 작은 편이라 대부분의 서비스에서 허용할 만한 경우가 많습니다. 다만 손실 폭은 데이터 분포에 따라 달라지므로, 서비스 데이터로 recall@10을 직접 재 보고 결정해야 합니다. 양자화된 벡터로 후보를 빠르게 수집하고, 상위 k개에 대해 원본 벡터로 재스코어링하는 oversampling + rescore 패턴을 함께 쓰면 recall을 원본 수준으로 회복할 수 있습니다.
# 스칼라 양자화와 rescore를 결합한 컬렉션 생성 예시
from qdrant_client import QdrantClient
from qdrant_client.models import (
VectorParams, Distance,
ScalarQuantizationConfig, ScalarType,
ScalarQuantization
)
client = QdrantClient(host="localhost", port=6333)
client.create_collection(
collection_name="documents",
vectors_config=VectorParams(
size=1536, # OpenAI text-embedding-3-small
distance=Distance.DOT,
on_disk=True, # mmap으로 벡터 저장
),
quantization_config=ScalarQuantization(
scalar=ScalarQuantizationConfig(
type=ScalarType.INT8,
quantile=0.99, # 상위 1% 이상치 클리핑
always_ram=True, # 양자화 벡터는 메모리에 유지
)
),
hnsw_config={
"m": 16,
"ef_construct": 200, # 고차원이므로 200 설정
"on_disk": False, # HNSW 그래프는 메모리 유지
},
)
# 위 설정으로 원본 벡터는 mmap, 양자화 벡터는 메모리,
# HNSW 그래프는 메모리에 유지됩니다.
# 설정 조합과 하드웨어·데이터 분포에 따라 지연이 크게 줄어들 수 있음양자화 설정에서 quantile=0.99는 값 분포의 상위·하위 0.5%씩을 클리핑해 이상치로 인한 스케일 왜곡을 방지합니다. always_ram=True는 양자화된 압축 벡터를 항상 메모리에 올려 인덱스 탐색 속도를 유지합니다.
페이로드 인덱스 선언과 검색 쿼리 예시
컬렉션 생성 후 페이로드 인덱스를 선언하고 필터 검색을 수행하는 패턴입니다.
from qdrant_client.models import (
PayloadSchemaType,
Filter, FieldCondition, MatchValue, Range
)
# 페이로드 인덱스 선언 (컬렉션 생성 후 즉시)
client.create_payload_index(
collection_name="documents",
field_name="tenant_id",
field_schema=PayloadSchemaType.KEYWORD,
)
client.create_payload_index(
collection_name="documents",
field_name="category",
field_schema=PayloadSchemaType.KEYWORD,
)
client.create_payload_index(
collection_name="documents",
field_name="created_at",
field_schema=PayloadSchemaType.INTEGER, # Unix epoch 정수
)
# 필터 + 벡터 검색 (oversampling + rescore 패턴)
results = client.search(
collection_name="documents",
query_vector=query_embedding, # list[float], 1536차원
query_filter=Filter(
must=[
FieldCondition(key="tenant_id", match=MatchValue(value="acme-corp")),
FieldCondition(key="category", match=MatchValue(value="finance")),
FieldCondition(
key="created_at",
range=Range(gte=1700000000, lt=1730000000),
),
]
),
limit=10,
search_params={
"hnsw_ef": 128,
"quantization": {
"ignore": False,
"rescore": True, # 상위 후보를 원본 벡터로 재스코어링
"oversampling": 2.0 # 10개 원하면 20개 후보 수집 후 재정렬
}
}
)
# results: ScoredPoint 리스트, .id / .score / .payload 접근 가능
# rescore + oversampling을 켜면 양자화로 잃은 recall 대부분을 되찾는다 (실제 값은 서비스 데이터로 측정)oversampling=2.0은 최종 limit의 두 배만큼 후보를 수집하고, 그 안에서 원본 벡터로 재스코어링해 최상위 10개를 반환합니다. 지연이 약간 늘어나지만 양자화로 인한 recall 손실을 대부분 회복합니다.
운영 환경 적용 시 고려사항
흔한 실수와 함정
인덱스 구축 중 max_indexing_threads 미설정이 가장 자주 만나는 함정입니다. Qdrant는 기본적으로 가용한 모든 CPU 코어를 인덱스 구축에 사용합니다. 초기 데이터 로드 시에는 이것이 빠른 인덱스 구축에 도움이 되지만, 운영 중에 대량 업서트(upsert)가 발생하면 인덱스 재구축으로 인해 CPU 사용률이 치솟으면서 쿼리 응답 시간이 수 배로 늘어납니다. 운영 환경에서는 max_indexing_threads: 2로 제한해 쿼리 처리에 나머지 코어를 확보하는 것이 좋습니다.
컬렉션 최적화(optimization) 타이밍 오해도 주의해야 합니다. Qdrant는 데이터가 특정 임계치에 도달하면 자동으로 세그먼트를 병합하고 인덱스를 재구축합니다. 이 최적화 과정에서 일시적으로 디스크 사용량이 두 배로 늘어납니다. 디스크를 벡터 스토리지 크기에 딱 맞게 프로비저닝하면 최적화 중에 디스크 풀이 발생해 Qdrant가 오류를 반환합니다. 최적화 중에는 일시적으로 두 배까지 쓰므로, 디스크는 예상 데이터 크기의 두 배에 여유분을 더해 잡는 것이 안전합니다.
필터 조건 없는 상태에서 ef 값을 낮게 설정하면 높은 recall이 필요한 쿼리에서 문제가 됩니다. 반대로 모든 쿼리에 높은 ef를 설정하면 간단한 유사도 검색에서 불필요한 지연이 발생합니다. 쿼리 타입별로 hnsw_ef를 다르게 설정하는 것이 실용적입니다. RAG 파이프라인의 일반 검색에는 ef=64, 정밀도가 중요한 의미 기반 검색에는 ef=256처럼 용도에 맞게 나눕니다.
운영 중 발생하는 세 가지 핵심 함정은 각각 독립적인 설정 조정으로 해결됩니다.
모니터링과 디버깅 지표
Qdrant는 /metrics 엔드포인트를 통해 Prometheus 형식의 메트릭을 노출합니다. 운영 중 주시해야 할 지표는 다음과 같습니다.
qdrant_collection_segments_count는 컬렉션 내 세그먼트 수입니다. 이 값이 과도하게 높으면(수십 개 이상) 최적화가 제때 이루어지지 않고 있음을 나타내며, 검색 시 여러 세그먼트를 병렬로 탐색하느라 지연이 늘어납니다. qdrant_collection_points_count 대비 qdrant_collection_indexed_vectors_count 비율이 현저히 낮으면 인덱싱이 뒤처지고 있다는 신호입니다. 인덱싱이 완료되지 않은 포인트는 브루트포스로 검색되므로 지연에 직접 영향을 줍니다.
응답 지연 분포는 p50이 아닌 p95, p99를 기준으로 모니터링해야 합니다. 벡터 검색 특성상 아웃라이어 쿼리가 평균을 왜곡하는 경우가 많습니다. 필터 조건이 복잡하거나 ef 값이 큰 쿼리가 p99 지연을 끌어올리는 주범인 경우가 많으므로, 쿼리 로그에서 파라미터와 지연을 함께 기록해 상관관계를 분석합니다.
아래 범위는 출발점으로 쓸 예시 값입니다. 하드웨어, 벡터 차원, 필터 조건에 따라 정상 범위가 달라지므로 첫 몇 주의 측정값과 서비스 SLA를 보고 다시 정합니다.
| 모니터링 지표 | 정상 범위(예시) | 이상 징후(예시) | 조치 |
|---|---|---|---|
| 세그먼트 수 | 5~15개 | 30개 이상 | 최적화 트리거, threshold 조정 |
| 인덱싱 비율 | 95% 이상 | 80% 미만 | indexing_threads 확인, 쓰기 부하 조절 |
| 검색 p99 지연 | < 50ms | > 200ms | ef 조정, 필터 인덱스 점검 |
| 메모리 사용률 | < 75% | > 90% | 양자화 적용, 온디스크 전환 |
확장과 마이그레이션 전략
Qdrant 클러스터를 수평으로 확장할 때는 **분산 모드(distributed mode)**를 사용합니다. 컬렉션을 생성할 때 shard_number를 설정하면 데이터가 여러 노드에 자동으로 분산됩니다. shard 수는 나중에 변경하기 어려우므로 초기 설계가 중요합니다. 경험적으로 노드당 1~2개의 shard가 균형 잡힌 설정이며, 향후 확장 가능성을 고려해 현재 노드 수의 2배 정도로 shard를 설정하는 경우도 있습니다.
기존 단일 노드 Qdrant를 클러스터로 전환할 때는 스냅샷(snapshot)을 활용합니다. Qdrant의 스냅샷 API로 컬렉션의 현재 상태를 파일로 저장하고, 새 클러스터에 복구하는 방식으로 다운타임 없이 마이그레이션이 가능합니다. 다만 스냅샷 복구 중에는 컬렉션이 읽기 전용 상태가 되므로, 트래픽을 임시로 다른 인스턴스로 전환하는 블루그린 배포를 함께 적용하는 것이 좋습니다.
임베딩 모델을 교체할 때는 기존 컬렉션을 유지한 채 새 컬렉션을 병렬로 운영하는 전략이 안전합니다. 새 모델로 벡터를 재생성하면서 새 컬렉션에 점진적으로 적재하고, 두 컬렉션의 검색 결과를 A/B 테스트로 비교한 뒤 전환 시점을 결정합니다. 차원이 다른 모델로 전환하는 경우 기존 컬렉션의 hnsw_config와 quantization_config를 그대로 재활용하지 말고 새 차원에 맞게 재설계합니다.
임베딩 모델 교체는 새 컬렉션을 병렬로 운영하며 A/B 테스트로 검증한 후 전환합니다.
맺음말
핵심 요약
Qdrant 프로덕션 배포에서 성능을 결정하는 세 축은 컬렉션 설계, HNSW 튜닝, 필터 인덱스입니다. 컬렉션 설계 단계에서는 임베딩 모델에 맞는 거리 메트릭을 선택하고, 벡터는 mmap으로 저장하면서 페이로드는 메모리에 유지하는 조합이 비용 효율적입니다. HNSW 파라미터에서는 m보다 ef가 검색 응답에 더 직접적인 영향을 주며, 쿼리 타입별로 동적 ef 설정을 활용하면 recall과 지연 사이를 유연하게 조절할 수 있습니다. 필터 인덱스는 모든 필터 조건 필드에 반드시 선언해야 하며, 선언하지 않으면 카디널리티 추정이 실패해 브루트포스 경로로 빠집니다. 벡터 수가 100만 개를 넘기 시작하면 스칼라 양자화와 rescore + oversampling 패턴으로 벡터 메모리를 약 75% 줄이면서 recall 손실을 작게 묶을 수 있습니다. 손실 폭은 서비스 데이터로 직접 재서 확인합니다.
적용 판단 기준
Qdrant를 기본 설정 그대로 운영해도 되는 경우는 벡터 수가 수십만 개 이하이고, 필터 없는 단순 유사도 검색만 사용하며, 인스턴스 메모리가 데이터 크기보다 충분히 큰 환경입니다. 이 조건 중 하나라도 벗어난다면 이 글에서 다룬 설정들을 단계적으로 적용하는 것이 권장됩니다. 먼저 페이로드 인덱스 선언으로 필터 성능을 확보하고, 메모리 여유가 줄어드는 시점에 스칼라 양자화를 도입하며, 처리량이 늘어날 때 max_indexing_threads와 ef 전략을 조정하는 순서가 운영 부담이 적습니다. 각 변경 후에는 recall@10과 p99 지연을 측정해 기준값과 비교하고, 개선이 확인된 경우에만 다음 단계로 진행하는 점진적 접근이 안전합니다.