Model migration tutorial: use insert-only update mode and Cloud Inference (#2168)

* Switch to insert-only mode instead of conditional upserts

* Make code snippets testable; use Cloud Inference

* Use regular upserts instead of batch_update_points

* Add snippets for TS, Rust, Java, C#, and Go
This commit is contained in:
Abdon Pijpelink
2026-03-04 14:19:10 +01:00
committed by GitHub
parent 0725a70f05
commit 662e9aeaa3
49 changed files with 2257 additions and 105 deletions
@@ -29,25 +29,14 @@ Re-embedding requires access to the original data used to create the embeddings.
The solution outlined in this tutorial only works for upsert operations. If you use deletes or partial updates, it is necessary to pause those operations during the migration or implement additional logic to handle them.
This tutorial assumes you use [Qdrant Cloud Inference](/documentation/concepts/inference/#qdrant-cloud-inference) to generate vector embeddings. If you manage your own embedding infrastructure, you can apply the same principles, but you will need to adapt the code examples to use your embedding service.
## Step 1: Create a New Collection
The first step is to create a new collection in Qdrant that will be used to store the new
embeddings, compatible with the new model in terms of vector size and similarity function.
```python
from qdrant_client import QdrantClient, models
client = QdrantClient(...)
client.create_collection(
collection_name=NEW_COLLECTION,
vectors_config=(
models.VectorParams(
size=512, # Size of the new embedding vectors
distance=models.Distance.COSINE # Similarity function for the new model
)
)
)
```
{{< code-snippet path="/documentation/headless/snippets/tutorial-model-migration/" block="create-new-collection" >}}
Now is also a good moment to consider changing any other settings for the collection, like custom sharding, replication factor, etc. Switching the model may be a good opportunity to improve the performance of your search.
@@ -60,34 +49,11 @@ To ensure that both collections are kept up-to-date during the migration, you ne
Ideally, the data in Qdrant is updated by an update service reading from an update queue. This service is responsible for embedding the documents and writing them to Qdrant. It uses code similar to this:
```python
client.upsert(
collection_name=OLD_COLLECTION,
points=[
models.PointStruct(
id=1,
vector=encode(text="Example document", model_name=OLD_MODEL),
payload={"text": "Example document"}
)
]
)
```
{{< code-snippet path="/documentation/headless/snippets/tutorial-model-migration/" block="upsert-old-collection" >}}
To update the new collection, deploy a second service that updates the new collection in parallel with the existing one. This service uses the new embedding model to encode the documents and writes them to the new collection:
```python
client.upsert(
collection_name=NEW_COLLECTION,
points=[
models.PointStruct(
id=1,
# Use the new embedding model to encode the document
vector=encode(text="Example document", model_name=NEW_MODEL),
payload={"text": "Example document"}
)
]
)
```
{{< code-snippet path="/documentation/headless/snippets/tutorial-model-migration/" block="upsert-new-collection" >}}
A good practice is to always ensure that both operations succeed. Any errors need to be handled on the client side. You could store errors in a log or "dead letter queue" for later processing. Transient errors can be retried at a later time. Other errors need to be analyzed and addressed accordingly.
@@ -116,63 +82,13 @@ in parallel with the regular upsert services.
The migration process reads the points from the old collection, re-embeds them using the new model, and writes them to the new collection, making sure not to overwrite existing points inserted by the update service. Here's an example of what the code for such a migration process could look like:
```python
last_offset = None
batch_size = 100 # Number of points to read in each batch
reached_end = False
while not reached_end:
# Get the next batch of points from the old collection
records, last_offset = client.scroll(
collection_name=OLD_COLLECTION,
limit=batch_size,
offset=last_offset,
# Include payloads in the response, as we need them to re-embed the vectors
with_payload=True,
# We don't need the old vectors, so let's save on the bandwidth
with_vectors=False,
)
# Re-embed the points using the new model
upsert_operations = [
models.UpsertOperation(
upsert=models.PointsList(
points=[models.PointStruct(
# Keep the original ID to ensure consistency
id=record.id,
# Use the new embedding model to encode the text from the payload,
# assuming that was the original source of the embedding
vector=encode(record.payload.get("text"), model_name=NEW_MODEL),
# Keep the original payload
payload=record.payload
)],
# Only insert the point if a point with this ID does not already exist.
update_filter=models.Filter(
must_not=[
models.HasIdCondition(has_id=[record.id]),
],
)
)
)
for record in records
]
# Upsert the re-embedded points into the new collection
client.batch_update_points(
collection_name=NEW_COLLECTION,
update_operations=upsert_operations
)
# Check if we reached the end of the collection
reached_end = (last_offset == None)
```
{{< code-snippet path="/documentation/headless/snippets/tutorial-model-migration/" block="migrate-points" >}}
Breaking down this code step by step:
- Data is read from the old collection in batches of 100 points using a [scroll](/documentation/concepts/points/#scroll-points). The `last_offset` variable keeps track of the scroll position in the collection.
- For each batch of points, the process re-embeds the vectors using the new embedding model. It assumes that the original text used for embedding is stored in the payload under the key `text`.
- With the re-embedded vectors, it prepares [conditional upsert operations](/documentation/concepts/points/#conditional-updates) for the new collection, keeping the original IDs and payloads. The conditional upserts use a filter condition to ensure that a point is only inserted if it does not already exist in the new collection. The filter checks whether a point with the given ID already exists. A point is only upserted if the ID does not exist in the new collection. This prevents overwriting newer updates from the regular update service.
- Finally, the process uses a [batch update](/documentation/concepts/points/#batch-update) to upsert the re-embedded points into the new collection. Note that it uses `batch_update_points` instead of `upsert`, because `batch_update_points` allows you to specify an update condition per upsert operation.
- With the re-embedded vectors, it upserts the points into the new collection, keeping the original IDs and payloads. The upserts use [insert-only mode](/documentation/concepts/points/#update-mode) to ensure that a point is only inserted if it does not already exist in the new collection. This prevents overwriting newer updates from the regular update service.
This kind of migration process can take some time, and the offset can be stored in a persistent way, so you can resume the migration process in case of a failure. You can use a database, a file, or any other persistent storage to keep track of the last offset. Having said that, because the conditional upserts would not overwrite any points in the new collection, you could safely restart the migration process from the beginning if needed.
@@ -185,23 +101,11 @@ Once the migration process is complete, and all the points from the old collecti
If these values are hardcoded in your application, you will need to change them directly in the code and deploy a new version of your application. For example, if your current search code looks like this:
```python
results = client.query_points(
collection_name=OLD_COLLECTION,
query=encode(text="my query", model_name=OLD_MODEL), # Old query vector
limit=10,
)
```
{{< code-snippet path="/documentation/headless/snippets/tutorial-model-migration/" block="search-old-collection" >}}
You need to change it in the following way:
```python
results = client.query_points(
collection_name=NEW_COLLECTION,
query=encode(text="my query", model_name=NEW_MODEL), # New query vector
limit=10,
)
```
{{< code-snippet path="/documentation/headless/snippets/tutorial-model-migration/" block="search-new-collection" >}}
## Step 5: Wrapping Up