From 644ad56e4ddb31f26c902d7dd4f01ba856d9a325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ha=C5=82as?= Date: Sat, 8 Aug 2026 18:55:03 +0200 Subject: [PATCH 1/4] fix: correct Vamana/RaBitQ query params in queryVector() (closes #193) ZVecVectorQuery::setVamanaParams() and setHnswRabitqParams() both created an HnswQueryParams object, so queryVector() on Vamana/RaBitQ indexes was always rejected by the engine. Add dedicated FFI setters constructing the correct subtypes (VamanaQueryParams / HnswRabitqQueryParams), wire the PHP setters to them, and add regression tests (RaBitQ guarded for Linux x86_64). --- CHANGELOG.md | 7 +++ docs/helpers/faq.md | 19 +++++++ ffi/zvec_ffi.cc | 12 ++++ ffi/zvec_ffi.h | 2 + ffi/zvec_ffi_php.h | 2 + src/ZVecVectorQuery.php | 4 +- tests/test_hnsw_rabitq_index.php | 62 --------------------- tests/test_vector_query_rabitq_params.phpt | 64 ++++++++++++++++++++++ tests/test_vector_query_vamana_params.phpt | 58 ++++++++++++++++++++ 9 files changed, 166 insertions(+), 64 deletions(-) delete mode 100644 tests/test_hnsw_rabitq_index.php create mode 100644 tests/test_vector_query_rabitq_params.phpt create mode 100644 tests/test_vector_query_vamana_params.phpt diff --git a/CHANGELOG.md b/CHANGELOG.md index e7d7cea..b380963 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **`queryVector()` with Vamana / HNSW RaBitQ query params** (#193) + - `ZVecVectorQuery::setVamanaParams()` and `setHnswRabitqParams()` previously created an `HnswQueryParams` object, so `queryVector()` on Vamana/RaBitQ indexes was always rejected by the engine (`query params type does not match the index type of vector field`). + - FFI: added `zvec_vector_query_set_vamana_ef_search()` (`VamanaQueryParams`) and `zvec_vector_query_set_hnsw_rabitq_ef()` (`HnswRabitqQueryParams`), mirroring `apply_query_params`; the legacy `query()` path was unaffected. + - Added regression tests `test_vector_query_vamana_params.phpt` and `test_vector_query_rabitq_params.phpt` (RaBitQ runs on Linux x86_64 only). + +### Fixed + - **Deprecated index creation warnings** (#169) - The FFI bindings’ `createHnswIndex()`, `createHnswRabitqIndex()`, `createFlatIndex()`, and `createIvfIndex()` now emit `E_USER_DEPRECATED` before delegating to the unified `createIndex()` API. - **Regression gate against zvec v0.6.0** (#175) diff --git a/docs/helpers/faq.md b/docs/helpers/faq.md index fdd1b96..b0ec682 100644 --- a/docs/helpers/faq.md +++ b/docs/helpers/faq.md @@ -58,3 +58,22 @@ FFI call via `self::checkStatus()`. **Solution:** only `close()` if the collection may be reopened; never use an object after `destroy()`. + +--- + +### queryVector() enforces query-param/index-type match + +**Problem:** `queryVector()` rejects a query when the params' index type +(`HnswQueryParams`, `VamanaQueryParams`, ...) differs from the field's index +type (validation in `zvec/src/db/index/common/query.cc`). +`setVamanaParams()`/`setHnswRabitqParams()` used to call +`zvec_vector_query_set_hnsw_ef` (HnswQueryParams), so queryVector() on +Vamana/RaBitQ indexes always failed with INVALID_ARGUMENT. + +**Solution:** dedicated FFI setters create the matching params class: +`zvec_vector_query_set_vamana_ef_search` → `VamanaQueryParams`, +`zvec_vector_query_set_hnsw_rabitq_ef` → `HnswRabitqQueryParams`. +The legacy `query()` path was never affected (it rebuilds params per +`queryParamType`). + +**Reference:** issue #193. diff --git a/ffi/zvec_ffi.cc b/ffi/zvec_ffi.cc index aa7876f..ce480e3 100644 --- a/ffi/zvec_ffi.cc +++ b/ffi/zvec_ffi.cc @@ -2067,6 +2067,18 @@ void zvec_vector_query_set_hnsw_ef(zvec_vector_query_t q, int ef) { holder->query.target_.query_params_ = std::make_shared(ef); } +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); +} + +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); +} + void zvec_vector_query_set_ivf_nprobe(zvec_vector_query_t q, int nprobe) { if (!q) return; auto* holder = static_cast(q); diff --git a/ffi/zvec_ffi.h b/ffi/zvec_ffi.h index 3224095..2f65fba 100644 --- a/ffi/zvec_ffi.h +++ b/ffi/zvec_ffi.h @@ -301,6 +301,8 @@ void zvec_vector_query_set_include_vector(zvec_vector_query_t q, int include); void zvec_vector_query_set_filter(zvec_vector_query_t q, const char* filter); void zvec_vector_query_set_output_fields(zvec_vector_query_t q, const char** fields, int count); void zvec_vector_query_set_hnsw_ef(zvec_vector_query_t q, int ef); +void zvec_vector_query_set_hnsw_rabitq_ef(zvec_vector_query_t q, int ef); +void zvec_vector_query_set_vamana_ef_search(zvec_vector_query_t q, int ef_search); void zvec_vector_query_set_ivf_nprobe(zvec_vector_query_t q, int nprobe); void zvec_vector_query_set_flat_mode(zvec_vector_query_t q); void zvec_vector_query_set_radius(zvec_vector_query_t q, float radius); diff --git a/ffi/zvec_ffi_php.h b/ffi/zvec_ffi_php.h index e733456..230809f 100644 --- a/ffi/zvec_ffi_php.h +++ b/ffi/zvec_ffi_php.h @@ -340,6 +340,8 @@ void zvec_vector_query_set_include_vector(zvec_vector_query_t q, int include); void zvec_vector_query_set_filter(zvec_vector_query_t q, const char* filter); void zvec_vector_query_set_output_fields(zvec_vector_query_t q, const char** fields, int count); void zvec_vector_query_set_hnsw_ef(zvec_vector_query_t q, int ef); +void zvec_vector_query_set_hnsw_rabitq_ef(zvec_vector_query_t q, int ef); +void zvec_vector_query_set_vamana_ef_search(zvec_vector_query_t q, int ef_search); void zvec_vector_query_set_ivf_nprobe(zvec_vector_query_t q, int nprobe); void zvec_vector_query_set_flat_mode(zvec_vector_query_t q); void zvec_vector_query_set_radius(zvec_vector_query_t q, float radius); diff --git a/src/ZVecVectorQuery.php b/src/ZVecVectorQuery.php index 03fe35d..7b4fdd8 100644 --- a/src/ZVecVectorQuery.php +++ b/src/ZVecVectorQuery.php @@ -137,7 +137,7 @@ public function setHnswRabitqParams(int $ef): self { $this->queryParamType = ZVec::QUERY_PARAM_HNSW_RABITQ; $this->hnswEf = $ef; - self::ffi()->zvec_vector_query_set_hnsw_ef($this->handle, $ef); + self::ffi()->zvec_vector_query_set_hnsw_rabitq_ef($this->handle, $ef); return $this; } @@ -163,7 +163,7 @@ public function setVamanaParams(int $efSearch): self { $this->queryParamType = ZVec::QUERY_PARAM_VAMANA; $this->hnswEf = $efSearch; - self::ffi()->zvec_vector_query_set_hnsw_ef($this->handle, $efSearch); + self::ffi()->zvec_vector_query_set_vamana_ef_search($this->handle, $efSearch); return $this; } diff --git a/tests/test_hnsw_rabitq_index.php b/tests/test_hnsw_rabitq_index.php deleted file mode 100644 index efcccad..0000000 --- a/tests/test_hnsw_rabitq_index.php +++ /dev/null @@ -1,62 +0,0 @@ -addInt64('id'); - $schema->addVectorFp32('vec', dimension: 64, metricType: ZVecSchema::METRIC_IP); - $coll = ZVec::create($path, $schema); - - $coll->createIndex('vec', ZVecIndexParams::forHnswRabitq( - metricType: ZVecSchema::METRIC_IP, - m: 50, - efConstruction: 500, - )); - - // Insert some docs with distinct vectors - $data = []; - for ($i = 0; $i < 10; $i++) { - $vec = []; - for ($j = 0; $j < 64; $j++) { - $vec[] = $j === $i ? 1.0 : 0.0; - } - $data[] = $vec; - $doc = new ZVecDoc('doc' . $i); - $doc->setInt64('id', $i); - $doc->setVectorFp32('vec', $vec); - $coll->insert($doc); - } - $coll->optimize(); - - $query = new ZVecVectorQuery('vec', $data[0]); - $query->setHnswRabitqParams(ef: 100); - $results = $coll->query($query); - - echo "Results: " . count($results) . "\n"; - echo "Top pk: " . $results[0]->getPk() . "\n"; - - // Query with legacy method - $results2 = $coll->query('vec', $data[0], topk: 3, queryParamType: ZVec::QUERY_PARAM_HNSW_RABITQ, hnswEf: 100); - echo "Legacy results: " . count($results2) . "\n"; - - $coll->close(); - - // Test via deprecated convenience method - $coll2 = ZVec::create($path . '_2', $schema); - $coll2->createHnswRabitqIndex('vec'); - $doc2 = new ZVecDoc('d1'); - $doc2->setInt64('id', 1); - $doc2->setVectorFp32('vec', array_fill(0, 64, 0.1)); - $coll2->insert($doc2); - $coll2->optimize(); - echo "Deprecated method works\n"; - $coll2->destroy(); - - echo "OK\n"; -} finally { - exec("rm -rf " . escapeshellarg($path)); - exec("rm -rf " . escapeshellarg($path . '_2')); -} -?> diff --git a/tests/test_vector_query_rabitq_params.phpt b/tests/test_vector_query_rabitq_params.phpt new file mode 100644 index 0000000..d629dc5 --- /dev/null +++ b/tests/test_vector_query_rabitq_params.phpt @@ -0,0 +1,64 @@ +--TEST-- +Vector query params: queryVector() with setHnswRabitqParams() on an HNSW RaBitQ index (issue #193) +--SKIPIF-- + +--FILE-- +addInt64('id'); + $schema->addVectorFp32('vec', dimension: 64, metricType: ZVecSchema::METRIC_IP); + $coll = ZVec::create($path, $schema); + + $coll->createIndex('vec', ZVecIndexParams::forHnswRabitq( + metricType: ZVecSchema::METRIC_IP, + m: 50, + efConstruction: 500, + )); + + $data = []; + for ($i = 0; $i < 10; $i++) { + $vec = []; + for ($j = 0; $j < 64; $j++) { + $vec[] = $j === $i ? 1.0 : 0.0; + } + $data[] = $vec; + $doc = new ZVecDoc('doc' . $i); + $doc->setInt64('id', $i); + $doc->setVectorFp32('vec', $vec); + $coll->insert($doc); + } + $coll->optimize(); + + $query = new ZVecVectorQuery('vec', $data[0]); + $query->setHnswRabitqParams(ef: 100); + $results = $coll->queryVector($query); + assert(count($results) >= 1, 'Expected at least 1 result'); + echo "queryVector with HNSW RaBitQ params returned " . count($results) . " results, top: " . $results[0]->getPk() . "\n"; + + try { + $bad = new ZVecVectorQuery('vec', $data[0]); + $bad->setHnswParams(ef: 200); + $coll->queryVector($bad); + echo "UNEXPECTED: HNSW params on RaBitQ index should have thrown\n"; + } catch (ZVecException $e) { + echo "HNSW params on RaBitQ index correctly rejected: " . $e->getErrorCodeString() . "\n"; + } + + $coll->close(); + echo "PASS: queryVector with setHnswRabitqParams works\n"; +} finally { + exec("rm -rf " . escapeshellarg($path)); +} +?> +--EXPECTF-- +queryVector with HNSW RaBitQ params returned %d results, top: doc0 +HNSW params on RaBitQ index correctly rejected: INVALID_ARGUMENT +PASS: queryVector with setHnswRabitqParams works diff --git a/tests/test_vector_query_vamana_params.phpt b/tests/test_vector_query_vamana_params.phpt new file mode 100644 index 0000000..71f6a6b --- /dev/null +++ b/tests/test_vector_query_vamana_params.phpt @@ -0,0 +1,58 @@ +--TEST-- +Vector query params: queryVector() with setVamanaParams() on a Vamana index (issue #193) +--SKIPIF-- + + +--FILE-- +addInt64('id', nullable: false) + ->addVectorFp32('v', dimension: 4, metricType: ZVecSchema::METRIC_IP); + + $c = ZVec::create($path, $schema); + + $c->createIndex('v', ZVecIndexParams::forVamana( + metricType: ZVecSchema::METRIC_IP, + maxDegree: 32, + searchListSize: 50, + alpha: 1.0, + saturateGraph: false, + )); + + $c->insert( + (new ZVecDoc('doc1'))->setInt64('id', 1)->setVectorFp32('v', [1.0, 0.0, 0.0, 0.0]), + (new ZVecDoc('doc2'))->setInt64('id', 2)->setVectorFp32('v', [0.0, 1.0, 0.0, 0.0]), + (new ZVecDoc('doc3'))->setInt64('id', 3)->setVectorFp32('v', [0.0, 0.0, 1.0, 0.0]), + ); + $c->optimize(); + + $vq = new ZVecVectorQuery('v', [1.0, 0.1, 0.0, 0.0]); + $vq->setVamanaParams(efSearch: 50); + $results = $c->queryVector($vq); + assert(count($results) === 3, 'Expected 3 results'); + echo "queryVector with Vamana params returned " . count($results) . " results, top: " . $results[0]->getPk() . "\n"; + + try { + $bad = new ZVecVectorQuery('v', [1.0, 0.1, 0.0, 0.0]); + $bad->setHnswParams(ef: 200); + $c->queryVector($bad); + echo "UNEXPECTED: HNSW params on Vamana index should have thrown\n"; + } catch (ZVecException $e) { + echo "HNSW params on Vamana index correctly rejected: " . $e->getErrorCodeString() . "\n"; + } + + $c->close(); + echo "PASS: queryVector with setVamanaParams works\n"; +} finally { + exec("rm -rf " . escapeshellarg($path)); +} +?> +--EXPECT-- +queryVector with Vamana params returned 3 results, top: doc1 +HNSW params on Vamana index correctly rejected: INVALID_ARGUMENT +PASS: queryVector with setVamanaParams works From 6456d123aa98542cfba6885e71a4dbb1b51e0eab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ha=C5=82as?= Date: Sat, 8 Aug 2026 18:55:19 +0200 Subject: [PATCH 2/4] chore: restore legacy test file deleted by run-tests.php (#187) --- tests/test_hnsw_rabitq_index.php | 62 ++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 tests/test_hnsw_rabitq_index.php diff --git a/tests/test_hnsw_rabitq_index.php b/tests/test_hnsw_rabitq_index.php new file mode 100644 index 0000000..efcccad --- /dev/null +++ b/tests/test_hnsw_rabitq_index.php @@ -0,0 +1,62 @@ +addInt64('id'); + $schema->addVectorFp32('vec', dimension: 64, metricType: ZVecSchema::METRIC_IP); + $coll = ZVec::create($path, $schema); + + $coll->createIndex('vec', ZVecIndexParams::forHnswRabitq( + metricType: ZVecSchema::METRIC_IP, + m: 50, + efConstruction: 500, + )); + + // Insert some docs with distinct vectors + $data = []; + for ($i = 0; $i < 10; $i++) { + $vec = []; + for ($j = 0; $j < 64; $j++) { + $vec[] = $j === $i ? 1.0 : 0.0; + } + $data[] = $vec; + $doc = new ZVecDoc('doc' . $i); + $doc->setInt64('id', $i); + $doc->setVectorFp32('vec', $vec); + $coll->insert($doc); + } + $coll->optimize(); + + $query = new ZVecVectorQuery('vec', $data[0]); + $query->setHnswRabitqParams(ef: 100); + $results = $coll->query($query); + + echo "Results: " . count($results) . "\n"; + echo "Top pk: " . $results[0]->getPk() . "\n"; + + // Query with legacy method + $results2 = $coll->query('vec', $data[0], topk: 3, queryParamType: ZVec::QUERY_PARAM_HNSW_RABITQ, hnswEf: 100); + echo "Legacy results: " . count($results2) . "\n"; + + $coll->close(); + + // Test via deprecated convenience method + $coll2 = ZVec::create($path . '_2', $schema); + $coll2->createHnswRabitqIndex('vec'); + $doc2 = new ZVecDoc('d1'); + $doc2->setInt64('id', 1); + $doc2->setVectorFp32('vec', array_fill(0, 64, 0.1)); + $coll2->insert($doc2); + $coll2->optimize(); + echo "Deprecated method works\n"; + $coll2->destroy(); + + echo "OK\n"; +} finally { + exec("rm -rf " . escapeshellarg($path)); + exec("rm -rf " . escapeshellarg($path . '_2')); +} +?> From 6406cbf6dc7e1bcab1e77be933fe2c173f8fe44f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ha=C5=82as?= Date: Sat, 8 Aug 2026 18:59:13 +0200 Subject: [PATCH 3/4] fix: tolerate engine stdout logs in RaBitQ query params test (#193) --- tests/test_vector_query_rabitq_params.phpt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_vector_query_rabitq_params.phpt b/tests/test_vector_query_rabitq_params.phpt index d629dc5..65dd900 100644 --- a/tests/test_vector_query_rabitq_params.phpt +++ b/tests/test_vector_query_rabitq_params.phpt @@ -59,6 +59,7 @@ try { } ?> --EXPECTF-- -queryVector with HNSW RaBitQ params returned %d results, top: doc0 -HNSW params on RaBitQ index correctly rejected: INVALID_ARGUMENT -PASS: queryVector with setHnswRabitqParams works +%AqueryVector with HNSW RaBitQ params returned %d results, top: doc0 +%AHNSW params on RaBitQ index correctly rejected: INVALID_ARGUMENT +%APASS: queryVector with setHnswRabitqParams works +%A From 3ee2731f822ec506939d54c917ddda592240dd2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ha=C5=82as?= Date: Sat, 8 Aug 2026 19:03:32 +0200 Subject: [PATCH 4/4] fix: drop trailing %A in RaBitQ test EXPECTF (trimmed output has no trailing newline) (#193) --- tests/test_vector_query_rabitq_params.phpt | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_vector_query_rabitq_params.phpt b/tests/test_vector_query_rabitq_params.phpt index 65dd900..258678f 100644 --- a/tests/test_vector_query_rabitq_params.phpt +++ b/tests/test_vector_query_rabitq_params.phpt @@ -62,4 +62,3 @@ try { %AqueryVector with HNSW RaBitQ params returned %d results, top: doc0 %AHNSW params on RaBitQ index correctly rejected: INVALID_ARGUMENT %APASS: queryVector with setHnswRabitqParams works -%A