diff --git a/test/t/029_hnsw_bit_vacuum_recall.pl b/test/t/029_hnsw_bit_vacuum_recall.pl new file mode 100644 index 0000000..08b4ec6 --- /dev/null +++ b/test/t/029_hnsw_bit_vacuum_recall.pl @@ -0,0 +1,100 @@ +use strict; +use warnings; +use PostgresNode; +use TestLib; +use Test::More; + +my $node; +my @queries = (); +my @expected; +my $limit = 20; +my $dim = 52; +my $max = 2**$dim; + +sub test_recall +{ + my ($min, $ef_search, $test_name) = @_; + my $correct = 0; + my $total = 0; + + my $explain = $node->safe_psql("postgres", qq( + SET enable_seqscan = off; + SET hnsw.ef_search = $ef_search; + EXPLAIN ANALYZE SELECT i FROM tst ORDER BY v <~> $queries[0] LIMIT $limit; + )); + like($explain, qr/Index Scan/); + + for my $i (0 .. $#queries) + { + my $actual = $node->safe_psql("postgres", qq( + SET enable_seqscan = off; + SET hnsw.ef_search = $ef_search; + SELECT i FROM tst ORDER BY v <~> $queries[$i] LIMIT $limit; + )); + my @actual_ids = split("\n", $actual); + + my @expected_ids = split("\n", $expected[$i]); + my %expected_set = map { $_ => 1 } @expected_ids; + + foreach (@actual_ids) + { + if (exists($expected_set{$_})) + { + $correct++; + } + } + + $total += $limit; + } + + cmp_ok($correct / $total, ">=", $min, $test_name); +} + +# Initialize node +$node = get_new_node('node'); +$node->init; +$node->start; + +# Create table +$node->safe_psql("postgres", "CREATE EXTENSION vector;"); +$node->safe_psql("postgres", "CREATE TABLE tst (i int4, v bit($dim));"); +$node->safe_psql("postgres", "ALTER TABLE tst SET (autovacuum_enabled = false);"); +$node->safe_psql("postgres", + "INSERT INTO tst SELECT i, (random() * $max)::bigint::bit($dim) FROM generate_series(1, 10000) i;" +); + +# Add index +$node->safe_psql("postgres", "CREATE INDEX ON tst USING hnsw (v bit_hamming_ops) WITH (m = 4, ef_construction = 8);"); + +# Delete data +$node->safe_psql("postgres", "DELETE FROM tst WHERE i > 2500;"); + +# Generate queries +for (1 .. 20) +{ + my $r = int(rand() * $max); + push(@queries, "${r}::bigint::bit($dim)"); +} + +# Get exact results +@expected = (); +foreach (@queries) +{ + my $res = $node->safe_psql("postgres", qq( + SET enable_indexscan = off; + WITH top AS ( + SELECT v <~> $_ AS distance FROM tst ORDER BY v <~> $_ LIMIT $limit + ) + SELECT i FROM tst WHERE (v <~> $_) <= (SELECT MAX(distance) FROM top) + )); + push(@expected, $res); +} + +test_recall(0.4, 100, "before vacuum"); + +# TODO Test concurrent inserts with vacuum +$node->safe_psql("postgres", "VACUUM tst;"); + +test_recall(0.8, 100, "after vacuum"); + +done_testing(); diff --git a/test/t/030_hnsw_halfvec_vacuum_recall.pl b/test/t/030_hnsw_halfvec_vacuum_recall.pl new file mode 100644 index 0000000..db05cf1 --- /dev/null +++ b/test/t/030_hnsw_halfvec_vacuum_recall.pl @@ -0,0 +1,97 @@ +use strict; +use warnings; +use PostgresNode; +use TestLib; +use Test::More; + +my $node; +my @queries = (); +my @expected; +my $limit = 20; + +sub test_recall +{ + my ($min, $ef_search, $test_name) = @_; + my $correct = 0; + my $total = 0; + + my $explain = $node->safe_psql("postgres", qq( + SET enable_seqscan = off; + SET hnsw.ef_search = $ef_search; + EXPLAIN ANALYZE SELECT i FROM tst ORDER BY v <-> '$queries[0]' LIMIT $limit; + )); + like($explain, qr/Index Scan/); + + for my $i (0 .. $#queries) + { + my $actual = $node->safe_psql("postgres", qq( + SET enable_seqscan = off; + SET hnsw.ef_search = $ef_search; + SELECT i FROM tst ORDER BY v <-> '$queries[$i]' LIMIT $limit; + )); + my @actual_ids = split("\n", $actual); + my %actual_set = map { $_ => 1 } @actual_ids; + + my @expected_ids = split("\n", $expected[$i]); + + foreach (@expected_ids) + { + if (exists($actual_set{$_})) + { + $correct++; + } + $total++; + } + } + + cmp_ok($correct / $total, ">=", $min, $test_name); +} + +# Initialize node +$node = get_new_node('node'); +$node->init; +$node->start; + +# Create table +$node->safe_psql("postgres", "CREATE EXTENSION vector;"); +$node->safe_psql("postgres", "CREATE TABLE tst (i int4, v halfvec(3));"); +$node->safe_psql("postgres", "ALTER TABLE tst SET (autovacuum_enabled = false);"); +$node->safe_psql("postgres", + "INSERT INTO tst SELECT i, ARRAY[random(), random(), random()] FROM generate_series(1, 10000) i;" +); + +# Add index +$node->safe_psql("postgres", "CREATE INDEX ON tst USING hnsw (v halfvec_l2_ops) WITH (m = 4, ef_construction = 8);"); + +# Delete data +$node->safe_psql("postgres", "DELETE FROM tst WHERE i > 2500;"); + +# Generate queries +for (1 .. 20) +{ + my $r1 = rand(); + my $r2 = rand(); + my $r3 = rand(); + push(@queries, "[$r1,$r2,$r3]"); +} + +# Get exact results +@expected = (); +foreach (@queries) +{ + my $res = $node->safe_psql("postgres", qq( + SET enable_indexscan = off; + SELECT i FROM tst ORDER BY v <-> '$_' LIMIT $limit; + )); + push(@expected, $res); +} + +test_recall(0.18, $limit, "before vacuum"); +test_recall(0.95, 100, "before vacuum"); + +# TODO Test concurrent inserts with vacuum +$node->safe_psql("postgres", "VACUUM tst;"); + +test_recall(0.95, $limit, "after vacuum"); + +done_testing(); diff --git a/test/t/031_hnsw_sparsevec_vacuum_recall.pl b/test/t/031_hnsw_sparsevec_vacuum_recall.pl new file mode 100644 index 0000000..7826f58 --- /dev/null +++ b/test/t/031_hnsw_sparsevec_vacuum_recall.pl @@ -0,0 +1,97 @@ +use strict; +use warnings; +use PostgresNode; +use TestLib; +use Test::More; + +my $node; +my @queries = (); +my @expected; +my $limit = 20; + +sub test_recall +{ + my ($min, $ef_search, $test_name) = @_; + my $correct = 0; + my $total = 0; + + my $explain = $node->safe_psql("postgres", qq( + SET enable_seqscan = off; + SET hnsw.ef_search = $ef_search; + EXPLAIN ANALYZE SELECT i FROM tst ORDER BY v <-> '$queries[0]' LIMIT $limit; + )); + like($explain, qr/Index Scan/); + + for my $i (0 .. $#queries) + { + my $actual = $node->safe_psql("postgres", qq( + SET enable_seqscan = off; + SET hnsw.ef_search = $ef_search; + SELECT i FROM tst ORDER BY v <-> '$queries[$i]' LIMIT $limit; + )); + my @actual_ids = split("\n", $actual); + my %actual_set = map { $_ => 1 } @actual_ids; + + my @expected_ids = split("\n", $expected[$i]); + + foreach (@expected_ids) + { + if (exists($actual_set{$_})) + { + $correct++; + } + $total++; + } + } + + cmp_ok($correct / $total, ">=", $min, $test_name); +} + +# Initialize node +$node = get_new_node('node'); +$node->init; +$node->start; + +# Create table +$node->safe_psql("postgres", "CREATE EXTENSION vector;"); +$node->safe_psql("postgres", "CREATE TABLE tst (i int4, v sparsevec(3));"); +$node->safe_psql("postgres", "ALTER TABLE tst SET (autovacuum_enabled = false);"); +$node->safe_psql("postgres", + "INSERT INTO tst SELECT i, ARRAY[random(), random(), random()]::vector::sparsevec(3) FROM generate_series(1, 10000) i;" +); + +# Add index +$node->safe_psql("postgres", "CREATE INDEX ON tst USING hnsw (v sparsevec_l2_ops) WITH (m = 4, ef_construction = 8);"); + +# Delete data +$node->safe_psql("postgres", "DELETE FROM tst WHERE i > 2500;"); + +# Generate queries +for (1 .. 20) +{ + my $r1 = rand(); + my $r2 = rand(); + my $r3 = rand(); + push(@queries, "{1:$r1,2:$r2,3:$r3}/3"); +} + +# Get exact results +@expected = (); +foreach (@queries) +{ + my $res = $node->safe_psql("postgres", qq( + SET enable_indexscan = off; + SELECT i FROM tst ORDER BY v <-> '$_' LIMIT $limit; + )); + push(@expected, $res); +} + +test_recall(0.18, $limit, "before vacuum"); +test_recall(0.95, 100, "before vacuum"); + +# TODO Test concurrent inserts with vacuum +$node->safe_psql("postgres", "VACUUM tst;"); + +test_recall(0.95, $limit, "after vacuum"); + +done_testing();