Qdrant Filter Cheat Sheet

Qdrant is a vector database, and filters on each point's payload decide which points a search, scroll, count or delete can touch. This sheet covers filter syntax in the Python client, payload indexes, query_points, scroll and point updates, with one products collection in every example.

Updated Oct 12, 2026Tested against Qdrant 1.19.2, qdrant-client 1.19.1

Filters

Examples on this page assume and a collection with the payload fields , , , , , , , , and .

TaskFilter
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:

python
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 :

console
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.

python
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

TaskCondition
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 theseone 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:

python
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.

python
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

TaskCondition
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.

python
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

TaskCondition
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.

python
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.

TaskCondition
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.

python
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

TaskCondition
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.

python
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.

TaskCall
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.

python
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 .

TaskCall
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.

python
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

TaskCall
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.

python
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

TaskCall
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.

python
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

TaskCall
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.

python
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

TaskCall
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.

python
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

TaskCode
Where LangChain stores fieldspayload
Filter on a metadata field
Filtered similarity search
Filtered retriever
n8n Qdrant Vector Store nodesame filter JSON as REST, keys under

Example: a LangChain retriever that only sees policy documents from 2026 or later.

python
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.

Work with Qdrant without memorizing it

1bench is a desktop GUI for Qdrant. Connect, browse your data and run queries in one window, with the query DSL still there when you want it.

Open Qdrant in 1bench

Qdrant FAQ

How do must and should work together in a Qdrant filter?
Every clause in a filter has to hold. must needs all of its conditions, must_not needs none of them, and should needs at least one, even when must is also present. So must=[in stock] with should=[shoes, accessories] returns in-stock shoes and in-stock accessories, not every in-stock product. This differs from Elasticsearch, where should becomes optional once a must clause exists. Use min_should when you need two or more of the should conditions.
Does Qdrant filter before or after the vector search?
Neither, in the usual sense. Qdrant applies the filter while it walks the HNSW graph, so you get the full limit of matching results instead of a post-filtered handful. When a filter matches only a small share of points, the query planner skips the graph and scores the matching points directly from the payload index. Both paths depend on payload indexes, so index every field you filter on.
How do I sort Qdrant results by date instead of score?
Create a datetime (or integer or float) payload index on the field, then pass order_by. On scroll that is order_by=models.OrderBy(key="created_at", direction="desc"). On query_points use query=models.OrderByQuery(order_by=models.OrderBy(key="created_at", direction="desc")) together with a query_filter. Without the index both return a 400 error saying there is no range index for the order_by key.
Does Qdrant support hybrid search with BM25?
Yes. Add a sparse vector to the collection with modifier=models.Modifier.IDF, then upsert and query it with models.Document(text=..., model="Qdrant/bm25"). Recent Qdrant servers compute the BM25 vector themselves, so the client needs no extra package. Combine it with a dense vector through prefetch and a FusionQuery (RRF), and the query_filter applies to the fused result.
How do I filter by metadata in LangChain with Qdrant?
LangChain's QdrantVectorStore stores each document as a payload with page_content and a metadata object, so your fields live one level down. Build a models.Filter with key="metadata.category" rather than key="category" and pass it as filter= to similarity_search or inside search_kwargs for as_retriever. A plain dict such as {"category": "policy"} fails validation, and a key without the metadata prefix silently matches nothing.