diff --git a/CHANGELOG.md b/CHANGELOG.md index f3d6cc4..d56082b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **SMELL-014: Decomposed `query()` (116 lines, 4+ responsibilities) into focused helpers** (#95) + - Extracted `resolveQueryParams()` — resolves parameters from explicit args or `ZVecVectorQuery` object, validates inputs + - Extracted `executeQuery()` — performs FFI call with FP32 float vectors + - Extracted `executeQueryFp64()` — performs FFI call with FP64 double vectors + - `query()` body reduced to ~15 lines calling the three helpers + - Refactored `queryFp64()`, `queryWithReranker()`, and `queryWithRerankerFp64()` to reuse helpers + - Eliminated duplicated FFI parameter setup and output field handling code + - Added `tests/test_query_decomposition.phpt` — 14 test cases covering all query paths + - **SMELL-017: Extracted `collection_batch_op()` template in `ffi/zvec_ffi.cc`** (#97) - Replaced ~130 lines of duplicated code across `zvec_collection_insert_batch`, `zvec_collection_upsert_batch`, and `zvec_collection_update_batch` with a shared diff --git a/src/ZVec.php b/src/ZVec.php index d76aade..f57f3b6 100644 --- a/src/ZVec.php +++ b/src/ZVec.php @@ -861,12 +861,25 @@ public function groupByVectorQuery(ZVecGroupByVectorQuery $query): array } /** - * @param float[] $queryVector - * @param string[]|null $outputFields - * @return ZVecDoc[] - * @deprecated Use queryWithReranker() when passing a $reranker. The $reranker parameter is deprecated. + * Resolve query parameters from explicit arguments or ZVecVectorQuery object. + * + * @return array{ + * fieldName: string, + * queryVector: float[], + * topk: int, + * includeVector: bool, + * filter: ?string, + * outputFields: ?string[], + * queryParamType: int, + * hnswEf: int, + * ivfNprobe: int, + * radius: float, + * isLinear: bool, + * isUsingRefiner: bool, + * useFp64: bool + * } */ - public function query( + private function resolveQueryParams( string|ZVecVectorQuery $fieldName, array $queryVector = [], int $topk = 10, @@ -879,32 +892,9 @@ public function query( float $radius = 0.0, bool $isLinear = false, bool $isUsingRefiner = false, - ?ZVecReRanker $reranker = null ): array { - if ($reranker !== null) { - trigger_error( - 'query(): Passing $reranker is deprecated. Use queryWithReranker() instead.', - E_USER_DEPRECATED - ); - return $this->queryWithReranker( - fieldName: $fieldName, - queryVector: $queryVector, - topk: $topk, - includeVector: $includeVector, - filter: $filter, - outputFields: $outputFields, - queryParamType: $queryParamType, - hnswEf: $hnswEf, - ivfNprobe: $ivfNprobe, - radius: $radius, - isLinear: $isLinear, - isUsingRefiner: $isUsingRefiner, - reranker: $reranker - ); - } - $this->checkClosed(); + $useFp64 = false; - // Handle ZVecVectorQuery object if ($fieldName instanceof ZVecVectorQuery) { $vq = $fieldName; $fieldName = $vq->fieldName; @@ -918,29 +908,11 @@ public function query( $topk = $vq->topk ?? $topk; $includeVector = $vq->includeVector ?? $includeVector; $filter = $vq->filter ?? $filter; + $useFp64 = $vq->useFp64; if ($vq->docId !== null) { throw new ZVecException("query() with docId not yet implemented. Use queryById() or fetch the vector first."); } - - // Route to FP64 query path if flagged - if ($vq->useFp64) { - return $this->queryFp64( - fieldName: $fieldName, - queryVector: $queryVector, - topk: $topk, - includeVector: $includeVector, - filter: $filter, - outputFields: $outputFields, - queryParamType: $queryParamType, - hnswEf: $hnswEf, - ivfNprobe: $ivfNprobe, - radius: $radius, - isLinear: $isLinear, - isUsingRefiner: $isUsingRefiner, - reranker: $reranker - ); - } } if ($topk <= 0) { @@ -950,6 +922,44 @@ public function query( throw new ZVecException('Field name must not be empty'); } + return [ + 'fieldName' => $fieldName, + 'queryVector' => $queryVector, + 'topk' => $topk, + 'includeVector' => $includeVector, + 'filter' => $filter, + 'outputFields' => $outputFields, + 'queryParamType' => $queryParamType, + 'hnswEf' => $hnswEf, + 'ivfNprobe' => $ivfNprobe, + 'radius' => $radius, + 'isLinear' => $isLinear, + 'isUsingRefiner' => $isUsingRefiner, + 'useFp64' => $useFp64, + ]; + } + + /** + * Execute FP32 vector query via FFI. + * + * @param float[] $queryVector + * @param string[]|null $outputFields + * @return ZVecDoc[] + */ + private function executeQuery( + string $fieldName, + array $queryVector, + int $topk, + bool $includeVector, + ?string $filter, + ?array $outputFields, + int $queryParamType, + int $hnswEf, + int $ivfNprobe, + float $radius, + bool $isLinear, + bool $isUsingRefiner, + ): array { $ffi = self::ffi(); $dim = count($queryVector); $vecData = $ffi->new("float[$dim]"); @@ -991,6 +1001,136 @@ public function query( return self::parseQueryResult($result); } + /** + * Execute FP64 vector query via FFI. + * + * @param float[] $queryVector + * @param string[]|null $outputFields + * @return ZVecDoc[] + */ + private function executeQueryFp64( + string $fieldName, + array $queryVector, + int $topk, + bool $includeVector, + ?string $filter, + ?array $outputFields, + int $queryParamType, + int $hnswEf, + int $ivfNprobe, + float $radius, + bool $isLinear, + bool $isUsingRefiner, + ): array { + $ffi = self::ffi(); + $dim = count($queryVector); + $vecData = $ffi->new("double[$dim]"); + foreach ($queryVector as $i => $v) { + $vecData[$i] = $v; + } + + $result = $ffi->new('zvec_query_result_t'); + + $ofCStrings = []; + try { + $ofArr = null; + $ofCount = -1; + if ($outputFields !== null || $queryParamType !== self::QUERY_PARAM_NONE) { + if ($outputFields !== null) { + [$ofArr, $ofCount, $ofCStrings] = self::toCStringArray($ffi, $outputFields); + } + + $status = $ffi->zvec_collection_query_fp64_ex( + $this->handle, $fieldName, $vecData, $dim, + $topk, $includeVector ? 1 : 0, $filter ?? '', + $ofArr, $ofCount, + $queryParamType, $hnswEf, $ivfNprobe, + $radius, $isLinear ? 1 : 0, $isUsingRefiner ? 1 : 0, + FFI::addr($result) + ); + } else { + $status = $ffi->zvec_collection_query_fp64( + $this->handle, $fieldName, $vecData, $dim, + $topk, $includeVector ? 1 : 0, $filter ?? '', + FFI::addr($result) + ); + } + self::checkStatus($status); + } finally { + self::freeCStringArray($ofCStrings); + } + + return self::parseQueryResult($result); + } + + /** + * @param float[] $queryVector + * @param string[]|null $outputFields + * @return ZVecDoc[] + * @deprecated Use queryWithReranker() when passing a $reranker. The $reranker parameter is deprecated. + */ + public function query( + string|ZVecVectorQuery $fieldName, + array $queryVector = [], + int $topk = 10, + bool $includeVector = false, + ?string $filter = null, + ?array $outputFields = null, + int $queryParamType = self::QUERY_PARAM_NONE, + int $hnswEf = 200, + int $ivfNprobe = 10, + float $radius = 0.0, + bool $isLinear = false, + bool $isUsingRefiner = false, + ?ZVecReRanker $reranker = null + ): array { + if ($reranker !== null) { + trigger_error( + 'query(): Passing $reranker is deprecated. Use queryWithReranker() instead.', + E_USER_DEPRECATED + ); + return $this->queryWithReranker( + fieldName: $fieldName, + queryVector: $queryVector, + topk: $topk, + includeVector: $includeVector, + filter: $filter, + outputFields: $outputFields, + queryParamType: $queryParamType, + hnswEf: $hnswEf, + ivfNprobe: $ivfNprobe, + radius: $radius, + isLinear: $isLinear, + isUsingRefiner: $isUsingRefiner, + reranker: $reranker + ); + } + + $this->checkClosed(); + + $params = $this->resolveQueryParams( + $fieldName, $queryVector, $topk, $includeVector, $filter, + $outputFields, $queryParamType, $hnswEf, $ivfNprobe, + $radius, $isLinear, $isUsingRefiner + ); + + if ($params['useFp64']) { + return $this->executeQueryFp64( + $params['fieldName'], $params['queryVector'], $params['topk'], + $params['includeVector'], $params['filter'], $params['outputFields'], + $params['queryParamType'], $params['hnswEf'], $params['ivfNprobe'], + $params['radius'], $params['isLinear'], $params['isUsingRefiner'] + ); + } + + return $this->executeQuery( + $params['fieldName'], $params['queryVector'], $params['topk'], + $params['includeVector'], $params['filter'], $params['outputFields'], + $params['queryParamType'], $params['hnswEf'], $params['ivfNprobe'], + $params['radius'], $params['isLinear'], $params['isUsingRefiner'] + ); + } + /** * @param int[] $queryVector */ @@ -1048,7 +1188,6 @@ public function queryFp64( bool $isUsingRefiner = false, ?ZVecReRanker $reranker = null ): array { - $this->checkClosed(); if ($reranker !== null) { trigger_error( 'queryFp64(): Passing $reranker is deprecated. Use queryWithReranker() instead.', @@ -1070,6 +1209,9 @@ public function queryFp64( reranker: $reranker ); } + + $this->checkClosed(); + if ($topk <= 0) { throw new ZVecException("topk must be a positive integer, got: {$topk}"); } @@ -1077,45 +1219,11 @@ public function queryFp64( throw new ZVecException('Field name must not be empty'); } - $ffi = self::ffi(); - $dim = count($queryVector); - $vecData = $ffi->new("double[$dim]"); - foreach ($queryVector as $i => $v) { - $vecData[$i] = $v; - } - - $result = $ffi->new('zvec_query_result_t'); - - $ofCStrings = []; - try { - $ofArr = null; - $ofCount = -1; - if ($outputFields !== null || $queryParamType !== self::QUERY_PARAM_NONE) { - if ($outputFields !== null) { - [$ofArr, $ofCount, $ofCStrings] = self::toCStringArray($ffi, $outputFields); - } - - $status = $ffi->zvec_collection_query_fp64_ex( - $this->handle, $fieldName, $vecData, $dim, - $topk, $includeVector ? 1 : 0, $filter ?? '', - $ofArr, $ofCount, - $queryParamType, $hnswEf, $ivfNprobe, - $radius, $isLinear ? 1 : 0, $isUsingRefiner ? 1 : 0, - FFI::addr($result) - ); - } else { - $status = $ffi->zvec_collection_query_fp64( - $this->handle, $fieldName, $vecData, $dim, - $topk, $includeVector ? 1 : 0, $filter ?? '', - FFI::addr($result) - ); - } - self::checkStatus($status); - } finally { - self::freeCStringArray($ofCStrings); - } - - return self::parseQueryResult($result); + return $this->executeQueryFp64( + $fieldName, $queryVector, $topk, $includeVector, $filter, + $outputFields, $queryParamType, $hnswEf, $ivfNprobe, + $radius, $isLinear, $isUsingRefiner + ); } /** @@ -1145,103 +1253,32 @@ public function queryWithReranker( throw new ZVecException('queryWithReranker() requires a $reranker argument'); } - // Fetch more results for two-stage retrieval - $fetchTopk = max($topk * 2, 100); - - // Resolve ZVecVectorQuery to individual params - $resolvedFieldName = $fieldName; - $resolvedQueryVector = $queryVector; - $resolvedQueryParamType = $queryParamType; - $resolvedHnswEf = $hnswEf; - $resolvedIvfNprobe = $ivfNprobe; - $resolvedRadius = $radius; - $resolvedIsLinear = $isLinear; - $resolvedIsUsingRefiner = $isUsingRefiner; - - if ($fieldName instanceof ZVecVectorQuery) { - $vq = $fieldName; - $resolvedFieldName = $vq->fieldName; - $resolvedQueryVector = $vq->vector; - $resolvedQueryParamType = $vq->queryParamType; - $resolvedHnswEf = $vq->hnswEf; - $resolvedIvfNprobe = $vq->ivfNprobe; - $resolvedRadius = $vq->radius; - $resolvedIsLinear = $vq->isLinear; - $resolvedIsUsingRefiner = $vq->isUsingRefiner; - $fetchTopk = $vq->topk !== null ? max($vq->topk * 2, 100) : $fetchTopk; - $includeVector = $vq->includeVector ?? $includeVector; - $filter = $vq->filter ?? $filter; - - if ($vq->docId !== null) { - throw new ZVecException("queryWithReranker() with docId not yet implemented."); - } - - if ($vq->useFp64) { - return $this->queryWithRerankerFp64( - fieldName: $resolvedFieldName, - queryVector: $resolvedQueryVector, - topk: $topk, - includeVector: $includeVector, - filter: $filter, - outputFields: $outputFields, - queryParamType: $resolvedQueryParamType, - hnswEf: $resolvedHnswEf, - ivfNprobe: $resolvedIvfNprobe, - radius: $resolvedRadius, - isLinear: $resolvedIsLinear, - isUsingRefiner: $resolvedIsUsingRefiner, - reranker: $reranker - ); - } - } - - if ($topk <= 0) { - throw new ZVecException("topk must be a positive integer, got: {$topk}"); - } - if (is_string($resolvedFieldName) && $resolvedFieldName === '') { - throw new ZVecException('Field name must not be empty'); - } - - $ffi = self::ffi(); - $dim = count($resolvedQueryVector); - $vecData = $ffi->new("float[$dim]"); - foreach ($resolvedQueryVector as $i => $v) { - $vecData[$i] = $v; - } - - $result = $ffi->new('zvec_query_result_t'); - - $ofCStrings = []; - try { - $ofArr = null; - $ofCount = -1; - if ($outputFields !== null || $resolvedQueryParamType !== self::QUERY_PARAM_NONE) { - if ($outputFields !== null) { - [$ofArr, $ofCount, $ofCStrings] = self::toCStringArray($ffi, $outputFields); - } + $params = $this->resolveQueryParams( + $fieldName, $queryVector, $topk, $includeVector, $filter, + $outputFields, $queryParamType, $hnswEf, $ivfNprobe, + $radius, $isLinear, $isUsingRefiner + ); - $status = $ffi->zvec_collection_query_ex( - $this->handle, $resolvedFieldName, $vecData, $dim, - $fetchTopk, $includeVector ? 1 : 0, $filter ?? '', - $ofArr, $ofCount, - $resolvedQueryParamType, $resolvedHnswEf, $resolvedIvfNprobe, - $resolvedRadius, $resolvedIsLinear ? 1 : 0, $resolvedIsUsingRefiner ? 1 : 0, - FFI::addr($result) - ); - } else { - $status = $ffi->zvec_collection_query( - $this->handle, $resolvedFieldName, $vecData, $dim, - $fetchTopk, $includeVector ? 1 : 0, $filter ?? '', - FFI::addr($result) - ); - } - self::checkStatus($status); - } finally { - self::freeCStringArray($ofCStrings); + // Fetch more results for two-stage retrieval + $fetchTopk = max($params['topk'] * 2, 100); + + if ($params['useFp64']) { + $docs = $this->executeQueryFp64( + $params['fieldName'], $params['queryVector'], $fetchTopk, + $params['includeVector'], $params['filter'], $params['outputFields'], + $params['queryParamType'], $params['hnswEf'], $params['ivfNprobe'], + $params['radius'], $params['isLinear'], $params['isUsingRefiner'] + ); + } else { + $docs = $this->executeQuery( + $params['fieldName'], $params['queryVector'], $fetchTopk, + $params['includeVector'], $params['filter'], $params['outputFields'], + $params['queryParamType'], $params['hnswEf'], $params['ivfNprobe'], + $params['radius'], $params['isLinear'], $params['isUsingRefiner'] + ); } - $docs = self::parseQueryResult($result); - $queryResults = [$resolvedFieldName => $docs]; + $queryResults = [$params['fieldName'] => $docs]; return $reranker->rerank($queryResults); } @@ -1280,45 +1317,12 @@ private function queryWithRerankerFp64( $fetchTopk = max($topk * 2, 100); - $ffi = self::ffi(); - $dim = count($queryVector); - $vecData = $ffi->new("double[$dim]"); - foreach ($queryVector as $i => $v) { - $vecData[$i] = $v; - } - - $result = $ffi->new('zvec_query_result_t'); - - $ofCStrings = []; - try { - $ofArr = null; - $ofCount = -1; - if ($outputFields !== null || $queryParamType !== self::QUERY_PARAM_NONE) { - if ($outputFields !== null) { - [$ofArr, $ofCount, $ofCStrings] = self::toCStringArray($ffi, $outputFields); - } - - $status = $ffi->zvec_collection_query_fp64_ex( - $this->handle, $fieldName, $vecData, $dim, - $fetchTopk, $includeVector ? 1 : 0, $filter ?? '', - $ofArr, $ofCount, - $queryParamType, $hnswEf, $ivfNprobe, - $radius, $isLinear ? 1 : 0, $isUsingRefiner ? 1 : 0, - FFI::addr($result) - ); - } else { - $status = $ffi->zvec_collection_query_fp64( - $this->handle, $fieldName, $vecData, $dim, - $fetchTopk, $includeVector ? 1 : 0, $filter ?? '', - FFI::addr($result) - ); - } - self::checkStatus($status); - } finally { - self::freeCStringArray($ofCStrings); - } + $docs = $this->executeQueryFp64( + $fieldName, $queryVector, $fetchTopk, $includeVector, $filter, + $outputFields, $queryParamType, $hnswEf, $ivfNprobe, + $radius, $isLinear, $isUsingRefiner + ); - $docs = self::parseQueryResult($result); $queryResults = [$fieldName => $docs]; return $reranker->rerank($queryResults); } diff --git a/tests/test_query_decomposition.phpt b/tests/test_query_decomposition.phpt new file mode 100644 index 0000000..502f09e --- /dev/null +++ b/tests/test_query_decomposition.phpt @@ -0,0 +1,157 @@ +--TEST-- +query() decomposition: resolveQueryParams, executeQuery, executeQueryFp64 helpers +--SKIPIF-- + +--FILE-- +addVectorFp32('vec', dimension: 4, metricType: ZVecSchema::METRIC_IP) + ->addString('category', nullable: false, withInvertIndex: true) + ->addString('title', nullable: false); + + $coll = ZVec::create($path, $schema); + + // Insert test documents + $docs = [ + (new ZVecDoc('d1'))->setVectorFp32('vec', [1.0, 0.0, 0.0, 0.0])->setString('category', 'A')->setString('title', 'Alpha'), + (new ZVecDoc('d2'))->setVectorFp32('vec', [0.0, 1.0, 0.0, 0.0])->setString('category', 'B')->setString('title', 'Beta'), + (new ZVecDoc('d3'))->setVectorFp32('vec', [0.0, 0.0, 1.0, 0.0])->setString('category', 'A')->setString('title', 'Gamma'), + (new ZVecDoc('d4'))->setVectorFp32('vec', [0.0, 0.0, 0.0, 1.0])->setString('category', 'B')->setString('title', 'Delta'), + ]; + $coll->insert(...$docs); + $coll->optimize(); + + // Test 1: query() with string fieldName works (uses executeQuery helper) + $results = $coll->query('vec', [1.0, 0.0, 0.0, 0.0], topk: 2); + assert(count($results) >= 1, 'query() with string fieldName must return results'); + assert($results[0]->getPk() !== null, 'Results must have PK'); + echo "1. query() with string fieldName works\n"; + + // Test 2: query() with ZVecVectorQuery object works (uses resolveQueryParams helper) + $vq = new ZVecVectorQuery('vec', [1.0, 0.0, 0.0, 0.0]); + $vq->setTopk(2); + $results2 = $coll->query($vq); + assert(count($results2) >= 1, 'query() with ZVecVectorQuery must return results'); + echo "2. query() with ZVecVectorQuery object works\n"; + + // Test 3: query() with outputFields works (tests output fields path in executeQuery) + $results3 = $coll->query('vec', [1.0, 0.0, 0.0, 0.0], topk: 2, outputFields: ['category', 'title']); + assert(count($results3) >= 1, 'query() with outputFields must return results'); + echo "3. query() with outputFields works\n"; + + // Test 4: query() with filter works (tests filter path in executeQuery) + $results4 = $coll->query('vec', [1.0, 0.0, 0.0, 0.0], topk: 4, filter: 'category = "A"'); + assert(count($results4) >= 1, 'query() with filter must return results'); + foreach ($results4 as $doc) { + assert($doc->getString('category') === 'A', 'Filtered results must match filter'); + } + echo "4. query() with filter works\n"; + + // Test 5: query() with queryParamType works (tests extended query path in executeQuery) + $results5 = $coll->query('vec', [1.0, 0.0, 0.0, 0.0], topk: 2, queryParamType: ZVec::QUERY_PARAM_HNSW, hnswEf: 200); + assert(count($results5) >= 1, 'query() with queryParamType must return results'); + echo "5. query() with queryParamType works\n"; + + // Test 6: query() with includeVector works + $results6 = $coll->query('vec', [1.0, 0.0, 0.0, 0.0], topk: 1, includeVector: true); + assert(count($results6) >= 1, 'query() with includeVector must return results'); + $vec = $results6[0]->getVectorFp32('vec'); + assert($vec !== null, 'Results with includeVector must have vector data'); + assert(count($vec) === 4, 'Vector must have correct dimension'); + echo "6. query() with includeVector works\n"; + + // Test 7: queryWithReranker() uses resolveQueryParams + executeQuery helpers + $vqRerank = new ZVecVectorQuery('vec', [1.0, 0.0, 0.0, 0.0]); + $vqRerank->setTopk(2); + $reranker = new ZVecRrfReRanker(topn: 2, rankConstant: 60); + $results7 = $coll->queryWithReranker($vqRerank, reranker: $reranker); + assert(count($results7) >= 1, 'queryWithReranker() must return results'); + echo "7. queryWithReranker() with ZVecVectorQuery works\n"; + + // Test 8: queryWithReranker() with string fieldName + $results8 = $coll->queryWithReranker('vec', [1.0, 0.0, 0.0, 0.0], topk: 2, reranker: $reranker); + assert(count($results8) >= 1, 'queryWithReranker() with string fieldName must return results'); + echo "8. queryWithReranker() with string fieldName works\n"; + + // Test 9: queryWithReranker() with outputFields + $results9 = $coll->queryWithReranker('vec', [1.0, 0.0, 0.0, 0.0], topk: 2, outputFields: ['category'], reranker: $reranker); + assert(count($results9) >= 1, 'queryWithReranker() with outputFields must return results'); + echo "9. queryWithReranker() with outputFields works\n"; + + // Test 10: Validation works through resolveQueryParams + $validationPassed = false; + try { + $coll->query('vec', [1.0, 0.0, 0.0, 0.0], topk: 0); + } catch (ZVecException $e) { + $validationPassed = str_contains($e->getMessage(), 'topk must be a positive integer'); + } + assert($validationPassed, 'resolveQueryParams must validate topk > 0'); + echo "10. resolveQueryParams validates topk\n"; + + // Test 11: Validation works for empty fieldName + $validationPassed2 = false; + try { + $coll->query('', [1.0, 0.0, 0.0, 0.0]); + } catch (ZVecException $e) { + $validationPassed2 = str_contains($e->getMessage(), 'Field name must not be empty'); + } + assert($validationPassed2, 'resolveQueryParams must validate fieldName'); + echo "11. resolveQueryParams validates fieldName\n"; + + // Test 12: ZVecVectorQuery with docId throws + $vqDocId = new ZVecVectorQuery('vec', [1.0, 0.0, 0.0, 0.0]); + $vqDocId->docId = 'some_doc'; + $docIdThrown = false; + try { + $coll->query($vqDocId); + } catch (ZVecException $e) { + $docIdThrown = str_contains($e->getMessage(), 'docId not yet implemented'); + } + assert($docIdThrown, 'resolveQueryParams must throw for docId'); + echo "12. resolveQueryParams throws for docId\n"; + + // Test 13: query() with combined outputFields and filter + $results13 = $coll->query('vec', [1.0, 0.0, 0.0, 0.0], topk: 2, outputFields: ['title'], filter: 'category = "A"'); + assert(count($results13) >= 1, 'query() with combined outputFields and filter must return results'); + echo "13. query() with combined outputFields and filter works\n"; + + // Test 14: query() with all params (outputFields + filter + queryParamType) + $results14 = $coll->query( + 'vec', [1.0, 0.0, 0.0, 0.0], + topk: 2, includeVector: true, filter: 'category = "A"', + outputFields: ['title'], queryParamType: ZVec::QUERY_PARAM_HNSW, hnswEf: 200 + ); + assert(count($results14) >= 1, 'query() with all params must return results'); + echo "14. query() with all params works\n"; + + $coll->close(); + echo "\nAll tests passed!\n"; + +} finally { + exec("rm -rf " . escapeshellarg($path)); +} +?> +--EXPECT-- +1. query() with string fieldName works +2. query() with ZVecVectorQuery object works +3. query() with outputFields works +4. query() with filter works +5. query() with queryParamType works +6. query() with includeVector works +7. queryWithReranker() with ZVecVectorQuery works +8. queryWithReranker() with string fieldName works +9. queryWithReranker() with outputFields works +10. resolveQueryParams validates topk +11. resolveQueryParams validates fieldName +12. resolveQueryParams throws for docId +13. query() with combined outputFields and filter works +14. query() with all params works + +All tests passed!