Compare commits

..

125 Commits

Author SHA1 Message Date
Andrew Kane
3d866844d3 Added PrintGraph function for HNSW [skip ci] 2023-08-26 13:08:27 -07:00
Andrew Kane
bb1e5ed98f Improved code [skip ci] 2023-08-25 21:05:18 -07:00
Andrew Kane
8801832946 Fixed check in SelectNeighbors 2023-08-25 20:46:05 -07:00
Andrew Kane
552c64d492 Improved HNSW code 2023-08-25 20:39:07 -07:00
Andrew Kane
86c29b3bf0 Improved param code 2023-08-23 21:10:00 -07:00
Andrew Kane
e406b7f5ea Added comments [skip ci] 2023-08-23 21:03:07 -07:00
Andrew Kane
6d88a9e1d2 Updated HNSW_PAGE_ID [skip ci] 2023-08-21 22:59:53 -07:00
Andrew Kane
1e851c12c0 Updated comment [skip ci] 2023-08-21 22:55:24 -07:00
Andrew Kane
8ed3cc5f0b Improved macro [skip ci] 2023-08-21 22:52:58 -07:00
Andrew Kane
bace0891bd Updated comments [skip ci] 2023-08-21 22:51:24 -07:00
Andrew Kane
4600979504 Updated comments [skip ci] 2023-08-21 22:38:09 -07:00
Andrew Kane
69addf05d0 Updated comments [skip ci] 2023-08-21 22:21:53 -07:00
Andrew Kane
4a5ae8a8df Updated comment [skip ci] 2023-08-21 21:59:47 -07:00
Andrew Kane
ce7da66ca2 Updated comment [skip ci] 2023-08-21 20:57:54 -07:00
Andrew Kane
c6be1c5e13 Fixed test 2023-08-21 20:45:56 -07:00
Andrew Kane
9b3d1a32ff Updated comment [skip ci] 2023-08-21 16:27:03 -07:00
Andrew Kane
8420734350 Updated comments [skip ci] 2023-08-21 16:26:32 -07:00
Andrew Kane
8b03267267 Improved locking for HNSW vacuum [skip ci] 2023-08-21 16:24:55 -07:00
Andrew Kane
641ddf5413 Improved locking for HNSW vacuum 2023-08-21 16:12:29 -07:00
Andrew Kane
782a1051e3 Improved locking for HNSW vacuum 2023-08-21 16:06:32 -07:00
Andrew Kane
ca2be5be6e Updated comment [skip ci] 2023-08-21 16:00:44 -07:00
Andrew Kane
0e1de45463 Improved locking code [skip ci] 2023-08-21 15:42:59 -07:00
Andrew Kane
3f3b3ca8e3 Made function static [skip ci] 2023-08-21 03:27:42 -07:00
Andrew Kane
d4fe67e8ee Simplified locking for entry point 2023-08-21 03:22:23 -07:00
Andrew Kane
02f4e0ec8b Revert "Added version to reduce stale reads and writes and prepare for optimistic locking"
This reverts commit ef1209eaf4.
2023-08-21 02:47:27 -07:00
Andrew Kane
1301706d30 Increased concurrency in HNSW insert test 2023-08-21 02:46:16 -07:00
Andrew Kane
0d58683011 Improved HNSW insert test 2023-08-21 02:44:20 -07:00
Andrew Kane
90a042e5aa Wait for selects to complete 2023-08-21 02:24:53 -07:00
Andrew Kane
bbd57dfebf Moved wait [skip ci] 2023-08-21 01:02:05 -07:00
Andrew Kane
40a013a622 Wait for inserts to complete when vacuuming HNSW 2023-08-21 00:36:32 -07:00
Andrew Kane
ef1209eaf4 Added version to reduce stale reads and writes and prepare for optimistic locking 2023-08-20 17:08:20 -07:00
Andrew Kane
687263ccd4 DRY HNSW vacuum code 2023-08-20 14:52:31 -07:00
Andrew Kane
a62c045c93 Update metapage if needed for HNSW vacuum 2023-08-19 12:19:42 -07:00
Andrew Kane
651e4343c1 Made entryPoint argument for RepairGraphElement 2023-08-19 11:47:13 -07:00
Andrew Kane
2ca2ef94e6 Use rmdir [skip ci] 2023-08-19 10:15:50 -07:00
Andrew Kane
6ba95691d3 Added headers for Windows 2023-08-19 10:03:15 -07:00
Andrew Kane
e9de248e4f Skip i386 tests for windows branches [skip ci] 2023-08-19 10:01:55 -07:00
Andrew Kane
fe79d435c0 Use HEADERS for simplicity [skip ci] 2023-08-19 09:58:25 -07:00
Florents Tselai
b34525fbc2 Make pgvector headers available to others. (#233) 2023-08-19 09:56:02 -07:00
Andrew Kane
206c42e170 Cleaned up header 2023-08-19 01:22:28 -07:00
Andrew Kane
4f8d824280 Use variable for uninstall [skip ci] 2023-08-19 01:21:05 -07:00
Andrew Kane
ca847d02b0 Fixed highest point in HNSW vacuum [skip ci] 2023-08-18 22:23:19 -07:00
Andrew Kane
ca726052ae Fixed flaky test [skip ci] 2023-08-18 20:54:31 -07:00
Andrew Kane
2b25127f3d Fixed flaky test [skip ci] 2023-08-18 20:53:47 -07:00
Andrew Kane
fecb8c06c9 Simplified code 2023-08-18 20:49:00 -07:00
Andrew Kane
a03f6ae4bc Added prefix to function name [skip ci] 2023-08-18 00:54:09 -07:00
Andrew Kane
ed20d98777 Check if item pointer is valid [skip ci] 2023-08-16 17:29:59 -07:00
Andrew Kane
b72a22b3c0 Fixed duplicate connections when repairing graph 2023-08-16 17:07:19 -07:00
Andrew Kane
8c5c0f791e Improved HNSW insert code [skip ci] 2023-08-16 16:31:11 -07:00
Andrew Kane
f9f5ae61df Added test for select with no data for ivfflat 2023-08-15 23:44:18 -07:00
Andrew Kane
2b138d6cb5 Fixed select with no data 2023-08-15 23:25:28 -07:00
Andrew Kane
e8f36aee92 Improved HNSW vacuum code [skip ci] 2023-08-15 22:36:47 -07:00
Andrew Kane
508a8a9ac7 Updated comments [skip ci] 2023-08-15 20:32:15 -07:00
Andrew Kane
d0b0298cda Improved HNSW vacuum code [skip ci] 2023-08-15 20:02:21 -07:00
Andrew Kane
c3bafc76e8 Only update entry point on inserts if level is greater 2023-08-15 12:31:06 -07:00
Andrew Kane
86a062e504 Set entry level in CreateMetaPage [skip ci] 2023-08-15 11:47:05 -07:00
Andrew Kane
b421e76f29 Updated comment [skip ci] 2023-08-13 17:23:52 -07:00
Andrew Kane
ef480ff0b9 Added comment [skip ci] 2023-08-13 17:22:25 -07:00
Andrew Kane
e1d6654063 Revert "Improved HNSW vacuum performance"
This reverts commit c5b2f3ac8b.
2023-08-13 17:19:22 -07:00
Andrew Kane
c5b2f3ac8b Improved HNSW vacuum performance 2023-08-13 16:50:36 -07:00
Andrew Kane
0f238b1fa9 Update neighbors when vacuuming 2023-08-13 16:17:30 -07:00
Andrew Kane
6fc7d06313 Improved HNSW vacuuming 2023-08-13 15:53:07 -07:00
Andrew Kane
304e262a41 Fixed initialization 2023-08-13 15:50:05 -07:00
Andrew Kane
23cb79c1dc Fixed recall for HNSW after vacuuming 2023-08-13 15:37:47 -07:00
Andrew Kane
dc8d467ba5 Improved HNSW vacuum recall test 2023-08-13 13:46:06 -07:00
Andrew Kane
29a0a00731 Improved test [skip ci] 2023-08-13 13:42:51 -07:00
Andrew Kane
eda07ccf7c Improved test 2023-08-13 13:35:43 -07:00
Andrew Kane
c694dbb776 Added test for HNSW vacuum recall 2023-08-13 13:09:51 -07:00
Andrew Kane
3a857ef9f9 Updated readme [skip ci] 2023-08-13 12:16:36 -07:00
Andrew Kane
b60e2483a9 Improved HNSW vacuum stats [skip ci] 2023-08-12 14:09:13 -07:00
Andrew Kane
4295ee3b3a Updated todos [skip ci] 2023-08-12 13:47:20 -07:00
Andrew Kane
8b1ea42d42 Improved HNSW vacuum test 2023-08-12 12:41:27 -07:00
Andrew Kane
3c291fea41 Added todo [skip ci] 2023-08-12 12:41:02 -07:00
Andrew Kane
3d568ef6a1 Added comments [skip ci] 2023-08-11 21:12:18 -07:00
Andrew Kane
7db3b08ef4 Simplified tests [skip ci] 2023-08-11 19:41:55 -07:00
Andrew Kane
4196c88dbd Added opclass to tests [skip ci] 2023-08-11 19:40:04 -07:00
Andrew Kane
3e7ee6ea04 Simplified opclasses in TAP tests [skip ci] 2023-08-11 19:20:03 -07:00
Andrew Kane
5616c1d7a7 Simplified TAP tests [skip ci] 2023-08-11 19:16:08 -07:00
Andrew Kane
0778a507de Use consistent style in TAP tests, part 2 [skip ci] 2023-08-11 19:13:22 -07:00
Andrew Kane
aabbeac30c Use consistent style in TAP tests [skip ci] 2023-08-11 19:08:15 -07:00
Andrew Kane
746dce72ef Use concurrent inserts for insert recall test 2023-08-11 18:41:00 -07:00
Andrew Kane
098813c483 Improved concurrent inserts test for HNSW [skip ci] 2023-08-11 18:33:01 -07:00
Andrew Kane
9d689afd7e Updated test name [skip ci] 2023-08-11 17:17:28 -07:00
Andrew Kane
03930e15a4 Updated comment [skip ci] 2023-08-11 17:09:09 -07:00
Andrew Kane
8a807c01f6 Test fallback path for duplicates [skip ci] 2023-08-11 17:07:38 -07:00
Andrew Kane
85ebb7714e Updated test name [skip ci] 2023-08-11 17:07:17 -07:00
Andrew Kane
34e99593ab Added comment [skip ci] 2023-08-11 17:02:50 -07:00
Andrew Kane
fc45231ef0 Improving naming [skip ci] 2023-08-11 14:56:46 -07:00
Andrew Kane
0191a90f9f Updated comment [skip ci] 2023-08-11 14:37:56 -07:00
Andrew Kane
82e83bd3b1 Added comment [skip ci] 2023-08-11 14:29:24 -07:00
Andrew Kane
d8a14c658a Fixed flaky test [skip ci] 2023-08-11 13:38:42 -07:00
Andrew Kane
ae5de26893 Improved logic for updating HNSW insert page 2023-08-11 13:24:56 -07:00
Andrew Kane
daef83a112 Added comment [skip ci] 2023-08-11 13:03:38 -07:00
Andrew Kane
c9d82c6bdc Revert "Improved insert logic [skip ci]"
This reverts commit 573a336f53.
2023-08-11 12:52:58 -07:00
Andrew Kane
573a336f53 Improved insert logic [skip ci] 2023-08-11 12:39:22 -07:00
Andrew Kane
e7a913f361 Updated comment [skip ci] 2023-08-11 10:11:01 -07:00
Andrew Kane
33438c3cf9 Use neighbor page for insert page [skip ci] 2023-08-11 09:51:41 -07:00
Andrew Kane
8ec1821f1e Added comment [skip ci] 2023-08-11 09:45:08 -07:00
Andrew Kane
b288de719d Consider previously deleted tuples for insert page [skip ci] 2023-08-11 09:41:31 -07:00
Andrew Kane
c13f713f29 Improved test [skip ci] 2023-08-11 09:36:29 -07:00
Andrew Kane
6d15435003 Updated comment [skip ci] 2023-08-11 09:26:54 -07:00
Andrew Kane
12d15a9c41 Improved inserts for HNSW 2023-08-11 09:23:56 -07:00
Andrew Kane
c64288377b Updated min ef_construction to 4 [skip ci] 2023-08-10 21:11:10 -07:00
Andrew Kane
451e36cee7 Added check for ef_construction [skip ci] 2023-08-10 21:09:38 -07:00
Andrew Kane
3ff2e34d7f Updated min M to 2 [skip ci] 2023-08-10 20:57:50 -07:00
Andrew Kane
94a684c9e9 Added test for duplicates 2023-08-10 20:52:58 -07:00
Andrew Kane
1a0d7bccc7 Updated min ef_search to 1 [skip ci] 2023-08-10 20:47:15 -07:00
Andrew Kane
ded8bda72a Fixed flaky test [skip ci] 2023-08-10 18:59:11 -07:00
Andrew Kane
9b967d592f Improved concurrent inserts with empty entry point, part 2 2023-08-10 18:31:58 -07:00
Andrew Kane
ed513e62c1 Improved code for skipping element [skip ci] 2023-08-10 15:29:08 -07:00
Andrew Kane
27ccb5fa7a Improved code [skip ci] 2023-08-10 15:23:26 -07:00
Andrew Kane
92e25e7733 Fixed warning 2023-08-10 15:10:07 -07:00
Andrew Kane
a18bea24b8 Improved concurrent inserts with low number of elements 2023-08-10 15:07:28 -07:00
Andrew Kane
483173460b Improved concurrent inserts with empty entry point 2023-08-10 14:35:24 -07:00
Andrew Kane
da8a914106 Moved FindDuplicate [skip ci] 2023-08-10 13:44:21 -07:00
Andrew Kane
4d6da72b08 Simplified code 2023-08-10 09:56:17 -07:00
Andrew Kane
443c7a5dab Updated variable name [skip ci] 2023-08-10 09:02:01 -07:00
Andrew Kane
9287fe8bcc Updated comment [skip ci] 2023-08-10 08:59:38 -07:00
Andrew Kane
309fa94c05 Improved performance for duplicates 2023-08-10 08:52:45 -07:00
Andrew Kane
6f15dd266c Improved construction code 2023-08-10 08:38:31 -07:00
Andrew Kane
df68eb4570 Look for duplicates last, since may need to fallback 2023-08-10 00:00:27 -07:00
Andrew Kane
7c0d94c99c Improved concurrent inserts 2023-08-09 23:51:35 -07:00
Andrew Kane
d63d430af8 Fixed flaky test [skip ci] 2023-08-09 20:06:28 -07:00
Andrew Kane
5d62e4d080 Moved SelectNeighbors after duplicate check 2023-08-09 19:19:41 -07:00
Andrew Kane
dab8f25d1c Fixed overflow with vector_norm 2023-08-09 16:33:54 -07:00
45 changed files with 1066 additions and 623 deletions

View File

@@ -77,5 +77,24 @@ jobs:
nmake /NOLOGO /F Makefile.win clean && ^ nmake /NOLOGO /F Makefile.win clean && ^
nmake /NOLOGO /F Makefile.win uninstall nmake /NOLOGO /F Makefile.win uninstall
shell: cmd shell: cmd
- if: ${{ failure() }} i386:
run: cat regression.diffs if: ${{ !startsWith(github.ref_name, 'mac') && !startsWith(github.ref_name, 'windows') }}
runs-on: ubuntu-latest
container:
image: debian:11
options: --platform linux/386
steps:
- run: apt-get update && apt-get install -y build-essential git libipc-run-perl postgresql-13 postgresql-server-dev-13 sudo
- run: service postgresql start
- run: |
git clone https://github.com/${{ github.repository }}.git pgvector
cd pgvector
git fetch origin ${{ github.ref }}
git reset --hard FETCH_HEAD
make
make install
chown -R postgres .
sudo -u postgres make installcheck
sudo -u postgres make prove_installcheck
env:
PG_CFLAGS: -Wall -Wextra -Werror -Wno-unused-parameter -Wno-sign-compare

View File

@@ -3,7 +3,6 @@
- Added HNSW index type - Added HNSW index type
- Added support for parallel index builds - Added support for parallel index builds
- Added `l1_distance` function - Added `l1_distance` function
- Added `normalize_l2` function
- Added element-wise multiplication for vectors - Added element-wise multiplication for vectors
- Added `sum` aggregate - Added `sum` aggregate
- Improved performance of distance functions - Improved performance of distance functions

View File

@@ -4,6 +4,7 @@ EXTVERSION = 0.4.4
MODULE_big = vector MODULE_big = vector
DATA = $(wildcard sql/*--*.sql) DATA = $(wildcard sql/*--*.sql)
OBJS = src/hnsw.o src/hnswbuild.o src/hnswinsert.o src/hnswscan.o src/hnswutils.o src/hnswvacuum.o src/ivfbuild.o src/ivfflat.o src/ivfinsert.o src/ivfkmeans.o src/ivfscan.o src/ivfutils.o src/ivfvacuum.o src/vector.o OBJS = src/hnsw.o src/hnswbuild.o src/hnswinsert.o src/hnswscan.o src/hnswutils.o src/hnswvacuum.o src/ivfbuild.o src/ivfflat.o src/ivfinsert.o src/ivfkmeans.o src/ivfscan.o src/ivfutils.o src/ivfvacuum.o src/vector.o
HEADERS = src/vector.h
TESTS = $(wildcard test/sql/*.sql) TESTS = $(wildcard test/sql/*.sql)
REGRESS = $(patsubst test/sql/%.sql,%,$(TESTS)) REGRESS = $(patsubst test/sql/%.sql,%,$(TESTS))

View File

@@ -2,6 +2,7 @@ EXTENSION = vector
EXTVERSION = 0.4.4 EXTVERSION = 0.4.4
OBJS = src\hnsw.obj src\hnswbuild.obj src\hnswinsert.obj src\hnswscan.obj src\hnswutils.obj src\hnswvacuum.obj src\ivfbuild.obj src\ivfflat.obj src\ivfinsert.obj src\ivfkmeans.obj src\ivfscan.obj src\ivfutils.obj src\ivfvacuum.obj src\vector.obj OBJS = src\hnsw.obj src\hnswbuild.obj src\hnswinsert.obj src\hnswscan.obj src\hnswutils.obj src\hnswvacuum.obj src\ivfbuild.obj src\ivfflat.obj src\ivfinsert.obj src\ivfkmeans.obj src\ivfscan.obj src\ivfutils.obj src\ivfvacuum.obj src\vector.obj
HEADERS = src\vector.h
REGRESS = btree cast copy functions input ivfflat_cosine ivfflat_ip ivfflat_l2 ivfflat_options ivfflat_unlogged REGRESS = btree cast copy functions input ivfflat_cosine ivfflat_ip ivfflat_l2 ivfflat_options ivfflat_unlogged
REGRESS_OPTS = --inputdir=test --load-extension=vector REGRESS_OPTS = --inputdir=test --load-extension=vector
@@ -54,6 +55,8 @@ install:
copy $(SHLIB) "$(PKGLIBDIR)" copy $(SHLIB) "$(PKGLIBDIR)"
copy $(EXTENSION).control "$(SHAREDIR)\extension" copy $(EXTENSION).control "$(SHAREDIR)\extension"
copy sql\$(EXTENSION)--*.sql "$(SHAREDIR)\extension" copy sql\$(EXTENSION)--*.sql "$(SHAREDIR)\extension"
mkdir "$(INCLUDEDIR_SERVER)\extension\$(EXTENSION)"
copy $(HEADERS) "$(INCLUDEDIR_SERVER)\extension\$(EXTENSION)"
installcheck: installcheck:
"$(BINDIR)\pg_regress" --bindir="$(BINDIR)" $(REGRESS_OPTS) $(REGRESS) "$(BINDIR)\pg_regress" --bindir="$(BINDIR)" $(REGRESS_OPTS) $(REGRESS)
@@ -61,7 +64,9 @@ installcheck:
uninstall: uninstall:
del /f "$(PKGLIBDIR)\$(SHLIB)" del /f "$(PKGLIBDIR)\$(SHLIB)"
del /f "$(SHAREDIR)\extension\$(EXTENSION).control" del /f "$(SHAREDIR)\extension\$(EXTENSION).control"
del /f "$(SHAREDIR)\extension\vector--*.sql" del /f "$(SHAREDIR)\extension\$(EXTENSION)--*.sql"
del /f "$(INCLUDEDIR_SERVER)\extension\$(EXTENSION)\*.h"
rmdir "$(INCLUDEDIR_SERVER)\extension\$(EXTENSION)"
clean: clean:
del /f $(SHLIB) $(EXTENSION).lib $(EXTENSION).exp del /f $(SHLIB) $(EXTENSION).lib $(EXTENSION).exp

View File

@@ -2,7 +2,7 @@
Open-source vector similarity search for Postgres Open-source vector similarity search for Postgres
Store all of your application data in one place. Supports: Store your vectors with the rest of your data. Supports:
- exact and approximate nearest neighbor search - exact and approximate nearest neighbor search
- L2 distance, inner product, and cosine distance - L2 distance, inner product, and cosine distance
@@ -392,7 +392,6 @@ cosine_distance(vector, vector) → double precision | cosine distance
inner_product(vector, vector) → double precision | inner product inner_product(vector, vector) → double precision | inner product
l2_distance(vector, vector) → double precision | Euclidean distance l2_distance(vector, vector) → double precision | Euclidean distance
l1_distance(vector, vector) → double precision | taxicab distance [unreleased] l1_distance(vector, vector) → double precision | taxicab distance [unreleased]
normalize_l2(vector) → vector | normalize with Euclidean norm [unreleased]
vector_dims(vector) → integer | number of dimensions vector_dims(vector) → integer | number of dimensions
vector_norm(vector) → double precision | Euclidean norm vector_norm(vector) → double precision | Euclidean norm

View File

@@ -4,9 +4,6 @@
CREATE FUNCTION l1_distance(vector, vector) RETURNS float8 CREATE FUNCTION l1_distance(vector, vector) RETURNS float8
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
CREATE FUNCTION normalize_l2(vector) RETURNS vector
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
CREATE FUNCTION vector_mul(vector, vector) RETURNS vector CREATE FUNCTION vector_mul(vector, vector) RETURNS vector
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;

View File

@@ -49,9 +49,6 @@ CREATE FUNCTION vector_dims(vector) RETURNS integer
CREATE FUNCTION vector_norm(vector) RETURNS float8 CREATE FUNCTION vector_norm(vector) RETURNS float8
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
CREATE FUNCTION normalize_l2(vector) RETURNS vector
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
CREATE FUNCTION vector_add(vector, vector) RETURNS vector CREATE FUNCTION vector_add(vector, vector) RETURNS vector
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;

View File

@@ -37,7 +37,7 @@ HnswInit(void)
); );
DefineCustomIntVariable("hnsw.ef_search", "Sets the size of the dynamic candidate list for search", DefineCustomIntVariable("hnsw.ef_search", "Sets the size of the dynamic candidate list for search",
"Valid range is 10..1000.", &hnsw_ef_search, "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); HNSW_DEFAULT_EF_SEARCH, HNSW_MIN_EF_SEARCH, HNSW_MAX_EF_SEARCH, PGC_USERSET, 0, NULL, NULL, NULL);
} }

View File

@@ -22,28 +22,37 @@
#define HNSW_VERSION 1 #define HNSW_VERSION 1
#define HNSW_MAGIC_NUMBER 0xA953A953 #define HNSW_MAGIC_NUMBER 0xA953A953
#define HNSW_PAGE_ID 0xFF85 #define HNSW_PAGE_ID 0xFF90
/* Preserved page numbers */ /* Preserved page numbers */
#define HNSW_METAPAGE_BLKNO 0 #define HNSW_METAPAGE_BLKNO 0
#define HNSW_HEAD_BLKNO 1 /* first element page */ #define HNSW_HEAD_BLKNO 1 /* first element page */
/* Must correspond to page numbers since page lock is used */
#define HNSW_UPDATE_LOCK 0
#define HNSW_SCAN_LOCK 1
/* HNSW parameters */
#define HNSW_DEFAULT_M 16 #define HNSW_DEFAULT_M 16
#define HNSW_MIN_M 4 #define HNSW_MIN_M 2
#define HNSW_MAX_M 100 #define HNSW_MAX_M 100
#define HNSW_DEFAULT_EF_CONSTRUCTION 40 #define HNSW_DEFAULT_EF_CONSTRUCTION 40
#define HNSW_MIN_EF_CONSTRUCTION 10 #define HNSW_MIN_EF_CONSTRUCTION 4
#define HNSW_MAX_EF_CONSTRUCTION 1000 #define HNSW_MAX_EF_CONSTRUCTION 1000
#define HNSW_DEFAULT_EF_SEARCH 40 #define HNSW_DEFAULT_EF_SEARCH 40
#define HNSW_MIN_EF_SEARCH 10 #define HNSW_MIN_EF_SEARCH 1
#define HNSW_MAX_EF_SEARCH 1000 #define HNSW_MAX_EF_SEARCH 1000
/* Tuple types */
#define HNSW_ELEMENT_TUPLE_TYPE 1 #define HNSW_ELEMENT_TUPLE_TYPE 1
#define HNSW_NEIGHBOR_TUPLE_TYPE 2 #define HNSW_NEIGHBOR_TUPLE_TYPE 2
/* Make graph robust against non-HOT updates */ /* Make graph robust against non-HOT updates */
#define HNSW_HEAPTIDS 10 #define HNSW_HEAPTIDS 10
#define HNSW_UPDATE_ENTRY_GREATER 1
#define HNSW_UPDATE_ENTRY_ALWAYS 2
/* Build phases */ /* Build phases */
/* PROGRESS_CREATEIDX_SUBPHASE_INITIALIZE is 1 */ /* PROGRESS_CREATEIDX_SUBPHASE_INITIALIZE is 1 */
#define PROGRESS_HNSW_PHASE_LOAD 2 #define PROGRESS_HNSW_PHASE_LOAD 2
@@ -68,10 +77,13 @@
#define HnswIsElementTuple(tup) ((tup)->type == HNSW_ELEMENT_TUPLE_TYPE) #define HnswIsElementTuple(tup) ((tup)->type == HNSW_ELEMENT_TUPLE_TYPE)
#define HnswIsNeighborTuple(tup) ((tup)->type == HNSW_NEIGHBOR_TUPLE_TYPE) #define HnswIsNeighborTuple(tup) ((tup)->type == HNSW_NEIGHBOR_TUPLE_TYPE)
#define HnswGetLayerM(m, layer) (layer == 0 ? m * 2 : m) /* 2 * M connections for ground layer */
#define HnswGetLayerM(m, layer) (layer == 0 ? (m) * 2 : (m))
/* Optimal ML from paper */
#define HnswGetMl(m) (1 / log(m)) #define HnswGetMl(m) (1 / log(m))
/* Ensure fits in uint8 */ /* 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 HnswGetMaxLevel(m) Min(((BLCKSZ - MAXALIGN(SizeOfPageHeaderData) - MAXALIGN(sizeof(HnswPageOpaqueData)) - offsetof(HnswNeighborTupleData, indextids) - sizeof(ItemIdData)) / (sizeof(ItemPointerData)) / m) - 2, 255)
/* Variables */ /* Variables */
@@ -106,13 +118,6 @@ typedef struct HnswNeighborArray
HnswCandidate *items; HnswCandidate *items;
} HnswNeighborArray; } HnswNeighborArray;
typedef struct HnswUpdate
{
HnswCandidate hc;
int level;
int index;
} HnswUpdate;
typedef struct HnswPairingHeapNode typedef struct HnswPairingHeapNode
{ {
pairingheap_node ph_node; pairingheap_node ph_node;
@@ -261,19 +266,25 @@ Buffer HnswNewBuffer(Relation index, ForkNumber forkNum);
void HnswInitPage(Buffer buf, Page page); void HnswInitPage(Buffer buf, Page page);
void HnswInitRegisterPage(Relation index, Buffer *buf, Page *page, GenericXLogState **state); void HnswInitRegisterPage(Relation index, Buffer *buf, Page *page, GenericXLogState **state);
void HnswInit(void); void HnswInit(void);
List *HnswSearchLayer(Datum q, List *ep, int ef, int lc, Relation index, FmgrInfo *procinfo, Oid collation, bool inserting, BlockNumber *skipPage, OffsetNumber *skipOffno); List *HnswSearchLayer(Datum q, List *ep, int ef, int lc, Relation index, FmgrInfo *procinfo, Oid collation, bool inserting, HnswElement skipElement);
HnswElement HnswGetEntryPoint(Relation index); HnswElement HnswGetEntryPoint(Relation index);
HnswElement HnswInitElement(ItemPointer tid, int m, double ml, int maxLevel); HnswElement HnswInitElement(ItemPointer tid, int m, double ml, int maxLevel);
void HnswFreeElement(HnswElement element); void HnswFreeElement(HnswElement element);
HnswElement HnswInsertElement(HnswElement element, HnswElement entryPoint, Relation index, FmgrInfo *procinfo, Oid collation, int m, int efConstruction, List **updates, bool vacuuming); HnswElement HnswInitElementFromBlock(BlockNumber blkno, OffsetNumber offno);
HnswCandidate *HnswEntryCandidate(HnswElement em, Datum q, Relation rel, FmgrInfo *procinfo, Oid collation, bool loadvec); void HnswInsertElement(HnswElement element, HnswElement entryPoint, Relation index, FmgrInfo *procinfo, Oid collation, int m, int efConstruction, bool existing);
void HnswUpdateMetaPage(Relation index, bool updateEntry, HnswElement entryPoint, BlockNumber insertPage, ForkNumber forkNum); HnswElement HnswFindDuplicate(HnswElement e);
HnswCandidate *HnswEntryCandidate(HnswElement em, Datum q, Relation rel, FmgrInfo *procinfo, Oid collation, bool loadVec);
void HnswUpdateMetaPage(Relation index, int updateEntry, HnswElement entryPoint, BlockNumber insertPage, ForkNumber forkNum);
void HnswSetNeighborTuple(HnswNeighborTuple ntup, HnswElement e, int m); void HnswSetNeighborTuple(HnswNeighborTuple ntup, HnswElement e, int m);
void HnswAddHeapTid(HnswElement element, ItemPointer heaptid); void HnswAddHeapTid(HnswElement element, ItemPointer heaptid);
void HnswInitNeighbors(HnswElement element, int m); void HnswInitNeighbors(HnswElement element, int m);
bool HnswInsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heap_tid, Relation heapRel); bool HnswInsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heap_tid, Relation heapRel);
void HnswUpdateNeighborPages(Relation index, FmgrInfo *procinfo, Oid collation, HnswElement e, int m, bool checkExisting);
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); void HnswLoadElement(HnswElement element, float *distance, Datum *q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec);
void HnswSetElementTuple(HnswElementTuple etup, HnswElement element); void HnswSetElementTuple(HnswElementTuple etup, HnswElement element);
void HnswUpdateConnection(HnswElement element, HnswCandidate * hc, int m, int lc, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation);
void HnswLoadNeighbors(HnswElement element, Relation index);
/* Index access methods */ /* Index access methods */
IndexBuildResult *hnswbuild(Relation heap, Relation index, IndexInfo *indexInfo); IndexBuildResult *hnswbuild(Relation heap, Relation index, IndexInfo *indexInfo);

