Yeah, you nailed it. "The true filtered-nearest was never in the pool" is the part that makes it invisible, and honestly you said it cleaner than I did.
That ordering is right, and it's basically the set of options the tool picks between. The thing I kept running into - and why I gave up on just writing down the ladder - is that the rung you want depends on how sparse the filter is right around the query, and that changes query to query. ef_search helps, but if the filter is basically absent near the query you'd need it around k / local_selectivity to get enough survivors, which is just a scan with extra steps. Iterative scan is the real fix most of the time, agreed, but its cost climbs the same ~1/selectivity way and max_scan_tuples caps it, so once you're sparse enough a plain exact filtered scan is actually faster and can't come back short. So there's a crossover, and it moves around. Working out which side of it you're on for a given query is the whole thing I'm trying to automate.
And yeah - the LIMIT check is the best free canary there is. Tenant has at least LIMIT rows and you got fewer back? Something's off. That's literally one of the two numbers I report (returns-k rate) for exactly that reason. Mind if I steal that framing for the README as the "no tool needed" check? Happy to credit you.
Anyway, thanks. Comments like this are why it's worth writing these up.
This is the pgvector footgun I've watched bite the most people, exactly because it fails quiet. The model that made it click for me: HNSW hands you ef_search candidates from the unfiltered graph, then the WHERE runs. If tenant 42 is a small slice, the true filtered-nearest may never have been in that candidate pool, so you silently get fewer than ten — or ten worse ones — and the plan says nothing.
Worth adding to the mitigation list, in rough order of cost: bump hnsw.ef_search first (widens the pool, buys margin, no schema change); then reach for iterative scan (hnsw.iterative_scan = strict_order/relaxed_order in 0.8+), which keeps walking the graph until it has enough rows that pass the filter — the closest thing to an actual fix. For a handful of large tenants, a partial HNSW index per tenant restores in-filter recall outright, at the cost of index proliferation. And the cheapest detector for anyone who can't adopt a tool yet: when the tenant has ≥LIMIT rows, assert you got LIMIT back — a short result set is the alarm the query plan won't raise.
The reproducible-from-fresh-checkout benchmark is what makes this land; most "recall collapsed" posts hand-wave exactly the part you measured.