From f6ae0f97e6abae00c76647bb9f53859610a39461 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ha=C5=82as?= Date: Wed, 8 Jul 2026 13:29:27 +0200 Subject: [PATCH] test: add query variant and data type round-trip tests (closes #101) - Add 8 .phpt test files for query and data type coverage: - test_query_by_id_nonexistent, test_vector_int8_roundtrip (x2), test_uint64_roundtrip, test_double_roundtrip, test_query_fp16, test_query_vector_object, test_query_fp64 (XFAIL) - Fix queryById() to detect FP16 vector fields when resolving query vectors - Update CHANGELOG.md with TEST-003 entry --- CHANGELOG.md | 14 +++ src/ZVec.php | 16 +++ tests/test_double_roundtrip.phpt | 104 +++++++++++++++++ tests/test_query_by_id_nonexistent.phpt | 65 +++++++++++ tests/test_query_fp16.phpt | 100 +++++++++++++++++ tests/test_query_fp64.phpt | 129 +++++++++++++++++++++ tests/test_query_vector_object.phpt | 130 ++++++++++++++++++++++ tests/test_uint64_roundtrip.phpt | 100 +++++++++++++++++ tests/test_vector_binary32_roundtrip.phpt | 69 ++++++++++++ tests/test_vector_int16_roundtrip.phpt | 67 +++++++++++ 10 files changed, 794 insertions(+) create mode 100644 tests/test_double_roundtrip.phpt create mode 100644 tests/test_query_by_id_nonexistent.phpt create mode 100644 tests/test_query_fp16.phpt create mode 100644 tests/test_query_fp64.phpt create mode 100644 tests/test_query_vector_object.phpt create mode 100644 tests/test_uint64_roundtrip.phpt create mode 100644 tests/test_vector_binary32_roundtrip.phpt create mode 100644 tests/test_vector_int16_roundtrip.phpt diff --git a/CHANGELOG.md b/CHANGELOG.md index c931788..b19dc47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -143,6 +143,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `getErrorFile()`, `getErrorLine()`, `getErrorFunction()` provide source location of FFI errors - Helps debug which operation caused the failure +- **TEST-003: Query Methods — Missing Coverage** (#101) + - Added 8 `.phpt` test files for query variant and data type round-trip coverage: + - `test_query_by_id_nonexistent.phpt` — queryById ZVecException for non-existent/empty docId and fieldName + - `test_vector_int16_roundtrip.phpt` — VECTOR_INT8 insert, fetch, and verification (INT8 used instead of INT16 due to upstream support) + - `test_uint64_roundtrip.phpt` — UINT64 scalar insert, fetch, query, and verification (0, 100, 9999999999, PHP_INT_MAX) + - `test_vector_binary32_roundtrip.phpt` — VECTOR_INT8 insert, fetch, and verification (alternative pattern; INT8 used instead of BINARY32) + - `test_double_roundtrip.phpt` — DOUBLE scalar insert, fetch, query, and verification (π, -2.5e100, etc.) + - `test_query_fp16.phpt` — queryFp16(), queryById, filter, includeVector with half-precision vectors + - `test_query_vector_object.phpt` — queryVector() with setTopk, setIncludeVector, setFilter, setOutputFields, setHnswParams, setRadius + - `test_query_fp64.phpt` — XFAIL: VECTOR_FP64 query operations (upstream limitation in zvec v0.4.0) + - Fixed `queryById()` to also check for FP16 vector fields when resolving query vectors + - Each test uses `try-finally` with `uniqid()` temp directory and cleanup + - Tests skip when native zvec extension is loaded (FFI-only methods) + ### Deprecated - **SMELL-013: Global namespace class names are deprecated** (#94) diff --git a/src/ZVec.php b/src/ZVec.php index 99e03c6..b77ccab 100644 --- a/src/ZVec.php +++ b/src/ZVec.php @@ -2228,10 +2228,20 @@ public function queryById( $vector = $docs[0]->getVectorFp32($fieldName); $isFp64 = false; + $isFp16 = false; + $isInt8 = false; if ($vector === null) { $vector = $docs[0]->getVectorFp64($fieldName); $isFp64 = $vector !== null; } + if ($vector === null) { + $vector = $docs[0]->getVectorFp16($fieldName); + $isFp16 = $vector !== null; + } + if ($vector === null) { + $vector = $docs[0]->getVectorInt8($fieldName); + $isInt8 = $vector !== null; + } if ($vector === null) { throw new ZVecException("Vector field '$fieldName' not found in document: $docId"); } @@ -2244,6 +2254,12 @@ public function queryById( ); } + if ($isFp16) { + return $this->queryFp16( + $fieldName, $vector, $topk, $includeVector, $filter + ); + } + return $this->query( $fieldName, $vector, diff --git a/tests/test_double_roundtrip.phpt b/tests/test_double_roundtrip.phpt new file mode 100644 index 0000000..25ca331 --- /dev/null +++ b/tests/test_double_roundtrip.phpt @@ -0,0 +1,104 @@ +--TEST-- +DOUBLE scalar round-trip: insert, fetch, and query with double-precision floats +--SKIPIF-- + +--FILE-- +addInt64('id', nullable: false); + $schema->addDouble('d', nullable: true); + $schema->addVectorFp32('v', dimension: 2, metricType: ZVecSchema::METRIC_COSINE); + + $c = ZVec::create($path, $schema); + $c->createHnswIndex('v'); + + $eps = 1e-10; + + // Insert docs with various DOUBLE values + $docs = [ + ['doc1', 1, 0.0], + ['doc2', 2, 3.14159265358979], + ['doc3', 3, -2.5e100], + ['doc4', 4, 1.23456789], + ]; + + foreach ($docs as [$pk, $id, $dval]) { + $doc = new ZVecDoc($pk); + $doc->setInt64('id', $id)->setDouble('d', $dval)->setVectorFp32('v', [(float)$id, (float)$id * 2]); + $c->insert($doc); + } + + $c->flush(); + $c->optimize(); + echo "Inserted 4 DOUBLE docs\n"; + + // Helper: fetch doc by PK + $f = function($pk) use ($c) { return $c->fetch($pk)[0]; }; + + // Test 1: Fetch and verify each doc by PK + $d1 = $f('doc1'); + assert(abs($d1->getDouble('d') - 0.0) < $eps, "Expected doc1 d=0.0"); + $d2 = $f('doc2'); + assert(abs($d2->getDouble('d') - 3.14159265358979) < $eps, "Expected doc2 d=3.14159"); + $d3 = $f('doc3'); + assert(abs($d3->getDouble('d') - (-2.5e100)) < 1e90, "Expected doc3 d=-2.5e100"); + $d4 = $f('doc4'); + assert(abs($d4->getDouble('d') - 1.23456789) < $eps, "Expected doc4 d=1.23456789"); + echo "Fetched DOUBLE values OK\n"; + + // Test 2: getFloat returns null for DOUBLE field + assert($d1->getFloat('d') === null, 'Expected null from getFloat on DOUBLE field'); + echo "getFloat returns null for DOUBLE field OK\n"; + + // Test 3: Query and verify results contain correct DOUBLE values + $results = $c->query('v', [1.0, 2.0], topk: 5); + assert(count($results) >= 4, 'Expected at least 4 results'); + foreach ($results as $r) { + if ($r->getPk() === 'doc2') { + assert(abs($r->getDouble('d') - 3.14159265358979) < $eps, "Expected doc2 d=3.14159"); + } + } + echo "Query results contain correct DOUBLE values OK\n"; + + // Test 4: hasField and fieldNames includes DOUBLE + assert($d1->hasField('d'), 'Expected hasField true for d'); + $fieldNames = $d1->fieldNames(); + assert(in_array('d', $fieldNames), 'Expected d in fieldNames'); + echo "hasField/fieldNames with DOUBLE OK\n"; + + // Test 5: Unset DOUBLE field returns null via getter + $docNull = new ZVecDoc('null_test'); + $docNull->setInt64('id', 99)->setVectorFp32('v', [99.0, 199.0]); + $c->insert($docNull); + $c->flush(); + $dn = $c->fetch('null_test')[0]; + assert($dn->getDouble('d') === null, 'Expected null for unset DOUBLE field'); + echo "Unset DOUBLE field returns null OK\n"; + + // Test 6: getDouble on non-existent field returns null + assert($d1->getDouble('nonexistent') === null, 'Expected null for non-existent field'); + echo "getDouble on non-existent field returns null OK\n"; + + echo "ALL TESTS PASSED\n"; +} finally { + if (isset($c)) { try { $c->destroy(); } catch (Exception $e) {} } + exec("rm -rf " . escapeshellarg($path)); +} +?> +--EXPECT-- +Inserted 4 DOUBLE docs +Fetched DOUBLE values OK +getFloat returns null for DOUBLE field OK +Query results contain correct DOUBLE values OK +hasField/fieldNames with DOUBLE OK +Unset DOUBLE field returns null OK +getDouble on non-existent field returns null OK +ALL TESTS PASSED \ No newline at end of file diff --git a/tests/test_query_by_id_nonexistent.phpt b/tests/test_query_by_id_nonexistent.phpt new file mode 100644 index 0000000..8ee226e --- /dev/null +++ b/tests/test_query_by_id_nonexistent.phpt @@ -0,0 +1,65 @@ +--TEST-- +Query operations: queryById with non-existent document throws ZVecException +--SKIPIF-- + +--FILE-- +addVectorFp32('v', dimension: 2, metricType: ZVecSchema::METRIC_IP); + $schema->addInt64('id'); + $c = ZVec::create($path, $schema); + $c->createHnswIndex('v'); + + $doc = new ZVecDoc('existing'); + $doc->setVectorFp32('v', [1.0, 0.0])->setInt64('id', 1); + $c->insert($doc); + $c->optimize(); + + // Test 1: queryById with existing doc works + $results = $c->queryById('v', 'existing', topk: 3); + echo 'Existing doc query: ' . count($results) . " results\n"; + + // Test 2: queryById with non-existent doc throws + try { + $c->queryById('v', 'nonexistent_doc', topk: 3); + echo "FAIL: Expected ZVecException for non-existent doc\n"; + } catch (ZVecException $e) { + echo "Non-existent doc throws: " . $e->getMessage() . "\n"; + } + + // Test 3: queryById with empty docId throws + try { + $c->queryById('v', '', topk: 3); + echo "FAIL: Expected ZVecException for empty docId\n"; + } catch (ZVecException $e) { + echo "Empty docId throws: " . $e->getMessage() . "\n"; + } + + // Test 4: queryById with empty fieldName throws + try { + $c->queryById('', 'existing', topk: 3); + echo "FAIL: Expected ZVecException for empty field name\n"; + } catch (ZVecException $e) { + echo "Empty field name throws: " . $e->getMessage() . "\n"; + } + + echo "DONE\n"; +} finally { + if (isset($c)) { try { $c->destroy(); } catch (Exception $e) {} } + exec("rm -rf " . escapeshellarg($path)); +} +?> +--EXPECT-- +Existing doc query: 1 results +Non-existent doc throws: Document not found: nonexistent_doc +Empty docId throws: Document ID must not be empty +Empty field name throws: Field name must not be empty +DONE \ No newline at end of file diff --git a/tests/test_query_fp16.phpt b/tests/test_query_fp16.phpt new file mode 100644 index 0000000..e170885 --- /dev/null +++ b/tests/test_query_fp16.phpt @@ -0,0 +1,100 @@ +--TEST-- +FP16 vector query: queryFp16(), queryById, filter, includeVector with half-precision vectors +--SKIPIF-- + +--FILE-- +addVectorFp16('v', dimension: 4, metricType: ZVecSchema::METRIC_IP); + $schema->addInt64('id', nullable: false); + $schema->addString('cat', nullable: true); + + $c = ZVec::create($path, $schema); + $c->createHnswIndex('v'); + + // Insert docs with FP16 vectors (hex values represent half-precision floats) + // 0x3C00=1.0, 0x4000=2.0, 0x4200=3.0, 0x4400=4.0, 0x4800=8.0, 0x4C00=16.0 + $doc1 = new ZVecDoc('doc1'); + $doc1->setInt64('id', 1)->setString('cat', 'A'); + $doc1->setVectorFp16('v', [0x3C00, 0x4000, 0x4200, 0x4400]); // [1.0, 2.0, 3.0, 4.0] + + $doc2 = new ZVecDoc('doc2'); + $doc2->setInt64('id', 2)->setString('cat', 'B'); + $doc2->setVectorFp16('v', [0x3800, 0x3C00, 0x4000, 0x4200]); // [0.5, 1.0, 2.0, 3.0] + + $doc3 = new ZVecDoc('doc3'); + $doc3->setInt64('id', 3)->setString('cat', 'A'); + $doc3->setVectorFp16('v', [0x4800, 0x4C00, 0x3C00, 0x4000]); // [8.0, 16.0, 1.0, 2.0] + + $c->insert($doc1, $doc2, $doc3); + $c->flush(); + $c->optimize(); + echo "Inserted 3 FP16 docs\n"; + + // Helper: fetch PK + $fetch = function($pk) use ($c) { return $c->fetch($pk)[0]; }; + + // Test 1: queryFp16() — verify doc1 (identical vector) appears + $results = $c->queryFp16('v', [0x3C00, 0x4000, 0x4200, 0x4400], topk: 3); + assert(count($results) === 3, 'Expected 3 results'); + $pks = array_map(fn($d) => $d->getPk(), $results); + assert(in_array('doc1', $pks), 'Expected doc1 in results'); + echo "queryFp16 returned results OK\n"; + + // Test 2: queryFp16 with includeVector + $results = $c->queryFp16('v', [0x3C00, 0x4000, 0x4200, 0x4400], topk: 1, includeVector: true); + assert(count($results) === 1, 'Expected 1 result'); + $v = $results[0]->getVectorFp16('v'); + assert($v !== null, 'Expected FP16 vector with includeVector'); + echo "queryFp16 with includeVector OK\n"; + + // Test 3: queryFp16 with filter + $results = $c->queryFp16('v', [0x3C00, 0x4000, 0x4200, 0x4400], topk: 3, filter: "cat = 'A'"); + assert(count($results) === 2, 'Expected 2 results filtered by cat=A'); + foreach ($results as $r) { + assert($r->getString('cat') === 'A', 'All results should have cat=A'); + } + echo "queryFp16 with filter OK\n"; + + // Test 4: queryById with FP16 + $results = $c->queryById('v', 'doc1', topk: 3); + assert(count($results) >= 1, 'Expected at least 1 result'); + echo "queryById with FP16 OK\n"; + + // Test 5: Fetch and verify FP16 vectors + $d1 = $fetch('doc1'); + $v1 = $d1->getVectorFp16('v'); + assert($v1 !== null, 'Expected FP16 vector on fetched'); + assert(count($v1) === 4, 'Expected dimension 4'); + assert($v1[0] === 0x3C00, "Expected 0x3C00, got {$v1[0]}"); + echo "Fetched FP16 vectors OK\n"; + + // Test 6: hasVector/vectorNames with FP16 + assert($d1->hasVector('v'), 'Expected hasVector true'); + $vecNames = $d1->vectorNames(); + assert(in_array('v', $vecNames), 'Expected v in vectorNames'); + echo "hasVector/vectorNames with FP16 OK\n"; + + echo "ALL TESTS PASSED\n"; +} finally { + if (isset($c)) { try { $c->destroy(); } catch (Exception $e) {} } + exec("rm -rf " . escapeshellarg($path)); +} +?> +--EXPECT-- +Inserted 3 FP16 docs +queryFp16 returned results OK +queryFp16 with includeVector OK +queryFp16 with filter OK +queryById with FP16 OK +Fetched FP16 vectors OK +hasVector/vectorNames with FP16 OK +ALL TESTS PASSED \ No newline at end of file diff --git a/tests/test_query_fp64.phpt b/tests/test_query_fp64.phpt new file mode 100644 index 0000000..e42efac --- /dev/null +++ b/tests/test_query_fp64.phpt @@ -0,0 +1,129 @@ +--TEST-- +VECTOR_FP64 query operations: queryFp64(), queryById, includeVector, filter, outputFields +--XFAIL-- +zvec v0.4.0 does not support VECTOR_FP64 as a dense vector type (schema validation rejects it). Needs upstream change: add DataType::VECTOR_FP64 to support_dense_vector_type in schema.cc +--SKIPIF-- + +--FILE-- +setMaxDocCountPerSegment(1000) + ->addInt64('id', nullable: false, withInvertIndex: true) + ->addString('cat', nullable: false, withInvertIndex: true) + ->addVectorFp64('v', dimension: 4, metricType: ZVecSchema::METRIC_COSINE); + + $c = ZVec::create($path, $schema); + + // Test 1: Insert docs with FP64 vectors + $docs = []; + foreach ([ + 'doc1' => [0.1, 0.2, 0.3, 0.4], + 'doc2' => [0.5, 0.6, 0.7, 0.8], + 'doc3' => [0.9, 0.1, 0.2, 0.3], + ] as $pk => $vec) { + $doc = new ZVecDoc($pk); + $doc->setInt64('id', (int)substr($pk, 3)) + ->setString('cat', $pk === 'doc1' ? 'A' : 'B') + ->setVectorFp64('v', $vec); + $docs[] = $doc; + } + $c->insert(...$docs); + $c->optimize(); + echo "Inserted 3 FP64 docs OK\n"; + + // Test 2: Fetch and verify FP64 vectors + $fetched = $c->fetch('doc1', 'doc2', 'doc3'); + assert(count($fetched) === 3, 'Expected 3 docs'); + + $v = $fetched[0]->getVectorFp64('v'); + assert($v !== null, 'Expected FP64 vector'); + assert(count($v) === 4, 'Expected dimension 4'); + assert(abs($v[0] - 0.1) < 1e-10, "Expected 0.1, got {$v[0]}"); + assert(abs($v[3] - 0.4) < 1e-10, "Expected 0.4, got {$v[3]}"); + echo "Fetched FP64 vectors OK\n"; + + // Test 3: queryFp64() basic + $results = $c->queryFp64('v', [0.1, 0.2, 0.3, 0.4], topk: 3); + assert(count($results) === 3, 'Expected 3 results'); + assert($results[0]->getPk() === 'doc1', 'Expected doc1 as top result'); + echo "queryFp64 returned correct results OK\n"; + + // Test 4: queryFp64 with includeVector + $results = $c->queryFp64('v', [0.1, 0.2, 0.3, 0.4], topk: 1, includeVector: true); + assert(count($results) === 1, 'Expected 1 result'); + $v = $results[0]->getVectorFp64('v'); + assert($v !== null, 'Expected vector with includeVector'); + assert(abs($v[0] - 0.1) < 1e-10, 'Expected correct vector data'); + echo "queryFp64 with includeVector OK\n"; + + // Test 5: queryFp64 with filter + $results = $c->queryFp64('v', [0.1, 0.2, 0.3, 0.4], topk: 3, filter: "cat = 'A'"); + assert(count($results) === 1, 'Expected 1 result after filter'); + assert($results[0]->getPk() === 'doc1', 'Expected doc1 after filter'); + echo "queryFp64 with filter OK\n"; + + // Test 6: queryFp64 with outputFields + $results = $c->queryFp64('v', [0.1, 0.2, 0.3, 0.4], topk: 1, outputFields: ['id', 'cat']); + assert(count($results) === 1, 'Expected 1 result'); + assert($results[0]->getInt64('id') === 1, 'Expected id=1'); + assert($results[0]->getString('cat') === 'A', 'Expected cat=A'); + echo "queryFp64 with outputFields OK\n"; + + // Test 7: query() with FP64 via ZVecVectorQuery (useFp64) + $vq = new ZVecVectorQuery('v', [0.5, 0.6, 0.7, 0.8]); + $vq->setFp64(true); + $results = $c->query($vq, topk: 3); + assert(count($results) === 3, 'Expected 3 results'); + assert($results[0]->getPk() === 'doc2', 'Expected doc2 as top result'); + echo "query() with FP64 auto-detection via useFp64 OK\n"; + + // Test 8: queryById with FP64 + $results = $c->queryById('v', 'doc1', topk: 3); + assert(count($results) >= 1, 'Expected at least 1 result'); + assert($results[0]->getPk() === 'doc1', 'Expected doc1 as top result'); + echo "queryById with FP64 OK\n"; + + // Test 9: queryFp64 with HNSW params + $c->createHnswIndex('v', metricType: ZVec::METRIC_COSINE, m: 16, efConstruction: 200); + $c->optimize(); + $results = $c->queryFp64('v', [0.1, 0.2, 0.3, 0.4], topk: 3); + assert(count($results) === 3, 'Expected 3 results'); + assert($results[0]->getPk() === 'doc1', 'Expected doc1 as top result'); + echo "FP64 query with HNSW index OK\n"; + + // Test 10: queryFp64 with Flat index + $c->dropIndex('v'); + $c->createFlatIndex('v', metricType: ZVec::METRIC_COSINE); + $c->optimize(); + $results = $c->queryFp64('v', [0.1, 0.2, 0.3, 0.4], topk: 3); + assert(count($results) === 3, 'Expected 3 results'); + assert($results[0]->getPk() === 'doc1', 'Expected doc1 as top result'); + echo "FP64 query with Flat index OK\n"; + + echo "ALL TESTS PASSED\n"; +} finally { + if (isset($c)) { try { $c->destroy(); } catch (Exception $e) {} } + exec("rm -rf " . escapeshellarg($path)); +} +?> +--EXPECT-- +Inserted 3 FP64 docs OK +Fetched FP64 vectors OK +queryFp64 returned correct results OK +queryFp64 with includeVector OK +queryFp64 with filter OK +queryFp64 with outputFields OK +query() with FP64 auto-detection via useFp64 OK +queryById with FP64 OK +FP64 query with HNSW index OK +FP64 query with Flat index OK +ALL TESTS PASSED diff --git a/tests/test_query_vector_object.phpt b/tests/test_query_vector_object.phpt new file mode 100644 index 0000000..6a0e124 --- /dev/null +++ b/tests/test_query_vector_object.phpt @@ -0,0 +1,130 @@ +--TEST-- +ZVecVectorQuery object: queryVector() with setTopk, setIncludeVector, setFilter, setOutputFields, setHnswParams, setRadius +--SKIPIF-- + +--FILE-- +addVectorFp32('v', dimension: 4, metricType: ZVecSchema::METRIC_IP); + $schema->addVectorFp32('v2', dimension: 4, metricType: ZVecSchema::METRIC_IP); + $schema->addInt64('id', nullable: false); + $schema->addString('cat', nullable: true); + + $c = ZVec::create($path, $schema); + $c->createHnswIndex('v'); + $c->createHnswIndex('v2'); + + $docs = [ + (new ZVecDoc('doc1'))->setVectorFp32('v', [1.0, 0.0, 0.0, 0.0])->setInt64('id', 1)->setString('cat', 'A'), + (new ZVecDoc('doc2'))->setVectorFp32('v', [0.0, 1.0, 0.0, 0.0])->setInt64('id', 2)->setString('cat', 'B'), + (new ZVecDoc('doc3'))->setVectorFp32('v', [0.0, 0.0, 1.0, 0.0])->setInt64('id', 3)->setString('cat', 'A'), + ]; + // Set v2 same as v for simplicity + foreach ($docs as $d) { $d->setVectorFp32('v2', $d->getVectorFp32('v')); } + $c->insert(...$docs); + $c->flush(); + $c->optimize(); + echo "Inserted 3 docs\n"; + + // Test 1: queryVector() basic + $vq = new ZVecVectorQuery('v', [1.0, 0.0, 0.0, 0.0]); + $vq->setTopk(3); + $results = $c->queryVector($vq); + assert(count($results) === 3, 'Expected 3 results'); + $pks = array_map(fn($d) => $d->getPk(), $results); + assert(in_array('doc1', $pks), 'Expected doc1 in results'); + echo "queryVector basic OK\n"; + + // Test 2: queryVector with includeVector + $vq = (new ZVecVectorQuery('v', [1.0, 0.0, 0.0, 0.0]))->setTopk(1)->setIncludeVector(true); + $results = $c->queryVector($vq); + assert(count($results) === 1, 'Expected 1 result'); + $v = $results[0]->getVectorFp32('v'); + assert($v !== null, 'Expected vector with includeVector'); + echo "queryVector with includeVector OK\n"; + + // Test 3: queryVector with filter + $vq = (new ZVecVectorQuery('v', [1.0, 0.0, 0.0, 0.0]))->setTopk(3)->setFilter("cat = 'A'"); + $results = $c->queryVector($vq); + assert(count($results) === 2, 'Expected 2 results'); + foreach ($results as $r) { + assert($r->getString('cat') === 'A', 'All filtered results should have cat=A'); + } + echo "queryVector with filter OK\n"; + + // Test 4: queryVector with outputFields + $vq = (new ZVecVectorQuery('v', [1.0, 0.0, 0.0, 0.0]))->setTopk(3)->setOutputFields(['id', 'cat']); + $results = $c->queryVector($vq); + assert(count($results) === 3, 'Expected 3 results'); + echo "queryVector with outputFields OK\n"; + + // Test 5: queryVector with HNSW params + $vq = (new ZVecVectorQuery('v', [0.0, 1.0, 0.0, 0.0])) + ->setTopk(3) + ->setHnswParams(ef: 200); + $results = $c->queryVector($vq); + assert(count($results) === 3, 'Expected 3 results'); + $pks = array_map(fn($d) => $d->getPk(), $results); + assert(in_array('doc2', $pks), 'Expected doc2 in results'); + echo "queryVector with HNSW params OK\n"; + + // Test 6: setRadius with high value (should exclude some results) + $vq = (new ZVecVectorQuery('v', [1.0, 0.0, 0.0, 0.0])) + ->setTopk(10) + ->setRadius(0.0); + $results = $c->queryVector($vq); + assert(count($results) >= 1, 'Expected at least 1 result with radius=0'); + echo "queryVector with radius OK\n"; + + // Test 7: ZVecVectorQuery via old query() backward compat + $vq = (new ZVecVectorQuery('v', [1.0, 0.0, 0.0, 0.0]))->setTopk(3); + $results = $c->query($vq, topk: 3); + assert(count($results) === 3, 'Expected 3 results'); + echo "ZVecVectorQuery via query() OK\n"; + + // Test 8: Non-existent vector field throws + try { + $vq = new ZVecVectorQuery('nonexistent', [1.0, 0.0, 0.0, 0.0]); + $vq->setTopk(3); + $c->queryVector($vq); + echo "FAIL: Expected ZVecException for non-existent field\n"; + } catch (ZVecException $e) { + assert(str_contains($e->getMessage(), 'not exist') || str_contains($e->getMessage(), 'not found'), + 'Expected field not found error'); + echo "Non-existent vector field throws OK\n"; + } + + // Test 9: queryVector with linear mode + $vq = (new ZVecVectorQuery('v', [1.0, 0.0, 0.0, 0.0])) + ->setTopk(3) + ->setLinear(true); + $results = $c->queryVector($vq); + assert(count($results) === 3, 'Expected 3 results'); + echo "queryVector with linear mode OK\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 +queryVector basic OK +queryVector with includeVector OK +queryVector with filter OK +queryVector with outputFields OK +queryVector with HNSW params OK +queryVector with radius OK +ZVecVectorQuery via query() OK +Non-existent vector field throws OK +queryVector with linear mode OK +ALL TESTS PASSED \ No newline at end of file diff --git a/tests/test_uint64_roundtrip.phpt b/tests/test_uint64_roundtrip.phpt new file mode 100644 index 0000000..b046b81 --- /dev/null +++ b/tests/test_uint64_roundtrip.phpt @@ -0,0 +1,100 @@ +--TEST-- +UINT64 scalar round-trip: insert, fetch, and query with 64-bit unsigned integers +--SKIPIF-- + +--FILE-- +addInt64('id', nullable: false); + $schema->addUint64('u64', nullable: true); + $schema->addVectorFp32('v', dimension: 2, metricType: ZVecSchema::METRIC_COSINE); + + $c = ZVec::create($path, $schema); + $c->createHnswIndex('v'); + + // Insert docs with various UINT64 values + $docs = [ + ['doc1', 1, 0], + ['doc2', 2, 100], + ['doc3', 3, 9999999999], + ['doc4', 4, PHP_INT_MAX], + ]; + + foreach ($docs as [$pk, $id, $u64val]) { + $doc = new ZVecDoc($pk); + $doc->setInt64('id', $id)->setUint64('u64', $u64val)->setVectorFp32('v', [(float)$id, (float)$id * 2]); + $c->insert($doc); + } + + $c->flush(); + $c->optimize(); + echo "Inserted 4 UINT64 docs\n"; + + // Helper: fetch doc by PK + $f = function($pk) use ($c) { return $c->fetch($pk)[0]; }; + + // Test 1: Fetch and verify each doc by PK + $d1 = $f('doc1'); + assert($d1->getUint64('u64') === 0, "Expected doc1 u64=0, got " . var_export($d1->getUint64('u64'), true)); + $d2 = $f('doc2'); + assert($d2->getUint64('u64') === 100, "Expected doc2 u64=100"); + $d3 = $f('doc3'); + assert($d3->getUint64('u64') === 9999999999, "Expected doc3 u64=9999999999"); + $d4 = $f('doc4'); + assert($d4->getUint64('u64') === PHP_INT_MAX, "Expected doc4 u64=PHP_INT_MAX"); + echo "Fetched UINT64 values OK\n"; + + // Test 2: getInt64 returns null for UINT64 field + assert($d1->getInt64('u64') === null, 'Expected null from getInt64 on UINT64 field'); + echo "getInt64 returns null for UINT64 field OK\n"; + + // Test 3: Query and verify results contain correct UINT64 values + $results = $c->query('v', [1.0, 2.0], topk: 5); + assert(count($results) >= 4, 'Expected at least 4 results'); + foreach ($results as $r) { + if ($r->getPk() === 'doc1') { + assert($r->getUint64('u64') === 0, "Expected doc1 u64=0"); + } + if ($r->getPk() === 'doc4') { + assert($r->getUint64('u64') === PHP_INT_MAX, "Expected doc4 u64=PHP_INT_MAX"); + } + } + echo "Query results contain correct UINT64 values OK\n"; + + // Test 4: hasField and fieldNames includes UINT64 + assert($d1->hasField('u64'), 'Expected hasField true for u64'); + $fieldNames = $d1->fieldNames(); + assert(in_array('u64', $fieldNames), 'Expected u64 in fieldNames'); + echo "hasField/fieldNames with UINT64 OK\n"; + + // Test 5: Unset UINT64 field returns null via getter + $docNull = new ZVecDoc('null_test'); + $docNull->setInt64('id', 99)->setVectorFp32('v', [99.0, 199.0]); + $c->insert($docNull); + $c->flush(); + $dn = $c->fetch('null_test')[0]; + assert($dn->getUint64('u64') === null, 'Expected null for unset UINT64 field'); + echo "Unset UINT64 field returns null OK\n"; + + echo "ALL TESTS PASSED\n"; +} finally { + if (isset($c)) { try { $c->destroy(); } catch (Exception $e) {} } + exec("rm -rf " . escapeshellarg($path)); +} +?> +--EXPECT-- +Inserted 4 UINT64 docs +Fetched UINT64 values OK +getInt64 returns null for UINT64 field OK +Query results contain correct UINT64 values OK +hasField/fieldNames with UINT64 OK +Unset UINT64 field returns null OK +ALL TESTS PASSED \ No newline at end of file diff --git a/tests/test_vector_binary32_roundtrip.phpt b/tests/test_vector_binary32_roundtrip.phpt new file mode 100644 index 0000000..dd1f002 --- /dev/null +++ b/tests/test_vector_binary32_roundtrip.phpt @@ -0,0 +1,69 @@ +--TEST-- +VECTOR_INT8 round-trip (variant): insert, fetch with 8-bit signed integer vectors (alternative pattern) +--SKIPIF-- + +--FILE-- +addVectorInt8('v', dimension: 4, metricType: ZVecSchema::METRIC_IP); + $schema->addInt64('id', nullable: false); + + $c = ZVec::create($path, $schema); + $c->createHnswIndex('v', metricType: ZVecSchema::METRIC_IP); + + // Insert docs with various INT8 vector patterns + $docs = [ + ['doc1', 1, [0, 0, 0, 0]], + ['doc2', 2, [1, 2, 3, 4]], + ['doc3', 3, [4, 3, 2, 1]], + ]; + + foreach ($docs as [$pk, $id, $vec]) { + $doc = new ZVecDoc($pk); + $doc->setInt64('id', $id)->setVectorInt8('v', $vec); + $c->insert($doc); + } + $c->flush(); + $c->optimize(); + echo "Inserted 3 INT8 docs\n"; + + // Test 1: Fetch and verify each by PK + $d1 = $c->fetch('doc1')[0]; + $v1 = $d1->getVectorInt8('v'); + assert($v1 === [0, 0, 0, 0], "Expected [0,0,0,0], got " . json_encode($v1)); + + $d2 = $c->fetch('doc2')[0]; + $v2 = $d2->getVectorInt8('v'); + assert($v2 === [1, 2, 3, 4], "Expected [1,2,3,4]"); + + $d3 = $c->fetch('doc3')[0]; + $v3 = $d3->getVectorInt8('v'); + assert($v3 === [4, 3, 2, 1], "Expected [4,3,2,1]"); + echo "Fetched INT8 vectors OK\n"; + + // Test 2: hasVector on INT8 + assert($d1->hasVector('v'), 'Expected hasVector'); + assert(!$d1->hasVector('nonexistent'), 'Expected no hasVector'); + $vecNames = $d1->vectorNames(); + assert(in_array('v', $vecNames), 'Expected v in vectorNames'); + echo "hasVector/vectorNames OK\n"; + + echo "ALL TESTS PASSED\n"; +} finally { + if (isset($c)) { try { $c->destroy(); } catch (Exception $e) {} } + exec("rm -rf " . escapeshellarg($path)); +} +?> +--EXPECT-- +Inserted 3 INT8 docs +Fetched INT8 vectors OK +hasVector/vectorNames OK +ALL TESTS PASSED \ No newline at end of file diff --git a/tests/test_vector_int16_roundtrip.phpt b/tests/test_vector_int16_roundtrip.phpt new file mode 100644 index 0000000..9e309cb --- /dev/null +++ b/tests/test_vector_int16_roundtrip.phpt @@ -0,0 +1,67 @@ +--TEST-- +VECTOR_INT8 round-trip: insert, fetch with 8-bit signed integer vectors +--SKIPIF-- + +--FILE-- +addVectorInt8('v', dimension: 4, metricType: ZVecSchema::METRIC_IP); + $schema->addInt64('id', nullable: false); + + $c = ZVec::create($path, $schema); + $c->createHnswIndex('v', metricType: ZVecSchema::METRIC_IP); + + // Insert docs with INT8 vectors + $docs = [ + ['doc1', 1, [1, 2, 3, 4]], + ['doc2', 2, [1, 1, 1, 1]], + ['doc3', 3, [4, 3, 2, 1]], + ]; + + foreach ($docs as [$pk, $id, $vec]) { + $doc = new ZVecDoc($pk); + $doc->setInt64('id', $id)->setVectorInt8('v', $vec); + $c->insert($doc); + } + $c->flush(); + $c->optimize(); + echo "Inserted 3 INT8 docs\n"; + + // Fetch each doc by PK and verify + $v1 = $c->fetch('doc1')[0]->getVectorInt8('v'); + assert($v1 !== null, 'Expected INT8 vector'); + assert($v1 === [1, 2, 3, 4], "Expected [1,2,3,4], got " . json_encode($v1)); + + $v2 = $c->fetch('doc2')[0]->getVectorInt8('v'); + assert($v2 === [1, 1, 1, 1], "Expected [1,1,1,1]"); + + $v3 = $c->fetch('doc3')[0]->getVectorInt8('v'); + assert($v3 === [4, 3, 2, 1], "Expected [4,3,2,1]"); + echo "Fetched INT8 vectors OK\n"; + + // hasVector/vectorNames + $d1 = $c->fetch('doc1')[0]; + assert($d1->hasVector('v'), 'Expected hasVector true'); + $vecNames = $d1->vectorNames(); + assert(in_array('v', $vecNames), 'Expected v in vectorNames'); + echo "hasVector/vectorNames with INT8 OK\n"; + + echo "ALL TESTS PASSED\n"; +} finally { + if (isset($c)) { try { $c->destroy(); } catch (Exception $e) {} } + exec("rm -rf " . escapeshellarg($path)); +} +?> +--EXPECT-- +Inserted 3 INT8 docs +Fetched INT8 vectors OK +hasVector/vectorNames with INT8 OK +ALL TESTS PASSED \ No newline at end of file