Skip to content

Data operations

Data statements make point writes and maintenance operations inspectable before they reach Qdrant. Use a WHERE selector for mutations and a SHARD clause only when you are targeting a known shard key.

Every value object requires exactly one case-insensitive id key: a duplicate key is rejected at parse time with QQL-PARSE-DUPLICATE-KEY, and a missing or non-point-ID value fails with QQL-VALIDATION-UPSERT-ID. A point can carry payload values, named dense vectors, sparse vectors, or multivectors, either as one unnamed vector or as an object of arbitrary named vectors.

Vector shapeValue
Dense[0.1, 0.2, 0.3]
Sparse{indices: [1, 9], values: [0.4, 0.7]}
Multivector[[0.1, 0.2], [0.3, 0.4]]
Inference document{text: 'hello', model: 'm', options: {temperature: 0.5}}
Inference image{image: 'https://x/y.jpg', model: 'c'}
Inference object{object: {a: 1}, model: 'm'}

A vector dict with a string text/image key or an object key is server-side inference input, not a named vector. Only model and options may accompany it. A dict with a numeric text value stays a named vector.

QQLPer-point inference vectorsTry in playground
UPSERT INTO docs VALUES {id: 1, vector: {text: 'hello', model: 'm'}};
UPSERT INTO docs VALUES {id: 1, vector: {dense: {text: 'hello', model: 'm'}, sparse: {indices: [1], values: [0.5]}}};
UPDATE docs SET VECTOR dense = {text: 'hello', model: 'm'} WHERE id = 1;
QQLPayload with a named dense vectorTry in playground
UPSERT INTO docs VALUES {id: 1, vector: {dense: [0.1, 0.2, 0.3], sparse: {indices: [1, 9], values: [0.4, 0.7]}}, title: 'QQL', text: 'Declarative vector retrieval'};

When execution owns the embedding step, use USING or EMBED directives. The configured embedder resolves text before the operation is planned. Embedding specs are comma-separated, and each may name a MODEL, an ON FIELD source, and a VECTOR/INTO destination.

QQLEmbed multiple payload fields into named vectorsTry in playground
UPSERT INTO articles VALUES {id: 'article-1', title: 'Planning hybrid retrieval', body: 'Dense and sparse candidate generation'}
EMBED body INTO dense_body USING DENSE,
title INTO sparse_title USING SPARSE;
UPSERT INTO articles VALUES {id: 'article-2', title: 'Vector search', body: 'Semantic ranking'} USING DENSE MODEL 'all-MiniLM-L6-v2' ON FIELD body INTO dense_body, SPARSE MODEL 'qdrant/bm25' ON FIELD title INTO sparse_title;

When ON FIELD is omitted, the default payload text field resolves in the deterministic priority order text, body, content, title, description, name, summary, document.

UPDATE FILTER restricts the write to matching points. UPDATE MODE picks insert_only, update_only, or explicit upsert. The guards commute and each appears at most once.

QQLConditional upsertsTry in playground
UPSERT INTO docs VALUES {id: 1, vector: [0.1]} UPDATE FILTER status = 'active';
UPSERT INTO docs VALUES {id: 1, vector: [0.1]} UPDATE MODE insert_only;
UPSERT INTO docs VALUES {id: 1, vector: [0.1]} UPDATE FILTER status = 'active' UPDATE MODE update_only;

A whole-point placeholder binds one point dict — or a list of them, splicing N points — with the same {id, vector, …payload} shape as an inline row. Misshapen rows fail closed (QQL-BIND-TYPE-MISMATCH, missing idQQL-VALIDATION-UPSERT-ID):

QQLInline rows and bound rows share one shapeTry in playground
UPSERT INTO docs VALUES {id: 1, vector: {dense: [0.1, 0.2, 0.3]}, tag: 'a'},
{id: 2, vector: {dense: [0.4, 0.5, 0.6]}, tag: 'b'};

For application ingest, prefer the chunked helpers over hand-rolled batch loops — Python client.upsert_many("docs", rows, batch_size=100), Node client.upsertMany("docs", rows, { batchSize: 100 }), Rust exec.upsert_many("docs", rows, 100, …), WASM client.upsertMany("docs", rows, { batchSize: 100 }). They prepare one :rows template and splice each chunk with no re-parse.

