Home / Filtering / Filtered ANN Search: Keeping Recall High Under Filters
ExplainerFiltered ANN Search: Keeping Recall High Under Filters
Filtered ANN search is approximate nearest neighbor search that returns only items matching a structured condition, such as a price limit or a tenant ID, while keeping recall high. It is harder than it sounds. Graph indexes like HNSW find neighbors by walking from node to node, and a restrictive filter removes most of the nodes on the path. Searching first and filtering afterward can then return too few or wrong results. In this site’s simulation, post-filtering kept only 15 percent recall when 2 percent of items passed the filter. Modern systems combine strategies: brute force over small allowed sets, filter-aware graph traversal such as ACORN for the middle range, and ordinary traversal when most items pass.
Filtered ANN search in four facts
- Post-filtering fails as filters tighten: recall fell from 0.98 at 50 percent selectivity to 0.04 at 0.5 percent in our simulation.
- Negatively correlated filters, which exclude the query’s own neighborhood, are the hardest case.
- ACORN computes distances only for matching nodes and expands two hops to stay connected; its paper reports 2 to 1,000 times higher throughput at fixed recall.
- Weaviate uses ACORN by default since version 1.34 and switches to flat search under 40,000 matching objects by default.
What is filtered ANN search?
Filtered ANN search is a vector search that must satisfy two conditions at once: find the items most similar to the query vector, and return only items whose metadata passes a filter. “ANN” stands for approximate nearest neighbor.
Most real queries carry filters. A shopper wants comfortable shoes under $200 in stock near them. A support assistant must search only the current customer’s documents. A RAG system should read only the active product version. Each filter is a predicate, a yes-or-no test on an object’s properties, and the search must respect it exactly while still ranking by similarity.
Exact search handles this easily: compute the distance to every matching item and sort. The trouble starts with approximate indexes, which avoid comparing every item. Approximate nearest neighbor search explains those indexes, and metadata filtering covers the filters themselves.
Why do restrictive filters break approximate search?
Restrictive filters break approximate search because graph indexes reach good results by hopping between neighbors, and a filter that excludes most nodes either starves the result list or cuts the paths the search depends on. Both failure modes get worse as the filter tightens.
Weaviate’s ACORN article describes the dilemma. If the search walks the graph ignoring the filter and drops non-matching results at the end, a strict filter can leave almost nothing. If it simply skips non-matching nodes, it risks disconnecting the graph. With a very selective filter, there may be no path from the entry point to the region where the matching results sit.
The article’s example is a search for cheap diamond rings. The query vector lands near expensive diamond rings, and few of those pass a low price filter. The search must work its way out to cheaper jewelry, evaluating many objects that will never be returned. Even though HNSW is normally very fast, that detour can make filtered queries slow.
How much of the dataset passes the filter is called selectivity. A filter that passes 50 percent of objects is easy; one that passes 0.5 percent is hard. Filter selectivity covers the concept in depth.
What are the main filtered search strategies?
The main filtered search strategies are pre-filtering with brute force, post-filtering, filtering during graph traversal, filter-aware traversal such as ACORN, and indexes built for known filters. Each fits a different range of selectivity.
| Strategy | How it works | Strong when | Weak when |
|---|---|---|---|
| Pre-filter, flat | Find matching objects first, then brute-force their distances | Few objects match | Many objects match: cost grows linearly |
| Post-filter | Search unfiltered, drop non-matching results | Most objects match | Strict filters: results run out |
| Sweeping | Walk the full graph, collect only matching results | Broad filters | Strict or negatively correlated filters: many wasted distance computations |
| ACORN-style | Compute distances only for matching nodes, expand two hops | Restrictive, low-correlation filters | Very sparse matches in a pruned graph |
| Filter-specific index | Add graph links for anticipated filters, as in Filtered DiskANN | A fixed set of labels | Arbitrary predicates such as price ranges |
Weaviate’s article notes the limit of the last approach. Filtered DiskANN keeps the graph connected for a predefined set of filters, but that requires anticipating filters at index time. With a price filter, every possible range cannot be anticipated. That is why predicate-agnostic methods like ACORN matter.
Production systems combine several strategies rather than choosing one. Pre-filtering versus post-filtering covers the first two in detail, and vector databases with metadata filtering compares how databases implement them.
How do the strategies compare in a simulation?
In our simulation, post-filtering collapsed under strict filters, sweeping kept recall but paid in distance computations, and ACORN-style traversal was cheapest until matches became very sparse. We built a simplified graph index to see the trade-offs directly.
The setup used 20,000 vectors in 32 dimensions, grouped into 50 clusters and 10 categories. The index was a single-layer navigable graph: each vector linked to its 16 nearest neighbors plus two random long links. That is a simplified stand-in for the bottom layer of HNSW, not Weaviate’s implementation. We ran 100 queries with a search list of 64 and measured recall@10 against the exact filtered answer.
| Share passing filter | Post-filter | Sweeping | ACORN-style | Flat |
|---|---|---|---|---|
| 50% | 0.98 / 619 | 0.99 / 806 | 0.99 / 1,080 | 1.00 / 9,936 |
| 10% | 0.60 / 619 | 0.99 / 2,426 | 0.96 / 388 | 1.00 / 1,917 |
| 2% | 0.15 / 619 | 0.95 / 9,003 | 0.60 / 143 | 1.00 / 413 |
| 0.5% | 0.04 / 619 | 0.99 / 18,143 | 0.16 / 18 | 1.00 / 103 |
Post-filtering always cost 619 distance computations, the same as an unfiltered search, but its recall fell to 0.04 as the filter tightened. Sweeping held recall near 0.99 by walking further, which cost 18,143 distance computations at 0.5 percent selectivity: nearly the whole dataset. ACORN-style traversal was the cheapest option at 10 percent, with 388 computations and 0.96 recall.
At 2 and 0.5 percent, our ACORN-style search lost its way. Two-hop expansion from 18 links found too few matching neighbors to keep the graph connected. The ACORN paper addresses this with a denser graph, and real systems avoid the case another way: when few objects match, brute force over them is cheap. Flat search needed only 103 computations at 0.5 percent, for perfect recall.
Why does filter correlation matter?
Filter correlation matters because a filter that removes the query’s own neighborhood forces the search to travel far through the graph before it finds any match. Selectivity alone does not predict difficulty.
Weaviate’s 1.27 release notes define the hard case. In a negatively correlated filtered search, the filter disproportionately removes the entities most similar to the query. The search starts at the “wrong” end of the vector space. A positively correlated filter, by contrast, keeps the query’s neighborhood, so the search finds matches almost immediately.
Our simulation shows the gap. At the same 10 percent selectivity, a positively correlated filter let sweeping finish in 623 distance computations with 0.99 recall. A negatively correlated filter pushed sweeping to 6,889 computations, and recall fell to 0.86. Post-filtering returned no correct results at all, because none of the query’s unfiltered neighbors passed. ACORN-style traversal used 359 computations with 0.80 recall.
Weaviate’s own tests on Cohere BEIR embeddings found the same pattern at scale. At 50 percent selectivity sweeping beat ACORN, but at 20 percent sweeping could deliver half of ACORN’s throughput at equal recall. With very low correlation, ACORN was an order of magnitude faster, because the old logic ended up doing nearly a brute-force pass over the whole graph.
What is the ACORN algorithm?
The ACORN algorithm is a filtered vector search method built on HNSW that searches only the subgraph of nodes passing the filter, using a denser graph and two-hop neighbor expansion to keep that subgraph connected. Its full name in the paper is “Performant and Predicate-Agnostic Search Over Vector Embeddings and Structured Data”.
The paper, by Liana Patel, Peter Kraft, Carlos Guestrin, and Matei Zaharia, appeared in 2024. It reports 2 to 1,000 times higher throughput at a fixed recall than prior methods. It evaluated simple benchmark predicates and complex multimodal datasets that earlier methods could not support. “Predicate-agnostic” means the index does not need to know the filters in advance.
The paper describes two variants. ACORN-γ builds a denser graph so that the filtered subgraph stays navigable. ACORN-1 builds a sparser graph and compensates at search time. Weaviate’s article explains ACORN-1’s core move: when a neighbor fails the filter, the search looks at that neighbor’s neighbors instead. Nodes two hops away become reachable without computing distances for the non-matching node between them.
ACORN also drops HNSW’s usual neighbor pruning during construction, so paths through filtered regions survive. Weaviate’s article notes the resulting graph is larger, though the effective graph is manageable once filtered. Semantic indexing and vector indexes explains how graph indexes are built.
How does Weaviate implement filtered ANN search?
Weaviate implements filtered ANN search by building an allow list from its inverted index, then either brute-forcing small allow lists or walking the HNSW graph with one of three filter strategies: ACORN, sweeping, or PathSeer. ACORN has been the default since version 1.34.
The first decision is the flat-search cutoff. If the filter matches fewer objects than flatSearchCutoff, 40,000 by default, Weaviate skips the graph and computes distances for the matching objects directly. That covers the very sparse filters where graph traversal struggles, as our simulation showed. Above the cutoff, the filter strategy takes over.
Weaviate’s ACORN is a custom version of the paper’s ACORN-1, with three changes described in its engineering article. It keeps vanilla HNSW construction, so ACORN can be enabled without reindexing. It uses two-hop expansion only when the connecting node fails the filter, behaving like normal HNSW in regions dense with matches. And it seeds extra entry points that match the filter, to escape regions where nothing matches.
Version 1.40 added PathSeer, from a SIGMOD 2026 paper by Zhiyue Li, Guangyan Zhang, and colleagues. It walks the graph like sweeping but checks neighbors of neighbors while results are still filling, and skips non-matching neighbors once the result set is full. Weaviate’s documentation reports it was fastest, or within a few percent, on random, topical, and combined filters. ACORN stays faster for sparse, negatively correlated filters. Semantic search with Weaviate covers the wider setup.
What is predicate-based search?
Predicate-based search is search constrained by predicates, logical conditions on an object’s properties such as price < 100 or tenant = "acme", applied together with similarity ranking. In vector databases it is another name for filtered search.
A predicate can be an equality test, a range, a set membership, or a combination with AND and OR. Some are fixed and known, like a tenant ID; others are arbitrary, like any price range a shopper chooses. That distinction drives index design: indexes built for known labels cannot serve arbitrary ranges, which is why the ACORN paper stresses being predicate-agnostic.
In Weaviate, predicates are written as filters, such as a less_than filter on a price property. Range filters on numbers and dates can use a dedicated range index, which Weaviate Academy notes is off by default. Searching versus filtering explains how predicates and ranking interact.
How do you choose a filter strategy?
You choose a filter strategy by measuring recall and latency on your real filtered queries, grouped by how selective and how correlated their filters are. Defaults are reasonable starting points, not guarantees.
- Log real filters: record which filters users apply and how many objects each matches.
- Group by selectivity: broad (over 20 percent), restrictive (1 to 20 percent), and sparse (under the flat cutoff).
- Check correlation: note filters that tend to exclude the query’s best matches, like low prices on luxury queries.
- Measure each group: compare recall@k against exact filtered results, and latency, per strategy.
- Tune the cutoff: raise or lower the flat-search cutoff if brute force wins or loses near the boundary.
The guidance from the sources is consistent. Sweeping does well on broad filters; ACORN does well on restrictive and negatively correlated ones; PathSeer aims to adapt per node. A GitHub issue on Weaviate’s repository reported data where ACORN consistently trailed sweeping, which led to PathSeer, a reminder to test on your own data.
How do you configure the filter strategy in Weaviate?
You configure the filter strategy in Weaviate through the HNSW vector index settings of a collection, with filter_strategy and flat_search_cutoff. Both are mutable, so you can change them without reindexing.
import weaviate
from weaviate.classes.config import Configure, Property, DataType, VectorFilterStrategy
from weaviate.classes.query import Filter
client = weaviate.connect_to_local()
client.collections.create(
"Product",
vector_config=Configure.Vectors.text2vec_openai(
vector_index_config=Configure.VectorIndex.hnsw(
filter_strategy=VectorFilterStrategy.ACORN, # or SWEEPING, PATHSEER
flat_search_cutoff=40000, # brute force under this many matches
)
),
properties=[
Property(name="title", data_type=DataType.TEXT),
Property(name="price", data_type=DataType.NUMBER),
],
)
products = client.collections.use("Product")
response = products.query.near_text(
query="diamond ring",
filters=Filter.by_property("price").less_than(200),
limit=10,
)
client.close()
The query itself does not change with the strategy; only the index setting does. To test strategies, run the same filtered queries against each setting and compare recall with an exact flat search of the matching objects. ANN recall explains how to measure it.
Filtered ANN search keeps approximate vector search fast and accurate when every result must also pass a filter. Post-filtering fails as filters tighten, sweeping stays accurate but slows down, and ACORN-style traversal stays cheap by skipping non-matching nodes and hopping past them. Real systems combine these: Weaviate brute-forces small allow lists, uses ACORN by default above the cutoff, and offers sweeping and PathSeer as alternatives. Measure your own filters by selectivity and correlation before trusting any default.