-
Notifications
You must be signed in to change notification settings - Fork 139
Add article: How to Handle Bulk Uploads in Qdrant #2489
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
jkupchanko
wants to merge
13
commits into
master
Choose a base branch
from
article/bulk-uploads-in-qdrant
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
5572708
Add article: How to Handle Bulk Uploads in Qdrant
jkupchanko c4f2ca2
Updated preview images and article title/date
kanungle e9337e1
unpublish older article on this topic
kanungle 0922daf
Address review feedback on bulk uploads article
jkupchanko ecc0365
Address second review round (kanungle, abdonpijpelink)
jkupchanko 4515784
feat(article): decision-tree diagram, refreshed strategy images, bestβ¦
jkupchanko 1104a2b
fix(article): address review β frontmatter, redirect, TurboQuant bitsβ¦
jkupchanko 84c517e
perf(article): compress the three large diagrams under 250KB
jkupchanko cc798d5
fix(article): decision-tree diagram with labels inside boxes, single β¦
jkupchanko 0063e25
docs(article): add environment and scale context to benchmark callouts
jkupchanko 0dc0fe0
docs(article): anchor benchmark callouts to the confirmed test enviroβ¦
jkupchanko a7f50d9
docs(article): use measured benchmark numbers and Qdrant 1.18.2
jkupchanko 7067a11
docs(article): use measured 18x batching result from re-run
jkupchanko File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
258 changes: 258 additions & 0 deletions
258
qdrant-landing/content/articles/bulk-uploads-in-qdrant.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,258 @@ | ||
| --- | ||
| title: "Bulk Uploading Data to Qdrant" | ||
| short_description: "Plan bulk uploads in Qdrant at scale: batching, parallelization, sharding, payload indexes, quantization, and on-disk storage." | ||
| description: "Plan bulk uploads in Qdrant: batching, parallelization, sharding, payload indexes, quantization, and on-disk storage." | ||
| preview_dir: /articles_data/bulk-uploads-in-qdrant/preview | ||
| social_preview_image: /articles_data/bulk-uploads-in-qdrant/preview/social_preview.jpg | ||
| weight: 35 | ||
| author: John Kupchanko | ||
| author_link: https://github.com/jkupchanko | ||
| keywords: | ||
| - bulk upload | ||
| - vector database | ||
| - batching | ||
| - quantization | ||
| - sharding | ||
| category: production-ops | ||
| date: 2026-07-14T00:00:00.000Z | ||
| draft: false | ||
| --- | ||
|
|
||
| ## Why Bulk Uploading Matters | ||
|
|
||
| When you start using Qdrant at scale, one of the first challenges you may run into is uploading large amounts of data efficiently. Small uploads are usually straightforward, but bulk ingestion introduces a different set of concerns. As millions of vectors, payloads, and indexes are written into a collection, the system has to manage memory usage, disk writes, background optimization, and search availability at the same time. | ||
|
|
||
| If this process is not planned carefully, bulk uploads can create pressure on RAM, slow down ingestion, increase query latency, or cause the optimizer to fall behind. In more constrained environments, large uploads can even lead to out-of-memory issues or unstable performance. | ||
|
|
||
| The goal is not simply to upload data as fast as possible. The goal is to upload data in a way that is predictable and safe for the workload you are running. In this guide, we'll walk through best practices for bulk uploads in Qdrant, including batching, parallelization, sharding, payload indexes, and on-disk vector storage. | ||
|
|
||
| ## Why Vector Type Matters | ||
|
|
||
| Before we get into the best practices, it's important to remember that not all vectors behave the same way during ingestion. Dense and sparse vectors use different indexing approaches, which means they can create different performance considerations during bulk uploads. | ||
|
|
||
| Let's quickly break down the difference before moving into the recommended upload strategies. | ||
|
|
||
| Dense and sparse vectors behave differently during ingestion because they use different indexing paths in Qdrant. Dense vectors rely on HNSW for fast similarity search. During a large upload, the background optimizer builds and updates this index as new segments are written. This can add CPU and memory pressure while uploads are in progress. | ||
|
|
||
| Sparse vectors use a separate indexing approach, and the sparse index is updated as points are written. This means sparse vector ingestion should not be treated the same way as dense HNSW indexing. | ||
|
|
||
| ## Choosing the Right Bulk Upload Strategy | ||
|
|
||
| Before we go through the best practices, understand **there is no single configuration that works best for every bulk upload**. The right approach depends on what you are trying to improve: upload speed, memory usage, search availability, or a balance of all three. | ||
|
|
||
| The safest approach is to choose the right strategy for the workload instead of relying on one universal setting. | ||
|
|
||
| ## Option 1: Reduce Memory Pressure | ||
|
|
||
| _Dense vectors_ | ||
|
|
||
| Memory usage can become one of the first bottlenecks during a large upload. Dense vectors are usually fixed-size embeddings, and when millions of them are inserted into a collection, the raw vector data alone can take up a large amount of RAM. | ||
|
|
||
| A safer approach is to store dense vectors directly on-disk when the collection is created. This allows incoming vector data to use memmap storage from the beginning, instead of relying on background optimization to move vectors from memory to disk later. | ||
|
|
||
|  | ||
|
|
||
| In Python, you can configure this with `on_disk=True` inside `VectorParams`: | ||
|
|
||
| ```python | ||
| client.create_collection( | ||
| collection_name="my_collection", | ||
| vectors_config=models.VectorParams( | ||
| size=768, | ||
| distance=models.Distance.COSINE, | ||
| on_disk=True, | ||
| ), | ||
| ) | ||
| ``` | ||
|
|
||
| **Best fit:** Large dense vector uploads where raw vector data may put pressure on RAM. | ||
|
|
||
| **Watch for:** Search performance may depend more on disk access, especially if the workload needs to read original vectors often. You can usually balance this with quantization at search time, but the important part for bulk uploads is that vector storage is handled safely from the beginning. | ||
|
|
||
| ## Option 2: Create Payload Indexes (Before Uploading) | ||
|
|
||
| _Dense vectors_ | ||
|
|
||
| Use payload indexes before uploading points when you already know which fields will be used for filtering. This matters because dense vector search often relies on HNSW. When filters are part of the query, Qdrant can use payload indexes to make filtered search more efficient. | ||
|
|
||
| If those indexes are created after a large dataset has already been uploaded, filtered search will fall back to slower query-time strategies until the HNSW graph is rebuilt. Rebuilding the graph after the fact is resource-intensive and can take a long time. | ||
|
|
||
|  | ||
|
|
||
| Create the payload index before uploading: | ||
|
|
||
| ```python | ||
| client.create_payload_index( | ||
| collection_name="my_collection", | ||
| field_name="category", | ||
| field_schema=models.PayloadSchemaType.KEYWORD, | ||
| ) | ||
| ``` | ||
|
|
||
| **Best fit:** Workloads that already know which payload fields will be used for filtering, such as category, tenant ID, document type, source, or user ID. | ||
|
|
||
| **Watch for:** Payload indexes should be intentional. Indexing fields that are not used for filtering can add extra work without helping the upload or search path. | ||
|
|
||
| ## Option 3: Quantization to Balance Memory and Search Performance | ||
|
|
||
| _Dense vectors_ | ||
|
|
||
| Storing original vectors on-disk can help reduce memory pressure during large uploads. However, this can also make search more dependent on disk access, especially when Qdrant needs to read the original vectors frequently. | ||
|
|
||
| Quantization can help balance this tradeoff. Instead of keeping full-size dense vectors in memory, Qdrant can keep a compressed version available while the original vectors remain on-disk. | ||
|
|
||
|  | ||
|
|
||
| In Python, configure TurboQuant when creating the collection. The `bits` parameter sets the compression level: `BITS4` (the default) stays closest to full precision, while `BITS1` gives the most compression. | ||
|
|
||
| ```python | ||
| 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, | ||
| ) | ||
| ), | ||
| ) | ||
| ``` | ||
|
|
||
| **Best fit:** Dense vector workloads that need lower memory usage while still keeping search performance practical. | ||
|
|
||
| **Watch for:** Quantization can affect precision depending on the workload and configuration. For many use cases, this tradeoff is worth it, but search quality and latency should be tested with real data. | ||
|
|
||
| ## Option 4: Reduce Sparse Index Memory During Uploads | ||
|
|
||
| _Sparse vectors_ | ||
|
|
||
| For large sparse vector workloads, one option is to store the sparse vector index on-disk. This can help reduce memory usage when the sparse index becomes large. | ||
|
|
||
|  | ||
|
|
||
| Enable on-disk storage for the sparse index: | ||
|
|
||
| ```python | ||
| client.create_collection( | ||
| collection_name="my_collection", | ||
| vectors_config={}, | ||
| sparse_vectors_config={ | ||
| "text": models.SparseVectorParams( | ||
| index=models.SparseIndexParams( | ||
| on_disk=True, | ||
| ) | ||
| ) | ||
| }, | ||
| ) | ||
| ``` | ||
|
|
||
| **Best fit:** Large sparse vector workloads where the sparse index is putting pressure on memory. | ||
|
|
||
| **Watch for:** Storing the sparse index on-disk may slow down search because queries can depend more on disk access. If sparse vector search is latency-sensitive, keeping the sparse index in memory may be better. | ||
|
|
||
| ## Best Practices for Every Upload | ||
|
|
||
| The strategies above depend on your workload, such as vector type, memory limits, and search needs. The following techniques are different. Batching, parallelization, and sharding are not situational choices; they apply to any bulk upload and help improve ingestion throughput and stability regardless of how your collection is configured. | ||
|
|
||
| > **Tip:** Connect with `QdrantClient(url, prefer_grpc=True)` for bulk work. gRPC has lower overhead than HTTP and is meaningfully faster for large uploads. | ||
|
|
||
| ### Batch Your Uploads | ||
|
|
||
| _Dense & sparse vectors_ | ||
|
|
||
| Uploading points one at a time can add unnecessary overhead. Each request has to go through the network, the write path, and internal processing. When this happens millions of times, the upload process can become slower than it needs to be. | ||
|
|
||
| A better approach is to upload points in batches. Batching allows Qdrant to process groups of points together instead of handling every point as a separate request. | ||
|
|
||
|  | ||
|
|
||
| > **Benchmark:** On a local single-node Qdrant 1.18.2 (Windows 11, `qdrant-client` 1.12.0, 10,000 synthetic 768-dim vectors), uploading in batches of 64 took 8.17s versus 152.19s one point at a time β about 18Γ faster. Most of that gap is per-request overhead (network and write-path round-trips), so batching pays off at essentially any scale; exact numbers still depend on hardware and Qdrant version. | ||
|
|
||
| Set a batch size when uploading points: | ||
|
|
||
| ```python | ||
| client.upload_points( | ||
| collection_name="my_collection", | ||
| points=points, | ||
| batch_size=256, | ||
| ) | ||
| ``` | ||
|
|
||
| **Best fit:** Large uploads where sending one point per request would create too much request overhead. | ||
|
|
||
| **Watch for:** A batch size of 64-256 points is a reasonable starting range. Larger batches can improve throughput but increase memory usage and make retries more expensive if a request fails. | ||
|
|
||
| ### Parallelize Uploads | ||
|
|
||
| _Dense & sparse vectors_ | ||
|
|
||
| A single upload stream may not fully use the available write capacity of your Qdrant deployment. When uploading a large dataset, you can often improve ingestion throughput by sending multiple batches in parallel. | ||
|
|
||
| Parallel uploads allow several workers to upload different parts of the dataset at the same time. This keeps Qdrant's write pipeline active, especially when the collection has multiple shards. | ||
|
|
||
|  | ||
|
|
||
| > **Benchmark:** On the same local single-node setup (Qdrant 1.18.2, Windows 11, `qdrant-client` 1.12.0, 10,000 synthetic 768-dim vectors, batch size 256), going from 1 to 4 parallel workers cut upload time from 5.58s to 4.61s (~1.2Γ faster). On a single local node the gain is modest β the workers share the same CPU β and grows more meaningful against a remote or multi-node deployment with more shards. | ||
|
|
||
| Note: Parallelism gains are not always linear; in some configurations, 2 workers may perform similarly to 1 before improvements appear at higher counts. | ||
|
|
||
| Add parallel workers to the upload: | ||
|
|
||
| ```python | ||
| client.upload_points( | ||
| collection_name="my_collection", | ||
| points=points, | ||
| batch_size=256, | ||
| parallel=4, | ||
| ) | ||
| ``` | ||
|
|
||
| **Best fit:** Large uploads where one upload worker is not enough to use the available write capacity. | ||
|
|
||
| **Watch for:** Too much parallelism can create extra pressure on CPU, memory, disk I/O, and network resources. Start with a smaller number first, then increase based on system behavior. | ||
|
|
||
| ### Use Multiple Shards for Larger Uploads | ||
|
|
||
| _Dense & sparse vectors_ | ||
|
|
||
| For larger uploads, sharding can help Qdrant process writes in parallel. A collection can be created with more than one shard, and each shard has its own write path. With multiple shards, Qdrant distributes ingestion work across independent write paths. | ||
|
|
||
|  | ||
|
|
||
| Set the shard count when creating the collection: | ||
|
|
||
| ```python | ||
| client.create_collection( | ||
| collection_name="my_collection", | ||
| vectors_config=models.VectorParams( | ||
| size=768, | ||
| distance=models.Distance.COSINE, | ||
| on_disk=True, | ||
| ), | ||
| shard_number=2, | ||
| ) | ||
| ``` | ||
|
|
||
| **Best fit:** Larger uploads where you want more ingestion parallelism, especially when paired with parallel upload workers. | ||
|
|
||
| **Watch for:** More shards are not always better. Each shard adds overhead, so the shard count should match the size of the deployment and the amount of write parallelism you actually need. | ||
|
|
||
| ## Choosing the Right Mix | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a good start for offering clear guidance and takeaways, but I feel it's still not solving my needs in figuring out what exactly to do. Maybe we point to Qdrant Skills here? |
||
|
|
||
|  | ||
|
|
||
| Still deciding exactly what to configure for your workload? [Qdrant's Agent Skills](https://qdrant.tech/documentation/skills/) provide hands-on, scenario-based guidance that walks you through the specific settings for your situation. | ||
|
|
||
| ## It's Not One-Size-Fits-All | ||
|
|
||
| Bulk uploads are not just about sending as much data as possible into Qdrant. As datasets grow, the upload process also needs to account for memory usage, indexing behavior, disk writes, search availability, and overall system stability. | ||
|
|
||
| The safest approach is to choose the right strategy for the workload instead of relying on one universal configuration. Dense vectors, sparse vectors, and hybrid setups can all create different performance considerations during ingestion. | ||
|
|
||
| > **Tip:** After a large upload, confirm the collection status is green and the optimizers have finished before serving production traffic. | ||
|
|
||
| By designing the collection and upload process before ingestion starts, you can make bulk uploads more efficient, more stable, and easier to scale as your dataset grows. To size your deployment, use the [Qdrant sizing calculator](https://sizing.qdrant.tech). | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file added
BIN
+192 KB
...-landing/static/articles_data/bulk-uploads-in-qdrant/choosing-the-right-mix.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+186 KB
qdrant-landing/static/articles_data/bulk-uploads-in-qdrant/option1-memory.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+207 KB
...t-landing/static/articles_data/bulk-uploads-in-qdrant/option2-payload-index.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+260 KB
...nt-landing/static/articles_data/bulk-uploads-in-qdrant/option3-quantization.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+172 KB
...t-landing/static/articles_data/bulk-uploads-in-qdrant/option4-sparse-ondisk.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+248 KB
qdrant-landing/static/articles_data/bulk-uploads-in-qdrant/option5-batching.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+140 KB
qdrant-landing/static/articles_data/bulk-uploads-in-qdrant/option6-parallel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+167 KB
qdrant-landing/static/articles_data/bulk-uploads-in-qdrant/option7-sharding.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+41.2 KB
qdrant-landing/static/articles_data/bulk-uploads-in-qdrant/preview/preview.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+34 KB
qdrant-landing/static/articles_data/bulk-uploads-in-qdrant/preview/preview.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+213 KB
...-landing/static/articles_data/bulk-uploads-in-qdrant/preview/social_preview.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+119 KB
qdrant-landing/static/articles_data/bulk-uploads-in-qdrant/preview/title.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+99.5 KB
qdrant-landing/static/articles_data/bulk-uploads-in-qdrant/preview/title.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, go ahead and move this line back to where you had it. Or you move "Before we get into..." sentence into this section too (which was my original intended feedback)