Filters
Examples on this page assume and a collection with the payload fields , , , , , , , , and .
| Task | Filter |
|---|---|
| All conditions match (SQL ) | |
| At least one matches (SQL ) | |
| None may match (SQL , ) | |
| At least N of several conditions | |
| Nest groups: (a AND b) OR c | |
| Filter a vector search | |
| Filter scroll, count and delete | , , |
Each condition is a on one payload key. A full request looks like this:
from qdrant_client import QdrantClient, models
client = QdrantClient(url="http://localhost:6333")
in_stock_shoes = models.Filter(
must=[
models.FieldCondition(key="category", match=models.MatchValue(value="shoes")),
models.FieldCondition(key="in_stock", match=models.MatchValue(value=True)),
],
must_not=[
models.FieldCondition(key="tags", match=models.MatchAny(any=["clearance"])),
],
)
hits = client.query_points(
"products", query=[0.9, 0.1, 0.1, 0.0], query_filter=in_stock_shoes, limit=5
).points
The REST API takes the same structure as JSON, with instead of :
POST /collections/products/points/query
{
"query": [0.9, 0.1, 0.1, 0.0],
"filter": {
"must": [
{ "key": "category", "match": { "value": "shoes" } },
{ "key": "price", "range": { "gte": 50, "lte": 150 } }
],
"must_not": [
{ "key": "tags", "match": { "any": ["clearance"] } }
]
},
"with_payload": ["name", "price"],
"limit": 5
}
Example: products that are in-stock shoes, or anything under 20, as one OR of two AND groups.
cheap_or_stocked_shoes = models.Filter(
should=[
models.Filter(
must=[
models.FieldCondition(key="category", match=models.MatchValue(value="shoes")),
models.FieldCondition(key="in_stock", match=models.MatchValue(value=True)),
]
),
models.FieldCondition(key="price", range=models.Range(lt=20)),
]
)
points, _ = client.scroll("products", scroll_filter=cheap_or_stocked_shoes, limit=100)
print([p.id for p in points])
In 1bench: the Qdrant GUI has a filter builder with field and operator pickers that writes the must or should filter JSON for you, and shows the generated request before you run it.
Match Conditions
| Task | Condition |
|---|---|
| Exact value (SQL ) | |
| Boolean field | |
| Any of several values (SQL ) | |
| None of these values (SQL ) | |
| Not equal to one value (SQL ) | |
| Array contains a value | |
| Array contains any of these | |
| Array contains all of these | one condition per value inside |
| All words, any order (closest to SQL ) | |
| Exact phrase, words in order | |
| Any of several words | |
| Exact number on a float field |
takes strings, integers and booleans, and it is case-sensitive: finds nothing when the payload says . Text conditions ignore case and match whole words, so finds "Rain Jacket" but finds nothing. There is no regex condition. is a Python keyword, which is why takes it through .
For prefix matching (search as you type), give the field a text index with the prefix tokenizer:
client.create_payload_index(
"products",
field_name="description",
field_schema=models.TextIndexParams(
type=models.TextIndexType.TEXT,
tokenizer=models.TokenizerType.PREFIX,
lowercase=True,
),
)
Example: a case-insensitive product name search, backed by a full-text index with phrase matching turned on.
client.create_payload_index(
"products",
field_name="name",
field_schema=models.TextIndexParams(
type=models.TextIndexType.TEXT,
tokenizer=models.TokenizerType.WORD,
lowercase=True,
phrase_matching=True,
),
)
points, _ = client.scroll(
"products",
scroll_filter=models.Filter(
must=[models.FieldCondition(key="name", match=models.MatchText(text="RUNNING shoe"))]
),
)
print([p.payload["name"] for p in points])
Range and Datetime
| Task | Condition |
|---|---|
| Between two numbers (SQL ) | |
| Greater than | |
| Less than or equal | |
| Date range (RFC 3339 strings) | |
| On or after a date | |
| Number of values in an array | |
| Sort scroll results (SQL ) |
accepts a date alone or a full timestamp; a timestamp without a zone is read as UTC. needs an integer, float or datetime index on the key.
Example: products added in the last 30 days that cost less than 150.
from datetime import datetime, timedelta, timezone
since = (datetime.now(timezone.utc) - timedelta(days=30)).isoformat()
recent_under_150 = models.Filter(
must=[
models.FieldCondition(key="created_at", range=models.DatetimeRange(gte=since)),
models.FieldCondition(key="price", range=models.Range(lt=150)),
]
)
points, _ = client.scroll("products", scroll_filter=recent_under_150, limit=100)
print([(p.id, p.payload["created_at"]) for p in points])
Nested and Arrays
| Task | Condition |
|---|---|
| Field inside an object | |
| Any element of an array of objects | |
| Several conditions on the same array element | |
| Array is empty or missing | |
| Index a field inside an array of objects |
Two flat conditions can be satisfied by two different elements. Product 1 has a red variant with no stock and a blue one in stock, so plus matches it. checks both conditions against each element on its own.
Example: products with a red variant that is actually in stock.
red_in_stock = models.Filter(
must=[
models.NestedCondition(
nested=models.Nested(
key="variants",
filter=models.Filter(
must=[
models.FieldCondition(key="color", match=models.MatchValue(value="red")),
models.FieldCondition(key="stock", range=models.Range(gt=0)),
]
),
)
)
]
)
points, _ = client.scroll("products", scroll_filter=red_in_stock)
print([p.id for p in points])
Geo
Store a location as in the payload and give it a index.
| Task | Condition |
|---|---|
| Within a radius, in meters | |
| Inside a bounding box | |
| Inside a polygon | , see below |
| Index the field |
Example: products stocked inside a polygon around Berlin, nearest match to a query vector first. The first and last points of the ring must be the same.
berlin = models.GeoPolygon(
exterior=models.GeoLineString(
points=[
models.GeoPoint(lon=13.0, lat=52.3),
models.GeoPoint(lon=13.8, lat=52.3),
models.GeoPoint(lon=13.8, lat=52.7),
models.GeoPoint(lon=13.0, lat=52.7),
models.GeoPoint(lon=13.0, lat=52.3),
]
)
)
hits = client.query_points(
"products",
query=[0.9, 0.1, 0.1, 0.0],
query_filter=models.Filter(must=[models.FieldCondition(key="location", geo_polygon=berlin)]),
limit=5,
).points
print([(h.id, h.payload["name"]) for h in hits])
Has ID, Is Empty and Is Null
| Task | Condition |
|---|---|
| Point ID in a list (SQL ) | |
| Point ID not in a list | |
| Missing, null or (SQL ) | |
| Explicitly set to null | |
| Has a value (SQL ) | |
| Point has a given named vector |
matches a key that is missing, null or an empty array. matches only a key that exists with the value null.
Example: products with no brand set, whether the key is null or was never written.
no_brand = models.Filter(
must=[models.IsEmptyCondition(is_empty=models.PayloadField(key="brand"))]
)
points, _ = client.scroll("products", scroll_filter=no_brand, with_payload=["name", "brand"])
print([(p.id, p.payload) for p in points])
Payload Indexes
Filters work without indexes, but every unindexed condition is a scan. Index each field you filter or sort on.
| Task | Call |
|---|---|
| Keyword (strings, exact match) | |
| Number for range filters and sorting | |
| Other types | , , , , , |
| Full-text with options | |
| Tenant field for multitenancy | |
| List indexed fields and counts | |
| Drop an index | |
| Reject filters on unindexed fields |
With strict mode on, a filter on an unindexed key fails with . A scroll with on an unindexed key fails with .
Example: index every payload field the filters on this page use.
P = models.PayloadSchemaType
for field, schema in [
("category", P.KEYWORD),
("tags", P.KEYWORD),
("brand", P.KEYWORD),
("variants[].color", P.KEYWORD),
("price", P.FLOAT),
("in_stock", P.BOOL),
("created_at", P.DATETIME),
("location", P.GEO),
]:
client.create_payload_index("products", field_name=field, field_schema=schema)
print({k: v.data_type for k, v in client.get_collection("products").payload_schema.items()})
Search and query_points
is the search call in current clients. is gone from qdrant-client 1.19 and raises .
| Task | Call |
|---|---|
| Vector search with a filter | |
| Search a named vector | |
| Return only some payload fields | |
| Return the stored vectors too | |
| Drop weak matches | |
| Skip the first results | |
| Points similar to an existing point | |
| More like 1, less like 3 | |
| Best hit per category (SQL ) | |
| Filtered points sorted by date | |
| Tune accuracy |
The response is an object; the hits are in , each with , and .
In 1bench: the Qdrant GUI has a Find Similar mode that takes a point ID and runs a recommend search with your filters applied, so you do not have to copy a vector out first.
Example: hybrid search on a collection, fusing a dense vector and BM25 with RRF, restricted to one category.
client.create_collection(
"docs",
vectors_config={"text": models.VectorParams(size=4, distance=models.Distance.COSINE)},
sparse_vectors_config={"bm25": models.SparseVectorParams(modifier=models.Modifier.IDF)},
)
client.upsert(
"docs",
points=[
models.PointStruct(
id=1,
vector={
"text": [0.1, 0.9, 0.2, 0.0],
"bm25": models.Document(text="waterproof rain jacket for hiking", model="Qdrant/bm25"),
},
payload={"category": "jackets"},
)
],
)
hits = client.query_points(
"docs",
prefetch=[
models.Prefetch(query=[0.1, 0.8, 0.2, 0.1], using="text", limit=20),
models.Prefetch(
query=models.Document(text="rain jacket", model="Qdrant/bm25"), using="bm25", limit=20
),
],
query=models.FusionQuery(fusion=models.Fusion.RRF),
query_filter=models.Filter(
must=[models.FieldCondition(key="category", match=models.MatchValue(value="jackets"))]
),
limit=5,
).points
Scroll and Count
| Task | Call |
|---|---|
| First page of points | |
| Next page | |
| Only matching points (SQL ) | |
| Sorted by a payload field (SQL ) | |
| Leave out payload, include vectors | |
| Count matching points (SQL ) | |
| Total points |
returns a tuple. is the ID to start the next page from, and it is on the last page. With it is always , so page with a range filter on the sort key instead.
In 1bench: the Qdrant GUI shows a collection as a paged table and keeps each page's scroll offset, so moving between pages does not mean replaying by hand.
Example: walk every in-stock product in pages of 100 without loading the vectors.
in_stock = models.Filter(
must=[models.FieldCondition(key="in_stock", match=models.MatchValue(value=True))]
)
offset = None
while True:
points, offset = client.scroll(
"products", scroll_filter=in_stock, limit=100, offset=offset, with_vectors=False
)
for p in points:
print(p.id, p.payload["name"])
if offset is None:
break
Points
| Task | Call |
|---|---|
| Insert or replace by ID | |
| Insert many, column style | |
| Get by ID (SQL ) | |
| Add or change payload keys (SQL ) | |
| Set payload on every filter match | |
| Change one key inside an object | |
| DestructiveReplace the whole payload | |
| Remove payload keys | |
| DestructiveRemove all payload from points | |
| Replace a vector, keep the payload | |
| DestructiveDelete by ID | |
| DestructiveDelete by filter (SQL ) |
, and both deletes cannot be undone. Point IDs must be unsigned integers or UUIDs; a string like is rejected with .
Example: put every shoe on a 10 percent discount, then delete the discontinued products.
shoes = models.Filter(
must=[models.FieldCondition(key="category", match=models.MatchValue(value="shoes"))]
)
client.set_payload("products", payload={"discount": 10}, points=shoes)
discontinued = models.Filter(
must=[models.FieldCondition(key="tags", match=models.MatchValue(value="discontinued"))]
)
client.delete("products", points_selector=models.FilterSelector(filter=discontinued), wait=True)
print(client.count("products").count)
Collections
| Task | Call |
|---|---|
| Create with one vector | |
| Create with named vectors | |
| Add a sparse vector | |
| Check it exists | |
| List collections | |
| Status, point count, config | |
| Change HNSW or optimizer settings | |
| List aliases | |
| DestructiveDelete a collection and all its points |
Distances are , , and . There is no rename. Create the new collection and point an alias at it.
Example: send reads through a alias, then switch it to a rebuilt collection in one atomic call.
client.update_collection_aliases(
change_aliases_operations=[
models.CreateAliasOperation(
create_alias=models.CreateAlias(collection_name="products", alias_name="products_live")
)
]
)
client.update_collection_aliases(
change_aliases_operations=[
models.DeleteAliasOperation(delete_alias=models.DeleteAlias(alias_name="products_live")),
models.CreateAliasOperation(
create_alias=models.CreateAlias(collection_name="products_v2", alias_name="products_live")
),
]
)
Snapshots
| Task | Call |
|---|---|
| Snapshot one collection | |
| List its snapshots | |
| Snapshot the whole storage | |
| Download a snapshot (REST) | |
| DestructiveRestore from a file or URL, replacing the collection | |
| Delete a snapshot |
Recovering into an existing collection replaces its points with the snapshot's. Snapshots are large even for small collections, and big ones can outlast the client's default 5 second timeout, so create the client with .
Example: back up and restore it as a separate collection to check it.
client = QdrantClient(url="http://localhost:6333", timeout=120)
snap = client.create_snapshot("products")
client.recover_snapshot(
"products_restored",
location=f"file:///qdrant/snapshots/products/{snap.name}",
wait=True,
)
print(client.count("products_restored").count)
LangChain and n8n
| Task | Code |
|---|---|
| Where LangChain stores fields | payload |
| Filter on a metadata field | |
| Filtered similarity search | |
| Filtered retriever | |
| n8n Qdrant Vector Store node | same filter JSON as REST, keys under |
Example: a LangChain retriever that only sees policy documents from 2026 or later.
from langchain_qdrant import QdrantVectorStore
store = QdrantVectorStore(client=client, collection_name="kb", embedding=embeddings)
policy_2026 = models.Filter(
must=[
models.FieldCondition(key="metadata.category", match=models.MatchValue(value="policy")),
models.FieldCondition(key="metadata.year", range=models.Range(gte=2026)),
]
)
retriever = store.as_retriever(search_kwargs={"k": 3, "filter": policy_2026})
docs = retriever.invoke("how do returns work")
Gotchas
- is required alongside . A filter with both returns points that match all of and at least one condition, unlike Elasticsearch.
- is case-sensitive and takes no floats. For case-insensitive search use on a text index; for an exact price use .
- Flat conditions can match across different array elements. Use when the conditions must hold for the same element.
- A delete with an empty filter, , removes every point in the collection. A misspelled key does the opposite and silently matches nothing, so run with the same filter first.
- no longer exists in qdrant-client 1.19. Port old code to and read from the response.
- Cosine collections normalize vectors on write, so returns a different vector from the one you upserted.