Qdrant 대량 업로드

Qdrant 대량 업로드 (Bulk Uploads in Qdrant)

Qdrant를 실제 규모로 쓰기 시작하면 맨 처음 만나는 벽이 있어요. 바로 많은 양의 데이터를 효율적으로 업로드하는 것이죠. 작은 업로드는 대개 어렵지 않은데, 대량 수집(bulk ingestion)은 이야기가 달라요. 수백만 개의 벡터와 페이로드, 인덱스가 컬렉션에 쓰이면서, 시스템은 메모리 사용량과 디스크 쓰기, 백그라운드 최적화, 검색 가용성을 동시에 관리해야 하거든요.

이 과정을 신중하게 계획하지 않으면 대량 업로드가 RAM에 부담을 주고, 수집을 느리게 만들고, 쿼리 지연시간을 늘리거나, 옵티마이저가 뒤쳐지게 만들 수 있어요. 더 제약이 많은 환경에서는 대량 업로드가 아예 메모리 부족 문제나 불안정한 성능으로 이어질 수도 있어요.

목표는 단순히 데이터를 최대한 빨리 업로드하는 게 아니에요. 실행 중인 워크로드에 대해 예측 가능하고 안전한 방식으로 데이터를 업로드하는 것이 목표예요. 이 가이드에서는 배칭, 병렬화, 샤딩, 페이로드 인덱스, 온디스크 벡터 저장을 포함한 Qdrant 대량 업로드의 모범 사례를 함께 살펴볼게요.

출처: 공식문서

벡터 타입이 왜 중요할까?

모범 사례에 들어가기 전에, 모든 벡터가 수집 중에 똑같이 동작하지 않는다는 점을 기억하는 게 중요해요. Dense 벡터와 sparse 벡터는 서로 다른 인덱싱 방식을 사용해서, 대량 업로드 중에 서로 다른 성능 고려사항을 만들 수 있어요.

권장 업로드 전략을 살펴보기 전에 그 차이를 빠르게 짚고 넘어갈게요.

Dense/sparse 벡터는 Qdrant에서 서로 다른 인덱싱 경로를 사용하기 때문에 수집 중에 다르게 동작해요. Dense 벡터는 빠른 유사도 검색을 위해 HNSW에 의존해요. 대량 업로드 중에는 새 세그먼트가 쓰이면서 백그라운드 옵티마이저가 이 인덱스를 만들고 갱신해요. 이는 업로드가 진행되는 동안 CPU와 메모리 부담을 더할 수 있어요.

Sparse 벡터는 별도의 인덱싱 방식을 사용하고, 포인트가 쓰이면서 sparse 인덱스가 갱신돼요. 즉 sparse 벡터 수집은 dense HNSW 인덱싱과 같은 방식으로 다뤄서는 안 돼요.

올바른 대량 업로드 전략 고르기

모범 사례를 살펴보기 전에, 모든 대량 업로드에 최적인 단일 구성은 없다는 점을 이해해야 해요. 올바른 접근 방식은 무엇을 개선하려는지에 따라 달라져요: 업로드 속도, 메모리 사용량, 검색 가용성, 아니면 셋의 균형. 가장 안전한 접근은 하나의 보편적인 설정에 의존하는 대신, 워크로드에 맞는 올바른 전략을 선택하는 거예요.

옵션 1: 메모리 부담 줄이기

Dense vectors

메모리 사용량은 대량 업로드 중 첫 번째 병목이 될 수 있어요. Dense 벡터는 보통 고정 크기의 임베딩이고, 수백만 개가 컬렉션에 삽입되면 원시 벡터 데이터만으로도 RAM을 많이 차지할 수 있어요.

더 안전한 접근은 컬렉션을 만들 때 dense 벡터를 처음부터 디스크에 직접 저장하는 거예요. 이렇게 하면 들어오는 벡터 데이터가 처음부터 memmap 저장을 사용해서, 나중에 백그라운드 최적화가 벡터를 메모리에서 디스크로 옮기는 것에 의존하지 않아도 돼요.

Python에서는 VectorParams 안의 on_disk=True로 설정할 수 있어요.

client.create_collection(
    collection_name="my_collection",
    vectors_config=models.VectorParams(
        size=768,
        distance=models.Distance.COSINE,
        on_disk=True,
    ),
)

가장 잘 맞는 경우: 원시 벡터 데이터가 RAM에 부담을 줄 수 있는 대규모 dense 벡터 업로드.

주의할 점: 워크로드가 원본 벡터를 자주 읽어야 한다면, 검색 성능이 디스크 접근에 더 의존하게 될 수 있어요. 보통 검색 시점의 양자화로 균형을 맞출 수 있지만, 대량 업로드 관점에서 중요한 건 벡터 저장이 처음부터 안전하게 처리된다는 거예요.

옵션 2: 페이로드 인덱스 만들기 (업로드 전에)

Dense vectors

