From 87ac108bf736de4ae31144ae828de82fdae740d1 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Mon, 23 Sep 2024 15:26:31 -0700 Subject: [PATCH 01/14] Removed code for Postgres 12 [skip ci] --- src/halfvec.c | 18 ------------------ src/vector.c | 18 ------------------ 2 files changed, 36 deletions(-) diff --git a/src/halfvec.c b/src/halfvec.c index 9cd3de6..aad320b 100644 --- a/src/halfvec.c +++ b/src/halfvec.c @@ -159,24 +159,6 @@ CheckStateArray(ArrayType *statearray, const char *caller) return (float8 *) ARR_DATA_PTR(statearray); } -#if PG_VERSION_NUM < 120003 -static pg_noinline void -float_overflow_error(void) -{ - ereport(ERROR, - (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), - errmsg("value out of range: overflow"))); -} - -static pg_noinline void -float_underflow_error(void) -{ - ereport(ERROR, - (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), - errmsg("value out of range: underflow"))); -} -#endif - /* * Convert textual representation to internal representation */ diff --git a/src/vector.c b/src/vector.c index facc07e..a5b2aac 100644 --- a/src/vector.c +++ b/src/vector.c @@ -155,24 +155,6 @@ CheckStateArray(ArrayType *statearray, const char *caller) return (float8 *) ARR_DATA_PTR(statearray); } -#if PG_VERSION_NUM < 120003 -static pg_noinline void -float_overflow_error(void) -{ - ereport(ERROR, - (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), - errmsg("value out of range: overflow"))); -} - -static pg_noinline void -float_underflow_error(void) -{ - ereport(ERROR, - (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), - errmsg("value out of range: underflow"))); -} -#endif - /* * Convert textual representation to internal representation */ From 62ffc3641c2e7f7a4ec4c98fd945543da5b48b99 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 24 Sep 2024 23:12:27 -0700 Subject: [PATCH 02/14] Added test for join [skip ci] --- test/t/017_hnsw_filtering.pl | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/t/017_hnsw_filtering.pl b/test/t/017_hnsw_filtering.pl index 249b32d..3d4feff 100644 --- a/test/t/017_hnsw_filtering.pl +++ b/test/t/017_hnsw_filtering.pl @@ -18,9 +18,13 @@ $node->start; # Create table and index $node->safe_psql("postgres", "CREATE EXTENSION vector;"); $node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector($dim), c int4, t text);"); +$node->safe_psql("postgres", "CREATE TABLE cat (i int4 PRIMARY KEY, t text);"); $node->safe_psql("postgres", "INSERT INTO tst SELECT i, ARRAY[$array_sql], i % $nc, 'test ' || i FROM generate_series(1, 10000) i;" ); +$node->safe_psql("postgres", + "INSERT INTO cat SELECT i, 'cat ' || i FROM generate_series(1, $nc) i;" +); $node->safe_psql("postgres", "CREATE INDEX idx ON tst USING hnsw (v vector_l2_ops);"); $node->safe_psql("postgres", "ANALYZE tst;"); @@ -96,6 +100,12 @@ $explain = $node->safe_psql("postgres", qq( )); like($explain, qr/Seq Scan/); +# Test join +$explain = $node->safe_psql("postgres", qq( + EXPLAIN ANALYZE SELECT cat.t FROM cat INNER JOIN tst ON cat.i = tst.i ORDER BY v <-> '$query' LIMIT $limit; +)); +like($explain, qr/Index Scan using idx/); + # Test attribute index $node->safe_psql("postgres", "CREATE INDEX attribute_idx ON tst (c);"); $explain = $node->safe_psql("postgres", qq( From ecd0738728792f1e09bd3e98f588322d84445efe Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 24 Sep 2024 23:13:30 -0700 Subject: [PATCH 03/14] Improved test [skip ci] --- test/t/017_hnsw_filtering.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/t/017_hnsw_filtering.pl b/test/t/017_hnsw_filtering.pl index 3d4feff..4ec8721 100644 --- a/test/t/017_hnsw_filtering.pl +++ b/test/t/017_hnsw_filtering.pl @@ -102,7 +102,7 @@ like($explain, qr/Seq Scan/); # Test join $explain = $node->safe_psql("postgres", qq( - EXPLAIN ANALYZE SELECT cat.t FROM cat INNER JOIN tst ON cat.i = tst.i ORDER BY v <-> '$query' LIMIT $limit; + EXPLAIN ANALYZE SELECT cat.t FROM cat INNER JOIN tst ON cat.i = tst.c ORDER BY v <-> '$query' LIMIT $limit; )); like($explain, qr/Index Scan using idx/); From 77b3d1f2a8e08aa6ca2545eddd9fb51796c32ea1 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 24 Sep 2024 23:21:34 -0700 Subject: [PATCH 04/14] Added test for join with attribute filtering [skip ci] --- test/t/017_hnsw_filtering.pl | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/t/017_hnsw_filtering.pl b/test/t/017_hnsw_filtering.pl index 4ec8721..0896d32 100644 --- a/test/t/017_hnsw_filtering.pl +++ b/test/t/017_hnsw_filtering.pl @@ -18,12 +18,12 @@ $node->start; # Create table and index $node->safe_psql("postgres", "CREATE EXTENSION vector;"); $node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector($dim), c int4, t text);"); -$node->safe_psql("postgres", "CREATE TABLE cat (i int4 PRIMARY KEY, t text);"); +$node->safe_psql("postgres", "CREATE TABLE cat (i int4 PRIMARY KEY, t text, b boolean);"); $node->safe_psql("postgres", "INSERT INTO tst SELECT i, ARRAY[$array_sql], i % $nc, 'test ' || i FROM generate_series(1, 10000) i;" ); $node->safe_psql("postgres", - "INSERT INTO cat SELECT i, 'cat ' || i FROM generate_series(1, $nc) i;" + "INSERT INTO cat SELECT i, 'cat ' || i, i % 5 = 0 FROM generate_series(1, $nc) i;" ); $node->safe_psql("postgres", "CREATE INDEX idx ON tst USING hnsw (v vector_l2_ops);"); $node->safe_psql("postgres", "ANALYZE tst;"); @@ -106,6 +106,12 @@ $explain = $node->safe_psql("postgres", qq( )); like($explain, qr/Index Scan using idx/); +# Test join with attribute filtering +$explain = $node->safe_psql("postgres", qq( + EXPLAIN ANALYZE SELECT cat.t FROM cat INNER JOIN tst ON cat.i = tst.c WHERE cat.b = 't' ORDER BY v <-> '$query' LIMIT $limit; +)); +like($explain, qr/Index Scan using idx/); + # Test attribute index $node->safe_psql("postgres", "CREATE INDEX attribute_idx ON tst (c);"); $explain = $node->safe_psql("postgres", qq( From 8e979ed37760b1b73e32fa149c27449bf2169da8 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 25 Sep 2024 13:48:24 -0700 Subject: [PATCH 05/14] Do not adjust index selectivity based on probes [skip ci] --- src/ivfflat.c | 7 ------- test/t/009_ivfflat_filtering.pl | 6 ++---- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/ivfflat.c b/src/ivfflat.c index 4e9b9a4..986e19d 100644 --- a/src/ivfflat.c +++ b/src/ivfflat.c @@ -120,13 +120,6 @@ ivfflatcostestimate(PlannerInfo *root, IndexPath *path, double loop_count, costs.indexTotalCost -= 0.5 * costs.numIndexPages * (costs.spc_random_page_cost - spc_seq_page_cost); } - /* - * If the list selectivity is lower than what is returned from the generic - * cost estimator, use that. - */ - if (ratio < costs.indexSelectivity) - costs.indexSelectivity = ratio; - /* Use total cost since most work happens before first tuple is returned */ *indexStartupCost = costs.indexTotalCost; *indexTotalCost = costs.indexTotalCost; diff --git a/test/t/009_ivfflat_filtering.pl b/test/t/009_ivfflat_filtering.pl index efe0866..72b2c53 100644 --- a/test/t/009_ivfflat_filtering.pl +++ b/test/t/009_ivfflat_filtering.pl @@ -94,8 +94,7 @@ like($explain, qr/Seq Scan/); $explain = $node->safe_psql("postgres", qq( EXPLAIN ANALYZE SELECT i FROM tst WHERE v <-> '$query' < 1 ORDER BY v <-> '$query'; )); -# TODO Do not use index -like($explain, qr/Index Scan using idx/); +like($explain, qr/Seq Scan/); # Test attribute index $node->safe_psql("postgres", "CREATE INDEX attribute_idx ON tst (c);"); @@ -110,7 +109,6 @@ $node->safe_psql("postgres", "CREATE INDEX partial_idx ON tst USING ivfflat (v v $explain = $node->safe_psql("postgres", qq( EXPLAIN ANALYZE SELECT i FROM tst WHERE c = $c ORDER BY v <-> '$query' LIMIT $limit; )); -# TODO Use partial index -like($explain, qr/Index Scan using idx/); +like($explain, qr/Index Scan using partial_idx/); done_testing(); From 2df9f24aadbfa4c52af01d271e6246671d00abdf Mon Sep 17 00:00:00 2001 From: "Jonathan S. Katz" Date: Wed, 25 Sep 2024 17:01:33 -0400 Subject: [PATCH 06/14] Update HNSW cost estimatation to utilize search and index info (#682) Previously, the cost estimation formula for a HNSW index scan utilized a methodology that only factored in the entry level for an HNSW scan and the "m" index parameter, which reflects the number of tuples (or vectors) to scan at each step of a HNSW graph traversal. While this would bias the PostgreSQL query planner to choose an HNSW index scan over other available paths, this could lead to potential suboptimal index selection, for example, choosing to use a HNSW index instead of an available B-tree index that has better selectivity. The number of tuples scanned during HNSW graph traversal is principally influenced by these factors: * The number of tuples stored in the index * `m` - the number of tuples that are scanned in each step of the graph traversal * `hnsw.ef_search` - which influences the total number of steps it takes for the scan to converge on the approximated nearest neighbors Through testing different source models for vectors, we also observed that the correlation of vectors in mdoels would impact this convergence. For this first iteration, we've opted to hardcode a constant scaling factor and set it to `0.55`, though a future commit may turn this into a configurable parameter. The high-level formula for estimating the cost of a HNSW index scan is as such: ``` (entryLevel * m) + (layer0TuplesMax * layer0Selectivity) ``` where - `(entryLevel * m)` is the lower bound of tuples to scan, as it accounts for the graph traversal to layer 0 (L0). (L1 and above has an ef=1) - `layer0TuplesMax` is an estimate of the maximum number of tuples to scan at L0. This accounts for tuples that may end up being discarded due to them already being visited. Testing shows that the number of steps until converge is similar to the value of `hnsw.ef_search`, thus we can estimate tuples max at `hnsw.ef_search * m * 2` - `layer0Selectivity` - estimates the percentage of tuples that will actually be scanned during the index traversal, multipled by the scaling factor In addition to the `m` build parameter and `hsnw.ef_search`, costs estimates can be influenced by standard PostgreSQL costing parameters, though adjusting those (e.g. `random_page_cost`) should be done with care. Co-authored-by: @ankane --- src/hnsw.c | 45 +++++++++++++++++++++++++++++++----- test/t/017_hnsw_filtering.pl | 4 ++-- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/hnsw.c b/src/hnsw.c index a7b1e5f..765392e 100644 --- a/src/hnsw.c +++ b/src/hnsw.c @@ -99,7 +99,10 @@ hnswcostestimate(PlannerInfo *root, IndexPath *path, double loop_count, { GenericCosts costs; int m; - int entryLevel; + int entryLevel; + int layer0TuplesMax; + double layer0Selectivity; + double scalingFactor = 0.55; Relation index; /* Never use index without order */ @@ -119,12 +122,42 @@ hnswcostestimate(PlannerInfo *root, IndexPath *path, double loop_count, HnswGetMetaPageInfo(index, &m, NULL); index_close(index, NoLock); - /* Approximate entry level */ - entryLevel = (int) -log(1.0 / path->indexinfo->tuples) * HnswGetMl(m); + /* + * HNSW cost estimation follows a formula that accounts for the total + * number of tuples indexed combined with the parameters that most influence + * the duration of the index scan, namely: + * m - the number of tuples that are scanned in each step of the HNSW + * graph traversal + * ef_search - which influences the total number of steps taken at layer 0 + * + * The source of the vector data can impact how many steps it takes to + * converge on the set of vectors to return to the executor. Currently, + * we use a hardcoded scaling factor (HNSWScanScalingFactor) to help + * influence that, but this could later become a configurable parameter + * based on the cost estimations. + * + * The tuple estimator formula is below: + * + * numIndexTuples = (entryLevel * m) + + * (layer0TuplesMax * layer0Selectivity) + * + * "entryLevel * m" represents the floor of tuples we need to scan to get + * to layer 0 (L0). + * + * "layer0TuplesMax" is the estimated total number of tuples we'd scan at + * L0 if we weren't discarding already visited tuples as part of the scan. + * + * "layer0Selectivity" estimates the percentage of tuples that are scanned + * at L0, accounting for previously visited tuples, multiplied by the + * "scalingFactor" (currently hardcoded). + */ + entryLevel = (int) floor(log(path->indexinfo->tuples + 1) * HnswGetMl(m)); + layer0TuplesMax = HnswGetLayerM(m, 0) * hnsw_ef_search; + layer0Selectivity = (scalingFactor * log(path->indexinfo->tuples + 1)) / + (log(m) * (1 + log(hnsw_ef_search))); - /* TODO Improve estimate of visited tuples (currently underestimates) */ - /* Account for number of tuples (or entry level), m, and ef_search */ - costs.numIndexTuples = (entryLevel + 2) * m; + costs.numIndexTuples = (entryLevel * m) + + (layer0TuplesMax * layer0Selectivity); genericcostestimate(root, path, loop_count, &costs); diff --git a/test/t/017_hnsw_filtering.pl b/test/t/017_hnsw_filtering.pl index 0896d32..9dbdcf3 100644 --- a/test/t/017_hnsw_filtering.pl +++ b/test/t/017_hnsw_filtering.pl @@ -117,8 +117,8 @@ $node->safe_psql("postgres", "CREATE INDEX attribute_idx ON tst (c);"); $explain = $node->safe_psql("postgres", qq( EXPLAIN ANALYZE SELECT i FROM tst WHERE c = $c ORDER BY v <-> '$query' LIMIT $limit; )); -# TODO Use attribute index -like($explain, qr/Index Scan using idx/); +# Use attribute index +like($explain, qr/Bitmap Index Scan on attribute_idx/); # Test partial index $node->safe_psql("postgres", "CREATE INDEX partial_idx ON tst USING hnsw (v vector_l2_ops) WHERE (c = $c);"); From a100dc67e56e9395b929917ebc1851a38ce5bed4 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 25 Sep 2024 14:03:51 -0700 Subject: [PATCH 07/14] Ran pgindent [skip ci] --- src/hnsw.c | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/hnsw.c b/src/hnsw.c index 765392e..2cc857d 100644 --- a/src/hnsw.c +++ b/src/hnsw.c @@ -99,8 +99,8 @@ hnswcostestimate(PlannerInfo *root, IndexPath *path, double loop_count, { GenericCosts costs; int m; - int entryLevel; - int layer0TuplesMax; + int entryLevel; + int layer0TuplesMax; double layer0Selectivity; double scalingFactor = 0.55; Relation index; @@ -124,22 +124,21 @@ hnswcostestimate(PlannerInfo *root, IndexPath *path, double loop_count, /* * HNSW cost estimation follows a formula that accounts for the total - * number of tuples indexed combined with the parameters that most influence - * the duration of the index scan, namely: - * m - the number of tuples that are scanned in each step of the HNSW - * graph traversal - * ef_search - which influences the total number of steps taken at layer 0 + * number of tuples indexed combined with the parameters that most + * influence the duration of the index scan, namely: m - the number of + * tuples that are scanned in each step of the HNSW graph traversal + * ef_search - which influences the total number of steps taken at layer 0 * - * The source of the vector data can impact how many steps it takes to - * converge on the set of vectors to return to the executor. Currently, - * we use a hardcoded scaling factor (HNSWScanScalingFactor) to help - * influence that, but this could later become a configurable parameter - * based on the cost estimations. + * The source of the vector data can impact how many steps it takes to + * converge on the set of vectors to return to the executor. Currently, we + * use a hardcoded scaling factor (HNSWScanScalingFactor) to help + * influence that, but this could later become a configurable parameter + * based on the cost estimations. * * The tuple estimator formula is below: * - * numIndexTuples = (entryLevel * m) + - * (layer0TuplesMax * layer0Selectivity) + * numIndexTuples = (entryLevel * m) + (layer0TuplesMax * + * layer0Selectivity) * * "entryLevel * m" represents the floor of tuples we need to scan to get * to layer 0 (L0). @@ -153,7 +152,7 @@ hnswcostestimate(PlannerInfo *root, IndexPath *path, double loop_count, */ entryLevel = (int) floor(log(path->indexinfo->tuples + 1) * HnswGetMl(m)); layer0TuplesMax = HnswGetLayerM(m, 0) * hnsw_ef_search; - layer0Selectivity = (scalingFactor * log(path->indexinfo->tuples + 1)) / + layer0Selectivity = (scalingFactor * log(path->indexinfo->tuples + 1)) / (log(m) * (1 + log(hnsw_ef_search))); costs.numIndexTuples = (entryLevel * m) + From 1370dd6e86c1f903cdfe6079ae233adb0f87534d Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 25 Sep 2024 14:13:02 -0700 Subject: [PATCH 08/14] Removed unneeded floor and fixed comment formatting [skip ci] --- src/hnsw.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/hnsw.c b/src/hnsw.c index 2cc857d..86c0ebc 100644 --- a/src/hnsw.c +++ b/src/hnsw.c @@ -137,8 +137,7 @@ hnswcostestimate(PlannerInfo *root, IndexPath *path, double loop_count, * * The tuple estimator formula is below: * - * numIndexTuples = (entryLevel * m) + (layer0TuplesMax * - * layer0Selectivity) + * numIndexTuples = entryLevel * m + layer0TuplesMax * layer0Selectivity * * "entryLevel * m" represents the floor of tuples we need to scan to get * to layer 0 (L0). @@ -150,7 +149,7 @@ hnswcostestimate(PlannerInfo *root, IndexPath *path, double loop_count, * at L0, accounting for previously visited tuples, multiplied by the * "scalingFactor" (currently hardcoded). */ - entryLevel = (int) floor(log(path->indexinfo->tuples + 1) * HnswGetMl(m)); + entryLevel = (int) (log(path->indexinfo->tuples + 1) * HnswGetMl(m)); layer0TuplesMax = HnswGetLayerM(m, 0) * hnsw_ef_search; layer0Selectivity = (scalingFactor * log(path->indexinfo->tuples + 1)) / (log(m) * (1 + log(hnsw_ef_search))); From 242a12b7d5afe1d631d1c6ff727a691353579d21 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 25 Sep 2024 15:33:57 -0700 Subject: [PATCH 09/14] Added same cost adjustment to HNSW as IVFFlat since TOAST not included in seq scan cost - #682 [skip ci] --- src/hnsw.c | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/hnsw.c b/src/hnsw.c index 86c0ebc..64ff080 100644 --- a/src/hnsw.c +++ b/src/hnsw.c @@ -12,6 +12,7 @@ #include "utils/float.h" #include "utils/guc.h" #include "utils/selfuncs.h" +#include "utils/spccache.h" #if PG_VERSION_NUM < 150000 #define MarkGUCPrefixReserved(x) EmitWarningsOnPlaceholders(x) @@ -103,6 +104,7 @@ hnswcostestimate(PlannerInfo *root, IndexPath *path, double loop_count, int layer0TuplesMax; double layer0Selectivity; double scalingFactor = 0.55; + double spc_seq_page_cost; Relation index; /* Never use index without order */ @@ -159,6 +161,23 @@ hnswcostestimate(PlannerInfo *root, IndexPath *path, double loop_count, genericcostestimate(root, path, loop_count, &costs); + get_tablespace_page_costs(path->indexinfo->reltablespace, NULL, &spc_seq_page_cost); + + /* Adjust cost if needed since TOAST not included in seq scan cost */ + if (costs.numIndexPages > path->indexinfo->rel->pages) + { + /* Change all page cost from random to sequential */ + costs.indexTotalCost -= costs.numIndexPages * (costs.spc_random_page_cost - spc_seq_page_cost); + + /* Remove cost of extra pages */ + costs.indexTotalCost -= (costs.numIndexPages - path->indexinfo->rel->pages) * spc_seq_page_cost; + } + else + { + /* Change some page cost from random to sequential */ + costs.indexTotalCost -= 0.5 * costs.numIndexPages * (costs.spc_random_page_cost - spc_seq_page_cost); + } + /* Use total cost since most work happens before first tuple is returned */ *indexStartupCost = costs.indexTotalCost; *indexTotalCost = costs.indexTotalCost; From 5776a4d937614cd453b15ccf067d6056d88566b5 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 25 Sep 2024 15:39:56 -0700 Subject: [PATCH 10/14] Only adjust for TOAST [skip ci] --- src/hnsw.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/hnsw.c b/src/hnsw.c index 64ff080..0b6640a 100644 --- a/src/hnsw.c +++ b/src/hnsw.c @@ -172,11 +172,6 @@ hnswcostestimate(PlannerInfo *root, IndexPath *path, double loop_count, /* Remove cost of extra pages */ costs.indexTotalCost -= (costs.numIndexPages - path->indexinfo->rel->pages) * spc_seq_page_cost; } - else - { - /* Change some page cost from random to sequential */ - costs.indexTotalCost -= 0.5 * costs.numIndexPages * (costs.spc_random_page_cost - spc_seq_page_cost); - } /* Use total cost since most work happens before first tuple is returned */ *indexStartupCost = costs.indexTotalCost; From e0ad441306fa82638136425a2aeb5035e341350a Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 25 Sep 2024 15:49:03 -0700 Subject: [PATCH 11/14] Added test for costs [skip ci] --- test/t/039_hnsw_cost.pl | 46 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 test/t/039_hnsw_cost.pl diff --git a/test/t/039_hnsw_cost.pl b/test/t/039_hnsw_cost.pl new file mode 100644 index 0000000..dad50cc --- /dev/null +++ b/test/t/039_hnsw_cost.pl @@ -0,0 +1,46 @@ +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my @dims = (384, 1536); +my $limit = 10; + +# Initialize node +my $node = PostgreSQL::Test::Cluster->new('node'); +$node->init; +$node->start; + +$node->safe_psql("postgres", "CREATE EXTENSION vector;"); + +for my $dim (@dims) +{ + my $array_sql = join(",", ('random()') x $dim); + my $n = $dim == 384 ? 2000 : 1000; + + # Create table and index + $node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector($dim));"); + $node->safe_psql("postgres", + "INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(1, $n) i;" + ); + $node->safe_psql("postgres", "CREATE INDEX idx ON tst USING hnsw (v vector_l2_ops);"); + $node->safe_psql("postgres", "ANALYZE tst;"); + + # Generate query + my @r = (); + for (1 .. $dim) + { + push(@r, rand()); + } + my $query = "[" . join(",", @r) . "]"; + + my $explain = $node->safe_psql("postgres", qq( + EXPLAIN ANALYZE SELECT i FROM tst ORDER BY v <-> '$query' LIMIT $limit; + )); + like($explain, qr/Index Scan using idx/); + + $node->safe_psql("postgres", "DROP TABLE tst;"); +} + +done_testing(); From 2d85af51a80734c8d9baa3500db2b2bde2780540 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 25 Sep 2024 15:53:34 -0700 Subject: [PATCH 12/14] Added test for IVFFlat costs [skip ci] --- test/t/040_ivfflat_cost.pl | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 test/t/040_ivfflat_cost.pl diff --git a/test/t/040_ivfflat_cost.pl b/test/t/040_ivfflat_cost.pl new file mode 100644 index 0000000..8042e4d --- /dev/null +++ b/test/t/040_ivfflat_cost.pl @@ -0,0 +1,45 @@ +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my @dims = (384, 1536); +my $limit = 10; + +# Initialize node +my $node = PostgreSQL::Test::Cluster->new('node'); +$node->init; +$node->start; + +$node->safe_psql("postgres", "CREATE EXTENSION vector;"); + +for my $dim (@dims) +{ + my $array_sql = join(",", ('random()') x $dim); + + # Create table and index + $node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector($dim));"); + $node->safe_psql("postgres", + "INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(1, 10000) i;" + ); + $node->safe_psql("postgres", "CREATE INDEX idx ON tst USING ivfflat (v vector_l2_ops) WITH (lists = 10);"); + $node->safe_psql("postgres", "ANALYZE tst;"); + + # Generate query + my @r = (); + for (1 .. $dim) + { + push(@r, rand()); + } + my $query = "[" . join(",", @r) . "]"; + + my $explain = $node->safe_psql("postgres", qq( + EXPLAIN ANALYZE SELECT i FROM tst ORDER BY v <-> '$query' LIMIT $limit; + )); + like($explain, qr/Index Scan using idx/); + + $node->safe_psql("postgres", "DROP TABLE tst;"); +} + +done_testing(); From b8c27914d4baad5ca909f7a170dd35a9c84a13c0 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 25 Sep 2024 15:58:41 -0700 Subject: [PATCH 13/14] Improved test [skip ci] --- test/t/040_ivfflat_cost.pl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/t/040_ivfflat_cost.pl b/test/t/040_ivfflat_cost.pl index 8042e4d..2f6fbf9 100644 --- a/test/t/040_ivfflat_cost.pl +++ b/test/t/040_ivfflat_cost.pl @@ -21,9 +21,9 @@ for my $dim (@dims) # Create table and index $node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector($dim));"); $node->safe_psql("postgres", - "INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(1, 10000) i;" + "INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(1, 5000) i;" ); - $node->safe_psql("postgres", "CREATE INDEX idx ON tst USING ivfflat (v vector_l2_ops) WITH (lists = 10);"); + $node->safe_psql("postgres", "CREATE INDEX idx ON tst USING ivfflat (v vector_l2_ops) WITH (lists = 5);"); $node->safe_psql("postgres", "ANALYZE tst;"); # Generate query From 46de265a2451970e913c4cf76e42a8fadb1e939e Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 25 Sep 2024 16:03:58 -0700 Subject: [PATCH 14/14] Updated changelog [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index db6798c..e5a17b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## 0.8.0 (unreleased) - Added casts for arrays to `sparsevec` +- Improved cost estimation - Reduced memory usage for HNSW index scans - Dropped support for Postgres 12