There are two UPDATE forms. The payload form sets PAYLOAD under any WHERE filter, with an optional nested KEY path and an optional full replace:

UPDATE docs SET PAYLOAD = {status: 'reviewed'} WHERE <filter>;
UPDATE docs SET PAYLOAD = {a: 1} KEY 'a.b' WHERE id = 1;

The vector form replaces vectors on existing points. Compact WHERE id = targets one point (optionally one named vector, or a name map). VALUES is the batch form that maps 1:1 onto REST PUT /points/vectors and gRPC UpdatePointVectors:

UPDATE docs SET VECTOR dense = [0.2, 0.3, 0.4] WHERE id = <point_id>;
UPDATE docs SET VECTOR = {dense: [0.2, 0.3], sparse: {indices: [1], values: [0.8]}} WHERE id = 42;
UPDATE docs SET VECTOR VALUES {id: 1, vector: [0.1, 0.2]}, {id: 2, vector: {dense: [0.3, 0.4]}};

DELETE PAYLOAD, DELETE VECTOR, DELETE, and CLEAR PAYLOAD all require a WHERE selector so an empty script never mutates an entire collection by accident.

QQLTargeted point mutationsTry in playground
UPDATE docs SET PAYLOAD = {status: 'reviewed'} WHERE id = 42;
UPDATE docs SET PAYLOAD = {a: 1} KEY 'a.b' WHERE id = 1;
UPDATE docs SET VECTOR dense = [0.2, 0.3, 0.4] WHERE id = 42;
UPDATE docs SET VECTOR VALUES {id: 1, vector: [0.1, 0.2]}, {id: 2, vector: {dense: [0.3, 0.4]}};
DELETE PAYLOAD draft_notes FROM docs WHERE status = 'published';
DELETE VECTOR dense FROM docs WHERE id = 42;
DELETE FROM docs WHERE status = 'expired';

KEY merges at that nested path. OVERWRITE replaces the full payload and runs only inside a BATCH block — a lone OVERWRITE fails closed with QQL-REST-OVERWRITE-BATCH-ONLY because POST /points/payload is merge-only:

QQLFull payload replace inside a batchTry in playground
BATCH { UPDATE docs SET PAYLOAD = {a: 1} OVERWRITE WHERE id = 1; UPDATE docs SET PAYLOAD = {b: 2} WHERE id = 2; };

SCROLL is for deterministic browsing, exports, and backfills. COUNT is for cardinality and accepts the same filter vocabulary.

QQLBrowse published documents and count themTry in playground
SCROLL FROM docs WHERE status = 'published' WITH VECTOR false LIMIT 100;
SCROLL FROM docs ORDER BY created_at DESC LIMIT 10;
SCROLL FROM docs WHERE status = 'active' AFTER 7 ORDER BY created_at DESC START FROM '2024-01-01T00:00:00Z' SHARD 'acme' WITH PAYLOAD INCLUDE (title) WITH VECTOR (dense) LIMIT 10;
COUNT FROM docs WHERE status = 'published' WITH (exact = true);

Use SCROLL ... AFTER point_id to continue from a cursor. ORDER BY takes ASC or DESC with an optional START FROM origin (integer, float, datetime string, or placeholder). WITH PAYLOAD takes false, INCLUDE (...), or EXCLUDE (...). Use COUNT without exact = true when an approximate answer is sufficient for a large collection.

FACET calculates categorical value distributions for any indexed payload key using Qdrant's /collections/{collection}/facet endpoint (or Points.Facet over gRPC), returning hit counts per unique value without fetching full point records.

QQLFacet counts with filtering and exact accuracyTry in playground
-- Compute top 5 room types in a hospitality collection under €150/night
FACET room_type FROM stays WHERE price &#x3C; 150 LIMIT 5 EXACT true;
-- Shard-routed facet using key front syntax
FACET category FROM catalog LIMIT 10 SHARD 'tenant_1';
FACET category FROM catalog LIMIT 10 SHARD 101;

Use WHERE to restrict the aggregation scope, LIMIT to cap the number of returned facet values, EXACT true for exact distributed counting, and SHARD '<key>' to route directly to a specific tenant partition. Numeric and keyword shard keys hash differently on the wire, so SHARD 101 and SHARD '101' target different partitions. SHARD :tenant binds by type: strings become keywords and non-negative integers become numeric keys.