1
0
mirror of https://github.com/sqlite/sqlite.git synced 2026-10-23 21:49:02 +03:00
Files
sqlite/ext/vec1/test/vec1bucket.test
dan a27bcfe21c Fix a vec1 problem where rounding errors when assigning vectors to centroids could cause delete operations to fail.
FossilOrigin-Name: aab150f4b39aa8dfa0df9161368b5f3b50859b2657be1bf55d06b67a03005c6b
2026-10-10 18:06:06 +00:00

195 lines
6.5 KiB
Plaintext

# 2026-10-10
#
# The author disclaims copyright to this source code. In place of
# a legal notice, here is a blessing:
#
# May you do good and not evil.
# May you find forgiveness for yourself and forgive others.
# May you share freely, never taking more than you give.
#
#***********************************************************************
#
# Tests for vectors that lie (almost) exactly on the boundary between two
# IVF buckets. The bucket each such vector is assigned to depends only on
# rounding in the L2 distance calculation. The 'rebuild' command, integrity
# check and DELETE must all agree on which bucket each vector is in.
#
# A real PQ model is trained, then its centroids are overwritten with known
# values. Centroids 0 and 1 are random, the others are far away from
# everything. All vectors are inserted near the midpoint of centroids
# 0 and 1. Using 8 centroids matters - with fewer than 8 the AVX2 rebuild
# code falls back to the same routine used by integrity-check and DELETE.
#
source [file join [file dirname [info script]] vec1_common.tcl]
set testprefix vec1bucket
vec1_load db
set nElem 128
set nVec 2000
set noise 1e-6
proc rlist {n} {
set v {}
for {set i 0} {$i<$n} {incr i} { lappend v [expr {rand()*2.0-1.0}] }
set v
}
# Train a model with 8 buckets, plain PQ, no rotation and no residuals.
do_test 1.0 {
db eval { CREATE TEMP TABLE tr(v) }
for {set i 0} {$i<600} {incr i} {
db eval { INSERT INTO tr VALUES( random_vector($nElem) ) }
}
set T [db one {
SELECT vec1_train(v, '{codesize:8, nbucket:8, quantizer:"pq", residual:0}')
FROM tr
}]
binary scan $T I6 hdr
lassign $hdr iVersion flags nE nCodebook nBucket eDist
list [expr {$flags & 0x16}] $nBucket
} {0 8}
# Replace the centroids in the model blob.
set nCodeElem [expr {($nElem + $nCodebook - 1) / $nCodebook}]
set iCentOff [expr {24 + $nCodebook*$nCodeElem*256*4}]
set c0 [rlist $nElem]
set c1 [rlist $nElem]
set aCent [concat $c0 $c1]
for {set i 2} {$i<$nBucket} {incr i} {
foreach x [rlist $nElem] { lappend aCent [expr {$x + 100.0}] }
}
set T [string replace $T $iCentOff [expr {$iCentOff+$nBucket*$nElem*4-1}] \
[binary format f* $aCent]]
# Insert vectors near the midpoint of c0 and c1, plus a little noise.
set mid {}
foreach a $c0 b $c1 { lappend mid [expr {($a+$b)/2.0}] }
do_test 1.1 {
db eval { CREATE VIRTUAL TABLE v1 USING vec1 }
db transaction {
for {set i 1} {$i<=$nVec} {incr i} {
set v {}
foreach m $mid { lappend v [expr {$m + (rand()-0.5)*$noise}] }
set blob [binary format f* $v]
db eval { INSERT INTO v1(rowid, vector) VALUES($i, $blob) }
}
}
db one { SELECT count(*) FROM v1 }
} $nVec
# Rebuild (batched N-vector path), then integrity-check (per-vector path).
do_execsql_test 1.2 { INSERT INTO v1(cmd, vector) VALUES('rebuild', $T) }
do_execsql_test 1.3 { PRAGMA integrity_check } ok
# Delete rows with rowids from iFirst to nVec, stepping by 2. Each DELETE
# recomputes the bucket using the per-vector path, so may compute a
# different bucket to the one used by 'rebuild'. Return the number of rows
# that could not be deleted.
proc delete_rows {tbl iFirst} {
global nVec
set nFail 0
for {set i $iFirst} {$i<=$nVec} {incr i 2} {
if {[catch { db eval "DELETE FROM $tbl WHERE rowid=$i" }]} {
incr nFail
}
}
set nFail
}
# Delete every row, checking the index is still consistent half way.
# Deleting rows causes lists to be rewritten once they contain enough
# tombstones, which must be done using the bucket the row was actually
# found in, not the one calculated for it.
do_test 1.4 { delete_rows v1 1 } 0
do_execsql_test 1.5 { PRAGMA integrity_check } ok
do_test 1.6 { delete_rows v1 2 } 0
do_execsql_test 1.7 {
SELECT count(*) FROM v1;
PRAGMA integrity_check;
} {0 ok}
#-------------------------------------------------------------------------
# Same again, but this time move all of bucket 1's lists into bucket 7,
# which is nowhere near any of the vectors. Integrity-check reports this.
# But DELETE should still be able to find and delete the rows.
#
do_test 2.0 {
db eval { CREATE VIRTUAL TABLE v2 USING vec1 }
db transaction {
for {set i 1} {$i<=$nVec} {incr i} {
set v {}
foreach m $mid { lappend v [expr {$m + (rand()-0.5)*$noise}] }
set blob [binary format f* $v]
db eval { INSERT INTO v2(rowid, vector) VALUES($i, $blob) }
}
}
db eval { INSERT INTO v2(cmd, vector) VALUES('rebuild', $T) }
db one { SELECT count(*)>0 FROM v2_idx WHERE bucket=1 }
} 1
do_execsql_test 2.1 {
UPDATE v2_idx SET bucket=7 WHERE bucket=1;
}
do_test 2.2 {
string match "*is in wrong bucket*" [db one { PRAGMA integrity_check }]
} 1
do_test 2.3 { delete_rows v2 1 } 0
do_test 2.4 { delete_rows v2 2 } 0
do_execsql_test 2.5 { SELECT count(*) FROM v2 } 0
#-------------------------------------------------------------------------
# Create a list in bucket 1 containing rows that belong in bucket 0 as
# well as rows that belong in bucket 1. Then delete all the bucket 0 rows.
# DELETE finds them by searching all buckets. When a list is rewritten to
# remove tombstones, it must stay in bucket 1, the bucket it was found in.
# If it were moved to bucket 0, the bucket 1 rows left in it would be in
# the wrong bucket.
#
# The list is created by rebuilding using model $TA, in which centroid 0
# is far away from everything, so all rows go to bucket 1. The model is
# then replaced by $T, in which centroid 0 is c0 again.
#
set TA [string replace $T $iCentOff [expr {$iCentOff+$nElem*4-1}] \
[binary format f* [lmap x $c0 {expr {$x + 200.0}}]]]
do_test 3.0 {
db eval { CREATE VIRTUAL TABLE v3 USING vec1 }
db transaction {
for {set i 1} {$i<=$nVec} {incr i} {
set c [expr {$i<=$nVec/2 ? $c0 : $c1}]
set v [lmap x $c {expr {$x + (rand()-0.5)*0.001}}]
set blob [binary format f* $v]
db eval { INSERT INTO v3(rowid, vector) VALUES($i, $blob) }
}
}
db eval { INSERT INTO v3(cmd, vector) VALUES('rebuild', $TA) }
db eval { SELECT DISTINCT bucket FROM v3_idx }
} 1
do_test 3.1 {
db eval { UPDATE v3_model SET val = $T WHERE id=1 }
db close
sqlite3 db test.db
vec1_load db
string match "*is in wrong bucket*" [db one { PRAGMA integrity_check }]
} 1
proc delete_range {tbl iFirst iLast} {
set nFail 0
for {set i $iFirst} {$i<=$iLast} {incr i} {
if {[catch { db eval "DELETE FROM $tbl WHERE rowid=$i" }]} {
incr nFail
}
}
set nFail
}
do_test 3.2 { delete_range v3 1 [expr {$nVec/2}] } 0
do_execsql_test 3.3 {
SELECT count(*) FROM v3;
PRAGMA integrity_check;
} [list [expr {$nVec/2}] ok]
finish_test