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_vector_query_rabitq_params.phpt b/tests/test_vector_query_rabitq_params.phpt new file mode 100644 index 0000000..258678f --- /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-- +%AqueryVector with HNSW RaBitQ params returned %d results, top: doc0 +%AHNSW params on RaBitQ index correctly rejected: INVALID_ARGUMENT +%APASS: 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