Qdrant 컬렉션에 벡터 대량 업로드하기

Qdrant 컬렉션에 벡터 대량 업로드하기 (bulk-upload)

대용량 데이터셋을 빠르게 업로드하는 건 꽤 까다로운 일이지만, Qdrant는 이를 돕는 여러 전략을 제공해요. 이 페이지를 읽고 나면 대량 업로드 시 병목이 어디서 오는지, 그리고 어떤 순서로 세팅하면 가장 빠르게 데이터를 넣을 수 있는지 알게 됩니다.

> 출처: [Qdrant 공식 문서 — bulk-upload](https://qdrant.tech/documentation/manage-data/bulk-upload)

업로드 중 병목은 보통 서버가 아니라 클라이언트 쪽에서 발생합니다. 따라서 큰 데이터셋을 올릴 땐 고성능 클라이언트 라이브러리를 쓰는 게 좋아요. Qdrant에서 가장 빠른 클라이언트로 알려진 Rust 클라이언트 라이브러리를 권장합니다.

업로드를 배치로 묶기 (Batch Your Uploads)

포인트를 하나씩 올리지 말고 배치 단위로 upsert하세요. Qdrant로 가는 각 요청에는 오버헤드가 있어요. 네트워크 왕복, Write-Ahead Log(WAL) 쓰기, 내부 라우팅이 그것이죠. 포인트를 개별 업로드하면 이 오버헤드가 처리량(throughput)에 그대로 영향을 줍니다.

배치당 64~256개 포인트를 목표로 하세요. 더 작은 배치는 네트워크를 충분히 활용하지 못하고, 더 큰 배치는 서버 메모리 압력을 높이고 실패 시 재시도 비용도 키울 수 있어요. 최적의 배치 크기는 데이터와 클러스터에 따라 달라지므로, 최상의 성능을 위해 여러 크기로 실험해 보는 걸 권장합니다.

여러 스레드로 병렬화하기 (Parallelize Across Multiple Threads)

업로드 스레드 하나로는 서버를 거의 포화시키지 못해요. 데이터셋을 2~4개의 동시 스레드로 나누고, 각 스레드가 자신만의 배치 스트림을 보내게 하세요. 그러면 Qdrant 내부 쓰기 워커가 샤드 전반에서 바쁘게 유지되고 총 업로드 시간이 줄어듭니다.

컬렉션에 샤드가 여러 개 있다면 샤드당 업로드 스레드 하나를 시작 지점으로 삼으세요. 각 샤드는 독립적인 WAL과 업데이트 워커를 갖고 있어서, 병렬 스트림이 사용 가능한 쓰기 용량에 그대로 매핑됩니다.

Python 클라이언트의 upload_points 메서드는 배칭과 병렬화를 알아서 처리해 줘요. 포인트 이터레이터를 넘기고 batch_sizeparallel을 설정하면 배치를 직접 관리할 필요 없이 처리량을 조절할 수 있어요. 다른 클라이언트 라이브러리는 배칭과 병렬화를 직접 구현해야 합니다.

샤드가 여러 개인 컬렉션 만들기 (Create Collections with Multiple Shards)

Qdrant에서 각 컬렉션은 샤드(shard) 로 나뉘어요. 기본적으로 컬렉션은 샤드 하나를 갖지만, 컬렉션 생성 시 더 많이 지정할 수 있어요. 샤드를 여러 개 만들면 대용량 데이터셋 업로드를 병렬화할 수 있죠. 머신당 샤드 2~4개가 합리적인 수치입니다.

PUT /collections/{collection_name}
{
    "vectors": {
      "size": 768,
      "distance": "Cosine"
    },
    "shard_number": 2
}
from qdrant_client import QdrantClient, models

client.create_collection(
    collection_name="{collection_name}",
    vectors_config=models.VectorParams(size=768, distance=models.Distance.COSINE),
    shard_number=2,
)
import { QdrantClient } from "@qdrant/js-client-rest";

client.createCollection("{collection_name}", {
  vectors: {
    size: 768,
    distance: "Cosine",
  },
  shard_number: 2,
});
use qdrant_client::qdrant::{CreateCollectionBuilder, Distance, VectorParamsBuilder};
use qdrant_client::Qdrant;

client
    .create_collection(
        CreateCollectionBuilder::new("{collection_name}")
            .vectors_config(VectorParamsBuilder::new(768, Distance::Cosine))
            .shard_number(2),
    )
    .await?;

데이터 수집 전에 Payload 인덱스 만들기 (Create Payload Indexes Before Ingesting Data)

컬렉션이 payload 인덱스를 사용한다면 포인트 업로드를 시작하기 전에 인덱스를 만들어 주세요. Qdrant는 각 payload 인덱스에 대해 추가 HNSW 링크를 만들어 필터링된 벡터 검색 품질을 최적화해요. HNSW 그래프가 이미 만들어진 뒤에 payload 인덱스를 추가하면 그 링크가 존재하지 않아서, 필터 검색이 그래프를 다시 만들기 전까지 더 느린 쿼리 타임 전략으로 대체됩니다. 그리고 그래프 재구성은 리소스를 많이 먹고 오래 걸릴 수 있어요.

올바른 순서는 이렇습니다:

  1. 컬렉션을 만든다.
  2. 모든 payload 인덱스를 만든다.
  3. 포인트를 업로드한다.

이 순서를 지키면 Qdrant가 그래프를 한 번에 만들 수 있어서, 사후에 다시 만드는 수고를 피할 수 있어요.

디스크에 직접 업로드하기 (Upload Directly to Disk)

업로드할 벡터가 전부 RAM에 들어가지 못할 때는, 벡터를 cold 메모리 티어로 직접 옮기는 게 좋아요.

컬렉션 생성 시 벡터별로 memory 파라미터를 cold로 설정하면, 벡터 데이터가 항상 디스크에 직접 저장됩니다.

이 경우 memmap_threshold를 쓰는 것은 권장하지 않아요. memmap_threshold는 옵티마이저가 세그먼트를 계속해서 cold 티어로 변환해야 하게 만드는데, 이 과정은 느리고 대량 데이터 수집 시 옵티마이저가 병목이 될 수 있어요.

전체 설정 방법은 'Configuring Memmap Storage' 문서를 참고하세요.

읽기-쓰기 경쟁 완화하기 (Mitigate Read-Write Contention)

대량 업로드는 Qdrant의 백그라운드 옵티마이저를 통해 연속적인 쓰기 스트림을 밀어 넣어요. 새 데이터가 들어올 때마다 HNSW 인덱스를 만들고, 세그먼트를 병합하고, 양자화를 적용해야 하죠. 동시에 검색 쿼리를 돌리고 있다면 옵티마이저와 쿼리가 동일한 CPU 시간, 메모리 대역폭, I/O를 두고 경쟁합니다. 이로 인해 수집 중 쿼리 지연 시간이 눈에 띄게 올라갈 수 있어요.

업로드하면서 검색도 계속 제공해야 한다면, 'Read-Write Contention' 문서에서 무거운 쓰기 부하 아래 읽기 지연 시간을 개선하는 일련의 설정 변경 사항을 확인하세요.

더 알아보기 (Learn more)