Filtering & Payload Indexing
|
This section documents the current Qdrant 1.x line (latest 1.19.1), written and verified against the official Qdrant documentation. No specific patch version is pinned. Some capabilities (Qdrant Cloud-only features and the full breadth of client-library APIs beyond Python and Java) are linked, not documented in depth. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Every point’s payload is arbitrary JSON, but it is also a filterable schema: declaring a payload index on a field lets Qdrant narrow a search to the points matching a boolean condition tree before (and during) the vector comparison, instead of scanning payload values one by one.
Payload
A point’s payload — introduced alongside the point itself on Collections & Points — is a free-form JSON object: strings, numbers, booleans, arrays, and nested objects, stored and returned with every point. Filtering treats that same JSON as a queryable schema: any payload field can appear in a filter condition, indexed or not, though an unindexed field is checked with a full scan of the field’s stored values rather than a lookup structure. See Payload for the complete set of supported value types and payload operations (set, overwrite, delete individual keys).
Payload indexes
A payload index is created explicitly, per field, and per value type — Qdrant does not infer an index from the values it sees. Four field schema types cover the condition types below:
| Index type | What it accelerates |
|---|---|
|
Exact-match filtering on string values (and arrays of strings) — category codes, tags, status enums. |
|
Exact match and numeric range filtering on integer values. |
|
Location filtering with |
|
Full-text, word-level matching (tokenized, so a |
PUT /collections/products/index
{
"field_name": "category",
"field_schema": "keyword"
}
PUT /collections/products/index
{
"field_name": "price",
"field_schema": "integer"
}
PUT /collections/products/index
{
"field_name": "location",
"field_schema": "geo"
}
PUT /collections/products/index
{
"field_name": "description",
"field_schema": "text"
}
from qdrant_client.models import PayloadSchemaType
client.create_payload_index(
collection_name="products",
field_name="category",
field_schema=PayloadSchemaType.KEYWORD,
)
client.create_payload_index(
collection_name="products",
field_name="price",
field_schema=PayloadSchemaType.INTEGER,
)
Filter clauses
A filter is a tree of three boolean clauses, each holding a list of conditions:
| Clause | Semantics |
|---|---|
|
Every condition in the list has to match — logical AND. |
|
At least one condition in the list has to match — logical OR. |
|
No condition in the list may match — logical NOT, applied to the whole list. |
The three can be combined in one filter (e.g. must narrows the set while should adds an
either/or preference among a subset of fields), and clauses can nest to build arbitrary boolean
expressions. Each condition itself is one of a few condition types:
-
match — exact value equality against a
keywordorintegerfield (or "any of these values" viamatch.any). -
range — numeric or date bounds (
gt,gte,lt,lte) against aninteger/float/datetime field. -
geo —
geo_radius(points within a distance of a center) orgeo_bounding_box(points inside a lat/lon rectangle) against ageofield.
POST /collections/products/points/scroll
{
"filter": {
"must": [
{ "key": "category", "match": { "value": "audio" } },
{ "key": "price", "range": { "lte": 300 } }
],
"should": [
{ "key": "brand", "match": { "any": ["acme", "globex"] } }
],
"must_not": [
{ "key": "discontinued", "match": { "value": true } }
]
},
"limit": 10
}
A geo condition narrows to points whose location field falls inside a radius or a rectangle:
POST /collections/products/points/scroll
{
"filter": {
"must": [
{
"key": "location",
"geo_radius": {
"center": { "lon": 13.403, "lat": 52.520 },
"radius": 5000.0
}
}
]
},
"limit": 10
}
geo_bounding_box takes top_left and bottom_right corners instead of a center and radius, for a
rectangular region rather than a circle. Full condition and clause reference:
Filtering.
from qdrant_client.models import (
Filter, FieldCondition, MatchValue, MatchAny, Range,
GeoRadius, GeoPoint, IsNullCondition, PayloadField,
)
results, _ = client.scroll(
collection_name="products",
scroll_filter=Filter(
must=[
FieldCondition(key="category", match=MatchValue(value="audio")),
FieldCondition(key="price", range=Range(lte=300)),
],
should=[
FieldCondition(key="brand", match=MatchAny(any=["acme", "globex"])),
],
must_not=[
FieldCondition(key="discontinued", match=MatchValue(value=True)),
],
),
limit=10,
)
Combining a filter with a vector query
Passing a filter alongside a query vector in the same search request does not run the vector
search first and thin the results afterwards (a post-filter, which can return fewer than limit
hits — or none — when the filter is highly selective). Qdrant’s HNSW graph is filterable: each
node carries enough payload-index information that graph traversal itself skips neighbors the filter
would exclude, so the search keeps exploring until it has limit matching points (or the true
candidate set is exhausted), rather than collecting limit unfiltered neighbors and only then
checking which ones pass. This is the same problem Elasticsearch’s filter inside knn and Solr’s
fq pre-filter into \{!knn\} solve, each with its own mechanism — Dense vector search contrasts Solr’s implicit fq pre-filter, which
restricts the HNSW traversal at the query-parser level, against Qdrant’s richer must/should/
must_not boolean model expressed as one clause inside the same search request.
POST /collections/products/points/query
{
"query": [0.11, -0.95, 0.30, 0.42],
"filter": {
"must": [
{ "key": "category", "match": { "value": "audio" } },
{ "key": "price", "range": { "lte": 300 } }
]
},
"limit": 10
}
from qdrant_client.models import Filter, FieldCondition, MatchValue, Range
results = client.query_points(
collection_name="products",
query=[0.11, -0.95, 0.30, 0.42],
query_filter=Filter(
must=[
FieldCondition(key="category", match=MatchValue(value="audio")),
FieldCondition(key="price", range=Range(lte=300)),
],
),
limit=10,
).points
Because the filter is honored while the graph is walked, indexing the fields a filter regularly restricts on (via Payload indexes) matters even more than it does for a filter-only query: an unindexed field forces the filter check on each visited node to fall back to a direct payload lookup, which slows exactly the traversal the filterable HNSW graph is meant to keep fast.
Where to go next
-
Vector Indexing & HNSW — how the HNSW graph these filters run over is built and tuned.
-
Hybrid & Sparse Search — combining a filtered dense-vector query like the one above with a sparse-vector query in the same request.
-
Dense vector search — Solr’s
fqimplicit pre-filter combined with\{!knn\}, contrasted against Qdrant’smust/should/must_notfilter model above.