Published on

Pinecone·Milvus 인덱싱 느림? HNSW 튜닝

Authors

서빙은 빠른데 인덱싱(업서트/빌드)이 유독 느리면, 대부분은 “벡터 DB가 느리다”가 아니라 HNSW 파라미터와 데이터 적재 방식이 현재 워크로드에 맞지 않아서입니다. Pinecone와 Milvus는 내부 구현과 운영 방식이 다르지만, HNSW의 병목 지점은 놀랄 만큼 비슷합니다.

이 글에서는 다음을 목표로 합니다.

  • 인덱싱이 느려지는 대표 원인을 HNSW 메커니즘 관점에서 분해
  • M, efConstruction이 인덱싱 속도/메모리/리콜에 미치는 영향 정리
  • Pinecone·Milvus에서 실무적으로 적용 가능한 튜닝 순서 제시
  • “빨라지긴 했는데 검색 품질이 망가짐”을 막는 검증 루틴 제공

RAG 파이프라인에서 검색 품질 검증까지 같이 묶어 운영한다면, 출력 형식 강제와 품질 관리 관점은 이 글도 함께 참고할 만합니다: RAG 환각을 줄이는 JSON Schema 강제 출력법

1) HNSW 인덱싱이 느려지는 진짜 이유

HNSW(Hierarchical Navigable Small World)는 삽입 시 매번 “그래프에 노드를 끼워 넣고, 근접 이웃과 연결”을 수행합니다. 이 과정이 느려지는 이유는 크게 4가지입니다.

1-1. efConstruction이 과도하게 큼

삽입 시 후보 이웃을 넓게 탐색할수록(= efConstruction 증가) 연결 품질이 좋아져 리콜이 오르지만, 삽입 비용이 거의 선형에 가깝게 증가합니다.

  • 체감 증상: 업서트 QPS가 점점 떨어지고, CPU가 꽉 차며, tail latency가 튐
  • 흔한 실수: 검색 리콜을 올리려고 ef(검색) 대신 efConstruction을 무작정 올림

1-2. M이 과도하게 큼

M은 노드당 최대 연결 수(대략적인 degree)입니다.

  • M 증가 효과
    • 장점: 그래프가 촘촘해져 리콜 상승, 특히 고차원에서 안정적
    • 단점: 삽입 시 연결 후보 평가량 증가, 메모리 사용량 증가, 빌드 시간 증가

1-3. 배치 업서트가 잘못됨(너무 작거나, 너무 큼)

벡터 DB는 보통 네트워크/직렬화/쓰기 경로가 병목이 되기도 합니다.

  • 너무 작은 배치: 요청 오버헤드가 지배 → QPS 하락
  • 너무 큰 배치: 한 번에 처리하는 동안 큐가 막히고, 메모리/GC/머지 작업이 폭증 → tail latency 증가

1-4. 세그먼트/컴팩션(머지) 비용이 뒤에서 터짐

특히 Milvus 계열에서 흔한데, 지속적으로 삽입하면 세그먼트가 늘고, 백그라운드 컴팩션이 시작되면서 인덱싱과 컴팩션이 자원을 경쟁합니다.

  • 체감 증상: 초반엔 빠르다가 어느 시점부터 갑자기 느려짐
  • 해결 방향: 인덱스 빌드 타이밍/세그먼트 정책/리소스 분리

2) HNSW 핵심 파라미터: MefConstruction을 어떻게 잡나

HNSW 튜닝은 결국 3가지 균형입니다.

  • 인덱싱 속도(삽입 처리량)
  • 메모리(그래프 엣지 저장 비용)
  • 검색 품질(리콜/정확도)

2-1. M 가이드

일반적인 출발점은 다음과 같습니다.

  • 384~768 차원 임베딩(예: sentence-transformers, OpenAI 계열): M=16 또는 M=24
  • 1024~1536 차원: M=24 또는 M=32

인덱싱이 느리다면 먼저 M을 줄이는 게 즉효가 큰 편입니다. 다만 M을 너무 낮추면 리콜이 급락하거나 검색 결과가 불안정해질 수 있습니다.

2-2. efConstruction 가이드

efConstruction은 삽입 시 탐색 폭입니다. 보통 다음처럼 시작합니다.

  • 빠른 인덱싱 우선: efConstruction=64 또는 100
  • 품질 우선(오프라인 빌드): efConstruction=200~400

실무적으로는 efConstruction을 먼저 낮춰서 인덱싱 병목을 풀고, 검색 시 ef(검색 파라미터)를 올려 리콜을 보정하는 접근이 안전합니다.

2-3. 검색 파라미터 ef(또는 유사 파라미터)와의 관계

