Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand Down
17 changes: 17 additions & 0 deletions ffi/zvec_ffi.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<zvec_vector_query_t>(holder);
Expand Down Expand Up @@ -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<VectorQueryHolder*>(q);
holder->query.target_.query_params_ = std::make_shared<HnswQueryParams>(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<VectorQueryHolder*>(q);
holder->query.target_.query_params_ = std::make_shared<HnswRabitqQueryParams>(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<VectorQueryHolder*>(q);
holder->query.target_.query_params_ = std::make_shared<VamanaQueryParams>(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<VectorQueryHolder*>(q);
holder->query.target_.query_params_ = std::make_shared<IVFQueryParams>(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<VectorQueryHolder*>(q);
holder->query.target_.query_params_ = std::make_shared<FlatQueryParams>();
merge_stored_query_settings(holder);
}

void zvec_vector_query_set_radius(zvec_vector_query_t q, float radius) {
Expand Down
104 changes: 104 additions & 0 deletions tests/test_query_params_order.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
--TEST--
Query params order: setRadius/setLinear/setUsingRefiner before set*Params() are honored (#197)
--SKIPIF--
<?php
if (extension_loaded('zvec')) die('skip Native zvec extension loaded (use FFI)');
if (!extension_loaded('ffi')) die('skip FFI extension not available');
?>
--FILE--
<?php
require_once __DIR__ . '/../src/ZVec.php';
ZVec::init(logType: ZVec::LOG_CONSOLE, logLevel: ZVec::LOG_WARN);

$path = __DIR__ . '/../test_dbs/qparams_order_' . uniqid();

/** @return string[] */
function pks(array $results): array
{
return array_map(fn($d) => $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
Loading