mirror of
https://github.com/sqlite/sqlite.git
synced 2026-10-23 21:49:02 +03:00
195 lines
6.5 KiB
Plaintext
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
|