CRUD & Batch Operations
|
This section documents the current Qdrant 1.x line (latest 1.19.1), written and verified against the official Qdrant documentation. No specific patch version is pinned. Some capabilities (Qdrant Cloud-only features and the full breadth of client-library APIs beyond Python and Java) are linked, not documented in depth. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
This page covers the point-level CRUD operations Qdrant exposes on top of the collections and points introduced in Collections & Points — upserting, retrieving, and deleting points — plus the scroll API for paging over a collection’s contents, and the batching techniques that keep large writes fast and memory-bounded.
Upsert: create or replace by id
upsert is Qdrant’s only write primitive for points: there is no separate "insert" that fails on a duplicate
id — a point with an id already present in the collection is replaced in full (vector and payload both), and
an id not yet present is created. This is the same call already shown in
Points, repeated here as the CRUD baseline the rest of
this page builds on.
from qdrant_client import QdrantClient
from qdrant_client.models import PointStruct
client = QdrantClient(url="http://localhost:6333")
client.upsert(
collection_name="products",
points=[
PointStruct(
id=1,
vector=[0.12, -0.98, 0.33, 0.47],
payload={"category": "audio", "in_stock": True},
),
],
)
Because upsert replaces the whole point, an upsert that omits a payload field the existing point had drops
that field rather than leaving it untouched — partial payload edits go through the separate
set_payload/overwrite_payload calls instead of upsert.
Retrieve points by id
retrieve fetches specific points by id, with independent flags for whether the response should include the
vector and/or the payload — useful for a lookup that only needs the payload (skipping the cost of returning a
large vector) or one that needs neither and is really just an existence check.
result = client.retrieve(
collection_name="products",
ids=[1, 2, 3],
with_vectors=True, # include the stored vector(s) in the response
with_payload=True, # include the payload in the response
)
for point in result:
print(point.id, point.payload)
with_payload also accepts a list of field names (or an inclusion/exclusion object) to return only part of the
payload, and both flags default to returning the payload but not the vector — set with_vectors=True
explicitly whenever the vector itself is needed.
Delete points
Points can be deleted either by an explicit list of ids or by a filter that matches on payload conditions — the same filter syntax used to narrow a search, covered in Filtering & Payload Indexing.
from qdrant_client.models import Filter, FieldCondition, MatchValue, PointIdsList, FilterSelector
# By id list.
client.delete(
collection_name="products",
points_selector=PointIdsList(points=[1, 2, 3]),
)
# By filter: every point whose payload matches, regardless of id.
client.delete(
collection_name="products",
points_selector=FilterSelector(
filter=Filter(
must=[FieldCondition(key="in_stock", match=MatchValue(value=False))]
)
),
)
A filter-based delete can match and remove an unbounded number of points in one call — there is no need to first page through matches and delete them one id at a time.
The scroll API: paging over all points
Retrieving a collection’s full contents — for a export, a migration, or a background job — does not go
through vector search at all, since search always needs a query vector to rank against. Instead, the
Scroll API pages through all points in a
collection (optionally narrowed by a filter) independently of any similarity ranking: each call takes a
limit and returns that many points plus a next_page_offset cursor, which is passed as the offset on the
next call. Iteration stops once next_page_offset comes back null (or the page is empty), meaning there is
nothing left to fetch.
next_offset = None
all_points = []
while True:
points, next_offset = client.scroll(
collection_name="products",
limit=100,
offset=next_offset,
with_payload=True,
with_vectors=False,
)
all_points.extend(points)
if next_offset is None:
break
print(f"scrolled {len(all_points)} points")
This is the same offset-forward, "resume after the last cursor I saw" shape as any other cursor-based pagination — see Pagination: Offset vs. Keyset for the general keyset/cursor pattern and why it scales better than a numeric offset; this section only shows the scroll API’s own request/response shape, not that general trade-off again.
Batch upsert and batch delete
Both upsert and delete already accept many points or many ids in a single request — the examples above
already send lists rather than one point/id per call, and that is the primary batching lever: fewer network
round trips, and the server can apply the whole set as one write. For a very large upsert (millions of
points, or vectors too large to build as one in-memory list) it still pays to split the upsert into batches
of a bounded size, so memory usage and any one request’s payload size stay predictable regardless of the total
data volume:
def batched(iterable, size):
batch = []
for item in iterable:
batch.append(item)
if len(batch) == size:
yield batch
batch = []
if batch:
yield batch
def all_points():
# A generator over however many points need to be upserted, e.g. read
# from a file or another database rather than held in memory at once.
for i in range(1, 1_000_001):
yield PointStruct(
id=i,
vector=[0.0] * 4,
payload={"category": "audio"},
)
for batch in batched(all_points(), size=256):
client.upsert(collection_name="products", points=batch)
Batch deletes follow the same shape: a PointIdsList (or a FilterSelector) sent in one delete call removes
every id/match in that call, so splitting a huge id-based delete into chunks of, say, a few thousand ids at a
time bounds request size the same way batched upserts do.
The Java/Kotlin client (io.qdrant:client) exposes the same batch-upsert call, built around protobuf
PointStruct messages and a list passed to upsertAsync:
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Points.PointStruct;
import io.qdrant.client.grpc.Points.PointId;
import io.qdrant.client.grpc.Points.Vectors;
import java.util.ArrayList;
import java.util.List;
import static io.qdrant.client.PointIdFactory.id;
import static io.qdrant.client.VectorsFactory.vectors;
import static io.qdrant.client.ValueFactory.value;
QdrantClient client = new QdrantClient(
QdrantGrpcClient.newBuilder("localhost", 6334, false).build());
List<PointStruct> batch = new ArrayList<>();
for (long i = 1; i <= 256; i++) {
batch.add(PointStruct.newBuilder()
.setId(PointId.newBuilder().setNum(i).build())
.setVectors(vectors(0.12f, -0.98f, 0.33f, 0.47f))
.putAllPayload(java.util.Map.of("category", value("audio")))
.build());
}
client.upsertAsync("products", batch).get();
As with the Python example, a huge total point count should still be split across several upsertAsync calls
of a fixed batch size rather than assembled into one very large List<PointStruct>.
Where to go next
-
Pagination: Offset vs. Keyset — the general offset-vs-keyset cursor pattern the scroll API’s
next_page_offsetfollows; this page deliberately did not re-derive that trade-off, only the scroll API’s own shape. -
Hybrid & Sparse Search — combining dense and sparse vector search, the previous topic in this reference’s reading order.
-
Clustering & Storage — how a collection’s points are sharded and replicated across a cluster once write volume outgrows a single node.