View File

@@ -60,6 +60,7 @@ CreateMetaPage(HnswBuildState * buildstate)
metap->efConstruction = buildstate->efConstruction; metap->efConstruction = buildstate->efConstruction;
metap->entryBlkno = InvalidBlockNumber; metap->entryBlkno = InvalidBlockNumber;
metap->entryOffno = InvalidOffsetNumber; metap->entryOffno = InvalidOffsetNumber;
metap->entryLevel = -1;
metap->insertPage = InvalidBlockNumber; metap->insertPage = InvalidBlockNumber;
((PageHeader) page)->pd_lower = ((PageHeader) page)->pd_lower =
((char *) metap + sizeof(HnswMetaPageData)) - (char *) page; ((char *) metap + sizeof(HnswMetaPageData)) - (char *) page;
@@ -182,7 +183,7 @@ CreateElementPages(HnswBuildState * buildstate)
GenericXLogFinish(state); GenericXLogFinish(state);
UnlockReleaseBuffer(buf); UnlockReleaseBuffer(buf);
HnswUpdateMetaPage(index, true, buildstate->entryPoint, insertPage, forkNum); HnswUpdateMetaPage(index, HNSW_UPDATE_ENTRY_ALWAYS, buildstate->entryPoint, insertPage, forkNum);
pfree(etup); pfree(etup);
pfree(ntup); pfree(ntup);
@@ -288,7 +289,23 @@ InsertTuple(Relation index, Datum *values, HnswElement element, HnswBuildState *
memcpy(element->vec, DatumGetVector(value), VECTOR_SIZE(buildstate->dimensions)); memcpy(element->vec, DatumGetVector(value), VECTOR_SIZE(buildstate->dimensions));
/* Insert element in graph */ /* Insert element in graph */
*dup = HnswInsertElement(element, entryPoint, NULL, procinfo, collation, m, efConstruction, NULL, false); HnswInsertElement(element, entryPoint, NULL, procinfo, collation, m, efConstruction, false);
/* Look for duplicate */
*dup = HnswFindDuplicate(element);
/* Update neighbors if needed */
if (*dup == NULL)
{
for (int lc = element->level; lc >= 0; lc--)
{
int lm = HnswGetLayerM(m, lc);
HnswNeighborArray *neighbors = &element->neighbors[lc];
for (int i = 0; i < neighbors->length; i++)
HnswUpdateConnection(element, &neighbors->items[i], lm, lc, NULL, NULL, procinfo, collation);
}
}
/* Update entry point if needed */ /* Update entry point if needed */
if (*dup == NULL && (entryPoint == NULL || element->level > entryPoint->level)) if (*dup == NULL && (entryPoint == NULL || element->level > entryPoint->level))
@@ -407,6 +424,9 @@ InitBuildState(HnswBuildState * buildstate, Relation heap, Relation index, Index
if (buildstate->dimensions > HNSW_MAX_DIM) if (buildstate->dimensions > HNSW_MAX_DIM)
elog(ERROR, "column cannot have more than %d dimensions for hnsw index", HNSW_MAX_DIM); elog(ERROR, "column cannot have more than %d dimensions for hnsw index", HNSW_MAX_DIM);
if (buildstate->efConstruction < 2 * buildstate->m)
elog(ERROR, "ef_construction must be greater than or equal to 2 * m");
buildstate->reltuples = 0; buildstate->reltuples = 0;
buildstate->indtuples = 0; buildstate->indtuples = 0;

View File

@@ -34,7 +34,7 @@ GetInsertPage(Relation index)
* Check for a free offset * Check for a free offset
*/ */
static bool static bool
HnswFreeOffset(Relation index, Buffer buf, Page page, HnswElement element, Size ntupSize, Buffer *nbuf, Page *npage, OffsetNumber *freeOffno, OffsetNumber *freeNeighborOffno, BlockNumber *firstFreePage) HnswFreeOffset(Relation index, Buffer buf, Page page, HnswElement element, Size ntupSize, Buffer *nbuf, Page *npage, OffsetNumber *freeOffno, OffsetNumber *freeNeighborOffno, BlockNumber *newInsertPage)
{ {
OffsetNumber offno; OffsetNumber offno;
OffsetNumber maxoffno = PageGetMaxOffsetNumber(page); OffsetNumber maxoffno = PageGetMaxOffsetNumber(page);
@@ -49,14 +49,15 @@ HnswFreeOffset(Relation index, Buffer buf, Page page, HnswElement element, Size
if (etup->deleted) if (etup->deleted)
{ {
BlockNumber elementPage = BufferGetBlockNumber(buf);
BlockNumber neighborPage = ItemPointerGetBlockNumber(&etup->neighbortid); BlockNumber neighborPage = ItemPointerGetBlockNumber(&etup->neighbortid);
OffsetNumber neighborOffno = ItemPointerGetOffsetNumber(&etup->neighbortid); OffsetNumber neighborOffno = ItemPointerGetOffsetNumber(&etup->neighbortid);
ItemId itemid; ItemId itemid;
if (!BlockNumberIsValid(*firstFreePage)) if (!BlockNumberIsValid(*newInsertPage))
*firstFreePage = neighborPage; *newInsertPage = elementPage;
if (neighborPage == BufferGetBlockNumber(buf)) if (neighborPage == elementPage)
{ {
*nbuf = buf; *nbuf = buf;
*npage = page; *npage = page;
@@ -110,7 +111,7 @@ HnswInsertAppendPage(Relation index, Buffer *nbuf, Page *npage, GenericXLogState
* Add to element and neighbor pages * Add to element and neighbor pages
*/ */
static void static void
WriteNewElementPages(Relation index, HnswElement e, int m) WriteNewElementPages(Relation index, HnswElement e, int m, BlockNumber insertPage, BlockNumber *updatedInsertPage)
{ {
Buffer buf; Buffer buf;
Page page; Page page;
@@ -118,21 +119,24 @@ WriteNewElementPages(Relation index, HnswElement e, int m)
Size etupSize; Size etupSize;
Size ntupSize; Size ntupSize;
Size combinedSize; Size combinedSize;
Size maxSize;
Size minCombinedSize;
HnswElementTuple etup; HnswElementTuple etup;
BlockNumber insertPage = GetInsertPage(index); BlockNumber currentPage = insertPage;
BlockNumber originalInsertPage = insertPage;
int dimensions = e->vec->dim; int dimensions = e->vec->dim;
HnswNeighborTuple ntup; HnswNeighborTuple ntup;
Buffer nbuf; Buffer nbuf;
Page npage; Page npage;
OffsetNumber freeOffno = InvalidOffsetNumber; OffsetNumber freeOffno = InvalidOffsetNumber;
OffsetNumber freeNeighborOffno = InvalidOffsetNumber; OffsetNumber freeNeighborOffno = InvalidOffsetNumber;
BlockNumber firstFreePage = InvalidBlockNumber; BlockNumber newInsertPage = InvalidBlockNumber;
/* Calculate sizes */ /* Calculate sizes */
etupSize = HNSW_ELEMENT_TUPLE_SIZE(dimensions); etupSize = HNSW_ELEMENT_TUPLE_SIZE(dimensions);
ntupSize = HNSW_NEIGHBOR_TUPLE_SIZE(e->level, m); ntupSize = HNSW_NEIGHBOR_TUPLE_SIZE(e->level, m);
combinedSize = etupSize + ntupSize + sizeof(ItemIdData); combinedSize = etupSize + ntupSize + sizeof(ItemIdData);
maxSize = BLCKSZ - MAXALIGN(SizeOfPageHeaderData) - MAXALIGN(sizeof(HnswPageOpaqueData));
minCombinedSize = etupSize + HNSW_NEIGHBOR_TUPLE_SIZE(0, m) + sizeof(ItemIdData);
/* Prepare element tuple */ /* Prepare element tuple */
etup = palloc0(etupSize); etup = palloc0(etupSize);
@@ -142,16 +146,22 @@ WriteNewElementPages(Relation index, HnswElement e, int m)
ntup = palloc0(ntupSize); ntup = palloc0(ntupSize);
HnswSetNeighborTuple(ntup, e, m); HnswSetNeighborTuple(ntup, e, m);
/* Find a page to insert the item */ /* Find a page (or two if needed) to insert the tuples */
for (;;) for (;;)
{ {
buf = ReadBuffer(index, insertPage); buf = ReadBuffer(index, currentPage);
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE); LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
state = GenericXLogStart(index); state = GenericXLogStart(index);
page = GenericXLogRegisterBuffer(state, buf, 0); page = GenericXLogRegisterBuffer(state, buf, 0);
/* Space for both */ /* Keep track of first page where element at level 0 can fit */
if (!BlockNumberIsValid(newInsertPage) && PageGetFreeSpace(page) >= minCombinedSize)
newInsertPage = currentPage;
/* First, try the fastest path */
/* Space for both tuples on the current page */
/* This can split existing tuples in rare cases */
if (PageGetFreeSpace(page) >= combinedSize) if (PageGetFreeSpace(page) >= combinedSize)
{ {
nbuf = buf; nbuf = buf;
@@ -159,15 +169,8 @@ WriteNewElementPages(Relation index, HnswElement e, int m)
break; break;
} }
/* Space for element but not neighbors and last page */ /* Next, try space from a deleted element */
if (PageGetFreeSpace(page) >= etupSize && !BlockNumberIsValid(HnswPageGetOpaque(page)->nextblkno)) if (HnswFreeOffset(index, buf, page, e, ntupSize, &nbuf, &npage, &freeOffno, &freeNeighborOffno, &newInsertPage))
{
HnswInsertAppendPage(index, &nbuf, &npage, state, page);
break;
}
/* Space from deleted item */
if (HnswFreeOffset(index, buf, page, e, ntupSize, &nbuf, &npage, &freeOffno, &freeNeighborOffno, &firstFreePage))
{ {
if (nbuf != buf) if (nbuf != buf)
npage = GenericXLogRegisterBuffer(state, nbuf, 0); npage = GenericXLogRegisterBuffer(state, nbuf, 0);
@@ -175,9 +178,17 @@ WriteNewElementPages(Relation index, HnswElement e, int m)
break; break;
} }
insertPage = HnswPageGetOpaque(page)->nextblkno; /* Finally, try space for element only if last page */
/* Skip if both tuples can fit on the same page */
if (combinedSize > maxSize && PageGetFreeSpace(page) >= etupSize && !BlockNumberIsValid(HnswPageGetOpaque(page)->nextblkno))
{
HnswInsertAppendPage(index, &nbuf, &npage, state, page);
break;
}
if (BlockNumberIsValid(insertPage)) currentPage = HnswPageGetOpaque(page)->nextblkno;
if (BlockNumberIsValid(currentPage))
{ {
/* Move to next page */ /* Move to next page */
GenericXLogAbort(state); GenericXLogAbort(state);
@@ -219,7 +230,10 @@ WriteNewElementPages(Relation index, HnswElement e, int m)
e->blkno = BufferGetBlockNumber(buf); e->blkno = BufferGetBlockNumber(buf);
e->neighborPage = BufferGetBlockNumber(nbuf); e->neighborPage = BufferGetBlockNumber(nbuf);
insertPage = e->neighborPage; /* Added tuple to new page if newInsertPage is not set */
/* So can set to neighbor page instead of element page */
if (!BlockNumberIsValid(newInsertPage))
newInsertPage = e->neighborPage;
if (OffsetNumberIsValid(freeOffno)) if (OffsetNumberIsValid(freeOffno))
{ {
@@ -265,75 +279,126 @@ WriteNewElementPages(Relation index, HnswElement e, int m)
UnlockReleaseBuffer(nbuf); UnlockReleaseBuffer(nbuf);
/* Update the insert page */ /* Update the insert page */
if (insertPage != originalInsertPage && (!OffsetNumberIsValid(freeOffno) || firstFreePage == insertPage)) if (BlockNumberIsValid(newInsertPage) && newInsertPage != insertPage)
HnswUpdateMetaPage(index, false, NULL, insertPage, MAIN_FORKNUM); *updatedInsertPage = newInsertPage;
} }
/* /*
* Calculate index for update * Check if connection already exists
*/ */
static int static bool
HnswGetIndex(HnswUpdate * update, int m) ConnectionExists(HnswElement e, HnswNeighborTuple ntup, int startIdx, int lm)
{ {
return (update->hc.element->level - update->level) * m + update->index; for (int i = 0; i < lm; i++)
{
ItemPointer indextid = &ntup->indextids[startIdx + i];
if (!ItemPointerIsValid(indextid))
break;
if (ItemPointerGetBlockNumber(indextid) == e->blkno && ItemPointerGetOffsetNumber(indextid) == e->offno)
return true;
}
return false;
} }
/* /*
* Update neighbors * Update neighbors
*/ */
static void void
UpdateNeighborPages(Relation index, HnswElement e, int m, List *updates) HnswUpdateNeighborPages(Relation index, FmgrInfo *procinfo, Oid collation, HnswElement e, int m, bool checkExisting)
{ {
ListCell *lc; for (int lc = e->level; lc >= 0; lc--)
/* Could update multiple at once for same element */
/* but should only happen a low percent of time, so keep simple for now */
foreach(lc, updates)
{ {
Buffer buf; int lm = HnswGetLayerM(m, lc);
Page page; HnswNeighborArray *neighbors = &e->neighbors[lc];
GenericXLogState *state;
HnswUpdate *update = lfirst(lc);
ItemId itemid;
HnswNeighborTuple ntup;
Size ntupSize;
int idx;
OffsetNumber offno = update->hc.element->neighborOffno;
/* Register page */ for (int i = 0; i < neighbors->length; i++)
buf = ReadBuffer(index, update->hc.element->neighborPage);
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
state = GenericXLogStart(index);
page = GenericXLogRegisterBuffer(state, buf, 0);
/* Get tuple */
itemid = PageGetItemId(page, offno);
ntup = (HnswNeighborTuple) PageGetItem(page, itemid);
ntupSize = ItemIdGetLength(itemid);
/* Calculate index */
idx = HnswGetIndex(update, m);
/* Make robust to issues */
if (idx < ntup->count)
{ {
ItemPointer indextid = &ntup->indextids[idx]; HnswCandidate *hc = &neighbors->items[i];
Buffer buf;
Page page;
GenericXLogState *state;
ItemId itemid;
HnswNeighborTuple ntup;
Size ntupSize;
int idx = -1;
int startIdx;
OffsetNumber offno = hc->element->neighborOffno;
/* Update neighbor */ /* Get latest neighbors since they may have changed */
ItemPointerSet(indextid, e->blkno, e->offno); /* Do not lock yet since selecting neighbors can take time */
HnswLoadNeighbors(hc->element, index);
/* Overwrite tuple */ /*
if (!PageIndexTupleOverwrite(page, offno, (Item) ntup, ntupSize)) * Could improve performance for vacuuming by checking neighbors
elog(ERROR, "failed to add index item to \"%s\"", RelationGetRelationName(index)); * 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.
*/
/* Commit */ /* Select neighbors */
MarkBufferDirty(buf); HnswUpdateConnection(e, hc, lm, lc, &idx, index, procinfo, collation);
GenericXLogFinish(state);
/* New element was not selected as a neighbor */
if (idx == -1)
continue;
/* Register page */
buf = ReadBuffer(index, hc->element->neighborPage);
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
state = GenericXLogStart(index);
page = GenericXLogRegisterBuffer(state, buf, 0);
/* Get tuple */
itemid = PageGetItemId(page, offno);
ntup = (HnswNeighborTuple) PageGetItem(page, itemid);
ntupSize = ItemIdGetLength(itemid);
/* Calculate index for update */
startIdx = (hc->element->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 */
ItemPointerSet(indextid, e->blkno, e->offno);
/* Overwrite tuple */
if (!PageIndexTupleOverwrite(page, offno, (Item) ntup, ntupSize))
elog(ERROR, "failed to add index item to \"%s\"", RelationGetRelationName(index));
/* Commit */
MarkBufferDirty(buf);
GenericXLogFinish(state);
}
else
GenericXLogAbort(state);
UnlockReleaseBuffer(buf);
} }
else
GenericXLogAbort(state);
UnlockReleaseBuffer(buf);
} }
} }
@@ -391,8 +456,10 @@ HnswAddDuplicate(Relation index, HnswElement element, HnswElement dup)
* Write changes to disk * Write changes to disk
*/ */
static void static void
WriteElement(Relation index, HnswElement element, int m, List *updates, HnswElement dup, HnswElement entryPoint) WriteElement(Relation index, FmgrInfo *procinfo, Oid collation, HnswElement element, int m, int efConstruction, HnswElement dup, HnswElement entryPoint)
{ {
BlockNumber newInsertPage = InvalidBlockNumber;
/* Try to add to existing page */ /* Try to add to existing page */
if (dup != NULL) if (dup != NULL)
{ {
@@ -400,13 +467,19 @@ WriteElement(Relation index, HnswElement element, int m, List *updates, HnswElem
return; return;
} }
/* If fails, take this path */ /* Write element and neighbor tuples */
WriteNewElementPages(index, element, m); WriteNewElementPages(index, element, m, GetInsertPage(index), &newInsertPage);
UpdateNeighborPages(index, element, m, updates);
/* Update insert page if needed */
if (BlockNumberIsValid(newInsertPage))
HnswUpdateMetaPage(index, 0, NULL, newInsertPage, MAIN_FORKNUM);
/* Update neighbors */
HnswUpdateNeighborPages(index, procinfo, collation, element, m, false);
/* Update metapage if needed */ /* Update metapage if needed */
if (entryPoint == NULL || element->level > entryPoint->level) if (entryPoint == NULL || element->level > entryPoint->level)
HnswUpdateMetaPage(index, true, element, InvalidBlockNumber, MAIN_FORKNUM); HnswUpdateMetaPage(index, HNSW_UPDATE_ENTRY_GREATER, element, InvalidBlockNumber, MAIN_FORKNUM);
} }
/* /*
@@ -424,8 +497,8 @@ HnswInsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heap_ti
double ml = HnswGetMl(m); double ml = HnswGetMl(m);
FmgrInfo *procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC); FmgrInfo *procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC);
Oid collation = index->rd_indcollation[0]; Oid collation = index->rd_indcollation[0];
List *updates = NIL;
HnswElement dup; HnswElement dup;
LOCKMODE lockmode = ShareLock;
/* Detoast once for all calls */ /* Detoast once for all calls */
value = PointerGetDatum(PG_DETOAST_DATUM(values[0])); value = PointerGetDatum(PG_DETOAST_DATUM(values[0]));
@@ -442,14 +515,41 @@ HnswInsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heap_ti
element = HnswInitElement(heap_tid, m, ml, HnswGetMaxLevel(m)); element = HnswInitElement(heap_tid, m, ml, HnswGetMaxLevel(m));
element->vec = DatumGetVector(value); element->vec = DatumGetVector(value);
/*
* 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
* buffer lock (or reads when vacuuming).
*/
LockPage(index, HNSW_UPDATE_LOCK, lockmode);
/* Get entry point */ /* Get entry point */
entryPoint = HnswGetEntryPoint(index); entryPoint = HnswGetEntryPoint(index);
/* Prevent concurrent inserts when likely updating entry point */
if (entryPoint == NULL || element->level > entryPoint->level)
{
/* Release shared lock */
UnlockPage(index, HNSW_UPDATE_LOCK, lockmode);
/* Get exclusive lock */
lockmode = ExclusiveLock;
LockPage(index, HNSW_UPDATE_LOCK, lockmode);
/* Get latest entry point after lock is acquired */
entryPoint = HnswGetEntryPoint(index);
}
/* Insert element in graph */ /* Insert element in graph */
dup = HnswInsertElement(element, entryPoint, index, procinfo, collation, m, efConstruction, &updates, false); HnswInsertElement(element, entryPoint, index, procinfo, collation, m, efConstruction, false);
/* Look for duplicate */
dup = HnswFindDuplicate(element);
/* Write to disk */ /* Write to disk */
WriteElement(index, element, m, updates, dup, entryPoint); WriteElement(index, procinfo, collation, element, m, efConstruction, dup, entryPoint);
/* Release lock */
UnlockPage(index, HNSW_UPDATE_LOCK, lockmode);
return true; return true;
} }

View File

@@ -4,34 +4,35 @@
#include "hnsw.h" #include "hnsw.h"
#include "pgstat.h" #include "pgstat.h"
#include "storage/bufmgr.h" #include "storage/bufmgr.h"
#include "storage/lmgr.h"
#include "utils/memutils.h" #include "utils/memutils.h"
/* /*
* Algorithm 5 from paper * Algorithm 5 from paper
*/ */
static void static List *
GetScanItems(IndexScanDesc scan, Datum q) GetScanItems(IndexScanDesc scan, Datum q)
{ {
HnswScanOpaque so = (HnswScanOpaque) scan->opaque; HnswScanOpaque so = (HnswScanOpaque) scan->opaque;
Relation index = scan->indexRelation; Relation index = scan->indexRelation;
FmgrInfo *procinfo = so->procinfo; FmgrInfo *procinfo = so->procinfo;
Oid collation = so->collation; Oid collation = so->collation;
List *ep = NIL; List *ep;
List *w; List *w;
HnswElement entryPoint = HnswGetEntryPoint(index); HnswElement entryPoint = HnswGetEntryPoint(index);
if (entryPoint == NULL) if (entryPoint == NULL)
return; return NIL;
ep = lappend(ep, HnswEntryCandidate(entryPoint, q, index, procinfo, collation, false)); ep = list_make1(HnswEntryCandidate(entryPoint, q, index, procinfo, collation, false));
for (int lc = entryPoint->level; lc >= 1; lc--) for (int lc = entryPoint->level; lc >= 1; lc--)
{ {
w = HnswSearchLayer(q, ep, 1, lc, index, procinfo, collation, false, NULL, NULL); w = HnswSearchLayer(q, ep, 1, lc, index, procinfo, collation, false, NULL);
ep = w; ep = w;
} }
so->w = HnswSearchLayer(q, ep, hnsw_ef_search, 0, index, procinfo, collation, false, NULL, NULL); return HnswSearchLayer(q, ep, hnsw_ef_search, 0, index, procinfo, collation, false, NULL);
} }
/* /*
@@ -144,7 +145,17 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir)
HnswNormValue(so->normprocinfo, so->collation, &value, NULL); HnswNormValue(so->normprocinfo, so->collation, &value, NULL);
} }
GetScanItems(scan, value); /*
* Get a shared lock. This allows vacuum to ensure no in-flight scans
* before marking tuples as deleted.
*/
LockPage(scan->indexRelation, HNSW_SCAN_LOCK, ShareLock);
so->w = GetScanItems(scan, value);
/* Release shared lock */
UnlockPage(scan->indexRelation, HNSW_SCAN_LOCK, ShareLock);
so->first = false; so->first = false;
} }

View File

@@ -7,7 +7,7 @@
#include "vector.h" #include "vector.h"
/* /*
* Get the number of connection in the index * Get the max number of connections in an upper layer for each element in the index
*/ */
int int
HnswGetM(Relation index) HnswGetM(Relation index)
@@ -197,8 +197,8 @@ HnswAddHeapTid(HnswElement element, ItemPointer heaptid)
/* /*
* Allocate an element from block and offset numbers * Allocate an element from block and offset numbers
*/ */
static HnswElement HnswElement
InitElementFromBlock(BlockNumber blkno, OffsetNumber offno) HnswInitElementFromBlock(BlockNumber blkno, OffsetNumber offno)
{ {
HnswElement element = palloc(sizeof(HnswElementData)); HnswElement element = palloc(sizeof(HnswElementData));
@@ -226,7 +226,7 @@ HnswGetEntryPoint(Relation index)
metap = HnswPageGetMeta(page); metap = HnswPageGetMeta(page);
if (BlockNumberIsValid(metap->entryBlkno)) if (BlockNumberIsValid(metap->entryBlkno))
entryPoint = InitElementFromBlock(metap->entryBlkno, metap->entryOffno); entryPoint = HnswInitElementFromBlock(metap->entryBlkno, metap->entryOffno);
UnlockReleaseBuffer(buf); UnlockReleaseBuffer(buf);
@@ -234,22 +234,12 @@ HnswGetEntryPoint(Relation index)
} }
/* /*
* Update the metapage * Update the metapage info
*/ */
void static void
HnswUpdateMetaPage(Relation index, bool updateEntry, HnswElement entryPoint, BlockNumber insertPage, ForkNumber forkNum) HnswUpdateMetaPageInfo(Page page, int updateEntry, HnswElement entryPoint, BlockNumber insertPage)
{ {
Buffer buf; HnswMetaPage metap = HnswPageGetMeta(page);
Page page;
GenericXLogState *state;
HnswMetaPage metap;
buf = ReadBufferExtended(index, forkNum, HNSW_METAPAGE_BLKNO, RBM_NORMAL, NULL);
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
state = GenericXLogStart(index);
page = GenericXLogRegisterBuffer(state, buf, 0);
metap = HnswPageGetMeta(page);
if (updateEntry) if (updateEntry)
{ {
@@ -259,7 +249,7 @@ HnswUpdateMetaPage(Relation index, bool updateEntry, HnswElement entryPoint, Blo
metap->entryOffno = InvalidOffsetNumber; metap->entryOffno = InvalidOffsetNumber;
metap->entryLevel = -1; metap->entryLevel = -1;
} }
else else if (entryPoint->level > metap->entryLevel || updateEntry == HNSW_UPDATE_ENTRY_ALWAYS)
{ {
metap->entryBlkno = entryPoint->blkno; metap->entryBlkno = entryPoint->blkno;
metap->entryOffno = entryPoint->offno; metap->entryOffno = entryPoint->offno;
@@ -269,6 +259,24 @@ HnswUpdateMetaPage(Relation index, bool updateEntry, HnswElement entryPoint, Blo
if (BlockNumberIsValid(insertPage)) if (BlockNumberIsValid(insertPage))
metap->insertPage = insertPage; metap->insertPage = insertPage;
}
/*
* Update the metapage
*/
void
HnswUpdateMetaPage(Relation index, int updateEntry, HnswElement entryPoint, BlockNumber insertPage, ForkNumber forkNum)
{
Buffer buf;
Page page;
GenericXLogState *state;
buf = ReadBufferExtended(index, forkNum, HNSW_METAPAGE_BLKNO, RBM_NORMAL, NULL);
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
state = GenericXLogStart(index);
page = GenericXLogRegisterBuffer(state, buf, 0);
HnswUpdateMetaPageInfo(page, updateEntry, entryPoint, insertPage);
HnswCommitBuffer(buf, state); HnswCommitBuffer(buf, state);
} }
@@ -356,7 +364,7 @@ LoadNeighborsFromPage(HnswElement element, Relation index, Page page)
if (!ItemPointerIsValid(indextid)) if (!ItemPointerIsValid(indextid))
continue; continue;
e = InitElementFromBlock(ItemPointerGetBlockNumber(indextid), ItemPointerGetOffsetNumber(indextid)); e = HnswInitElementFromBlock(ItemPointerGetBlockNumber(indextid), ItemPointerGetOffsetNumber(indextid));
/* Calculate level based on offset */ /* Calculate level based on offset */
level = element->level - i / m; level = element->level - i / m;
@@ -372,8 +380,8 @@ LoadNeighborsFromPage(HnswElement element, Relation index, Page page)
/* /*
* Load neighbors * Load neighbors
*/ */
static void void
LoadNeighbors(HnswElement element, Relation index) HnswLoadNeighbors(HnswElement element, Relation index)
{ {
Buffer buf; Buffer buf;
Page page; Page page;
@@ -387,6 +395,37 @@ LoadNeighbors(HnswElement element, Relation index)
UnlockReleaseBuffer(buf); UnlockReleaseBuffer(buf);
} }
/*
* Load an element from a tuple
*/
void
HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool loadHeaptids, bool loadVec)
{
element->level = etup->level;
element->deleted = etup->deleted;
element->neighborPage = ItemPointerGetBlockNumber(&etup->neighbortid);
element->neighborOffno = ItemPointerGetOffsetNumber(&etup->neighbortid);
element->heaptids = NIL;
if (loadHeaptids)
{
for (int i = 0; i < HNSW_HEAPTIDS; i++)
{
/* Can stop at first invalid */
if (!ItemPointerIsValid(&etup->heaptids[i]))
break;
HnswAddHeapTid(element, &etup->heaptids[i]);
}
}
if (loadVec)
{
element->vec = palloc(VECTOR_SIZE(etup->vec.dim));
memcpy(element->vec, &etup->vec, VECTOR_SIZE(etup->vec.dim));
}
}
/* /*
* Load an element and optionally get its distance from q * Load an element and optionally get its distance from q
*/ */
@@ -407,25 +446,7 @@ HnswLoadElement(HnswElement element, float *distance, Datum *q, Relation index,
Assert(HnswIsElementTuple(etup)); Assert(HnswIsElementTuple(etup));
/* Load element */ /* Load element */
element->heaptids = NIL; HnswLoadElementFromTuple(element, etup, true, loadVec);
for (int i = 0; i < HNSW_HEAPTIDS; i++)
{
/* Can stop at first invalid */
if (!ItemPointerIsValid(&etup->heaptids[i]))
break;
HnswAddHeapTid(element, &etup->heaptids[i]);
}
element->level = etup->level;
element->neighborPage = ItemPointerGetBlockNumber(&etup->neighbortid);
element->neighborOffno = ItemPointerGetOffsetNumber(&etup->neighbortid);
element->deleted = etup->deleted;
if (loadVec)
{
element->vec = palloc(VECTOR_SIZE(etup->vec.dim));
memcpy(element->vec, &etup->vec, VECTOR_SIZE(etup->vec.dim));
}
/* Calculate distance */ /* Calculate distance */
if (distance != NULL) if (distance != NULL)
@@ -447,7 +468,7 @@ GetCandidateDistance(HnswCandidate * hc, Datum q, FmgrInfo *procinfo, Oid collat
* Create a candidate for the entry point * Create a candidate for the entry point
*/ */
HnswCandidate * HnswCandidate *
HnswEntryCandidate(HnswElement entryPoint, Datum q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadvec) HnswEntryCandidate(HnswElement entryPoint, Datum q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec)
{ {
HnswCandidate *hc = palloc(sizeof(HnswCandidate)); HnswCandidate *hc = palloc(sizeof(HnswCandidate));
@@ -455,7 +476,7 @@ HnswEntryCandidate(HnswElement entryPoint, Datum q, Relation index, FmgrInfo *pr
if (index == NULL) if (index == NULL)
hc->distance = GetCandidateDistance(hc, q, procinfo, collation); hc->distance = GetCandidateDistance(hc, q, procinfo, collation);
else else
HnswLoadElement(hc->element, &hc->distance, &q, index, procinfo, collation, loadvec); HnswLoadElement(hc->element, &hc->distance, &q, index, procinfo, collation, loadVec);
return hc; return hc;
} }
@@ -522,7 +543,7 @@ AddToVisited(HTAB *v, HnswCandidate * hc, Relation index, bool *found)
* Algorithm 2 from paper * Algorithm 2 from paper
*/ */
List * List *
HnswSearchLayer(Datum q, List *ep, int ef, int lc, Relation index, FmgrInfo *procinfo, Oid collation, bool inserting, BlockNumber *skipPage, OffsetNumber *skipOffno) HnswSearchLayer(Datum q, List *ep, int ef, int lc, Relation index, FmgrInfo *procinfo, Oid collation, bool inserting, HnswElement skipElement)
{ {
ListCell *lc2; ListCell *lc2;
@@ -558,7 +579,13 @@ HnswSearchLayer(Datum q, List *ep, int ef, int lc, Relation index, FmgrInfo *pro
pairingheap_add(C, &(CreatePairingHeapNode(hc)->ph_node)); pairingheap_add(C, &(CreatePairingHeapNode(hc)->ph_node));
pairingheap_add(W, &(CreatePairingHeapNode(hc)->ph_node)); pairingheap_add(W, &(CreatePairingHeapNode(hc)->ph_node));
wlen++; /*
* 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 (skipElement == NULL || list_length(hc->element->heaptids) != 0)
wlen++;
} }
while (!pairingheap_is_empty(C)) while (!pairingheap_is_empty(C))
@@ -571,7 +598,7 @@ HnswSearchLayer(Datum q, List *ep, int ef, int lc, Relation index, FmgrInfo *pro
break; break;
if (c->element->neighbors == NULL) if (c->element->neighbors == NULL)
LoadNeighbors(c->element, index); HnswLoadNeighbors(c->element, index);
/* Get the neighborhood at layer lc */ /* Get the neighborhood at layer lc */
neighborhood = &c->element->neighbors[lc]; neighborhood = &c->element->neighbors[lc];
@@ -594,16 +621,10 @@ HnswSearchLayer(Datum q, List *ep, int ef, int lc, Relation index, FmgrInfo *pro
else else
HnswLoadElement(e->element, &eDistance, &q, index, procinfo, collation, inserting); HnswLoadElement(e->element, &eDistance, &q, index, procinfo, collation, inserting);
/* Skip if fully deleted */ Assert(!e->element->deleted);
if (e->element->deleted)
continue;
/* Skip for inserts if deleting */
if (inserting && list_length(e->element->heaptids) == 0)
continue;
/* Skip self for vacuuming update */ /* Skip self for vacuuming update */
if (skipPage != NULL && e->element->neighborPage == *skipPage && e->element->neighborOffno == *skipOffno) if (skipElement != NULL && e->element->blkno == skipElement->blkno && e->element->offno == skipElement->offno)
continue; continue;
/* Make robust to issues */ /* Make robust to issues */
@@ -620,11 +641,20 @@ HnswSearchLayer(Datum q, List *ep, int ef, int lc, Relation index, FmgrInfo *pro
pairingheap_add(C, &(CreatePairingHeapNode(ec)->ph_node)); pairingheap_add(C, &(CreatePairingHeapNode(ec)->ph_node));
pairingheap_add(W, &(CreatePairingHeapNode(ec)->ph_node)); pairingheap_add(W, &(CreatePairingHeapNode(ec)->ph_node));
wlen++;
/* No need to decrement wlen */ /*
if (wlen > ef) * Do not count elements being deleted towards ef when
pairingheap_remove_first(W); * vacuuming. It would be ideal to do this for inserts as
* well, but this could affect insert performance.
*/
if (skipElement == NULL || list_length(e->element->heaptids) != 0)
{
wlen++;
/* No need to decrement wlen */
if (wlen > ef)
pairingheap_remove_first(W);
}
} }
} }
} }
@@ -703,7 +733,7 @@ SelectNeighbors(List *c, int m, int lc, FmgrInfo *procinfo, Oid collation, HnswC
List *w = list_copy(c); List *w = list_copy(c);
pairingheap *wd; pairingheap *wd;
if (list_length(w) < m) if (list_length(w) <= m)
return w; return w;
wd = pairingheap_allocate(CompareNearestCandidates, NULL); wd = pairingheap_allocate(CompareNearestCandidates, NULL);
@@ -743,14 +773,14 @@ SelectNeighbors(List *c, int m, int lc, FmgrInfo *procinfo, Oid collation, HnswC
/* /*
* Find duplicate element * Find duplicate element
*/ */
static HnswElement HnswElement
HnswFindDuplicate(HnswElement e, List *neighbors) HnswFindDuplicate(HnswElement e)
{ {
ListCell *lc; HnswNeighborArray *neighbors = &e->neighbors[0];
foreach(lc, neighbors) for (int i = 0; i < neighbors->length; i++)
{ {
HnswCandidate *neighbor = lfirst(lc); HnswCandidate *neighbor = &neighbors->items[i];
/* Exit early since ordered by distance */ /* Exit early since ordered by distance */
if (vector_cmp_internal(e->vec, neighbor->element->vec) != 0) if (vector_cmp_internal(e->vec, neighbor->element->vec) != 0)
@@ -799,144 +829,134 @@ CompareCandidateDistances(const void *a, const void *b)
return 0; return 0;
} }
/*
* Create update
*/
static HnswUpdate *
CreateUpdate(HnswCandidate * hc, int level, int index)
{
HnswUpdate *update = palloc(sizeof(HnswUpdate));
update->hc = *hc;
update->level = level;
update->index = index;
return update;
}
/* /*
* Update connections * Update connections
*/ */
static void void
UpdateConnections(HnswElement element, List *neighbors, int m, int lc, List **updates, Relation index, FmgrInfo *procinfo, Oid collation) HnswUpdateConnection(HnswElement element, HnswCandidate * hc, int m, int lc, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation)
{ {
ListCell *lc2; HnswNeighborArray *currentNeighbors = &hc->element->neighbors[lc];
foreach(lc2, neighbors) HnswCandidate hc2;
hc2.element = element;
hc2.distance = hc->distance;
if (currentNeighbors->length < m)
{ {
HnswCandidate *hc = (HnswCandidate *) lfirst(lc2); currentNeighbors->items[currentNeighbors->length++] = hc2;
HnswNeighborArray *currentNeighbors = &hc->element->neighbors[lc];
HnswCandidate hc2; /* Track update */
if (updateIdx != NULL)
*updateIdx = -2;
}
else
{
/* Shrink connections */
HnswCandidate *pruned = NULL;
hc2.element = element; /* Load elements on insert */
hc2.distance = hc->distance; if (index != NULL)
if (currentNeighbors->length < m)
{ {
currentNeighbors->items[currentNeighbors->length++] = hc2; Datum q = PointerGetDatum(hc->element->vec);
/* Track updates */
if (updates != NULL)
*updates = lappend(*updates, CreateUpdate(hc, lc, currentNeighbors->length - 1));
}
else
{
/* Shrink connections */
HnswCandidate *pruned = NULL;
List *c = NIL;
/* Load elements on insert */
if (index != NULL)
{
Datum q = PointerGetDatum(hc->element->vec);
for (int i = 0; i < currentNeighbors->length; i++)
{
HnswCandidate *hc3 = &currentNeighbors->items[i];
if (hc3->element->vec == NULL)
HnswLoadElement(hc3->element, &hc3->distance, &q, index, procinfo, collation, true);
else
hc3->distance = GetCandidateDistance(hc3, q, procinfo, collation);
/* Prune element if being deleted */
if (list_length(hc3->element->heaptids) == 0)
{
pruned = &currentNeighbors->items[i];
break;
}
}
}
if (pruned == NULL)
{
/* Add and sort candidates */
for (int i = 0; i < currentNeighbors->length; i++)
c = lappend(c, &currentNeighbors->items[i]);
c = lappend(c, &hc2);
list_sort(c, CompareCandidateDistances);
SelectNeighbors(c, m, lc, procinfo, collation, &pruned);
/* Should not happen */
if (pruned == NULL)
continue;
}
/* Find and replace the pruned element */
for (int i = 0; i < currentNeighbors->length; i++) for (int i = 0; i < currentNeighbors->length; i++)
{ {
if (currentNeighbors->items[i].element == pruned->element) HnswCandidate *hc3 = &currentNeighbors->items[i];
if (hc3->element->vec == NULL)
HnswLoadElement(hc3->element, &hc3->distance, &q, index, procinfo, collation, true);
else
hc3->distance = GetCandidateDistance(hc3, q, procinfo, collation);
/* Prune element if being deleted */
if (list_length(hc3->element->heaptids) == 0)
{ {
currentNeighbors->items[i] = hc2; pruned = &currentNeighbors->items[i];
/* Track updates */
if (updates != NULL)
*updates = lappend(*updates, CreateUpdate(hc, lc, i));
break; break;
} }
} }
} }
if (pruned == NULL)
{
List *c = NIL;
/* Add and sort candidates */
for (int i = 0; i < currentNeighbors->length; i++)
c = lappend(c, &currentNeighbors->items[i]);
c = lappend(c, &hc2);
list_sort(c, CompareCandidateDistances);
SelectNeighbors(c, m, lc, procinfo, collation, &pruned);
/* Should not happen */
if (pruned == NULL)
return;
}
/* Find and replace the pruned element */
for (int i = 0; i < currentNeighbors->length; i++)
{
if (currentNeighbors->items[i].element == pruned->element)
{
currentNeighbors->items[i] = hc2;
/* Track update */
if (updateIdx != NULL)
*updateIdx = i;
break;
}
}
} }
} }
/*
* Remove elements being deleted
*/
static List *
RemoveElementsBeingDeleted(List *w)
{
ListCell *lc2;
List *w2 = NIL;
foreach(lc2, w)
{
HnswCandidate *hc = (HnswCandidate *) lfirst(lc2);
if (list_length(hc->element->heaptids) != 0)
w2 = lappend(w2, hc);
}
return w2;
}
/* /*
* Algorithm 1 from paper * Algorithm 1 from paper
*/ */
HnswElement void
HnswInsertElement(HnswElement element, HnswElement entryPoint, Relation index, FmgrInfo *procinfo, Oid collation, int m, int efConstruction, List **updates, bool vacuuming) HnswInsertElement(HnswElement element, HnswElement entryPoint, Relation index, FmgrInfo *procinfo, Oid collation, int m, int efConstruction, bool existing)
{ {
List *ep = NIL; List *ep;
List *w; List *w;
int level = element->level; int level = element->level;
int entryLevel; int entryLevel;
List **newNeighbors = palloc(sizeof(List *) * (level + 1));
Datum q = PointerGetDatum(element->vec); Datum q = PointerGetDatum(element->vec);
HnswElement dup; HnswElement skipElement = existing ? element : NULL;
BlockNumber *skipPage = vacuuming ? &element->neighborPage : NULL;
OffsetNumber *skipOffno = vacuuming ? &element->neighborOffno : NULL; /* No neighbors if no entry point */
bool removeEntryPoint; if (entryPoint == NULL)
HnswCandidate *entryCandidate; return;
/* Get entry point and level */ /* Get entry point and level */
if (entryPoint != NULL) ep = list_make1(HnswEntryCandidate(entryPoint, q, index, procinfo, collation, true));
{ entryLevel = entryPoint->level;
entryCandidate = HnswEntryCandidate(entryPoint, q, index, procinfo, collation, true);
ep = lappend(ep, entryCandidate);
entryLevel = entryPoint->level;
removeEntryPoint = vacuuming && list_length(entryPoint->heaptids) == 0;
}
else
{
entryLevel = -1;
removeEntryPoint = false;
}
/* 1st phase: greedy search to insert level */ /* 1st phase: greedy search to insert level */
for (int lc = entryLevel; lc >= level + 1; lc--) for (int lc = entryLevel; lc >= level + 1; lc--)
{ {
w = HnswSearchLayer(q, ep, 1, lc, index, procinfo, collation, true, skipPage, skipOffno); w = HnswSearchLayer(q, ep, 1, lc, index, procinfo, collation, true, skipElement);
ep = w; ep = w;
} }
@@ -947,35 +967,19 @@ HnswInsertElement(HnswElement element, HnswElement entryPoint, Relation index, F
for (int lc = level; lc >= 0; lc--) for (int lc = level; lc >= 0; lc--)
{ {
int lm = HnswGetLayerM(m, lc); int lm = HnswGetLayerM(m, lc);
List *neighbors;
w = HnswSearchLayer(q, ep, efConstruction, lc, index, procinfo, collation, true, skipPage, skipOffno); w = HnswSearchLayer(q, ep, efConstruction, lc, index, procinfo, collation, true, skipElement);
/* Remove entry point if it's being deleted */ /* Elements being deleted can help with search */
if (removeEntryPoint) /* but should be removed before selecting neighbors */
w = list_delete_ptr(w, entryCandidate); if (index != NULL)
w = RemoveElementsBeingDeleted(w);
neighbors = SelectNeighbors(w, lm, lc, procinfo, collation, NULL);
AddConnections(element, neighbors, lm, lc);
newNeighbors[lc] = SelectNeighbors(w, lm, lc, procinfo, collation, NULL);
ep = w; ep = w;
} }
/* Look for duplicate */
if (level >= 0 && !vacuuming)
{
dup = HnswFindDuplicate(element, newNeighbors[0]);
if (dup != NULL)
return dup;
}
/* Update connections */
for (int lc = level; lc >= 0; lc--)
{
int lm = HnswGetLayerM(m, lc);
AddConnections(element, newNeighbors[lc], lm, lc);
if (!vacuuming)
UpdateConnections(element, newNeighbors[lc], lm, lc, updates, index, procinfo, collation);
}
return NULL;
} }

View File

@@ -5,10 +5,11 @@
#include "commands/vacuum.h" #include "commands/vacuum.h"
#include "hnsw.h" #include "hnsw.h"
#include "storage/bufmgr.h" #include "storage/bufmgr.h"
#include "storage/lmgr.h"
#include "utils/memutils.h" #include "utils/memutils.h"
/* /*
* Check if deleted list contains an index tid * Check if deleted list contains an index TID
*/ */
static bool static bool
DeletedContains(HTAB *deleted, ItemPointer indextid) DeletedContains(HTAB *deleted, ItemPointer indextid)
@@ -32,6 +33,7 @@ RemoveHeapTids(HnswVacuumState * vacuumstate)
Relation index = vacuumstate->index; Relation index = vacuumstate->index;
BufferAccessStrategy bas = vacuumstate->bas; BufferAccessStrategy bas = vacuumstate->bas;
HnswElement entryPoint = HnswGetEntryPoint(vacuumstate->index); HnswElement entryPoint = HnswGetEntryPoint(vacuumstate->index);
IndexBulkDeleteResult *stats = vacuumstate->stats;
/* Store separately since highestPoint.level is uint8 */ /* Store separately since highestPoint.level is uint8 */
int highestLevel = -1; int highestLevel = -1;
@@ -77,11 +79,15 @@ RemoveHeapTids(HnswVacuumState * vacuumstate)
break; break;
if (vacuumstate->callback(&etup->heaptids[i], vacuumstate->callback_state)) if (vacuumstate->callback(&etup->heaptids[i], vacuumstate->callback_state))
{
itemUpdated = true; itemUpdated = true;
stats->tuples_removed++;
}
else else
{ {
/* Move to front of list */ /* Move to front of list */
etup->heaptids[idx++] = etup->heaptids[i]; etup->heaptids[idx++] = etup->heaptids[i];
stats->num_index_tuples++;
} }
} }
@@ -109,7 +115,7 @@ RemoveHeapTids(HnswVacuumState * vacuumstate)
(void) hash_search(vacuumstate->deleted, &ip, HASH_ENTER, NULL); (void) hash_search(vacuumstate->deleted, &ip, HASH_ENTER, NULL);
} }
else if (etup->level > highestLevel && !(blkno == entryPoint->blkno && offno == entryPoint->offno)) else if (etup->level > highestLevel && !(entryPoint != NULL && blkno == entryPoint->blkno && offno == entryPoint->offno))
{ {
/* Keep track of highest non-entry point */ /* Keep track of highest non-entry point */
highestPoint->blkno = blkno; highestPoint->blkno = blkno;
@@ -169,6 +175,11 @@ NeedsUpdated(HnswVacuumState * vacuumstate, HnswElement element)
} }
} }
/* Also update if layer 0 is not full */
/* This could indicate too many candidates being deleted during insert */
if (!needsUpdated)
needsUpdated = !ItemPointerIsValid(&ntup->indextids[ntup->count - 1]);
UnlockReleaseBuffer(buf); UnlockReleaseBuffer(buf);
return needsUpdated; return needsUpdated;
@@ -178,7 +189,7 @@ NeedsUpdated(HnswVacuumState * vacuumstate, HnswElement element)
* Repair graph for a single element * Repair graph for a single element
*/ */
static void static void
RepairGraphElement(HnswVacuumState * vacuumstate, HnswElement element) RepairGraphElement(HnswVacuumState * vacuumstate, HnswElement element, HnswElement entryPoint)
{ {
Relation index = vacuumstate->index; Relation index = vacuumstate->index;
Buffer buf; Buffer buf;
@@ -188,42 +199,20 @@ RepairGraphElement(HnswVacuumState * vacuumstate, HnswElement element)
int efConstruction = vacuumstate->efConstruction; int efConstruction = vacuumstate->efConstruction;
FmgrInfo *procinfo = vacuumstate->procinfo; FmgrInfo *procinfo = vacuumstate->procinfo;
Oid collation = vacuumstate->collation; Oid collation = vacuumstate->collation;
HnswElement entryPoint;
BufferAccessStrategy bas = vacuumstate->bas; BufferAccessStrategy bas = vacuumstate->bas;
HnswNeighborTuple ntup = vacuumstate->ntup; HnswNeighborTuple ntup = vacuumstate->ntup;
Size ntupSize = HNSW_NEIGHBOR_TUPLE_SIZE(element->level, m); Size ntupSize = HNSW_NEIGHBOR_TUPLE_SIZE(element->level, m);
/* Check if any neighbors point to deleted values */ /* Skip if element is entry point */
if (!NeedsUpdated(vacuumstate, element)) if (entryPoint != NULL && element->blkno == entryPoint->blkno && element->offno == entryPoint->offno)
return; return;
/* Refresh entry point for each element */
entryPoint = HnswGetEntryPoint(index);
/* Special case for entry point */
if (element->blkno == entryPoint->blkno && element->offno == entryPoint->offno)
{
if (BlockNumberIsValid(vacuumstate->highestPoint.blkno))
{
/* Already updated */
if (vacuumstate->highestPoint.blkno == element->blkno && vacuumstate->highestPoint.offno == element->offno)
return;
entryPoint = &vacuumstate->highestPoint;
/* Reset neighbors from previous update */
entryPoint->neighbors = NULL;
}
else
entryPoint = NULL;
}
/* Init fields */ /* Init fields */
HnswInitNeighbors(element, m); HnswInitNeighbors(element, m);
element->heaptids = NIL; element->heaptids = NIL;
/* Add element to graph, skipping itself */ /* Add element to graph, skipping itself */
HnswInsertElement(element, entryPoint, index, procinfo, collation, m, efConstruction, NULL, true); HnswInsertElement(element, entryPoint, index, procinfo, collation, m, efConstruction, true);
/* Update neighbor tuple */ /* Update neighbor tuple */
/* Do this before getting page to minimize locking */ /* Do this before getting page to minimize locking */
@@ -243,6 +232,9 @@ RepairGraphElement(HnswVacuumState * vacuumstate, HnswElement element)
MarkBufferDirty(buf); MarkBufferDirty(buf);
GenericXLogFinish(state); GenericXLogFinish(state);
UnlockReleaseBuffer(buf); UnlockReleaseBuffer(buf);
/* Update neighbors */
HnswUpdateNeighborPages(index, procinfo, collation, element, m, true);
} }
/* /*
@@ -256,16 +248,35 @@ RepairGraphEntryPoint(HnswVacuumState * vacuumstate)
HnswElement entryPoint; HnswElement entryPoint;
MemoryContext oldCtx = MemoryContextSwitchTo(vacuumstate->tmpCtx); MemoryContext oldCtx = MemoryContextSwitchTo(vacuumstate->tmpCtx);
/* Repair graph for highest non-entry point */ if (!BlockNumberIsValid(highestPoint->blkno))
/* This may not be the highest with new inserts, but should be fine */ highestPoint = NULL;
if (BlockNumberIsValid(highestPoint->blkno))
/*
* Repair graph for highest non-entry point. Highest point may be outdated
* due to inserts that happen during and after RemoveHeapTids.
*/
if (highestPoint != NULL)
{ {
/* Get a shared lock */
LockPage(index, HNSW_UPDATE_LOCK, ShareLock);
/* Load element */
HnswLoadElement(highestPoint, NULL, NULL, index, vacuumstate->procinfo, vacuumstate->collation, true); HnswLoadElement(highestPoint, NULL, NULL, index, vacuumstate->procinfo, vacuumstate->collation, true);
RepairGraphElement(vacuumstate, highestPoint);
/* Repair if needed */
if (NeedsUpdated(vacuumstate, highestPoint))
RepairGraphElement(vacuumstate, highestPoint, HnswGetEntryPoint(index));
/* Release lock */
UnlockPage(index, HNSW_UPDATE_LOCK, ShareLock);
} }
/* See if entry point needs updated */ /* Prevent concurrent inserts when possibly updating entry point */
LockPage(index, HNSW_UPDATE_LOCK, ExclusiveLock);
/* Get latest entry point */
entryPoint = HnswGetEntryPoint(index); entryPoint = HnswGetEntryPoint(index);
if (entryPoint != NULL) if (entryPoint != NULL)
{ {
ItemPointerData epData; ItemPointerData epData;
@@ -273,15 +284,37 @@ RepairGraphEntryPoint(HnswVacuumState * vacuumstate)
ItemPointerSet(&epData, entryPoint->blkno, entryPoint->offno); ItemPointerSet(&epData, entryPoint->blkno, entryPoint->offno);
if (DeletedContains(vacuumstate->deleted, &epData)) if (DeletedContains(vacuumstate->deleted, &epData))
HnswUpdateMetaPage(index, true, highestPoint, InvalidBlockNumber, MAIN_FORKNUM); {
/*
* Replace the entry point with the highest point. If highest
* point is outdated and empty, the entry point will be empty
* until an element is repaired.
*/
HnswUpdateMetaPage(index, HNSW_UPDATE_ENTRY_ALWAYS, highestPoint, InvalidBlockNumber, MAIN_FORKNUM);
}
else else
{ {
/* Highest point will be used to repair */ /*
* Repair the entry point with the highest point. If highest point
* 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); HnswLoadElement(entryPoint, NULL, NULL, index, vacuumstate->procinfo, vacuumstate->collation, true);
RepairGraphElement(vacuumstate, entryPoint);
if (NeedsUpdated(vacuumstate, entryPoint))
{
/* Reset neighbors from previous update */
if (highestPoint != NULL)
highestPoint->neighbors = NULL;
RepairGraphElement(vacuumstate, entryPoint, highestPoint);
}
} }
} }
/* Release lock */
UnlockPage(index, HNSW_UPDATE_LOCK, ExclusiveLock);
/* Reset memory context */ /* Reset memory context */
MemoryContextSwitchTo(oldCtx); MemoryContextSwitchTo(oldCtx);
MemoryContextReset(vacuumstate->tmpCtx); MemoryContextReset(vacuumstate->tmpCtx);
@@ -297,6 +330,11 @@ RepairGraph(HnswVacuumState * vacuumstate)
BufferAccessStrategy bas = vacuumstate->bas; BufferAccessStrategy bas = vacuumstate->bas;
BlockNumber blkno = HNSW_HEAD_BLKNO; BlockNumber blkno = HNSW_HEAD_BLKNO;
/* Wait for inserts to complete */
LockPage(index, HNSW_UPDATE_LOCK, ExclusiveLock);
UnlockPage(index, HNSW_UPDATE_LOCK, ExclusiveLock);
/* Repair entry point first */
RepairGraphEntryPoint(vacuumstate); RepairGraphEntryPoint(vacuumstate);
while (BlockNumberIsValid(blkno)) while (BlockNumberIsValid(blkno))
@@ -333,14 +371,8 @@ RepairGraph(HnswVacuumState * vacuumstate)
continue; continue;
/* Create an element */ /* Create an element */
element = palloc(sizeof(HnswElementData)); element = HnswInitElementFromBlock(blkno, offno);
element->neighborPage = ItemPointerGetBlockNumber(&etup->neighbortid); HnswLoadElementFromTuple(element, etup, false, true);
element->neighborOffno = ItemPointerGetOffsetNumber(&etup->neighbortid);
element->level = etup->level;
element->blkno = blkno;
element->offno = offno;
element->vec = palloc(VECTOR_SIZE(etup->vec.dim));
memcpy(element->vec, &etup->vec, VECTOR_SIZE(etup->vec.dim));
elements = lappend(elements, element); elements = lappend(elements, element);
} }
@@ -351,7 +383,48 @@ RepairGraph(HnswVacuumState * vacuumstate)
/* Update neighbor pages */ /* Update neighbor pages */
foreach(lc2, elements) foreach(lc2, elements)
RepairGraphElement(vacuumstate, (HnswElement) lfirst(lc2)); {
HnswElement element = (HnswElement) lfirst(lc2);
HnswElement entryPoint;
LOCKMODE lockmode = ShareLock;
/* Check if any neighbors point to deleted values */
if (!NeedsUpdated(vacuumstate, element))
continue;
/* Get a shared lock */
LockPage(index, HNSW_UPDATE_LOCK, lockmode);
/* Refresh entry point for each element */
entryPoint = HnswGetEntryPoint(index);
/* Prevent concurrent inserts when likely updating entry point */
if (entryPoint == NULL || element->level > entryPoint->level)
{
/* Release shared lock */
UnlockPage(index, HNSW_UPDATE_LOCK, lockmode);
/* Get exclusive lock */
lockmode = ExclusiveLock;
LockPage(index, HNSW_UPDATE_LOCK, lockmode);
/* Get latest entry point after lock is acquired */
entryPoint = HnswGetEntryPoint(index);
}
/* Repair connections */
RepairGraphElement(vacuumstate, element, entryPoint);
/*
* Update metapage if needed. Should only happen if entry point
* was replaced and highest point was outdated.
*/
if (entryPoint == NULL || element->level > entryPoint->level)
HnswUpdateMetaPage(index, HNSW_UPDATE_ENTRY_GREATER, element, InvalidBlockNumber, MAIN_FORKNUM);
/* Release lock */
UnlockPage(index, HNSW_UPDATE_LOCK, lockmode);
}
/* Reset memory context */ /* Reset memory context */
MemoryContextSwitchTo(oldCtx); MemoryContextSwitchTo(oldCtx);
@@ -369,7 +442,10 @@ MarkDeleted(HnswVacuumState * vacuumstate)
BlockNumber insertPage = InvalidBlockNumber; BlockNumber insertPage = InvalidBlockNumber;
Relation index = vacuumstate->index; Relation index = vacuumstate->index;
BufferAccessStrategy bas = vacuumstate->bas; BufferAccessStrategy bas = vacuumstate->bas;
IndexBulkDeleteResult *stats = vacuumstate->stats;
/* Wait for selects to complete */
LockPage(index, HNSW_SCAN_LOCK, ExclusiveLock);
UnlockPage(index, HNSW_SCAN_LOCK, ExclusiveLock);
while (BlockNumberIsValid(blkno)) while (BlockNumberIsValid(blkno))
{ {
@@ -413,17 +489,17 @@ MarkDeleted(HnswVacuumState * vacuumstate)
/* Skip deleted tuples */ /* Skip deleted tuples */
if (etup->deleted) if (etup->deleted)
continue;
/* Skip live tuples */
if (ItemPointerIsValid(&etup->heaptids[0]))
{ {
stats->num_index_tuples++; /* Set to first free page */
if (!BlockNumberIsValid(insertPage))
insertPage = blkno;
continue; continue;
} }
/* Update stats */ /* Skip live tuples */
stats->tuples_removed++; if (ItemPointerIsValid(&etup->heaptids[0]))
continue;
/* Calculate sizes */ /* Calculate sizes */
etupSize = HNSW_ELEMENT_TUPLE_SIZE(etup->vec.dim); etupSize = HNSW_ELEMENT_TUPLE_SIZE(etup->vec.dim);
@@ -486,7 +562,8 @@ MarkDeleted(HnswVacuumState * vacuumstate)
UnlockReleaseBuffer(buf); UnlockReleaseBuffer(buf);
} }
HnswUpdateMetaPage(index, false, NULL, insertPage, MAIN_FORKNUM); /* Update insert page last, after everything has been marked as deleted */
HnswUpdateMetaPage(index, 0, NULL, insertPage, MAIN_FORKNUM);
} }
/* /*
@@ -534,6 +611,67 @@ FreeVacuumState(HnswVacuumState * vacuumstate)
MemoryContextDelete(vacuumstate->tmpCtx); MemoryContextDelete(vacuumstate->tmpCtx);
} }
/*
* Print graph
*/
#ifdef HNSW_DEBUG
static void
PrintGraph(HnswVacuumState * vacuumstate)
{
BlockNumber blkno = HNSW_HEAD_BLKNO;
Relation index = vacuumstate->index;
while (BlockNumberIsValid(blkno))
{
Buffer buf;
Page page;
OffsetNumber offno;
OffsetNumber maxoffno;
buf = ReadBuffer(index, blkno);
LockBuffer(buf, BUFFER_LOCK_SHARE);
page = BufferGetPage(buf);
maxoffno = PageGetMaxOffsetNumber(page);
for (offno = FirstOffsetNumber; offno <= maxoffno; offno = OffsetNumberNext(offno))
{
HnswElementTuple etup = (HnswElementTuple) PageGetItem(page, PageGetItemId(page, offno));
HnswElement element;
/* Skip neighbor tuples */
if (!HnswIsElementTuple(etup))
continue;
/* Skip deleted tuples */
if (etup->deleted)
continue;
element = HnswInitElementFromBlock(blkno, offno);
HnswLoadElementFromTuple(element, etup, false, true);
HnswLoadNeighbors(element, index);
elog(INFO, "element (%d,%d)", element->blkno, element->offno);
for (int lc = element->level; lc >= 0; lc--)
{
HnswNeighborArray *neighbors = &element->neighbors[lc];
for (int i = 0; i < neighbors->length; i++)
{
HnswElement e = neighbors->items[i].element;
elog(INFO, "%d: (%d,%d)", lc, e->blkno, e->offno);
}
}
}
blkno = HnswPageGetOpaque(page)->nextblkno;
UnlockReleaseBuffer(buf);
}
}
#endif
/* /*
* Bulk delete tuples from the index * Bulk delete tuples from the index
*/ */
@@ -554,6 +692,10 @@ hnswbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
/* Pass 3: Mark as deleted */ /* Pass 3: Mark as deleted */
MarkDeleted(&vacuumstate); MarkDeleted(&vacuumstate);
#ifdef HNSW_DEBUG
PrintGraph(&vacuumstate);
#endif
FreeVacuumState(&vacuumstate); FreeVacuumState(&vacuumstate);
return vacuumstate.stats; return vacuumstate.stats;

View File

@@ -24,7 +24,7 @@ IvfflatInit(void)
{ {
ivfflat_relopt_kind = add_reloption_kind(); ivfflat_relopt_kind = add_reloption_kind();
add_int_reloption(ivfflat_relopt_kind, "lists", "Number of inverted lists", add_int_reloption(ivfflat_relopt_kind, "lists", "Number of inverted lists",
IVFFLAT_DEFAULT_LISTS, 1, IVFFLAT_MAX_LISTS IVFFLAT_DEFAULT_LISTS, IVFFLAT_MIN_LISTS, IVFFLAT_MAX_LISTS
#if PG_VERSION_NUM >= 130000 #if PG_VERSION_NUM >= 130000
,AccessExclusiveLock ,AccessExclusiveLock
#endif #endif
@@ -32,7 +32,7 @@ IvfflatInit(void)
DefineCustomIntVariable("ivfflat.probes", "Sets the number of probes", DefineCustomIntVariable("ivfflat.probes", "Sets the number of probes",
"Valid range is 1..lists.", &ivfflat_probes, "Valid range is 1..lists.", &ivfflat_probes,
1, 1, IVFFLAT_MAX_LISTS, PGC_USERSET, 0, NULL, NULL, NULL); IVFFLAT_DEFAULT_PROBES, IVFFLAT_MIN_LISTS, IVFFLAT_MAX_LISTS, PGC_USERSET, 0, NULL, NULL, NULL);
} }
/* /*

View File

@@ -40,8 +40,11 @@
#define IVFFLAT_METAPAGE_BLKNO 0 #define IVFFLAT_METAPAGE_BLKNO 0
#define IVFFLAT_HEAD_BLKNO 1 /* first list page */ #define IVFFLAT_HEAD_BLKNO 1 /* first list page */
/* IVFFlat parameters */
#define IVFFLAT_DEFAULT_LISTS 100 #define IVFFLAT_DEFAULT_LISTS 100
#define IVFFLAT_MIN_LISTS 1
#define IVFFLAT_MAX_LISTS 32768 #define IVFFLAT_MAX_LISTS 32768
#define IVFFLAT_DEFAULT_PROBES 1
/* Build phases */ /* Build phases */
/* PROGRESS_CREATEIDX_SUBPHASE_INITIALIZE is 1 */ /* PROGRESS_CREATEIDX_SUBPHASE_INITIALIZE is 1 */

View File

@@ -15,6 +15,10 @@
#include "utils/numeric.h" #include "utils/numeric.h"
#include "vector.h" #include "vector.h"
#if PG_VERSION_NUM >= 160000
#include "varatt.h"
#endif
#if PG_VERSION_NUM >= 120000 #if PG_VERSION_NUM >= 120000
#include "common/shortest_dec.h" #include "common/shortest_dec.h"
#include "utils/float.h" #include "utils/float.h"
@@ -35,6 +39,7 @@ PG_MODULE_MAGIC;
/* /*
* Initialize index options and variables * Initialize index options and variables
*/ */
PGDLLEXPORT void _PG_init(void);
void void
_PG_init(void) _PG_init(void)
{ {
@@ -100,6 +105,23 @@ CheckElement(float value)
errmsg("infinite value not allowed in vector"))); errmsg("infinite value not allowed in vector")));
} }
/*
* Allocate and initialize a new vector
*/
Vector *
InitVector(int dim)
{
Vector *result;
int size;
size = VECTOR_SIZE(dim);
result = (Vector *) palloc0(size);
SET_VARSIZE(result, size);
result->dim = dim;
return result;
}
/* /*
* Check for whitespace, since array_isspace() is static * Check for whitespace, since array_isspace() is static
*/ */
@@ -733,55 +755,16 @@ vector_dims(PG_FUNCTION_ARGS)
PGDLLEXPORT PG_FUNCTION_INFO_V1(vector_norm); PGDLLEXPORT PG_FUNCTION_INFO_V1(vector_norm);
Datum Datum
vector_norm(PG_FUNCTION_ARGS) vector_norm(PG_FUNCTION_ARGS)
{
Vector *a = PG_GETARG_VECTOR_P(0);
float *ax = a->x;
float norm = 0.0;
/* Auto-vectorized */
for (int i = 0; i < a->dim; i++)
norm += ax[i] * ax[i];
PG_RETURN_FLOAT8(sqrt((double) norm));
}
/*
* Normalize a vector with the L2 norm
*/
PGDLLEXPORT PG_FUNCTION_INFO_V1(normalize_l2);
Datum
normalize_l2(PG_FUNCTION_ARGS)
{ {
Vector *a = PG_GETARG_VECTOR_P(0); Vector *a = PG_GETARG_VECTOR_P(0);
float *ax = a->x; float *ax = a->x;
double norm = 0.0; double norm = 0.0;
Vector *result;
float *rx;
result = InitVector(a->dim);
rx = result->x;
/* Auto-vectorized */ /* Auto-vectorized */
for (int i = 0; i < a->dim; i++) for (int i = 0; i < a->dim; i++)
norm += (double) ax[i] * (double) ax[i]; norm += (double) ax[i] * (double) ax[i];
norm = sqrt(norm); PG_RETURN_FLOAT8(sqrt(norm));
if (norm > 0)
{
/* Auto-vectorized */
for (int i = 0, imax = a->dim; i < imax; i++)
rx[i] = ax[i] / norm;
/* Check for overflow */
for (int i = 0, imax = a->dim; i < imax; i++)
{
if (isinf(rx[i]))
float_overflow_error();
}
}
PG_RETURN_POINTER(result);
} }
/* /*

View File

@@ -1,12 +1,6 @@
#ifndef VECTOR_H #ifndef VECTOR_H
#define VECTOR_H #define VECTOR_H
#include "postgres.h"
#if PG_VERSION_NUM >= 160000
#include "varatt.h"
#endif
#define VECTOR_MAX_DIM 16000 #define VECTOR_MAX_DIM 16000
#define VECTOR_SIZE(_dim) (offsetof(Vector, x) + sizeof(float)*(_dim)) #define VECTOR_SIZE(_dim) (offsetof(Vector, x) + sizeof(float)*(_dim))
@@ -14,9 +8,6 @@
#define PG_GETARG_VECTOR_P(x) DatumGetVector(PG_GETARG_DATUM(x)) #define PG_GETARG_VECTOR_P(x) DatumGetVector(PG_GETARG_DATUM(x))
#define PG_RETURN_VECTOR_P(x) PG_RETURN_POINTER(x) #define PG_RETURN_VECTOR_P(x) PG_RETURN_POINTER(x)
/* Exported functions */
PGDLLEXPORT void _PG_init(void);
typedef struct Vector typedef struct Vector
{ {
int32 vl_len_; /* varlena header (do not touch directly!) */ int32 vl_len_; /* varlena header (do not touch directly!) */
@@ -25,24 +16,8 @@ typedef struct Vector
float x[FLEXIBLE_ARRAY_MEMBER]; float x[FLEXIBLE_ARRAY_MEMBER];
} Vector; } Vector;
Vector *InitVector(int dim);
void PrintVector(char *msg, Vector * vector); void PrintVector(char *msg, Vector * vector);
int vector_cmp_internal(Vector * a, Vector * b); int vector_cmp_internal(Vector * a, Vector * b);
/*
* Allocate and initialize a new vector
*/
static inline Vector *
InitVector(int dim)
{
Vector *result;
int size;
size = VECTOR_SIZE(dim);
result = (Vector *) palloc0(size);
SET_VARSIZE(result, size);
result->dim = dim;
return result;
}
#endif #endif

View File

@@ -48,34 +48,10 @@ SELECT vector_norm('[0,1]');
1 1
(1 row) (1 row)
SELECT normalize_l2('[3,4]'); SELECT vector_norm('[3e37,4e37]')::real;
normalize_l2 vector_norm
-------------- -------------
[0.6,0.8] 5e+37
(1 row)
SELECT normalize_l2('[3,0]');
normalize_l2
--------------
[1,0]
(1 row)
SELECT normalize_l2('[0,0.1]');
normalize_l2
--------------
[0,1]
(1 row)
SELECT normalize_l2('[0,0]');
normalize_l2
--------------
[0,0]
(1 row)
SELECT normalize_l2('[3e38]');
normalize_l2
--------------
[1]
(1 row) (1 row)
SELECT l2_distance('[0,0]', '[3,4]'); SELECT l2_distance('[0,0]', '[3,4]');

View File

@@ -27,4 +27,10 @@ SELECT COUNT(*) FROM t;
5 5
(1 row) (1 row)
TRUNCATE t;
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
val
-----
(0 rows)
DROP TABLE t; DROP TABLE t;

View File

@@ -1,25 +1,26 @@
SET enable_seqscan = off;
CREATE TABLE t (val vector(3)); CREATE TABLE t (val vector(3));
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (m = 3); CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (m = 1);
ERROR: value 3 out of bounds for option "m" ERROR: value 1 out of bounds for option "m"
DETAIL: Valid values are between "4" and "100". DETAIL: Valid values are between "2" and "100".
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (m = 101); CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (m = 101);
ERROR: value 101 out of bounds for option "m" ERROR: value 101 out of bounds for option "m"
DETAIL: Valid values are between "4" and "100". DETAIL: Valid values are between "2" and "100".
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (ef_construction = 9); CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (ef_construction = 3);
ERROR: value 9 out of bounds for option "ef_construction" ERROR: value 3 out of bounds for option "ef_construction"
DETAIL: Valid values are between "10" and "1000". DETAIL: Valid values are between "4" and "1000".
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (ef_construction = 1001); CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (ef_construction = 1001);
ERROR: value 1001 out of bounds for option "ef_construction" ERROR: value 1001 out of bounds for option "ef_construction"
DETAIL: Valid values are between "10" and "1000". DETAIL: Valid values are between "4" and "1000".
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (m = 16, ef_construction = 31);
ERROR: ef_construction must be greater than or equal to 2 * m
SHOW hnsw.ef_search; SHOW hnsw.ef_search;
hnsw.ef_search hnsw.ef_search
---------------- ----------------
40 40
(1 row) (1 row)
SET hnsw.ef_search = 9; SET hnsw.ef_search = 0;
ERROR: 9 is outside the valid range for parameter "hnsw.ef_search" (10 .. 1000) ERROR: 0 is outside the valid range for parameter "hnsw.ef_search" (1 .. 1000)
SET hnsw.ef_search = 1001; SET hnsw.ef_search = 1001;
ERROR: 1001 is outside the valid range for parameter "hnsw.ef_search" (10 .. 1000) ERROR: 1001 is outside the valid range for parameter "hnsw.ef_search" (1 .. 1000)
DROP TABLE t; DROP TABLE t;

View File

@@ -1,7 +1,7 @@
SET enable_seqscan = off; SET enable_seqscan = off;
CREATE TABLE t (val vector(3)); CREATE TABLE t (val vector(3));
INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL); INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL);
CREATE INDEX ON t USING ivfflat (val) WITH (lists = 1); CREATE INDEX ON t USING ivfflat (val vector_l2_ops) WITH (lists = 1);
INSERT INTO t (val) VALUES ('[1,2,4]'); INSERT INTO t (val) VALUES ('[1,2,4]');
SELECT * FROM t ORDER BY val <-> '[3,3,3]'; SELECT * FROM t ORDER BY val <-> '[3,3,3]';
val val
@@ -27,4 +27,13 @@ SELECT COUNT(*) FROM t;
5 5
(1 row) (1 row)
TRUNCATE t;
NOTICE: ivfflat index created with little data
DETAIL: This will cause low recall.
HINT: Drop the index until the table has more data.
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
val
-----
(0 rows)
DROP TABLE t; DROP TABLE t;

View File

@@ -1,9 +1,8 @@
SET enable_seqscan = off;
CREATE TABLE t (val vector(3)); CREATE TABLE t (val vector(3));
CREATE INDEX ON t USING ivfflat (val) WITH (lists = 0); CREATE INDEX ON t USING ivfflat (val vector_l2_ops) WITH (lists = 0);
ERROR: value 0 out of bounds for option "lists" ERROR: value 0 out of bounds for option "lists"
DETAIL: Valid values are between "1" and "32768". DETAIL: Valid values are between "1" and "32768".
CREATE INDEX ON t USING ivfflat (val) WITH (lists = 32769); CREATE INDEX ON t USING ivfflat (val vector_l2_ops) WITH (lists = 32769);
ERROR: value 32769 out of bounds for option "lists" ERROR: value 32769 out of bounds for option "lists"
DETAIL: Valid values are between "1" and "32768". DETAIL: Valid values are between "1" and "32768".
SHOW ivfflat.probes; SHOW ivfflat.probes;

View File

@@ -1,7 +1,7 @@
SET enable_seqscan = off; SET enable_seqscan = off;
CREATE UNLOGGED TABLE t (val vector(3)); CREATE UNLOGGED TABLE t (val vector(3));
INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL); INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL);
CREATE INDEX ON t USING ivfflat (val) WITH (lists = 1); CREATE INDEX ON t USING ivfflat (val vector_l2_ops) WITH (lists = 1);
SELECT * FROM t ORDER BY val <-> '[3,3,3]'; SELECT * FROM t ORDER BY val <-> '[3,3,3]';
val val
--------- ---------

View File

@@ -11,12 +11,7 @@ SELECT vector_dims('[1,2,3]');
SELECT round(vector_norm('[1,1]')::numeric, 5); SELECT round(vector_norm('[1,1]')::numeric, 5);
SELECT vector_norm('[3,4]'); SELECT vector_norm('[3,4]');
SELECT vector_norm('[0,1]'); SELECT vector_norm('[0,1]');
SELECT vector_norm('[3e37,4e37]')::real;
SELECT normalize_l2('[3,4]');
SELECT normalize_l2('[3,0]');
SELECT normalize_l2('[0,0.1]');
SELECT normalize_l2('[0,0]');
SELECT normalize_l2('[3e38]');
SELECT l2_distance('[0,0]', '[3,4]'); SELECT l2_distance('[0,0]', '[3,4]');
SELECT l2_distance('[0,0]', '[0,1]'); SELECT l2_distance('[0,0]', '[0,1]');

View File

@@ -10,4 +10,7 @@ SELECT * FROM t ORDER BY val <-> '[3,3,3]';
SELECT * FROM t ORDER BY val <-> (SELECT NULL::vector); SELECT * FROM t ORDER BY val <-> (SELECT NULL::vector);
SELECT COUNT(*) FROM t; SELECT COUNT(*) FROM t;
TRUNCATE t;
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
DROP TABLE t; DROP TABLE t;

View File

@@ -1,14 +1,13 @@
SET enable_seqscan = off;
CREATE TABLE t (val vector(3)); CREATE TABLE t (val vector(3));
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (m = 3); CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (m = 1);
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (m = 101); CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (m = 101);
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (ef_construction = 9); CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (ef_construction = 3);
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (ef_construction = 1001); CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (ef_construction = 1001);
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (m = 16, ef_construction = 31);
SHOW hnsw.ef_search; SHOW hnsw.ef_search;
SET hnsw.ef_search = 9; SET hnsw.ef_search = 0;
SET hnsw.ef_search = 1001; SET hnsw.ef_search = 1001;
DROP TABLE t; DROP TABLE t;

View File

@@ -2,7 +2,7 @@ SET enable_seqscan = off;
CREATE TABLE t (val vector(3)); CREATE TABLE t (val vector(3));
INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL); INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL);
CREATE INDEX ON t USING ivfflat (val) WITH (lists = 1); CREATE INDEX ON t USING ivfflat (val vector_l2_ops) WITH (lists = 1);
INSERT INTO t (val) VALUES ('[1,2,4]'); INSERT INTO t (val) VALUES ('[1,2,4]');
@@ -10,4 +10,7 @@ SELECT * FROM t ORDER BY val <-> '[3,3,3]';
SELECT * FROM t ORDER BY val <-> (SELECT NULL::vector); SELECT * FROM t ORDER BY val <-> (SELECT NULL::vector);
SELECT COUNT(*) FROM t; SELECT COUNT(*) FROM t;
TRUNCATE t;
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
DROP TABLE t; DROP TABLE t;

View File

@@ -1,8 +1,6 @@
SET enable_seqscan = off;
CREATE TABLE t (val vector(3)); CREATE TABLE t (val vector(3));
CREATE INDEX ON t USING ivfflat (val) WITH (lists = 0); CREATE INDEX ON t USING ivfflat (val vector_l2_ops) WITH (lists = 0);
CREATE INDEX ON t USING ivfflat (val) WITH (lists = 32769); CREATE INDEX ON t USING ivfflat (val vector_l2_ops) WITH (lists = 32769);
SHOW ivfflat.probes; SHOW ivfflat.probes;

View File

@@ -2,7 +2,7 @@ SET enable_seqscan = off;
CREATE UNLOGGED TABLE t (val vector(3)); CREATE UNLOGGED TABLE t (val vector(3));
INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL); INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL);
CREATE INDEX ON t USING ivfflat (val) WITH (lists = 1); CREATE INDEX ON t USING ivfflat (val vector_l2_ops) WITH (lists = 1);
SELECT * FROM t ORDER BY val <-> '[3,3,3]'; SELECT * FROM t ORDER BY val <-> '[3,3,3]';

View File

@@ -26,7 +26,8 @@ sub test_index_replay
or die "Timed out while waiting for replica 1 to catch up"; or die "Timed out while waiting for replica 1 to catch up";
my @r = (); my @r = ();
for (1 .. $dim) { for (1 .. $dim)
{
push(@r, rand()); push(@r, rand());
} }
my $sql = join(",", @r); my $sql = join(",", @r);
@@ -52,11 +53,13 @@ my $array_sql = join(",", ('random()') x $dim);
# Initialize primary node # Initialize primary node
$node_primary = get_new_node('primary'); $node_primary = get_new_node('primary');
$node_primary->init(allows_streaming => 1); $node_primary->init(allows_streaming => 1);
if ($dim > 32) { if ($dim > 32)
{
# TODO use wal_keep_segments for Postgres < 13 # TODO use wal_keep_segments for Postgres < 13
$node_primary->append_conf('postgresql.conf', qq(wal_keep_size = 1GB)); $node_primary->append_conf('postgresql.conf', qq(wal_keep_size = 1GB));
} }
if ($dim > 1500) { if ($dim > 1500)
{
$node_primary->append_conf('postgresql.conf', qq(maintenance_work_mem = 128MB)); $node_primary->append_conf('postgresql.conf', qq(maintenance_work_mem = 128MB));
} }
$node_primary->start; $node_primary->start;
@@ -67,8 +70,7 @@ $node_primary->backup($backup_name);
# Create streaming replica linking to primary # Create streaming replica linking to primary
$node_replica = get_new_node('replica'); $node_replica = get_new_node('replica');
$node_replica->init_from_backup($node_primary, $backup_name, $node_replica->init_from_backup($node_primary, $backup_name, has_streaming => 1);
has_streaming => 1);
$node_replica->start; $node_replica->start;
# Create ivfflat index on primary # Create ivfflat index on primary
@@ -77,7 +79,7 @@ $node_primary->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector($dim));
$node_primary->safe_psql("postgres", $node_primary->safe_psql("postgres",
"INSERT INTO tst SELECT i % 10, ARRAY[$array_sql] FROM generate_series(1, 100000) i;" "INSERT INTO tst SELECT i % 10, ARRAY[$array_sql] FROM generate_series(1, 100000) i;"
); );
$node_primary->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v);"); $node_primary->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v vector_l2_ops);");
# Test that queries give same result # Test that queries give same result
test_index_replay('initial'); test_index_replay('initial');

