Published on

Qdrant HNSW 튜닝으로 RAG 지연 50% 줄이기

Authors

서버리스 LLM 호출 비용이 내려가도, RAG 파이프라인의 체감 속도는 여전히 retrieval이 좌우하는 경우가 많습니다. 특히 Qdrant를 벡터 DB로 쓰면 기본 설정만으로도 충분히 빠르지만, 데이터 규모가 커지고(수십만~수천만), 필터가 붙고, 동시 요청이 늘면 HNSW 인덱스의 탐색 비용이 그대로 지연으로 나타납니다.

이 글은 “Qdrant의 HNSW를 어떻게 만져야 RAG 지연을 실제로 50% 가까이 줄일 수 있는가”를 목표로 합니다. 단순히 파라미터 설명이 아니라,

  • 어떤 지표를 먼저 재고
  • 어떤 순서로 튜닝하고
  • 어떤 부작용(메모리, 빌드 시간, recall 저하)을 관리할지

를 실무 관점에서 정리합니다.

참고로 HNSW 튜닝의 기본 개념은 Milvus에서도 유사합니다. 벡터 DB가 달라도 “탐색 폭을 늘리면 recall이 오르고 느려진다” 같은 본질은 같기 때문에, 비교 학습이 필요하면 Milvus HNSW 튜닝 - recall↑·latency↓ 실전도 같이 보면 좋습니다.

RAG에서 지연이 터지는 지점부터 분해하기

RAG 요청 1건의 지연을 단순화하면 보통 아래 합입니다.

  1. 임베딩 생성(또는 쿼리 임베딩 캐시 hit)
  2. 벡터 검색(Top k + 필터)
  3. 재랭킹(있다면)
  4. 컨텍스트 조립
  5. LLM 호출

여기서 Qdrant HNSW 튜닝이 직접 영향을 주는 건 2번입니다. 그런데 2번을 더 쪼개면 다음 요소들이 latency를 결정합니다.

  • 그래프 탐색 폭(ef_search)
  • 그래프의 연결도(m)
  • 인덱스 빌드 시 탐색 폭(ef_construct)
  • 필터 적용 방식(후처리 vs prefilter)
  • 세그먼트/샤드 구조, 디스크/메모리 상주 여부

“지연 50%↓”는 보통 다음 중 하나를 달성할 때 나옵니다.

  • ef_search를 낮춰도 recall이 유지되는 구조를 만들기(m, ef_construct 개선)
  • 필터가 있는 쿼리를 prefilter로 바꿔 후보군 낭비를 줄이기
  • k나 payload 크기 때문에 발생하는 네트워크/직렬화 비용을 줄이기

즉, 단순히 ef_search만 내리면 recall이 무너질 수 있으니, “낮춰도 되는 상태”를 먼저 만들어야 합니다.

Qdrant HNSW 핵심 파라미터 3개만 제대로 잡기

Qdrant에서 HNSW는 컬렉션의 벡터 인덱싱 설정으로 관리합니다. 이름은 환경/버전에 따라 표현이 조금 다를 수 있지만 핵심은 동일합니다.

m: 그래프 연결도(메모리와 recall의 뼈대)

  • 의미: 각 노드가 가지는 최대 연결 수(대략적인 개념)
  • 효과: m이 높을수록 그래프가 촘촘해져 탐색이 쉬워지고 recall이 좋아지는 경향
  • 비용: 메모리 증가, 인덱스 빌드 시간 증가

실무 팁:

  • RAG에서 문서 chunk가 많고(수백만 이상) 의미적으로 비슷한 벡터가 많이 뭉치는 데이터라면 m이 낮을 때 “탐색이 길어지는” 현상이 생깁니다.
  • m을 올리면 같은 recall을 더 낮은 ef_search로 달성할 여지가 커져, 결과적으로 p95/p99가 떨어지는 경우가 많습니다.

ef_construct: 인덱스 빌드 품질(나중의 검색 비용을 줄이는 투자)

  • 의미: 인덱스 생성 시 후보 탐색 폭
  • 효과: 높을수록 인덱스 품질이 좋아져 검색 시 필요한 ef_search를 낮출 수 있음
  • 비용: 인덱스 빌드 시간 증가(대량 적재 시 체감 큼)

실무 팁:

  • “대량 적재 후 거의 읽기 전용”인 RAG 컬렉션이라면 ef_construct를 높이는 게 유리합니다.
  • 반대로 실시간 인서트가 매우 잦고 인덱스 빌드/머지가 계속 돈다면, 과도한 ef_construct는 운영 부담이 됩니다.

ef_search: 검색 품질 vs 지연의 다이얼

  • 의미: 검색 시 후보 탐색 폭
  • 효과: 높을수록 recall 상승, 지연 상승
  • 비용: CPU 사용량 증가, 동시성 하락

