From 533b3d9da3f87a13e75022ffd504433629342817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ha=C5=82as?= Date: Sat, 8 Aug 2026 19:22:46 +0200 Subject: [PATCH 1/2] fix: honor radius/linear/refiner set before set*Params() (closes #197) --- ffi/zvec_ffi.cc | 17 +++++ tests/test_query_params_order.phpt | 104 +++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 tests/test_query_params_order.phpt diff --git a/ffi/zvec_ffi.cc b/ffi/zvec_ffi.cc index ce480e3..100ac2a 100644 --- a/ffi/zvec_ffi.cc +++ b/ffi/zvec_ffi.cc @@ -2017,6 +2017,18 @@ struct GroupByVectorQueryHolder { bool is_using_refiner_ = false; }; +// Merge radius/is_linear/is_using_refiner set before a set*Params() call into +// the newly created params so setter ordering does not matter (#197). +// Constructor defaults for these fields are 0.0f/false/false, so an +// unconditional merge is a no-op when they were never set. +static void merge_stored_query_settings(const VectorQueryHolder* holder) { + auto& params = holder->query.target_.query_params_; + if (!params) return; + params->set_radius(holder->radius_); + params->set_is_linear(holder->is_linear_); + params->set_is_using_refiner(holder->is_using_refiner_); +} + zvec_vector_query_t zvec_vector_query_create(void) { auto* holder = new VectorQueryHolder(); return static_cast(holder); @@ -2065,30 +2077,35 @@ void zvec_vector_query_set_hnsw_ef(zvec_vector_query_t q, int ef) { if (!q) return; auto* holder = static_cast(q); holder->query.target_.query_params_ = std::make_shared(ef); + merge_stored_query_settings(holder); } void zvec_vector_query_set_hnsw_rabitq_ef(zvec_vector_query_t q, int ef) { if (!q) return; auto* holder = static_cast(q); holder->query.target_.query_params_ = std::make_shared(ef); + merge_stored_query_settings(holder); } void zvec_vector_query_set_vamana_ef_search(zvec_vector_query_t q, int ef_search) { if (!q) return; auto* holder = static_cast(q); holder->query.target_.query_params_ = std::make_shared(ef_search); + merge_stored_query_settings(holder); } void zvec_vector_query_set_ivf_nprobe(zvec_vector_query_t q, int nprobe) { if (!q) return; auto* holder = static_cast(q); holder->query.target_.query_params_ = std::make_shared(nprobe); + merge_stored_query_settings(holder); } void zvec_vector_query_set_flat_mode(zvec_vector_query_t q) { if (!q) return; auto* holder = static_cast(q); holder->query.target_.query_params_ = std::make_shared(); + merge_stored_query_settings(holder); } void zvec_vector_query_set_radius(zvec_vector_query_t q, float radius) { diff --git a/tests/test_query_params_order.phpt b/tests/test_query_params_order.phpt new file mode 100644 index 0000000..0765b03 --- /dev/null +++ b/tests/test_query_params_order.phpt @@ -0,0 +1,104 @@ +--TEST-- +Query params order: setRadius/setLinear/setUsingRefiner before set*Params() are honored (#197) +--SKIPIF-- + +--FILE-- + $d->getPk(), $results); +} + +try { + $schema = new ZVecSchema('test'); + $schema->addVectorFp32('vf', dimension: 4, metricType: ZVecSchema::METRIC_L2); + $schema->addVectorFp32('vh', dimension: 4, metricType: ZVecSchema::METRIC_L2); + + $c = ZVec::create($path, $schema); + $c->createIndex('vf', ZVecIndexParams::forFlat(ZVecSchema::METRIC_L2)); + $c->createIndex('vh', ZVecIndexParams::forHnsw(ZVecSchema::METRIC_L2)); + + // Squared L2 distances from query [1,0,0,0]: doc1=0, doc2=0.25, doc3=4 + $vecs = [ + 'doc1' => [1.0, 0.0, 0.0, 0.0], + 'doc2' => [1.5, 0.0, 0.0, 0.0], + 'doc3' => [3.0, 0.0, 0.0, 0.0], + ]; + $docs = []; + foreach ($vecs as $pk => $v) { + $docs[] = (new ZVecDoc($pk))->setVectorFp32('vf', $v)->setVectorFp32('vh', $v); + } + $c->insert(...$docs); + $c->flush(); + $c->optimize(); + echo "Inserted 3 docs\n"; + + $qv = [1.0, 0.0, 0.0, 0.0]; + + // setUsingRefiner before/after params (flat index acts as its own reference). + // Must run before any radius query: a radius-filtered query leaks its + // threshold into subsequent refiner queries (upstream zvec db-layer issue, + // unrelated to #197). + $q = (new ZVecVectorQuery('vf', $qv))->setTopk(10)->setUsingRefiner(true)->setFlatParams(); + echo 'flat refiner-before-params: ', implode(',', pks($c->queryVector($q))), "\n"; + + $q = (new ZVecVectorQuery('vf', $qv))->setTopk(10)->setFlatParams()->setUsingRefiner(true); + echo 'flat refiner-after-params: ', implode(',', pks($c->queryVector($q))), "\n"; + + // Baseline (params first, then radius) vs bug order (radius first, then params) + $q = (new ZVecVectorQuery('vf', $qv))->setTopk(10)->setFlatParams()->setRadius(0.3); + echo 'flat params-then-radius: ', implode(',', pks($c->queryVector($q))), "\n"; + + $q = (new ZVecVectorQuery('vf', $qv))->setTopk(10)->setRadius(0.3)->setFlatParams(); + echo 'flat radius-then-params: ', implode(',', pks($c->queryVector($q))), "\n"; + + $q = (new ZVecVectorQuery('vh', $qv))->setTopk(10)->setHnswParams(200)->setRadius(0.3); + echo 'hnsw params-then-radius: ', implode(',', pks($c->queryVector($q))), "\n"; + + $q = (new ZVecVectorQuery('vh', $qv))->setTopk(10)->setRadius(0.3)->setHnswParams(200); + echo 'hnsw radius-then-params: ', implode(',', pks($c->queryVector($q))), "\n"; + + // Wide radius before params keeps all docs (doc3 dist^2 = 4 <= 5) + $q = (new ZVecVectorQuery('vh', $qv))->setTopk(10)->setRadius(5.0)->setHnswParams(200); + echo 'hnsw wide-radius-before-params: ', implode(',', pks($c->queryVector($q))), "\n"; + + // setLinear before params + $q = (new ZVecVectorQuery('vh', $qv))->setTopk(10)->setLinear(true)->setHnswParams(200); + echo 'hnsw linear-before-params: ', implode(',', pks($c->queryVector($q))), "\n"; + + // setRadius + setLinear before params + $q = (new ZVecVectorQuery('vh', $qv))->setTopk(10)->setRadius(0.3)->setLinear(true)->setHnswParams(200); + echo 'hnsw radius+linear-before-params: ', implode(',', pks($c->queryVector($q))), "\n"; + + // Params-first path still works with no radius set (unchanged behavior) + $q = (new ZVecVectorQuery('vh', $qv))->setTopk(10)->setHnswParams(200); + echo 'hnsw no-radius: ', implode(',', pks($c->queryVector($q))), "\n"; + + echo "ALL TESTS PASSED\n"; +} finally { + if (isset($c)) { try { $c->destroy(); } catch (Exception $e) {} } + exec("rm -rf " . escapeshellarg($path)); +} +?> +--EXPECT-- +Inserted 3 docs +flat refiner-before-params: doc1,doc2,doc3 +flat refiner-after-params: doc1,doc2,doc3 +flat params-then-radius: doc1,doc2 +flat radius-then-params: doc1,doc2 +hnsw params-then-radius: doc1,doc2 +hnsw radius-then-params: doc1,doc2 +hnsw wide-radius-before-params: doc1,doc2,doc3 +hnsw linear-before-params: doc1,doc2,doc3 +hnsw radius+linear-before-params: doc1,doc2 +hnsw no-radius: doc1,doc2,doc3 +ALL TESTS PASSED From be09c5c9e97e7b867fe8a0e27345f1d70930048a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ha=C5=82as?= Date: Sat, 8 Aug 2026 19:25:35 +0200 Subject: [PATCH 2/2] docs: add CHANGELOG entry for query params ordering fix (#197) --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b380963..886f7db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **`queryVector()` honors radius/linear/refiner set before `set*Params()`** (#197) + - Calling `setRadius()`, `setLinear()`, or `setUsingRefiner()` on a `ZVecVectorQuery` *before* any `set*Params()` method previously dropped those settings silently (the param setters replaced `query_params_` wholesale) — queries ran with wrong settings (e.g. radius 0.0) and no error. + - FFI: `merge_stored_query_settings()` now applies stored radius/linear/refiner onto the params object right after each param setter (`set_hnsw_ef`, `set_hnsw_rabitq_ef`, `set_vamana_ef_search`, `set_ivf_nprobe`, `set_flat_mode`), so setter order no longer matters for HNSW, IVF, Vamana, RaBitQ and Flat. + - Note: a second `set*Params()` call no longer implicitly resets previously set radius/linear/refiner (merge semantics — set `setRadius(0.0)` to restore the default). + - Added regression test `test_query_params_order.phpt`. + +### Fixed + - **Random rotation for INT8/INT4 quantization** (#177) - `ZVecIndexParams::setQuantizerEnableRotate(bool)` (fluent) enables random rotation before INT8/INT4 quantization for HNSW, Flat, IVF, and Vamana indexes — reduces quantization error and improves recall on quantized indexes. - Mirrors upstream zvec v0.6.0 `QuantizerParam(enable_rotate)` (C API: `zvec_index_params_set_quantizer_enable_rotate`).