View File

@@ -7,7 +7,8 @@ use Test::More;
my $dim = 3; my $dim = 3;
my @r = (); my @r = ();
for (1 .. $dim) { for (1 .. $dim)
{
my $v = int(rand(1000)) + 1; my $v = int(rand(1000)) + 1;
push(@r, "i % $v"); push(@r, "i % $v");
} }
@@ -24,7 +25,7 @@ $node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector($dim));");
$node->safe_psql("postgres", $node->safe_psql("postgres",
"INSERT INTO tst SELECT i % 10, ARRAY[$array_sql] FROM generate_series(1, 100000) i;" "INSERT INTO tst SELECT i % 10, ARRAY[$array_sql] FROM generate_series(1, 100000) i;"
); );
$node->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v);"); $node->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v vector_l2_ops);");
# Get size # Get size
my $size = $node->safe_psql("postgres", "SELECT pg_total_relation_size('tst_v_idx');"); my $size = $node->safe_psql("postgres", "SELECT pg_total_relation_size('tst_v_idx');");

View File

@@ -22,7 +22,8 @@ sub test_recall
)); ));
like($explain, qr/Index Scan using idx on tst/); like($explain, qr/Index Scan using idx on tst/);
for my $i (0 .. $#queries) { for my $i (0 .. $#queries)
{
my $actual = $node->safe_psql("postgres", qq( my $actual = $node->safe_psql("postgres", qq(
SET enable_seqscan = off; SET enable_seqscan = off;
SET ivfflat.probes = $probes; SET ivfflat.probes = $probes;
@@ -33,8 +34,10 @@ sub test_recall
my @expected_ids = split("\n", $expected[$i]); my @expected_ids = split("\n", $expected[$i]);
foreach (@expected_ids) { foreach (@expected_ids)
if (exists($actual_set{$_})) { {
if (exists($actual_set{$_}))
{
$correct++; $correct++;
} }
$total++; $total++;
@@ -57,7 +60,8 @@ $node->safe_psql("postgres",
); );
# Generate queries # Generate queries
for (1..20) { for (1 .. 20)
{
my $r1 = rand(); my $r1 = rand();
my $r2 = rand(); my $r2 = rand();
my $r3 = rand(); my $r3 = rand();
@@ -66,26 +70,21 @@ for (1..20) {
# Check each index type # Check each index type
my @operators = ("<->", "<#>", "<=>"); my @operators = ("<->", "<#>", "<=>");
my @opclasses = ("vector_l2_ops", "vector_ip_ops", "vector_cosine_ops");
foreach (@operators) { for my $i (0 .. $#operators)
my $operator = $_; {
my $operator = $operators[$i];
my $opclass = $opclasses[$i];
# Get exact results # Get exact results
@expected = (); @expected = ();
foreach (@queries) { foreach (@queries)
{
my $res = $node->safe_psql("postgres", "SELECT i FROM tst ORDER BY v $operator '$_' LIMIT $limit;"); my $res = $node->safe_psql("postgres", "SELECT i FROM tst ORDER BY v $operator '$_' LIMIT $limit;");
push(@expected, $res); push(@expected, $res);
} }
my $opclass;
if ($operator eq "<->") {
$opclass = "vector_l2_ops";
} elsif ($operator eq "<#>") {
$opclass = "vector_ip_ops";
} else {
$opclass = "vector_cosine_ops";
}
# Build index serially # Build index serially
$node->safe_psql("postgres", qq( $node->safe_psql("postgres", qq(
SET max_parallel_maintenance_workers = 0; SET max_parallel_maintenance_workers = 0;
@@ -93,13 +92,14 @@ foreach (@operators) {
)); ));
# Test approximate results # Test approximate results
if ($operator ne "<#>") { if ($operator ne "<#>")
{
# TODO fix test # TODO fix test
test_recall(1, 0.75, $operator); test_recall(1, 0.71, $operator);
test_recall(10, 0.95, $operator); test_recall(10, 0.95, $operator);
} }
# Account for equal distances # Account for equal distances
test_recall(100, 0.995, $operator); test_recall(100, 0.9925, $operator);
$node->safe_psql("postgres", "DROP INDEX idx;"); $node->safe_psql("postgres", "DROP INDEX idx;");
@@ -113,13 +113,14 @@ foreach (@operators) {
like($stderr, qr/using \d+ parallel workers/); like($stderr, qr/using \d+ parallel workers/);
# Test approximate results # Test approximate results
if ($operator ne "<#>") { if ($operator ne "<#>")
{
# TODO fix test # TODO fix test
test_recall(1, 0.75, $operator); test_recall(1, 0.71, $operator);
test_recall(10, 0.95, $operator); test_recall(10, 0.95, $operator);
} }
# Account for equal distances # Account for equal distances
test_recall(100, 0.995, $operator); test_recall(100, 0.9925, $operator);
$node->safe_psql("postgres", "DROP INDEX idx;"); $node->safe_psql("postgres", "DROP INDEX idx;");
} }

View File

@@ -20,7 +20,7 @@ sub test_centers
{ {
my ($lists, $min) = @_; my ($lists, $min) = @_;
my ($ret, $stdout, $stderr) = $node->psql("postgres", "CREATE INDEX ON tst USING ivfflat (v) WITH (lists = $lists);"); my ($ret, $stdout, $stderr) = $node->psql("postgres", "CREATE INDEX ON tst USING ivfflat (v vector_l2_ops) WITH (lists = $lists);");
is($ret, 0, $stderr); is($ret, 0, $stderr);
} }

View File

@@ -18,24 +18,21 @@ $node->safe_psql("postgres",
# Check each index type # Check each index type
my @operators = ("<->", "<#>", "<=>"); my @operators = ("<->", "<#>", "<=>");
foreach (@operators) { my @opclasses = ("vector_l2_ops", "vector_ip_ops", "vector_cosine_ops");
my $operator = $_;
for my $i (0 .. $#operators)
{
my $operator = $operators[$i];
my $opclass = $opclasses[$i];
# Add index # Add index
my $opclass;
if ($operator eq "<->") {
$opclass = "vector_l2_ops";
} elsif ($operator eq "<#>") {
$opclass = "vector_ip_ops";
} else {
$opclass = "vector_cosine_ops";
}
$node->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v $opclass);"); $node->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v $opclass);");
# Test 100% recall # Test 100% recall
for (1..20) { for (1 .. 20)
my $i = int(rand() * 100000); {
my $query = $node->safe_psql("postgres", "SELECT v FROM tst WHERE i = $i;"); my $id = int(rand() * 100000);
my $query = $node->safe_psql("postgres", "SELECT v FROM tst WHERE i = $id;");
my $res = $node->safe_psql("postgres", qq( my $res = $node->safe_psql("postgres", qq(
SET enable_seqscan = off; SET enable_seqscan = off;
SELECT v FROM tst ORDER BY v <-> '$query' LIMIT 1; SELECT v FROM tst ORDER BY v <-> '$query' LIMIT 1;

View File

@@ -16,8 +16,8 @@ $node->safe_psql("postgres",
"INSERT INTO tst SELECT ARRAY[random(), random(), random()] FROM generate_series(1, 100000) i;" "INSERT INTO tst SELECT ARRAY[random(), random(), random()] FROM generate_series(1, 100000) i;"
); );
$node->safe_psql("postgres", "CREATE INDEX lists50 ON tst USING ivfflat (v) WITH (lists = 50);"); $node->safe_psql("postgres", "CREATE INDEX lists50 ON tst USING ivfflat (v vector_l2_ops) WITH (lists = 50);");
$node->safe_psql("postgres", "CREATE INDEX lists100 ON tst USING ivfflat (v) WITH (lists = 100);"); $node->safe_psql("postgres", "CREATE INDEX lists100 ON tst USING ivfflat (v vector_l2_ops) WITH (lists = 100);");
# Test prefers more lists # Test prefers more lists
my $res = $node->safe_psql("postgres", "EXPLAIN SELECT v FROM tst ORDER BY v <-> '[0.5,0.5,0.5]' LIMIT 10;"); my $res = $node->safe_psql("postgres", "EXPLAIN SELECT v FROM tst ORDER BY v <-> '[0.5,0.5,0.5]' LIMIT 10;");
@@ -26,7 +26,7 @@ unlike($res, qr/lists50/);
# Test errors with too much memory # Test errors with too much memory
my ($ret, $stdout, $stderr) = $node->psql("postgres", my ($ret, $stdout, $stderr) = $node->psql("postgres",
"CREATE INDEX lists10000 ON tst USING ivfflat (v) WITH (lists = 10000);" "CREATE INDEX lists10000 ON tst USING ivfflat (v vector_l2_ops) WITH (lists = 10000);"
); );
like($stderr, qr/memory required is/); like($stderr, qr/memory required is/);

View File

@@ -19,7 +19,7 @@ $node->safe_psql("postgres", "CREATE TABLE tst (v vector($dim));");
$node->safe_psql("postgres", $node->safe_psql("postgres",
"INSERT INTO tst SELECT ARRAY[$array_sql] FROM generate_series(1, 10000) i;" "INSERT INTO tst SELECT ARRAY[$array_sql] FROM generate_series(1, 10000) i;"
); );
$node->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v);"); $node->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v vector_l2_ops);");
$node->pgbench( $node->pgbench(
"--no-vacuum --client=5 --transactions=100", "--no-vacuum --client=5 --transactions=100",
@@ -28,7 +28,7 @@ $node->pgbench(
[qr{^$}], [qr{^$}],
"concurrent INSERTs", "concurrent INSERTs",
{ {
"007_inserts" => "INSERT INTO tst SELECT ARRAY[$array_sql] FROM generate_series(1, 10) i;" "007_ivfflat_inserts" => "INSERT INTO tst SELECT ARRAY[$array_sql] FROM generate_series(1, 10) i;"
} }
); );

View File

@@ -30,7 +30,8 @@ sub test_aggregate
# Test matches real for avg # Test matches real for avg
# Cannot test sum since sum(real) varies between calls # Cannot test sum since sum(real) varies between calls
if ($agg eq 'avg') { if ($agg eq 'avg')
{
my $r1 = $node->safe_psql("postgres", "SELECT $agg(r1)::float4 FROM tst;"); my $r1 = $node->safe_psql("postgres", "SELECT $agg(r1)::float4 FROM tst;");
my $r2 = $node->safe_psql("postgres", "SELECT $agg(r2)::float4 FROM tst;"); my $r2 = $node->safe_psql("postgres", "SELECT $agg(r2)::float4 FROM tst;");
my $r3 = $node->safe_psql("postgres", "SELECT $agg(r3)::float4 FROM tst;"); my $r3 = $node->safe_psql("postgres", "SELECT $agg(r3)::float4 FROM tst;");

View File

@@ -26,7 +26,8 @@ sub test_index_replay
or die "Timed out while waiting for replica 1 to catch up"; or die "Timed out while waiting for replica 1 to catch up";
my @r = (); my @r = ();
for (1 .. $dim) { for (1 .. $dim)
{
push(@r, rand()); push(@r, rand());
} }
my $sql = join(",", @r); my $sql = join(",", @r);
@@ -52,11 +53,13 @@ my $array_sql = join(",", ('random()') x $dim);
# Initialize primary node # Initialize primary node
$node_primary = get_new_node('primary'); $node_primary = get_new_node('primary');
$node_primary->init(allows_streaming => 1); $node_primary->init(allows_streaming => 1);
if ($dim > 32) { if ($dim > 32)
{
# TODO use wal_keep_segments for Postgres < 13 # TODO use wal_keep_segments for Postgres < 13
$node_primary->append_conf('postgresql.conf', qq(wal_keep_size = 1GB)); $node_primary->append_conf('postgresql.conf', qq(wal_keep_size = 1GB));
} }
if ($dim > 1500) { if ($dim > 1500)
{
$node_primary->append_conf('postgresql.conf', qq(maintenance_work_mem = 128MB)); $node_primary->append_conf('postgresql.conf', qq(maintenance_work_mem = 128MB));
} }
$node_primary->start; $node_primary->start;
@@ -67,8 +70,7 @@ $node_primary->backup($backup_name);
# Create streaming replica linking to primary # Create streaming replica linking to primary
$node_replica = get_new_node('replica'); $node_replica = get_new_node('replica');
$node_replica->init_from_backup($node_primary, $backup_name, $node_replica->init_from_backup($node_primary, $backup_name, has_streaming => 1);
has_streaming => 1);
$node_replica->start; $node_replica->start;
# Create hnsw index on primary # Create hnsw index on primary

View File

@@ -7,7 +7,8 @@ use Test::More;
my $dim = 3; my $dim = 3;
my @r = (); my @r = ();
for (1 .. $dim) { for (1 .. $dim)
{
my $v = int(rand(1000)) + 1; my $v = int(rand(1000)) + 1;
push(@r, "i % $v"); push(@r, "i % $v");
} }
@@ -22,7 +23,7 @@ $node->start;
$node->safe_psql("postgres", "CREATE EXTENSION vector;"); $node->safe_psql("postgres", "CREATE EXTENSION vector;");
$node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector($dim));"); $node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector($dim));");
$node->safe_psql("postgres", $node->safe_psql("postgres",
"INSERT INTO tst SELECT i % 10, ARRAY[$array_sql] FROM generate_series(1, 10000) i;" "INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(1, 10000) i;"
); );
$node->safe_psql("postgres", "CREATE INDEX ON tst USING hnsw (v vector_l2_ops);"); $node->safe_psql("postgres", "CREATE INDEX ON tst USING hnsw (v vector_l2_ops);");
@@ -33,11 +34,21 @@ my $size = $node->safe_psql("postgres", "SELECT pg_total_relation_size('tst_v_id
$node->safe_psql("postgres", "DELETE FROM tst;"); $node->safe_psql("postgres", "DELETE FROM tst;");
$node->safe_psql("postgres", "VACUUM tst;"); $node->safe_psql("postgres", "VACUUM tst;");
$node->safe_psql("postgres", $node->safe_psql("postgres",
"INSERT INTO tst SELECT i % 10, ARRAY[$array_sql] FROM generate_series(1, 10000) i;" "INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(1, 10000) i;"
); );
# Check size # Check size
# May increase some due to different levels
my $new_size = $node->safe_psql("postgres", "SELECT pg_total_relation_size('tst_v_idx');"); my $new_size = $node->safe_psql("postgres", "SELECT pg_total_relation_size('tst_v_idx');");
cmp_ok($new_size, "<=", $size * 1.01, "size does not increase too much"); cmp_ok($new_size, "<=", $size * 1.02, "size does not increase too much");
# Delete all but one
$node->safe_psql("postgres", "DELETE FROM tst WHERE i != 123;");
$node->safe_psql("postgres", "VACUUM tst;");
my $res = $node->safe_psql("postgres", qq(
SET enable_seqscan = off;
SELECT i FROM tst ORDER BY v <-> '[0,0,0]' LIMIT 10;
));
is($res, 123);
done_testing(); done_testing();

View File

@@ -21,7 +21,8 @@ sub test_recall
)); ));
like($explain, qr/Index Scan/); like($explain, qr/Index Scan/);
for my $i (0 .. $#queries) { for my $i (0 .. $#queries)
{
my $actual = $node->safe_psql("postgres", qq( my $actual = $node->safe_psql("postgres", qq(
SET enable_seqscan = off; SET enable_seqscan = off;
SELECT i FROM tst ORDER BY v $operator '$queries[$i]' LIMIT $limit; SELECT i FROM tst ORDER BY v $operator '$queries[$i]' LIMIT $limit;
@@ -31,8 +32,10 @@ sub test_recall
my @expected_ids = split("\n", $expected[$i]); my @expected_ids = split("\n", $expected[$i]);
foreach (@expected_ids) { foreach (@expected_ids)
if (exists($actual_set{$_})) { {
if (exists($actual_set{$_}))
{
$correct++; $correct++;
} }
$total++; $total++;
@@ -55,7 +58,8 @@ $node->safe_psql("postgres",
); );
# Generate queries # Generate queries
for (1..20) { for (1 .. 20)
{
my $r1 = rand(); my $r1 = rand();
my $r2 = rand(); my $r2 = rand();
my $r3 = rand(); my $r3 = rand();
@@ -64,33 +68,26 @@ for (1..20) {
# Check each index type # Check each index type
my @operators = ("<->", "<#>", "<=>"); my @operators = ("<->", "<#>", "<=>");
my @opclasses = ("vector_l2_ops", "vector_ip_ops", "vector_cosine_ops");
foreach (@operators) { for my $i (0 .. $#operators)
my $operator = $_; {
my $operator = $operators[$i];
my $opclass = $opclasses[$i];
# Get exact results # Get exact results
@expected = (); @expected = ();
foreach (@queries) { foreach (@queries)
{
my $res = $node->safe_psql("postgres", "SELECT i FROM tst ORDER BY v $operator '$_' LIMIT $limit;"); my $res = $node->safe_psql("postgres", "SELECT i FROM tst ORDER BY v $operator '$_' LIMIT $limit;");
push(@expected, $res); push(@expected, $res);
} }
# Add index # Add index
my $opclass;
if ($operator eq "<->") {
$opclass = "vector_l2_ops";
} elsif ($operator eq "<#>") {
$opclass = "vector_ip_ops";
} else {
$opclass = "vector_cosine_ops";
}
$node->safe_psql("postgres", "CREATE INDEX ON tst USING hnsw (v $opclass);"); $node->safe_psql("postgres", "CREATE INDEX ON tst USING hnsw (v $opclass);");
if ($operator eq "<#>") { my $min = $operator eq "<#>" ? 0.80 : 0.99;
test_recall(0.80, $operator); test_recall($min, $operator);
} else {
test_recall(0.99, $operator);
}
} }
done_testing(); done_testing();

View File

@@ -21,7 +21,8 @@ sub test_recall
)); ));
like($explain, qr/Index Scan/); like($explain, qr/Index Scan/);
for my $i (0 .. $#queries) { for my $i (0 .. $#queries)
{
my $actual = $node->safe_psql("postgres", qq( my $actual = $node->safe_psql("postgres", qq(
SET enable_seqscan = off; SET enable_seqscan = off;
SELECT i FROM tst ORDER BY v $operator '$queries[$i]' LIMIT $limit; SELECT i FROM tst ORDER BY v $operator '$queries[$i]' LIMIT $limit;
@@ -31,8 +32,10 @@ sub test_recall
my @expected_ids = split("\n", $expected[$i]); my @expected_ids = split("\n", $expected[$i]);
foreach (@expected_ids) { foreach (@expected_ids)
if (exists($actual_set{$_})) { {
if (exists($actual_set{$_}))
{
$correct++; $correct++;
} }
$total++; $total++;
@@ -49,10 +52,11 @@ $node->start;
# Create table # Create table
$node->safe_psql("postgres", "CREATE EXTENSION vector;"); $node->safe_psql("postgres", "CREATE EXTENSION vector;");
$node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector(3));"); $node->safe_psql("postgres", "CREATE TABLE tst (i serial, v vector(3));");
# Generate queries # Generate queries
for (1..20) { for (1 .. 20)
{
my $r1 = rand(); my $r1 = rand();
my $r2 = rand(); my $r2 = rand();
my $r3 = rand(); my $r3 = rand();
@@ -61,28 +65,32 @@ for (1..20) {
# Check each index type # Check each index type
my @operators = ("<->", "<#>", "<=>"); my @operators = ("<->", "<#>", "<=>");
my @opclasses = ("vector_l2_ops", "vector_ip_ops", "vector_cosine_ops");
foreach (@operators) { for my $i (0 .. $#operators)
my $operator = $_; {
my $operator = $operators[$i];
my $opclass = $opclasses[$i];
# Add index # Add index
my $opclass;
if ($operator eq "<->") {
$opclass = "vector_l2_ops";
} elsif ($operator eq "<#>") {
$opclass = "vector_ip_ops";
} else {
$opclass = "vector_cosine_ops";
}
$node->safe_psql("postgres", "CREATE INDEX idx ON tst USING hnsw (v $opclass);"); $node->safe_psql("postgres", "CREATE INDEX idx ON tst USING hnsw (v $opclass);");
$node->safe_psql("postgres", # Use concurrent inserts
"INSERT INTO tst SELECT i, ARRAY[random(), random(), random()] FROM generate_series(1, 10000) i;" $node->pgbench(
"--no-vacuum --client=10 --transactions=1000",
0,
[qr{actually processed}],
[qr{^$}],
"concurrent INSERTs",
{
"013_hnsw_insert_recall_$opclass" => "INSERT INTO tst (v) VALUES (ARRAY[random(), random(), random()]);"
}
); );
# Get exact results # Get exact results
@expected = (); @expected = ();
foreach (@queries) { foreach (@queries)
{
my $res = $node->safe_psql("postgres", qq( my $res = $node->safe_psql("postgres", qq(
SET enable_indexscan = off; SET enable_indexscan = off;
SELECT i FROM tst ORDER BY v $operator '$_' LIMIT $limit; SELECT i FROM tst ORDER BY v $operator '$_' LIMIT $limit;
@@ -90,11 +98,8 @@ foreach (@operators) {
push(@expected, $res); push(@expected, $res);
} }
if ($operator eq "<#>") { my $min = $operator eq "<#>" ? 0.80 : 0.99;
test_recall(0.80, $operator); test_recall($min, $operator);
} else {
test_recall(0.99, $operator);
}
$node->safe_psql("postgres", "DROP INDEX idx;"); $node->safe_psql("postgres", "DROP INDEX idx;");
$node->safe_psql("postgres", "TRUNCATE tst;"); $node->safe_psql("postgres", "TRUNCATE tst;");

View File

@@ -17,22 +17,8 @@ $node->start;
# Create table and index # Create table and index
$node->safe_psql("postgres", "CREATE EXTENSION vector;"); $node->safe_psql("postgres", "CREATE EXTENSION vector;");
$node->safe_psql("postgres", "CREATE TABLE tst (v vector($dim));"); $node->safe_psql("postgres", "CREATE TABLE tst (v vector($dim));");
$node->safe_psql("postgres",
"INSERT INTO tst SELECT ARRAY[$array_sql] FROM generate_series(1, 100) i;"
);
$node->safe_psql("postgres", "CREATE INDEX ON tst USING hnsw (v vector_l2_ops);"); $node->safe_psql("postgres", "CREATE INDEX ON tst USING hnsw (v vector_l2_ops);");
$node->pgbench(
"--no-vacuum --client=5 --transactions=100",
0,
[qr{actually processed}],
[qr{^$}],
"concurrent INSERTs",
{
"007_inserts" => "INSERT INTO tst SELECT ARRAY[$array_sql] FROM generate_series(1, 10) i;"
}
);
sub idx_scan sub idx_scan
{ {
# Stats do not update instantaneously # Stats do not update instantaneously
@@ -41,18 +27,48 @@ sub idx_scan
$node->safe_psql("postgres", "SELECT idx_scan FROM pg_stat_user_indexes WHERE indexrelid = 'tst_v_idx'::regclass;"); $node->safe_psql("postgres", "SELECT idx_scan FROM pg_stat_user_indexes WHERE indexrelid = 'tst_v_idx'::regclass;");
} }
my $expected = 100 + 5 * 100 * 10; for my $i (1 .. 20)
{
$node->pgbench(
"--no-vacuum --client=10 --transactions=1",
0,
[qr{actually processed}],
[qr{^$}],
"concurrent INSERTs",
{
"014_hnsw_inserts_$i" => "INSERT INTO tst VALUES (ARRAY[$array_sql]);"
}
);
my $count = $node->safe_psql("postgres", "SELECT COUNT(*) FROM tst;"); my $count = $node->safe_psql("postgres", qq(
is($count, $expected); SET enable_seqscan = off;
is(idx_scan(), 0); SELECT COUNT(*) FROM (SELECT v FROM tst ORDER BY v <-> (SELECT v FROM tst LIMIT 1)) t;
));
is($count, 10);
$count = $node->safe_psql("postgres", qq( $node->safe_psql("postgres", "TRUNCATE tst;");
}
$node->pgbench(
"--no-vacuum --client=20 --transactions=5",
0,
[qr{actually processed}],
[qr{^$}],
"concurrent INSERTs",
{
"014_hnsw_inserts" => "INSERT INTO tst SELECT ARRAY[$array_sql] FROM generate_series(1, 10) i;"
}
);
my $count = $node->safe_psql("postgres", qq(
SET enable_seqscan = off; SET enable_seqscan = off;
SET hnsw.ef_search = 400; SET hnsw.ef_search = 1000;
SELECT COUNT(*) FROM (SELECT v FROM tst ORDER BY v <-> (SELECT v FROM tst LIMIT 1)) t; SELECT COUNT(*) FROM (SELECT v FROM tst ORDER BY v <-> (SELECT v FROM tst LIMIT 1)) t;
)); ));
is($count, 400); # Elements may lose all incoming connections with the HNSW algorithm
is(idx_scan(), 1); # Vacuuming can fix this if one of the elements neighbors is deleted
cmp_ok($count, ">=", 997);
is(idx_scan(), 21);
done_testing(); done_testing();

View File

@@ -0,0 +1,58 @@
use strict;
use warnings;
use PostgresNode;
use TestLib;
use Test::More;
# Initialize node
my $node = get_new_node('node');
$node->init;
$node->start;
# Create table
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
$node->safe_psql("postgres", "CREATE TABLE tst (v vector(3));");
sub insert_vectors
{
for my $i (1 .. 20)
{
$node->safe_psql("postgres", "INSERT INTO tst VALUES ('[1,1,1]');");
}
}
sub test_duplicates
{
my $res = $node->safe_psql("postgres", qq(
SET enable_seqscan = off;
SET hnsw.ef_search = 1;
SELECT COUNT(*) FROM (SELECT * FROM tst ORDER BY v <-> '[1,1,1]') t;
));
is($res, 10);
}
# Test duplicates with build
insert_vectors();
$node->safe_psql("postgres", "CREATE INDEX idx ON tst USING hnsw (v vector_l2_ops);");
test_duplicates();
# Reset
$node->safe_psql("postgres", "TRUNCATE tst;");
# Test duplicates with inserts
insert_vectors();
test_duplicates();
# Test fallback path for inserts
$node->pgbench(
"--no-vacuum --client=5 --transactions=100",
0,
[qr{actually processed}],
[qr{^$}],
"concurrent INSERTs",
{
"015_hnsw_duplicates" => "INSERT INTO tst VALUES ('[1,1,1]');"
}
);
done_testing();

View File

@@ -0,0 +1,97 @@
use strict;
use warnings;
use PostgresNode;
use TestLib;
use Test::More;
my $node;
my @queries = ();
my @expected;
my $limit = 20;
sub test_recall
{
my ($min, $ef_search, $test_name) = @_;
my $correct = 0;
my $total = 0;
my $explain = $node->safe_psql("postgres", qq(
SET enable_seqscan = off;
SET hnsw.ef_search = $ef_search;
EXPLAIN ANALYZE SELECT i FROM tst ORDER BY v <-> '$queries[0]' LIMIT $limit;
));
like($explain, qr/Index Scan/);
for my $i (0 .. $#queries)
{
my $actual = $node->safe_psql("postgres", qq(
SET enable_seqscan = off;
SET hnsw.ef_search = $ef_search;
SELECT i FROM tst ORDER BY v <-> '$queries[$i]' LIMIT $limit;
));
my @actual_ids = split("\n", $actual);
my %actual_set = map { $_ => 1 } @actual_ids;
my @expected_ids = split("\n", $expected[$i]);
foreach (@expected_ids)
{
if (exists($actual_set{$_}))
{
$correct++;
}
$total++;
}
}
cmp_ok($correct / $total, ">=", $min, $test_name);
}
# Initialize node
$node = get_new_node('node');
$node->init;
$node->start;
# Create table
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
$node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector(3));");
$node->safe_psql("postgres", "ALTER TABLE tst SET (autovacuum_enabled = false);");
$node->safe_psql("postgres",
"INSERT INTO tst SELECT i, ARRAY[random(), random(), random()] FROM generate_series(1, 10000) i;"
);
# Add index
$node->safe_psql("postgres", "CREATE INDEX ON tst USING hnsw (v vector_l2_ops) WITH (m = 4, ef_construction = 8);");
# Delete data
$node->safe_psql("postgres", "DELETE FROM tst WHERE i > 2500;");
# Generate queries
for (1 .. 20)
{
my $r1 = rand();
my $r2 = rand();
my $r3 = rand();
push(@queries, "[$r1,$r2,$r3]");
}
# Get exact results
@expected = ();
foreach (@queries)
{
my $res = $node->safe_psql("postgres", qq(
SET enable_indexscan = off;
SELECT i FROM tst ORDER BY v <-> '$_' LIMIT $limit;
));
push(@expected, $res);
}
test_recall(0.20, $limit, "before vacuum");
test_recall(0.95, 100, "before vacuum");
# TODO test concurrent inserts with vacuum
$node->safe_psql("postgres", "VACUUM tst;");
test_recall(0.95, $limit, "after vacuum");
done_testing();