필터링에 사용할 필드를 이미 알고 있다면, 포인트를 업로드하기 전에 페이로드 인덱스를 만들어 두세요. 이게 중요한 이유는 dense 벡터 검색이 종종 HNSW에 의존하기 때문이에요. 필터가 쿼리의 일부일 때 Qdrant는 페이로드 인덱스를 사용해 필터링된 검색을 더 효율적으로 만들 수 있어요.

그런 인덱스가 큰 데이터셋을 업로드한 후에 만들어지면, HNSW 그래프가 재구축될 때까지 필터링된 검색이 더 느린 쿼리 시점 전략으로 빠져요. 사후에 그래프를 재구축하는 건 리소스 집약적이고 오래 걸릴 수 있어요.

업로드 전에 페이로드 인덱스를 만들어 두세요.

client.create_payload_index(
    collection_name="my_collection",
    field_name="category",
    field_schema=models.PayloadSchemaType.KEYWORD,
)

가장 잘 맞는 경우: category, tenant ID, document type, source, user ID처럼 어떤 페이로드 필드를 필터링에 사용할지 이미 아는 워크로드.

주의할 점: 페이로드 인덱스는 의도적으로 만들어야 해요. 필터링에 쓰지 않는 필드를 인덱싱하면 업로드나 검색 경로에 도움이 되지 않는 추가 작업만 늘어날 수 있어요.

옵션 3: 양자화로 메모리와 검색 성능 균형 맞추기

Dense vectors

원본 벡터를 디스크에 저장하면 대량 업로드 중 메모리 부담을 줄이는 데 도움이 돼요. 하지만 특히 Qdrant가 원본 벡터를 자주 읽어야 할 때, 검색을 디스크 접근에 더 의존하게 만들 수도 있어요.

양자화가 이 트레이드오프의 균형을 잡는 데 도움이 돼요. 전체 크기의 dense 벡터를 메모리에 유지하는 대신, 원본 벡터는 디스크에 남겨두고 압축된 버전을 메모리에 준비해둘 수 있어요.

Python에서 컬렉션을 만들 때 TurboQuant를 구성해요. bits 파라미터가 압축 수준을 설정해요: BITS4(기본값)는 전체 정밀도에 가장 가깝고, BITS1이 가장 많이 압축해요.

client.create_collection(
    collection_name="my_collection",
    vectors_config=models.VectorParams(
        size=768,
        distance=models.Distance.COSINE,
        on_disk=True,
    ),
    quantization_config=models.TurboQuantization(
        turbo=models.TurboQuantQuantizationConfig(
            always_ram=True,
            bits=models.TurboQuantBitSize.BITS4,
        )
    ),
)

가장 잘 맞는 경우: 검색 성능을 실용적으로 유지하면서 메모리 사용량을 낮춰야 하는 dense 벡터 워크로드.

주의할 점: 양자화는 워크로드와 구성에 따라 정밀도에 영향을 줄 수 있어요. 많은 사례에서 이 트레이드오프는 가치가 있지만, 검색 품질과 지연시간은 실제 데이터로 테스트해야 해요.

옵션 4: 업로드 중 sparse 인덱스 메모리 줄이기

Sparse vectors

대규모 sparse 벡터 워크로드의 한 가지 옵션은 sparse 벡터 인덱스를 디스크에 저장하는 거예요. 이는 sparse 인덱스가 커질 때 메모리 사용량을 줄이는 데 도움이 돼요.

sparse 인덱스에 온디스크 저장을 활성화해요.

client.create_collection(
    collection_name="my_collection",
    vectors_config={},
    sparse_vectors_config={
        "text": models.SparseVectorParams(
            index=models.SparseIndexParams(
                on_disk=True,
            )
        )
    },
)

가장 잘 맞는 경우: sparse 인덱스가 메모리에 부담을 주는 대규모 sparse 벡터 워크로드.

주의할 점: sparse 인덱스를 디스크에 저장하면 쿼리가 디스크 접근에 더 의존할 수 있어서 검색이 느려질 수 있어요. sparse 벡터 검색이 지연시간에 민감하다면 sparse 인덱스를 메모리에 유지하는 게 더 나을 수 있어요.

모든 업로드에 적용되는 모범 사례

위의 전략들은 벡터 타입, 메모리 한도, 검색 요구 같은 워크로드에 따라 달라져요. 아래 기법들은 달라요. 배칭, 병렬화, 샤딩은 상황에 따라 고르는 선택이 아니라, 모든 대량 업로드에 적용되고 컬렉션 구성과 무관하게 수집 처리량과 안정성을 높여 주는 기법이에요.

팁: 대량 작업에는 QdrantClient(url, prefer_grpc=True)로 연결하세요. gRPC는 HTTP보다 오버헤드가 낮고, 대규모 업로드에서 의미 있게 더 빠릅니다.

업로드를 배치로 나누기

Dense & sparse vectors

