Short answer: It runs similarity only inside a metadata-defined subset, such as one user, category, or time range.
Post-filtering after a global similarity search is unreliable when few results match the filter. Filtering before or during search keeps recall predictable. Very tight filters and filters that fight the natural neighborhood graph are harder. Scoped Engram memory retrieval relies on this machinery so scope is a real constraint, not a cosmetic discard step.
Everything covered so far in this Part has treated similarity search as if it operated over an entire collection at once. Real searches almost never work that way. A query usually needs to be restricted to a specific subset first, a specific user’s own memories, a specific category, a specific time range, before similarity even enters the picture. Filtered vector search is exactly this combination, and how that combination gets implemented turns out to matter far more than it first appears.
Why Isn’t It Enough to Just Run a Similarity Search and Then Throw Away Results That Don’t Match a Filter?
Running the similarity search first and discarding non-matching results afterward, called post-filtering, has an obvious appeal: it’s simple, reusing the exact same search already covered throughout this Part with an extra check bolted on at the end. The problem is predictability. If a filter is restrictive, matching only a small slice of the overall collection, there’s no reliable way to know in advance how many of the initial similarity search’s results will actually survive that filter. Asking for ten results and getting back only two after filtering, or even zero, is a real, common failure mode of this approach, since the initial search had no awareness of the filter at all while deciding which candidates to return in the first place.
How Does Filtering Before the Similarity Search Instead of After Actually Solve This?
The alternative, called pre-filtering, determines which candidates satisfy the filter first, before the similarity search ever runs, then constrains the similarity search to only consider that already-narrowed set. This guarantees that every result the similarity search could possibly return already satisfies the filter, eliminating the unpredictable, sometimes-empty results post-filtering risks. The tradeoff is that this requires the underlying system to actually support efficiently identifying and searching within just that restricted subset, rather than always searching the full collection and sorting things out afterward.
What Actually Happens When a Filter Is Extremely Restrictive, Matching Only a Tiny Fraction of the Collection?
An index like HNSW, covered earlier in this Part, is built around efficiently navigating a large, densely connected graph. When a filter restricts the eligible candidates down to a small handful out of millions, that graph’s usual efficiency advantage largely disappears, since most of the graph’s neighbors at any given point turn out to be ineligible, and the search has to work harder to find its way toward the small, scattered pocket of candidates that actually pass the filter. In the most extreme cases, searching a heavily restricted subset directly, essentially checking each eligible candidate one by one, ends up being more efficient than trying to force a graph built for a much larger, unrestricted search to navigate toward a tiny, sparse target.
This is exactly why well-built filtered search systems don’t apply one single strategy universally, they adapt based on just how restrictive a given filter actually turns out to be, switching approaches when a filter narrows things down enough that a different strategy becomes genuinely faster.
Does It Matter Whether a Filter Happens to Correlate With What the Similarity Search Would Have Found Anyway?
It matters a great deal. If a filter’s eligible candidates tend to already cluster near where the similarity search would naturally be looking, filtering barely slows anything down, since the search was already heading in roughly the right direction regardless. If a filter’s eligible candidates are scattered in a completely different region of the space from where the similarity search naturally wants to look, the search has to work considerably harder to find its way from one region to the other, since the graph’s normal navigation logic was never built with that specific combination of filter and query in mind. This is a genuinely tricky case for a filtered search to handle well, and it’s exactly the scenario that has driven meaningful engineering effort in modern vector databases specifically aimed at keeping performance solid even when a filter and a query pull in very different directions.
How Does This Connect Directly to the Scoping Already Covered Throughout This Knowledge Base’s Earlier Parts?
Every scoped memory search covered extensively in earlier Parts, restricting results to one specific user, one specific conversation, one specific property value, is a filtered vector search in exactly the sense this chapter has been describing. The scoping parameter narrows the eligible set, and the similarity search then operates only within that narrowed set, precisely the pre-filtering pattern this chapter has explained. This means everything covered here about restrictive filters and correlation applies directly to memory scoping: a search scoped to one very active user with thousands of stored memories behaves very differently, performance-wise, from a search scoped to a brand-new user with only a handful.
How Does Weaviate Engram Apply This Filtered-Search Machinery to Scoped Memory Retrieval?
Weaviate Engram’s `user_id` and `properties` scoping parameters function as exactly this kind of pre-filter, narrowing a search to the correct eligible set before similarity search ever runs, benefiting directly from the underlying filtered-search optimizations this chapter has described. Consider a clinical-trial patient-matching assistant helping researchers find patients who both satisfy strict eligibility criteria and share a genuinely similar clinical history to a specific reference case:
from engram import EngramClient
client = EngramClient(api_key=os.environ["ENGRAM_API_KEY"])
client.memories.add(
"Patient presents with early-stage response to the trial compound, elevated biomarker levels consistent with the target patient profile, no prior enrollment in a competing trial within the last twelve months.",
properties={"trial_id": "trial-nx-4471", "eligibility_status": "confirmed"},
)
A researcher searching for similar eligible patients benefits from the filter and similarity search working together correctly:
results = client.memories.search(
query="Which confirmed-eligible patients show an early biomarker response pattern similar to this reference case?",
properties={"trial_id": "trial-nx-4471", "eligibility_status": "confirmed"},
retrieval_config=HybridRetrieval(limit=10),
)
Restricting the search to only confirmed-eligible patients before similarity search even runs guarantees every result returned is actually eligible, avoiding exactly the unpredictable, sometimes-empty result set a naive post-filter approach risks producing if eligibility happens to be a genuinely restrictive criterion for this particular trial. This is precisely why scoped memory search throughout this knowledge base has always relied on the underlying system correctly implementing filtered search rather than treating scoping as a cosmetic afterthought applied to results after the fact.
Filtered search combines structured criteria with similarity in a single query. A separate, upstream question this Part hasn’t yet addressed is how a longer piece of text actually gets broken down into the individual pieces that get embedded and searched in the first place. Our next chapter, Why do chunking strategies matter for memory?, takes up exactly that upstream decision.