Skip to content

fix: add resetRadiusThreshold() mitigation for radius threshold leak (closes #200) - #209

Merged
s2x merged 2 commits into
mainfrom
fix/issue-200-radius-refiner-threshold-leak
Aug 30, 2026
Merged

fix: add resetRadiusThreshold() mitigation for radius threshold leak (closes #200)#209
s2x merged 2 commits into
mainfrom
fix/issue-200-radius-refiner-threshold-leak

Conversation

@s2x

@s2x s2x commented Aug 30, 2026

Copy link
Copy Markdown
Member

Description

Closes #200

A radius-filtered Flat/IVF query poisons the thread-local search context in upstream zvec (threshold cached on a thread_local context shared per index type; Flat/IVF reset() is a no-op; radius gated by if (radius > 0.0f)): every later radius-less query on that thread — refiner, plain, any field of the same index type, even other collections — silently returns only the radius-filtered subset.

Changes

  • ZVec::RADIUS_THRESHOLD_RESET (FLT_MAX) — purge value mirroring upstream reset_threshold()
  • ZVec::resetRadiusThreshold(string $fieldName, array $vector) — throwaway topk-1 purge query that overwrites the leaked threshold
  • Regression test test_radius_threshold_leak.phpt (documents the leak + recovery)
  • CHANGELOG, docs/helpers/faq.md, docs/helpers/decisions.md knowledge-base entries

Empirical verification (built dylib, zvec v0.6.0)

  • Leak reproduced and purge restores full results on FLAT-L2, IVF-L2, FLAT-COSINE, FLAT-MIPSL2 and FLAT-IP (incl. negative-similarity docs)
  • Cross-field same-index-type leak: purge works from the other field
  • Pre-emptive purge (before any radius query) is harmless; repeatable
  • Scalar filters do NOT leak; HNSW self-resets (its context reset() calls reset_threshold())
  • IP detail: radius filters docs with similarity < -radius (opposite vectors) — the leak occurs for IP too, and the purge restores the missing docs
  • Not eligible for purge: sparse fields (purge query is always dense fp32 — documented)

Testing

  • FFI builds locally (dylib)
  • All .phpt tests pass (macOS known-fail test_collections_thread_safety aside, environmental — documented)
  • No test database leftovers
  • Subagent code review passed — all findings (docs accuracy: IP behavior, class name, PHP_FLOAT_MAX rationale, MipsL2 denormalization attribution) fixed

Piotr Hałas added 2 commits August 30, 2026 19:41
…loses #200)

A radius-filtered Flat/IVF query poisons the thread-local search context:
subsequent radius-less queries on the same thread return only the
radius-filtered subset (upstream: no-op Flat/IVF context reset, radius>0
gate in _prepare_for_search). Add ZVecCollection::resetRadiusThreshold()
plus ZVec::RADIUS_THRESHOLD_RESET (FLT_MAX) purge query and regression
test test_radius_threshold_leak.phpt.
- IP metrics: radius DOES filter docs with similarity < -radius (e.g.
  opposite vectors) — the leak can occur for IP too, and the purge
  restores the missing negative-similarity docs (empirically verified).
  Replaces the false 'radius never filters / leak cannot occur / no-op'
  claims in CHANGELOG, method docblock, faq and decisions.
- ZVec:: not ZVecCollection:: (no such class).
- RADIUS_THRESHOLD_RESET rationale: PHP_FLOAT_MAX overflows to inf in
  float32 (not 'rejected by the FFI'); FLT_MAX mirrors upstream
  reset_threshold(). Fix MipsL2 '-1' misattribution (denormalize is a
  sign flip via its InnerProduct query metric; the -= 1 shift is cosine).
- Docblock: purge query is always dense fp32 — sparse fields cannot be
  purged through this method.
@s2x s2x self-assigned this Aug 30, 2026
@s2x
s2x merged commit 983be55 into main Aug 30, 2026
3 checks passed
@s2x
s2x deleted the fix/issue-200-radius-refiner-threshold-leak branch August 30, 2026 19:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

queryVector(): radius-filtered query leaks threshold into subsequent usingRefiner() queries

1 participant