실무 팁:

  • p50보다 p95/p99에서 차이가 크게 납니다. 동시 요청이 있을 때 ef_search가 큰 컬렉션은 tail latency가 급격히 나빠집니다.

“지연 50%↓”를 만드는 튜닝 순서(실전 루틴)

아래 순서가 중요한 이유는, 앞 단계를 건너뛰고 ef_search만 낮추면 품질이 흔들리기 때문입니다.

1) 먼저 측정: recall 대신 “정답 근사” 지표를 만든다

RAG는 정답 레이블이 없는 경우가 많습니다. 그래서 검색 품질을 다음 중 하나로 근사 측정합니다.

  • 오프라인: 쿼리-정답 문서 페어(사내 검색 로그, FAQ, 티켓)를 만들어 hit@k 측정
  • 온라인: LLM 답변의 근거 문서 포함률, 사용자 클릭률, “답변 재질문율” 등 대체 지표

최소한 오프라인 기준으로 top_k=10에서 hit@10 같은 지표 하나는 만들어야 튜닝이 안전합니다.

2) 필터가 있다면 prefilter 전략부터 점검한다

RAG에서 흔한 필터는 tenant_id, project_id, language, doc_type, time_range입니다.

  • 후처리(post-filter): 벡터로 먼저 뽑고 나중에 payload 조건으로 걸러서 k를 채움
  • 사전 필터(pre-filter): 조건을 만족하는 후보군에서만 탐색

후처리는 “필터로 대부분이 탈락하는” 상황에서 검색 낭비가 커져 지연이 튑니다. Qdrant는 필터를 지원하지만, 워크로드 특성상 실제로는 post-filter처럼 동작하는 형태가 될 수 있으니, 다음을 체크하세요.

  • 필터 선택도가 낮은가(예: 특정 tenant_id가 전체의 1% 이하)
  • 필터 조건이 복잡해서 후보군을 많이 버리는가
  • k가 커서(k=50 이상) 필터 통과분을 채우느라 탐색이 길어지는가

이 단계에서 지연이 20~40% 줄어드는 케이스도 많습니다.

3) mef_construct로 “낮은 ef_search에서도 유지되는 인덱스” 만들기

추천 접근은 아래처럼 2단계 실험입니다.

  • 실험 A: m만 올린 인덱스 vs 기본 인덱스
  • 실험 B: m을 올린 상태에서 ef_construct를 올린 인덱스 vs A

그 다음에야 ef_search를 내려도 됩니다.

경험적으로 많이 쓰는 범위(데이터/차원/거리함수에 따라 다름):

  • m: 16에서 시작해서 24, 32까지 단계적으로
  • ef_construct: 100~400 범위에서 단계적

목표는 “기존 recall을 유지하는 최소 ef_search”를 찾는 것입니다.

4) 마지막에 ef_search를 낮추며 p95를 깎는다

튜닝은 보통 아래 패턴으로 끝납니다.

  • 기존 ef_search=128로 운영 중
  • 인덱스 품질 개선 후 ef_search=64로 내려도 오프라인 hit@10 유지
  • p95가 40ms에서 20ms로 떨어져 “체감 50%↓” 달성

중요한 건 p50이 아니라 p95/p99입니다. RAG는 LLM 호출이 뒤에 붙기 때문에 tail이 길면 전체 UX가 나빠집니다.

Qdrant 컬렉션 생성/업데이트 예시 코드

아래는 Qdrant REST API로 컬렉션을 만들 때 HNSW 설정을 넣는 예시입니다. 환경에 따라 필드명이 다를 수 있으니, 실제 운영 버전의 Qdrant OpenAPI 문서를 기준으로 확인하세요.

curl -X PUT "http://localhost:6333/collections/rag_chunks" \
  -H "Content-Type: application/json" \
  -d '{
    "vectors": {
      "size": 1536,
      "distance": "Cosine"
    },
    "hnsw_config": {
      "m": 32,
      "ef_construct": 256,
      "full_scan_threshold": 10000
    }
  }'

검색 시에는 paramsef_search를 조절합니다.

curl -X POST "http://localhost:6333/collections/rag_chunks/points/search" \
  -H "Content-Type: application/json" \
  -d '{
    "vector": [0.01, 0.02, 0.03],
    "limit": 10,
    "with_payload": true,
    "params": {
      "hnsw_ef": 64
    },
    "filter": {
      "must": [
        {"key": "tenant_id", "match": {"value": "t_123"}},
        {"key": "lang", "match": {"value": "ko"}}
      ]
    }
  }'

여기서 핵심은 limit(Top k)와 hnsw_ef를 같이 보면서, 필터 선택도가 낮을수록 hnsw_ef를 무작정 올리는 방식이 비용 폭탄이 될 수 있다는 점입니다.

벤치마크 방법: p95 지연과 품질을 같이 잡기