많은 시스템에서 검색 시 ef를 올리면 리콜이 오르지만 지연이 늘어납니다.

  • 인덱싱이 느리다: efConstruction을 낮추는 게 정답일 가능성이 큼
  • 검색이 부정확하다: 먼저 ef를 올려보고, 그래도 부족하면 그때 M/efConstruction을 재검토

3) Pinecone에서 인덱싱이 느릴 때 체크리스트

Pinecone은 관리형이라 내부 세그먼트/컴팩션을 직접 만지기 어렵지만, 대신 “인덱스 타입/파라미터/적재 패턴”이 핵심입니다.

3-1. 업서트 배치 크기와 동시성부터 조정

가장 먼저 할 일은 배치 크기와 병렬 업서트를 튜닝하는 것입니다.

  • 배치 크기: 100~500부터 시작해 점진적으로 증가
  • 동시성: 4~32 사이에서 지표를 보며 조정

아래는 Python에서 스레드 풀로 업서트를 병렬화하는 예시입니다.

from concurrent.futures import ThreadPoolExecutor, as_completed

BATCH = 200
WORKERS = 16

# vectors: list[tuple[str, list[float], dict]]  # (id, vector, metadata)

def chunks(xs, n):
    for i in range(0, len(xs), n):
        yield xs[i:i+n]

def upsert_batch(index, batch):
    # Pinecone client 형태는 버전에 따라 다를 수 있음
    return index.upsert(vectors=batch)

futures = []
with ThreadPoolExecutor(max_workers=WORKERS) as ex:
    for batch in chunks(vectors, BATCH):
        futures.append(ex.submit(upsert_batch, index, batch))

    for f in as_completed(futures):
        _ = f.result()

튜닝 요령은 간단합니다.

  • 처리량이 낮다: 배치 또는 동시성 증가
  • tail latency가 튄다/에러가 늘어난다: 배치 또는 동시성 감소

3-2. 메타데이터가 과도하게 크지 않은지 확인

Pinecone에서 메타데이터는 필터링에 유용하지만, 적재 비용과 저장 비용에 영향을 줍니다.

  • 큰 원문 텍스트를 메타데이터에 그대로 넣는 패턴은 피하기
  • 원문은 오브젝트 스토리지나 DB에 두고, 메타데이터에는 키만 저장

3-3. 인덱스 스펙과 리소스(파드/노드 규모)를 의심

관리형에서도 결국 리소스가 부족하면 인덱싱이 밀립니다.

  • 증상: CPU/메모리 제한에 가까운 패턴, 지속적인 backpressure
  • 해결: 스펙 상향, 샤딩/파티션 전략 재검토

4) Milvus에서 인덱싱이 느릴 때 체크리스트

Milvus는 구성 요소가 많아(Proxy, QueryNode, DataNode, IndexNode 등) 병목이 더 다양합니다. 대신 조절 가능한 레버도 많습니다.

4-1. HNSW 인덱스 파라미터 예시

Milvus에서 HNSW를 만들 때는 보통 아래처럼 파라미터를 지정합니다(버전/SDK에 따라 API는 다를 수 있습니다).

index_params = {
    "index_type": "HNSW",
    "metric_type": "COSINE",
    "params": {
        "M": 16,
        "efConstruction": 100
    }
}

collection.create_index(
    field_name="embedding",
    index_params=index_params
)

인덱싱이 느리다면 다음 순서로 낮춰보는 것을 권장합니다.

  1. efConstruction을 200 -> 100 -> 64로 단계적으로 감소
  2. 그래도 느리면 M을 32 -> 24 -> 16으로 감소

위 화살표 표기는 반드시 인라인 코드로 감쌌습니다.

4-2. “실시간 삽입 + 즉시 인덱싱”을 강요하지 않기

Milvus에서 가장 흔한 운영 실수는 다음입니다.

  • 계속 insert
  • 동시에 인덱스 빌드/머지
  • 동시에 서빙 쿼리

이 3개를 같은 리소스 풀에서 돌리면, 인덱싱이 느려지거나 쿼리 지연이 튀기 쉽습니다.

권장 패턴은 2가지입니다.

  • 오프라인 빌드형: 대량 적재 후 인덱스 생성/빌드, 그 다음 서빙
  • 하이브리드형: 최신 데이터는 brute force(또는 작은 인덱스)로 보조 검색, 일정 주기로 메인 인덱스에 병합

4-3. 컴팩션/세그먼트 정책으로 뒤통수 맞는 경우

인덱싱이 “처음엔 빠르다가 점점 느려지는” 패턴이면 컴팩션을 의심해야 합니다.

  • 세그먼트가 너무 잘게 쪼개져 있으면, 인덱스 빌드 단위가 비효율적
  • 컴팩션이 인덱스 노드/데이터 노드 자원을 잡아먹으면 업서트가 밀림

