Filtering in Vector Search: When to Use What

Imagine you sell computer hardware. A shopper looking for laptops under $1000 wants similar laptops, but a plain vector search can still return laptops over $1000. A payload filter guarantees that every result meets the constraint:

vector-search-ecommerce

POST /collections/online_store/points/query
{
  "query": [0.2, 0.1, 0.9, 0.7],
  "filter": {
    "must": [
      { "key": "category", "match": { "value": "laptop" } },
      { "key": "price", "range": { "lte": 1000 } }
    ]
  },
  "limit": 3,
  "with_payload": true
}

This guide covers the decisions around a filter like this one: which fields to index, how Qdrant runs the search, and when to change the defaults. For the syntax of every filter condition, see the filtering documentation.

Quick Reference

SituationWhat to doDetails
You filter on a field in dense vector searchCreate a payload index on it, before you ingest dataIndex Every Field You Filter On
You filter on a field only in sparse searchIndex it with enable_hnsw: falsePayload indexes
You added an index to a collection that already has dataRebuild the HNSW index to get the filter edgesRebuild the HNSW index
Many customers share one collectionIndex the tenant field with is_tenant: trueUse One Collection for Many Tenants
A query combines two or more strict filters and recall dropsTurn on ACORN for that queryTurn On ACORN for Strict Combined Filters
You filter on a float fieldUse range, not matchCommon Pitfalls
You filter on fields inside an array of objectsUse a nested conditionCommon Pitfalls
You need matching points without rankingUse scroll or count with the filterScroll points

Filter During the Search, Not Before or After

There are two traditional ways to combine a filter with vector search, and both have a cost.

  • Post-filtering runs the vector search first and then drops results that fail the filter. If many of the top results fail, you get fewer results than you asked for.
  • Pre-filtering applies the filter first and then compares the query with every matching point. Results are exact, but scoring every match is expensive when many points pass the filter.

The diagram ranks five laptops by similarity to the query vector [0.2, 0.1, 0.9, 0.7].

Post-filtering returns two of three results; pre-filtering returns three
Post-filtering takes the top 3 and then applies the price filter, so one of the three slots is lost. Pre-filtering applies the filter first and scores only the matches, so all three results fit the filter.

Qdrant filters during the search, so you do not have to choose. Its query planner either scores the matching points directly when few points match, or searches the HNSW graph while skipping points that fail the filter. To keep that graph walk from getting stuck, Qdrant builds a filterable HNSW index: extra edges between points that share a value of an indexed payload field. When a single field’s edges are not enough, ACORN can step through filtered-out neighbors at search time.

The same graph, repaired two ways
The same graph, repaired two ways. ACORN steps through filtered-out neighbors at search time; filterable HNSW adds extra edges at index time that a filtered query can walk directly. The graph is a toy; the traversals are computed on it.

Index Every Field You Filter On

Qdrant uses payload indexes in three ways: to find matching points without scanning every payload, to estimate how many points a filter matches so the query planner can pick a strategy, and to build the extra filterable HNSW edges.

  • Index every field you filter on in dense vector search. Without an index, the planner cannot estimate how many points match, and Qdrant has to read payloads to evaluate the filter. You can make Qdrant reject queries that filter on unindexed fields, so a missing index surfaces as an error instead of a slow query.
  • Match the index type to the values and conditions. Use keyword for exact matches on strings, integer or float for ranges, and bool, datetime, uuid, geo, or text for those value types. The payload index documentation lists every type and its parameters.
  • Create payload indexes before you ingest data. Qdrant builds the extra filterable HNSW edges only for fields that are indexed when the graph is built. An index added later still serves filters, but the graph has no extra edges for it until you rebuild the HNSW index.
  • Skip the extra edges for fields you filter only in sparse search. Sparse vector search does not use the HNSW graph, so set enable_hnsw to false on those payload indexes to save memory and indexing time. This option is available as of v1.17.
  • Mark a tenant field and a principal field. Use is_tenant for the field that separates customers in a shared collection, and is_principal for the field most of your queries filter on, such as a timestamp. Both let Qdrant organize storage around the field.

Here is how to create a payload index for the category field:

PUT /collections/online_store/index
{
    "field_name": "category",
    "field_schema": "keyword"
}
from qdrant_client import QdrantClient

client = QdrantClient(url="http://localhost:6333")

client.create_payload_index(
   collection_name="online_store",
   field_name="category",
   field_schema="keyword",
)

