Connect
export Q=http://qdrant:6333
export H="api-key: $QDRANT_API_KEY"
curl -s -H "$H" $Q/ | jq # version info
from qdrant_client import QdrantClient, models
client = QdrantClient(url="http://qdrant:6333", api_key=API_KEY, prefer_grpc=True)
Collections
curl -s -H "$H" $Q/collections | jq # list
curl -s -H "$H" $Q/collections/kb_chunks | jq '.result' # status, counts, config
curl -X DELETE -H "$H" $Q/collections/kb_chunks # delete
client.create_collection(
"kb_chunks",
vectors_config=models.VectorParams(size=1536, distance=models.Distance.COSINE),
)
# Named vectors (e.g. dense + title vector) in one collection
client.create_collection(
"kb_multi",
vectors_config={"content": models.VectorParams(size=1536, distance=models.Distance.COSINE),
"title": models.VectorParams(size=384, distance=models.Distance.COSINE)},
sparse_vectors_config={"bm25": models.SparseVectorParams(modifier=models.Modifier.IDF)},
)
client.collection_exists("kb_chunks")
Distances: Cosine, Dot, Euclid, Manhattan.
Points
Point IDs must be unsigned integers or UUIDs. A string like doc42#0 isn't valid; derive a UUID from it:
import uuid
pid = str(uuid.uuid5(uuid.NAMESPACE_URL, "doc42#0")) # deterministic
client.upsert("kb_chunks", points=[
models.PointStruct(id=pid, vector=emb, payload={"doc_id": "doc42", "tenant_id": "t1", "year": 2026}),
])
client.retrieve("kb_chunks", ids=[pid], with_payload=True)
client.set_payload("kb_chunks", payload={"reviewed": True}, points=[pid]) # merge
client.overwrite_payload("kb_chunks", payload={"doc_id": "doc42"}, points=[pid]) # replace
client.delete_payload("kb_chunks", keys=["reviewed"], points=[pid])
Delete:
client.delete("kb_chunks", points_selector=models.PointIdsList(points=[pid]))
client.delete("kb_chunks", points_selector=models.FilterSelector(
filter=models.Filter(must=[models.FieldCondition(key="doc_id", match=models.MatchValue(value="doc42"))])))
Filters
| Need | Condition |
|---|---|
| Equals | FieldCondition(key="tenant_id", match=MatchValue(value="t1")) |
| Any of | match=MatchAny(any=["jira", "confluence"]) |
| None of | match=MatchExcept(**{"except": ["hr"]}) |
| Range | range=Range(gte=2025, lt=2027) |
| Date range | range=DatetimeRange(gte="2026-01-01T00:00:00Z") |
| Full text (needs text index) | match=MatchText(text="vpn timeout") |
| Field missing/empty | IsEmptyCondition(is_empty=PayloadField(key="owner")) |
| Null | IsNullCondition(is_null=PayloadField(key="owner")) |
| By ID | HasIdCondition(has_id=[pid1, pid2]) |
| Nested array of objects | NestedCondition(nested=Nested(key="authors", filter=...)) |
Combine with must (AND), should (OR), must_not (NOT):
f = models.Filter(
must=[models.FieldCondition(key="tenant_id", match=models.MatchValue(value="t1"))],
should=[models.FieldCondition(key="source", match=models.MatchValue(value="jira")),
models.FieldCondition(key="source", match=models.MatchValue(value="confluence"))],
must_not=[models.FieldCondition(key="status", match=models.MatchValue(value="archived"))],
)
As JSON in REST: {"must":[{"key":"tenant_id","match":{"value":"t1"}}]}.
Query API
res = client.query_points(
"kb_chunks", query=qvec, limit=10, query_filter=f,
with_payload=["doc_id", "title"],
search_params=models.SearchParams(hnsw_ef=128),
score_threshold=0.3,
)
for p in res.points:
print(p.id, round(p.score, 3), p.payload)
curl -X POST -H "$H" -H "Content-Type: application/json" $Q/collections/kb_chunks/points/query \
-d '{"query": [0.01, 0.02, ...], "limit": 10, "filter": {"must":[{"key":"tenant_id","match":{"value":"t1"}}]}, "with_payload": true}'
Hybrid (dense + sparse) with fusion:
client.query_points("kb_multi",
prefetch=[models.Prefetch(query=dense_vec, using="content", limit=40),
models.Prefetch(query=models.SparseVector(indices=idx, values=vals), using="bm25", limit=40)],
query=models.FusionQuery(fusion=models.Fusion.RRF), limit=10)
Exact search for recall checks: search_params=models.SearchParams(exact=True).
Scroll and count
points, next_offset = client.scroll("kb_chunks", scroll_filter=f, limit=500, with_vectors=False)
client.count("kb_chunks", count_filter=f, exact=True).count
Payload indexes
client.create_payload_index("kb_chunks", "tenant_id",
field_schema=models.KeywordIndexParams(type="keyword", is_tenant=True))
client.create_payload_index("kb_chunks", "year", field_schema=models.PayloadSchemaType.INTEGER)
Aliases (zero-downtime switch)
client.update_collection_aliases(change_aliases_operations=[
models.DeleteAliasOperation(delete_alias=models.DeleteAlias(alias_name="kb_live")),
models.CreateAliasOperation(create_alias=models.CreateAlias(collection_name="kb_chunks_v2", alias_name="kb_live")),
])
Both operations run atomically. Applications should always query the alias.
Snapshots
curl -X POST -H "$H" $Q/collections/kb_chunks/snapshots # create
curl -s -H "$H" $Q/collections/kb_chunks/snapshots | jq # list
In a cluster, collection snapshots are per node; take them on each peer.
Health and diagnostics
| Endpoint | Use |
|---|---|
/healthz, /livez, /readyz | Probes (readyz waits for consensus/shards) |
/metrics | Prometheus metrics |
/telemetry | Detailed internal stats (JSON) |
/cluster | Peers and Raft state |
/collections/{name}/cluster | Shard placement and transfers |
Common errors
| Error | Cause |
|---|---|
Wrong input: Vector dimension error | Embedding size ≠ collection size |
Unable to parse UUID | String point ID that isn't a UUID |
Bad request: Index required but not found for "tenant_id" | Strict mode requires payload index for filtered field |
403 Forbidden | Missing/wrong API key, or read-only key used for writes |
Prevent mistakes next time
- Use deterministic UUIDs from your own chunk IDs.
- Query aliases, not collections.
- Create payload indexes with the collection.
- Keep
with_vectors=Falsein production reads.