- Published on
Qdrant RAG 검색 느림? HNSW·Payload 인덱스 튜닝
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
RAG 파이프라인에서 “검색이 느리다”는 말은 대개 두 가지가 섞여 있습니다. 첫째는 벡터 유사도 탐색(HNSW) 자체가 느린 경우, 둘째는 필터링(메타데이터 조건) 때문에 payload 스캔이 커져서 느린 경우입니다. Qdrant는 이 둘을 동시에 다루는 구조라서, 병목이 어디인지 먼저 분리하고 그에 맞게 HNSW와 Payload 인덱스를 각각 튜닝해야 합니다.
이 글은 Qdrant에서 RAG 검색이 느려질 때 가장 흔한 원인과, HNSW 파라미터 및 payload index를 어떻게 잡아야 체감 성능이 개선되는지에 초점을 맞춥니다.
1) 먼저 병목을 분리하자: “벡터 탐색” vs “필터”
RAG 검색은 보통 아래 형태입니다.
- 임베딩 벡터로 근접 이웃 탐색
- 동시에 payload 조건(tenant, language, doc_type, time_range 등)으로 후보를 제한
- 상위
limit개를 반환
여기서 느려지는 지점은 크게 3가지입니다.
- HNSW 탐색 비용 증가: 컬렉션이 커졌거나,
ef_search가 과도하거나,m/ef_construct가 부적절해서 그래프 품질이 낮은 경우 - 필터 비용 증가: payload 인덱스가 없어서 조건 필터가 사실상 스캔으로 동작하거나, cardinality(값 종류)가 낮은 필터를 잘못 인덱싱한 경우
- 스토리지/메모리 병목: 세그먼트가 많아 random access가 늘거나, 디스크 기반 검색으로 내려가거나, quantization/압축 설정이 맞지 않는 경우
실무에서 가장 흔한 패턴은 “필터가 있는 RAG에서 payload 인덱스 미설정으로 느려짐” 입니다. 그 다음이 HNSW 파라미터를 정확도 위주로만 올려서 지연이 폭증하는 케이스입니다.
2) HNSW 핵심 파라미터: m, ef_construct, ef_search
Qdrant의 HNSW는 근사 탐색이므로, 파라미터는 결국 지연시간 vs recall(정확도) 트레이드오프입니다.
2.1 m: 그래프 연결도(메모리와 품질)
- 의미: 각 노드가 유지하는 최대 이웃 수(대략적인 연결도)
- 효과:
m이 커지면 그래프 품질이 좋아져 recall이 올라가고, 동일 recall을 더 낮은ef_search로 달성할 수도 있음 - 비용: 메모리 증가, 빌드 시간 증가
권장 감각치(일반적인 텍스트 임베딩 기준):
- 작은 컬렉션(수십만):
m16 전후 - 수백만 이상:
m16~32 범위에서 실험
무작정 m을 올리면 메모리와 빌드 시간이 크게 증가합니다. 특히 RAG에서 필터가 강하면(tenant, namespace 등) 실제 탐색 공간이 줄어들기 때문에 m을 과도하게 키워도 체감이 적을 수 있습니다.
2.2 ef_construct: 인덱스 빌드 품질
- 의미: HNSW 인덱스 생성 시 후보 탐색 폭
- 효과: 높을수록 인덱스 품질이 좋아져 recall이 상승하고, 런타임
ef_search를 낮춰도 성능이 나올 수 있음 - 비용: 인덱싱(업서트) 시간 증가
실무 팁:
- 데이터가 자주 바뀌지 않고, 배치로 적재하는 RAG(문서 인덱싱)라면
ef_construct를 높여서 인덱스 품질을 미리 확보하는 전략이 유리합니다. - 반대로 실시간 업데이트가 많다면
ef_construct를 너무 높이면 ingest가 병목이 됩니다.
2.3 ef_search: 검색 시 후보 탐색 폭(가장 체감 큰 레버)
- 의미: 검색 시 탐색하는 후보 수
- 효과: 높을수록 recall 상승
- 비용: 지연시간 상승
RAG에서 top_k가 520 수준이라면, 256 사이에서 튜닝하는 경우가 많습니다.ef_search는 보통 64
중요한 포인트는 필터가 있는 경우 ef_search를 단순히 올리는 것이 답이 아닐 수 있다는 점입니다. 필터로 인해 후보가 잘 안 잡히면 탐색이 길어지고, 결과적으로 지연이 늘면서도 recall은 기대만큼 오르지 않습니다. 이때는 payload 인덱스와 필터 설계를 먼저 봐야 합니다.
3) Qdrant 컬렉션 생성 예시: HNSW 설정
아래는 Qdrant REST로 컬렉션을 만들면서 HNSW를 지정하는 예시입니다. Qdrant 버전에 따라 필드명이 조금 다를 수 있으니, 운영 버전의 OpenAPI 스펙을 함께 확인하세요.
curl -X PUT "http://localhost:6333/collections/rag_docs" \
-H "Content-Type: application/json" \
-d '{
"vectors": {
"size": 768,
"distance": "Cosine"
},
"hnsw_config": {
"m": 16,
"ef_construct": 128
}
}'
검색 시에는 params로 ef_search를 조정할 수 있습니다.
curl -X POST "http://localhost:6333/collections/rag_docs/points/search" \
-H "Content-Type: application/json" \
-d '{
"vector": [0.01, 0.02, 0.03],
"limit": 10,
"params": {
"hnsw_ef": 128
}
}'
운영에서는 hnsw_ef를 고정값으로 두기보다, 쿼리 종류에 따라 다르게 가져가는 전략이 유효합니다.
- 일반 질의:
hnsw_ef낮게 - “정확도 최우선” 질의(예: 재검색, fallback):
hnsw_ef높게
4) RAG에서 payload 필터가 느린 진짜 이유: “인덱스 없는 조건”
RAG 시스템은 보통 아래 같은 payload를 씁니다.
tenant_id: 멀티테넌시namespace또는project_id: 워크스페이스doc_type: pdf, notion, slack 등lang: ko, ensource: ingestion pipeline 구분created_at: 시간 범위
여기서 조건이 붙는 순간 Qdrant는 후보를 제한해야 하는데, payload 인덱스가 없으면 필터 평가 비용이 커져서 HNSW 탐색보다 필터가 더 비싸질 수 있습니다.
특히 다음 조합이 위험합니다.
must로 tenant 필터를 걸었는데 tenant별 데이터가 수백만should/must_not조건이 복잡하고, 인덱스가 없어 조건 평가가 느림- 시간 범위 조건이 있는데 range 인덱스가 없거나, 필드 타입이 잘못 들어가 인덱싱이 안 됨
5) Payload 인덱스 설계: 무엇을 인덱싱해야 하나
payload 인덱스는 “많이 쓰는 필터”를 빠르게 하기 위한 장치지만, 무조건 많이 만든다고 좋은 게 아닙니다.
5.1 인덱싱 우선순위(실무 기준)
- 항상 걸리는 강제 필터:
tenant_id,namespace,project_id - 자주 함께 쓰는 분기 필터:
doc_type,lang - 범위 필터:
created_at,updated_at
반대로 아래는 신중해야 합니다.
- 값 종류가 지나치게 적은 필드(예:
lang이 ko/en 2개뿐)만 단독으로 인덱싱하면 효율이 제한적일 수 있음 - 거의 안 쓰는 필드는 인덱스 유지 비용만 증가
다만 멀티테넌시에서 tenant_id는 거의 필수입니다. 이게 없으면 “모든 테넌트를 대상으로 벡터 탐색 후 필터링”이 되어 지연이 급격히 증가합니다.
5.2 Payload 인덱스 생성 예시
Qdrant는 payload 필드에 대해 인덱스를 생성할 수 있습니다. 예시는 keyword 필드와 integer 필드를 함께 인덱싱하는 형태입니다.
# tenant_id: keyword 인덱스
curl -X PUT "http://localhost:6333/collections/rag_docs/index" \
-H "Content-Type: application/json" \
-d '{
"field_name": "tenant_id",
"field_schema": "keyword"
}'
# doc_type: keyword 인덱스
curl -X PUT "http://localhost:6333/collections/rag_docs/index" \
-H "Content-Type: application/json" \
-d '{
"field_name": "doc_type",
"field_schema": "keyword"
}'
# created_at: integer 인덱스(예: epoch seconds)
curl -X PUT "http://localhost:6333/collections/rag_docs/index" \
-H "Content-Type: application/json" \
-d '{
"field_name": "created_at",
"field_schema": "integer"
}'
주의할 점:
created_at같은 시간은 문자열로 저장하면 range 필터 최적화가 제한될 수 있습니다. 가능하면 epoch 기반 정수로 저장하세요.- 실제로는 “필터에 쓰는 타입”과 “저장된 payload 타입”이 일치해야 합니다.
6) 필터 쿼리 구조 튜닝: must를 먼저, 조건을 단순하게
Qdrant 필터는 논리 구조가 복잡해질수록 평가 비용이 커질 수 있습니다. RAG에서 흔히 하는 실수는 “필터를 너무 많이 한 번에 넣고, 인덱스 없이 기대”하는 것입니다.
권장 패턴:
- 강제 스코프 제한(tenant, namespace)을
must로 최상단에 둔다 - 그 다음에 doc_type, lang 같은 분기 조건을 붙인다
- 불필요한
should남발을 피한다(특히minimum_should_match가 들어가는 경우)
예시:
curl -X POST "http://localhost:6333/collections/rag_docs/points/search" \
-H "Content-Type: application/json" \
-d '{
"vector": [0.01, 0.02, 0.03],
"limit": 10,
"params": {"hnsw_ef": 96},
"filter": {
"must": [
{"key": "tenant_id", "match": {"value": "t_123"}},
{"key": "namespace", "match": {"value": "prod"}},
{"key": "lang", "match": {"value": "ko"}}
],
"range": {
"key": "created_at",
"gte": 1704067200
}
}
}'
여기서 tenant_id, namespace, lang, created_at가 인덱싱되어 있으면 필터 비용이 눈에 띄게 내려갑니다.
7) 성능 튜닝 체크리스트: “정확도-지연-비용”을 같이 본다
7.1 빠르게 효과 보는 순서
- 필터 필드 payload 인덱스 생성: 특히
tenant_id같은 스코프 필터 ef_search낮춰보기: 목표 recall을 만족하는 최소값 찾기m과ef_construct재조정: 인덱스 품질을 올려 동일 recall에서ef_search를 낮추기- 세그먼트/업서트 패턴 점검: 너무 잦은 업데이트로 세그먼트가 쪼개지는지
7.2 RAG 품질을 안 깨는 실전 접근
- 오프라인에서 “정답 문서가 top_k에 들어오는 비율”을 측정해 recall을 수치화
ef_search를 단계적으로 낮추면서 latency와 recall 곡선을 만든다- latency 목표를 만족하는 지점에서 멈추고, 부족한 recall은
m/ef_construct로 보완
이 과정은 로컬 LLM을 함께 돌릴 때 더 중요해집니다. 검색이 느려지면 LLM 호출이 대기하게 되어 전체 응답 시간이 폭증합니다. 로컬 추론 최적화가 필요하다면 Transformers 로컬 LLM OOM - 4bit·offload 최적화처럼 모델 메모리/속도 최적화도 함께 고려해야 엔드투엔드 성능이 안정됩니다.
8) 운영에서 자주 만나는 함정 5가지
8.1 tenant 필터를 안 걸고 검색한다
개발 환경에서는 데이터가 적어서 티가 안 나지만, 운영에서는 tenant가 늘어나는 순간 검색 공간이 폭발합니다. 멀티테넌시 RAG라면 tenant 스코프는 거의 필수입니다.
8.2 payload 타입이 일관되지 않다
created_at을 어떤 문서는 정수로, 어떤 문서는 문자열로 넣으면 인덱스가 기대대로 동작하지 않거나 필터가 느려질 수 있습니다. ingestion 단계에서 스키마를 강제하세요.
8.3 ef_search를 “정확도 올리는 버튼”으로만 쓴다
ef_search를 올리면 recall은 오르지만, 필터가 비효율적이면 지연만 오를 수 있습니다. 먼저 payload 인덱스를 확인한 뒤 ef_search를 조정하세요.
8.4 필터 조건이 과도하게 복잡하다
should가 많고 must_not까지 섞이면, 인덱스가 있어도 평가 비용이 커질 수 있습니다. RAG는 “검색 후보를 좁히는” 것이 목적이므로, 조건을 단순화하고 정말 필요한 것만 남기는 게 좋습니다.
8.5 네트워크 및 타임아웃 설계가 없다
검색이 느려질 때는 결국 타임아웃과 재시도 정책이 서비스 품질을 좌우합니다. gRPC 기반으로 Qdrant 앞단을 감싸거나, 검색 서비스를 별도로 둔다면 Go gRPC DEADLINE_EXCEEDED 원인과 재시도·타임아웃 설계 같은 방식으로 타임아웃 예산을 계층별로 나눠 잡는 게 안전합니다.
9) 추천 튜닝 레시피(출발점)
아래는 “일반적인 텍스트 임베딩 RAG”에서 무난한 출발점입니다.
- HNSW
m: 16ef_construct: 128 (배치 적재 위주면 256도 고려)ef_search: 64~128에서 시작해 목표 recall에 맞춰 조정
- Payload 인덱스
tenant_id,namespace: keyword 인덱스 필수doc_type,lang: 쿼리 패턴에 따라 추가created_at: epoch 정수로 저장하고 integer 인덱스
그리고 가장 중요한 운영 원칙은 하나입니다.
- “느려졌을 때” 튜닝하지 말고, 데이터 규모가 커지기 전부터 latency-recall 곡선을 만들어 두기
이렇게 해두면 컬렉션이 10배 커져도, 어떤 레버를 돌려야 하는지(필터 인덱스인지, ef_search인지, 인덱스 재빌드인지)를 빠르게 결정할 수 있습니다.
10) 마무리
Qdrant RAG 검색이 느릴 때는 HNSW만 의심하기 쉽지만, 실무에서는 payload 필터가 병목인 경우가 매우 많습니다. 해결 전략은 단순합니다.
- 스코프 필터에 payload 인덱스를 먼저 만든다
ef_search를 최소화해서 latency를 잡는다- 부족한 recall은
m/ef_construct로 인덱스 품질을 올려 보완한다
이 순서대로 접근하면 “정확도는 유지하면서 검색만 빨라지는” 튜닝이 가능해집니다.