Let the Query Planner Choose

For each filtered query, the query planner estimates the filter cardinality, the number of points that satisfy the filter, from the payload indexes. It plans each segment of the collection separately.

The query planner chooses between scoring the matches directly and searching the filterable HNSW graph
The query planner estimates how many points match, then scores them directly or searches the filterable HNSW graph. ACORN is an option for strict filter combinations.
  • Few points match: if the vectors of the estimated matches take up less than full_scan_threshold, Qdrant scores each match directly and returns exact results. The default is 10,000 KB, about 10,000 vectors of 256 dimensions or 2,500 vectors of 1024 dimensions.
  • Many points match: Qdrant searches the filterable HNSW graph and skips points that fail the filter.
  • A small share of points match, but too many to score directly: this is where the graph can fall apart, especially with two or more strict filters. Turn on ACORN for these queries.

The defaults suit most collections. You can change full_scan_threshold in the collection’s HNSW configuration.

Turn On ACORN for Strict Combined Filters

Filterable HNSW builds extra edges for each indexed field separately, not for combinations of fields. When a query combines two or more strict filters, the points that satisfy all of them may not be connected in the graph, and recall drops. The same can happen when a collection holds many deleted points.

ACORN, available as of v1.16, fixes this at search time: when a neighbor is filtered out, the search also looks at that neighbor’s neighbors. That costs extra work per query, so use it where it pays off.

  • Turn ACORN on for queries that combine two or more strict filters, or for collections with many deleted points, when filtered recall is lower than you need.
  • Leave it off for single-field filters with payload indexes, because the filterable HNSW edges already cover them.

To check whether you need it, run a sample of your filtered queries twice: once as usual and once with "params": { "exact": true }, which scores every matching point and gives the true top results. If the normal results miss too many of the exact ones, turn on ACORN and compare again.

ACORN is off by default. Once enabled, it runs only when the estimated share of matching points is below max_selectivity, which defaults to 0.4:

POST /collections/online_store/points/query
{
    "query": [0.2, 0.1, 0.9, 0.7],
    "filter": {
        "must": [
            { "key": "category", "match": { "value": "laptop" } },
            { "key": "price", "range": { "lte": 1000 } }
        ]
    },
    "params": {
        "acorn": { "enable": true, "max_selectivity": 0.4 }
    },
    "limit": 3
}

To see how ACORN and filterable HNSW compare on real filters, read Filtered Vector Search: What ACORN Fixes, and What Fixes ACORN.

Use One Collection for Many Tenants

Do not create a separate collection for each customer in the same cluster. With many tenants, this exhausts the cluster’s resources and can lead to out-of-memory errors.

Store all tenants in one collection, give each point a tenant field such as user_id, filter every query by it, and mark it as the tenant field when you index it:

PUT /collections/{collection_name}/index
{
   "field_name": "user_id",
   "field_schema": {
       "type": "keyword",
       "is_tenant": true
   }
}

With is_tenant set, Qdrant keeps each tenant’s points together, which makes tenant-filtered queries faster. See multitenancy for the full setup.

Common Pitfalls

  • Floats need range, not match. Qdrant rejects a match condition with a float value such as 11.99. To find an exact stored value, set both bounds to it: "range": { "gte": 11.99, "lte": 11.99 }. If values carry rounding errors, use a small window instead. For money, you can also store whole cents as an integer.
  • Conditions on an array of objects need nested. Two plain conditions such as diet[].food is meat and diet[].likes is true can each match a different element of the array. To require both on the same element, use a nested object filter.
  • Multiple values do not create duplicates. When a field holds several values, such as a product in red, blue, and green, the point matches if any value satisfies the condition, and it appears once in the results.
  • Ordering by a field needs a range index. scroll with order_by fails on a field without a payload index that supports ranges, and it returns no next_page_offset. See ordering points by payload key.

Filters also work outside search: you can delete, count, or update the payload of every point that matches one.

Further Reading

Try Filtering in a Live Cluster

The easiest way to try filtering is in a free Qdrant Cloud cluster. The interactive tutorial in the dashboard shows you how to create a collection, add data, and run filters.

qdrant-filtering-tutorial

qdrant-hybrid-cloud

Was this page useful?

Thank you for your feedback! 🙏

We are sorry to hear that. 😔 You can edit this page on GitHub, or create a GitHub issue.