포인트를 한 번에 하나씩 업로드하면 불필요한 오버헤드가 생겨요. 각 요청이 네트워크, 쓰기 경로, 내부 처리를 거쳐야 하니까요. 이게 수백만 번 반복되면 업로드 프로세스가 필요 이상으로 느려질 수 있어요.

더 좋은 접근은 포인트를 **배치(batch)**로 업로드하는 거예요. 배칭을 쓰면 Qdrant가 각 포인트를 개별 요청으로 처리하는 대신 포인트 그룹을 함께 처리할 수 있어요.

포인트를 업로드할 때 배치 크기를 설정해요.

client.upload_points(
    collection_name="my_collection",
    points=points,
    batch_size=256,
)

가장 잘 맞는 경우: 요청당 하나의 포인트를 보내면 요청 오버헤드가 너무 커지는 대규모 업로드.

주의할 점: 배치 크기 64-256 포인트가 합리적인 시작 범위예요. 더 큰 배치는 처리량을 높일 수 있지만 메모리 사용량을 늘리고, 요청 실패 시 재시도 비용을 더 비싸게 만들어요.

업로드 병렬화하기

Dense & sparse vectors

단일 업로드 스트림은 Qdrant 배포의 사용 가능한 쓰기 용량을 완전히 활용하지 못할 수 있어요. 대용량 데이터셋을 업로드할 때는 여러 배치를 병렬로 보내면 수집 처리량을 개선할 수 있는 경우가 많아요.

병렬 업로드는 여러 워커가 데이터셋의 서로 다른 부분을 동시에 업로드할 수 있게 해 줘요. 특히 컬렉션에 여러 샤드가 있을 때 Qdrant의 쓰기 파이프라인을 활성 상태로 유지해요.

참고: 병렬화 이득이 항상 선형적이지는 않아요. 일부 구성에서는 더 높은 수에서 개선이 나타나기 전까지 2개의 워커가 1개와 비슷하게 동작할 수 있어요.

업로드에 병렬 워커를 추가해요.

client.upload_points(
    collection_name="my_collection",
    points=points,
    batch_size=256,
    parallel=4,
)

가장 잘 맞는 경우: 단일 업로드 워커로는 사용 가능한 쓰기 용량을 활용하지 못하는 대규모 업로드.

주의할 점: 너무 과한 병렬화는 CPU, 메모리, 디스크 I/O, 네트워크 리소스에 추가 부담을 줄 수 있어요. 더 적은 수로 시작해서 시스템 동작을 보고 늘리세요.

더 큰 업로드에는 여러 샤드 사용하기

Dense & sparse vectors

더 큰 업로드에서는 샤딩이 Qdrant가 쓰기를 병렬로 처리하는 데 도움이 돼요. 컬렉션은 두 개 이상의 샤드로 만들 수 있고, 각 샤드는 고유한 쓰기 경로를 가져요. 여러 샤드가 있으면 Qdrant가 수집 작업을 독립적인 쓰기 경로에 분산해요.

컬렉션을 만들 때 샤드 수를 설정해요.

client.create_collection(
    collection_name="my_collection",
    vectors_config=models.VectorParams(
        size=768,
        distance=models.Distance.COSINE,
        on_disk=True,
    ),
    shard_number=2,
)

가장 잘 맞는 경우: 병렬 업로드 워커와 함께 쓸 때 더 많은 수집 병렬화를 원하는 대규모 업로드.

주의할 점: 샤드가 많다고 항상 더 좋은 건 아니에요. 각 샤드는 오버헤드를 추가하므로, 샤드 수는 배포 규모와 실제로 필요한 쓰기 병렬화 양에 맞춰야 해요.

올바른 조합 고르기

여전히 워크로드에 정확히 무엇을 구성할지 결정 중이라면, Qdrant의 Agent Skills가 상황에 맞는 구체적인 설정을 안내하는 실습형 시나리오 기반 가이드를 제공해요.

만능 해법은 없다

대량 업로드는 단지 가능한 한 많은 데이터를 Qdrant에 보내는 것이 아니에요. 데이터셋이 커질수록 업로드 프로세스는 메모리 사용량, 인덱싱 동작, 디스크 쓰기, 검색 가용성, 전반적인 시스템 안정성도 고려해야 해요.

가장 안전한 접근은 하나의 보편적인 구성에 의존하는 대신 워크로드에 맞는 올바른 전략을 고르는 거예요. Dense 벡터, sparse 벡터, 하이브리드 구성 모두 수집 중에 서로 다른 성능 고려사항을 만들 수 있어요.

팁: 대규모 업로드 후에는 운영 트래픽을 서빙하기 전에 컬렉션 상태가 green이고 옵티마이저가 끝났는지 확인하세요.

수집을 시작하기 전에 컬렉션과 업로드 프로세스를 설계하면, 대량 업로드를 더 효율적이고 안정적으로 만들고 데이터셋이 커짐에 따라 확장하기 쉽게 만들 수 있어요. 배포 규모를 정하려면 Qdrant 사이징 계산기를 사용하세요.

더 알아보기 (Learn more)