mirror of
https://github.com/qdrant/landing_page.git
synced 2026-10-04 10:28:29 +02:00
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:
+9
-105
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user