From 87ac108bf736de4ae31144ae828de82fdae740d1 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Mon, 23 Sep 2024 15:26:31 -0700 Subject: [PATCH 01/88] 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/88] 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/88] 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/88] 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/88] 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/88] 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/88] 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/88] 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/88] 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/88] 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/88] 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/88] 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/88] 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/88] 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 From 54fa16e3e35683f65bbb7277363451f9f2b7c8e5 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 26 Sep 2024 08:32:44 -0700 Subject: [PATCH 15/88] Added safety check [skip ci] --- src/hnswutils.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/hnswutils.c b/src/hnswutils.c index ac1e7de..d807248 100644 --- a/src/hnswutils.c +++ b/src/hnswutils.c @@ -759,9 +759,16 @@ HnswLoadUnvisitedFromDisk(HnswElement element, HnswUnvisited * unvisited, int *u page = BufferGetPage(buf); ntup = (HnswNeighborTuple) PageGetItem(page, PageGetItemId(page, element->neighborOffno)); - start = (element->level - lc) * m; + + /* Ensure expected neighbors */ + if (ntup->count != (element->level + 2) * m) + { + UnlockReleaseBuffer(buf); + return; + } /* Copy to minimize lock time */ + start = (element->level - lc) * m; memcpy(&indextids, ntup->indextids + start, lm * sizeof(ItemPointerData)); UnlockReleaseBuffer(buf); From 44d8d28b40ddaca6b79ecf598c42996f5ad71ac5 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 27 Sep 2024 13:39:54 -0700 Subject: [PATCH 16/88] Added note about postgresql@17 formula [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7439964..c832e9e 100644 --- a/README.md +++ b/README.md @@ -1064,7 +1064,7 @@ With Homebrew Postgres, you can use: brew install pgvector ``` -Note: This only adds it to the `postgresql@14` formula +Note: This only adds it to the `postgresql@17` and `postgresql@14` formulas ### PGXN From 74020a90dac83024aafc9593985f5beacce813de Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 27 Sep 2024 13:49:43 -0700 Subject: [PATCH 17/88] Updated package versions in readme [skip ci] --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c832e9e..6e3af41 100644 --- a/README.md +++ b/README.md @@ -1079,29 +1079,29 @@ pgxn install vector Debian and Ubuntu packages are available from the [PostgreSQL APT Repository](https://wiki.postgresql.org/wiki/Apt). Follow the [setup instructions](https://wiki.postgresql.org/wiki/Apt#Quickstart) and run: ```sh -sudo apt install postgresql-16-pgvector +sudo apt install postgresql-17-pgvector ``` -Note: Replace `16` with your Postgres server version +Note: Replace `17` with your Postgres server version ### Yum RPM packages are available from the [PostgreSQL Yum Repository](https://yum.postgresql.org/). Follow the [setup instructions](https://www.postgresql.org/download/linux/redhat/) for your distribution and run: ```sh -sudo yum install pgvector_16 +sudo yum install pgvector_17 # or -sudo dnf install pgvector_16 +sudo dnf install pgvector_17 ``` -Note: Replace `16` with your Postgres server version +Note: Replace `17` with your Postgres server version ### pkg Install the FreeBSD package with: ```sh -pkg install postgresql15-pgvector +pkg install postgresql16-pgvector ``` or the port with: From 2bca4e406b19108d5525537f30e92b2e781b078b Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 27 Sep 2024 13:50:57 -0700 Subject: [PATCH 18/88] Restored quarterly package version for FreeBSD in readme [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6e3af41..14ab90a 100644 --- a/README.md +++ b/README.md @@ -1101,7 +1101,7 @@ Note: Replace `17` with your Postgres server version Install the FreeBSD package with: ```sh -pkg install postgresql16-pgvector +pkg install postgresql15-pgvector ``` or the port with: From daf9c5c7431d85667151622a26bba6889d03fb83 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 27 Sep 2024 13:52:11 -0700 Subject: [PATCH 19/88] Updated package versions in readme [skip ci] --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 14ab90a..29b7662 100644 --- a/README.md +++ b/README.md @@ -983,7 +983,7 @@ l2_normalize(sparsevec) → sparsevec | Normalize with Euclidean norm | 0.7.0 If your machine has multiple Postgres installations, specify the path to [pg_config](https://www.postgresql.org/docs/current/app-pgconfig.html) with: ```sh -export PG_CONFIG=/Library/PostgreSQL/16/bin/pg_config +export PG_CONFIG=/Library/PostgreSQL/17/bin/pg_config ``` Then re-run the installation instructions (run `make clean` before `make` if needed). If `sudo` is needed for `make install`, use: @@ -994,11 +994,11 @@ sudo --preserve-env=PG_CONFIG make install A few common paths on Mac are: -- EDB installer - `/Library/PostgreSQL/16/bin/pg_config` -- Homebrew (arm64) - `/opt/homebrew/opt/postgresql@16/bin/pg_config` -- Homebrew (x86-64) - `/usr/local/opt/postgresql@16/bin/pg_config` +- EDB installer - `/Library/PostgreSQL/17/bin/pg_config` +- Homebrew (arm64) - `/opt/homebrew/opt/postgresql@17/bin/pg_config` +- Homebrew (x86-64) - `/usr/local/opt/postgresql@17/bin/pg_config` -Note: Replace `16` with your Postgres server version +Note: Replace `17` with your Postgres server version ### Missing Header @@ -1007,10 +1007,10 @@ If compilation fails with `fatal error: postgres.h: No such file or directory`, For Ubuntu and Debian, use: ```sh -sudo apt install postgresql-server-dev-16 +sudo apt install postgresql-server-dev-17 ``` -Note: Replace `16` with your Postgres server version +Note: Replace `17` with your Postgres server version ### Missing SDK From 8a2eebd6a4a3c5ede931931006d602441e4f1422 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 27 Sep 2024 14:05:36 -0700 Subject: [PATCH 20/88] Added note about Postgres 17 on Windows - #669 [skip ci] --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 29b7662..f4f1d33 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,8 @@ nmake /F Makefile.win nmake /F Makefile.win install ``` +Note: Postgres 17 is not supported yet due to an upstream issue + See the [installation notes](#installation-notes---windows) if you run into issues You can also install it with [Docker](#docker) or [conda-forge](#conda-forge). From cf419f448bc4b28c905c244220427bcad01bbb21 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 27 Sep 2024 16:19:06 -0700 Subject: [PATCH 21/88] Updated Postgres version for Docker [skip ci] --- Makefile | 2 +- README.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index e7ae85e..2c85e10 100644 --- a/Makefile +++ b/Makefile @@ -66,7 +66,7 @@ dist: git archive --format zip --prefix=$(EXTENSION)-$(EXTVERSION)/ --output dist/$(EXTENSION)-$(EXTVERSION).zip master # for Docker -PG_MAJOR ?= 16 +PG_MAJOR ?= 17 .PHONY: docker diff --git a/README.md b/README.md index f4f1d33..d6a1346 100644 --- a/README.md +++ b/README.md @@ -1045,17 +1045,17 @@ If installation fails with `Access is denied`, re-run the installation instructi Get the [Docker image](https://hub.docker.com/r/pgvector/pgvector) with: ```sh -docker pull pgvector/pgvector:pg16 +docker pull pgvector/pgvector:pg17 ``` -This adds pgvector to the [Postgres image](https://hub.docker.com/_/postgres) (replace `16` with your Postgres server version, and run it the same way). +This adds pgvector to the [Postgres image](https://hub.docker.com/_/postgres) (replace `17` with your Postgres server version, and run it the same way). You can also build the image manually: ```sh git clone --branch v0.7.4 https://github.com/pgvector/pgvector.git cd pgvector -docker build --pull --build-arg PG_MAJOR=16 -t myuser/pgvector . +docker build --pull --build-arg PG_MAJOR=17 -t myuser/pgvector . ``` ### Homebrew From 54f8d9733d505086739748e93cc268bcbedc0676 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 27 Sep 2024 16:19:57 -0700 Subject: [PATCH 22/88] Updated default Postgres version in Dockerfile [skip ci] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index eef0c8f..9364409 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG PG_MAJOR=16 +ARG PG_MAJOR=17 FROM postgres:$PG_MAJOR ARG PG_MAJOR From 5ee0471ead6c72815fa538da3248e5fa225f8143 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 28 Sep 2024 09:23:41 -0700 Subject: [PATCH 23/88] Updated readme [skip ci] --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index d6a1346..6cdca4f 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,8 @@ Or add a vector column to an existing table ALTER TABLE items ADD COLUMN embedding vector(3); ``` +Also supports [half-precision](#half-precision-vectors), [binary](#binary-vectors), and [sparse](#sparse-vectors) vectors + Insert vectors ```sql @@ -147,6 +149,8 @@ Supported distance functions are: - `<#>` - (negative) inner product - `<=>` - cosine distance - `<+>` - L1 distance (added in 0.7.0) +- `<~>` - Hamming distance (binary vectors, added in 0.7.0) +- `<%>` - Jaccard distance (binary vectors, added in 0.7.0) Get the nearest neighbors to a row From 158d9340bca5796d8f98182e5a65356bac676b74 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 28 Sep 2024 14:50:23 -0700 Subject: [PATCH 24/88] Added distance filters to cost tests [skip ci] --- test/t/039_hnsw_cost.pl | 7 ++++++- test/t/040_ivfflat_cost.pl | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/test/t/039_hnsw_cost.pl b/test/t/039_hnsw_cost.pl index dad50cc..a26c09a 100644 --- a/test/t/039_hnsw_cost.pl +++ b/test/t/039_hnsw_cost.pl @@ -17,7 +17,7 @@ $node->safe_psql("postgres", "CREATE EXTENSION vector;"); for my $dim (@dims) { my $array_sql = join(",", ('random()') x $dim); - my $n = $dim == 384 ? 2000 : 1000; + my $n = $dim == 384 ? 3000 : 1000; # Create table and index $node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector($dim));"); @@ -40,6 +40,11 @@ for my $dim (@dims) )); like($explain, qr/Index Scan using idx/); + $explain = $node->safe_psql("postgres", qq( + EXPLAIN ANALYZE SELECT i FROM tst WHERE v <-> '$query' < 1 ORDER BY v <-> '$query' LIMIT $limit; + )); + like($explain, qr/Index Scan using idx/); + $node->safe_psql("postgres", "DROP TABLE tst;"); } diff --git a/test/t/040_ivfflat_cost.pl b/test/t/040_ivfflat_cost.pl index 2f6fbf9..1c311a3 100644 --- a/test/t/040_ivfflat_cost.pl +++ b/test/t/040_ivfflat_cost.pl @@ -39,6 +39,11 @@ for my $dim (@dims) )); like($explain, qr/Index Scan using idx/); + $explain = $node->safe_psql("postgres", qq( + EXPLAIN ANALYZE SELECT i FROM tst WHERE v <-> '$query' < 1 ORDER BY v <-> '$query' LIMIT $limit; + )); + like($explain, qr/Index Scan using idx/); + $node->safe_psql("postgres", "DROP TABLE tst;"); } From dc23752618e7719317b2a852cdd92e51e83b1af5 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 28 Sep 2024 19:18:52 -0700 Subject: [PATCH 25/88] Fixed uninitialized variable [skip ci] --- src/hnswutils.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hnswutils.c b/src/hnswutils.c index d807248..c76c6bb 100644 --- a/src/hnswutils.c +++ b/src/hnswutils.c @@ -754,6 +754,8 @@ HnswLoadUnvisitedFromDisk(HnswElement element, HnswUnvisited * unvisited, int *u int start; ItemPointerData indextids[HNSW_MAX_M * 2]; + *unvisitedLength = 0; + buf = ReadBuffer(index, element->neighborPage); LockBuffer(buf, BUFFER_LOCK_SHARE); page = BufferGetPage(buf); @@ -773,8 +775,6 @@ HnswLoadUnvisitedFromDisk(HnswElement element, HnswUnvisited * unvisited, int *u UnlockReleaseBuffer(buf); - *unvisitedLength = 0; - for (int i = 0; i < lm; i++) { ItemPointer indextid = &indextids[i]; From cae3458329518d30ce4e7513ba26eb9359dabcb6 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 29 Sep 2024 15:06:50 -0700 Subject: [PATCH 26/88] Updated distance to use double --- src/hnsw.h | 4 ++-- src/hnswutils.c | 17 +++++++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/hnsw.h b/src/hnsw.h index 9fb650a..b57e9f8 100644 --- a/src/hnsw.h +++ b/src/hnsw.h @@ -160,7 +160,7 @@ typedef struct HnswSearchCandidate pairingheap_node c_node; pairingheap_node w_node; HnswElementPtr element; - float distance; + double distance; } HnswSearchCandidate; /* HNSW index options */ @@ -390,7 +390,7 @@ void HnswInitNeighbors(char *base, HnswElement element, int m, HnswAllocator * bool HnswInsertTupleOnDisk(Relation index, Datum value, Datum *values, bool *isnull, ItemPointer heap_tid, bool building); void HnswUpdateNeighborsOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, HnswElement e, int m, bool checkExisting, bool building); void HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool loadHeaptids, bool loadVec); -void HnswLoadElement(HnswElement element, float *distance, Datum *q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec, float *maxDistance); +void HnswLoadElement(HnswElement element, double *distance, Datum *q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec, double *maxDistance); void HnswSetElementTuple(char *base, HnswElementTuple etup, HnswElement element); void HnswUpdateConnection(char *base, HnswElement element, HnswCandidate * hc, int lm, int lc, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation); void HnswLoadNeighbors(HnswElement element, Relation index, int m); diff --git a/src/hnswutils.c b/src/hnswutils.c index c76c6bb..0a8d847 100644 --- a/src/hnswutils.c +++ b/src/hnswutils.c @@ -549,7 +549,7 @@ HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool loadHe * Load an element and optionally get its distance from q */ static void -HnswLoadElementImpl(BlockNumber blkno, OffsetNumber offno, float *distance, Datum *q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec, float *maxDistance, HnswElement * element) +HnswLoadElementImpl(BlockNumber blkno, OffsetNumber offno, double *distance, Datum *q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec, double *maxDistance, HnswElement * element) { Buffer buf; Page page; @@ -570,7 +570,7 @@ HnswLoadElementImpl(BlockNumber blkno, OffsetNumber offno, float *distance, Datu if (DatumGetPointer(*q) == NULL) *distance = 0; else - *distance = (float) DatumGetFloat8(FunctionCall2Coll(procinfo, collation, *q, PointerGetDatum(&etup->data))); + *distance = DatumGetFloat8(FunctionCall2Coll(procinfo, collation, *q, PointerGetDatum(&etup->data))); } /* Load element */ @@ -589,7 +589,7 @@ HnswLoadElementImpl(BlockNumber blkno, OffsetNumber offno, float *distance, Datu * Load an element and optionally get its distance from q */ void -HnswLoadElement(HnswElement element, float *distance, Datum *q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec, float *maxDistance) +HnswLoadElement(HnswElement element, double *distance, Datum *q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec, double *maxDistance) { HnswLoadElementImpl(element->blkno, element->offno, distance, q, index, procinfo, collation, loadVec, maxDistance, &element); } @@ -597,7 +597,7 @@ HnswLoadElement(HnswElement element, float *distance, Datum *q, Relation index, /* * Get the distance for an element */ -static float +static double GetElementDistance(char *base, HnswElement element, Datum q, FmgrInfo *procinfo, Oid collation) { Datum value = HnswGetValue(base, element); @@ -857,7 +857,7 @@ HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, F { HnswElement eElement; HnswSearchCandidate *e; - float eDistance; + double eDistance; bool alwaysAdd = wlen < ef; f = HnswGetSearchCandidate(w_node, pairingheap_first(W)); @@ -1163,7 +1163,12 @@ HnswUpdateConnection(char *base, HnswElement element, HnswCandidate * hc, int lm HnswElement hc3Element = HnswPtrAccess(base, hc3->element); if (HnswPtrIsNull(base, hc3Element->value)) - HnswLoadElement(hc3Element, &hc3->distance, &q, index, procinfo, collation, true, NULL); + { + double distance; + + HnswLoadElement(hc3Element, &distance, &q, index, procinfo, collation, true, NULL); + hc3->distance = distance; + } else hc3->distance = GetElementDistance(base, hc3Element, q, procinfo, collation); From f2afd1125752d45777cf738856f3985e8fe9b2e9 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 29 Sep 2024 15:09:54 -0700 Subject: [PATCH 27/88] Use sc for search candidates [skip ci] --- src/hnswutils.c | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/hnswutils.c b/src/hnswutils.c index 0a8d847..79bc086 100644 --- a/src/hnswutils.c +++ b/src/hnswutils.c @@ -611,14 +611,14 @@ GetElementDistance(char *base, HnswElement element, Datum q, FmgrInfo *procinfo, HnswSearchCandidate * HnswEntryCandidate(char *base, HnswElement entryPoint, Datum q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec) { - HnswSearchCandidate *hc = palloc(sizeof(HnswSearchCandidate)); + HnswSearchCandidate *sc = palloc(sizeof(HnswSearchCandidate)); - HnswPtrStore(base, hc->element, entryPoint); + HnswPtrStore(base, sc->element, entryPoint); if (index == NULL) - hc->distance = GetElementDistance(base, entryPoint, q, procinfo, collation); + sc->distance = GetElementDistance(base, entryPoint, q, procinfo, collation); else - HnswLoadElement(entryPoint, &hc->distance, &q, index, procinfo, collation, loadVec, NULL); - return hc; + HnswLoadElement(entryPoint, &sc->distance, &q, index, procinfo, collation, loadVec, NULL); + return sc; } #define HnswGetSearchCandidate(membername, ptr) pairingheap_container(HnswSearchCandidate, membername, ptr) @@ -820,20 +820,20 @@ HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, F /* Add entry points to v, C, and W */ foreach(lc2, ep) { - HnswSearchCandidate *hc = (HnswSearchCandidate *) lfirst(lc2); + HnswSearchCandidate *sc = (HnswSearchCandidate *) lfirst(lc2); bool found; - AddToVisited(base, &v, hc->element, index, &found); + AddToVisited(base, &v, sc->element, index, &found); - pairingheap_add(C, &hc->c_node); - pairingheap_add(W, &hc->w_node); + pairingheap_add(C, &sc->c_node); + pairingheap_add(W, &sc->w_node); /* * Do not count elements being deleted towards ef when vacuuming. It * would be ideal to do this for inserts as well, but this could * affect insert performance. */ - if (CountElement(skipElement, HnswPtrAccess(base, hc->element))) + if (CountElement(skipElement, HnswPtrAccess(base, sc->element))) wlen++; } @@ -916,9 +916,9 @@ HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, F /* Add each element of W to w */ while (!pairingheap_is_empty(W)) { - HnswSearchCandidate *hc = HnswGetSearchCandidate(w_node, pairingheap_remove_first(W)); + HnswSearchCandidate *sc = HnswGetSearchCandidate(w_node, pairingheap_remove_first(W)); - w = lappend(w, hc); + w = lappend(w, sc); } return w; From 0b6214aad6b3f5e82b8de81472e92ae5b313df56 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 29 Sep 2024 15:49:01 -0700 Subject: [PATCH 28/88] Moved HnswLoadNeighbors to hnswinsert.c [skip ci] --- src/hnsw.h | 1 - src/hnswinsert.c | 63 ++++++++++++++++++++++++++++++++++++++++++++++++ src/hnswutils.c | 63 ------------------------------------------------ 3 files changed, 63 insertions(+), 64 deletions(-) diff --git a/src/hnsw.h b/src/hnsw.h index b57e9f8..cfbcd3f 100644 --- a/src/hnsw.h +++ b/src/hnsw.h @@ -393,7 +393,6 @@ void HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool void HnswLoadElement(HnswElement element, double *distance, Datum *q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec, double *maxDistance); void HnswSetElementTuple(char *base, HnswElementTuple etup, HnswElement element); void HnswUpdateConnection(char *base, HnswElement element, HnswCandidate * hc, int lm, int lc, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation); -void HnswLoadNeighbors(HnswElement element, Relation index, int m); void HnswInitLockTranche(void); const HnswTypeInfo *HnswGetTypeInfo(Relation index); PGDLLEXPORT void HnswParallelBuildMain(dsm_segment *seg, shm_toc *toc); diff --git a/src/hnswinsert.c b/src/hnswinsert.c index 2dce16f..42d7378 100644 --- a/src/hnswinsert.c +++ b/src/hnswinsert.c @@ -334,6 +334,69 @@ AddElementOnDisk(Relation index, HnswElement e, int m, BlockNumber insertPage, B *updatedInsertPage = newInsertPage; } +/* + * Load neighbors from page + */ +static void +LoadNeighborsFromPage(HnswElement element, Relation index, Page page, int m) +{ + char *base = NULL; + + HnswNeighborTuple ntup = (HnswNeighborTuple) PageGetItem(page, PageGetItemId(page, element->neighborOffno)); + int neighborCount = (element->level + 2) * m; + + Assert(HnswIsNeighborTuple(ntup)); + + HnswInitNeighbors(base, element, m, NULL); + + /* Ensure expected neighbors */ + if (ntup->count != neighborCount) + return; + + for (int i = 0; i < neighborCount; i++) + { + HnswElement e; + int level; + HnswCandidate *hc; + ItemPointer indextid; + HnswNeighborArray *neighbors; + + indextid = &ntup->indextids[i]; + + if (!ItemPointerIsValid(indextid)) + continue; + + e = HnswInitElementFromBlock(ItemPointerGetBlockNumber(indextid), ItemPointerGetOffsetNumber(indextid)); + + /* Calculate level based on offset */ + level = element->level - i / m; + if (level < 0) + level = 0; + + neighbors = HnswGetNeighbors(base, element, level); + hc = &neighbors->items[neighbors->length++]; + HnswPtrStore(base, hc->element, e); + } +} + +/* + * Load neighbors + */ +static void +HnswLoadNeighbors(HnswElement element, Relation index, int m) +{ + Buffer buf; + Page page; + + buf = ReadBuffer(index, element->neighborPage); + LockBuffer(buf, BUFFER_LOCK_SHARE); + page = BufferGetPage(buf); + + LoadNeighborsFromPage(element, index, page, m); + + UnlockReleaseBuffer(buf); +} + /* * Check if connection already exists */ diff --git a/src/hnswutils.c b/src/hnswutils.c index 79bc086..3a66d85 100644 --- a/src/hnswutils.c +++ b/src/hnswutils.c @@ -449,69 +449,6 @@ HnswSetNeighborTuple(char *base, HnswNeighborTuple ntup, HnswElement e, int m) ntup->count = idx; } -/* - * Load neighbors from page - */ -static void -LoadNeighborsFromPage(HnswElement element, Relation index, Page page, int m) -{ - char *base = NULL; - - HnswNeighborTuple ntup = (HnswNeighborTuple) PageGetItem(page, PageGetItemId(page, element->neighborOffno)); - int neighborCount = (element->level + 2) * m; - - Assert(HnswIsNeighborTuple(ntup)); - - HnswInitNeighbors(base, element, m, NULL); - - /* Ensure expected neighbors */ - if (ntup->count != neighborCount) - return; - - for (int i = 0; i < neighborCount; i++) - { - HnswElement e; - int level; - HnswCandidate *hc; - ItemPointer indextid; - HnswNeighborArray *neighbors; - - indextid = &ntup->indextids[i]; - - if (!ItemPointerIsValid(indextid)) - continue; - - e = HnswInitElementFromBlock(ItemPointerGetBlockNumber(indextid), ItemPointerGetOffsetNumber(indextid)); - - /* Calculate level based on offset */ - level = element->level - i / m; - if (level < 0) - level = 0; - - neighbors = HnswGetNeighbors(base, element, level); - hc = &neighbors->items[neighbors->length++]; - HnswPtrStore(base, hc->element, e); - } -} - -/* - * Load neighbors - */ -void -HnswLoadNeighbors(HnswElement element, Relation index, int m) -{ - Buffer buf; - Page page; - - buf = ReadBuffer(index, element->neighborPage); - LockBuffer(buf, BUFFER_LOCK_SHARE); - page = BufferGetPage(buf); - - LoadNeighborsFromPage(element, index, page, m); - - UnlockReleaseBuffer(buf); -} - /* * Load an element from a tuple */ From 382a25aefbe825ac2a63dcee7caf3ebbbefe5e4c Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 29 Sep 2024 17:20:54 -0700 Subject: [PATCH 29/88] Split loading neighbor TIDs into separate function [skip ci] --- src/hnswutils.c | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/hnswutils.c b/src/hnswutils.c index 3a66d85..5c6d68e 100644 --- a/src/hnswutils.c +++ b/src/hnswutils.c @@ -680,18 +680,15 @@ HnswLoadUnvisitedFromMemory(char *base, HnswElement element, HnswUnvisited * unv } /* - * Load unvisited neighbors from disk + * Load neighbor index TIDs */ -static void -HnswLoadUnvisitedFromDisk(HnswElement element, HnswUnvisited * unvisited, int *unvisitedLength, visited_hash * v, Relation index, int m, int lm, int lc) +static bool +HnswLoadNeighborTids(HnswElement element, ItemPointerData *indextids, Relation index, int m, int lm, int lc) { Buffer buf; Page page; HnswNeighborTuple ntup; int start; - ItemPointerData indextids[HNSW_MAX_M * 2]; - - *unvisitedLength = 0; buf = ReadBuffer(index, element->neighborPage); LockBuffer(buf, BUFFER_LOCK_SHARE); @@ -703,14 +700,29 @@ HnswLoadUnvisitedFromDisk(HnswElement element, HnswUnvisited * unvisited, int *u if (ntup->count != (element->level + 2) * m) { UnlockReleaseBuffer(buf); - return; + return false; } /* Copy to minimize lock time */ start = (element->level - lc) * m; - memcpy(&indextids, ntup->indextids + start, lm * sizeof(ItemPointerData)); + memcpy(indextids, ntup->indextids + start, lm * sizeof(ItemPointerData)); UnlockReleaseBuffer(buf); + return true; +} + +/* + * Load unvisited neighbors from disk + */ +static void +HnswLoadUnvisitedFromDisk(HnswElement element, HnswUnvisited * unvisited, int *unvisitedLength, visited_hash * v, Relation index, int m, int lm, int lc) +{ + ItemPointerData indextids[HNSW_MAX_M * 2]; + + *unvisitedLength = 0; + + if (!HnswLoadNeighborTids(element, indextids, index, m, lm, lc)) + return; for (int i = 0; i < lm; i++) { From f371eb119b37ae524a7cc4dac7fb6309c6f2d868 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 29 Sep 2024 18:14:28 -0700 Subject: [PATCH 30/88] Removed lc from SelectNeighbors [skip ci] --- src/hnswutils.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/hnswutils.c b/src/hnswutils.c index 5c6d68e..c2f1128 100644 --- a/src/hnswutils.c +++ b/src/hnswutils.c @@ -959,14 +959,13 @@ CheckElementCloser(char *base, HnswCandidate * e, List *r, FmgrInfo *procinfo, O * Algorithm 4 from paper */ static List * -SelectNeighbors(char *base, List *c, int lm, int lc, FmgrInfo *procinfo, Oid collation, HnswElement e2, HnswCandidate * newCandidate, HnswCandidate * *pruned, bool sortCandidates) +SelectNeighbors(char *base, List *c, int lm, FmgrInfo *procinfo, Oid collation, HnswNeighborArray * neighbors, HnswCandidate * newCandidate, HnswCandidate * *pruned, bool sortCandidates) { List *r = NIL; List *w = list_copy(c); HnswCandidate **wd; int wdlen = 0; int wdoff = 0; - HnswNeighborArray *neighbors = HnswGetNeighbors(base, e2, lc); bool mustCalculate = !neighbors->closerSet; List *added = NIL; bool removedAny = false; @@ -1139,7 +1138,7 @@ HnswUpdateConnection(char *base, HnswElement element, HnswCandidate * hc, int lm c = lappend(c, ¤tNeighbors->items[i]); c = lappend(c, &hc2); - SelectNeighbors(base, c, lm, lc, procinfo, collation, hce, &hc2, &pruned, true); + SelectNeighbors(base, c, lm, procinfo, collation, currentNeighbors, &hc2, &pruned, true); /* Should not happen */ if (pruned == NULL) @@ -1278,7 +1277,7 @@ HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint * sortCandidates to true for in-memory builds to enable closer * caching, but there does not seem to be a difference in performance. */ - neighbors = SelectNeighbors(base, lw, lm, lc, procinfo, collation, element, NULL, NULL, false); + neighbors = SelectNeighbors(base, lw, lm, procinfo, collation, HnswGetNeighbors(base, element, lc), NULL, NULL, false); AddConnections(base, element, neighbors, lc); From 5ce367e18b11089eb4fb1569d3a44221809e57cd Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 29 Sep 2024 18:18:42 -0700 Subject: [PATCH 31/88] Removed lc from HnswUpdateConnection [skip ci] --- src/hnsw.h | 2 +- src/hnswbuild.c | 2 +- src/hnswinsert.c | 2 +- src/hnswutils.c | 3 +-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/hnsw.h b/src/hnsw.h index cfbcd3f..caec2e4 100644 --- a/src/hnsw.h +++ b/src/hnsw.h @@ -392,7 +392,7 @@ void HnswUpdateNeighborsOnDisk(Relation index, FmgrInfo *procinfo, Oid collatio void HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool loadHeaptids, bool loadVec); void HnswLoadElement(HnswElement element, double *distance, Datum *q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec, double *maxDistance); void HnswSetElementTuple(char *base, HnswElementTuple etup, HnswElement element); -void HnswUpdateConnection(char *base, HnswElement element, HnswCandidate * hc, int lm, int lc, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation); +void HnswUpdateConnection(char *base, HnswElement element, HnswCandidate * hc, HnswNeighborArray * currentNeighbors, int lm, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation); void HnswInitLockTranche(void); const HnswTypeInfo *HnswGetTypeInfo(Relation index); PGDLLEXPORT void HnswParallelBuildMain(dsm_segment *seg, shm_toc *toc); diff --git a/src/hnswbuild.c b/src/hnswbuild.c index 498b5d9..10bda20 100644 --- a/src/hnswbuild.c +++ b/src/hnswbuild.c @@ -388,7 +388,7 @@ UpdateNeighborsInMemory(char *base, FmgrInfo *procinfo, Oid collation, HnswEleme Assert(neighborElement); LWLockAcquire(&neighborElement->lock, LW_EXCLUSIVE); - HnswUpdateConnection(base, e, hc, lm, lc, NULL, NULL, procinfo, collation); + HnswUpdateConnection(base, e, hc, HnswGetNeighbors(base, neighborElement, lc), lm, NULL, NULL, procinfo, collation); LWLockRelease(&neighborElement->lock); } } diff --git a/src/hnswinsert.c b/src/hnswinsert.c index 42d7378..6456005 100644 --- a/src/hnswinsert.c +++ b/src/hnswinsert.c @@ -458,7 +458,7 @@ HnswUpdateNeighborsOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, Hns */ /* Select neighbors */ - HnswUpdateConnection(NULL, e, hc, lm, lc, &idx, index, procinfo, collation); + HnswUpdateConnection(NULL, e, hc, HnswGetNeighbors(base, neighborElement, lc), lm, &idx, index, procinfo, collation); /* New element was not selected as a neighbor */ if (idx == -1) diff --git a/src/hnswutils.c b/src/hnswutils.c index c2f1128..0659609 100644 --- a/src/hnswutils.c +++ b/src/hnswutils.c @@ -1078,10 +1078,9 @@ AddConnections(char *base, HnswElement element, List *neighbors, int lc) * Update connections */ void -HnswUpdateConnection(char *base, HnswElement element, HnswCandidate * hc, int lm, int lc, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation) +HnswUpdateConnection(char *base, HnswElement element, HnswCandidate * hc, HnswNeighborArray * currentNeighbors, int lm, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation) { HnswElement hce = HnswPtrAccess(base, hc->element); - HnswNeighborArray *currentNeighbors = HnswGetNeighbors(base, hce, lc); HnswCandidate hc2; HnswPtrStore(base, hc2.element, element); From ee43ee9b1699d41527443b30f803a6a332cc66fc Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 29 Sep 2024 18:52:12 -0700 Subject: [PATCH 32/88] Use HnswLoadNeighborTids for inserts --- src/hnsw.h | 2 ++ src/hnswinsert.c | 60 +++++++++++------------------------------------- src/hnswutils.c | 4 ++-- 3 files changed, 18 insertions(+), 48 deletions(-) diff --git a/src/hnsw.h b/src/hnsw.h index caec2e4..1b3f2ac 100644 --- a/src/hnsw.h +++ b/src/hnsw.h @@ -386,6 +386,7 @@ HnswSearchCandidate *HnswEntryCandidate(char *base, HnswElement em, Datum q, Rel void HnswUpdateMetaPage(Relation index, int updateEntry, HnswElement entryPoint, BlockNumber insertPage, ForkNumber forkNum, bool building); void HnswSetNeighborTuple(char *base, HnswNeighborTuple ntup, HnswElement e, int m); void HnswAddHeapTid(HnswElement element, ItemPointer heaptid); +HnswNeighborArray *HnswInitNeighborArray(int lm, HnswAllocator * allocator); void HnswInitNeighbors(char *base, HnswElement element, int m, HnswAllocator * alloc); bool HnswInsertTupleOnDisk(Relation index, Datum value, Datum *values, bool *isnull, ItemPointer heap_tid, bool building); void HnswUpdateNeighborsOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, HnswElement e, int m, bool checkExisting, bool building); @@ -393,6 +394,7 @@ void HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool void HnswLoadElement(HnswElement element, double *distance, Datum *q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec, double *maxDistance); void HnswSetElementTuple(char *base, HnswElementTuple etup, HnswElement element); void HnswUpdateConnection(char *base, HnswElement element, HnswCandidate * hc, HnswNeighborArray * currentNeighbors, int lm, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation); +bool HnswLoadNeighborTids(HnswElement element, ItemPointerData *indextids, Relation index, int m, int lm, int lc); void HnswInitLockTranche(void); const HnswTypeInfo *HnswGetTypeInfo(Relation index); PGDLLEXPORT void HnswParallelBuildMain(dsm_segment *seg, shm_toc *toc); diff --git a/src/hnswinsert.c b/src/hnswinsert.c index 6456005..712eaa1 100644 --- a/src/hnswinsert.c +++ b/src/hnswinsert.c @@ -335,66 +335,33 @@ AddElementOnDisk(Relation index, HnswElement e, int m, BlockNumber insertPage, B } /* - * Load neighbors from page + * Load neighbors */ -static void -LoadNeighborsFromPage(HnswElement element, Relation index, Page page, int m) +static HnswNeighborArray * +HnswLoadNeighbors(HnswElement element, Relation index, int m, int lm, int lc) { char *base = NULL; + HnswNeighborArray *neighbors = HnswInitNeighborArray(lm, NULL); + ItemPointerData indextids[HNSW_MAX_M * 2]; - HnswNeighborTuple ntup = (HnswNeighborTuple) PageGetItem(page, PageGetItemId(page, element->neighborOffno)); - int neighborCount = (element->level + 2) * m; + if (!HnswLoadNeighborTids(element, indextids, index, m, lm, lc)) + return neighbors; - Assert(HnswIsNeighborTuple(ntup)); - - HnswInitNeighbors(base, element, m, NULL); - - /* Ensure expected neighbors */ - if (ntup->count != neighborCount) - return; - - for (int i = 0; i < neighborCount; i++) + for (int i = 0; i < lm; i++) { + ItemPointer indextid = &indextids[i]; HnswElement e; - int level; HnswCandidate *hc; - ItemPointer indextid; - HnswNeighborArray *neighbors; - - indextid = &ntup->indextids[i]; if (!ItemPointerIsValid(indextid)) - continue; + break; e = HnswInitElementFromBlock(ItemPointerGetBlockNumber(indextid), ItemPointerGetOffsetNumber(indextid)); - - /* Calculate level based on offset */ - level = element->level - i / m; - if (level < 0) - level = 0; - - neighbors = HnswGetNeighbors(base, element, level); hc = &neighbors->items[neighbors->length++]; HnswPtrStore(base, hc->element, e); } -} -/* - * Load neighbors - */ -static void -HnswLoadNeighbors(HnswElement element, Relation index, int m) -{ - Buffer buf; - Page page; - - buf = ReadBuffer(index, element->neighborPage); - LockBuffer(buf, BUFFER_LOCK_SHARE); - page = BufferGetPage(buf); - - LoadNeighborsFromPage(element, index, page, m); - - UnlockReleaseBuffer(buf); + return neighbors; } /* @@ -441,6 +408,7 @@ HnswUpdateNeighborsOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, Hns int startIdx; HnswElement neighborElement = HnswPtrAccess(base, hc->element); OffsetNumber offno = neighborElement->neighborOffno; + HnswNeighborArray *neighborNeighbors; /* * Get latest neighbors since they may have changed. Do not lock @@ -448,7 +416,7 @@ HnswUpdateNeighborsOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, Hns * optimistic locking to retry if another update occurs before * getting exclusive lock. */ - HnswLoadNeighbors(neighborElement, index, m); + neighborNeighbors = HnswLoadNeighbors(neighborElement, index, m, lm, lc); /* * Could improve performance for vacuuming by checking neighbors @@ -458,7 +426,7 @@ HnswUpdateNeighborsOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, Hns */ /* Select neighbors */ - HnswUpdateConnection(NULL, e, hc, HnswGetNeighbors(base, neighborElement, lc), lm, &idx, index, procinfo, collation); + HnswUpdateConnection(NULL, e, hc, neighborNeighbors, lm, &idx, index, procinfo, collation); /* New element was not selected as a neighbor */ if (idx == -1) diff --git a/src/hnswutils.c b/src/hnswutils.c index 0659609..3a75672 100644 --- a/src/hnswutils.c +++ b/src/hnswutils.c @@ -197,7 +197,7 @@ HnswInitPage(Buffer buf, Page page) /* * Allocate a neighbor array */ -static HnswNeighborArray * +HnswNeighborArray * HnswInitNeighborArray(int lm, HnswAllocator * allocator) { HnswNeighborArray *a = HnswAlloc(allocator, HNSW_NEIGHBOR_ARRAY_SIZE(lm)); @@ -682,7 +682,7 @@ HnswLoadUnvisitedFromMemory(char *base, HnswElement element, HnswUnvisited * unv /* * Load neighbor index TIDs */ -static bool +bool HnswLoadNeighborTids(HnswElement element, ItemPointerData *indextids, Relation index, int m, int lm, int lc) { Buffer buf; From 648dd8af7810116da1758795600924a2c5e77160 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 29 Sep 2024 19:12:38 -0700 Subject: [PATCH 33/88] Moved LoadElementsForInsert to separate function and removed unused code path --- src/hnswutils.c | 53 +++++++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/src/hnswutils.c b/src/hnswutils.c index 3a75672..6b83d7c 100644 --- a/src/hnswutils.c +++ b/src/hnswutils.c @@ -1074,6 +1074,32 @@ AddConnections(char *base, HnswElement element, List *neighbors, int lc) a->items[a->length++] = *((HnswCandidate *) lfirst(lc2)); } +/* + * Load elements for insert + */ +static void +LoadElementsForInsert(HnswNeighborArray * neighbors, Datum q, HnswCandidate * *pruned, Relation index, FmgrInfo *procinfo, Oid collation) +{ + char *base = NULL; + + for (int i = 0; i < neighbors->length; i++) + { + HnswCandidate *hc = &neighbors->items[i]; + HnswElement element = HnswPtrAccess(base, hc->element); + double distance; + + HnswLoadElement(element, &distance, &q, index, procinfo, collation, true, NULL); + hc->distance = distance; + + /* Prune element if being deleted */ + if (element->heaptidsLength == 0) + { + *pruned = &neighbors->items[i]; + break; + } + } +} + /* * Update connections */ @@ -1101,32 +1127,7 @@ HnswUpdateConnection(char *base, HnswElement element, HnswCandidate * hc, HnswNe /* Load elements on insert */ if (index != NULL) - { - Datum q = HnswGetValue(base, hce); - - for (int i = 0; i < currentNeighbors->length; i++) - { - HnswCandidate *hc3 = ¤tNeighbors->items[i]; - HnswElement hc3Element = HnswPtrAccess(base, hc3->element); - - if (HnswPtrIsNull(base, hc3Element->value)) - { - double distance; - - HnswLoadElement(hc3Element, &distance, &q, index, procinfo, collation, true, NULL); - hc3->distance = distance; - } - else - hc3->distance = GetElementDistance(base, hc3Element, q, procinfo, collation); - - /* Prune element if being deleted */ - if (hc3Element->heaptidsLength == 0) - { - pruned = ¤tNeighbors->items[i]; - break; - } - } - } + LoadElementsForInsert(currentNeighbors, HnswGetValue(base, hce), &pruned, index, procinfo, collation); if (pruned == NULL) { From 4ac86f62a1dcd5db6c758eaef4619550a968f721 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 29 Sep 2024 19:22:35 -0700 Subject: [PATCH 34/88] Improved variable names [skip ci] --- src/hnsw.h | 2 +- src/hnswutils.c | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/hnsw.h b/src/hnsw.h index 1b3f2ac..e3974c3 100644 --- a/src/hnsw.h +++ b/src/hnsw.h @@ -393,7 +393,7 @@ void HnswUpdateNeighborsOnDisk(Relation index, FmgrInfo *procinfo, Oid collatio void HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool loadHeaptids, bool loadVec); void HnswLoadElement(HnswElement element, double *distance, Datum *q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec, double *maxDistance); void HnswSetElementTuple(char *base, HnswElementTuple etup, HnswElement element); -void HnswUpdateConnection(char *base, HnswElement element, HnswCandidate * hc, HnswNeighborArray * currentNeighbors, int lm, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation); +void HnswUpdateConnection(char *base, HnswElement element, HnswCandidate * hc, HnswNeighborArray * neighbors, int lm, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation); bool HnswLoadNeighborTids(HnswElement element, ItemPointerData *indextids, Relation index, int m, int lm, int lc); void HnswInitLockTranche(void); const HnswTypeInfo *HnswGetTypeInfo(Relation index); diff --git a/src/hnswutils.c b/src/hnswutils.c index 6b83d7c..6016cc8 100644 --- a/src/hnswutils.c +++ b/src/hnswutils.c @@ -1104,17 +1104,17 @@ LoadElementsForInsert(HnswNeighborArray * neighbors, Datum q, HnswCandidate * *p * Update connections */ void -HnswUpdateConnection(char *base, HnswElement element, HnswCandidate * hc, HnswNeighborArray * currentNeighbors, int lm, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation) +HnswUpdateConnection(char *base, HnswElement element, HnswCandidate * hc, HnswNeighborArray * neighbors, int lm, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation) { HnswElement hce = HnswPtrAccess(base, hc->element); - HnswCandidate hc2; + HnswCandidate newHc; - HnswPtrStore(base, hc2.element, element); - hc2.distance = hc->distance; + HnswPtrStore(base, newHc.element, element); + newHc.distance = hc->distance; - if (currentNeighbors->length < lm) + if (neighbors->length < lm) { - currentNeighbors->items[currentNeighbors->length++] = hc2; + neighbors->items[neighbors->length++] = newHc; /* Track update */ if (updateIdx != NULL) @@ -1127,18 +1127,18 @@ HnswUpdateConnection(char *base, HnswElement element, HnswCandidate * hc, HnswNe /* Load elements on insert */ if (index != NULL) - LoadElementsForInsert(currentNeighbors, HnswGetValue(base, hce), &pruned, index, procinfo, collation); + LoadElementsForInsert(neighbors, HnswGetValue(base, hce), &pruned, index, procinfo, collation); if (pruned == NULL) { List *c = NIL; /* Add candidates */ - for (int i = 0; i < currentNeighbors->length; i++) - c = lappend(c, ¤tNeighbors->items[i]); - c = lappend(c, &hc2); + for (int i = 0; i < neighbors->length; i++) + c = lappend(c, &neighbors->items[i]); + c = lappend(c, &newHc); - SelectNeighbors(base, c, lm, procinfo, collation, currentNeighbors, &hc2, &pruned, true); + SelectNeighbors(base, c, lm, procinfo, collation, neighbors, &newHc, &pruned, true); /* Should not happen */ if (pruned == NULL) @@ -1146,11 +1146,11 @@ HnswUpdateConnection(char *base, HnswElement element, HnswCandidate * hc, HnswNe } /* Find and replace the pruned element */ - for (int i = 0; i < currentNeighbors->length; i++) + for (int i = 0; i < neighbors->length; i++) { - if (HnswPtrEqual(base, currentNeighbors->items[i].element, pruned->element)) + if (HnswPtrEqual(base, neighbors->items[i].element, pruned->element)) { - currentNeighbors->items[i] = hc2; + neighbors->items[i] = newHc; /* Track update */ if (updateIdx != NULL) From 4c72f912068b1ed870d1660e242c216ae9b9b02a Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 29 Sep 2024 19:26:15 -0700 Subject: [PATCH 35/88] Improved variable name [skip ci] --- src/hnswutils.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hnswutils.c b/src/hnswutils.c index 6016cc8..019e230 100644 --- a/src/hnswutils.c +++ b/src/hnswutils.c @@ -1104,12 +1104,12 @@ LoadElementsForInsert(HnswNeighborArray * neighbors, Datum q, HnswCandidate * *p * Update connections */ void -HnswUpdateConnection(char *base, HnswElement element, HnswCandidate * hc, HnswNeighborArray * neighbors, int lm, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation) +HnswUpdateConnection(char *base, HnswElement newElement, HnswCandidate * hc, HnswNeighborArray * neighbors, int lm, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation) { HnswElement hce = HnswPtrAccess(base, hc->element); HnswCandidate newHc; - HnswPtrStore(base, newHc.element, element); + HnswPtrStore(base, newHc.element, newElement); newHc.distance = hc->distance; if (neighbors->length < lm) From 8eb8cdf0f3052af85587b4bf420acfdc558940c3 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 29 Sep 2024 19:44:11 -0700 Subject: [PATCH 36/88] Moved insert-specific code to hnswinsert.c --- src/hnswinsert.c | 38 +++++++++++++++++++++++++++++++++- src/hnswutils.c | 53 ++++++++---------------------------------------- 2 files changed, 46 insertions(+), 45 deletions(-) diff --git a/src/hnswinsert.c b/src/hnswinsert.c index 712eaa1..106428b 100644 --- a/src/hnswinsert.c +++ b/src/hnswinsert.c @@ -364,6 +364,32 @@ HnswLoadNeighbors(HnswElement element, Relation index, int m, int lm, int lc) return neighbors; } +/* + * Load elements for insert + */ +static void +LoadElementsForInsert(HnswNeighborArray * neighbors, Datum q, int *idx, Relation index, FmgrInfo *procinfo, Oid collation) +{ + char *base = NULL; + + for (int i = 0; i < neighbors->length; i++) + { + HnswCandidate *hc = &neighbors->items[i]; + HnswElement element = HnswPtrAccess(base, hc->element); + double distance; + + HnswLoadElement(element, &distance, &q, index, procinfo, collation, true, NULL); + hc->distance = distance; + + /* Prune element if being deleted */ + if (element->heaptidsLength == 0) + { + *idx = i; + break; + } + } +} + /* * Check if connection already exists */ @@ -426,7 +452,17 @@ HnswUpdateNeighborsOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, Hns */ /* Select neighbors */ - HnswUpdateConnection(NULL, e, hc, neighborNeighbors, lm, &idx, index, procinfo, collation); + if (neighbors->length < lm) + idx = -2; + else + { + Datum q = HnswGetValue(base, neighborElement); + + LoadElementsForInsert(neighborNeighbors, q, &idx, index, procinfo, collation); + + if (idx == -1) + HnswUpdateConnection(base, e, hc, neighborNeighbors, lm, &idx, index, procinfo, collation); + } /* New element was not selected as a neighbor */ if (idx == -1) diff --git a/src/hnswutils.c b/src/hnswutils.c index 019e230..8ac9c08 100644 --- a/src/hnswutils.c +++ b/src/hnswutils.c @@ -1074,39 +1074,12 @@ AddConnections(char *base, HnswElement element, List *neighbors, int lc) a->items[a->length++] = *((HnswCandidate *) lfirst(lc2)); } -/* - * Load elements for insert - */ -static void -LoadElementsForInsert(HnswNeighborArray * neighbors, Datum q, HnswCandidate * *pruned, Relation index, FmgrInfo *procinfo, Oid collation) -{ - char *base = NULL; - - for (int i = 0; i < neighbors->length; i++) - { - HnswCandidate *hc = &neighbors->items[i]; - HnswElement element = HnswPtrAccess(base, hc->element); - double distance; - - HnswLoadElement(element, &distance, &q, index, procinfo, collation, true, NULL); - hc->distance = distance; - - /* Prune element if being deleted */ - if (element->heaptidsLength == 0) - { - *pruned = &neighbors->items[i]; - break; - } - } -} - /* * Update connections */ void HnswUpdateConnection(char *base, HnswElement newElement, HnswCandidate * hc, HnswNeighborArray * neighbors, int lm, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation) { - HnswElement hce = HnswPtrAccess(base, hc->element); HnswCandidate newHc; HnswPtrStore(base, newHc.element, newElement); @@ -1123,27 +1096,19 @@ HnswUpdateConnection(char *base, HnswElement newElement, HnswCandidate * hc, Hns else { /* Shrink connections */ + List *c = NIL; HnswCandidate *pruned = NULL; - /* Load elements on insert */ - if (index != NULL) - LoadElementsForInsert(neighbors, HnswGetValue(base, hce), &pruned, index, procinfo, collation); + /* Add candidates */ + for (int i = 0; i < neighbors->length; i++) + c = lappend(c, &neighbors->items[i]); + c = lappend(c, &newHc); + SelectNeighbors(base, c, lm, procinfo, collation, neighbors, &newHc, &pruned, true); + + /* Should not happen */ if (pruned == NULL) - { - List *c = NIL; - - /* Add candidates */ - for (int i = 0; i < neighbors->length; i++) - c = lappend(c, &neighbors->items[i]); - c = lappend(c, &newHc); - - SelectNeighbors(base, c, lm, procinfo, collation, neighbors, &newHc, &pruned, true); - - /* Should not happen */ - if (pruned == NULL) - return; - } + return; /* Find and replace the pruned element */ for (int i = 0; i < neighbors->length; i++) From 525e3b81e1cedf02bb5ef32ac093310113ac9cca Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 29 Sep 2024 19:47:25 -0700 Subject: [PATCH 37/88] Improved HnswUpdateConnection parameters [skip ci] --- src/hnsw.h | 2 +- src/hnswbuild.c | 2 +- src/hnswinsert.c | 2 +- src/hnswutils.c | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/hnsw.h b/src/hnsw.h index e3974c3..c58ced1 100644 --- a/src/hnsw.h +++ b/src/hnsw.h @@ -393,7 +393,7 @@ void HnswUpdateNeighborsOnDisk(Relation index, FmgrInfo *procinfo, Oid collatio void HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool loadHeaptids, bool loadVec); void HnswLoadElement(HnswElement element, double *distance, Datum *q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec, double *maxDistance); void HnswSetElementTuple(char *base, HnswElementTuple etup, HnswElement element); -void HnswUpdateConnection(char *base, HnswElement element, HnswCandidate * hc, HnswNeighborArray * neighbors, int lm, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation); +void HnswUpdateConnection(char *base, HnswNeighborArray * neighbors, HnswElement newElement, float distance, int lm, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation); bool HnswLoadNeighborTids(HnswElement element, ItemPointerData *indextids, Relation index, int m, int lm, int lc); void HnswInitLockTranche(void); const HnswTypeInfo *HnswGetTypeInfo(Relation index); diff --git a/src/hnswbuild.c b/src/hnswbuild.c index 10bda20..82c981b 100644 --- a/src/hnswbuild.c +++ b/src/hnswbuild.c @@ -388,7 +388,7 @@ UpdateNeighborsInMemory(char *base, FmgrInfo *procinfo, Oid collation, HnswEleme Assert(neighborElement); LWLockAcquire(&neighborElement->lock, LW_EXCLUSIVE); - HnswUpdateConnection(base, e, hc, HnswGetNeighbors(base, neighborElement, lc), lm, NULL, NULL, procinfo, collation); + HnswUpdateConnection(base, HnswGetNeighbors(base, neighborElement, lc), e, hc->distance, lm, NULL, NULL, procinfo, collation); LWLockRelease(&neighborElement->lock); } } diff --git a/src/hnswinsert.c b/src/hnswinsert.c index 106428b..3d73f2a 100644 --- a/src/hnswinsert.c +++ b/src/hnswinsert.c @@ -461,7 +461,7 @@ HnswUpdateNeighborsOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, Hns LoadElementsForInsert(neighborNeighbors, q, &idx, index, procinfo, collation); if (idx == -1) - HnswUpdateConnection(base, e, hc, neighborNeighbors, lm, &idx, index, procinfo, collation); + HnswUpdateConnection(base, neighborNeighbors, e, hc->distance, lm, &idx, index, procinfo, collation); } /* New element was not selected as a neighbor */ diff --git a/src/hnswutils.c b/src/hnswutils.c index 8ac9c08..f8f627f 100644 --- a/src/hnswutils.c +++ b/src/hnswutils.c @@ -1078,12 +1078,12 @@ AddConnections(char *base, HnswElement element, List *neighbors, int lc) * Update connections */ void -HnswUpdateConnection(char *base, HnswElement newElement, HnswCandidate * hc, HnswNeighborArray * neighbors, int lm, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation) +HnswUpdateConnection(char *base, HnswNeighborArray * neighbors, HnswElement newElement, float distance, int lm, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation) { HnswCandidate newHc; HnswPtrStore(base, newHc.element, newElement); - newHc.distance = hc->distance; + newHc.distance = distance; if (neighbors->length < lm) { From 7ba593c492b5029a490d424f3c467ccdb677aed2 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 29 Sep 2024 23:03:02 -0700 Subject: [PATCH 38/88] Improved SelectNeighbors signature [skip ci] --- src/hnswutils.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/hnswutils.c b/src/hnswutils.c index f8f627f..3d0b484 100644 --- a/src/hnswutils.c +++ b/src/hnswutils.c @@ -959,14 +959,14 @@ CheckElementCloser(char *base, HnswCandidate * e, List *r, FmgrInfo *procinfo, O * Algorithm 4 from paper */ static List * -SelectNeighbors(char *base, List *c, int lm, FmgrInfo *procinfo, Oid collation, HnswNeighborArray * neighbors, HnswCandidate * newCandidate, HnswCandidate * *pruned, bool sortCandidates) +SelectNeighbors(char *base, List *c, int lm, FmgrInfo *procinfo, Oid collation, bool *closerSet, HnswCandidate * newCandidate, HnswCandidate * *pruned, bool sortCandidates) { List *r = NIL; List *w = list_copy(c); HnswCandidate **wd; int wdlen = 0; int wdoff = 0; - bool mustCalculate = !neighbors->closerSet; + bool mustCalculate = !(*closerSet); List *added = NIL; bool removedAny = false; @@ -1043,7 +1043,7 @@ SelectNeighbors(char *base, List *c, int lm, FmgrInfo *procinfo, Oid collation, } /* Cached value can only be used in future if sorted deterministically */ - neighbors->closerSet = sortCandidates; + *closerSet = sortCandidates; /* Keep pruned connections */ while (wdoff < wdlen && list_length(r) < lm) @@ -1104,7 +1104,7 @@ HnswUpdateConnection(char *base, HnswNeighborArray * neighbors, HnswElement newE c = lappend(c, &neighbors->items[i]); c = lappend(c, &newHc); - SelectNeighbors(base, c, lm, procinfo, collation, neighbors, &newHc, &pruned, true); + SelectNeighbors(base, c, lm, procinfo, collation, &neighbors->closerSet, &newHc, &pruned, true); /* Should not happen */ if (pruned == NULL) @@ -1242,7 +1242,7 @@ HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint * sortCandidates to true for in-memory builds to enable closer * caching, but there does not seem to be a difference in performance. */ - neighbors = SelectNeighbors(base, lw, lm, procinfo, collation, HnswGetNeighbors(base, element, lc), NULL, NULL, false); + neighbors = SelectNeighbors(base, lw, lm, procinfo, collation, &HnswGetNeighbors(base, element, lc)->closerSet, NULL, NULL, false); AddConnections(base, element, neighbors, lc); From 658d74e2f6e4c68e71d77c65e421988361ce1131 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 29 Sep 2024 23:48:58 -0700 Subject: [PATCH 39/88] Use Size for memory [skip ci] --- src/hnsw.h | 4 ++-- src/hnswbuild.c | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/hnsw.h b/src/hnsw.h index c58ced1..116d9bc 100644 --- a/src/hnsw.h +++ b/src/hnsw.h @@ -185,8 +185,8 @@ typedef struct HnswGraph /* Allocations state */ LWLock allocatorLock; - long memoryUsed; - long memoryTotal; + Size memoryUsed; + Size memoryTotal; /* Flushed state */ LWLock flushLock; diff --git a/src/hnswbuild.c b/src/hnswbuild.c index 82c981b..87d4823 100644 --- a/src/hnswbuild.c +++ b/src/hnswbuild.c @@ -607,7 +607,7 @@ BuildCallback(Relation index, ItemPointer tid, Datum *values, * Initialize the graph */ static void -InitGraph(HnswGraph * graph, char *base, long memoryTotal) +InitGraph(HnswGraph * graph, char *base, Size memoryTotal) { /* Initialize the lock tranche if needed */ HnswInitLockTranche(); @@ -708,7 +708,7 @@ InitBuildState(HnswBuildState * buildstate, Relation heap, Relation index, Index buildstate->normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC); buildstate->collation = index->rd_indcollation[0]; - InitGraph(&buildstate->graphData, NULL, maintenance_work_mem * 1024L); + InitGraph(&buildstate->graphData, NULL, (Size) maintenance_work_mem * 1024L); buildstate->graph = &buildstate->graphData; buildstate->ml = HnswGetMl(buildstate->m); buildstate->maxLevel = HnswGetMaxLevel(buildstate->m); From d148b4e61b4c2464c66a13ac8bb0f2583524a22e Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Mon, 30 Sep 2024 09:59:12 -0700 Subject: [PATCH 40/88] Fixed insert logic --- src/hnswinsert.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hnswinsert.c b/src/hnswinsert.c index 3d73f2a..8e3a27c 100644 --- a/src/hnswinsert.c +++ b/src/hnswinsert.c @@ -452,7 +452,7 @@ HnswUpdateNeighborsOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, Hns */ /* Select neighbors */ - if (neighbors->length < lm) + if (neighborNeighbors->length < lm) idx = -2; else { From a8b4b6675a947974f021476b2e51dc657283d7ea Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Mon, 30 Sep 2024 10:14:52 -0700 Subject: [PATCH 41/88] Moved logic to get update index to separate function --- src/hnswinsert.c | 70 ++++++++++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 29 deletions(-) diff --git a/src/hnswinsert.c b/src/hnswinsert.c index 8e3a27c..c198c93 100644 --- a/src/hnswinsert.c +++ b/src/hnswinsert.c @@ -390,6 +390,45 @@ LoadElementsForInsert(HnswNeighborArray * neighbors, Datum q, int *idx, Relation } } +/* + * Get update index + */ +static int +GetUpdateIndex(HnswElement element, HnswElement newElement, float distance, int m, int lm, int lc, Relation index, FmgrInfo *procinfo, Oid collation) +{ + char *base = NULL; + int idx = -1; + HnswNeighborArray *neighbors; + + /* + * Get latest neighbors since they may have changed. Do not lock yet since + * selecting neighbors can take time. Could use optimistic locking to + * retry if another update occurs before getting exclusive lock. + */ + neighbors = HnswLoadNeighbors(element, index, m, lm, lc); + + /* + * Could improve performance for vacuuming by checking neighbors against + * list of elements being deleted to find index. It's important to exclude + * already deleted elements for this since they can be replaced at any + * time. + */ + + if (neighbors->length < lm) + idx = -2; + else + { + Datum q = HnswGetValue(base, element); + + LoadElementsForInsert(neighbors, q, &idx, index, procinfo, collation); + + if (idx == -1) + HnswUpdateConnection(base, neighbors, newElement, distance, lm, &idx, index, procinfo, collation); + } + + return idx; +} + /* * Check if connection already exists */ @@ -430,39 +469,12 @@ HnswUpdateNeighborsOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, Hns Page page; GenericXLogState *state; HnswNeighborTuple ntup; - int idx = -1; + int idx; int startIdx; HnswElement neighborElement = HnswPtrAccess(base, hc->element); OffsetNumber offno = neighborElement->neighborOffno; - HnswNeighborArray *neighborNeighbors; - /* - * Get latest neighbors since they may have changed. Do not lock - * yet since selecting neighbors can take time. Could use - * optimistic locking to retry if another update occurs before - * getting exclusive lock. - */ - neighborNeighbors = HnswLoadNeighbors(neighborElement, index, m, lm, lc); - - /* - * Could improve performance for vacuuming by checking neighbors - * against list of elements being deleted to find index. It's - * important to exclude already deleted elements for this since - * they can be replaced at any time. - */ - - /* Select neighbors */ - if (neighborNeighbors->length < lm) - idx = -2; - else - { - Datum q = HnswGetValue(base, neighborElement); - - LoadElementsForInsert(neighborNeighbors, q, &idx, index, procinfo, collation); - - if (idx == -1) - HnswUpdateConnection(base, neighborNeighbors, e, hc->distance, lm, &idx, index, procinfo, collation); - } + idx = GetUpdateIndex(neighborElement, e, hc->distance, m, lm, lc, index, procinfo, collation); /* New element was not selected as a neighbor */ if (idx == -1) From ff6da4fceab43305e4892ac76764a37417e4cada Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Mon, 30 Sep 2024 10:30:01 -0700 Subject: [PATCH 42/88] Moved logic to get update neighbor on disk to separate function --- src/hnswinsert.c | 138 +++++++++++++++++++++++++---------------------- 1 file changed, 74 insertions(+), 64 deletions(-) diff --git a/src/hnswinsert.c b/src/hnswinsert.c index c198c93..a3949b2 100644 --- a/src/hnswinsert.c +++ b/src/hnswinsert.c @@ -449,6 +449,78 @@ ConnectionExists(HnswElement e, HnswNeighborTuple ntup, int startIdx, int lm) return false; } +/* + * Update neighbor + */ +static void +UpdateNeighborOnDisk(HnswElement element, HnswElement newElement, int idx, int m, int lm, int lc, Relation index, bool checkExisting, bool building) +{ + Buffer buf; + Page page; + GenericXLogState *state; + HnswNeighborTuple ntup; + int startIdx; + OffsetNumber offno = element->neighborOffno; + + /* Register page */ + buf = ReadBuffer(index, element->neighborPage); + LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE); + if (building) + { + state = NULL; + page = BufferGetPage(buf); + } + else + { + state = GenericXLogStart(index); + page = GenericXLogRegisterBuffer(state, buf, 0); + } + + /* Get tuple */ + ntup = (HnswNeighborTuple) PageGetItem(page, PageGetItemId(page, offno)); + + /* Calculate index for update */ + startIdx = (element->level - lc) * m; + + /* Check for existing connection */ + if (checkExisting && ConnectionExists(newElement, ntup, startIdx, lm)) + idx = -1; + else if (idx == -2) + { + /* Find free offset if still exists */ + /* TODO Retry updating connections if not */ + for (int j = 0; j < lm; j++) + { + if (!ItemPointerIsValid(&ntup->indextids[startIdx + j])) + { + idx = startIdx + j; + break; + } + } + } + else + idx += startIdx; + + /* Make robust to issues */ + if (idx >= 0 && idx < ntup->count) + { + ItemPointer indextid = &ntup->indextids[idx]; + + /* Update neighbor on the buffer */ + ItemPointerSet(indextid, newElement->blkno, newElement->offno); + + /* Commit */ + if (building) + MarkBufferDirty(buf); + else + GenericXLogFinish(state); + } + else if (!building) + GenericXLogAbort(state); + + UnlockReleaseBuffer(buf); +} + /* * Update neighbors */ @@ -465,14 +537,8 @@ HnswUpdateNeighborsOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, Hns for (int i = 0; i < neighbors->length; i++) { HnswCandidate *hc = &neighbors->items[i]; - Buffer buf; - Page page; - GenericXLogState *state; - HnswNeighborTuple ntup; - int idx; - int startIdx; HnswElement neighborElement = HnswPtrAccess(base, hc->element); - OffsetNumber offno = neighborElement->neighborOffno; + int idx; idx = GetUpdateIndex(neighborElement, e, hc->distance, m, lm, lc, index, procinfo, collation); @@ -480,63 +546,7 @@ HnswUpdateNeighborsOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, Hns if (idx == -1) continue; - /* Register page */ - buf = ReadBuffer(index, neighborElement->neighborPage); - LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE); - if (building) - { - state = NULL; - page = BufferGetPage(buf); - } - else - { - state = GenericXLogStart(index); - page = GenericXLogRegisterBuffer(state, buf, 0); - } - - /* Get tuple */ - ntup = (HnswNeighborTuple) PageGetItem(page, PageGetItemId(page, offno)); - - /* Calculate index for update */ - startIdx = (neighborElement->level - lc) * m; - - /* Check for existing connection */ - if (checkExisting && ConnectionExists(e, ntup, startIdx, lm)) - idx = -1; - else if (idx == -2) - { - /* Find free offset if still exists */ - /* TODO Retry updating connections if not */ - for (int j = 0; j < lm; j++) - { - if (!ItemPointerIsValid(&ntup->indextids[startIdx + j])) - { - idx = startIdx + j; - break; - } - } - } - else - idx += startIdx; - - /* Make robust to issues */ - if (idx >= 0 && idx < ntup->count) - { - ItemPointer indextid = &ntup->indextids[idx]; - - /* Update neighbor on the buffer */ - ItemPointerSet(indextid, e->blkno, e->offno); - - /* Commit */ - if (building) - MarkBufferDirty(buf); - else - GenericXLogFinish(state); - } - else if (!building) - GenericXLogAbort(state); - - UnlockReleaseBuffer(buf); + UpdateNeighborOnDisk(neighborElement, e, idx, m, lm, lc, index, checkExisting, building); } } } From 57248ba128f1f4b3773d0575de8a8c936e3eee59 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Mon, 30 Sep 2024 11:15:27 -0700 Subject: [PATCH 43/88] Use separate memory context for updating neighbors, which improves performance around 10% for larger vectors --- CHANGELOG.md | 1 + src/hnswinsert.c | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5a17b3..a7d9924 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - Added casts for arrays to `sparsevec` - Improved cost estimation +- Improved performance of HNSW inserts and on-disk index builds - Reduced memory usage for HNSW index scans - Dropped support for Postgres 12 diff --git a/src/hnswinsert.c b/src/hnswinsert.c index a3949b2..2f18b12 100644 --- a/src/hnswinsert.c +++ b/src/hnswinsert.c @@ -394,11 +394,12 @@ LoadElementsForInsert(HnswNeighborArray * neighbors, Datum q, int *idx, Relation * Get update index */ static int -GetUpdateIndex(HnswElement element, HnswElement newElement, float distance, int m, int lm, int lc, Relation index, FmgrInfo *procinfo, Oid collation) +GetUpdateIndex(HnswElement element, HnswElement newElement, float distance, int m, int lm, int lc, Relation index, FmgrInfo *procinfo, Oid collation, MemoryContext updateCtx) { char *base = NULL; int idx = -1; HnswNeighborArray *neighbors; + MemoryContext oldCtx = MemoryContextSwitchTo(updateCtx); /* * Get latest neighbors since they may have changed. Do not lock yet since @@ -426,6 +427,9 @@ GetUpdateIndex(HnswElement element, HnswElement newElement, float distance, int HnswUpdateConnection(base, neighbors, newElement, distance, lm, &idx, index, procinfo, collation); } + MemoryContextSwitchTo(oldCtx); + MemoryContextReset(updateCtx); + return idx; } @@ -529,6 +533,14 @@ HnswUpdateNeighborsOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, Hns { char *base = NULL; + /* Use separate memory context to improve performance for larger vectors */ + MemoryContext updateCtx = GenerationContextCreate(CurrentMemoryContext, + "Hnsw insert update context", +#if PG_VERSION_NUM >= 150000 + 128 * 1024, 128 * 1024, +#endif + 128 * 1024); + for (int lc = e->level; lc >= 0; lc--) { int lm = HnswGetLayerM(m, lc); @@ -540,7 +552,7 @@ HnswUpdateNeighborsOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, Hns HnswElement neighborElement = HnswPtrAccess(base, hc->element); int idx; - idx = GetUpdateIndex(neighborElement, e, hc->distance, m, lm, lc, index, procinfo, collation); + idx = GetUpdateIndex(neighborElement, e, hc->distance, m, lm, lc, index, procinfo, collation, updateCtx); /* New element was not selected as a neighbor */ if (idx == -1) From d5f4a0e43515caed7a7ef287e317e1bd238acddd Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 8 Oct 2024 12:21:26 -0700 Subject: [PATCH 44/88] Fixed memory context leak in HnswUpdateNeighborsOnDisk - fixes #692 --- src/hnswinsert.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/hnswinsert.c b/src/hnswinsert.c index 2f18b12..2dfd8d3 100644 --- a/src/hnswinsert.c +++ b/src/hnswinsert.c @@ -561,6 +561,8 @@ HnswUpdateNeighborsOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, Hns UpdateNeighborOnDisk(neighborElement, e, idx, m, lm, lc, index, checkExisting, building); } } + + MemoryContextDelete(updateCtx); } /* From 77688b43093c3461ed97f7232e268c8c3b638600 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 8 Oct 2024 12:42:03 -0700 Subject: [PATCH 45/88] Improve total cost for cost estimation (#686) --- src/hnsw.c | 40 +++++++++++++++++++++--------------- src/ivfflat.c | 36 +++++++++++++++----------------- test/t/017_hnsw_filtering.pl | 6 ++---- test/t/039_hnsw_cost.pl | 13 ++++++++++-- 4 files changed, 53 insertions(+), 42 deletions(-) diff --git a/src/hnsw.c b/src/hnsw.c index 0b6640a..c2579c1 100644 --- a/src/hnsw.c +++ b/src/hnsw.c @@ -100,10 +100,8 @@ hnswcostestimate(PlannerInfo *root, IndexPath *path, double loop_count, { GenericCosts costs; int m; - int entryLevel; - int layer0TuplesMax; - double layer0Selectivity; - double scalingFactor = 0.55; + double ratio; + double startupPages; double spc_seq_page_cost; Relation index; @@ -120,6 +118,8 @@ hnswcostestimate(PlannerInfo *root, IndexPath *path, double loop_count, MemSet(&costs, 0, sizeof(costs)); + genericcostestimate(root, path, loop_count, &costs); + index = index_open(path->indexinfo->indexoid, NoLock); HnswGetMetaPageInfo(index, &m, NULL); index_close(index, NoLock); @@ -151,30 +151,38 @@ hnswcostestimate(PlannerInfo *root, IndexPath *path, double loop_count, * at L0, accounting for previously visited tuples, multiplied by the * "scalingFactor" (currently hardcoded). */ - 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))); + if (path->indexinfo->tuples > 0) + { + double scalingFactor = 0.55; + int entryLevel = (int) (log(path->indexinfo->tuples) * HnswGetMl(m)); + int layer0TuplesMax = HnswGetLayerM(m, 0) * hnsw_ef_search; + double layer0Selectivity = scalingFactor * log(path->indexinfo->tuples) / (log(m) * (1 + log(hnsw_ef_search))); - costs.numIndexTuples = (entryLevel * m) + - (layer0TuplesMax * layer0Selectivity); + ratio = (entryLevel * m + layer0TuplesMax * layer0Selectivity) / path->indexinfo->tuples; - genericcostestimate(root, path, loop_count, &costs); + if (ratio > 1) + ratio = 1; + } + else + ratio = 1; get_tablespace_page_costs(path->indexinfo->reltablespace, NULL, &spc_seq_page_cost); + /* Startup cost is cost before returning the first row */ + costs.indexStartupCost = costs.indexTotalCost * ratio; + /* Adjust cost if needed since TOAST not included in seq scan cost */ - if (costs.numIndexPages > path->indexinfo->rel->pages) + startupPages = costs.numIndexPages * ratio; + if (startupPages > path->indexinfo->rel->pages && ratio < 0.5) { /* Change all page cost from random to sequential */ - costs.indexTotalCost -= costs.numIndexPages * (costs.spc_random_page_cost - spc_seq_page_cost); + costs.indexStartupCost -= startupPages * (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; + costs.indexStartupCost -= (startupPages - path->indexinfo->rel->pages) * spc_seq_page_cost; } - /* Use total cost since most work happens before first tuple is returned */ - *indexStartupCost = costs.indexTotalCost; + *indexStartupCost = costs.indexStartupCost; *indexTotalCost = costs.indexTotalCost; *indexSelectivity = costs.indexSelectivity; *indexCorrelation = costs.indexCorrelation; diff --git a/src/ivfflat.c b/src/ivfflat.c index 986e19d..395040d 100644 --- a/src/ivfflat.c +++ b/src/ivfflat.c @@ -69,6 +69,8 @@ ivfflatcostestimate(PlannerInfo *root, IndexPath *path, double loop_count, GenericCosts costs; int lists; double ratio; + double sequentialRatio = 0.5; + double startupPages; double spc_seq_page_cost; Relation index; @@ -85,6 +87,8 @@ ivfflatcostestimate(PlannerInfo *root, IndexPath *path, double loop_count, MemSet(&costs, 0, sizeof(costs)); + genericcostestimate(root, path, loop_count, &costs); + index = index_open(path->indexinfo->indexoid, NoLock); IvfflatGetMetaPageInfo(index, &lists, NULL); index_close(index, NoLock); @@ -94,34 +98,26 @@ ivfflatcostestimate(PlannerInfo *root, IndexPath *path, double loop_count, if (ratio > 1.0) ratio = 1.0; - /* - * This gives us the subset of tuples to visit. This value is passed into - * the generic cost estimator to determine the number of pages to visit - * during the index scan. - */ - costs.numIndexTuples = path->indexinfo->tuples * ratio; - - genericcostestimate(root, path, loop_count, &costs); - get_tablespace_page_costs(path->indexinfo->reltablespace, NULL, &spc_seq_page_cost); + /* Change some page cost from random to sequential */ + costs.indexTotalCost -= sequentialRatio * costs.numIndexPages * (costs.spc_random_page_cost - spc_seq_page_cost); + + /* Startup cost is cost before returning the first row */ + costs.indexStartupCost = costs.indexTotalCost * ratio; + /* Adjust cost if needed since TOAST not included in seq scan cost */ - if (costs.numIndexPages > path->indexinfo->rel->pages && ratio < 0.5) + startupPages = costs.numIndexPages * ratio; + if (startupPages > path->indexinfo->rel->pages && ratio < 0.5) { - /* Change all page cost from random to sequential */ - costs.indexTotalCost -= costs.numIndexPages * (costs.spc_random_page_cost - spc_seq_page_cost); + /* Change rest of page cost from random to sequential */ + costs.indexStartupCost -= (1 - sequentialRatio) * startupPages * (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); + costs.indexStartupCost -= (startupPages - path->indexinfo->rel->pages) * spc_seq_page_cost; } - /* Use total cost since most work happens before first tuple is returned */ - *indexStartupCost = costs.indexTotalCost; + *indexStartupCost = costs.indexStartupCost; *indexTotalCost = costs.indexTotalCost; *indexSelectivity = costs.indexSelectivity; *indexCorrelation = costs.indexCorrelation; diff --git a/test/t/017_hnsw_filtering.pl b/test/t/017_hnsw_filtering.pl index 9dbdcf3..afa2a1c 100644 --- a/test/t/017_hnsw_filtering.pl +++ b/test/t/017_hnsw_filtering.pl @@ -41,8 +41,7 @@ my $c = int(rand() * $nc); my $explain = $node->safe_psql("postgres", qq( EXPLAIN ANALYZE SELECT i FROM tst WHERE c = $c ORDER BY v <-> '$query' LIMIT $limit; )); -# TODO Do not use index -like($explain, qr/Index Scan using idx/); +like($explain, qr/Seq Scan/); # Test attribute filtering with few rows removed $explain = $node->safe_psql("postgres", qq( @@ -60,8 +59,7 @@ like($explain, qr/Index Scan using idx/); $explain = $node->safe_psql("postgres", qq( EXPLAIN ANALYZE SELECT i FROM tst WHERE c < 1 ORDER BY v <-> '$query' LIMIT $limit; )); -# TODO Do not use index -like($explain, qr/Index Scan using idx/); +like($explain, qr/Seq Scan/); # Test attribute filtering with few rows removed like $explain = $node->safe_psql("postgres", qq( diff --git a/test/t/039_hnsw_cost.pl b/test/t/039_hnsw_cost.pl index a26c09a..97ea5e7 100644 --- a/test/t/039_hnsw_cost.pl +++ b/test/t/039_hnsw_cost.pl @@ -17,12 +17,11 @@ $node->safe_psql("postgres", "CREATE EXTENSION vector;"); for my $dim (@dims) { my $array_sql = join(",", ('random()') x $dim); - my $n = $dim == 384 ? 3000 : 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;" + "INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(1, 2000) i;" ); $node->safe_psql("postgres", "CREATE INDEX idx ON tst USING hnsw (v vector_l2_ops);"); $node->safe_psql("postgres", "ANALYZE tst;"); @@ -40,6 +39,16 @@ for my $dim (@dims) )); like($explain, qr/Index Scan using idx/); + # 3x the rows are needed for distance filters + # since the planner uses DEFAULT_INEQ_SEL for the selectivity (should be 1) + # Recreate index for performance + $node->safe_psql("postgres", "DROP INDEX idx;"); + $node->safe_psql("postgres", + "INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(2001, 6000) i;" + ); + $node->safe_psql("postgres", "CREATE INDEX idx ON tst USING hnsw (v vector_l2_ops);"); + $node->safe_psql("postgres", "ANALYZE tst;"); + $explain = $node->safe_psql("postgres", qq( EXPLAIN ANALYZE SELECT i FROM tst WHERE v <-> '$query' < 1 ORDER BY v <-> '$query' LIMIT $limit; )); From f4b67b078f8dc158b4d88a125d7043cc6cc6ac65 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 9 Oct 2024 17:01:49 -0700 Subject: [PATCH 46/88] DRY HNSW distance calculations --- src/hnswutils.c | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/hnswutils.c b/src/hnswutils.c index 3d0b484..a7eb819 100644 --- a/src/hnswutils.c +++ b/src/hnswutils.c @@ -482,6 +482,15 @@ HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool loadHe } } +/* + * Calculate the distance between values + */ +static inline float +HnswGetDistance(Datum a, Datum b, FmgrInfo *procinfo, Oid collation) +{ + return DatumGetFloat8(FunctionCall2Coll(procinfo, collation, a, b)); +} + /* * Load an element and optionally get its distance from q */ @@ -507,7 +516,7 @@ HnswLoadElementImpl(BlockNumber blkno, OffsetNumber offno, double *distance, Dat if (DatumGetPointer(*q) == NULL) *distance = 0; else - *distance = DatumGetFloat8(FunctionCall2Coll(procinfo, collation, *q, PointerGetDatum(&etup->data))); + *distance = HnswGetDistance(*q, PointerGetDatum(&etup->data), procinfo, collation); } /* Load element */ @@ -539,7 +548,7 @@ GetElementDistance(char *base, HnswElement element, Datum q, FmgrInfo *procinfo, { Datum value = HnswGetValue(base, element); - return DatumGetFloat8(FunctionCall2Coll(procinfo, collation, q, value)); + return HnswGetDistance(q, value, procinfo, collation); } /* @@ -921,18 +930,6 @@ CompareCandidateDistancesOffset(const ListCell *a, const ListCell *b) return 0; } -/* - * Calculate the distance between elements - */ -static float -HnswGetDistance(char *base, HnswElement a, HnswElement b, FmgrInfo *procinfo, Oid collation) -{ - Datum aValue = HnswGetValue(base, a); - Datum bValue = HnswGetValue(base, b); - - return DatumGetFloat8(FunctionCall2Coll(procinfo, collation, aValue, bValue)); -} - /* * Check if an element is closer to q than any element from R */ @@ -940,13 +937,15 @@ static bool CheckElementCloser(char *base, HnswCandidate * e, List *r, FmgrInfo *procinfo, Oid collation) { HnswElement eElement = HnswPtrAccess(base, e->element); + Datum eValue = HnswGetValue(base, eElement); ListCell *lc2; foreach(lc2, r) { HnswCandidate *ri = lfirst(lc2); HnswElement riElement = HnswPtrAccess(base, ri->element); - float distance = HnswGetDistance(base, eElement, riElement, procinfo, collation); + Datum riValue = HnswGetValue(base, riElement); + float distance = HnswGetDistance(eValue, riValue, procinfo, collation); if (distance <= e->distance) return false; From 3126fbdb6f24fe32fe6ace7a83d72c4fdabab69c Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 9 Oct 2024 17:04:25 -0700 Subject: [PATCH 47/88] Use double for distance [skip ci] --- src/hnswutils.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hnswutils.c b/src/hnswutils.c index a7eb819..856c309 100644 --- a/src/hnswutils.c +++ b/src/hnswutils.c @@ -485,7 +485,7 @@ HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool loadHe /* * Calculate the distance between values */ -static inline float +static inline double HnswGetDistance(Datum a, Datum b, FmgrInfo *procinfo, Oid collation) { return DatumGetFloat8(FunctionCall2Coll(procinfo, collation, a, b)); From 57c05c59a2c5ba0f786d55a650da7517170fdf7e Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 9 Oct 2024 20:50:17 -0700 Subject: [PATCH 48/88] DRY code for forming index value --- src/hnsw.h | 3 ++- src/hnswbuild.c | 24 ++++++------------------ src/hnswinsert.c | 29 ++++++++--------------------- src/hnswutils.c | 27 +++++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 40 deletions(-) diff --git a/src/hnsw.h b/src/hnsw.h index 116d9bc..f9e5621 100644 --- a/src/hnsw.h +++ b/src/hnsw.h @@ -388,10 +388,11 @@ void HnswSetNeighborTuple(char *base, HnswNeighborTuple ntup, HnswElement e, in void HnswAddHeapTid(HnswElement element, ItemPointer heaptid); HnswNeighborArray *HnswInitNeighborArray(int lm, HnswAllocator * allocator); void HnswInitNeighbors(char *base, HnswElement element, int m, HnswAllocator * alloc); -bool HnswInsertTupleOnDisk(Relation index, Datum value, Datum *values, bool *isnull, ItemPointer heap_tid, bool building); +bool HnswInsertTupleOnDisk(Relation index, Datum value, ItemPointer heaptid, bool building); void HnswUpdateNeighborsOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, HnswElement e, int m, bool checkExisting, bool building); void HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool loadHeaptids, bool loadVec); void HnswLoadElement(HnswElement element, double *distance, Datum *q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec, double *maxDistance); +bool HnswFormIndexValue(Datum *out, Datum *values, bool *isnull, const HnswTypeInfo * typeInfo, FmgrInfo *normprocinfo, Oid collation); void HnswSetElementTuple(char *base, HnswElementTuple etup, HnswElement element); void HnswUpdateConnection(char *base, HnswNeighborArray * neighbors, HnswElement newElement, float distance, int lm, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation); bool HnswLoadNeighborTids(HnswElement element, ItemPointerData *indextids, Relation index, int m, int lm, int lc); diff --git a/src/hnswbuild.c b/src/hnswbuild.c index 87d4823..02e1749 100644 --- a/src/hnswbuild.c +++ b/src/hnswbuild.c @@ -473,7 +473,6 @@ InsertTupleInMemory(HnswBuildState * buildstate, HnswElement element) static bool InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid, HnswBuildState * buildstate) { - const HnswTypeInfo *typeInfo = buildstate->typeInfo; HnswGraph *graph = buildstate->graph; HnswElement element; HnswAllocator *allocator = &buildstate->allocator; @@ -481,22 +480,11 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid, Hn Pointer valuePtr; LWLock *flushLock = &graph->flushLock; char *base = buildstate->hnswarea; + Datum value; - /* Detoast once for all calls */ - Datum value = PointerGetDatum(PG_DETOAST_DATUM(values[0])); - - /* Check value */ - if (typeInfo->checkValue != NULL) - typeInfo->checkValue(DatumGetPointer(value)); - - /* Normalize if needed */ - if (buildstate->normprocinfo != NULL) - { - if (!HnswCheckNorm(buildstate->normprocinfo, buildstate->collation, value)) - return false; - - value = HnswNormValue(typeInfo, buildstate->collation, value); - } + /* Form index value */ + if (!HnswFormIndexValue(&value, values, isnull, buildstate->typeInfo, buildstate->normprocinfo, buildstate->collation)) + return false; /* Get datum size */ valueSize = VARSIZE_ANY(DatumGetPointer(value)); @@ -509,7 +497,7 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid, Hn { LWLockRelease(flushLock); - return HnswInsertTupleOnDisk(index, value, values, isnull, heaptid, true); + return HnswInsertTupleOnDisk(index, value, heaptid, true); } /* @@ -541,7 +529,7 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid, Hn LWLockRelease(flushLock); - return HnswInsertTupleOnDisk(index, value, values, isnull, heaptid, true); + return HnswInsertTupleOnDisk(index, value, heaptid, true); } /* Ok, we can proceed to allocate the element */ diff --git a/src/hnswinsert.c b/src/hnswinsert.c index 2dfd8d3..9c5b190 100644 --- a/src/hnswinsert.c +++ b/src/hnswinsert.c @@ -679,7 +679,7 @@ UpdateGraphOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, HnswElement * Insert a tuple into the index */ bool -HnswInsertTupleOnDisk(Relation index, Datum value, Datum *values, bool *isnull, ItemPointer heap_tid, bool building) +HnswInsertTupleOnDisk(Relation index, Datum value, ItemPointer heaptid, bool building) { HnswElement entryPoint; HnswElement element; @@ -701,7 +701,7 @@ HnswInsertTupleOnDisk(Relation index, Datum value, Datum *values, bool *isnull, HnswGetMetaPageInfo(index, &m, &entryPoint); /* Create an element */ - element = HnswInitElement(base, heap_tid, m, HnswGetMl(m), HnswGetMaxLevel(m), NULL); + element = HnswInitElement(base, heaptid, m, HnswGetMl(m), HnswGetMaxLevel(m), NULL); HnswPtrStore(base, element->value, DatumGetPointer(value)); /* Prevent concurrent inserts when likely updating entry point */ @@ -734,31 +734,18 @@ HnswInsertTupleOnDisk(Relation index, Datum value, Datum *values, bool *isnull, * Insert a tuple into the index */ static void -HnswInsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heap_tid) +HnswInsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid) { Datum value; const HnswTypeInfo *typeInfo = HnswGetTypeInfo(index); - FmgrInfo *normprocinfo; + FmgrInfo *normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC); Oid collation = index->rd_indcollation[0]; - /* Detoast once for all calls */ - value = PointerGetDatum(PG_DETOAST_DATUM(values[0])); + /* Form index value */ + if (!HnswFormIndexValue(&value, values, isnull, typeInfo, normprocinfo, collation)) + return; - /* Check value */ - if (typeInfo->checkValue != NULL) - typeInfo->checkValue(DatumGetPointer(value)); - - /* Normalize if needed */ - normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC); - if (normprocinfo != NULL) - { - if (!HnswCheckNorm(normprocinfo, collation, value)) - return; - - value = HnswNormValue(typeInfo, collation, value); - } - - HnswInsertTupleOnDisk(index, value, values, isnull, heap_tid, false); + HnswInsertTupleOnDisk(index, value, heaptid, false); } /* diff --git a/src/hnswutils.c b/src/hnswutils.c index 856c309..743fa87 100644 --- a/src/hnswutils.c +++ b/src/hnswutils.c @@ -394,6 +394,33 @@ HnswUpdateMetaPage(Relation index, int updateEntry, HnswElement entryPoint, Bloc UnlockReleaseBuffer(buf); } +/* + * Form index value + */ +bool +HnswFormIndexValue(Datum *out, Datum *values, bool *isnull, const HnswTypeInfo * typeInfo, FmgrInfo *normprocinfo, Oid collation) +{ + /* Detoast once for all calls */ + Datum value = PointerGetDatum(PG_DETOAST_DATUM(values[0])); + + /* Check value */ + if (typeInfo->checkValue != NULL) + typeInfo->checkValue(DatumGetPointer(value)); + + /* Normalize if needed */ + if (normprocinfo != NULL) + { + if (!HnswCheckNorm(normprocinfo, collation, value)) + return false; + + value = HnswNormValue(typeInfo, collation, value); + } + + *out = value; + + return true; +} + /* * Set element tuple, except for neighbor info */ From a98534e5ab4735c4a33adb8ea63aef5b832ae5c0 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 9 Oct 2024 21:03:18 -0700 Subject: [PATCH 49/88] DRY HNSW procinfo --- src/hnsw.h | 1 + src/hnswbuild.c | 4 +--- src/hnswinsert.c | 6 ++++-- src/hnswscan.c | 4 +--- src/hnswutils.c | 13 +++++++++++++ src/hnswvacuum.c | 4 ++-- 6 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/hnsw.h b/src/hnsw.h index f9e5621..10cc04a 100644 --- a/src/hnsw.h +++ b/src/hnsw.h @@ -370,6 +370,7 @@ typedef struct HnswVacuumState int HnswGetM(Relation index); int HnswGetEfConstruction(Relation index); FmgrInfo *HnswOptionalProcInfo(Relation index, uint16 procnum); +void HnswSetProcinfo(Relation index, FmgrInfo **procinfo, FmgrInfo **normprocinfo, Oid *collation); Datum HnswNormValue(const HnswTypeInfo * typeInfo, Oid collation, Datum value); bool HnswCheckNorm(FmgrInfo *procinfo, Oid collation, Datum value); Buffer HnswNewBuffer(Relation index, ForkNumber forkNum); diff --git a/src/hnswbuild.c b/src/hnswbuild.c index 02e1749..12a2169 100644 --- a/src/hnswbuild.c +++ b/src/hnswbuild.c @@ -692,9 +692,7 @@ InitBuildState(HnswBuildState * buildstate, Relation heap, Relation index, Index buildstate->indtuples = 0; /* Get support functions */ - buildstate->procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC); - buildstate->normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC); - buildstate->collation = index->rd_indcollation[0]; + HnswSetProcinfo(index, &buildstate->procinfo, &buildstate->normprocinfo, &buildstate->collation); InitGraph(&buildstate->graphData, NULL, (Size) maintenance_work_mem * 1024L); buildstate->graph = &buildstate->graphData; diff --git a/src/hnswinsert.c b/src/hnswinsert.c index 9c5b190..b916521 100644 --- a/src/hnswinsert.c +++ b/src/hnswinsert.c @@ -685,11 +685,13 @@ HnswInsertTupleOnDisk(Relation index, Datum value, ItemPointer heaptid, bool bui HnswElement element; int m; int efConstruction = HnswGetEfConstruction(index); - FmgrInfo *procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC); - Oid collation = index->rd_indcollation[0]; + FmgrInfo *procinfo; + Oid collation; LOCKMODE lockmode = ShareLock; char *base = NULL; + HnswSetProcinfo(index, &procinfo, NULL, &collation); + /* * Get a shared lock. This allows vacuum to ensure no in-flight inserts * before repairing graph. Use a page lock so it does not interfere with diff --git a/src/hnswscan.c b/src/hnswscan.c index 30815af..88ecf68 100644 --- a/src/hnswscan.c +++ b/src/hnswscan.c @@ -86,9 +86,7 @@ hnswbeginscan(Relation index, int nkeys, int norderbys) ALLOCSET_DEFAULT_SIZES); /* Set support functions */ - so->procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC); - so->normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC); - so->collation = index->rd_indcollation[0]; + HnswSetProcinfo(index, &so->procinfo, &so->normprocinfo, &so->collation); scan->opaque = so; diff --git a/src/hnswutils.c b/src/hnswutils.c index 743fa87..198e438 100644 --- a/src/hnswutils.c +++ b/src/hnswutils.c @@ -153,6 +153,19 @@ HnswOptionalProcInfo(Relation index, uint16 procnum) return index_getprocinfo(index, 1, procnum); } +/* + * Set procinfo + */ +void +HnswSetProcinfo(Relation index, FmgrInfo **procinfo, FmgrInfo **normprocinfo, Oid *collation) +{ + *procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC); + *collation = index->rd_indcollation[0]; + + if (normprocinfo != NULL) + *normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC); +} + /* * Normalize value */ diff --git a/src/hnswvacuum.c b/src/hnswvacuum.c index 67cc645..7931f85 100644 --- a/src/hnswvacuum.c +++ b/src/hnswvacuum.c @@ -573,13 +573,13 @@ InitVacuumState(HnswVacuumState * vacuumstate, IndexVacuumInfo *info, IndexBulkD vacuumstate->callback_state = callback_state; vacuumstate->efConstruction = HnswGetEfConstruction(index); vacuumstate->bas = GetAccessStrategy(BAS_BULKREAD); - vacuumstate->procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC); - vacuumstate->collation = index->rd_indcollation[0]; vacuumstate->ntup = palloc0(HNSW_TUPLE_ALLOC_SIZE); vacuumstate->tmpCtx = AllocSetContextCreate(CurrentMemoryContext, "Hnsw vacuum temporary context", ALLOCSET_DEFAULT_SIZES); + HnswSetProcinfo(index, &vacuumstate->procinfo, NULL, &vacuumstate->collation); + /* Get m from metapage */ HnswGetMetaPageInfo(index, &vacuumstate->m, NULL); From 17266ed409f6075569e886851a19f79a49606ed6 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 9 Oct 2024 21:49:32 -0700 Subject: [PATCH 50/88] Use inMemory for conditionals --- src/hnswutils.c | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/hnswutils.c b/src/hnswutils.c index 198e438..03c033e 100644 --- a/src/hnswutils.c +++ b/src/hnswutils.c @@ -598,9 +598,10 @@ HnswSearchCandidate * HnswEntryCandidate(char *base, HnswElement entryPoint, Datum q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec) { HnswSearchCandidate *sc = palloc(sizeof(HnswSearchCandidate)); + bool inMemory = index == NULL; HnswPtrStore(base, sc->element, entryPoint); - if (index == NULL) + if (inMemory) sc->distance = GetElementDistance(base, entryPoint, q, procinfo, collation); else HnswLoadElement(entryPoint, &sc->distance, &q, index, procinfo, collation, loadVec, NULL); @@ -644,9 +645,9 @@ CompareFurthestCandidates(const pairingheap_node *a, const pairingheap_node *b, * Init visited */ static inline void -InitVisited(char *base, visited_hash * v, Relation index, int ef, int m) +InitVisited(char *base, visited_hash * v, bool inMemory, int ef, int m) { - if (index != NULL) + if (!inMemory) v->tids = tidhash_create(CurrentMemoryContext, ef * m * 2, NULL); else if (base != NULL) v->offsets = offsethash_create(CurrentMemoryContext, ef * m * 2, NULL); @@ -658,9 +659,9 @@ InitVisited(char *base, visited_hash * v, Relation index, int ef, int m) * Add to visited */ static inline void -AddToVisited(char *base, visited_hash * v, HnswElementPtr elementPtr, Relation index, bool *found) +AddToVisited(char *base, visited_hash * v, HnswElementPtr elementPtr, bool inMemory, bool *found) { - if (index != NULL) + if (!inMemory) { HnswElement element = HnswPtrAccess(base, elementPtr); ItemPointerData indextid; @@ -721,7 +722,7 @@ HnswLoadUnvisitedFromMemory(char *base, HnswElement element, HnswUnvisited * unv HnswCandidate *hc = &localNeighborhood->items[i]; bool found; - AddToVisited(base, v, hc->element, NULL, &found); + AddToVisited(base, v, hc->element, true, &found); if (!found) unvisited[(*unvisitedLength)++].element = HnswPtrAccess(base, hc->element); @@ -805,11 +806,12 @@ HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, F int lm = HnswGetLayerM(m, lc); HnswUnvisited *unvisited = palloc(lm * sizeof(HnswUnvisited)); int unvisitedLength; + bool inMemory = index == NULL; - InitVisited(base, &v, index, ef, m); + InitVisited(base, &v, inMemory, ef, m); /* Create local memory for neighborhood if needed */ - if (index == NULL) + if (inMemory) { neighborhoodSize = HNSW_NEIGHBOR_ARRAY_SIZE(lm); localNeighborhood = palloc(neighborhoodSize); @@ -821,7 +823,7 @@ HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, F HnswSearchCandidate *sc = (HnswSearchCandidate *) lfirst(lc2); bool found; - AddToVisited(base, &v, sc->element, index, &found); + AddToVisited(base, &v, sc->element, inMemory, &found); pairingheap_add(C, &sc->c_node); pairingheap_add(W, &sc->w_node); @@ -846,7 +848,7 @@ HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, F cElement = HnswPtrAccess(base, c->element); - if (index == NULL) + if (inMemory) HnswLoadUnvisitedFromMemory(base, cElement, unvisited, &unvisitedLength, &v, lc, localNeighborhood, neighborhoodSize); else HnswLoadUnvisitedFromDisk(cElement, unvisited, &unvisitedLength, &v, index, m, lm, lc); @@ -860,7 +862,7 @@ HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, F f = HnswGetSearchCandidate(w_node, pairingheap_first(W)); - if (index == NULL) + if (inMemory) { eElement = unvisited[i].element; eDistance = GetElementDistance(base, eElement, q, procinfo, collation); @@ -1222,9 +1224,10 @@ HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint int entryLevel; Datum q = HnswGetValue(base, element); HnswElement skipElement = existing ? element : NULL; + bool inMemory = index == NULL; /* Precompute hash */ - if (index == NULL) + if (inMemory) PrecomputeHash(base, element); /* No neighbors if no entry point */ @@ -1273,7 +1276,7 @@ HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint /* Elements being deleted or skipped can help with search */ /* but should be removed before selecting neighbors */ - if (index != NULL) + if (!inMemory) lw = RemoveElements(base, lw, skipElement); /* From 45a6eef9e0cd9298dd0800941e9c6b5eaf159d17 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 9 Oct 2024 21:52:10 -0700 Subject: [PATCH 51/88] Improved variable name [skip ci] --- src/hnswscan.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hnswscan.c b/src/hnswscan.c index 88ecf68..c8af889 100644 --- a/src/hnswscan.c +++ b/src/hnswscan.c @@ -166,8 +166,8 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir) while (list_length(so->w) > 0) { char *base = NULL; - HnswSearchCandidate *hc = llast(so->w); - HnswElement element = HnswPtrAccess(base, hc->element); + HnswSearchCandidate *sc = llast(so->w); + HnswElement element = HnswPtrAccess(base, sc->element); ItemPointer heaptid; /* Move to next element if no valid heap TIDs */ From 064db12de7d0ca1625a77152d2ad13304f003639 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 9 Oct 2024 21:59:21 -0700 Subject: [PATCH 52/88] Moved procinfo initialization for inserts [skip ci] --- src/hnsw.h | 2 +- src/hnswbuild.c | 8 +++++--- src/hnswinsert.c | 15 +++++++-------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/hnsw.h b/src/hnsw.h index 10cc04a..364e03e 100644 --- a/src/hnsw.h +++ b/src/hnsw.h @@ -389,7 +389,7 @@ void HnswSetNeighborTuple(char *base, HnswNeighborTuple ntup, HnswElement e, in void HnswAddHeapTid(HnswElement element, ItemPointer heaptid); HnswNeighborArray *HnswInitNeighborArray(int lm, HnswAllocator * allocator); void HnswInitNeighbors(char *base, HnswElement element, int m, HnswAllocator * alloc); -bool HnswInsertTupleOnDisk(Relation index, Datum value, ItemPointer heaptid, bool building); +bool HnswInsertTupleOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, Datum value, ItemPointer heaptid, bool building); void HnswUpdateNeighborsOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, HnswElement e, int m, bool checkExisting, bool building); void HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool loadHeaptids, bool loadVec); void HnswLoadElement(HnswElement element, double *distance, Datum *q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec, double *maxDistance); diff --git a/src/hnswbuild.c b/src/hnswbuild.c index 12a2169..6fab132 100644 --- a/src/hnswbuild.c +++ b/src/hnswbuild.c @@ -476,6 +476,8 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid, Hn HnswGraph *graph = buildstate->graph; HnswElement element; HnswAllocator *allocator = &buildstate->allocator; + FmgrInfo *procinfo = buildstate->procinfo; + Oid collation = buildstate->collation; Size valueSize; Pointer valuePtr; LWLock *flushLock = &graph->flushLock; @@ -483,7 +485,7 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid, Hn Datum value; /* Form index value */ - if (!HnswFormIndexValue(&value, values, isnull, buildstate->typeInfo, buildstate->normprocinfo, buildstate->collation)) + if (!HnswFormIndexValue(&value, values, isnull, buildstate->typeInfo, buildstate->normprocinfo, collation)) return false; /* Get datum size */ @@ -497,7 +499,7 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid, Hn { LWLockRelease(flushLock); - return HnswInsertTupleOnDisk(index, value, heaptid, true); + return HnswInsertTupleOnDisk(index, procinfo, collation, value, heaptid, true); } /* @@ -529,7 +531,7 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid, Hn LWLockRelease(flushLock); - return HnswInsertTupleOnDisk(index, value, heaptid, true); + return HnswInsertTupleOnDisk(index, procinfo, collation, value, heaptid, true); } /* Ok, we can proceed to allocate the element */ diff --git a/src/hnswinsert.c b/src/hnswinsert.c index b916521..45530b9 100644 --- a/src/hnswinsert.c +++ b/src/hnswinsert.c @@ -679,19 +679,15 @@ UpdateGraphOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, HnswElement * Insert a tuple into the index */ bool -HnswInsertTupleOnDisk(Relation index, Datum value, ItemPointer heaptid, bool building) +HnswInsertTupleOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, Datum value, ItemPointer heaptid, bool building) { HnswElement entryPoint; HnswElement element; int m; int efConstruction = HnswGetEfConstruction(index); - FmgrInfo *procinfo; - Oid collation; LOCKMODE lockmode = ShareLock; char *base = NULL; - HnswSetProcinfo(index, &procinfo, NULL, &collation); - /* * Get a shared lock. This allows vacuum to ensure no in-flight inserts * before repairing graph. Use a page lock so it does not interfere with @@ -740,14 +736,17 @@ HnswInsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid { Datum value; const HnswTypeInfo *typeInfo = HnswGetTypeInfo(index); - FmgrInfo *normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC); - Oid collation = index->rd_indcollation[0]; + FmgrInfo *procinfo; + FmgrInfo *normprocinfo; + Oid collation; + + HnswSetProcinfo(index, &procinfo, &normprocinfo, &collation); /* Form index value */ if (!HnswFormIndexValue(&value, values, isnull, typeInfo, normprocinfo, collation)) return; - HnswInsertTupleOnDisk(index, value, heaptid, false); + HnswInsertTupleOnDisk(index, procinfo, collation, value, heaptid, false); } /* From 32ab27d72a36f4aced101a5d8f42e1fc78d42024 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 9 Oct 2024 23:10:26 -0700 Subject: [PATCH 53/88] Added HnswSupport struct for support functions --- src/hnsw.h | 38 ++++++++++++----------- src/hnswbuild.c | 26 ++++++++-------- src/hnswinsert.c | 34 ++++++++++---------- src/hnswscan.c | 15 +++++---- src/hnswutils.c | 80 +++++++++++++++++++++++------------------------- src/hnswvacuum.c | 14 ++++----- 6 files changed, 101 insertions(+), 106 deletions(-) diff --git a/src/hnsw.h b/src/hnsw.h index 364e03e..bfc12f6 100644 --- a/src/hnsw.h +++ b/src/hnsw.h @@ -237,6 +237,13 @@ typedef struct HnswTypeInfo void (*checkValue) (Pointer v); } HnswTypeInfo; +typedef struct HnswSupport +{ + FmgrInfo *procinfo; + FmgrInfo *normprocinfo; + Oid collation; +} HnswSupport; + typedef struct HnswBuildState { /* Info */ @@ -256,9 +263,7 @@ typedef struct HnswBuildState double reltuples; /* Support functions */ - FmgrInfo *procinfo; - FmgrInfo *normprocinfo; - Oid collation; + HnswSupport support; /* Variables */ HnswGraph graphData; @@ -333,9 +338,7 @@ typedef struct HnswScanOpaqueData MemoryContext tmpCtx; /* Support functions */ - FmgrInfo *procinfo; - FmgrInfo *normprocinfo; - Oid collation; + HnswSupport support; } HnswScanOpaqueData; typedef HnswScanOpaqueData * HnswScanOpaque; @@ -353,8 +356,7 @@ typedef struct HnswVacuumState int efConstruction; /* Support functions */ - FmgrInfo *procinfo; - Oid collation; + HnswSupport support; /* Variables */ struct tidhash_hash *deleted; @@ -370,32 +372,32 @@ typedef struct HnswVacuumState int HnswGetM(Relation index); int HnswGetEfConstruction(Relation index); FmgrInfo *HnswOptionalProcInfo(Relation index, uint16 procnum); -void HnswSetProcinfo(Relation index, FmgrInfo **procinfo, FmgrInfo **normprocinfo, Oid *collation); +void HnswInitSupport(HnswSupport * support, Relation index); Datum HnswNormValue(const HnswTypeInfo * typeInfo, Oid collation, Datum value); -bool HnswCheckNorm(FmgrInfo *procinfo, Oid collation, Datum value); +bool HnswCheckNorm(HnswSupport * support, Datum value); Buffer HnswNewBuffer(Relation index, ForkNumber forkNum); void HnswInitPage(Buffer buf, Page page); void HnswInit(void); -List *HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, FmgrInfo *procinfo, Oid collation, int m, bool inserting, HnswElement skipElement); +List *HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, HnswSupport * support, int m, bool inserting, HnswElement skipElement); HnswElement HnswGetEntryPoint(Relation index); void HnswGetMetaPageInfo(Relation index, int *m, HnswElement * entryPoint); void *HnswAlloc(HnswAllocator * allocator, Size size); HnswElement HnswInitElement(char *base, ItemPointer tid, int m, double ml, int maxLevel, HnswAllocator * alloc); HnswElement HnswInitElementFromBlock(BlockNumber blkno, OffsetNumber offno); -void HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint, Relation index, FmgrInfo *procinfo, Oid collation, int m, int efConstruction, bool existing); -HnswSearchCandidate *HnswEntryCandidate(char *base, HnswElement em, Datum q, Relation rel, FmgrInfo *procinfo, Oid collation, bool loadVec); +void HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint, Relation index, HnswSupport * support, int m, int efConstruction, bool existing); +HnswSearchCandidate *HnswEntryCandidate(char *base, HnswElement em, Datum q, Relation rel, HnswSupport * support, bool loadVec); void HnswUpdateMetaPage(Relation index, int updateEntry, HnswElement entryPoint, BlockNumber insertPage, ForkNumber forkNum, bool building); void HnswSetNeighborTuple(char *base, HnswNeighborTuple ntup, HnswElement e, int m); void HnswAddHeapTid(HnswElement element, ItemPointer heaptid); HnswNeighborArray *HnswInitNeighborArray(int lm, HnswAllocator * allocator); void HnswInitNeighbors(char *base, HnswElement element, int m, HnswAllocator * alloc); -bool HnswInsertTupleOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, Datum value, ItemPointer heaptid, bool building); -void HnswUpdateNeighborsOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, HnswElement e, int m, bool checkExisting, bool building); +bool HnswInsertTupleOnDisk(Relation index, HnswSupport * support, Datum value, ItemPointer heaptid, bool building); +void HnswUpdateNeighborsOnDisk(Relation index, HnswSupport * support, HnswElement e, int m, bool checkExisting, bool building); void HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool loadHeaptids, bool loadVec); -void HnswLoadElement(HnswElement element, double *distance, Datum *q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec, double *maxDistance); -bool HnswFormIndexValue(Datum *out, Datum *values, bool *isnull, const HnswTypeInfo * typeInfo, FmgrInfo *normprocinfo, Oid collation); +void HnswLoadElement(HnswElement element, double *distance, Datum *q, Relation index, HnswSupport * support, bool loadVec, double *maxDistance); +bool HnswFormIndexValue(Datum *out, Datum *values, bool *isnull, const HnswTypeInfo * typeInfo, HnswSupport * support); void HnswSetElementTuple(char *base, HnswElementTuple etup, HnswElement element); -void HnswUpdateConnection(char *base, HnswNeighborArray * neighbors, HnswElement newElement, float distance, int lm, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation); +void HnswUpdateConnection(char *base, HnswNeighborArray * neighbors, HnswElement newElement, float distance, int lm, int *updateIdx, Relation index, HnswSupport * support); bool HnswLoadNeighborTids(HnswElement element, ItemPointerData *indextids, Relation index, int m, int lm, int lc); void HnswInitLockTranche(void); const HnswTypeInfo *HnswGetTypeInfo(Relation index); diff --git a/src/hnswbuild.c b/src/hnswbuild.c index 6fab132..b667478 100644 --- a/src/hnswbuild.c +++ b/src/hnswbuild.c @@ -366,7 +366,7 @@ AddElementInMemory(char *base, HnswGraph * graph, HnswElement element) * Update neighbors */ static void -UpdateNeighborsInMemory(char *base, FmgrInfo *procinfo, Oid collation, HnswElement e, int m) +UpdateNeighborsInMemory(char *base, HnswSupport * support, HnswElement e, int m) { for (int lc = e->level; lc >= 0; lc--) { @@ -388,7 +388,7 @@ UpdateNeighborsInMemory(char *base, FmgrInfo *procinfo, Oid collation, HnswEleme Assert(neighborElement); LWLockAcquire(&neighborElement->lock, LW_EXCLUSIVE); - HnswUpdateConnection(base, HnswGetNeighbors(base, neighborElement, lc), e, hc->distance, lm, NULL, NULL, procinfo, collation); + HnswUpdateConnection(base, HnswGetNeighbors(base, neighborElement, lc), e, hc->distance, lm, NULL, NULL, support); LWLockRelease(&neighborElement->lock); } } @@ -398,7 +398,7 @@ UpdateNeighborsInMemory(char *base, FmgrInfo *procinfo, Oid collation, HnswEleme * Update graph in memory */ static void -UpdateGraphInMemory(FmgrInfo *procinfo, Oid collation, HnswElement element, int m, int efConstruction, HnswElement entryPoint, HnswBuildState * buildstate) +UpdateGraphInMemory(HnswSupport * support, HnswElement element, int m, int efConstruction, HnswElement entryPoint, HnswBuildState * buildstate) { HnswGraph *graph = buildstate->graph; char *base = buildstate->hnswarea; @@ -411,7 +411,7 @@ UpdateGraphInMemory(FmgrInfo *procinfo, Oid collation, HnswElement element, int AddElementInMemory(base, graph, element); /* Update neighbors */ - UpdateNeighborsInMemory(base, procinfo, collation, element, m); + UpdateNeighborsInMemory(base, support, element, m); /* Update entry point if needed (already have lock) */ if (entryPoint == NULL || element->level > entryPoint->level) @@ -424,9 +424,8 @@ UpdateGraphInMemory(FmgrInfo *procinfo, Oid collation, HnswElement element, int static void InsertTupleInMemory(HnswBuildState * buildstate, HnswElement element) { - FmgrInfo *procinfo = buildstate->procinfo; - Oid collation = buildstate->collation; HnswGraph *graph = buildstate->graph; + HnswSupport *support = &buildstate->support; HnswElement entryPoint; LWLock *entryLock = &graph->entryLock; LWLock *entryWaitLock = &graph->entryWaitLock; @@ -458,10 +457,10 @@ InsertTupleInMemory(HnswBuildState * buildstate, HnswElement element) } /* Find neighbors for element */ - HnswFindElementNeighbors(base, element, entryPoint, NULL, procinfo, collation, m, efConstruction, false); + HnswFindElementNeighbors(base, element, entryPoint, NULL, support, m, efConstruction, false); /* Update graph in memory */ - UpdateGraphInMemory(procinfo, collation, element, m, efConstruction, entryPoint, buildstate); + UpdateGraphInMemory(support, element, m, efConstruction, entryPoint, buildstate); /* Release entry lock */ LWLockRelease(entryLock); @@ -476,8 +475,7 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid, Hn HnswGraph *graph = buildstate->graph; HnswElement element; HnswAllocator *allocator = &buildstate->allocator; - FmgrInfo *procinfo = buildstate->procinfo; - Oid collation = buildstate->collation; + HnswSupport *support = &buildstate->support; Size valueSize; Pointer valuePtr; LWLock *flushLock = &graph->flushLock; @@ -485,7 +483,7 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid, Hn Datum value; /* Form index value */ - if (!HnswFormIndexValue(&value, values, isnull, buildstate->typeInfo, buildstate->normprocinfo, collation)) + if (!HnswFormIndexValue(&value, values, isnull, buildstate->typeInfo, support)) return false; /* Get datum size */ @@ -499,7 +497,7 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid, Hn { LWLockRelease(flushLock); - return HnswInsertTupleOnDisk(index, procinfo, collation, value, heaptid, true); + return HnswInsertTupleOnDisk(index, support, value, heaptid, true); } /* @@ -531,7 +529,7 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid, Hn LWLockRelease(flushLock); - return HnswInsertTupleOnDisk(index, procinfo, collation, value, heaptid, true); + return HnswInsertTupleOnDisk(index, support, value, heaptid, true); } /* Ok, we can proceed to allocate the element */ @@ -694,7 +692,7 @@ InitBuildState(HnswBuildState * buildstate, Relation heap, Relation index, Index buildstate->indtuples = 0; /* Get support functions */ - HnswSetProcinfo(index, &buildstate->procinfo, &buildstate->normprocinfo, &buildstate->collation); + HnswInitSupport(&buildstate->support, index); InitGraph(&buildstate->graphData, NULL, (Size) maintenance_work_mem * 1024L); buildstate->graph = &buildstate->graphData; diff --git a/src/hnswinsert.c b/src/hnswinsert.c index 45530b9..87204ca 100644 --- a/src/hnswinsert.c +++ b/src/hnswinsert.c @@ -368,7 +368,7 @@ HnswLoadNeighbors(HnswElement element, Relation index, int m, int lm, int lc) * Load elements for insert */ static void -LoadElementsForInsert(HnswNeighborArray * neighbors, Datum q, int *idx, Relation index, FmgrInfo *procinfo, Oid collation) +LoadElementsForInsert(HnswNeighborArray * neighbors, Datum q, int *idx, Relation index, HnswSupport * support) { char *base = NULL; @@ -378,7 +378,7 @@ LoadElementsForInsert(HnswNeighborArray * neighbors, Datum q, int *idx, Relation HnswElement element = HnswPtrAccess(base, hc->element); double distance; - HnswLoadElement(element, &distance, &q, index, procinfo, collation, true, NULL); + HnswLoadElement(element, &distance, &q, index, support, true, NULL); hc->distance = distance; /* Prune element if being deleted */ @@ -394,7 +394,7 @@ LoadElementsForInsert(HnswNeighborArray * neighbors, Datum q, int *idx, Relation * Get update index */ static int -GetUpdateIndex(HnswElement element, HnswElement newElement, float distance, int m, int lm, int lc, Relation index, FmgrInfo *procinfo, Oid collation, MemoryContext updateCtx) +GetUpdateIndex(HnswElement element, HnswElement newElement, float distance, int m, int lm, int lc, Relation index, HnswSupport * support, MemoryContext updateCtx) { char *base = NULL; int idx = -1; @@ -421,10 +421,10 @@ GetUpdateIndex(HnswElement element, HnswElement newElement, float distance, int { Datum q = HnswGetValue(base, element); - LoadElementsForInsert(neighbors, q, &idx, index, procinfo, collation); + LoadElementsForInsert(neighbors, q, &idx, index, support); if (idx == -1) - HnswUpdateConnection(base, neighbors, newElement, distance, lm, &idx, index, procinfo, collation); + HnswUpdateConnection(base, neighbors, newElement, distance, lm, &idx, index, support); } MemoryContextSwitchTo(oldCtx); @@ -529,7 +529,7 @@ UpdateNeighborOnDisk(HnswElement element, HnswElement newElement, int idx, int m * Update neighbors */ void -HnswUpdateNeighborsOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, HnswElement e, int m, bool checkExisting, bool building) +HnswUpdateNeighborsOnDisk(Relation index, HnswSupport * support, HnswElement e, int m, bool checkExisting, bool building) { char *base = NULL; @@ -552,7 +552,7 @@ HnswUpdateNeighborsOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, Hns HnswElement neighborElement = HnswPtrAccess(base, hc->element); int idx; - idx = GetUpdateIndex(neighborElement, e, hc->distance, m, lm, lc, index, procinfo, collation, updateCtx); + idx = GetUpdateIndex(neighborElement, e, hc->distance, m, lm, lc, index, support, updateCtx); /* New element was not selected as a neighbor */ if (idx == -1) @@ -652,7 +652,7 @@ FindDuplicateOnDisk(Relation index, HnswElement element, bool building) * Update graph on disk */ static void -UpdateGraphOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, HnswElement element, int m, int efConstruction, HnswElement entryPoint, bool building) +UpdateGraphOnDisk(Relation index, HnswSupport * support, HnswElement element, int m, int efConstruction, HnswElement entryPoint, bool building) { BlockNumber newInsertPage = InvalidBlockNumber; @@ -668,7 +668,7 @@ UpdateGraphOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, HnswElement HnswUpdateMetaPage(index, 0, NULL, newInsertPage, MAIN_FORKNUM, building); /* Update neighbors */ - HnswUpdateNeighborsOnDisk(index, procinfo, collation, element, m, false, building); + HnswUpdateNeighborsOnDisk(index, support, element, m, false, building); /* Update entry point if needed */ if (entryPoint == NULL || element->level > entryPoint->level) @@ -679,7 +679,7 @@ UpdateGraphOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, HnswElement * Insert a tuple into the index */ bool -HnswInsertTupleOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, Datum value, ItemPointer heaptid, bool building) +HnswInsertTupleOnDisk(Relation index, HnswSupport * support, Datum value, ItemPointer heaptid, bool building) { HnswElement entryPoint; HnswElement element; @@ -717,10 +717,10 @@ HnswInsertTupleOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, Datum v } /* Find neighbors for element */ - HnswFindElementNeighbors(base, element, entryPoint, index, procinfo, collation, m, efConstruction, false); + HnswFindElementNeighbors(base, element, entryPoint, index, support, m, efConstruction, false); /* Update graph on disk */ - UpdateGraphOnDisk(index, procinfo, collation, element, m, efConstruction, entryPoint, building); + UpdateGraphOnDisk(index, support, element, m, efConstruction, entryPoint, building); /* Release lock */ UnlockPage(index, HNSW_UPDATE_LOCK, lockmode); @@ -736,17 +736,15 @@ HnswInsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid { Datum value; const HnswTypeInfo *typeInfo = HnswGetTypeInfo(index); - FmgrInfo *procinfo; - FmgrInfo *normprocinfo; - Oid collation; + HnswSupport support; - HnswSetProcinfo(index, &procinfo, &normprocinfo, &collation); + HnswInitSupport(&support, index); /* Form index value */ - if (!HnswFormIndexValue(&value, values, isnull, typeInfo, normprocinfo, collation)) + if (!HnswFormIndexValue(&value, values, isnull, typeInfo, &support)) return; - HnswInsertTupleOnDisk(index, procinfo, collation, value, heaptid, false); + HnswInsertTupleOnDisk(index, &support, value, heaptid, false); } /* diff --git a/src/hnswscan.c b/src/hnswscan.c index c8af889..e3aaced 100644 --- a/src/hnswscan.c +++ b/src/hnswscan.c @@ -15,8 +15,7 @@ GetScanItems(IndexScanDesc scan, Datum q) { HnswScanOpaque so = (HnswScanOpaque) scan->opaque; Relation index = scan->indexRelation; - FmgrInfo *procinfo = so->procinfo; - Oid collation = so->collation; + HnswSupport *support = &so->support; List *ep; List *w; int m; @@ -29,15 +28,15 @@ GetScanItems(IndexScanDesc scan, Datum q) if (entryPoint == NULL) return NIL; - ep = list_make1(HnswEntryCandidate(base, entryPoint, q, index, procinfo, collation, false)); + ep = list_make1(HnswEntryCandidate(base, entryPoint, q, index, support, false)); for (int lc = entryPoint->level; lc >= 1; lc--) { - w = HnswSearchLayer(base, q, ep, 1, lc, index, procinfo, collation, m, false, NULL); + w = HnswSearchLayer(base, q, ep, 1, lc, index, support, m, false, NULL); ep = w; } - return HnswSearchLayer(base, q, ep, hnsw_ef_search, 0, index, procinfo, collation, m, false, NULL); + return HnswSearchLayer(base, q, ep, hnsw_ef_search, 0, index, support, m, false, NULL); } /* @@ -60,8 +59,8 @@ GetScanValue(IndexScanDesc scan) Assert(!VARATT_IS_EXTENDED(DatumGetPointer(value))); /* Normalize if needed */ - if (so->normprocinfo != NULL) - value = HnswNormValue(so->typeInfo, so->collation, value); + if (so->support.normprocinfo != NULL) + value = HnswNormValue(so->typeInfo, so->support.collation, value); } return value; @@ -86,7 +85,7 @@ hnswbeginscan(Relation index, int nkeys, int norderbys) ALLOCSET_DEFAULT_SIZES); /* Set support functions */ - HnswSetProcinfo(index, &so->procinfo, &so->normprocinfo, &so->collation); + HnswInitSupport(&so->support, index); scan->opaque = so; diff --git a/src/hnswutils.c b/src/hnswutils.c index 03c033e..7fa0720 100644 --- a/src/hnswutils.c +++ b/src/hnswutils.c @@ -154,16 +154,14 @@ HnswOptionalProcInfo(Relation index, uint16 procnum) } /* - * Set procinfo + * Init support functions */ void -HnswSetProcinfo(Relation index, FmgrInfo **procinfo, FmgrInfo **normprocinfo, Oid *collation) +HnswInitSupport(HnswSupport * support, Relation index) { - *procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC); - *collation = index->rd_indcollation[0]; - - if (normprocinfo != NULL) - *normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC); + support->procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC); + support->collation = index->rd_indcollation[0]; + support->normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC); } /* @@ -179,9 +177,9 @@ HnswNormValue(const HnswTypeInfo * typeInfo, Oid collation, Datum value) * Check if non-zero norm */ bool -HnswCheckNorm(FmgrInfo *procinfo, Oid collation, Datum value) +HnswCheckNorm(HnswSupport * support, Datum value) { - return DatumGetFloat8(FunctionCall1Coll(procinfo, collation, value)) > 0; + return DatumGetFloat8(FunctionCall1Coll(support->normprocinfo, support->collation, value)) > 0; } /* @@ -411,7 +409,7 @@ HnswUpdateMetaPage(Relation index, int updateEntry, HnswElement entryPoint, Bloc * Form index value */ bool -HnswFormIndexValue(Datum *out, Datum *values, bool *isnull, const HnswTypeInfo * typeInfo, FmgrInfo *normprocinfo, Oid collation) +HnswFormIndexValue(Datum *out, Datum *values, bool *isnull, const HnswTypeInfo * typeInfo, HnswSupport * support) { /* Detoast once for all calls */ Datum value = PointerGetDatum(PG_DETOAST_DATUM(values[0])); @@ -421,12 +419,12 @@ HnswFormIndexValue(Datum *out, Datum *values, bool *isnull, const HnswTypeInfo * typeInfo->checkValue(DatumGetPointer(value)); /* Normalize if needed */ - if (normprocinfo != NULL) + if (support->normprocinfo != NULL) { - if (!HnswCheckNorm(normprocinfo, collation, value)) + if (!HnswCheckNorm(support, value)) return false; - value = HnswNormValue(typeInfo, collation, value); + value = HnswNormValue(typeInfo, support->collation, value); } *out = value; @@ -526,16 +524,16 @@ HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool loadHe * Calculate the distance between values */ static inline double -HnswGetDistance(Datum a, Datum b, FmgrInfo *procinfo, Oid collation) +HnswGetDistance(Datum a, Datum b, HnswSupport * support) { - return DatumGetFloat8(FunctionCall2Coll(procinfo, collation, a, b)); + return DatumGetFloat8(FunctionCall2Coll(support->procinfo, support->collation, a, b)); } /* * Load an element and optionally get its distance from q */ static void -HnswLoadElementImpl(BlockNumber blkno, OffsetNumber offno, double *distance, Datum *q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec, double *maxDistance, HnswElement * element) +HnswLoadElementImpl(BlockNumber blkno, OffsetNumber offno, double *distance, Datum *q, Relation index, HnswSupport * support, bool loadVec, double *maxDistance, HnswElement * element) { Buffer buf; Page page; @@ -556,7 +554,7 @@ HnswLoadElementImpl(BlockNumber blkno, OffsetNumber offno, double *distance, Dat if (DatumGetPointer(*q) == NULL) *distance = 0; else - *distance = HnswGetDistance(*q, PointerGetDatum(&etup->data), procinfo, collation); + *distance = HnswGetDistance(*q, PointerGetDatum(&etup->data), support); } /* Load element */ @@ -575,36 +573,36 @@ HnswLoadElementImpl(BlockNumber blkno, OffsetNumber offno, double *distance, Dat * Load an element and optionally get its distance from q */ void -HnswLoadElement(HnswElement element, double *distance, Datum *q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec, double *maxDistance) +HnswLoadElement(HnswElement element, double *distance, Datum *q, Relation index, HnswSupport * support, bool loadVec, double *maxDistance) { - HnswLoadElementImpl(element->blkno, element->offno, distance, q, index, procinfo, collation, loadVec, maxDistance, &element); + HnswLoadElementImpl(element->blkno, element->offno, distance, q, index, support, loadVec, maxDistance, &element); } /* * Get the distance for an element */ static double -GetElementDistance(char *base, HnswElement element, Datum q, FmgrInfo *procinfo, Oid collation) +GetElementDistance(char *base, HnswElement element, Datum q, HnswSupport * support) { Datum value = HnswGetValue(base, element); - return HnswGetDistance(q, value, procinfo, collation); + return HnswGetDistance(q, value, support); } /* * Create a candidate for the entry point */ HnswSearchCandidate * -HnswEntryCandidate(char *base, HnswElement entryPoint, Datum q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec) +HnswEntryCandidate(char *base, HnswElement entryPoint, Datum q, Relation index, HnswSupport * support, bool loadVec) { HnswSearchCandidate *sc = palloc(sizeof(HnswSearchCandidate)); bool inMemory = index == NULL; HnswPtrStore(base, sc->element, entryPoint); if (inMemory) - sc->distance = GetElementDistance(base, entryPoint, q, procinfo, collation); + sc->distance = GetElementDistance(base, entryPoint, q, support); else - HnswLoadElement(entryPoint, &sc->distance, &q, index, procinfo, collation, loadVec, NULL); + HnswLoadElement(entryPoint, &sc->distance, &q, index, support, loadVec, NULL); return sc; } @@ -793,7 +791,7 @@ HnswLoadUnvisitedFromDisk(HnswElement element, HnswUnvisited * unvisited, int *u * Algorithm 2 from paper */ List * -HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, FmgrInfo *procinfo, Oid collation, int m, bool inserting, HnswElement skipElement) +HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, HnswSupport * support, int m, bool inserting, HnswElement skipElement) { List *w = NIL; pairingheap *C = pairingheap_allocate(CompareNearestCandidates, NULL); @@ -865,7 +863,7 @@ HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, F if (inMemory) { eElement = unvisited[i].element; - eDistance = GetElementDistance(base, eElement, q, procinfo, collation); + eDistance = GetElementDistance(base, eElement, q, support); } else { @@ -875,7 +873,7 @@ HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, F /* Avoid any allocations if not adding */ eElement = NULL; - HnswLoadElementImpl(blkno, offno, &eDistance, &q, index, procinfo, collation, inserting, alwaysAdd ? NULL : &f->distance, &eElement); + HnswLoadElementImpl(blkno, offno, &eDistance, &q, index, support, inserting, alwaysAdd ? NULL : &f->distance, &eElement); if (eElement == NULL) continue; @@ -976,7 +974,7 @@ CompareCandidateDistancesOffset(const ListCell *a, const ListCell *b) * Check if an element is closer to q than any element from R */ static bool -CheckElementCloser(char *base, HnswCandidate * e, List *r, FmgrInfo *procinfo, Oid collation) +CheckElementCloser(char *base, HnswCandidate * e, List *r, HnswSupport * support) { HnswElement eElement = HnswPtrAccess(base, e->element); Datum eValue = HnswGetValue(base, eElement); @@ -987,7 +985,7 @@ CheckElementCloser(char *base, HnswCandidate * e, List *r, FmgrInfo *procinfo, O HnswCandidate *ri = lfirst(lc2); HnswElement riElement = HnswPtrAccess(base, ri->element); Datum riValue = HnswGetValue(base, riElement); - float distance = HnswGetDistance(eValue, riValue, procinfo, collation); + float distance = HnswGetDistance(eValue, riValue, support); if (distance <= e->distance) return false; @@ -1000,7 +998,7 @@ CheckElementCloser(char *base, HnswCandidate * e, List *r, FmgrInfo *procinfo, O * Algorithm 4 from paper */ static List * -SelectNeighbors(char *base, List *c, int lm, FmgrInfo *procinfo, Oid collation, bool *closerSet, HnswCandidate * newCandidate, HnswCandidate * *pruned, bool sortCandidates) +SelectNeighbors(char *base, List *c, int lm, HnswSupport * support, bool *closerSet, HnswCandidate * newCandidate, HnswCandidate * *pruned, bool sortCandidates) { List *r = NIL; List *w = list_copy(c); @@ -1034,7 +1032,7 @@ SelectNeighbors(char *base, List *c, int lm, FmgrInfo *procinfo, Oid collation, /* Use previous state of r and wd to skip work when possible */ if (mustCalculate) - e->closer = CheckElementCloser(base, e, r, procinfo, collation); + e->closer = CheckElementCloser(base, e, r, support); else if (list_length(added) > 0) { /* Keep Valgrind happy for in-memory, parallel builds */ @@ -1047,7 +1045,7 @@ SelectNeighbors(char *base, List *c, int lm, FmgrInfo *procinfo, Oid collation, */ if (e->closer) { - e->closer = CheckElementCloser(base, e, added, procinfo, collation); + e->closer = CheckElementCloser(base, e, added, support); if (!e->closer) removedAny = true; @@ -1060,7 +1058,7 @@ SelectNeighbors(char *base, List *c, int lm, FmgrInfo *procinfo, Oid collation, */ if (removedAny) { - e->closer = CheckElementCloser(base, e, r, procinfo, collation); + e->closer = CheckElementCloser(base, e, r, support); if (e->closer) added = lappend(added, e); } @@ -1068,7 +1066,7 @@ SelectNeighbors(char *base, List *c, int lm, FmgrInfo *procinfo, Oid collation, } else if (e == newCandidate) { - e->closer = CheckElementCloser(base, e, r, procinfo, collation); + e->closer = CheckElementCloser(base, e, r, support); if (e->closer) added = lappend(added, e); } @@ -1119,7 +1117,7 @@ AddConnections(char *base, HnswElement element, List *neighbors, int lc) * Update connections */ void -HnswUpdateConnection(char *base, HnswNeighborArray * neighbors, HnswElement newElement, float distance, int lm, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation) +HnswUpdateConnection(char *base, HnswNeighborArray * neighbors, HnswElement newElement, float distance, int lm, int *updateIdx, Relation index, HnswSupport * support) { HnswCandidate newHc; @@ -1145,7 +1143,7 @@ HnswUpdateConnection(char *base, HnswNeighborArray * neighbors, HnswElement newE c = lappend(c, &neighbors->items[i]); c = lappend(c, &newHc); - SelectNeighbors(base, c, lm, procinfo, collation, &neighbors->closerSet, &newHc, &pruned, true); + SelectNeighbors(base, c, lm, support, &neighbors->closerSet, &newHc, &pruned, true); /* Should not happen */ if (pruned == NULL) @@ -1216,7 +1214,7 @@ PrecomputeHash(char *base, HnswElement element) * Algorithm 1 from paper */ void -HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint, Relation index, FmgrInfo *procinfo, Oid collation, int m, int efConstruction, bool existing) +HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint, Relation index, HnswSupport * support, int m, int efConstruction, bool existing) { List *ep; List *w; @@ -1235,13 +1233,13 @@ HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint return; /* Get entry point and level */ - ep = list_make1(HnswEntryCandidate(base, entryPoint, q, index, procinfo, collation, true)); + ep = list_make1(HnswEntryCandidate(base, entryPoint, q, index, support, true)); entryLevel = entryPoint->level; /* 1st phase: greedy search to insert level */ for (int lc = entryLevel; lc >= level + 1; lc--) { - w = HnswSearchLayer(base, q, ep, 1, lc, index, procinfo, collation, m, true, skipElement); + w = HnswSearchLayer(base, q, ep, 1, lc, index, support, m, true, skipElement); ep = w; } @@ -1260,7 +1258,7 @@ HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint List *lw = NIL; ListCell *lc2; - w = HnswSearchLayer(base, q, ep, efConstruction, lc, index, procinfo, collation, m, true, skipElement); + w = HnswSearchLayer(base, q, ep, efConstruction, lc, index, support, m, true, skipElement); /* Convert search candidates to candidates */ foreach(lc2, w) @@ -1284,7 +1282,7 @@ HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint * sortCandidates to true for in-memory builds to enable closer * caching, but there does not seem to be a difference in performance. */ - neighbors = SelectNeighbors(base, lw, lm, procinfo, collation, &HnswGetNeighbors(base, element, lc)->closerSet, NULL, NULL, false); + neighbors = SelectNeighbors(base, lw, lm, support, &HnswGetNeighbors(base, element, lc)->closerSet, NULL, NULL, false); AddConnections(base, element, neighbors, lc); diff --git a/src/hnswvacuum.c b/src/hnswvacuum.c index 7931f85..d3cdf68 100644 --- a/src/hnswvacuum.c +++ b/src/hnswvacuum.c @@ -184,13 +184,12 @@ static void RepairGraphElement(HnswVacuumState * vacuumstate, HnswElement element, HnswElement entryPoint) { Relation index = vacuumstate->index; + HnswSupport *support = &vacuumstate->support; Buffer buf; Page page; GenericXLogState *state; int m = vacuumstate->m; int efConstruction = vacuumstate->efConstruction; - FmgrInfo *procinfo = vacuumstate->procinfo; - Oid collation = vacuumstate->collation; BufferAccessStrategy bas = vacuumstate->bas; HnswNeighborTuple ntup = vacuumstate->ntup; Size ntupSize = HNSW_NEIGHBOR_TUPLE_SIZE(element->level, m); @@ -205,7 +204,7 @@ RepairGraphElement(HnswVacuumState * vacuumstate, HnswElement element, HnswEleme element->heaptidsLength = 0; /* Find neighbors for element, skipping itself */ - HnswFindElementNeighbors(base, element, entryPoint, index, procinfo, collation, m, efConstruction, true); + HnswFindElementNeighbors(base, element, entryPoint, index, support, m, efConstruction, true); /* Zero memory for each element */ MemSet(ntup, 0, HNSW_TUPLE_ALLOC_SIZE); @@ -229,7 +228,7 @@ RepairGraphElement(HnswVacuumState * vacuumstate, HnswElement element, HnswEleme UnlockReleaseBuffer(buf); /* Update neighbors */ - HnswUpdateNeighborsOnDisk(index, procinfo, collation, element, m, true, false); + HnswUpdateNeighborsOnDisk(index, support, element, m, true, false); } /* @@ -239,6 +238,7 @@ static void RepairGraphEntryPoint(HnswVacuumState * vacuumstate) { Relation index = vacuumstate->index; + HnswSupport *support = &vacuumstate->support; HnswElement highestPoint = &vacuumstate->highestPoint; HnswElement entryPoint; MemoryContext oldCtx = MemoryContextSwitchTo(vacuumstate->tmpCtx); @@ -256,7 +256,7 @@ RepairGraphEntryPoint(HnswVacuumState * vacuumstate) LockPage(index, HNSW_UPDATE_LOCK, ShareLock); /* Load element */ - HnswLoadElement(highestPoint, NULL, NULL, index, vacuumstate->procinfo, vacuumstate->collation, true, NULL); + HnswLoadElement(highestPoint, NULL, NULL, index, support, true, NULL); /* Repair if needed */ if (NeedsUpdated(vacuumstate, highestPoint)) @@ -294,7 +294,7 @@ RepairGraphEntryPoint(HnswVacuumState * vacuumstate) * is outdated, this can remove connections at higher levels in * the graph until they are repaired, but this should be fine. */ - HnswLoadElement(entryPoint, NULL, NULL, index, vacuumstate->procinfo, vacuumstate->collation, true, NULL); + HnswLoadElement(entryPoint, NULL, NULL, index, support, true, NULL); if (NeedsUpdated(vacuumstate, entryPoint)) { @@ -578,7 +578,7 @@ InitVacuumState(HnswVacuumState * vacuumstate, IndexVacuumInfo *info, IndexBulkD "Hnsw vacuum temporary context", ALLOCSET_DEFAULT_SIZES); - HnswSetProcinfo(index, &vacuumstate->procinfo, NULL, &vacuumstate->collation); + HnswInitSupport(&vacuumstate->support, index); /* Get m from metapage */ HnswGetMetaPageInfo(index, &vacuumstate->m, NULL); From fa6782985ab9fd6c3f289196b7cf3053c1d46eef Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 9 Oct 2024 23:45:47 -0700 Subject: [PATCH 54/88] Added HnswQuery struct for query data --- src/hnsw.h | 11 ++++++++--- src/hnswinsert.c | 10 ++++++---- src/hnswscan.c | 11 +++++++---- src/hnswutils.c | 30 ++++++++++++++++-------------- 4 files changed, 37 insertions(+), 25 deletions(-) diff --git a/src/hnsw.h b/src/hnsw.h index bfc12f6..e034068 100644 --- a/src/hnsw.h +++ b/src/hnsw.h @@ -244,6 +244,11 @@ typedef struct HnswSupport Oid collation; } HnswSupport; +typedef struct HnswQuery +{ + Datum value; +} HnswQuery; + typedef struct HnswBuildState { /* Info */ @@ -378,14 +383,14 @@ bool HnswCheckNorm(HnswSupport * support, Datum value); Buffer HnswNewBuffer(Relation index, ForkNumber forkNum); void HnswInitPage(Buffer buf, Page page); void HnswInit(void); -List *HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, HnswSupport * support, int m, bool inserting, HnswElement skipElement); +List *HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation index, HnswSupport * support, int m, bool inserting, HnswElement skipElement); HnswElement HnswGetEntryPoint(Relation index); void HnswGetMetaPageInfo(Relation index, int *m, HnswElement * entryPoint); void *HnswAlloc(HnswAllocator * allocator, Size size); HnswElement HnswInitElement(char *base, ItemPointer tid, int m, double ml, int maxLevel, HnswAllocator * alloc); HnswElement HnswInitElementFromBlock(BlockNumber blkno, OffsetNumber offno); void HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint, Relation index, HnswSupport * support, int m, int efConstruction, bool existing); -HnswSearchCandidate *HnswEntryCandidate(char *base, HnswElement em, Datum q, Relation rel, HnswSupport * support, bool loadVec); +HnswSearchCandidate *HnswEntryCandidate(char *base, HnswElement em, HnswQuery * q, Relation rel, HnswSupport * support, bool loadVec); void HnswUpdateMetaPage(Relation index, int updateEntry, HnswElement entryPoint, BlockNumber insertPage, ForkNumber forkNum, bool building); void HnswSetNeighborTuple(char *base, HnswNeighborTuple ntup, HnswElement e, int m); void HnswAddHeapTid(HnswElement element, ItemPointer heaptid); @@ -394,7 +399,7 @@ void HnswInitNeighbors(char *base, HnswElement element, int m, HnswAllocator * bool HnswInsertTupleOnDisk(Relation index, HnswSupport * support, Datum value, ItemPointer heaptid, bool building); void HnswUpdateNeighborsOnDisk(Relation index, HnswSupport * support, HnswElement e, int m, bool checkExisting, bool building); void HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool loadHeaptids, bool loadVec); -void HnswLoadElement(HnswElement element, double *distance, Datum *q, Relation index, HnswSupport * support, bool loadVec, double *maxDistance); +void HnswLoadElement(HnswElement element, double *distance, HnswQuery * q, Relation index, HnswSupport * support, bool loadVec, double *maxDistance); bool HnswFormIndexValue(Datum *out, Datum *values, bool *isnull, const HnswTypeInfo * typeInfo, HnswSupport * support); void HnswSetElementTuple(char *base, HnswElementTuple etup, HnswElement element); void HnswUpdateConnection(char *base, HnswNeighborArray * neighbors, HnswElement newElement, float distance, int lm, int *updateIdx, Relation index, HnswSupport * support); diff --git a/src/hnswinsert.c b/src/hnswinsert.c index 87204ca..84eb1d4 100644 --- a/src/hnswinsert.c +++ b/src/hnswinsert.c @@ -368,7 +368,7 @@ HnswLoadNeighbors(HnswElement element, Relation index, int m, int lm, int lc) * Load elements for insert */ static void -LoadElementsForInsert(HnswNeighborArray * neighbors, Datum q, int *idx, Relation index, HnswSupport * support) +LoadElementsForInsert(HnswNeighborArray * neighbors, HnswQuery * q, int *idx, Relation index, HnswSupport * support) { char *base = NULL; @@ -378,7 +378,7 @@ LoadElementsForInsert(HnswNeighborArray * neighbors, Datum q, int *idx, Relation HnswElement element = HnswPtrAccess(base, hc->element); double distance; - HnswLoadElement(element, &distance, &q, index, support, true, NULL); + HnswLoadElement(element, &distance, q, index, support, true, NULL); hc->distance = distance; /* Prune element if being deleted */ @@ -419,9 +419,11 @@ GetUpdateIndex(HnswElement element, HnswElement newElement, float distance, int idx = -2; else { - Datum q = HnswGetValue(base, element); + HnswQuery q; - LoadElementsForInsert(neighbors, q, &idx, index, support); + q.value = HnswGetValue(base, element); + + LoadElementsForInsert(neighbors, &q, &idx, index, support); if (idx == -1) HnswUpdateConnection(base, neighbors, newElement, distance, lm, &idx, index, support); diff --git a/src/hnswscan.c b/src/hnswscan.c index e3aaced..2c6a454 100644 --- a/src/hnswscan.c +++ b/src/hnswscan.c @@ -11,7 +11,7 @@ * Algorithm 5 from paper */ static List * -GetScanItems(IndexScanDesc scan, Datum q) +GetScanItems(IndexScanDesc scan, Datum value) { HnswScanOpaque so = (HnswScanOpaque) scan->opaque; Relation index = scan->indexRelation; @@ -21,6 +21,9 @@ GetScanItems(IndexScanDesc scan, Datum q) int m; HnswElement entryPoint; char *base = NULL; + HnswQuery q; + + q.value = value; /* Get m and entry point */ HnswGetMetaPageInfo(index, &m, &entryPoint); @@ -28,15 +31,15 @@ GetScanItems(IndexScanDesc scan, Datum q) if (entryPoint == NULL) return NIL; - ep = list_make1(HnswEntryCandidate(base, entryPoint, q, index, support, false)); + ep = list_make1(HnswEntryCandidate(base, entryPoint, &q, index, support, false)); for (int lc = entryPoint->level; lc >= 1; lc--) { - w = HnswSearchLayer(base, q, ep, 1, lc, index, support, m, false, NULL); + w = HnswSearchLayer(base, &q, ep, 1, lc, index, support, m, false, NULL); ep = w; } - return HnswSearchLayer(base, q, ep, hnsw_ef_search, 0, index, support, m, false, NULL); + return HnswSearchLayer(base, &q, ep, hnsw_ef_search, 0, index, support, m, false, NULL); } /* diff --git a/src/hnswutils.c b/src/hnswutils.c index 7fa0720..fe2b16e 100644 --- a/src/hnswutils.c +++ b/src/hnswutils.c @@ -533,7 +533,7 @@ HnswGetDistance(Datum a, Datum b, HnswSupport * support) * Load an element and optionally get its distance from q */ static void -HnswLoadElementImpl(BlockNumber blkno, OffsetNumber offno, double *distance, Datum *q, Relation index, HnswSupport * support, bool loadVec, double *maxDistance, HnswElement * element) +HnswLoadElementImpl(BlockNumber blkno, OffsetNumber offno, double *distance, HnswQuery * q, Relation index, HnswSupport * support, bool loadVec, double *maxDistance, HnswElement * element) { Buffer buf; Page page; @@ -551,10 +551,10 @@ HnswLoadElementImpl(BlockNumber blkno, OffsetNumber offno, double *distance, Dat /* Calculate distance */ if (distance != NULL) { - if (DatumGetPointer(*q) == NULL) + if (DatumGetPointer(q->value) == NULL) *distance = 0; else - *distance = HnswGetDistance(*q, PointerGetDatum(&etup->data), support); + *distance = HnswGetDistance(q->value, PointerGetDatum(&etup->data), support); } /* Load element */ @@ -573,7 +573,7 @@ HnswLoadElementImpl(BlockNumber blkno, OffsetNumber offno, double *distance, Dat * Load an element and optionally get its distance from q */ void -HnswLoadElement(HnswElement element, double *distance, Datum *q, Relation index, HnswSupport * support, bool loadVec, double *maxDistance) +HnswLoadElement(HnswElement element, double *distance, HnswQuery * q, Relation index, HnswSupport * support, bool loadVec, double *maxDistance) { HnswLoadElementImpl(element->blkno, element->offno, distance, q, index, support, loadVec, maxDistance, &element); } @@ -582,18 +582,18 @@ HnswLoadElement(HnswElement element, double *distance, Datum *q, Relation index, * Get the distance for an element */ static double -GetElementDistance(char *base, HnswElement element, Datum q, HnswSupport * support) +GetElementDistance(char *base, HnswElement element, HnswQuery * q, HnswSupport * support) { Datum value = HnswGetValue(base, element); - return HnswGetDistance(q, value, support); + return HnswGetDistance(q->value, value, support); } /* * Create a candidate for the entry point */ HnswSearchCandidate * -HnswEntryCandidate(char *base, HnswElement entryPoint, Datum q, Relation index, HnswSupport * support, bool loadVec) +HnswEntryCandidate(char *base, HnswElement entryPoint, HnswQuery * q, Relation index, HnswSupport * support, bool loadVec) { HnswSearchCandidate *sc = palloc(sizeof(HnswSearchCandidate)); bool inMemory = index == NULL; @@ -602,7 +602,7 @@ HnswEntryCandidate(char *base, HnswElement entryPoint, Datum q, Relation index, if (inMemory) sc->distance = GetElementDistance(base, entryPoint, q, support); else - HnswLoadElement(entryPoint, &sc->distance, &q, index, support, loadVec, NULL); + HnswLoadElement(entryPoint, &sc->distance, q, index, support, loadVec, NULL); return sc; } @@ -791,7 +791,7 @@ HnswLoadUnvisitedFromDisk(HnswElement element, HnswUnvisited * unvisited, int *u * Algorithm 2 from paper */ List * -HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, HnswSupport * support, int m, bool inserting, HnswElement skipElement) +HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation index, HnswSupport * support, int m, bool inserting, HnswElement skipElement) { List *w = NIL; pairingheap *C = pairingheap_allocate(CompareNearestCandidates, NULL); @@ -873,7 +873,7 @@ HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, H /* Avoid any allocations if not adding */ eElement = NULL; - HnswLoadElementImpl(blkno, offno, &eDistance, &q, index, support, inserting, alwaysAdd ? NULL : &f->distance, &eElement); + HnswLoadElementImpl(blkno, offno, &eDistance, q, index, support, inserting, alwaysAdd ? NULL : &f->distance, &eElement); if (eElement == NULL) continue; @@ -1220,10 +1220,12 @@ HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint List *w; int level = element->level; int entryLevel; - Datum q = HnswGetValue(base, element); + HnswQuery q; HnswElement skipElement = existing ? element : NULL; bool inMemory = index == NULL; + q.value = HnswGetValue(base, element); + /* Precompute hash */ if (inMemory) PrecomputeHash(base, element); @@ -1233,13 +1235,13 @@ HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint return; /* Get entry point and level */ - ep = list_make1(HnswEntryCandidate(base, entryPoint, q, index, support, true)); + ep = list_make1(HnswEntryCandidate(base, entryPoint, &q, index, support, true)); entryLevel = entryPoint->level; /* 1st phase: greedy search to insert level */ for (int lc = entryLevel; lc >= level + 1; lc--) { - w = HnswSearchLayer(base, q, ep, 1, lc, index, support, m, true, skipElement); + w = HnswSearchLayer(base, &q, ep, 1, lc, index, support, m, true, skipElement); ep = w; } @@ -1258,7 +1260,7 @@ HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint List *lw = NIL; ListCell *lc2; - w = HnswSearchLayer(base, q, ep, efConstruction, lc, index, support, m, true, skipElement); + w = HnswSearchLayer(base, &q, ep, efConstruction, lc, index, support, m, true, skipElement); /* Convert search candidates to candidates */ foreach(lc2, w) From 2cbd08b6c027ead161bfbf30d24a06457aa98879 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 10 Oct 2024 09:41:26 -0700 Subject: [PATCH 55/88] Moved unions and macros [skip ci] --- src/hnsw.h | 16 ++++++++++++++++ src/hnswutils.c | 16 ---------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/hnsw.h b/src/hnsw.h index e034068..b2614d1 100644 --- a/src/hnsw.h +++ b/src/hnsw.h @@ -88,6 +88,9 @@ /* Ensure fits on page and in uint8 */ #define HnswGetMaxLevel(m) Min(((BLCKSZ - MAXALIGN(SizeOfPageHeaderData) - MAXALIGN(sizeof(HnswPageOpaqueData)) - offsetof(HnswNeighborTupleData, indextids) - sizeof(ItemIdData)) / (sizeof(ItemPointerData)) / (m)) - 2, 255) +#define HnswGetSearchCandidate(membername, ptr) pairingheap_container(HnswSearchCandidate, membername, ptr) +#define HnswGetSearchCandidateConst(membername, ptr) pairingheap_const_container(HnswSearchCandidate, membername, ptr) + #define HnswGetValue(base, element) PointerGetDatum(HnswPtrAccess(base, (element)->value)) #if PG_VERSION_NUM < 140005 @@ -335,6 +338,19 @@ typedef struct HnswNeighborTupleData typedef HnswNeighborTupleData * HnswNeighborTuple; +typedef union +{ + struct pointerhash_hash *pointers; + struct offsethash_hash *offsets; + struct tidhash_hash *tids; +} visited_hash; + +typedef union +{ + HnswElement element; + ItemPointerData indextid; +} HnswUnvisited; + typedef struct HnswScanOpaqueData { const HnswTypeInfo *typeInfo; diff --git a/src/hnswutils.c b/src/hnswutils.c index fe2b16e..c51fe28 100644 --- a/src/hnswutils.c +++ b/src/hnswutils.c @@ -100,19 +100,6 @@ hash_offset(Size offset) #define SH_DEFINE #include "lib/simplehash.h" -typedef union -{ - pointerhash_hash *pointers; - offsethash_hash *offsets; - tidhash_hash *tids; -} visited_hash; - -typedef union -{ - HnswElement element; - ItemPointerData indextid; -} HnswUnvisited; - /* * Get the max number of connections in an upper layer for each element in the index */ @@ -606,9 +593,6 @@ HnswEntryCandidate(char *base, HnswElement entryPoint, HnswQuery * q, Relation i return sc; } -#define HnswGetSearchCandidate(membername, ptr) pairingheap_container(HnswSearchCandidate, membername, ptr) -#define HnswGetSearchCandidateConst(membername, ptr) pairingheap_const_container(HnswSearchCandidate, membername, ptr) - /* * Compare candidate distances */ From edc2126a4a9d17aa71878ddd993dfae28ac641d1 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 10 Oct 2024 12:16:57 -0700 Subject: [PATCH 56/88] Renamed tupdesc to sortdesc [skip ci] --- src/ivfbuild.c | 16 ++++++++-------- src/ivfflat.h | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/ivfbuild.c b/src/ivfbuild.c index 85a247f..cb5a78c 100644 --- a/src/ivfbuild.c +++ b/src/ivfbuild.c @@ -254,7 +254,7 @@ InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum) IndexTuple itup = NULL; /* silence compiler warning */ int64 inserted = 0; - TupleTableSlot *slot = MakeSingleTupleTableSlot(buildstate->tupdesc, &TTSOpsMinimalTuple); + TupleTableSlot *slot = MakeSingleTupleTableSlot(buildstate->sortdesc, &TTSOpsMinimalTuple); TupleDesc tupdesc = RelationGetDescr(index); pgstat_progress_update_param(PROGRESS_CREATEIDX_SUBPHASE, PROGRESS_IVFFLAT_PHASE_LOAD); @@ -356,12 +356,12 @@ InitBuildState(IvfflatBuildState * buildstate, Relation heap, Relation index, In errmsg("dimensions must be greater than one for this opclass"))); /* Create tuple description for sorting */ - buildstate->tupdesc = CreateTemplateTupleDesc(3); - TupleDescInitEntry(buildstate->tupdesc, (AttrNumber) 1, "list", INT4OID, -1, 0); - TupleDescInitEntry(buildstate->tupdesc, (AttrNumber) 2, "tid", TIDOID, -1, 0); - TupleDescInitEntry(buildstate->tupdesc, (AttrNumber) 3, "vector", RelationGetDescr(index)->attrs[0].atttypid, -1, 0); + buildstate->sortdesc = CreateTemplateTupleDesc(3); + TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 1, "list", INT4OID, -1, 0); + TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 2, "tid", TIDOID, -1, 0); + TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 3, "vector", RelationGetDescr(index)->attrs[0].atttypid, -1, 0); - buildstate->slot = MakeSingleTupleTableSlot(buildstate->tupdesc, &TTSOpsVirtual); + buildstate->slot = MakeSingleTupleTableSlot(buildstate->sortdesc, &TTSOpsVirtual); buildstate->centers = VectorArrayInit(buildstate->lists, buildstate->dimensions, buildstate->typeInfo->itemSize(buildstate->dimensions)); buildstate->listInfo = palloc(sizeof(ListInfo) * buildstate->lists); @@ -633,7 +633,7 @@ IvfflatParallelScanAndSort(IvfflatSpool * ivfspool, IvfflatShared * ivfshared, S InitBuildState(&buildstate, ivfspool->heap, ivfspool->index, indexInfo); memcpy(buildstate.centers->items, ivfcenters, buildstate.centers->itemsize * buildstate.centers->maxlen); buildstate.centers->length = buildstate.centers->maxlen; - ivfspool->sortstate = InitBuildSortState(buildstate.tupdesc, sortmem, coordinate); + ivfspool->sortstate = InitBuildSortState(buildstate.sortdesc, sortmem, coordinate); buildstate.sortstate = ivfspool->sortstate; scan = table_beginscan_parallel(ivfspool->heap, ParallelTableScanFromIvfflatShared(ivfshared)); @@ -950,7 +950,7 @@ AssignTuples(IvfflatBuildState * buildstate) } /* Begin serial/leader tuplesort */ - buildstate->sortstate = InitBuildSortState(buildstate->tupdesc, maintenance_work_mem, coordinate); + buildstate->sortstate = InitBuildSortState(buildstate->sortdesc, maintenance_work_mem, coordinate); /* Add tuples to sort */ if (buildstate->heap != NULL) diff --git a/src/ivfflat.h b/src/ivfflat.h index 8518317..73cf565 100644 --- a/src/ivfflat.h +++ b/src/ivfflat.h @@ -198,7 +198,7 @@ typedef struct IvfflatBuildState /* Sorting */ Tuplesortstate *sortstate; - TupleDesc tupdesc; + TupleDesc sortdesc; TupleTableSlot *slot; /* Memory */ From e2fab306ac04ed5eb2e9ecb6dc37756f3c01c2dc Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 10 Oct 2024 12:18:41 -0700 Subject: [PATCH 57/88] Added tupdesc to buildstate [skip ci] --- src/ivfbuild.c | 5 +++-- src/ivfflat.h | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ivfbuild.c b/src/ivfbuild.c index cb5a78c..2f8c7bf 100644 --- a/src/ivfbuild.c +++ b/src/ivfbuild.c @@ -255,7 +255,7 @@ InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum) int64 inserted = 0; TupleTableSlot *slot = MakeSingleTupleTableSlot(buildstate->sortdesc, &TTSOpsMinimalTuple); - TupleDesc tupdesc = RelationGetDescr(index); + TupleDesc tupdesc = buildstate->tupdesc; pgstat_progress_update_param(PROGRESS_CREATEIDX_SUBPHASE, PROGRESS_IVFFLAT_PHASE_LOAD); @@ -319,6 +319,7 @@ InitBuildState(IvfflatBuildState * buildstate, Relation heap, Relation index, In buildstate->index = index; buildstate->indexInfo = indexInfo; buildstate->typeInfo = IvfflatGetTypeInfo(index); + buildstate->tupdesc = RelationGetDescr(index); buildstate->lists = IvfflatGetLists(index); buildstate->dimensions = TupleDescAttr(index->rd_att, 0)->atttypmod; @@ -359,7 +360,7 @@ InitBuildState(IvfflatBuildState * buildstate, Relation heap, Relation index, In buildstate->sortdesc = CreateTemplateTupleDesc(3); TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 1, "list", INT4OID, -1, 0); TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 2, "tid", TIDOID, -1, 0); - TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 3, "vector", RelationGetDescr(index)->attrs[0].atttypid, -1, 0); + TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 3, "vector", buildstate->tupdesc->attrs[0].atttypid, -1, 0); buildstate->slot = MakeSingleTupleTableSlot(buildstate->sortdesc, &TTSOpsVirtual); diff --git a/src/ivfflat.h b/src/ivfflat.h index 73cf565..abf71fe 100644 --- a/src/ivfflat.h +++ b/src/ivfflat.h @@ -165,6 +165,7 @@ typedef struct IvfflatBuildState Relation index; IndexInfo *indexInfo; const IvfflatTypeInfo *typeInfo; + TupleDesc tupdesc; /* Settings */ int dimensions; From e13e9a9614e87cb4c73bd2d6221f5781596acf64 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 10 Oct 2024 12:19:08 -0700 Subject: [PATCH 58/88] Improved scoping [skip ci] --- src/ivfbuild.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ivfbuild.c b/src/ivfbuild.c index 2f8c7bf..54a5be5 100644 --- a/src/ivfbuild.c +++ b/src/ivfbuild.c @@ -228,11 +228,11 @@ BuildCallback(Relation index, ItemPointer tid, Datum *values, static inline void GetNextTuple(Tuplesortstate *sortstate, TupleDesc tupdesc, TupleTableSlot *slot, IndexTuple *itup, int *list) { - Datum value; - bool isnull; - if (tuplesort_gettupleslot(sortstate, true, false, slot, NULL)) { + Datum value; + bool isnull; + *list = DatumGetInt32(slot_getattr(slot, 1, &isnull)); value = slot_getattr(slot, 3, &isnull); From 772ab69de6ea640e8923706dcf8b52f4cabfe035 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 10 Oct 2024 12:34:03 -0700 Subject: [PATCH 59/88] Updated IVFFlat to support multiple attributes (not enabled yet) --- src/ivfbuild.c | 36 ++++++++++++++++++++++++------------ src/ivfinsert.c | 8 +++++++- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/ivfbuild.c b/src/ivfbuild.c index 54a5be5..fa7a0a8 100644 --- a/src/ivfbuild.c +++ b/src/ivfbuild.c @@ -138,7 +138,7 @@ SampleRows(IvfflatBuildState * buildstate) * Add tuple to sort */ static void -AddTupleToSort(Relation index, ItemPointer tid, Datum *values, IvfflatBuildState * buildstate) +AddTupleToSort(Relation index, ItemPointer tid, Datum *values, bool *isnull, IvfflatBuildState * buildstate) { double distance; double minDistance = DBL_MAX; @@ -184,6 +184,11 @@ AddTupleToSort(Relation index, ItemPointer tid, Datum *values, IvfflatBuildState slot->tts_isnull[1] = false; slot->tts_values[2] = value; slot->tts_isnull[2] = false; + for (int i = 1; i < buildstate->tupdesc->natts; i++) + { + slot->tts_values[2 + i] = values[i]; + slot->tts_isnull[2 + i] = isnull[i]; + } ExecStoreVirtualTuple(slot); /* @@ -215,7 +220,7 @@ BuildCallback(Relation index, ItemPointer tid, Datum *values, oldCtx = MemoryContextSwitchTo(buildstate->tmpCtx); /* Add tuple to sort */ - AddTupleToSort(index, tid, values, buildstate); + AddTupleToSort(index, tid, values, isnull, buildstate); /* Reset memory context */ MemoryContextSwitchTo(oldCtx); @@ -226,19 +231,20 @@ BuildCallback(Relation index, ItemPointer tid, Datum *values, * Get index tuple from sort state */ static inline void -GetNextTuple(Tuplesortstate *sortstate, TupleDesc tupdesc, TupleTableSlot *slot, IndexTuple *itup, int *list) +GetNextTuple(Tuplesortstate *sortstate, TupleDesc tupdesc, TupleTableSlot *slot, Datum *values, bool *isnull, IndexTuple *itup, int *list) { if (tuplesort_gettupleslot(sortstate, true, false, slot, NULL)) { - Datum value; - bool isnull; + bool unused; - *list = DatumGetInt32(slot_getattr(slot, 1, &isnull)); - value = slot_getattr(slot, 3, &isnull); + *list = DatumGetInt32(slot_getattr(slot, 1, &unused)); + + for (int i = 0; i < tupdesc->natts; i++) + values[i] = slot_getattr(slot, 3 + i, &isnull[i]); /* Form the index tuple */ - *itup = index_form_tuple(tupdesc, &value, &isnull); - (*itup)->t_tid = *((ItemPointer) DatumGetPointer(slot_getattr(slot, 2, &isnull))); + *itup = index_form_tuple(tupdesc, values, isnull); + (*itup)->t_tid = *((ItemPointer) DatumGetPointer(slot_getattr(slot, 2, &unused))); } else *list = -1; @@ -256,12 +262,14 @@ InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum) TupleTableSlot *slot = MakeSingleTupleTableSlot(buildstate->sortdesc, &TTSOpsMinimalTuple); TupleDesc tupdesc = buildstate->tupdesc; + Datum *values = palloc(tupdesc->natts * sizeof(Datum)); + bool *isnull = palloc(tupdesc->natts * sizeof(bool)); pgstat_progress_update_param(PROGRESS_CREATEIDX_SUBPHASE, PROGRESS_IVFFLAT_PHASE_LOAD); pgstat_progress_update_param(PROGRESS_CREATEIDX_TUPLES_TOTAL, buildstate->indtuples); - GetNextTuple(buildstate->sortstate, tupdesc, slot, &itup, &list); + GetNextTuple(buildstate->sortstate, tupdesc, slot, values, isnull, &itup, &list); for (int i = 0; i < buildstate->centers->length; i++) { @@ -297,7 +305,7 @@ InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum) pgstat_progress_update_param(PROGRESS_CREATEIDX_TUPLES_DONE, ++inserted); - GetNextTuple(buildstate->sortstate, tupdesc, slot, &itup, &list); + GetNextTuple(buildstate->sortstate, tupdesc, slot, values, isnull, &itup, &list); } insertPage = BufferGetBlockNumber(buf); @@ -307,6 +315,9 @@ InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum) /* Set the start and insert pages */ IvfflatUpdateList(index, buildstate->listInfo[i], insertPage, InvalidBlockNumber, startPage, forkNum); } + + pfree(values); + pfree(isnull); } /* @@ -360,7 +371,8 @@ InitBuildState(IvfflatBuildState * buildstate, Relation heap, Relation index, In buildstate->sortdesc = CreateTemplateTupleDesc(3); TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 1, "list", INT4OID, -1, 0); TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 2, "tid", TIDOID, -1, 0); - TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 3, "vector", buildstate->tupdesc->attrs[0].atttypid, -1, 0); + for (int i = 0; i < buildstate->tupdesc->natts; i++) + TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) (3 + i), NULL, buildstate->tupdesc->attrs[0].atttypid, -1, 0); buildstate->slot = MakeSingleTupleTableSlot(buildstate->sortdesc, &TTSOpsVirtual); diff --git a/src/ivfinsert.c b/src/ivfinsert.c index b748c5e..57f839b 100644 --- a/src/ivfinsert.c +++ b/src/ivfinsert.c @@ -78,6 +78,8 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heap_tid, R BlockNumber insertPage = InvalidBlockNumber; ListInfo listInfo; BlockNumber originalInsertPage; + TupleDesc tupdesc = RelationGetDescr(index); + Datum *newValues = palloc(tupdesc->natts * sizeof(Datum)); /* Detoast once for all calls */ value = PointerGetDatum(PG_DETOAST_DATUM(values[0])); @@ -102,8 +104,12 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heap_tid, R Assert(BlockNumberIsValid(insertPage)); originalInsertPage = insertPage; + newValues[0] = value; + for (int i = 1; i < tupdesc->natts; i++) + newValues[i] = values[i]; + /* Form tuple */ - itup = index_form_tuple(RelationGetDescr(index), &value, isnull); + itup = index_form_tuple(tupdesc, newValues, isnull); itup->t_tid = *heap_tid; /* Get tuple size */ From 7d2eb49c2a5ce3af51188af814a7b90132bd68f4 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 10 Oct 2024 12:54:13 -0700 Subject: [PATCH 60/88] Fixed list assignment for cosine distance and non-normalized vectors for IVFFlat --- CHANGELOG.md | 1 + src/ivfinsert.c | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7d9924..ab74796 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Improved cost estimation - Improved performance of HNSW inserts and on-disk index builds - Reduced memory usage for HNSW index scans +- Fixed list assignment for cosine distance and non-normalized vectors for IVFFlat - Dropped support for Postgres 12 ## 0.7.4 (2024-08-05) diff --git a/src/ivfinsert.c b/src/ivfinsert.c index 57f839b..f683ea7 100644 --- a/src/ivfinsert.c +++ b/src/ivfinsert.c @@ -100,7 +100,7 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heap_tid, R IvfflatGetMetaPageInfo(index, NULL, NULL); /* Find the insert page - sets the page and list info */ - FindInsertPage(index, values, &insertPage, &listInfo); + FindInsertPage(index, &value, &insertPage, &listInfo); Assert(BlockNumberIsValid(insertPage)); originalInsertPage = insertPage; From 08d034065565dee30cb626bf311cb1440bcdfce0 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 10 Oct 2024 14:24:26 -0700 Subject: [PATCH 61/88] Improved IVFFlat vacuum test [skip ci] --- test/t/002_ivfflat_vacuum.pl | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/test/t/002_ivfflat_vacuum.pl b/test/t/002_ivfflat_vacuum.pl index d4cfeaf..a7f1d9e 100644 --- a/test/t/002_ivfflat_vacuum.pl +++ b/test/t/002_ivfflat_vacuum.pl @@ -6,13 +6,7 @@ use Test::More; my $dim = 3; -my @r = (); -for (1 .. $dim) -{ - my $v = int(rand(1000)) + 1; - push(@r, "i % $v"); -} -my $array_sql = join(", ", @r); +my $array_sql = join(",", ('random()') x $dim); # Initialize node my $node = PostgreSQL::Test::Cluster->new('node'); @@ -23,19 +17,20 @@ $node->start; $node->safe_psql("postgres", "CREATE EXTENSION vector;"); $node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector($dim));"); $node->safe_psql("postgres", - "INSERT INTO tst SELECT i % 10, ARRAY[$array_sql] FROM generate_series(1, 100000) i;" + "INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(1, 100000) i;" ); $node->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v vector_l2_ops);"); # Get size my $size = $node->safe_psql("postgres", "SELECT pg_total_relation_size('tst_v_idx');"); +# Store values +$node->safe_psql("postgres", "CREATE TABLE tmp AS SELECT * FROM tst;"); + # Delete all, vacuum, and insert same data $node->safe_psql("postgres", "DELETE FROM tst;"); $node->safe_psql("postgres", "VACUUM tst;"); -$node->safe_psql("postgres", - "INSERT INTO tst SELECT i % 10, ARRAY[$array_sql] FROM generate_series(1, 100000) i;" -); +$node->safe_psql("postgres", "INSERT INTO tst SELECT * FROM tmp;"); # Check size my $new_size = $node->safe_psql("postgres", "SELECT pg_total_relation_size('tst_v_idx');"); From 29908405ab7843288998159b381d7cd33b214f60 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 10 Oct 2024 15:16:08 -0700 Subject: [PATCH 62/88] List assignment does not change with scaling [skip ci] --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab74796..a7d9924 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,6 @@ - Improved cost estimation - Improved performance of HNSW inserts and on-disk index builds - Reduced memory usage for HNSW index scans -- Fixed list assignment for cosine distance and non-normalized vectors for IVFFlat - Dropped support for Postgres 12 ## 0.7.4 (2024-08-05) From 48fe70c21952d1209690d0755be856fbdac5c596 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 10 Oct 2024 15:22:23 -0700 Subject: [PATCH 63/88] Fixed sortdesc for multiple attributes [skip ci] --- src/ivfbuild.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ivfbuild.c b/src/ivfbuild.c index fa7a0a8..e88a0bb 100644 --- a/src/ivfbuild.c +++ b/src/ivfbuild.c @@ -368,11 +368,11 @@ InitBuildState(IvfflatBuildState * buildstate, Relation heap, Relation index, In errmsg("dimensions must be greater than one for this opclass"))); /* Create tuple description for sorting */ - buildstate->sortdesc = CreateTemplateTupleDesc(3); + buildstate->sortdesc = CreateTemplateTupleDesc(2 + buildstate->tupdesc->natts); TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 1, "list", INT4OID, -1, 0); TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 2, "tid", TIDOID, -1, 0); for (int i = 0; i < buildstate->tupdesc->natts; i++) - TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) (3 + i), NULL, buildstate->tupdesc->attrs[0].atttypid, -1, 0); + TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) (3 + i), NULL, buildstate->tupdesc->attrs[i].atttypid, -1, 0); buildstate->slot = MakeSingleTupleTableSlot(buildstate->sortdesc, &TTSOpsVirtual); From c91ed7b2c399d5fa37c1a53e3b64d558858786eb Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 10 Oct 2024 18:12:27 -0700 Subject: [PATCH 64/88] Added iterative search for IVFFlat [skip ci] --- src/ivfflat.c | 16 +++ src/ivfflat.h | 12 ++ src/ivfscan.c | 72 ++++++---- src/ivfvacuum.c | 6 +- test/t/041_ivfflat_iterative_search.pl | 54 ++++++++ test/t/042_ivfflat_iterative_search_recall.pl | 125 ++++++++++++++++++ 6 files changed, 258 insertions(+), 27 deletions(-) create mode 100644 test/t/041_ivfflat_iterative_search.pl create mode 100644 test/t/042_ivfflat_iterative_search_recall.pl diff --git a/src/ivfflat.c b/src/ivfflat.c index 395040d..0b24875 100644 --- a/src/ivfflat.c +++ b/src/ivfflat.c @@ -17,8 +17,16 @@ #endif int ivfflat_probes; +int ivfflat_iterative_search; +int ivfflat_iterative_search_max_probes; static relopt_kind ivfflat_relopt_kind; +static const struct config_enum_entry ivfflat_iterative_search_options[] = { + {"off", IVFFLAT_ITERATIVE_SEARCH_OFF, false}, + {"on", IVFFLAT_ITERATIVE_SEARCH_RELAXED, false}, + {NULL, 0, false} +}; + /* * Initialize index options and variables */ @@ -33,6 +41,14 @@ IvfflatInit(void) "Valid range is 1..lists.", &ivfflat_probes, IVFFLAT_DEFAULT_PROBES, IVFFLAT_MIN_LISTS, IVFFLAT_MAX_LISTS, PGC_USERSET, 0, NULL, NULL, NULL); + DefineCustomEnumVariable("ivfflat.iterative_search", "Sets whether to use iterative search", + NULL, &ivfflat_iterative_search, + IVFFLAT_ITERATIVE_SEARCH_OFF, ivfflat_iterative_search_options, PGC_USERSET, 0, NULL, NULL, NULL); + + DefineCustomIntVariable("ivfflat.iterative_search_max_probes", "Sets the max number of probes for iterative search", + "Zero sets to the number of lists", &ivfflat_iterative_search_max_probes, + 0, 0, IVFFLAT_MAX_LISTS, PGC_USERSET, 0, NULL, NULL, NULL); + MarkGUCPrefixReserved("ivfflat"); } diff --git a/src/ivfflat.h b/src/ivfflat.h index abf71fe..c2b4121 100644 --- a/src/ivfflat.h +++ b/src/ivfflat.h @@ -80,6 +80,14 @@ /* Variables */ extern int ivfflat_probes; +extern int ivfflat_iterative_search; +extern int ivfflat_iterative_search_max_probes; + +typedef enum IvfflatIterativeSearchType +{ + IVFFLAT_ITERATIVE_SEARCH_OFF, + IVFFLAT_ITERATIVE_SEARCH_RELAXED +} IvfflatIterativeSearchType; typedef struct VectorArrayData { @@ -248,8 +256,10 @@ typedef struct IvfflatScanOpaqueData { const IvfflatTypeInfo *typeInfo; int probes; + int maxProbes; int dimensions; bool first; + Datum value; /* Sorting */ Tuplesortstate *sortstate; @@ -266,6 +276,8 @@ typedef struct IvfflatScanOpaqueData /* Lists */ pairingheap *listQueue; + BlockNumber *listPages; + int listIndex; IvfflatScanList lists[FLEXIBLE_ARRAY_MEMBER]; /* must come last */ } IvfflatScanOpaqueData; diff --git a/src/ivfscan.c b/src/ivfscan.c index 74e3675..578f6aa 100644 --- a/src/ivfscan.c +++ b/src/ivfscan.c @@ -65,7 +65,7 @@ GetScanLists(IndexScanDesc scan, Datum value) /* Use procinfo from the index instead of scan key for performance */ distance = DatumGetFloat8(so->distfunc(so->procinfo, so->collation, PointerGetDatum(&list->center), value)); - if (listCount < so->probes) + if (listCount < so->maxProbes) { IvfflatScanList *scanlist; @@ -78,7 +78,7 @@ GetScanLists(IndexScanDesc scan, Datum value) pairingheap_add(so->listQueue, &scanlist->ph_node); /* Calculate max distance */ - if (listCount == so->probes) + if (listCount == so->maxProbes) maxDistance = GetScanList(pairingheap_first(so->listQueue))->distance; } else if (distance < maxDistance) @@ -102,6 +102,11 @@ GetScanLists(IndexScanDesc scan, Datum value) UnlockReleaseBuffer(cbuf); } + + for (int i = listCount - 1; i >= 0; i--) + so->listPages[i] = GetScanList(pairingheap_remove_first(so->listQueue))->startPage; + + Assert(pairingheap_is_empty(so->listQueue)); } /* @@ -114,11 +119,14 @@ GetScanItems(IndexScanDesc scan, Datum value) TupleDesc tupdesc = RelationGetDescr(scan->indexRelation); double tuples = 0; TupleTableSlot *slot = so->vslot; + int batchProbes = 0; + + tuplesort_reset(so->sortstate); /* Search closest probes lists */ - while (!pairingheap_is_empty(so->listQueue)) + while (so->listIndex < so->maxProbes && (++batchProbes) <= so->probes) { - BlockNumber searchPage = GetScanList(pairingheap_remove_first(so->listQueue))->startPage; + BlockNumber searchPage = so->listPages[so->listIndex++]; /* Search all entry pages for list */ while (BlockNumberIsValid(searchPage)) @@ -166,13 +174,17 @@ GetScanItems(IndexScanDesc scan, Datum value) } } - if (tuples < 100) + if (tuples < 100 && ivfflat_iterative_search == IVFFLAT_ITERATIVE_SEARCH_OFF) ereport(DEBUG1, (errmsg("index scan found few tuples"), errdetail("Index may have been created with little data."), errhint("Recreate the index and possibly decrease lists."))); tuplesort_performsort(so->sortstate); + +#if defined(IVFFLAT_MEMORY) + elog(INFO, "memory: %zu MB", MemoryContextMemAllocated(CurrentMemoryContext, true) / (1024 * 1024)); +#endif } /* @@ -240,6 +252,7 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys) int lists; int dimensions; int probes = ivfflat_probes; + int maxProbes; scan = RelationGetIndexScan(index, nkeys, norderbys); @@ -249,10 +262,21 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys) if (probes > lists) probes = lists; - so = (IvfflatScanOpaque) palloc(offsetof(IvfflatScanOpaqueData, lists) + probes * sizeof(IvfflatScanList)); + if (ivfflat_iterative_search != IVFFLAT_ITERATIVE_SEARCH_OFF) + { + if (ivfflat_iterative_search_max_probes == 0) + maxProbes = lists; + else + maxProbes = Min(ivfflat_iterative_search_max_probes, lists); + } + else + maxProbes = probes; + + so = (IvfflatScanOpaque) palloc(offsetof(IvfflatScanOpaqueData, lists) + maxProbes * sizeof(IvfflatScanList)); so->typeInfo = IvfflatGetTypeInfo(index); so->first = true; so->probes = probes; + so->maxProbes = maxProbes; so->dimensions = dimensions; /* Set support functions */ @@ -280,6 +304,8 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys) so->bas = GetAccessStrategy(BAS_BULKREAD); so->listQueue = pairingheap_allocate(CompareLists, scan); + so->listPages = palloc(maxProbes * sizeof(BlockNumber)); + so->listIndex = 0; scan->opaque = so; @@ -294,11 +320,9 @@ ivfflatrescan(IndexScanDesc scan, ScanKey keys, int nkeys, ScanKey orderbys, int { IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque; - if (!so->first) - tuplesort_reset(so->sortstate); - so->first = true; pairingheap_reset(so->listQueue); + so->listIndex = 0; if (keys && scan->numberOfKeys > 0) memmove(scan->keyData, keys, scan->numberOfKeys * sizeof(ScanKeyData)); @@ -314,6 +338,8 @@ bool ivfflatgettuple(IndexScanDesc scan, ScanDirection dir) { IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque; + ItemPointer heaptid; + bool isnull; /* * Index can be used to scan backward, but Postgres doesn't support @@ -341,28 +367,25 @@ ivfflatgettuple(IndexScanDesc scan, ScanDirection dir) IvfflatBench("GetScanLists", GetScanLists(scan, value)); IvfflatBench("GetScanItems", GetScanItems(scan, value)); so->first = false; + so->value = value; -#if defined(IVFFLAT_MEMORY) - elog(INFO, "memory: %zu MB", MemoryContextMemAllocated(CurrentMemoryContext, true) / (1024 * 1024)); -#endif - - /* Clean up if we allocated a new value */ - if (value != scan->orderByData->sk_argument) - pfree(DatumGetPointer(value)); + /* TODO clean up if we allocated a new value */ } - if (tuplesort_gettupleslot(so->sortstate, true, false, so->mslot, NULL)) + while (!tuplesort_gettupleslot(so->sortstate, true, false, so->mslot, NULL)) { - bool isnull; - ItemPointer heaptid = (ItemPointer) DatumGetPointer(slot_getattr(so->mslot, 2, &isnull)); + if (so->listIndex == so->maxProbes) + return false; - scan->xs_heaptid = *heaptid; - scan->xs_recheck = false; - scan->xs_recheckorderby = false; - return true; + IvfflatBench("GetScanItems", GetScanItems(scan, so->value)); } - return false; + heaptid = (ItemPointer) DatumGetPointer(slot_getattr(so->mslot, 2, &isnull)); + + scan->xs_heaptid = *heaptid; + scan->xs_recheck = false; + scan->xs_recheckorderby = false; + return true; } /* @@ -374,6 +397,7 @@ ivfflatendscan(IndexScanDesc scan) IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque; pairingheap_free(so->listQueue); + pfree(so->listPages); tuplesort_end(so->sortstate); FreeAccessStrategy(so->bas); FreeTupleDesc(so->tupdesc); diff --git a/src/ivfvacuum.c b/src/ivfvacuum.c index 57815af..1272da8 100644 --- a/src/ivfvacuum.c +++ b/src/ivfvacuum.c @@ -26,7 +26,7 @@ ivfflatbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, Page cpage; OffsetNumber coffno; OffsetNumber cmaxoffno; - BlockNumber startPages[MaxOffsetNumber]; + BlockNumber listPages[MaxOffsetNumber]; ListInfo listInfo; cbuf = ReadBuffer(index, blkno); @@ -40,7 +40,7 @@ ivfflatbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, { IvfflatList list = (IvfflatList) PageGetItem(cpage, PageGetItemId(cpage, coffno)); - startPages[coffno - FirstOffsetNumber] = list->startPage; + listPages[coffno - FirstOffsetNumber] = list->startPage; } listInfo.blkno = blkno; @@ -50,7 +50,7 @@ ivfflatbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, for (coffno = FirstOffsetNumber; coffno <= cmaxoffno; coffno = OffsetNumberNext(coffno)) { - BlockNumber searchPage = startPages[coffno - FirstOffsetNumber]; + BlockNumber searchPage = listPages[coffno - FirstOffsetNumber]; BlockNumber insertPage = InvalidBlockNumber; /* Iterate over entry pages */ diff --git a/test/t/041_ivfflat_iterative_search.pl b/test/t/041_ivfflat_iterative_search.pl new file mode 100644 index 0000000..231c49e --- /dev/null +++ b/test/t/041_ivfflat_iterative_search.pl @@ -0,0 +1,54 @@ +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $dim = 3; +my $array_sql = join(",", ('random()') x $dim); + +# Initialize node +my $node = PostgreSQL::Test::Cluster->new('node'); +$node->init; +$node->start; + +# Create table +$node->safe_psql("postgres", "CREATE EXTENSION vector;"); +$node->safe_psql("postgres", "CREATE TABLE tst (i int4 PRIMARY KEY, v vector($dim));"); +$node->safe_psql("postgres", + "INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(1, 100000) i;" +); +$node->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v vector_l2_ops);"); + +my $count = $node->safe_psql("postgres", qq( + SET enable_seqscan = off; + SET ivfflat.probes = 10; + SET ivfflat.iterative_search = on; + SELECT COUNT(*) FROM (SELECT v FROM tst WHERE i % 10000 = 0 ORDER BY v <-> (SELECT v FROM tst LIMIT 1) LIMIT 11) t; +)); +is($count, 10); + +foreach ((30, 50, 70)) +{ + my $max_probes = $_; + my $expected = $max_probes / 10; + my $sum = 0; + + for my $i (1 .. 20) + { + $count = $node->safe_psql("postgres", qq( + SET enable_seqscan = off; + SET ivfflat.probes = 10; + SET ivfflat.iterative_search = on; + SET ivfflat.iterative_search_max_probes = $max_probes; + SELECT COUNT(*) FROM (SELECT v FROM tst WHERE i % 10000 = 0 ORDER BY v <-> (SELECT v FROM tst WHERE i = $i) LIMIT 11) t; + )); + $sum += $count; + } + + my $avg = $sum / 20; + cmp_ok($avg, '>', $expected - 2); + cmp_ok($avg, '<', $expected + 2); +} + +done_testing(); diff --git a/test/t/042_ivfflat_iterative_search_recall.pl b/test/t/042_ivfflat_iterative_search_recall.pl new file mode 100644 index 0000000..6bdddd0 --- /dev/null +++ b/test/t/042_ivfflat_iterative_search_recall.pl @@ -0,0 +1,125 @@ +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node; +my @queries = (); +my @expected; +my $limit = 20; +my @cs = (100, 1000); + +sub test_recall +{ + my ($c, $probes, $min, $operator) = @_; + my $correct = 0; + my $total = 0; + + my $explain = $node->safe_psql("postgres", qq( + SET enable_seqscan = off; + SET ivfflat.probes = $probes; + SET ivfflat.iterative_search = on; + EXPLAIN ANALYZE SELECT i FROM tst WHERE i % $c = 0 ORDER BY v $operator '$queries[0]' LIMIT $limit; + )); + like($explain, qr/Index Scan using idx on tst/); + + for my $i (0 .. $#queries) + { + my $actual = $node->safe_psql("postgres", qq( + SET enable_seqscan = off; + SET ivfflat.probes = $probes; + SET ivfflat.iterative_search = on; + SELECT i FROM tst WHERE i % $c = 0 ORDER BY v $operator '$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, $operator); +} + +# Initialize node +$node = PostgreSQL::Test::Cluster->new('node'); +$node->init; +$node->start; + +# Create table +$node->safe_psql("postgres", "CREATE EXTENSION vector;"); +$node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector(3));"); +$node->safe_psql("postgres", + "INSERT INTO tst SELECT i, ARRAY[random(), random(), random()] FROM generate_series(1, 100000) i;" +); + +# Generate queries +for (1 .. 20) +{ + my $r1 = rand(); + my $r2 = rand(); + my $r3 = rand(); + push(@queries, "[$r1,$r2,$r3]"); +} + +# Check each index type +my @operators = ("<->", "<=>"); +my @opclasses = ("vector_l2_ops", "vector_cosine_ops"); + +for my $i (0 .. $#operators) +{ + my $operator = $operators[$i]; + my $opclass = $opclasses[$i]; + + $node->safe_psql("postgres", "CREATE INDEX idx ON tst USING ivfflat (v $opclass);"); + + foreach (@cs) + { + my $c = $_; + + # Get exact results + @expected = (); + foreach (@queries) + { + my $res = $node->safe_psql("postgres", qq( + SET enable_indexscan = off; + WITH top AS ( + SELECT v $operator '$_' AS distance FROM tst WHERE i % $c = 0 ORDER BY distance LIMIT $limit + ) + SELECT i FROM tst WHERE (v $operator '$_') <= (SELECT MAX(distance) FROM top) + )); + push(@expected, $res); + } + + if ($c == 100) + { + test_recall($c, 1, 0.58, $operator); + test_recall($c, 10, 0.98, $operator); + } + else + { + if ($operator eq "<->") + { + test_recall($c, 1, 0.80, $operator); + } + else + { + test_recall($c, 1, 0.88, $operator); + } + } + } + + $node->safe_psql("postgres", "DROP INDEX idx;"); +} + +done_testing(); From 961cb17d80654249a5ab035ab3cf7d175dcac92b Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 10 Oct 2024 18:14:39 -0700 Subject: [PATCH 65/88] Added iterative search for HNSW [skip ci] --- CHANGELOG.md | 1 + src/hnsw.c | 18 +++ src/hnsw.h | 24 +++- src/hnswinsert.c | 10 +- src/hnswscan.c | 140 +++++++++++++++++++-- src/hnswutils.c | 88 ++++++++++--- src/hnswvacuum.c | 8 ++ test/t/043_hnsw_iterative_search.pl | 67 ++++++++++ test/t/044_hnsw_iterative_search_recall.pl | 131 +++++++++++++++++++ 9 files changed, 457 insertions(+), 30 deletions(-) create mode 100644 test/t/043_hnsw_iterative_search.pl create mode 100644 test/t/044_hnsw_iterative_search_recall.pl diff --git a/CHANGELOG.md b/CHANGELOG.md index a7d9924..6753a7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 0.8.0 (unreleased) +- Added support for iterative index scans - Added casts for arrays to `sparsevec` - Improved cost estimation - Improved performance of HNSW inserts and on-disk index builds diff --git a/src/hnsw.c b/src/hnsw.c index c2579c1..57fcbdb 100644 --- a/src/hnsw.c +++ b/src/hnsw.c @@ -18,7 +18,16 @@ #define MarkGUCPrefixReserved(x) EmitWarningsOnPlaceholders(x) #endif +static const struct config_enum_entry hnsw_iterative_search_options[] = { + {"off", HNSW_ITERATIVE_SEARCH_OFF, false}, + {"on", HNSW_ITERATIVE_SEARCH_RELAXED, false}, + {"strict", HNSW_ITERATIVE_SEARCH_STRICT, false}, + {NULL, 0, false} +}; + int hnsw_ef_search; +int hnsw_iterative_search_max_tuples; +int hnsw_iterative_search; int hnsw_lock_tranche_id; static relopt_kind hnsw_relopt_kind; @@ -69,6 +78,15 @@ HnswInit(void) "Valid range is 1..1000.", &hnsw_ef_search, HNSW_DEFAULT_EF_SEARCH, HNSW_MIN_EF_SEARCH, HNSW_MAX_EF_SEARCH, PGC_USERSET, 0, NULL, NULL, NULL); + DefineCustomEnumVariable("hnsw.iterative_search", "Sets iterative search", + NULL, &hnsw_iterative_search, + HNSW_ITERATIVE_SEARCH_OFF, hnsw_iterative_search_options, PGC_USERSET, 0, NULL, NULL, NULL); + + /* TODO Ensure ivfflat.max_probes uses same value for "all" */ + DefineCustomIntVariable("hnsw.iterative_search_max_tuples", "Sets the max number of candidates to visit for iterative search", + "-1 means all", &hnsw_iterative_search_max_tuples, + -1, -1, INT_MAX, PGC_USERSET, 0, NULL, NULL, NULL); + MarkGUCPrefixReserved("hnsw"); } diff --git a/src/hnsw.h b/src/hnsw.h index b2614d1..254a60a 100644 --- a/src/hnsw.h +++ b/src/hnsw.h @@ -109,8 +109,17 @@ /* Variables */ extern int hnsw_ef_search; +extern int hnsw_iterative_search; +extern int hnsw_iterative_search_max_tuples; extern int hnsw_lock_tranche_id; +typedef enum HnswIterativeSearchType +{ + HNSW_ITERATIVE_SEARCH_OFF, + HNSW_ITERATIVE_SEARCH_RELAXED, + HNSW_ITERATIVE_SEARCH_STRICT +} HnswIterativeSearchType; + typedef struct HnswElementData HnswElementData; typedef struct HnswNeighborArray HnswNeighborArray; @@ -132,6 +141,7 @@ struct HnswElementData uint8 heaptidsLength; uint8 level; uint8 deleted; + uint8 version; uint32 hash; HnswNeighborsPtr neighbors; BlockNumber blkno; @@ -319,10 +329,10 @@ typedef struct HnswElementTupleData uint8 type; uint8 level; uint8 deleted; - uint8 unused; + uint8 version; ItemPointerData heaptids[HNSW_HEAPTIDS]; ItemPointerData neighbortid; - uint16 unused2; + uint16 unused; Vector data; } HnswElementTupleData; @@ -331,7 +341,7 @@ typedef HnswElementTupleData * HnswElementTuple; typedef struct HnswNeighborTupleData { uint8 type; - uint8 unused; + uint8 version; uint16 count; ItemPointerData indextids[FLEXIBLE_ARRAY_MEMBER]; } HnswNeighborTupleData; @@ -356,6 +366,12 @@ typedef struct HnswScanOpaqueData const HnswTypeInfo *typeInfo; bool first; List *w; + visited_hash v; + pairingheap *discarded; + HnswQuery q; + int m; + int64 tuples; + double previousDistance; MemoryContext tmpCtx; /* Support functions */ @@ -399,7 +415,7 @@ bool HnswCheckNorm(HnswSupport * support, Datum value); Buffer HnswNewBuffer(Relation index, ForkNumber forkNum); void HnswInitPage(Buffer buf, Page page); void HnswInit(void); -List *HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation index, HnswSupport * support, int m, bool inserting, HnswElement skipElement); +List *HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation index, HnswSupport * support, int m, bool inserting, HnswElement skipElement, visited_hash * v, pairingheap **discarded, bool initVisited, int64 *tuples); HnswElement HnswGetEntryPoint(Relation index); void HnswGetMetaPageInfo(Relation index, int *m, HnswElement * entryPoint); void *HnswAlloc(HnswAllocator * allocator, Size size); diff --git a/src/hnswinsert.c b/src/hnswinsert.c index 84eb1d4..a5fac4e 100644 --- a/src/hnswinsert.c +++ b/src/hnswinsert.c @@ -36,7 +36,7 @@ GetInsertPage(Relation index) * Check for a free offset */ static bool -HnswFreeOffset(Relation index, Buffer buf, Page page, HnswElement element, Size etupSize, Size ntupSize, Buffer *nbuf, Page *npage, OffsetNumber *freeOffno, OffsetNumber *freeNeighborOffno, BlockNumber *newInsertPage) +HnswFreeOffset(Relation index, Buffer buf, Page page, HnswElement element, Size etupSize, Size ntupSize, Buffer *nbuf, Page *npage, OffsetNumber *freeOffno, OffsetNumber *freeNeighborOffno, BlockNumber *newInsertPage, uint8 *tupleVersion) { OffsetNumber offno; OffsetNumber maxoffno = PageGetMaxOffsetNumber(page); @@ -98,6 +98,7 @@ HnswFreeOffset(Relation index, Buffer buf, Page page, HnswElement element, Size { *freeOffno = offno; *freeNeighborOffno = neighborOffno; + *tupleVersion = etup->version; return true; } else if (*nbuf != buf) @@ -153,6 +154,7 @@ AddElementOnDisk(Relation index, HnswElement e, int m, BlockNumber insertPage, B OffsetNumber freeOffno = InvalidOffsetNumber; OffsetNumber freeNeighborOffno = InvalidOffsetNumber; BlockNumber newInsertPage = InvalidBlockNumber; + uint8 tupleVersion; char *base = NULL; /* Calculate sizes */ @@ -202,7 +204,7 @@ AddElementOnDisk(Relation index, HnswElement e, int m, BlockNumber insertPage, B } /* Next, try space from a deleted element */ - if (HnswFreeOffset(index, buf, page, e, etupSize, ntupSize, &nbuf, &npage, &freeOffno, &freeNeighborOffno, &newInsertPage)) + if (HnswFreeOffset(index, buf, page, e, etupSize, ntupSize, &nbuf, &npage, &freeOffno, &freeNeighborOffno, &newInsertPage, &tupleVersion)) { if (nbuf != buf) { @@ -212,6 +214,10 @@ AddElementOnDisk(Relation index, HnswElement e, int m, BlockNumber insertPage, B npage = GenericXLogRegisterBuffer(state, nbuf, 0); } + /* Set tuple version */ + etup->version = tupleVersion; + ntup->version = tupleVersion; + break; } diff --git a/src/hnswscan.c b/src/hnswscan.c index 2c6a454..3a5c5d5 100644 --- a/src/hnswscan.c +++ b/src/hnswscan.c @@ -1,5 +1,7 @@ #include "postgres.h" +#include + #include "access/relscan.h" #include "hnsw.h" #include "pgstat.h" @@ -21,25 +23,57 @@ GetScanItems(IndexScanDesc scan, Datum value) int m; HnswElement entryPoint; char *base = NULL; - HnswQuery q; - - q.value = value; + HnswQuery *q = &so->q; /* Get m and entry point */ HnswGetMetaPageInfo(index, &m, &entryPoint); + q->value = value; + so->m = m; + if (entryPoint == NULL) return NIL; - ep = list_make1(HnswEntryCandidate(base, entryPoint, &q, index, support, false)); + ep = list_make1(HnswEntryCandidate(base, entryPoint, q, index, support, false)); for (int lc = entryPoint->level; lc >= 1; lc--) { - w = HnswSearchLayer(base, &q, ep, 1, lc, index, support, m, false, NULL); + w = HnswSearchLayer(base, q, ep, 1, lc, index, support, m, false, NULL, NULL, NULL, true, NULL); ep = w; } - return HnswSearchLayer(base, &q, ep, hnsw_ef_search, 0, index, support, m, false, NULL); + return HnswSearchLayer(base, q, ep, hnsw_ef_search, 0, index, support, m, false, NULL, &so->v, hnsw_iterative_search != HNSW_ITERATIVE_SEARCH_OFF ? &so->discarded : NULL, true, &so->tuples); +} + +/* + * Resume scan at ground level with discarded candidates + */ +static List * +ResumeScanItems(IndexScanDesc scan) +{ + HnswScanOpaque so = (HnswScanOpaque) scan->opaque; + Relation index = scan->indexRelation; + List *ep = NIL; + char *base = NULL; + int batch_size = hnsw_ef_search; + + if (pairingheap_is_empty(so->discarded)) + return NIL; + + /* Get next batch of candidates */ + for (int i = 0; i < batch_size; i++) + { + HnswSearchCandidate *sc; + + if (pairingheap_is_empty(so->discarded)) + break; + + sc = HnswGetSearchCandidate(w_node, pairingheap_remove_first(so->discarded)); + + ep = lappend(ep, sc); + } + + return HnswSearchLayer(base, &so->q, ep, batch_size, 0, index, &so->support, so->m, false, NULL, &so->v, &so->discarded, false, &so->tuples); } /* @@ -83,6 +117,8 @@ hnswbeginscan(Relation index, int nkeys, int norderbys) so = (HnswScanOpaque) palloc(sizeof(HnswScanOpaqueData)); so->typeInfo = HnswGetTypeInfo(index); so->first = true; + so->v.tids = NULL; + so->discarded = NULL; so->tmpCtx = AllocSetContextCreate(CurrentMemoryContext, "Hnsw scan temporary context", ALLOCSET_DEFAULT_SIZES); @@ -103,7 +139,15 @@ hnswrescan(IndexScanDesc scan, ScanKey keys, int nkeys, ScanKey orderbys, int no { HnswScanOpaque so = (HnswScanOpaque) scan->opaque; + if (so->v.tids != NULL) + tidhash_reset(so->v.tids); + + if (so->discarded != NULL) + pairingheap_reset(so->discarded); + so->first = true; + so->tuples = 0; + so->previousDistance = -INFINITY; MemoryContextReset(so->tmpCtx); if (keys && scan->numberOfKeys > 0) @@ -165,22 +209,100 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir) #endif } - while (list_length(so->w) > 0) + for (;;) { char *base = NULL; - HnswSearchCandidate *sc = llast(so->w); - HnswElement element = HnswPtrAccess(base, sc->element); + HnswSearchCandidate *sc; + HnswElement element; ItemPointer heaptid; + if (list_length(so->w) == 0) + { + if (hnsw_iterative_search == HNSW_ITERATIVE_SEARCH_OFF) + break; + + /* Empty index */ + if (so->discarded == NULL) + break; + + /* Reached max number of additional tuples */ + if (hnsw_iterative_search_max_tuples != -1 && so->tuples >= hnsw_iterative_search_max_tuples) + { + if (pairingheap_is_empty(so->discarded)) + break; + + /* Return remaining tuples */ + so->w = lappend(so->w, HnswGetSearchCandidate(w_node, pairingheap_remove_first(so->discarded))); + } + /* Prevent scans from consuming too much memory */ + else if (MemoryContextMemAllocated(so->tmpCtx, false) > (Size) work_mem * 1024L) + { + if (pairingheap_is_empty(so->discarded)) + { + ereport(DEBUG1, + (errmsg("hnsw index scan exceeded work_mem after " INT64_FORMAT " tuples", so->tuples), + errhint("Increase work_mem to scan more tuples."))); + + break; + } + + /* Return remaining tuples */ + so->w = lappend(so->w, HnswGetSearchCandidate(w_node, pairingheap_remove_first(so->discarded))); + } + else + { + /* + * Locking ensures when neighbors are read, the elements they + * reference will not be deleted (and replaced) during the + * iteration. + * + * Elements loaded into memory on previous iterations may have + * been deleted (and replaced), so when reading neighbors, the + * element version must be checked. + */ + LockPage(scan->indexRelation, HNSW_SCAN_LOCK, ShareLock); + + so->w = ResumeScanItems(scan); + + UnlockPage(scan->indexRelation, HNSW_SCAN_LOCK, ShareLock); + +#if defined(HNSW_MEMORY) + elog(INFO, "memory: %zu KB", MemoryContextMemAllocated(so->tmpCtx, false) / 1024); +#endif + } + + if (list_length(so->w) == 0) + break; + } + + sc = llast(so->w); + element = HnswPtrAccess(base, sc->element); + /* Move to next element if no valid heap TIDs */ if (element->heaptidsLength == 0) { so->w = list_delete_last(so->w); + + /* Mark memory as free for next iteration */ + if (hnsw_iterative_search != HNSW_ITERATIVE_SEARCH_OFF) + { + pfree(element); + pfree(sc); + } + continue; } heaptid = &element->heaptids[--element->heaptidsLength]; + if (hnsw_iterative_search == HNSW_ITERATIVE_SEARCH_STRICT) + { + if (sc->distance < so->previousDistance) + continue; + + so->previousDistance = sc->distance; + } + MemoryContextSwitchTo(oldCtx); scan->xs_heaptid = *heaptid; diff --git a/src/hnswutils.c b/src/hnswutils.c index c51fe28..732fcd7 100644 --- a/src/hnswutils.c +++ b/src/hnswutils.c @@ -251,6 +251,8 @@ HnswInitElement(char *base, ItemPointer heaptid, int m, double ml, int maxLevel, element->level = level; element->deleted = 0; + /* Start at one to make it easier to find issues */ + element->version = 1; HnswInitNeighbors(base, element, m, allocator); @@ -430,6 +432,7 @@ HnswSetElementTuple(char *base, HnswElementTuple etup, HnswElement element) etup->type = HNSW_ELEMENT_TUPLE_TYPE; etup->level = element->level; etup->deleted = 0; + etup->version = element->version; for (int i = 0; i < HNSW_HEAPTIDS; i++) { if (i < element->heaptidsLength) @@ -472,6 +475,7 @@ HnswSetNeighborTuple(char *base, HnswNeighborTuple ntup, HnswElement e, int m) } ntup->count = idx; + ntup->version = e->version; } /* @@ -482,6 +486,7 @@ HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool loadHe { element->level = etup->level; element->deleted = etup->deleted; + element->version = etup->version; element->neighborPage = ItemPointerGetBlockNumber(&etup->neighbortid); element->neighborOffno = ItemPointerGetOffsetNumber(&etup->neighbortid); element->heaptidsLength = 0; @@ -608,6 +613,21 @@ CompareNearestCandidates(const pairingheap_node *a, const pairingheap_node *b, v return 0; } +/* + * Compare discarded candidate distances + */ +static int +CompareNearestDiscardedCandidates(const pairingheap_node *a, const pairingheap_node *b, void *arg) +{ + if (HnswGetSearchCandidateConst(w_node, a)->distance < HnswGetSearchCandidateConst(w_node, b)->distance) + return 1; + + if (HnswGetSearchCandidateConst(w_node, a)->distance > HnswGetSearchCandidateConst(w_node, b)->distance) + return -1; + + return 0; +} + /* * Compare candidate distances */ @@ -728,8 +748,11 @@ HnswLoadNeighborTids(HnswElement element, ItemPointerData *indextids, Relation i ntup = (HnswNeighborTuple) PageGetItem(page, PageGetItemId(page, element->neighborOffno)); - /* Ensure expected neighbors */ - if (ntup->count != (element->level + 2) * m) + /* + * Ensure the neighbor tuple has not been deleted or replaced between + * index scan iterations + */ + if (ntup->version != element->version || ntup->count != (element->level + 2) * m) { UnlockReleaseBuffer(buf); return false; @@ -775,13 +798,13 @@ HnswLoadUnvisitedFromDisk(HnswElement element, HnswUnvisited * unvisited, int *u * Algorithm 2 from paper */ List * -HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation index, HnswSupport * support, int m, bool inserting, HnswElement skipElement) +HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation index, HnswSupport * support, int m, bool inserting, HnswElement skipElement, visited_hash * v, pairingheap **discarded, bool initVisited, int64 *tuples) { List *w = NIL; pairingheap *C = pairingheap_allocate(CompareNearestCandidates, NULL); pairingheap *W = pairingheap_allocate(CompareFurthestCandidates, NULL); int wlen = 0; - visited_hash v; + visited_hash vh; ListCell *lc2; HnswNeighborArray *localNeighborhood = NULL; Size neighborhoodSize = 0; @@ -790,7 +813,19 @@ HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation in int unvisitedLength; bool inMemory = index == NULL; - InitVisited(base, &v, inMemory, ef, m); + if (v == NULL) + { + v = &vh; + initVisited = true; + } + + if (initVisited) + { + InitVisited(base, v, inMemory, ef, m); + + if (discarded != NULL) + *discarded = pairingheap_allocate(CompareNearestDiscardedCandidates, NULL); + } /* Create local memory for neighborhood if needed */ if (inMemory) @@ -805,7 +840,13 @@ HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation in HnswSearchCandidate *sc = (HnswSearchCandidate *) lfirst(lc2); bool found; - AddToVisited(base, &v, sc->element, inMemory, &found); + if (initVisited) + { + AddToVisited(base, v, sc->element, inMemory, &found); + + if (tuples != NULL) + (*tuples)++; + } pairingheap_add(C, &sc->c_node); pairingheap_add(W, &sc->w_node); @@ -831,9 +872,12 @@ HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation in cElement = HnswPtrAccess(base, c->element); if (inMemory) - HnswLoadUnvisitedFromMemory(base, cElement, unvisited, &unvisitedLength, &v, lc, localNeighborhood, neighborhoodSize); + HnswLoadUnvisitedFromMemory(base, cElement, unvisited, &unvisitedLength, v, lc, localNeighborhood, neighborhoodSize); else - HnswLoadUnvisitedFromDisk(cElement, unvisited, &unvisitedLength, &v, index, m, lm, lc); + HnswLoadUnvisitedFromDisk(cElement, unvisited, &unvisitedLength, v, index, m, lm, lc); + + if (tuples != NULL) + (*tuples) += unvisitedLength; for (int i = 0; i < unvisitedLength; i++) { @@ -857,16 +901,25 @@ HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation in /* Avoid any allocations if not adding */ eElement = NULL; - HnswLoadElementImpl(blkno, offno, &eDistance, q, index, support, inserting, alwaysAdd ? NULL : &f->distance, &eElement); + HnswLoadElementImpl(blkno, offno, &eDistance, q, index, support, inserting, alwaysAdd || discarded != NULL ? NULL : &f->distance, &eElement); if (eElement == NULL) continue; } - if (!(eDistance < f->distance || alwaysAdd)) - continue; + if (eElement == NULL || !(eDistance < f->distance || alwaysAdd)) + { + if (discarded != NULL) + { + /* Create a new candidate */ + e = palloc(sizeof(HnswSearchCandidate)); + HnswPtrStore(base, e->element, eElement); + e->distance = eDistance; + pairingheap_add(*discarded, &e->w_node); + } - Assert(!eElement->deleted); + continue; + } /* Make robust to issues */ if (eElement->level < lc) @@ -890,7 +943,12 @@ HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation in /* No need to decrement wlen */ if (wlen > ef) - pairingheap_remove_first(W); + { + HnswSearchCandidate *d = HnswGetSearchCandidate(w_node, pairingheap_remove_first(W)); + + if (discarded != NULL) + pairingheap_add(*discarded, &d->w_node); + } } } } @@ -1225,7 +1283,7 @@ HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint /* 1st phase: greedy search to insert level */ for (int lc = entryLevel; lc >= level + 1; lc--) { - w = HnswSearchLayer(base, &q, ep, 1, lc, index, support, m, true, skipElement); + w = HnswSearchLayer(base, &q, ep, 1, lc, index, support, m, true, skipElement, NULL, NULL, true, NULL); ep = w; } @@ -1244,7 +1302,7 @@ HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint List *lw = NIL; ListCell *lc2; - w = HnswSearchLayer(base, &q, ep, efConstruction, lc, index, support, m, true, skipElement); + w = HnswSearchLayer(base, &q, ep, efConstruction, lc, index, support, m, true, skipElement, NULL, NULL, true, NULL); /* Convert search candidates to candidates */ foreach(lc2, w) diff --git a/src/hnswvacuum.c b/src/hnswvacuum.c index d3cdf68..251d9d9 100644 --- a/src/hnswvacuum.c +++ b/src/hnswvacuum.c @@ -527,6 +527,14 @@ MarkDeleted(HnswVacuumState * vacuumstate) for (int i = 0; i < ntup->count; i++) ItemPointerSetInvalid(&ntup->indextids[i]); + /* Increment version */ + /* This is used to avoid incorrect reads for iterative scans */ + /* Reserve some bits for future use */ + etup->version++; + if (etup->version > 15) + etup->version = 1; + ntup->version = etup->version; + /* * We modified the tuples in place, no need to call * PageIndexTupleOverwrite diff --git a/test/t/043_hnsw_iterative_search.pl b/test/t/043_hnsw_iterative_search.pl new file mode 100644 index 0000000..6905fc4 --- /dev/null +++ b/test/t/043_hnsw_iterative_search.pl @@ -0,0 +1,67 @@ +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $dim = 3; +my $array_sql = join(",", ('random()') x $dim); + +# Initialize node +my $node = PostgreSQL::Test::Cluster->new('node'); +$node->init; +$node->start; + +# Create table +$node->safe_psql("postgres", "CREATE EXTENSION vector;"); +$node->safe_psql("postgres", "CREATE TABLE tst (i int4 PRIMARY KEY, v vector($dim));"); +$node->safe_psql("postgres", + "INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(1, 100000) i;" +); +$node->safe_psql("postgres", qq( + SET maintenance_work_mem = '128MB'; + SET max_parallel_maintenance_workers = 2; + CREATE INDEX ON tst USING hnsw (v vector_l2_ops) +)); + +my $count = $node->safe_psql("postgres", qq( + SET enable_seqscan = off; + SET hnsw.iterative_search = on; + SET work_mem = '8MB'; + SELECT COUNT(*) FROM (SELECT v FROM tst WHERE i % 10000 = 0 ORDER BY v <-> (SELECT v FROM tst LIMIT 1) LIMIT 11) t; +)); +is($count, 10); + +foreach ((30000, 50000, 70000)) +{ + my $max_tuples = $_; + my $expected = $max_tuples / 10000; + my $sum = 0; + + for my $i (1 .. 20) + { + $count = $node->safe_psql("postgres", qq( + SET enable_seqscan = off; + SET hnsw.iterative_search = on; + SET hnsw.iterative_search_max_tuples = $max_tuples; + SET work_mem = '8MB'; + SELECT COUNT(*) FROM (SELECT v FROM tst WHERE i % 10000 = 0 ORDER BY v <-> (SELECT v FROM tst WHERE i = $i) LIMIT 11) t; + )); + $sum += $count; + } + + my $avg = $sum / 20; + cmp_ok($avg, '>', $expected - 2); + cmp_ok($avg, '<', $expected + 2); +} + +my ($ret, $stdout, $stderr) = $node->psql("postgres", qq( + SET enable_seqscan = off; + SET hnsw.iterative_search = on; + SET client_min_messages = debug1; + SET work_mem = '2MB'; + SELECT COUNT(*) FROM (SELECT v FROM tst WHERE i % 10000 = 0 ORDER BY v <-> (SELECT v FROM tst LIMIT 1) LIMIT 11) t; +)); +like($stderr, qr/hnsw index scan exceeded work_mem after \d+ tuples/); + +done_testing(); diff --git a/test/t/044_hnsw_iterative_search_recall.pl b/test/t/044_hnsw_iterative_search_recall.pl new file mode 100644 index 0000000..8bedc32 --- /dev/null +++ b/test/t/044_hnsw_iterative_search_recall.pl @@ -0,0 +1,131 @@ +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node; +my @queries = (); +my @expected; +my $limit = 20; +my $dim = 3; +my $array_sql = join(",", ('random()') x $dim); +my @cs = (100, 1000); + +sub test_recall +{ + my ($c, $ef_search, $min, $operator) = @_; + my $correct = 0; + my $total = 0; + + my $explain = $node->safe_psql("postgres", qq( + SET enable_seqscan = off; + SET hnsw.ef_search = $ef_search; + SET hnsw.iterative_search = on; + EXPLAIN ANALYZE SELECT i FROM tst WHERE i % $c = 0 ORDER BY v $operator '$queries[0]' LIMIT $limit; + )); + like($explain, qr/Index Scan using idx on tst/); + + for my $i (0 .. $#queries) + { + my $actual = $node->safe_psql("postgres", qq( + SET enable_seqscan = off; + SET hnsw.ef_search = $ef_search; + SET hnsw.iterative_search = on; + SELECT i FROM tst WHERE i % $c = 0 ORDER BY v $operator '$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, $operator); +} + +# Initialize node +$node = PostgreSQL::Test::Cluster->new('node'); +$node->init; +$node->start; + +# Create table +$node->safe_psql("postgres", "CREATE EXTENSION vector;"); +$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, 100000) i;" +); + +# Generate queries +for (1 .. 20) +{ + my @r = (); + for (1 .. $dim) + { + push(@r, rand()); + } + push(@queries, "[" . join(",", @r) . "]"); +} + +# Check each index type +my @operators = ("<->", "<=>"); +my @opclasses = ("vector_l2_ops", "vector_cosine_ops"); + +for my $i (0 .. $#operators) +{ + my $operator = $operators[$i]; + my $opclass = $opclasses[$i]; + + $node->safe_psql("postgres", qq( + SET maintenance_work_mem = '128MB'; + CREATE INDEX idx ON tst USING hnsw (v $opclass); + )); + + foreach (@cs) + { + my $c = $_; + + # Get exact results + @expected = (); + foreach (@queries) + { + my $res = $node->safe_psql("postgres", qq( + SET enable_indexscan = off; + WITH top AS ( + SELECT v $operator '$_' AS distance FROM tst WHERE i % $c = 0 ORDER BY distance LIMIT $limit + ) + SELECT i FROM tst WHERE (v $operator '$_') <= (SELECT MAX(distance) FROM top) + )); + push(@expected, $res); + } + + if ($c == 100) + { + test_recall($c, 40, 0.99, $operator); + } + else + { + if ($operator eq "<->") + { + test_recall($c, 40, 0.99, $operator); + } + else + { + test_recall($c, 40, 0.99, $operator); + } + } + } + + $node->safe_psql("postgres", "DROP INDEX idx;"); +} + +done_testing(); From 2832e746f0ab76651b07d07c38353d51b9bd915e Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 10 Oct 2024 18:16:39 -0700 Subject: [PATCH 66/88] Use get_float8_infinity instead of INFINITY Co-authored-by: "Jonathan S. Katz" --- src/hnswscan.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/hnswscan.c b/src/hnswscan.c index 3a5c5d5..b473e2b 100644 --- a/src/hnswscan.c +++ b/src/hnswscan.c @@ -1,12 +1,11 @@ #include "postgres.h" -#include - #include "access/relscan.h" #include "hnsw.h" #include "pgstat.h" #include "storage/bufmgr.h" #include "storage/lmgr.h" +#include "utils/float.h" #include "utils/memutils.h" /* @@ -147,7 +146,7 @@ hnswrescan(IndexScanDesc scan, ScanKey keys, int nkeys, ScanKey orderbys, int no so->first = true; so->tuples = 0; - so->previousDistance = -INFINITY; + so->previousDistance = -get_float8_infinity(); MemoryContextReset(so->tmpCtx); if (keys && scan->numberOfKeys > 0) From 35b252a3e371f499ff0418b4b6c2348adf3d82e9 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 10 Oct 2024 18:33:35 -0700 Subject: [PATCH 67/88] Switched to macos-13 on CI [skip ci] --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a4682e7..3b81560 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,7 +49,7 @@ jobs: - postgres: 16 os: macos-14 - postgres: 14 - os: macos-12 + os: macos-13 steps: - uses: actions/checkout@v4 - uses: ankane/setup-postgres@v1 From 124018b8dd24b24ae43e2cc67b591f193b9d2cab Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 10 Oct 2024 19:30:47 -0700 Subject: [PATCH 68/88] Added HnswInitSearchCandidate function --- src/hnswutils.c | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/hnswutils.c b/src/hnswutils.c index 732fcd7..e84561e 100644 --- a/src/hnswutils.c +++ b/src/hnswutils.c @@ -581,21 +581,34 @@ GetElementDistance(char *base, HnswElement element, HnswQuery * q, HnswSupport * return HnswGetDistance(q->value, value, support); } +/* + * Allocate a search candidate + */ +static HnswSearchCandidate * +HnswInitSearchCandidate(char *base, HnswElement element, double distance) +{ + HnswSearchCandidate *sc = palloc(sizeof(HnswSearchCandidate)); + + HnswPtrStore(base, sc->element, element); + sc->distance = distance; + return sc; +} + /* * Create a candidate for the entry point */ HnswSearchCandidate * HnswEntryCandidate(char *base, HnswElement entryPoint, HnswQuery * q, Relation index, HnswSupport * support, bool loadVec) { - HnswSearchCandidate *sc = palloc(sizeof(HnswSearchCandidate)); bool inMemory = index == NULL; + double distance; - HnswPtrStore(base, sc->element, entryPoint); if (inMemory) - sc->distance = GetElementDistance(base, entryPoint, q, support); + distance = GetElementDistance(base, entryPoint, q, support); else - HnswLoadElement(entryPoint, &sc->distance, q, index, support, loadVec, NULL); - return sc; + HnswLoadElement(entryPoint, &distance, q, index, support, loadVec, NULL); + + return HnswInitSearchCandidate(base, entryPoint, distance); } /* @@ -912,9 +925,7 @@ HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation in if (discarded != NULL) { /* Create a new candidate */ - e = palloc(sizeof(HnswSearchCandidate)); - HnswPtrStore(base, e->element, eElement); - e->distance = eDistance; + e = HnswInitSearchCandidate(base, eElement, eDistance); pairingheap_add(*discarded, &e->w_node); } @@ -926,9 +937,7 @@ HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation in continue; /* Create a new candidate */ - e = palloc(sizeof(HnswSearchCandidate)); - HnswPtrStore(base, e->element, eElement); - e->distance = eDistance; + e = HnswInitSearchCandidate(base, eElement, eDistance); pairingheap_add(C, &e->c_node); pairingheap_add(W, &e->w_node); From 8e88b481a66b3e636d64625759e1485c99789af3 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 10 Oct 2024 20:57:57 -0700 Subject: [PATCH 69/88] Use a lower max allocation size than default to allow scanning more tuples for iterative search before exceeding work_mem [skip ci] --- src/hnswscan.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/hnswscan.c b/src/hnswscan.c index b473e2b..1b103eb 100644 --- a/src/hnswscan.c +++ b/src/hnswscan.c @@ -118,9 +118,14 @@ hnswbeginscan(Relation index, int nkeys, int norderbys) so->first = true; so->v.tids = NULL; so->discarded = NULL; + + /* + * Use a lower max allocation size than default to allow scanning more + * tuples for iterative search before exceeding work_mem + */ so->tmpCtx = AllocSetContextCreate(CurrentMemoryContext, "Hnsw scan temporary context", - ALLOCSET_DEFAULT_SIZES); + 0, 8 * 1024, 512 * 1024); /* Set support functions */ HnswInitSupport(&so->support, index); From 960d2848cbdef7594e6c1e18bbfa232058e6b309 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 10 Oct 2024 21:02:33 -0700 Subject: [PATCH 70/88] Updated comment [skip ci] --- src/hnswscan.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hnswscan.c b/src/hnswscan.c index 1b103eb..6e9a9ab 100644 --- a/src/hnswscan.c +++ b/src/hnswscan.c @@ -229,7 +229,7 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir) if (so->discarded == NULL) break; - /* Reached max number of additional tuples */ + /* Reached max number of tuples */ if (hnsw_iterative_search_max_tuples != -1 && so->tuples >= hnsw_iterative_search_max_tuples) { if (pairingheap_is_empty(so->discarded)) From 2dc392ed6c953da581651a2decc667b29136e356 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 10 Oct 2024 23:50:11 -0700 Subject: [PATCH 71/88] Updated GUC names [skip ci] --- src/hnsw.c | 4 ++-- src/ivfflat.c | 2 +- test/t/041_ivfflat_iterative_search.pl | 4 ++-- test/t/042_ivfflat_iterative_search_recall.pl | 4 ++-- test/t/043_hnsw_iterative_search.pl | 6 +++--- test/t/044_hnsw_iterative_search_recall.pl | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/hnsw.c b/src/hnsw.c index 57fcbdb..ca60455 100644 --- a/src/hnsw.c +++ b/src/hnsw.c @@ -20,8 +20,8 @@ static const struct config_enum_entry hnsw_iterative_search_options[] = { {"off", HNSW_ITERATIVE_SEARCH_OFF, false}, - {"on", HNSW_ITERATIVE_SEARCH_RELAXED, false}, - {"strict", HNSW_ITERATIVE_SEARCH_STRICT, false}, + {"relaxed_order", HNSW_ITERATIVE_SEARCH_RELAXED, false}, + {"strict_order", HNSW_ITERATIVE_SEARCH_STRICT, false}, {NULL, 0, false} }; diff --git a/src/ivfflat.c b/src/ivfflat.c index 0b24875..1854fba 100644 --- a/src/ivfflat.c +++ b/src/ivfflat.c @@ -23,7 +23,7 @@ static relopt_kind ivfflat_relopt_kind; static const struct config_enum_entry ivfflat_iterative_search_options[] = { {"off", IVFFLAT_ITERATIVE_SEARCH_OFF, false}, - {"on", IVFFLAT_ITERATIVE_SEARCH_RELAXED, false}, + {"relaxed_order", IVFFLAT_ITERATIVE_SEARCH_RELAXED, false}, {NULL, 0, false} }; diff --git a/test/t/041_ivfflat_iterative_search.pl b/test/t/041_ivfflat_iterative_search.pl index 231c49e..2b1add3 100644 --- a/test/t/041_ivfflat_iterative_search.pl +++ b/test/t/041_ivfflat_iterative_search.pl @@ -23,7 +23,7 @@ $node->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v vector_l2_ops my $count = $node->safe_psql("postgres", qq( SET enable_seqscan = off; SET ivfflat.probes = 10; - SET ivfflat.iterative_search = on; + SET ivfflat.iterative_search = relaxed_order; SELECT COUNT(*) FROM (SELECT v FROM tst WHERE i % 10000 = 0 ORDER BY v <-> (SELECT v FROM tst LIMIT 1) LIMIT 11) t; )); is($count, 10); @@ -39,7 +39,7 @@ foreach ((30, 50, 70)) $count = $node->safe_psql("postgres", qq( SET enable_seqscan = off; SET ivfflat.probes = 10; - SET ivfflat.iterative_search = on; + SET ivfflat.iterative_search = relaxed_order; SET ivfflat.iterative_search_max_probes = $max_probes; SELECT COUNT(*) FROM (SELECT v FROM tst WHERE i % 10000 = 0 ORDER BY v <-> (SELECT v FROM tst WHERE i = $i) LIMIT 11) t; )); diff --git a/test/t/042_ivfflat_iterative_search_recall.pl b/test/t/042_ivfflat_iterative_search_recall.pl index 6bdddd0..5cdcd71 100644 --- a/test/t/042_ivfflat_iterative_search_recall.pl +++ b/test/t/042_ivfflat_iterative_search_recall.pl @@ -19,7 +19,7 @@ sub test_recall my $explain = $node->safe_psql("postgres", qq( SET enable_seqscan = off; SET ivfflat.probes = $probes; - SET ivfflat.iterative_search = on; + SET ivfflat.iterative_search = relaxed_order; EXPLAIN ANALYZE SELECT i FROM tst WHERE i % $c = 0 ORDER BY v $operator '$queries[0]' LIMIT $limit; )); like($explain, qr/Index Scan using idx on tst/); @@ -29,7 +29,7 @@ sub test_recall my $actual = $node->safe_psql("postgres", qq( SET enable_seqscan = off; SET ivfflat.probes = $probes; - SET ivfflat.iterative_search = on; + SET ivfflat.iterative_search = relaxed_order; SELECT i FROM tst WHERE i % $c = 0 ORDER BY v $operator '$queries[$i]' LIMIT $limit; )); my @actual_ids = split("\n", $actual); diff --git a/test/t/043_hnsw_iterative_search.pl b/test/t/043_hnsw_iterative_search.pl index 6905fc4..6ccef21 100644 --- a/test/t/043_hnsw_iterative_search.pl +++ b/test/t/043_hnsw_iterative_search.pl @@ -26,7 +26,7 @@ $node->safe_psql("postgres", qq( my $count = $node->safe_psql("postgres", qq( SET enable_seqscan = off; - SET hnsw.iterative_search = on; + SET hnsw.iterative_search = relaxed_order; SET work_mem = '8MB'; SELECT COUNT(*) FROM (SELECT v FROM tst WHERE i % 10000 = 0 ORDER BY v <-> (SELECT v FROM tst LIMIT 1) LIMIT 11) t; )); @@ -42,7 +42,7 @@ foreach ((30000, 50000, 70000)) { $count = $node->safe_psql("postgres", qq( SET enable_seqscan = off; - SET hnsw.iterative_search = on; + SET hnsw.iterative_search = relaxed_order; SET hnsw.iterative_search_max_tuples = $max_tuples; SET work_mem = '8MB'; SELECT COUNT(*) FROM (SELECT v FROM tst WHERE i % 10000 = 0 ORDER BY v <-> (SELECT v FROM tst WHERE i = $i) LIMIT 11) t; @@ -57,7 +57,7 @@ foreach ((30000, 50000, 70000)) my ($ret, $stdout, $stderr) = $node->psql("postgres", qq( SET enable_seqscan = off; - SET hnsw.iterative_search = on; + SET hnsw.iterative_search = relaxed_order; SET client_min_messages = debug1; SET work_mem = '2MB'; SELECT COUNT(*) FROM (SELECT v FROM tst WHERE i % 10000 = 0 ORDER BY v <-> (SELECT v FROM tst LIMIT 1) LIMIT 11) t; diff --git a/test/t/044_hnsw_iterative_search_recall.pl b/test/t/044_hnsw_iterative_search_recall.pl index 8bedc32..cec42f8 100644 --- a/test/t/044_hnsw_iterative_search_recall.pl +++ b/test/t/044_hnsw_iterative_search_recall.pl @@ -21,7 +21,7 @@ sub test_recall my $explain = $node->safe_psql("postgres", qq( SET enable_seqscan = off; SET hnsw.ef_search = $ef_search; - SET hnsw.iterative_search = on; + SET hnsw.iterative_search = relaxed_order; EXPLAIN ANALYZE SELECT i FROM tst WHERE i % $c = 0 ORDER BY v $operator '$queries[0]' LIMIT $limit; )); like($explain, qr/Index Scan using idx on tst/); @@ -31,7 +31,7 @@ sub test_recall my $actual = $node->safe_psql("postgres", qq( SET enable_seqscan = off; SET hnsw.ef_search = $ef_search; - SET hnsw.iterative_search = on; + SET hnsw.iterative_search = relaxed_order; SELECT i FROM tst WHERE i % $c = 0 ORDER BY v $operator '$queries[$i]' LIMIT $limit; )); my @actual_ids = split("\n", $actual); From b26a21b848fdb0a642fa16053ebdd1baddac0e25 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 11 Oct 2024 11:07:11 -0700 Subject: [PATCH 72/88] Added regression tests for iterative search [skip ci] --- test/expected/hnsw_vector.out | 43 ++++++++++++++++++++++++++++++++ test/expected/ivfflat_vector.out | 38 ++++++++++++++++++++++++++++ test/sql/hnsw_vector.sql | 25 +++++++++++++++++++ test/sql/ivfflat_vector.sql | 24 ++++++++++++++++++ 4 files changed, 130 insertions(+) diff --git a/test/expected/hnsw_vector.out b/test/expected/hnsw_vector.out index cbda5fa..6a1f984 100644 --- a/test/expected/hnsw_vector.out +++ b/test/expected/hnsw_vector.out @@ -99,6 +99,32 @@ SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <+> (SELECT NULL::vector)) t2 4 (1 row) +DROP TABLE t; +-- iterative +CREATE TABLE t (val vector(3)); +INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL); +CREATE INDEX ON t USING hnsw (val vector_l2_ops); +SET hnsw.iterative_search = strict_order; +SET hnsw.ef_search = 1; +SELECT * FROM t ORDER BY val <-> '[3,3,3]'; + val +--------- + [1,2,3] + [1,1,1] + [0,0,0] +(3 rows) + +SET hnsw.iterative_search = relaxed_order; +SELECT * FROM t ORDER BY val <-> '[3,3,3]'; + val +--------- + [1,2,3] + [1,1,1] + [0,0,0] +(3 rows) + +RESET hnsw.iterative_search; +RESET hnsw.ef_search; DROP TABLE t; -- unlogged CREATE UNLOGGED TABLE t (val vector(3)); @@ -139,4 +165,21 @@ SET hnsw.ef_search = 0; ERROR: 0 is outside the valid range for parameter "hnsw.ef_search" (1 .. 1000) SET hnsw.ef_search = 1001; ERROR: 1001 is outside the valid range for parameter "hnsw.ef_search" (1 .. 1000) +SHOW hnsw.iterative_search; + hnsw.iterative_search +----------------------- + off +(1 row) + +SET hnsw.iterative_search = on; +ERROR: invalid value for parameter "hnsw.iterative_search": "on" +HINT: Available values: off, relaxed_order, strict_order. +SHOW hnsw.iterative_search_max_tuples; + hnsw.iterative_search_max_tuples +---------------------------------- + -1 +(1 row) + +SET hnsw.iterative_search_max_tuples = -2; +ERROR: -2 is outside the valid range for parameter "hnsw.iterative_search_max_tuples" (-1 .. 2147483647) DROP TABLE t; diff --git a/test/expected/ivfflat_vector.out b/test/expected/ivfflat_vector.out index 84871b4..c22135a 100644 --- a/test/expected/ivfflat_vector.out +++ b/test/expected/ivfflat_vector.out @@ -81,6 +81,21 @@ SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <=> (SELECT NULL::vector)) t2 3 (1 row) +DROP TABLE t; +-- iterative +CREATE TABLE t (val vector(3)); +INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL); +CREATE INDEX ON t USING ivfflat (val vector_l2_ops) WITH (lists = 3); +SET ivfflat.iterative_search = relaxed_order; +SELECT * FROM t ORDER BY val <-> '[3,3,3]'; + val +--------- + [1,2,3] + [1,1,1] + [0,0,0] +(3 rows) + +RESET ivfflat.iterative_search; DROP TABLE t; -- unlogged CREATE UNLOGGED TABLE t (val vector(3)); @@ -109,4 +124,27 @@ SHOW ivfflat.probes; 1 (1 row) +SET ivfflat.probes = 0; +ERROR: 0 is outside the valid range for parameter "ivfflat.probes" (1 .. 32768) +SET ivfflat.probes = 32769; +ERROR: 32769 is outside the valid range for parameter "ivfflat.probes" (1 .. 32768) +SHOW ivfflat.iterative_search; + ivfflat.iterative_search +-------------------------- + off +(1 row) + +SET ivfflat.iterative_search = on; +ERROR: invalid value for parameter "ivfflat.iterative_search": "on" +HINT: Available values: off, relaxed_order. +SHOW ivfflat.iterative_search_max_probes; + ivfflat.iterative_search_max_probes +------------------------------------- + 0 +(1 row) + +SET ivfflat.iterative_search_max_probes = -1; +ERROR: -1 is outside the valid range for parameter "ivfflat.iterative_search_max_probes" (0 .. 32768) +SET ivfflat.iterative_search_max_probes = 32769; +ERROR: 32769 is outside the valid range for parameter "ivfflat.iterative_search_max_probes" (0 .. 32768) DROP TABLE t; diff --git a/test/sql/hnsw_vector.sql b/test/sql/hnsw_vector.sql index b7896cf..8f69a4c 100644 --- a/test/sql/hnsw_vector.sql +++ b/test/sql/hnsw_vector.sql @@ -57,6 +57,23 @@ SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <+> (SELECT NULL::vector)) t2 DROP TABLE t; +-- iterative + +CREATE TABLE t (val vector(3)); +INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL); +CREATE INDEX ON t USING hnsw (val vector_l2_ops); + +SET hnsw.iterative_search = strict_order; +SET hnsw.ef_search = 1; +SELECT * FROM t ORDER BY val <-> '[3,3,3]'; + +SET hnsw.iterative_search = relaxed_order; +SELECT * FROM t ORDER BY val <-> '[3,3,3]'; + +RESET hnsw.iterative_search; +RESET hnsw.ef_search; +DROP TABLE t; + -- unlogged CREATE UNLOGGED TABLE t (val vector(3)); @@ -81,4 +98,12 @@ SHOW hnsw.ef_search; SET hnsw.ef_search = 0; SET hnsw.ef_search = 1001; +SHOW hnsw.iterative_search; + +SET hnsw.iterative_search = on; + +SHOW hnsw.iterative_search_max_tuples; + +SET hnsw.iterative_search_max_tuples = -2; + DROP TABLE t; diff --git a/test/sql/ivfflat_vector.sql b/test/sql/ivfflat_vector.sql index 32759e3..93f4224 100644 --- a/test/sql/ivfflat_vector.sql +++ b/test/sql/ivfflat_vector.sql @@ -44,6 +44,18 @@ SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <=> (SELECT NULL::vector)) t2 DROP TABLE t; +-- iterative + +CREATE TABLE t (val vector(3)); +INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL); +CREATE INDEX ON t USING ivfflat (val vector_l2_ops) WITH (lists = 3); + +SET ivfflat.iterative_search = relaxed_order; +SELECT * FROM t ORDER BY val <-> '[3,3,3]'; + +RESET ivfflat.iterative_search; +DROP TABLE t; + -- unlogged CREATE UNLOGGED TABLE t (val vector(3)); @@ -62,4 +74,16 @@ CREATE INDEX ON t USING ivfflat (val vector_l2_ops) WITH (lists = 32769); SHOW ivfflat.probes; +SET ivfflat.probes = 0; +SET ivfflat.probes = 32769; + +SHOW ivfflat.iterative_search; + +SET ivfflat.iterative_search = on; + +SHOW ivfflat.iterative_search_max_probes; + +SET ivfflat.iterative_search_max_probes = -1; +SET ivfflat.iterative_search_max_probes = 32769; + DROP TABLE t; From a3a20f9816131912c047ab1bd6a054aea145d0c1 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 11 Oct 2024 11:18:01 -0700 Subject: [PATCH 73/88] Simplified GUC names [skip ci] --- src/hnsw.c | 10 +++++----- src/hnsw.h | 2 +- src/hnswscan.c | 2 +- src/ivfflat.c | 6 +++--- src/ivfflat.h | 2 +- src/ivfscan.c | 4 ++-- test/expected/hnsw_vector.out | 10 +++++----- test/expected/ivfflat_vector.out | 14 +++++++------- test/sql/hnsw_vector.sql | 4 ++-- test/sql/ivfflat_vector.sql | 6 +++--- test/t/041_ivfflat_iterative_search.pl | 2 +- test/t/043_hnsw_iterative_search.pl | 2 +- 12 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/hnsw.c b/src/hnsw.c index ca60455..acfd14f 100644 --- a/src/hnsw.c +++ b/src/hnsw.c @@ -26,7 +26,7 @@ static const struct config_enum_entry hnsw_iterative_search_options[] = { }; int hnsw_ef_search; -int hnsw_iterative_search_max_tuples; +int hnsw_max_search_tuples; int hnsw_iterative_search; int hnsw_lock_tranche_id; static relopt_kind hnsw_relopt_kind; @@ -78,13 +78,13 @@ HnswInit(void) "Valid range is 1..1000.", &hnsw_ef_search, HNSW_DEFAULT_EF_SEARCH, HNSW_MIN_EF_SEARCH, HNSW_MAX_EF_SEARCH, PGC_USERSET, 0, NULL, NULL, NULL); - DefineCustomEnumVariable("hnsw.iterative_search", "Sets iterative search", + DefineCustomEnumVariable("hnsw.iterative_search", "Sets iterative search mode", NULL, &hnsw_iterative_search, HNSW_ITERATIVE_SEARCH_OFF, hnsw_iterative_search_options, PGC_USERSET, 0, NULL, NULL, NULL); - /* TODO Ensure ivfflat.max_probes uses same value for "all" */ - DefineCustomIntVariable("hnsw.iterative_search_max_tuples", "Sets the max number of candidates to visit for iterative search", - "-1 means all", &hnsw_iterative_search_max_tuples, + /* TODO Ensure ivfflat.max_probes uses same value for no limit */ + DefineCustomIntVariable("hnsw.max_search_tuples", "Sets the max number of candidates to visit for iterative search", + "-1 means no limit", &hnsw_max_search_tuples, -1, -1, INT_MAX, PGC_USERSET, 0, NULL, NULL, NULL); MarkGUCPrefixReserved("hnsw"); diff --git a/src/hnsw.h b/src/hnsw.h index 254a60a..5f170dd 100644 --- a/src/hnsw.h +++ b/src/hnsw.h @@ -110,7 +110,7 @@ /* Variables */ extern int hnsw_ef_search; extern int hnsw_iterative_search; -extern int hnsw_iterative_search_max_tuples; +extern int hnsw_max_search_tuples; extern int hnsw_lock_tranche_id; typedef enum HnswIterativeSearchType diff --git a/src/hnswscan.c b/src/hnswscan.c index 6e9a9ab..84e2332 100644 --- a/src/hnswscan.c +++ b/src/hnswscan.c @@ -230,7 +230,7 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir) break; /* Reached max number of tuples */ - if (hnsw_iterative_search_max_tuples != -1 && so->tuples >= hnsw_iterative_search_max_tuples) + if (hnsw_max_search_tuples != -1 && so->tuples >= hnsw_max_search_tuples) { if (pairingheap_is_empty(so->discarded)) break; diff --git a/src/ivfflat.c b/src/ivfflat.c index 1854fba..2ad27b4 100644 --- a/src/ivfflat.c +++ b/src/ivfflat.c @@ -18,7 +18,7 @@ int ivfflat_probes; int ivfflat_iterative_search; -int ivfflat_iterative_search_max_probes; +int ivfflat_max_probes; static relopt_kind ivfflat_relopt_kind; static const struct config_enum_entry ivfflat_iterative_search_options[] = { @@ -45,8 +45,8 @@ IvfflatInit(void) NULL, &ivfflat_iterative_search, IVFFLAT_ITERATIVE_SEARCH_OFF, ivfflat_iterative_search_options, PGC_USERSET, 0, NULL, NULL, NULL); - DefineCustomIntVariable("ivfflat.iterative_search_max_probes", "Sets the max number of probes for iterative search", - "Zero sets to the number of lists", &ivfflat_iterative_search_max_probes, + DefineCustomIntVariable("ivfflat.max_probes", "Sets the max number of probes for iterative search", + "Zero sets to the number of lists", &ivfflat_max_probes, 0, 0, IVFFLAT_MAX_LISTS, PGC_USERSET, 0, NULL, NULL, NULL); MarkGUCPrefixReserved("ivfflat"); diff --git a/src/ivfflat.h b/src/ivfflat.h index c2b4121..1ca37ad 100644 --- a/src/ivfflat.h +++ b/src/ivfflat.h @@ -81,7 +81,7 @@ /* Variables */ extern int ivfflat_probes; extern int ivfflat_iterative_search; -extern int ivfflat_iterative_search_max_probes; +extern int ivfflat_max_probes; typedef enum IvfflatIterativeSearchType { diff --git a/src/ivfscan.c b/src/ivfscan.c index 578f6aa..548af81 100644 --- a/src/ivfscan.c +++ b/src/ivfscan.c @@ -264,10 +264,10 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys) if (ivfflat_iterative_search != IVFFLAT_ITERATIVE_SEARCH_OFF) { - if (ivfflat_iterative_search_max_probes == 0) + if (ivfflat_max_probes == 0) maxProbes = lists; else - maxProbes = Min(ivfflat_iterative_search_max_probes, lists); + maxProbes = Min(ivfflat_max_probes, lists); } else maxProbes = probes; diff --git a/test/expected/hnsw_vector.out b/test/expected/hnsw_vector.out index 6a1f984..60eb011 100644 --- a/test/expected/hnsw_vector.out +++ b/test/expected/hnsw_vector.out @@ -174,12 +174,12 @@ SHOW hnsw.iterative_search; SET hnsw.iterative_search = on; ERROR: invalid value for parameter "hnsw.iterative_search": "on" HINT: Available values: off, relaxed_order, strict_order. -SHOW hnsw.iterative_search_max_tuples; - hnsw.iterative_search_max_tuples ----------------------------------- +SHOW hnsw.max_search_tuples; + hnsw.max_search_tuples +------------------------ -1 (1 row) -SET hnsw.iterative_search_max_tuples = -2; -ERROR: -2 is outside the valid range for parameter "hnsw.iterative_search_max_tuples" (-1 .. 2147483647) +SET hnsw.max_search_tuples = -2; +ERROR: -2 is outside the valid range for parameter "hnsw.max_search_tuples" (-1 .. 2147483647) DROP TABLE t; diff --git a/test/expected/ivfflat_vector.out b/test/expected/ivfflat_vector.out index c22135a..9bb7b13 100644 --- a/test/expected/ivfflat_vector.out +++ b/test/expected/ivfflat_vector.out @@ -137,14 +137,14 @@ SHOW ivfflat.iterative_search; SET ivfflat.iterative_search = on; ERROR: invalid value for parameter "ivfflat.iterative_search": "on" HINT: Available values: off, relaxed_order. -SHOW ivfflat.iterative_search_max_probes; - ivfflat.iterative_search_max_probes -------------------------------------- +SHOW ivfflat.max_probes; + ivfflat.max_probes +-------------------- 0 (1 row) -SET ivfflat.iterative_search_max_probes = -1; -ERROR: -1 is outside the valid range for parameter "ivfflat.iterative_search_max_probes" (0 .. 32768) -SET ivfflat.iterative_search_max_probes = 32769; -ERROR: 32769 is outside the valid range for parameter "ivfflat.iterative_search_max_probes" (0 .. 32768) +SET ivfflat.max_probes = -1; +ERROR: -1 is outside the valid range for parameter "ivfflat.max_probes" (0 .. 32768) +SET ivfflat.max_probes = 32769; +ERROR: 32769 is outside the valid range for parameter "ivfflat.max_probes" (0 .. 32768) DROP TABLE t; diff --git a/test/sql/hnsw_vector.sql b/test/sql/hnsw_vector.sql index 8f69a4c..ba8d4aa 100644 --- a/test/sql/hnsw_vector.sql +++ b/test/sql/hnsw_vector.sql @@ -102,8 +102,8 @@ SHOW hnsw.iterative_search; SET hnsw.iterative_search = on; -SHOW hnsw.iterative_search_max_tuples; +SHOW hnsw.max_search_tuples; -SET hnsw.iterative_search_max_tuples = -2; +SET hnsw.max_search_tuples = -2; DROP TABLE t; diff --git a/test/sql/ivfflat_vector.sql b/test/sql/ivfflat_vector.sql index 93f4224..7e83915 100644 --- a/test/sql/ivfflat_vector.sql +++ b/test/sql/ivfflat_vector.sql @@ -81,9 +81,9 @@ SHOW ivfflat.iterative_search; SET ivfflat.iterative_search = on; -SHOW ivfflat.iterative_search_max_probes; +SHOW ivfflat.max_probes; -SET ivfflat.iterative_search_max_probes = -1; -SET ivfflat.iterative_search_max_probes = 32769; +SET ivfflat.max_probes = -1; +SET ivfflat.max_probes = 32769; DROP TABLE t; diff --git a/test/t/041_ivfflat_iterative_search.pl b/test/t/041_ivfflat_iterative_search.pl index 2b1add3..6e0f721 100644 --- a/test/t/041_ivfflat_iterative_search.pl +++ b/test/t/041_ivfflat_iterative_search.pl @@ -40,7 +40,7 @@ foreach ((30, 50, 70)) SET enable_seqscan = off; SET ivfflat.probes = 10; SET ivfflat.iterative_search = relaxed_order; - SET ivfflat.iterative_search_max_probes = $max_probes; + SET ivfflat.max_probes = $max_probes; SELECT COUNT(*) FROM (SELECT v FROM tst WHERE i % 10000 = 0 ORDER BY v <-> (SELECT v FROM tst WHERE i = $i) LIMIT 11) t; )); $sum += $count; diff --git a/test/t/043_hnsw_iterative_search.pl b/test/t/043_hnsw_iterative_search.pl index 6ccef21..8e1aa1e 100644 --- a/test/t/043_hnsw_iterative_search.pl +++ b/test/t/043_hnsw_iterative_search.pl @@ -43,7 +43,7 @@ foreach ((30000, 50000, 70000)) $count = $node->safe_psql("postgres", qq( SET enable_seqscan = off; SET hnsw.iterative_search = relaxed_order; - SET hnsw.iterative_search_max_tuples = $max_tuples; + SET hnsw.max_search_tuples = $max_tuples; SET work_mem = '8MB'; SELECT COUNT(*) FROM (SELECT v FROM tst WHERE i % 10000 = 0 ORDER BY v <-> (SELECT v FROM tst WHERE i = $i) LIMIT 11) t; )); From 9d15a76b601fc75a3f8223b9deed96bbd0539c8a Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 11 Oct 2024 11:20:36 -0700 Subject: [PATCH 74/88] Improved enum naming [skip ci] --- src/hnsw.h | 4 ++-- src/ivfflat.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/hnsw.h b/src/hnsw.h index 5f170dd..6b184ec 100644 --- a/src/hnsw.h +++ b/src/hnsw.h @@ -113,12 +113,12 @@ extern int hnsw_iterative_search; extern int hnsw_max_search_tuples; extern int hnsw_lock_tranche_id; -typedef enum HnswIterativeSearchType +typedef enum HnswIterativeSearchMode { HNSW_ITERATIVE_SEARCH_OFF, HNSW_ITERATIVE_SEARCH_RELAXED, HNSW_ITERATIVE_SEARCH_STRICT -} HnswIterativeSearchType; +} HnswIterativeSearchMode; typedef struct HnswElementData HnswElementData; typedef struct HnswNeighborArray HnswNeighborArray; diff --git a/src/ivfflat.h b/src/ivfflat.h index 1ca37ad..71acb43 100644 --- a/src/ivfflat.h +++ b/src/ivfflat.h @@ -83,11 +83,11 @@ extern int ivfflat_probes; extern int ivfflat_iterative_search; extern int ivfflat_max_probes; -typedef enum IvfflatIterativeSearchType +typedef enum IvfflatIterativeSearchMode { IVFFLAT_ITERATIVE_SEARCH_OFF, IVFFLAT_ITERATIVE_SEARCH_RELAXED -} IvfflatIterativeSearchType; +} IvfflatIterativeSearchMode; typedef struct VectorArrayData { From 42af8aa1d1234b09f902470332c02f1e7d0e2d83 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 11 Oct 2024 11:26:27 -0700 Subject: [PATCH 75/88] Updated GUC descriptions [skip ci] --- src/hnsw.c | 2 +- src/ivfflat.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hnsw.c b/src/hnsw.c index acfd14f..fdd35b3 100644 --- a/src/hnsw.c +++ b/src/hnsw.c @@ -78,7 +78,7 @@ HnswInit(void) "Valid range is 1..1000.", &hnsw_ef_search, HNSW_DEFAULT_EF_SEARCH, HNSW_MIN_EF_SEARCH, HNSW_MAX_EF_SEARCH, PGC_USERSET, 0, NULL, NULL, NULL); - DefineCustomEnumVariable("hnsw.iterative_search", "Sets iterative search mode", + DefineCustomEnumVariable("hnsw.iterative_search", "Sets the iterative search mode", NULL, &hnsw_iterative_search, HNSW_ITERATIVE_SEARCH_OFF, hnsw_iterative_search_options, PGC_USERSET, 0, NULL, NULL, NULL); diff --git a/src/ivfflat.c b/src/ivfflat.c index 2ad27b4..4c02a5a 100644 --- a/src/ivfflat.c +++ b/src/ivfflat.c @@ -41,7 +41,7 @@ IvfflatInit(void) "Valid range is 1..lists.", &ivfflat_probes, IVFFLAT_DEFAULT_PROBES, IVFFLAT_MIN_LISTS, IVFFLAT_MAX_LISTS, PGC_USERSET, 0, NULL, NULL, NULL); - DefineCustomEnumVariable("ivfflat.iterative_search", "Sets whether to use iterative search", + DefineCustomEnumVariable("ivfflat.iterative_search", "Sets the iterative search mode", NULL, &ivfflat_iterative_search, IVFFLAT_ITERATIVE_SEARCH_OFF, ivfflat_iterative_search_options, PGC_USERSET, 0, NULL, NULL, NULL); From d1ebb8db73c8d33f18cd77c11871fd0c7b6874b2 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 11 Oct 2024 11:43:32 -0700 Subject: [PATCH 76/88] Use -1 for no limit for ivfflat.max_probes [skip ci] --- src/hnsw.c | 1 - src/ivfflat.c | 4 ++-- src/ivfscan.c | 20 ++++++++++++++------ test/expected/ivfflat_vector.out | 31 +++++++++++++++++++++++++++---- test/sql/ivfflat_vector.sql | 12 +++++++++++- 5 files changed, 54 insertions(+), 14 deletions(-) diff --git a/src/hnsw.c b/src/hnsw.c index fdd35b3..d1bc233 100644 --- a/src/hnsw.c +++ b/src/hnsw.c @@ -82,7 +82,6 @@ HnswInit(void) NULL, &hnsw_iterative_search, HNSW_ITERATIVE_SEARCH_OFF, hnsw_iterative_search_options, PGC_USERSET, 0, NULL, NULL, NULL); - /* TODO Ensure ivfflat.max_probes uses same value for no limit */ DefineCustomIntVariable("hnsw.max_search_tuples", "Sets the max number of candidates to visit for iterative search", "-1 means no limit", &hnsw_max_search_tuples, -1, -1, INT_MAX, PGC_USERSET, 0, NULL, NULL, NULL); diff --git a/src/ivfflat.c b/src/ivfflat.c index 4c02a5a..531bc3a 100644 --- a/src/ivfflat.c +++ b/src/ivfflat.c @@ -46,8 +46,8 @@ IvfflatInit(void) IVFFLAT_ITERATIVE_SEARCH_OFF, ivfflat_iterative_search_options, PGC_USERSET, 0, NULL, NULL, NULL); DefineCustomIntVariable("ivfflat.max_probes", "Sets the max number of probes for iterative search", - "Zero sets to the number of lists", &ivfflat_max_probes, - 0, 0, IVFFLAT_MAX_LISTS, PGC_USERSET, 0, NULL, NULL, NULL); + "-1 means no limit", &ivfflat_max_probes, + -1, -1, IVFFLAT_MAX_LISTS, PGC_USERSET, 0, NULL, NULL, NULL); MarkGUCPrefixReserved("ivfflat"); } diff --git a/src/ivfscan.c b/src/ivfscan.c index 548af81..bc795e3 100644 --- a/src/ivfscan.c +++ b/src/ivfscan.c @@ -259,19 +259,27 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys) /* Get lists and dimensions from metapage */ IvfflatGetMetaPageInfo(index, &lists, &dimensions); - if (probes > lists) - probes = lists; - if (ivfflat_iterative_search != IVFFLAT_ITERATIVE_SEARCH_OFF) { - if (ivfflat_max_probes == 0) + maxProbes = ivfflat_max_probes; + + if (maxProbes < 0) maxProbes = lists; - else - maxProbes = Min(ivfflat_max_probes, lists); + else if (maxProbes < probes) + { + /* TODO Show notice */ + maxProbes = probes; + } } else maxProbes = probes; + if (probes > lists) + probes = lists; + + if (maxProbes > lists) + maxProbes = lists; + so = (IvfflatScanOpaque) palloc(offsetof(IvfflatScanOpaqueData, lists) + maxProbes * sizeof(IvfflatScanList)); so->typeInfo = IvfflatGetTypeInfo(index); so->first = true; diff --git a/test/expected/ivfflat_vector.out b/test/expected/ivfflat_vector.out index 9bb7b13..8a80ea3 100644 --- a/test/expected/ivfflat_vector.out +++ b/test/expected/ivfflat_vector.out @@ -95,7 +95,30 @@ SELECT * FROM t ORDER BY val <-> '[3,3,3]'; [0,0,0] (3 rows) +SET ivfflat.max_probes = 0; +SELECT * FROM t ORDER BY val <-> '[3,3,3]'; + val +--------- + [1,2,3] +(1 row) + +SET ivfflat.max_probes = 1; +SELECT * FROM t ORDER BY val <-> '[3,3,3]'; + val +--------- + [1,2,3] +(1 row) + +SET ivfflat.max_probes = 2; +SELECT * FROM t ORDER BY val <-> '[3,3,3]'; + val +--------- + [1,2,3] + [1,1,1] +(2 rows) + RESET ivfflat.iterative_search; +RESET ivfflat.max_probes; DROP TABLE t; -- unlogged CREATE UNLOGGED TABLE t (val vector(3)); @@ -140,11 +163,11 @@ HINT: Available values: off, relaxed_order. SHOW ivfflat.max_probes; ivfflat.max_probes -------------------- - 0 + -1 (1 row) -SET ivfflat.max_probes = -1; -ERROR: -1 is outside the valid range for parameter "ivfflat.max_probes" (0 .. 32768) +SET ivfflat.max_probes = -2; +ERROR: -2 is outside the valid range for parameter "ivfflat.max_probes" (-1 .. 32768) SET ivfflat.max_probes = 32769; -ERROR: 32769 is outside the valid range for parameter "ivfflat.max_probes" (0 .. 32768) +ERROR: 32769 is outside the valid range for parameter "ivfflat.max_probes" (-1 .. 32768) DROP TABLE t; diff --git a/test/sql/ivfflat_vector.sql b/test/sql/ivfflat_vector.sql index 7e83915..9e20060 100644 --- a/test/sql/ivfflat_vector.sql +++ b/test/sql/ivfflat_vector.sql @@ -53,7 +53,17 @@ CREATE INDEX ON t USING ivfflat (val vector_l2_ops) WITH (lists = 3); SET ivfflat.iterative_search = relaxed_order; SELECT * FROM t ORDER BY val <-> '[3,3,3]'; +SET ivfflat.max_probes = 0; +SELECT * FROM t ORDER BY val <-> '[3,3,3]'; + +SET ivfflat.max_probes = 1; +SELECT * FROM t ORDER BY val <-> '[3,3,3]'; + +SET ivfflat.max_probes = 2; +SELECT * FROM t ORDER BY val <-> '[3,3,3]'; + RESET ivfflat.iterative_search; +RESET ivfflat.max_probes; DROP TABLE t; -- unlogged @@ -83,7 +93,7 @@ SET ivfflat.iterative_search = on; SHOW ivfflat.max_probes; -SET ivfflat.max_probes = -1; +SET ivfflat.max_probes = -2; SET ivfflat.max_probes = 32769; DROP TABLE t; From 7484625227adf44b4bc4beb885661b9f0f9fd6b9 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 11 Oct 2024 11:59:36 -0700 Subject: [PATCH 77/88] Added comments [skip ci] --- src/hnsw.c | 1 + src/ivfflat.c | 1 + 2 files changed, 2 insertions(+) diff --git a/src/hnsw.c b/src/hnsw.c index d1bc233..5c747b5 100644 --- a/src/hnsw.c +++ b/src/hnsw.c @@ -82,6 +82,7 @@ HnswInit(void) NULL, &hnsw_iterative_search, HNSW_ITERATIVE_SEARCH_OFF, hnsw_iterative_search_options, PGC_USERSET, 0, NULL, NULL, NULL); + /* This is approximate and does not apply to the initial scan */ DefineCustomIntVariable("hnsw.max_search_tuples", "Sets the max number of candidates to visit for iterative search", "-1 means no limit", &hnsw_max_search_tuples, -1, -1, INT_MAX, PGC_USERSET, 0, NULL, NULL, NULL); diff --git a/src/ivfflat.c b/src/ivfflat.c index 531bc3a..be29dd5 100644 --- a/src/ivfflat.c +++ b/src/ivfflat.c @@ -45,6 +45,7 @@ IvfflatInit(void) NULL, &ivfflat_iterative_search, IVFFLAT_ITERATIVE_SEARCH_OFF, ivfflat_iterative_search_options, PGC_USERSET, 0, NULL, NULL, NULL); + /* If this is less than probes, probes is used */ DefineCustomIntVariable("ivfflat.max_probes", "Sets the max number of probes for iterative search", "-1 means no limit", &ivfflat_max_probes, -1, -1, IVFFLAT_MAX_LISTS, PGC_USERSET, 0, NULL, NULL, NULL); From 53a8734bacc96dd33eb2193c6560f07d93246ef3 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 11 Oct 2024 14:12:01 -0700 Subject: [PATCH 78/88] Added IndexTuple to HNSW elements (first step to support multiple attributes) --- src/hnsw.h | 8 ++++++-- src/hnswbuild.c | 29 +++++++++++++++++------------ src/hnswinsert.c | 15 +++++++++------ src/hnswutils.c | 22 +++++++++++++++++++--- 4 files changed, 51 insertions(+), 23 deletions(-) diff --git a/src/hnsw.h b/src/hnsw.h index 6b184ec..fd34de1 100644 --- a/src/hnsw.h +++ b/src/hnsw.h @@ -133,6 +133,7 @@ HnswPtrDeclare(HnswElementData, HnswElementRelptr, HnswElementPtr); HnswPtrDeclare(HnswNeighborArray, HnswNeighborArrayRelptr, HnswNeighborArrayPtr); HnswPtrDeclare(HnswNeighborArrayPtr, HnswNeighborsRelptr, HnswNeighborsPtr); HnswPtrDeclare(char, DatumRelptr, DatumPtr); +HnswPtrDeclare(IndexTupleData, IndexTupleRelptr, IndexTuplePtr); struct HnswElementData { @@ -149,6 +150,7 @@ struct HnswElementData OffsetNumber neighborOffno; BlockNumber neighborPage; DatumPtr value; + IndexTuplePtr itup; LWLock lock; }; @@ -288,6 +290,7 @@ typedef struct HnswBuildState HnswGraph *graph; double ml; int maxLevel; + TupleDesc tupdesc; /* Memory */ MemoryContext graphCtx; @@ -428,11 +431,12 @@ void HnswSetNeighborTuple(char *base, HnswNeighborTuple ntup, HnswElement e, in void HnswAddHeapTid(HnswElement element, ItemPointer heaptid); HnswNeighborArray *HnswInitNeighborArray(int lm, HnswAllocator * allocator); void HnswInitNeighbors(char *base, HnswElement element, int m, HnswAllocator * alloc); -bool HnswInsertTupleOnDisk(Relation index, HnswSupport * support, Datum value, ItemPointer heaptid, bool building); +bool HnswInsertTupleOnDisk(Relation index, HnswSupport * support, TupleDesc tupdesc, IndexTuple itup, ItemPointer heaptid, bool building); void HnswUpdateNeighborsOnDisk(Relation index, HnswSupport * support, HnswElement e, int m, bool checkExisting, bool building); void HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool loadHeaptids, bool loadVec); void HnswLoadElement(HnswElement element, double *distance, HnswQuery * q, Relation index, HnswSupport * support, bool loadVec, double *maxDistance); -bool HnswFormIndexValue(Datum *out, Datum *values, bool *isnull, const HnswTypeInfo * typeInfo, HnswSupport * support); +TupleDesc HnswTupleDesc(Relation index); +bool HnswFormIndexTuple(IndexTuple *out, Datum *values, bool *isnull, const HnswTypeInfo * typeInfo, HnswSupport * support, TupleDesc tupdesc); void HnswSetElementTuple(char *base, HnswElementTuple etup, HnswElement element); void HnswUpdateConnection(char *base, HnswNeighborArray * neighbors, HnswElement newElement, float distance, int lm, int *updateIdx, Relation index, HnswSupport * support); bool HnswLoadNeighborTids(HnswElement element, ItemPointerData *indextids, Relation index, int m, int lm, int lc); diff --git a/src/hnswbuild.c b/src/hnswbuild.c index b667478..85fc060 100644 --- a/src/hnswbuild.c +++ b/src/hnswbuild.c @@ -476,18 +476,20 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid, Hn HnswElement element; HnswAllocator *allocator = &buildstate->allocator; HnswSupport *support = &buildstate->support; - Size valueSize; - Pointer valuePtr; LWLock *flushLock = &graph->flushLock; char *base = buildstate->hnswarea; - Datum value; + TupleDesc tupdesc = buildstate->tupdesc; + IndexTuple itup; + Size itupSize; + IndexTuple itupShared; + bool unused; /* Form index value */ - if (!HnswFormIndexValue(&value, values, isnull, buildstate->typeInfo, support)) + if (!HnswFormIndexTuple(&itup, values, isnull, buildstate->typeInfo, support, tupdesc)) return false; - /* Get datum size */ - valueSize = VARSIZE_ANY(DatumGetPointer(value)); + /* Get tuple size */ + itupSize = IndexTupleSize(itup); /* Ensure graph not flushed when inserting */ LWLockAcquire(flushLock, LW_SHARED); @@ -497,7 +499,7 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid, Hn { LWLockRelease(flushLock); - return HnswInsertTupleOnDisk(index, support, value, heaptid, true); + return HnswInsertTupleOnDisk(index, support, tupdesc, itup, heaptid, true); } /* @@ -529,12 +531,12 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid, Hn LWLockRelease(flushLock); - return HnswInsertTupleOnDisk(index, support, value, heaptid, true); + return HnswInsertTupleOnDisk(index, support, tupdesc, itup, heaptid, true); } /* Ok, we can proceed to allocate the element */ element = HnswInitElement(base, heaptid, buildstate->m, buildstate->ml, buildstate->maxLevel, allocator); - valuePtr = HnswAlloc(allocator, valueSize); + itupShared = HnswAlloc(allocator, itupSize); /* * We have now allocated the space needed for the element, so we don't @@ -543,9 +545,10 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid, Hn */ LWLockRelease(&graph->allocatorLock); - /* Copy the datum */ - memcpy(valuePtr, DatumGetPointer(value), valueSize); - HnswPtrStore(base, element->value, valuePtr); + /* Copy the tuple */ + memcpy(itupShared, itup, itupSize); + HnswPtrStore(base, element->itup, itupShared); + HnswPtrStore(base, element->value, DatumGetPointer(index_getattr(itupShared, 1, tupdesc, &unused))); /* Create a lock for the element */ LWLockInitialize(&element->lock, hnsw_lock_tranche_id); @@ -698,6 +701,7 @@ InitBuildState(HnswBuildState * buildstate, Relation heap, Relation index, Index buildstate->graph = &buildstate->graphData; buildstate->ml = HnswGetMl(buildstate->m); buildstate->maxLevel = HnswGetMaxLevel(buildstate->m); + buildstate->tupdesc = HnswTupleDesc(index); buildstate->graphCtx = GenerationContextCreate(CurrentMemoryContext, "Hnsw build graph context", @@ -722,6 +726,7 @@ InitBuildState(HnswBuildState * buildstate, Relation heap, Relation index, Index static void FreeBuildState(HnswBuildState * buildstate) { + pfree(buildstate->tupdesc); MemoryContextDelete(buildstate->graphCtx); MemoryContextDelete(buildstate->tmpCtx); } diff --git a/src/hnswinsert.c b/src/hnswinsert.c index a5fac4e..a1447e4 100644 --- a/src/hnswinsert.c +++ b/src/hnswinsert.c @@ -687,7 +687,7 @@ UpdateGraphOnDisk(Relation index, HnswSupport * support, HnswElement element, in * Insert a tuple into the index */ bool -HnswInsertTupleOnDisk(Relation index, HnswSupport * support, Datum value, ItemPointer heaptid, bool building) +HnswInsertTupleOnDisk(Relation index, HnswSupport * support, TupleDesc tupdesc, IndexTuple itup, ItemPointer heaptid, bool building) { HnswElement entryPoint; HnswElement element; @@ -695,6 +695,7 @@ HnswInsertTupleOnDisk(Relation index, HnswSupport * support, Datum value, ItemPo int efConstruction = HnswGetEfConstruction(index); LOCKMODE lockmode = ShareLock; char *base = NULL; + bool unused; /* * Get a shared lock. This allows vacuum to ensure no in-flight inserts @@ -708,7 +709,8 @@ HnswInsertTupleOnDisk(Relation index, HnswSupport * support, Datum value, ItemPo /* Create an element */ element = HnswInitElement(base, heaptid, m, HnswGetMl(m), HnswGetMaxLevel(m), NULL); - HnswPtrStore(base, element->value, DatumGetPointer(value)); + HnswPtrStore(base, element->itup, itup); + HnswPtrStore(base, element->value, DatumGetPointer(index_getattr(itup, 1, tupdesc, &unused))); /* Prevent concurrent inserts when likely updating entry point */ if (entryPoint == NULL || element->level > entryPoint->level) @@ -742,17 +744,18 @@ HnswInsertTupleOnDisk(Relation index, HnswSupport * support, Datum value, ItemPo static void HnswInsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid) { - Datum value; + IndexTuple itup; const HnswTypeInfo *typeInfo = HnswGetTypeInfo(index); HnswSupport support; + TupleDesc tupdesc = HnswTupleDesc(index); HnswInitSupport(&support, index); - /* Form index value */ - if (!HnswFormIndexValue(&value, values, isnull, typeInfo, &support)) + /* Form index tuple */ + if (!HnswFormIndexTuple(&itup, values, isnull, typeInfo, &support, tupdesc)) return; - HnswInsertTupleOnDisk(index, &support, value, heaptid, false); + HnswInsertTupleOnDisk(index, &support, tupdesc, itup, heaptid, false); } /* diff --git a/src/hnswutils.c b/src/hnswutils.c index e84561e..ad3edfa 100644 --- a/src/hnswutils.c +++ b/src/hnswutils.c @@ -395,10 +395,24 @@ HnswUpdateMetaPage(Relation index, int updateEntry, HnswElement entryPoint, Bloc } /* - * Form index value + * Get the tuple descriptor + */ +TupleDesc +HnswTupleDesc(Relation index) +{ + TupleDesc tupdesc = CreateTupleDescCopyConstr(RelationGetDescr(index)); + + /* Prevent compression */ + TupleDescAttr(tupdesc, 0)->attstorage = TYPSTORAGE_PLAIN; + + return tupdesc; +} + +/* + * Form index tuple */ bool -HnswFormIndexValue(Datum *out, Datum *values, bool *isnull, const HnswTypeInfo * typeInfo, HnswSupport * support) +HnswFormIndexTuple(IndexTuple *out, Datum *values, bool *isnull, const HnswTypeInfo * typeInfo, HnswSupport * support, TupleDesc tupdesc) { /* Detoast once for all calls */ Datum value = PointerGetDatum(PG_DETOAST_DATUM(values[0])); @@ -416,7 +430,9 @@ HnswFormIndexValue(Datum *out, Datum *values, bool *isnull, const HnswTypeInfo * value = HnswNormValue(typeInfo, support->collation, value); } - *out = value; + /* TODO Combine value with values to support multiple attributes */ + Assert(tupdesc->natts == 1); + *out = index_form_tuple(tupdesc, &value, isnull); return true; } From a2408e60fa48b492efb5526cc18ba72841b61b37 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 11 Oct 2024 14:57:57 -0700 Subject: [PATCH 79/88] Revert "Added IndexTuple to HNSW elements (first step to support multiple attributes)" This reverts commit 53a8734bacc96dd33eb2193c6560f07d93246ef3. --- src/hnsw.h | 8 ++------ src/hnswbuild.c | 29 ++++++++++++----------------- src/hnswinsert.c | 15 ++++++--------- src/hnswutils.c | 22 +++------------------- 4 files changed, 23 insertions(+), 51 deletions(-) diff --git a/src/hnsw.h b/src/hnsw.h index fd34de1..6b184ec 100644 --- a/src/hnsw.h +++ b/src/hnsw.h @@ -133,7 +133,6 @@ HnswPtrDeclare(HnswElementData, HnswElementRelptr, HnswElementPtr); HnswPtrDeclare(HnswNeighborArray, HnswNeighborArrayRelptr, HnswNeighborArrayPtr); HnswPtrDeclare(HnswNeighborArrayPtr, HnswNeighborsRelptr, HnswNeighborsPtr); HnswPtrDeclare(char, DatumRelptr, DatumPtr); -HnswPtrDeclare(IndexTupleData, IndexTupleRelptr, IndexTuplePtr); struct HnswElementData { @@ -150,7 +149,6 @@ struct HnswElementData OffsetNumber neighborOffno; BlockNumber neighborPage; DatumPtr value; - IndexTuplePtr itup; LWLock lock; }; @@ -290,7 +288,6 @@ typedef struct HnswBuildState HnswGraph *graph; double ml; int maxLevel; - TupleDesc tupdesc; /* Memory */ MemoryContext graphCtx; @@ -431,12 +428,11 @@ void HnswSetNeighborTuple(char *base, HnswNeighborTuple ntup, HnswElement e, in void HnswAddHeapTid(HnswElement element, ItemPointer heaptid); HnswNeighborArray *HnswInitNeighborArray(int lm, HnswAllocator * allocator); void HnswInitNeighbors(char *base, HnswElement element, int m, HnswAllocator * alloc); -bool HnswInsertTupleOnDisk(Relation index, HnswSupport * support, TupleDesc tupdesc, IndexTuple itup, ItemPointer heaptid, bool building); +bool HnswInsertTupleOnDisk(Relation index, HnswSupport * support, Datum value, ItemPointer heaptid, bool building); void HnswUpdateNeighborsOnDisk(Relation index, HnswSupport * support, HnswElement e, int m, bool checkExisting, bool building); void HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool loadHeaptids, bool loadVec); void HnswLoadElement(HnswElement element, double *distance, HnswQuery * q, Relation index, HnswSupport * support, bool loadVec, double *maxDistance); -TupleDesc HnswTupleDesc(Relation index); -bool HnswFormIndexTuple(IndexTuple *out, Datum *values, bool *isnull, const HnswTypeInfo * typeInfo, HnswSupport * support, TupleDesc tupdesc); +bool HnswFormIndexValue(Datum *out, Datum *values, bool *isnull, const HnswTypeInfo * typeInfo, HnswSupport * support); void HnswSetElementTuple(char *base, HnswElementTuple etup, HnswElement element); void HnswUpdateConnection(char *base, HnswNeighborArray * neighbors, HnswElement newElement, float distance, int lm, int *updateIdx, Relation index, HnswSupport * support); bool HnswLoadNeighborTids(HnswElement element, ItemPointerData *indextids, Relation index, int m, int lm, int lc); diff --git a/src/hnswbuild.c b/src/hnswbuild.c index 85fc060..b667478 100644 --- a/src/hnswbuild.c +++ b/src/hnswbuild.c @@ -476,20 +476,18 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid, Hn HnswElement element; HnswAllocator *allocator = &buildstate->allocator; HnswSupport *support = &buildstate->support; + Size valueSize; + Pointer valuePtr; LWLock *flushLock = &graph->flushLock; char *base = buildstate->hnswarea; - TupleDesc tupdesc = buildstate->tupdesc; - IndexTuple itup; - Size itupSize; - IndexTuple itupShared; - bool unused; + Datum value; /* Form index value */ - if (!HnswFormIndexTuple(&itup, values, isnull, buildstate->typeInfo, support, tupdesc)) + if (!HnswFormIndexValue(&value, values, isnull, buildstate->typeInfo, support)) return false; - /* Get tuple size */ - itupSize = IndexTupleSize(itup); + /* Get datum size */ + valueSize = VARSIZE_ANY(DatumGetPointer(value)); /* Ensure graph not flushed when inserting */ LWLockAcquire(flushLock, LW_SHARED); @@ -499,7 +497,7 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid, Hn { LWLockRelease(flushLock); - return HnswInsertTupleOnDisk(index, support, tupdesc, itup, heaptid, true); + return HnswInsertTupleOnDisk(index, support, value, heaptid, true); } /* @@ -531,12 +529,12 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid, Hn LWLockRelease(flushLock); - return HnswInsertTupleOnDisk(index, support, tupdesc, itup, heaptid, true); + return HnswInsertTupleOnDisk(index, support, value, heaptid, true); } /* Ok, we can proceed to allocate the element */ element = HnswInitElement(base, heaptid, buildstate->m, buildstate->ml, buildstate->maxLevel, allocator); - itupShared = HnswAlloc(allocator, itupSize); + valuePtr = HnswAlloc(allocator, valueSize); /* * We have now allocated the space needed for the element, so we don't @@ -545,10 +543,9 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid, Hn */ LWLockRelease(&graph->allocatorLock); - /* Copy the tuple */ - memcpy(itupShared, itup, itupSize); - HnswPtrStore(base, element->itup, itupShared); - HnswPtrStore(base, element->value, DatumGetPointer(index_getattr(itupShared, 1, tupdesc, &unused))); + /* Copy the datum */ + memcpy(valuePtr, DatumGetPointer(value), valueSize); + HnswPtrStore(base, element->value, valuePtr); /* Create a lock for the element */ LWLockInitialize(&element->lock, hnsw_lock_tranche_id); @@ -701,7 +698,6 @@ InitBuildState(HnswBuildState * buildstate, Relation heap, Relation index, Index buildstate->graph = &buildstate->graphData; buildstate->ml = HnswGetMl(buildstate->m); buildstate->maxLevel = HnswGetMaxLevel(buildstate->m); - buildstate->tupdesc = HnswTupleDesc(index); buildstate->graphCtx = GenerationContextCreate(CurrentMemoryContext, "Hnsw build graph context", @@ -726,7 +722,6 @@ InitBuildState(HnswBuildState * buildstate, Relation heap, Relation index, Index static void FreeBuildState(HnswBuildState * buildstate) { - pfree(buildstate->tupdesc); MemoryContextDelete(buildstate->graphCtx); MemoryContextDelete(buildstate->tmpCtx); } diff --git a/src/hnswinsert.c b/src/hnswinsert.c index a1447e4..a5fac4e 100644 --- a/src/hnswinsert.c +++ b/src/hnswinsert.c @@ -687,7 +687,7 @@ UpdateGraphOnDisk(Relation index, HnswSupport * support, HnswElement element, in * Insert a tuple into the index */ bool -HnswInsertTupleOnDisk(Relation index, HnswSupport * support, TupleDesc tupdesc, IndexTuple itup, ItemPointer heaptid, bool building) +HnswInsertTupleOnDisk(Relation index, HnswSupport * support, Datum value, ItemPointer heaptid, bool building) { HnswElement entryPoint; HnswElement element; @@ -695,7 +695,6 @@ HnswInsertTupleOnDisk(Relation index, HnswSupport * support, TupleDesc tupdesc, int efConstruction = HnswGetEfConstruction(index); LOCKMODE lockmode = ShareLock; char *base = NULL; - bool unused; /* * Get a shared lock. This allows vacuum to ensure no in-flight inserts @@ -709,8 +708,7 @@ HnswInsertTupleOnDisk(Relation index, HnswSupport * support, TupleDesc tupdesc, /* Create an element */ element = HnswInitElement(base, heaptid, m, HnswGetMl(m), HnswGetMaxLevel(m), NULL); - HnswPtrStore(base, element->itup, itup); - HnswPtrStore(base, element->value, DatumGetPointer(index_getattr(itup, 1, tupdesc, &unused))); + HnswPtrStore(base, element->value, DatumGetPointer(value)); /* Prevent concurrent inserts when likely updating entry point */ if (entryPoint == NULL || element->level > entryPoint->level) @@ -744,18 +742,17 @@ HnswInsertTupleOnDisk(Relation index, HnswSupport * support, TupleDesc tupdesc, static void HnswInsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid) { - IndexTuple itup; + Datum value; const HnswTypeInfo *typeInfo = HnswGetTypeInfo(index); HnswSupport support; - TupleDesc tupdesc = HnswTupleDesc(index); HnswInitSupport(&support, index); - /* Form index tuple */ - if (!HnswFormIndexTuple(&itup, values, isnull, typeInfo, &support, tupdesc)) + /* Form index value */ + if (!HnswFormIndexValue(&value, values, isnull, typeInfo, &support)) return; - HnswInsertTupleOnDisk(index, &support, tupdesc, itup, heaptid, false); + HnswInsertTupleOnDisk(index, &support, value, heaptid, false); } /* diff --git a/src/hnswutils.c b/src/hnswutils.c index ad3edfa..e84561e 100644 --- a/src/hnswutils.c +++ b/src/hnswutils.c @@ -395,24 +395,10 @@ HnswUpdateMetaPage(Relation index, int updateEntry, HnswElement entryPoint, Bloc } /* - * Get the tuple descriptor - */ -TupleDesc -HnswTupleDesc(Relation index) -{ - TupleDesc tupdesc = CreateTupleDescCopyConstr(RelationGetDescr(index)); - - /* Prevent compression */ - TupleDescAttr(tupdesc, 0)->attstorage = TYPSTORAGE_PLAIN; - - return tupdesc; -} - -/* - * Form index tuple + * Form index value */ bool -HnswFormIndexTuple(IndexTuple *out, Datum *values, bool *isnull, const HnswTypeInfo * typeInfo, HnswSupport * support, TupleDesc tupdesc) +HnswFormIndexValue(Datum *out, Datum *values, bool *isnull, const HnswTypeInfo * typeInfo, HnswSupport * support) { /* Detoast once for all calls */ Datum value = PointerGetDatum(PG_DETOAST_DATUM(values[0])); @@ -430,9 +416,7 @@ HnswFormIndexTuple(IndexTuple *out, Datum *values, bool *isnull, const HnswTypeI value = HnswNormValue(typeInfo, support->collation, value); } - /* TODO Combine value with values to support multiple attributes */ - Assert(tupdesc->natts == 1); - *out = index_form_tuple(tupdesc, &value, isnull); + *out = value; return true; } From 38285aacc7c0b14ca5513b032d34355607ab2af5 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 11 Oct 2024 15:01:54 -0700 Subject: [PATCH 80/88] Revert "Updated IVFFlat to support multiple attributes (not enabled yet)" This reverts commit 772ab69de6ea640e8923706dcf8b52f4cabfe035. --- src/ivfbuild.c | 38 +++++++++++++------------------------- src/ivfinsert.c | 8 +------- 2 files changed, 14 insertions(+), 32 deletions(-) diff --git a/src/ivfbuild.c b/src/ivfbuild.c index e88a0bb..54a5be5 100644 --- a/src/ivfbuild.c +++ b/src/ivfbuild.c @@ -138,7 +138,7 @@ SampleRows(IvfflatBuildState * buildstate) * Add tuple to sort */ static void -AddTupleToSort(Relation index, ItemPointer tid, Datum *values, bool *isnull, IvfflatBuildState * buildstate) +AddTupleToSort(Relation index, ItemPointer tid, Datum *values, IvfflatBuildState * buildstate) { double distance; double minDistance = DBL_MAX; @@ -184,11 +184,6 @@ AddTupleToSort(Relation index, ItemPointer tid, Datum *values, bool *isnull, Ivf slot->tts_isnull[1] = false; slot->tts_values[2] = value; slot->tts_isnull[2] = false; - for (int i = 1; i < buildstate->tupdesc->natts; i++) - { - slot->tts_values[2 + i] = values[i]; - slot->tts_isnull[2 + i] = isnull[i]; - } ExecStoreVirtualTuple(slot); /* @@ -220,7 +215,7 @@ BuildCallback(Relation index, ItemPointer tid, Datum *values, oldCtx = MemoryContextSwitchTo(buildstate->tmpCtx); /* Add tuple to sort */ - AddTupleToSort(index, tid, values, isnull, buildstate); + AddTupleToSort(index, tid, values, buildstate); /* Reset memory context */ MemoryContextSwitchTo(oldCtx); @@ -231,20 +226,19 @@ BuildCallback(Relation index, ItemPointer tid, Datum *values, * Get index tuple from sort state */ static inline void -GetNextTuple(Tuplesortstate *sortstate, TupleDesc tupdesc, TupleTableSlot *slot, Datum *values, bool *isnull, IndexTuple *itup, int *list) +GetNextTuple(Tuplesortstate *sortstate, TupleDesc tupdesc, TupleTableSlot *slot, IndexTuple *itup, int *list) { if (tuplesort_gettupleslot(sortstate, true, false, slot, NULL)) { - bool unused; + Datum value; + bool isnull; - *list = DatumGetInt32(slot_getattr(slot, 1, &unused)); - - for (int i = 0; i < tupdesc->natts; i++) - values[i] = slot_getattr(slot, 3 + i, &isnull[i]); + *list = DatumGetInt32(slot_getattr(slot, 1, &isnull)); + value = slot_getattr(slot, 3, &isnull); /* Form the index tuple */ - *itup = index_form_tuple(tupdesc, values, isnull); - (*itup)->t_tid = *((ItemPointer) DatumGetPointer(slot_getattr(slot, 2, &unused))); + *itup = index_form_tuple(tupdesc, &value, &isnull); + (*itup)->t_tid = *((ItemPointer) DatumGetPointer(slot_getattr(slot, 2, &isnull))); } else *list = -1; @@ -262,14 +256,12 @@ InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum) TupleTableSlot *slot = MakeSingleTupleTableSlot(buildstate->sortdesc, &TTSOpsMinimalTuple); TupleDesc tupdesc = buildstate->tupdesc; - Datum *values = palloc(tupdesc->natts * sizeof(Datum)); - bool *isnull = palloc(tupdesc->natts * sizeof(bool)); pgstat_progress_update_param(PROGRESS_CREATEIDX_SUBPHASE, PROGRESS_IVFFLAT_PHASE_LOAD); pgstat_progress_update_param(PROGRESS_CREATEIDX_TUPLES_TOTAL, buildstate->indtuples); - GetNextTuple(buildstate->sortstate, tupdesc, slot, values, isnull, &itup, &list); + GetNextTuple(buildstate->sortstate, tupdesc, slot, &itup, &list); for (int i = 0; i < buildstate->centers->length; i++) { @@ -305,7 +297,7 @@ InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum) pgstat_progress_update_param(PROGRESS_CREATEIDX_TUPLES_DONE, ++inserted); - GetNextTuple(buildstate->sortstate, tupdesc, slot, values, isnull, &itup, &list); + GetNextTuple(buildstate->sortstate, tupdesc, slot, &itup, &list); } insertPage = BufferGetBlockNumber(buf); @@ -315,9 +307,6 @@ InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum) /* Set the start and insert pages */ IvfflatUpdateList(index, buildstate->listInfo[i], insertPage, InvalidBlockNumber, startPage, forkNum); } - - pfree(values); - pfree(isnull); } /* @@ -368,11 +357,10 @@ InitBuildState(IvfflatBuildState * buildstate, Relation heap, Relation index, In errmsg("dimensions must be greater than one for this opclass"))); /* Create tuple description for sorting */ - buildstate->sortdesc = CreateTemplateTupleDesc(2 + buildstate->tupdesc->natts); + buildstate->sortdesc = CreateTemplateTupleDesc(3); TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 1, "list", INT4OID, -1, 0); TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 2, "tid", TIDOID, -1, 0); - for (int i = 0; i < buildstate->tupdesc->natts; i++) - TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) (3 + i), NULL, buildstate->tupdesc->attrs[i].atttypid, -1, 0); + TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 3, "vector", buildstate->tupdesc->attrs[0].atttypid, -1, 0); buildstate->slot = MakeSingleTupleTableSlot(buildstate->sortdesc, &TTSOpsVirtual); diff --git a/src/ivfinsert.c b/src/ivfinsert.c index f683ea7..014c9be 100644 --- a/src/ivfinsert.c +++ b/src/ivfinsert.c @@ -78,8 +78,6 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heap_tid, R BlockNumber insertPage = InvalidBlockNumber; ListInfo listInfo; BlockNumber originalInsertPage; - TupleDesc tupdesc = RelationGetDescr(index); - Datum *newValues = palloc(tupdesc->natts * sizeof(Datum)); /* Detoast once for all calls */ value = PointerGetDatum(PG_DETOAST_DATUM(values[0])); @@ -104,12 +102,8 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heap_tid, R Assert(BlockNumberIsValid(insertPage)); originalInsertPage = insertPage; - newValues[0] = value; - for (int i = 1; i < tupdesc->natts; i++) - newValues[i] = values[i]; - /* Form tuple */ - itup = index_form_tuple(tupdesc, newValues, isnull); + itup = index_form_tuple(RelationGetDescr(index), &value, isnull); itup->t_tid = *heap_tid; /* Get tuple size */ From e1bc929429eead48c85d332a39e1a216415b1d0a Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 11 Oct 2024 15:29:23 -0700 Subject: [PATCH 81/88] Simplify lists for IvfflatScanOpaque [skip ci] --- src/ivfflat.h | 2 +- src/ivfscan.c | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ivfflat.h b/src/ivfflat.h index 71acb43..c9955ff 100644 --- a/src/ivfflat.h +++ b/src/ivfflat.h @@ -278,7 +278,7 @@ typedef struct IvfflatScanOpaqueData pairingheap *listQueue; BlockNumber *listPages; int listIndex; - IvfflatScanList lists[FLEXIBLE_ARRAY_MEMBER]; /* must come last */ + IvfflatScanList *lists; } IvfflatScanOpaqueData; typedef IvfflatScanOpaqueData * IvfflatScanOpaque; diff --git a/src/ivfscan.c b/src/ivfscan.c index bc795e3..d526ec9 100644 --- a/src/ivfscan.c +++ b/src/ivfscan.c @@ -280,7 +280,7 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys) if (maxProbes > lists) maxProbes = lists; - so = (IvfflatScanOpaque) palloc(offsetof(IvfflatScanOpaqueData, lists) + maxProbes * sizeof(IvfflatScanList)); + so = (IvfflatScanOpaque) palloc(sizeof(IvfflatScanOpaqueData)); so->typeInfo = IvfflatGetTypeInfo(index); so->first = true; so->probes = probes; @@ -314,6 +314,7 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys) so->listQueue = pairingheap_allocate(CompareLists, scan); so->listPages = palloc(maxProbes * sizeof(BlockNumber)); so->listIndex = 0; + so->lists = palloc(maxProbes * sizeof(IvfflatScanList)); scan->opaque = so; @@ -409,6 +410,7 @@ ivfflatendscan(IndexScanDesc scan) tuplesort_end(so->sortstate); FreeAccessStrategy(so->bas); FreeTupleDesc(so->tupdesc); + pfree(so->lists); /* TODO Free vslot and mslot without freeing TupleDesc */ From bf379eed862fec64ad206eb00ff7a9a9310dd6e4 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 11 Oct 2024 15:46:38 -0700 Subject: [PATCH 82/88] Use a memory context for IVFFlat index scans --- src/ivfflat.h | 1 + src/ivfscan.c | 28 +++++++++++++++++----------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/ivfflat.h b/src/ivfflat.h index c9955ff..91753c8 100644 --- a/src/ivfflat.h +++ b/src/ivfflat.h @@ -260,6 +260,7 @@ typedef struct IvfflatScanOpaqueData int dimensions; bool first; Datum value; + MemoryContext tmpCtx; /* Sorting */ Tuplesortstate *sortstate; diff --git a/src/ivfscan.c b/src/ivfscan.c index d526ec9..251f70f 100644 --- a/src/ivfscan.c +++ b/src/ivfscan.c @@ -10,10 +10,7 @@ #include "miscadmin.h" #include "pgstat.h" #include "storage/bufmgr.h" - -#ifdef IVFFLAT_MEMORY #include "utils/memutils.h" -#endif #define GetScanList(ptr) pairingheap_container(IvfflatScanList, ph_node, ptr) #define GetScanListConst(ptr) pairingheap_const_container(IvfflatScanList, ph_node, ptr) @@ -221,7 +218,13 @@ GetScanValue(IndexScanDesc scan) /* Normalize if needed */ if (so->normprocinfo != NULL) + { + MemoryContext oldCtx = MemoryContextSwitchTo(so->tmpCtx); + value = IvfflatNormValue(so->typeInfo, so->collation, value); + + MemoryContextSwitchTo(oldCtx); + } } return value; @@ -253,6 +256,7 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys) int dimensions; int probes = ivfflat_probes; int maxProbes; + MemoryContext oldCtx; scan = RelationGetIndexScan(index, nkeys, norderbys); @@ -292,6 +296,12 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys) so->normprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_NORM_PROC); so->collation = index->rd_indcollation[0]; + so->tmpCtx = AllocSetContextCreate(CurrentMemoryContext, + "Ivfflat scan temporary context", + ALLOCSET_DEFAULT_SIZES); + + oldCtx = MemoryContextSwitchTo(so->tmpCtx); + /* Create tuple description for sorting */ so->tupdesc = CreateTemplateTupleDesc(2); TupleDescInitEntry(so->tupdesc, (AttrNumber) 1, "distance", FLOAT8OID, -1, 0); @@ -316,6 +326,8 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys) so->listIndex = 0; so->lists = palloc(maxProbes * sizeof(IvfflatScanList)); + MemoryContextSwitchTo(oldCtx); + scan->opaque = so; return scan; @@ -377,8 +389,6 @@ ivfflatgettuple(IndexScanDesc scan, ScanDirection dir) IvfflatBench("GetScanItems", GetScanItems(scan, value)); so->first = false; so->value = value; - - /* TODO clean up if we allocated a new value */ } while (!tuplesort_gettupleslot(so->sortstate, true, false, so->mslot, NULL)) @@ -405,14 +415,10 @@ ivfflatendscan(IndexScanDesc scan) { IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque; - pairingheap_free(so->listQueue); - pfree(so->listPages); + /* Free any temporary files */ tuplesort_end(so->sortstate); - FreeAccessStrategy(so->bas); - FreeTupleDesc(so->tupdesc); - pfree(so->lists); - /* TODO Free vslot and mslot without freeing TupleDesc */ + MemoryContextDelete(so->tmpCtx); pfree(so); scan->opaque = NULL; From 388e42f6e689ee5974c001fa6670888a3a35611f Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 11 Oct 2024 15:48:19 -0700 Subject: [PATCH 83/88] Fixed flaky test [skip ci] --- test/t/042_ivfflat_iterative_search_recall.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/t/042_ivfflat_iterative_search_recall.pl b/test/t/042_ivfflat_iterative_search_recall.pl index 5cdcd71..aec6d49 100644 --- a/test/t/042_ivfflat_iterative_search_recall.pl +++ b/test/t/042_ivfflat_iterative_search_recall.pl @@ -103,7 +103,7 @@ for my $i (0 .. $#operators) if ($c == 100) { - test_recall($c, 1, 0.58, $operator); + test_recall($c, 1, 0.57, $operator); test_recall($c, 10, 0.98, $operator); } else From 02b01e1ca900386d7cb9deb59fc70e640c59d8c5 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 12 Oct 2024 22:05:13 -0700 Subject: [PATCH 84/88] Show tuples with memory usage [skip ci] --- src/hnswscan.c | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/hnswscan.c b/src/hnswscan.c index 84e2332..ba3dea8 100644 --- a/src/hnswscan.c +++ b/src/hnswscan.c @@ -102,6 +102,17 @@ GetScanValue(IndexScanDesc scan) return value; } +#if defined(HNSW_MEMORY) +/* + * Show memory usage + */ +static void +ShowMemoryUsage(HnswScanOpaque so) +{ + elog(INFO, "memory: %zu KB, tuples: " INT64_FORMAT, MemoryContextMemAllocated(so->tmpCtx, false) / 1024, so->tuples); +} +#endif + /* * Prepare for an index scan */ @@ -209,7 +220,7 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir) so->first = false; #if defined(HNSW_MEMORY) - elog(INFO, "memory: %zu KB", MemoryContextMemAllocated(so->tmpCtx, false) / 1024); + ShowMemoryUsage(so); #endif } @@ -271,7 +282,7 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir) UnlockPage(scan->indexRelation, HNSW_SCAN_LOCK, ShareLock); #if defined(HNSW_MEMORY) - elog(INFO, "memory: %zu KB", MemoryContextMemAllocated(so->tmpCtx, false) / 1024); + ShowMemoryUsage(so); #endif } From 7f735ebd9bf7b1ceedc9c93d4c4017713a3bc57b Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 13 Oct 2024 17:04:03 -0700 Subject: [PATCH 85/88] Added test for strict order [skip ci] --- test/t/044_hnsw_iterative_search_recall.pl | 23 +++++----------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/test/t/044_hnsw_iterative_search_recall.pl b/test/t/044_hnsw_iterative_search_recall.pl index cec42f8..cacffd3 100644 --- a/test/t/044_hnsw_iterative_search_recall.pl +++ b/test/t/044_hnsw_iterative_search_recall.pl @@ -14,14 +14,14 @@ my @cs = (100, 1000); sub test_recall { - my ($c, $ef_search, $min, $operator) = @_; + my ($c, $ef_search, $min, $operator, $mode) = @_; my $correct = 0; my $total = 0; my $explain = $node->safe_psql("postgres", qq( SET enable_seqscan = off; SET hnsw.ef_search = $ef_search; - SET hnsw.iterative_search = relaxed_order; + SET hnsw.iterative_search = $mode; EXPLAIN ANALYZE SELECT i FROM tst WHERE i % $c = 0 ORDER BY v $operator '$queries[0]' LIMIT $limit; )); like($explain, qr/Index Scan using idx on tst/); @@ -31,7 +31,7 @@ sub test_recall my $actual = $node->safe_psql("postgres", qq( SET enable_seqscan = off; SET hnsw.ef_search = $ef_search; - SET hnsw.iterative_search = relaxed_order; + SET hnsw.iterative_search = $mode; SELECT i FROM tst WHERE i % $c = 0 ORDER BY v $operator '$queries[$i]' LIMIT $limit; )); my @actual_ids = split("\n", $actual); @@ -108,21 +108,8 @@ for my $i (0 .. $#operators) push(@expected, $res); } - if ($c == 100) - { - test_recall($c, 40, 0.99, $operator); - } - else - { - if ($operator eq "<->") - { - test_recall($c, 40, 0.99, $operator); - } - else - { - test_recall($c, 40, 0.99, $operator); - } - } + test_recall($c, 40, 0.99, $operator, "strict_order"); + test_recall($c, 40, 0.99, $operator, "relaxed_order"); } $node->safe_psql("postgres", "DROP INDEX idx;"); From a41b327b336e6737a3f4c48af0374981a5fdf285 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 13 Oct 2024 17:12:12 -0700 Subject: [PATCH 86/88] Speed up test [skip ci] --- test/t/044_hnsw_iterative_search_recall.pl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/t/044_hnsw_iterative_search_recall.pl b/test/t/044_hnsw_iterative_search_recall.pl index cacffd3..4722bfa 100644 --- a/test/t/044_hnsw_iterative_search_recall.pl +++ b/test/t/044_hnsw_iterative_search_recall.pl @@ -10,7 +10,7 @@ my @expected; my $limit = 20; my $dim = 3; my $array_sql = join(",", ('random()') x $dim); -my @cs = (100, 1000); +my @cs = (50, 500); sub test_recall { @@ -62,7 +62,7 @@ $node->start; $node->safe_psql("postgres", "CREATE EXTENSION vector;"); $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, 100000) i;" + "INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(1, 50000) i;" ); # Generate queries From 61027645e9a486caa1f34504d7682b2acfe4364f Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 13 Oct 2024 17:21:38 -0700 Subject: [PATCH 87/88] Improved test output [skip ci] --- test/t/044_hnsw_iterative_search_recall.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/t/044_hnsw_iterative_search_recall.pl b/test/t/044_hnsw_iterative_search_recall.pl index 4722bfa..911fccb 100644 --- a/test/t/044_hnsw_iterative_search_recall.pl +++ b/test/t/044_hnsw_iterative_search_recall.pl @@ -50,7 +50,7 @@ sub test_recall $total += $limit; } - cmp_ok($correct / $total, ">=", $min, $operator); + cmp_ok($correct / $total, ">=", $min, "$operator $mode $c"); } # Initialize node From 049972a4a3a04e0f49de73d78915706377035f48 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 13 Oct 2024 17:22:49 -0700 Subject: [PATCH 88/88] Improved test output [skip ci] --- test/t/042_ivfflat_iterative_search_recall.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/t/042_ivfflat_iterative_search_recall.pl b/test/t/042_ivfflat_iterative_search_recall.pl index aec6d49..b6844b5 100644 --- a/test/t/042_ivfflat_iterative_search_recall.pl +++ b/test/t/042_ivfflat_iterative_search_recall.pl @@ -48,7 +48,7 @@ sub test_recall $total += $limit; } - cmp_ok($correct / $total, ">=", $min, $operator); + cmp_ok($correct / $total, ">=", $min, "$operator $c"); } # Initialize node