튜닝은 “한 번에 정답”이 아니라 실험입니다. 추천하는 최소 벤치마크 세트는 아래입니다.

측정 항목

  • 검색 지연: p50/p95/p99
  • 처리량: QPS(동시성 N에서)
  • 품질: 오프라인 hit@k 또는 recall@k 근사 지표
  • 시스템: CPU 사용률, RSS 메모리, 디스크 IO

실험 설계

  • 고정: 데이터 스냅샷, 쿼리 세트(예: 1,000개), k, 필터 분포
  • 변수: m, ef_construct, ef_search

예를 들어 아래처럼 3x3 그리드로 돌리면 경향이 빨리 보입니다.

  • m: 16, 24, 32
  • ef_search: 32, 64, 128

그리고 품질이 비슷한 조합 중 p95가 가장 낮은 것을 고릅니다.

운영에서 자주 터지는 함정 6가지

1) k를 과하게 크게 잡아 검색을 느리게 만든다

RAG에서 k=50을 기본으로 두고 “LLM이 알아서 고르겠지”라고 하면 retrieval이 느려집니다. 보통은 k=5~15에서 시작하고, 재랭커가 있으면 더 줄일 수 있습니다.

2) payload를 과하게 가져와 네트워크/직렬화가 병목이 된다

with_payload=true로 큰 본문을 통째로 가져오면 검색 자체는 빨라도 응답이 느립니다.

  • 먼저 ID와 필요한 메타만 가져오고
  • 본문은 별도 스토리지에서 가져오거나
  • payload에서 필드 선택 기능을 활용

하는 쪽이 안정적입니다.

3) 필터 선택도가 낮은데 post-filter로 k를 채우느라 탐색이 폭증한다

이 경우 ef_search를 올리는 처방은 “더 느리게” 만드는 방향일 수 있습니다. 필터 전략과 데이터 파티셔닝(컬렉션 분리, 샤딩 키)을 먼저 보세요.

4) 동시성에서 tail latency가 튄다

ef_search가 큰 설정은 CPU를 더 쓰고, 동시 요청에서 큐잉이 생기며 p99가 급증합니다. RAG는 사용자 체감이 p95/p99에 민감합니다.

5) 인덱스 빌드/머지 작업이 검색과 경쟁한다

대량 업서트, 컴팩션, 스냅샷 등 백그라운드 작업이 검색과 같은 자원을 쓰면 지연이 튑니다. 배치 적재 시간대를 분리하거나 리소스 격리를 고려하세요.

6) “검색은 빨라졌는데 LLM 호출이 429로 폭주”한다

retrieval이 빨라지면 전체 파이프라인 처리량이 올라가면서 LLM API가 병목이 될 수 있습니다. 특히 스트리밍/동시성에서 429가 늘면 사용자 체감은 오히려 나빠집니다. 이 경우에는 LangChain OpenAI 스트리밍 중 429 폭주 해결법처럼 rate limit 완화(지터, 큐, 세마포어)까지 같이 설계해야 “진짜 end-to-end 지연”이 줄어듭니다.

추천 튜닝 레시피(현업에서 많이 먹히는 조합)

아래는 “대량 문서 + 읽기 위주 + 필터 존재”라는 전형적인 RAG 조건에서 자주 쓰는 출발점입니다.

  • 인덱스(컬렉션)
    • m=24 또는 m=32
    • ef_construct=256
  • 검색(쿼리)
    • hnsw_ef=64에서 시작
    • 품질 부족 시 96, 128로 단계 상승
    • k=10 전후

이 조합의 논리는 간단합니다.

  • 인덱스 품질을 올려서(구축 시 비용 지불)
  • 런타임 탐색 폭을 줄이고(요청당 비용 절감)
  • 동시성에서 p95/p99를 안정화

시키는 방향입니다.

마무리: “빠르게”가 아니라 “낮은 ef_search로도 정확하게”

Qdrant HNSW 튜닝으로 RAG 지연을 크게 줄이는 핵심은 ef_search를 무작정 내리는 게 아니라, mef_construct로 인덱스 자체를 “탐색하기 쉬운 구조”로 만들어 같은 품질을 더 적은 탐색으로 얻는 것입니다.

정리하면 다음 체크리스트로 접근하세요.

  • 필터 선택도 점검 및 prefilter 최적화
  • m 상향으로 그래프 품질 개선
  • ef_construct 상향으로 구축 품질 개선
  • 그 다음 ef_search를 내려 p95/p99를 깎기
  • k, payload 크기, 동시성까지 포함해 end-to-end로 검증

이 루틴대로 실험하면, 워크로드에 따라 검색 p95 기준으로 30~60% 개선이 충분히 나옵니다. 특히 기존에 필터+큰 ef_search 조합으로 tail latency가 튀던 시스템이라면 “50%↓”는 과장이 아니라 현실적인 목표입니다.