해결은 운영 환경에 따라 다르지만, 핵심은 다음입니다.

  • 인덱스 빌드와 대량 적재를 같은 시간대에 겹치지 않게 스케줄링
  • 인덱스 노드 리소스를 분리하거나 확장
  • 모니터링으로 “insert rate, flush, compaction, index build queue”를 함께 보기

5) 튜닝 절차: 빠르게 원인 좁히는 6단계

아래 순서로 하면 “파라미터를 이것저것 바꿔봤는데 왜 좋아졌는지 모르는 상태”를 피할 수 있습니다.

5-1. 목표를 수치로 고정

  • 인덱싱 처리량: 초당 업서트 벡터 수
  • 검색 품질: top-k 리콜(가능하면 정답셋 기반)
  • 검색 지연: p50/p95

5-2. 적재 방식부터 최적화

  • 배치 크기, 동시성, 네트워크 오버헤드 제거
  • 메타데이터 최소화

5-3. efConstruction을 먼저 낮추기

인덱싱 느림 이슈의 체감 효과가 가장 큰 레버입니다.

5-4. M을 조정

리콜이 유지되는 최소 M을 찾는 방향이 좋습니다.

5-5. 검색 시 ef로 리콜 보정

인덱싱을 위해 efConstruction을 낮췄다면, 검색 시 ef를 올려 리콜을 맞춥니다.

5-6. 회귀 테스트(품질) 자동화

튜닝은 성능만 올리면 실패합니다. 검색 품질이 떨어지면 RAG 답변 품질이 바로 흔들립니다.

간단한 리콜 측정 예시는 아래처럼 만들 수 있습니다.

def recall_at_k(retrieved_ids, relevant_ids):
    # retrieved_ids: list[str]
    # relevant_ids: set[str]
    hit = 0
    for rid in retrieved_ids:
        if rid in relevant_ids:
            hit += 1
    return hit / max(1, len(relevant_ids))

# 예: 쿼리별 relevant set을 준비해두고, 파라미터 변경 전후를 비교

6) 흔한 오해 5가지(실무에서 자주 터짐)

6-1. “리콜이 낮으니 efConstruction부터 올리자”

대량 삽입 환경에서는 인덱싱 비용이 폭증해 전체 파이프라인이 무너질 수 있습니다. 먼저 검색 ef로 보정 가능한지 확인하세요.

6-2. “M을 올리면 무조건 품질이 좋아진다”

품질은 좋아질 수 있지만, 메모리/인덱싱 시간이 같이 증가합니다. 특히 고차원에서 M=48 같은 값은 비용이 급격히 커질 수 있습니다.

6-3. “인덱싱이 느리면 DB가 문제다”

실제로는 업서트 배치가 너무 작거나, 메타데이터가 과도하거나, 네트워크 병목인 경우가 많습니다.

6-4. “실시간으로 다 인덱싱돼야 한다”

실시간성이 정말 필요한 범위(예: 최근 1시간 데이터)만 별도로 다루고, 나머지는 배치/주기적 병합으로 설계하면 비용이 크게 줄어듭니다.

6-5. “품질 검증은 나중에 하자”

HNSW 튜닝은 품질과 직결됩니다. 최소한의 회귀 테스트(리콜/정답률)를 파라미터 변경과 함께 묶어야 합니다.

7) 운영 팁: 컨테이너/노드 환경에서 성능이 흔들릴 때

Milvus를 Kubernetes나 Docker에서 돌릴 때, CPU 제한/NUMA/스토리지/네트워크가 인덱싱 속도에 큰 영향을 줍니다. 특히 GPU를 쓰는 구성(또는 GPU가 필요하다고 믿는 구성)에서는 “호스트는 되는데 컨테이너에서 장치를 못 잡는” 류의 문제도 성능/안정성을 악화시킵니다.

GPU/드라이버/컨테이너 런타임 이슈를 겪고 있다면 이 글이 진단에 도움이 됩니다: Docker에서 NVIDIA GPU가 안 잡힐 때 nvidia-smi는 되는데 컨테이너에서 CUDA not found 뜨는 7가지 원인과 해결

8) 결론: 가장 안전한 HNSW 튜닝 전략

  • 인덱싱이 느리면, 먼저 업서트 배치/동시성/메타데이터를 점검
  • 그 다음 efConstruction을 낮춰 삽입 비용을 줄이기
  • 필요하면 M을 낮추되, 리콜 회귀 테스트로 품질 하락을 감시
  • 검색 품질은 가능하면 검색 ef로 보정하고, 오프라인 빌드/병합 전략으로 운영 복잡도를 낮추기

이 과정을 지표 기반으로 반복하면 Pinecone든 Milvus든 “인덱싱이 병목이라 전체 RAG 파이프라인이 밀리는 상태”에서 벗어나, 비용과 성능을 예측 가능한 범위로 가져올 수 있습니다.