Runs at:
- REST:
http://127.0.0.1:6333 - gRPC:
127.0.0.1:6334 - Dashboard:
http://127.0.0.1:6333/dashboard
Data persists in ./storage/ (gitignored). make wipe CONFIRM=1 to reset.
Pair with the local embeddings API at 127.0.0.1:8080 (see top-level
CLAUDE.md) — nomic-embed-text/vision share a single 768-dim cosine space, so
text and image vectors can live in the same collection.
uv add qdrant-client # or: uv run --with qdrant-client python -c '...'All snippets below assume:
from qdrant_client import QdrantClient, models
client = QdrantClient(url="http://localhost:6333")
# For higher throughput / embedded use, prefer gRPC:
# client = QdrantClient(host="localhost", grpc_port=6334, prefer_grpc=True)client.create_collection(
collection_name="docs",
vectors_config=models.VectorParams(size=768, distance=models.Distance.COSINE),
)Distances: COSINE, DOT, EUCLID, MANHATTAN. For nomic-embed vectors, use
COSINE (they're L2-normalized).
Use named vectors if you want several embeddings per point (e.g. different models or text-vs-summary).
client.create_collection(
collection_name="multimodal",
vectors_config={
"text": models.VectorParams(size=768, distance=models.Distance.COSINE),
"image": models.VectorParams(size=768, distance=models.Distance.COSINE),
},
)client.create_collection(
collection_name="big",
vectors_config=models.VectorParams(
size=768,
distance=models.Distance.COSINE,
on_disk=True, # vectors on disk instead of RAM
hnsw_config=models.HnswConfigDiff(m=32, ef_construct=128),
),
quantization_config=models.ScalarQuantization(
scalar=models.ScalarQuantizationConfig(
type=models.ScalarType.INT8, quantile=0.99, always_ram=True,
),
),
)create_collection errors if it exists. For idempotent setup use
recreate_collection (destructive) or check first:
if not client.collection_exists("docs"):
client.create_collection(...)Filtering without an index triggers a full scan. Create an index for every
payload field you'll filter on — do this right after create_collection,
before inserting lots of data.
client.create_payload_index("docs", "tenant_id", models.PayloadSchemaType.KEYWORD)
client.create_payload_index("docs", "created_at", models.PayloadSchemaType.DATETIME)
client.create_payload_index("docs", "price", models.PayloadSchemaType.FLOAT)
client.create_payload_index("docs", "count", models.PayloadSchemaType.INTEGER)
client.create_payload_index("docs", "is_active", models.PayloadSchemaType.BOOL)
client.create_payload_index("docs", "user_uuid", models.PayloadSchemaType.UUID)
client.create_payload_index("docs", "location", models.PayloadSchemaType.GEO)client.create_payload_index(
"docs", "body",
field_schema=models.TextIndexParams(
type=models.TextIndexType.TEXT,
tokenizer=models.TokenizerType.WORD,
lowercase=True,
min_token_len=2,
),
)Nested keys are written with dots ("country.name"); arrays with []
("tags[]", "country.cities[].population").
client.upsert(
collection_name="docs",
points=[
models.PointStruct(
id=1, # int or UUID str
vector=[0.1, 0.2, ...], # 768 floats
payload={"tenant_id": "acme", "tags": ["ai", "search"], "price": 12.5},
),
models.PointStruct(id=2, vector=[...], payload={"tenant_id": "acme"}),
],
)Named-vector collections take a dict in vector=:
models.PointStruct(id=1, vector={"text": [...], "image": [...]}, payload={...})Batch-friendly helper: client.upload_collection(...) or
client.upload_points(...) for large ingests (handles batching + parallelism).
Note: client.search() was removed in qdrant-client ≥1.10 — use
client.query_points(...).points instead. Older examples in upstream docs
still show .search(); if you see that, mentally rewrite it.
res = client.query_points(
collection_name="docs",
query=[0.1, 0.2, ...], # query vector
query_filter=models.Filter(must=[...]),
limit=10,
with_payload=True,
)
hits = res.points # list[ScoredPoint]Full example with filter combinators:
hits = client.query_points(
collection_name="docs",
query_vector=[0.1, 0.2, ...], # plain list for unnamed
# query_vector=("text", [0.1, ...]), # for named-vector collection
query_filter=models.Filter(
must=[
models.FieldCondition(key="tenant_id",
match=models.MatchValue(value="acme")),
models.FieldCondition(key="price",
range=models.Range(gte=10, lte=100)),
],
should=[ # OR block
models.FieldCondition(key="tags",
match=models.MatchAny(any=["ai", "ml"])),
],
must_not=[
models.FieldCondition(key="is_deleted",
match=models.MatchValue(value=True)),
],
),
limit=10,
with_payload=True,
).points
for h in hits:
print(h.id, h.score, h.payload)| Want | Condition |
|---|---|
| exact match | match=models.MatchValue(value=x) |
| in a set (OR on one field) | match=models.MatchAny(any=[...]) |
| not in a set | match=models.MatchExcept(**{"except": [...]}) |
| numeric / date range | range=models.Range(gt=, gte=, lt=, lte=) |
| full text (needs text index) | match=models.MatchText(text="...") |
| geo radius | geo_radius=models.GeoRadius(center=..., radius=meters) |
| nested object | models.NestedCondition(nested=models.Nested(key=..., filter=...)) |
| payload key exists | models.IsEmptyCondition(is_empty=models.PayloadField(key=...)) (negate with must_not) |
Filter semantics: must = AND, should = OR (matches if any satisfied),
must_not = NOT. Nest Filter inside must/should for arbitrary logic.
points, next_page = client.scroll(
collection_name="docs",
scroll_filter=models.Filter(must=[
models.FieldCondition(key="tenant_id",
match=models.MatchValue(value="acme")),
]),
limit=500,
with_payload=True,
)Paginate with offset=next_page until next_page is None.
client.recommend(
collection_name="docs",
positive=[1, 7], # IDs or vectors you liked
negative=[3],
query_filter=...,
limit=10,
)client.get_collection("docs") # schema + counts
client.count("docs", exact=True)
client.delete(collection_name="docs",
points_selector=models.FilterSelector(filter=...))
client.set_payload("docs", payload={"k": "v"}, points=[1, 2])
client.delete_payload("docs", keys=["k"], points=[1, 2])
client.delete_collection("docs")
client.create_snapshot("docs") # for backup- Always create payload indexes before bulk inserts — filtering without an index is a full scan and the index has to be built retroactively.
- Distance must match the embedding model. Nomic =
COSINE. - Named vs unnamed vectors are not interchangeable; pick up-front. Named vectors are the safe default if you might ever add a second embedding.
- IDs can be positive ints or UUID strings — not arbitrary strings.
shouldon its own matches if any condition matches (OR). Combined withmust, it acts as a "nice-to-have" scorer influence? No — it's still a strict match: at least oneshouldmust match in addition to allmust.- gRPC client is faster for high-volume upserts and search. Toggle via
prefer_grpc=True. - Full-text match (
MatchText) only works on fields with aTEXTpayload index — otherwise it errors.
- Concepts: https://qdrant.tech/documentation/concepts/
- Python client: https://python-client.qdrant.tech/
- REST API: http://127.0.0.1:6333/ (once running)