Compare commits

..

132 Commits

Author SHA1 Message Date
Andrew Kane
3913432303 Version bump to 0.5.0 [skip ci] 2023-08-28 17:01:16 -07:00
Andrew Kane
baeebec244 Updated readme [skip ci] 2023-08-28 15:34:07 -07:00
Andrew Kane
d578a4cccc Updated readme [skip ci] 2023-08-28 15:30:28 -07:00
Andrew Kane
1e18e19496 Updated readme [skip ci] 2023-08-28 15:25:11 -07:00
Andrew Kane
39f973dab2 Updated readme [skip ci] 2023-08-28 15:00:58 -07:00
Andrew Kane
453fa15f28 Updated readme [skip ci] 2023-08-28 09:57:46 -07:00
Jonathan S. Katz
e50a79108f Set default HNSW "ef_construction" to 64 (#230) 2023-08-26 16:05:08 -07:00
Andrew Kane
cfaa2ecd7f Improved HNSW graph repair - #239 2023-08-26 14:06:03 -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
53 changed files with 1155 additions and 738 deletions

View File

@@ -78,6 +78,7 @@ jobs:
nmake /NOLOGO /F Makefile.win uninstall nmake /NOLOGO /F Makefile.win uninstall
shell: cmd shell: cmd
i386: i386:
if: ${{ !startsWith(github.ref_name, 'mac') && !startsWith(github.ref_name, 'windows') }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: debian:11 image: debian:11

View File

@@ -1,9 +1,8 @@
## 0.5.0 (unreleased) ## 0.5.0 (2023-08-28)
- 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

@@ -2,7 +2,7 @@
"name": "vector", "name": "vector",
"abstract": "Open-source vector similarity search for Postgres", "abstract": "Open-source vector similarity search for Postgres",
"description": "Supports L2 distance, inner product, and cosine distance", "description": "Supports L2 distance, inner product, and cosine distance",
"version": "0.4.4", "version": "0.5.0",
"maintainer": [ "maintainer": [
"Andrew Kane <andrew@ankane.org>" "Andrew Kane <andrew@ankane.org>"
], ],
@@ -20,7 +20,7 @@
"vector": { "vector": {
"file": "sql/vector.sql", "file": "sql/vector.sql",
"docfile": "README.md", "docfile": "README.md",
"version": "0.4.4", "version": "0.5.0",
"abstract": "Open-source vector similarity search for Postgres" "abstract": "Open-source vector similarity search for Postgres"
} }
}, },

View File

@@ -1,9 +1,10 @@
EXTENSION = vector EXTENSION = vector
EXTVERSION = 0.4.4 EXTVERSION = 0.5.0
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

@@ -1,7 +1,8 @@
EXTENSION = vector EXTENSION = vector
EXTVERSION = 0.4.4 EXTVERSION = 0.5.0
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

127
README.md
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
@@ -18,7 +18,7 @@ Compile and install the extension (supports Postgres 11+)
```sh ```sh
cd /tmp cd /tmp
git clone --branch v0.4.4 https://github.com/pgvector/pgvector.git git clone --branch v0.5.0 https://github.com/pgvector/pgvector.git
cd pgvector cd pgvector
make make
make install # may need sudo make install # may need sudo
@@ -157,7 +157,16 @@ SELECT category_id, AVG(embedding) FROM items GROUP BY category_id;
By default, pgvector performs exact nearest neighbor search, which provides perfect recall. By default, pgvector performs exact nearest neighbor search, which provides perfect recall.
You can add an index to use approximate nearest neighbor search, which trades some recall for performance. Unlike typical indexes, you will see different results for queries after adding an approximate index. You can add an index to use approximate nearest neighbor search, which trades some recall for speed. Unlike typical indexes, you will see different results for queries after adding an approximate index.
Supported index types are:
- [IVFFlat](#ivfflat)
- [HNSW](#hnsw) - *added in 0.5.0*
## IVFFlat
An IVFFlat index divides vectors into lists, and then searches a subset of those lists that are closest to the query vector. It has faster build times and uses less memory than HNSW, but has lower query performance (in terms of speed-recall tradeoff).
Three keys to achieving good recall are: Three keys to achieving good recall are:
@@ -206,7 +215,63 @@ SELECT ...
COMMIT; COMMIT;
``` ```
### Indexing Progress ## HNSW
An HNSW index creates a multilayer graph. It has slower build times and uses more memory than IVFFlat, but has better query performance (in terms of speed-recall tradeoff). Theres no training step like IVFFlat, so the index can be created without any data in the table.
Add an index for each distance function you want to use.
L2 distance
```sql
CREATE INDEX ON items USING hnsw (embedding vector_l2_ops);
```
Inner product
```sql
CREATE INDEX ON items USING hnsw (embedding vector_ip_ops);
```
Cosine distance
```sql
CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops);
```
Vectors with up to 2,000 dimensions can be indexed.
### Index Options
Specify HNSW parameters
- `m` - the max number of connections per layer (16 by default)
- `ef_construction` - the size of the dynamic candidate list for constructing the graph (64 by default)
```sql
CREATE INDEX ON items USING hnsw (embedding vector_l2_ops) WITH (m = 16, ef_construction = 64);
```
### Query Options
Specify the size of the dynamic candidate list for search (40 by default)
```sql
SET hnsw.ef_search = 100;
```
A higher value provides better recall at the cost of speed.
Use `SET LOCAL` inside a transaction to set it for a single query
```sql
BEGIN;
SET LOCAL hnsw.ef_search = 100;
SELECT ...
COMMIT;
```
## Indexing Progress
Check [indexing progress](https://www.postgresql.org/docs/current/progress-reporting.html#CREATE-INDEX-PROGRESS-REPORTING) with Postgres 12+ Check [indexing progress](https://www.postgresql.org/docs/current/progress-reporting.html#CREATE-INDEX-PROGRESS-REPORTING) with Postgres 12+
@@ -217,8 +282,8 @@ SELECT phase, tuples_done, tuples_total FROM pg_stat_progress_create_index;
The phases are: The phases are:
1. `initializing` 1. `initializing`
2. `performing k-means` 2. `performing k-means` (IVFFlat only)
3. `sorting tuples` 3. `assigning tuples` (IVFFlat only)
4. `loading tuples` 4. `loading tuples`
Note: `tuples_done` and `tuples_total` are only populated during the `loading tuples` phase Note: `tuples_done` and `tuples_total` are only populated during the `loading tuples` phase
@@ -283,7 +348,7 @@ SELECT * FROM items ORDER BY embedding <#> '[3,1,2]' LIMIT 5;
### Approximate Search ### Approximate Search
To speed up queries with an index, increase the number of inverted lists (at the expense of recall). To speed up queries with an IVFFlat index, increase the number of inverted lists (at the expense of recall).
```sql ```sql
CREATE INDEX ON items USING ivfflat (embedding vector_l2_ops) WITH (lists = 1000); CREATE INDEX ON items USING ivfflat (embedding vector_l2_ops) WITH (lists = 1000);
@@ -359,7 +424,7 @@ or choose to store vectors inline:
ALTER TABLE items ALTER COLUMN embedding SET STORAGE PLAIN; ALTER TABLE items ALTER COLUMN embedding SET STORAGE PLAIN;
``` ```
#### Why are there less results for a query after adding an index? #### Why are there less results for a query after adding an IVFFlat index?
The index was likely created with too little data for the number of lists. Drop the index until the table has more data. The index was likely created with too little data for the number of lists. Drop the index until the table has more data.
@@ -375,33 +440,32 @@ Each vector takes `4 * dimensions + 8` bytes of storage. Each element is a singl
### Vector Operators ### Vector Operators
Operator | Description Operator | Description | Added
--- | --- --- | --- | ---
\+ | element-wise addition \+ | element-wise addition |
\- | element-wise subtraction \- | element-wise subtraction |
\* | element-wise multiplication [unreleased] \* | element-wise multiplication | 0.5.0
<-> | Euclidean distance <-> | Euclidean distance |
<#> | negative inner product <#> | negative inner product |
<=> | cosine distance <=> | cosine distance |
### Vector Functions ### Vector Functions
Function | Description Function | Description | Added
--- | --- --- | --- | ---
cosine_distance(vector, vector) → double precision | cosine distance 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 | 0.5.0
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
### Aggregate Functions ### Aggregate Functions
Function | Description Function | Description | Added
--- | --- --- | --- | ---
avg(vector) → vector | arithmetic mean avg(vector) → vector | average |
sum(vector) → vector | sum [unreleased] sum(vector) → vector | sum | 0.5.0
## Installation Notes ## Installation Notes
@@ -445,7 +509,7 @@ Then use `nmake` to build:
```cmd ```cmd
set "PGROOT=C:\Program Files\PostgreSQL\15" set "PGROOT=C:\Program Files\PostgreSQL\15"
git clone --branch v0.4.4 https://github.com/pgvector/pgvector.git git clone --branch v0.5.0 https://github.com/pgvector/pgvector.git
cd pgvector cd pgvector
nmake /F Makefile.win nmake /F Makefile.win
nmake /F Makefile.win install nmake /F Makefile.win install
@@ -466,9 +530,8 @@ This adds pgvector to the [Postgres image](https://hub.docker.com/_/postgres) (r
You can also build the image manually: You can also build the image manually:
```sh ```sh
git clone --branch v0.4.4 https://github.com/pgvector/pgvector.git git clone --branch v0.5.0 https://github.com/pgvector/pgvector.git
cd pgvector cd pgvector
git cherry-pick 237a6df
docker build --build-arg PG_MAJOR=15 -t myuser/pgvector . docker build --build-arg PG_MAJOR=15 -t myuser/pgvector .
``` ```

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;
@@ -268,15 +265,15 @@ CREATE OPERATOR CLASS vector_ip_ops
OPERATOR 1 <#> (vector, vector) FOR ORDER BY float_ops, OPERATOR 1 <#> (vector, vector) FOR ORDER BY float_ops,
FUNCTION 1 vector_negative_inner_product(vector, vector), FUNCTION 1 vector_negative_inner_product(vector, vector),
FUNCTION 3 vector_spherical_distance(vector, vector), FUNCTION 3 vector_spherical_distance(vector, vector),
FUNCTION 6 normalize_l2(vector); FUNCTION 4 vector_norm(vector);
CREATE OPERATOR CLASS vector_cosine_ops CREATE OPERATOR CLASS vector_cosine_ops
FOR TYPE vector USING ivfflat AS FOR TYPE vector USING ivfflat AS
OPERATOR 1 <=> (vector, vector) FOR ORDER BY float_ops, OPERATOR 1 <=> (vector, vector) FOR ORDER BY float_ops,
FUNCTION 1 vector_negative_inner_product(vector, vector), FUNCTION 1 vector_negative_inner_product(vector, vector),
FUNCTION 2 vector_norm(vector),
FUNCTION 3 vector_spherical_distance(vector, vector), FUNCTION 3 vector_spherical_distance(vector, vector),
FUNCTION 5 normalize_l2(vector), FUNCTION 4 vector_norm(vector);
FUNCTION 6 normalize_l2(vector);
CREATE OPERATOR CLASS vector_l2_ops CREATE OPERATOR CLASS vector_l2_ops
FOR TYPE vector USING hnsw AS FOR TYPE vector USING hnsw AS
@@ -292,4 +289,4 @@ CREATE OPERATOR CLASS vector_cosine_ops
FOR TYPE vector USING hnsw AS FOR TYPE vector USING hnsw AS
OPERATOR 1 <=> (vector, vector) FOR ORDER BY float_ops, OPERATOR 1 <=> (vector, vector) FOR ORDER BY float_ops,
FUNCTION 1 vector_negative_inner_product(vector, vector), FUNCTION 1 vector_negative_inner_product(vector, vector),
FUNCTION 3 normalize_l2(vector); FUNCTION 2 vector_norm(vector);

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);
} }
@@ -167,7 +167,7 @@ hnswhandler(PG_FUNCTION_ARGS)
IndexAmRoutine *amroutine = makeNode(IndexAmRoutine); IndexAmRoutine *amroutine = makeNode(IndexAmRoutine);
amroutine->amstrategies = 0; amroutine->amstrategies = 0;
amroutine->amsupport = 3; amroutine->amsupport = 2;
#if PG_VERSION_NUM >= 130000 #if PG_VERSION_NUM >= 130000
amroutine->amoptsprocnum = 0; amroutine->amoptsprocnum = 0;
#endif #endif

View File

@@ -19,32 +19,40 @@
/* Support functions */ /* Support functions */
#define HNSW_DISTANCE_PROC 1 #define HNSW_DISTANCE_PROC 1
#define HNSW_NORM_PROC 2 #define HNSW_NORM_PROC 2
#define HNSW_NORMALIZE_PROC 3
#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 64
#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
@@ -69,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 */
@@ -107,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;
@@ -148,7 +152,6 @@ typedef struct HnswBuildState
/* Support functions */ /* Support functions */
FmgrInfo *procinfo; FmgrInfo *procinfo;
FmgrInfo *normprocinfo; FmgrInfo *normprocinfo;
FmgrInfo *normalizeprocinfo;
Oid collation; Oid collation;
/* Variables */ /* Variables */
@@ -222,7 +225,6 @@ typedef struct HnswScanOpaqueData
/* Support functions */ /* Support functions */
FmgrInfo *procinfo; FmgrInfo *procinfo;
FmgrInfo *normprocinfo; FmgrInfo *normprocinfo;
FmgrInfo *normalizeprocinfo;
Oid collation; Oid collation;
} HnswScanOpaqueData; } HnswScanOpaqueData;
@@ -258,25 +260,31 @@ typedef struct HnswVacuumState
int HnswGetM(Relation index); int HnswGetM(Relation index);
int HnswGetEfConstruction(Relation index); int HnswGetEfConstruction(Relation index);
FmgrInfo *HnswOptionalProcInfo(Relation rel, uint16 procnum); FmgrInfo *HnswOptionalProcInfo(Relation rel, uint16 procnum);
void HnswNormValue(FmgrInfo *procinfo, FmgrInfo *normalizeprocinfo, Oid collation, Datum *value, Vector * result); bool HnswNormValue(FmgrInfo *procinfo, Oid collation, Datum *value, Vector * result);
void HnswCommitBuffer(Buffer buf, GenericXLogState *state); void HnswCommitBuffer(Buffer buf, GenericXLogState *state);
Buffer HnswNewBuffer(Relation index, ForkNumber forkNum); 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);
@@ -278,13 +279,33 @@ InsertTuple(Relation index, Datum *values, HnswElement element, HnswBuildState *
Datum value = PointerGetDatum(PG_DETOAST_DATUM(values[0])); Datum value = PointerGetDatum(PG_DETOAST_DATUM(values[0]));
/* Normalize if needed */ /* Normalize if needed */
HnswNormValue(buildstate->normprocinfo, buildstate->normalizeprocinfo, collation, &value, buildstate->normvec); if (buildstate->normprocinfo != NULL)
{
if (!HnswNormValue(buildstate->normprocinfo, collation, &value, buildstate->normvec))
return false;
}
/* Copy value to element so accessible outside of memory context */ /* Copy value to element so accessible outside of memory context */
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))
@@ -403,13 +424,15 @@ 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;
/* Get support functions */ /* Get support functions */
buildstate->procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC); buildstate->procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC);
buildstate->normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC); buildstate->normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC);
buildstate->normalizeprocinfo = HnswOptionalProcInfo(index, HNSW_NORMALIZE_PROC);
buildstate->collation = index->rd_indcollation[0]; buildstate->collation = index->rd_indcollation[0];
buildstate->elements = NIL; buildstate->elements = NIL;

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);
} }
/* /*
@@ -417,7 +490,6 @@ HnswInsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heap_ti
{ {
Datum value; Datum value;
FmgrInfo *normprocinfo; FmgrInfo *normprocinfo;
FmgrInfo *normalizeprocinfo;
HnswElement entryPoint; HnswElement entryPoint;
HnswElement element; HnswElement element;
int m = HnswGetM(index); int m = HnswGetM(index);
@@ -425,29 +497,59 @@ 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]));
/* Normalize if needed */ /* Normalize if needed */
normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC); normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC);
normalizeprocinfo = HnswOptionalProcInfo(index, HNSW_NORMALIZE_PROC); if (normprocinfo != NULL)
HnswNormValue(normprocinfo, normalizeprocinfo, collation, &value, NULL); {
if (!HnswNormValue(normprocinfo, collation, &value, NULL))
return false;
}
/* Create an element */ /* Create an element */
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);
} }
/* /*
@@ -78,7 +79,6 @@ hnswbeginscan(Relation index, int nkeys, int norderbys)
/* Set support functions */ /* Set support functions */
so->procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC); so->procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC);
so->normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC); so->normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC);
so->normalizeprocinfo = HnswOptionalProcInfo(index, HNSW_NORMALIZE_PROC);
so->collation = index->rd_indcollation[0]; so->collation = index->rd_indcollation[0];
scan->opaque = so; scan->opaque = so;
@@ -141,10 +141,21 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir)
Assert(!VARATT_IS_EXTENDED(DatumGetPointer(value))); Assert(!VARATT_IS_EXTENDED(DatumGetPointer(value)));
/* Fine if normalization fails */ /* Fine if normalization fails */
HnswNormValue(so->normprocinfo, so->normalizeprocinfo, so->collation, &value, NULL); if (so->normprocinfo != 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)
@@ -47,26 +47,17 @@ HnswOptionalProcInfo(Relation rel, uint16 procnum)
} }
/* /*
* Normalize a vector * Divide by the norm
*
* Returns false if value should not be indexed
* *
* The caller needs to free the pointer stored in value * The caller needs to free the pointer stored in value
* if it's different than the original value * if it's different than the original value
*/ */
void bool
HnswNormValue(FmgrInfo *procinfo, FmgrInfo *normalizeprocinfo, Oid collation, Datum *value, Vector * result) HnswNormValue(FmgrInfo *procinfo, Oid collation, Datum *value, Vector * result)
{ {
double norm; double norm = DatumGetFloat8(FunctionCall1Coll(procinfo, collation, *value));
if (normalizeprocinfo != NULL)
{
*value = FunctionCall1Coll(normalizeprocinfo, collation, *value);
return;
}
if (procinfo == NULL)
return;
norm = DatumGetFloat8(FunctionCall1Coll(procinfo, collation, *value));
if (norm > 0) if (norm > 0)
{ {
@@ -79,7 +70,11 @@ HnswNormValue(FmgrInfo *procinfo, FmgrInfo *normalizeprocinfo, Oid collation, Da
result->x[i] = v->x[i] / norm; result->x[i] = v->x[i] / norm;
*value = PointerGetDatum(result); *value = PointerGetDatum(result);
return true;
} }
return false;
} }
/* /*
@@ -202,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));
@@ -231,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);
@@ -239,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)
{ {
@@ -264,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;
@@ -274,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);
} }
@@ -361,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;
@@ -377,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;
@@ -392,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
*/ */
@@ -412,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)
@@ -452,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));
@@ -460,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;
} }
@@ -527,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;
@@ -563,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))
@@ -576,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];
@@ -599,17 +621,7 @@ 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 */
if (skipPage != NULL && e->element->neighborPage == *skipPage && e->element->neighborOffno == *skipOffno)
continue;
/* Make robust to issues */ /* Make robust to issues */
if (e->element->level < lc) if (e->element->level < lc)
@@ -625,11 +637,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);
}
} }
} }
} }
@@ -708,7 +729,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);
@@ -748,14 +769,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)
@@ -804,183 +825,168 @@ 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 or skipped
*/
static List *
RemoveElements(List *w, HnswElement skipElement)
{
ListCell *lc2;
List *w2 = NIL;
foreach(lc2, w)
{
HnswCandidate *hc = (HnswCandidate *) lfirst(lc2);
/* Skip self for vacuuming update */
if (skipElement != NULL && hc->element->blkno == skipElement->blkno && hc->element->offno == skipElement->offno)
continue;
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;
} }
if (level > entryLevel) if (level > entryLevel)
level = entryLevel; level = entryLevel;
/* Add one for existing element */
if (existing)
efConstruction++;
/* 2nd phase */ /* 2nd phase */
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;
List *lw;
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 or skipped can help with search */
if (removeEntryPoint) /* but should be removed before selecting neighbors */
w = list_delete_ptr(w, entryCandidate); if (index != NULL)
lw = RemoveElements(w, skipElement);
else
lw = w;
neighbors = SelectNeighbors(lw, 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);
} }
/* /*

View File

@@ -75,7 +75,11 @@ AddSample(Datum *values, IvfflatBuildState * buildstate)
* Normalize with KMEANS_NORM_PROC since spherical distance function * Normalize with KMEANS_NORM_PROC since spherical distance function
* expects unit vectors * expects unit vectors
*/ */
IvfflatNormValue(buildstate->kmeansnormprocinfo, buildstate->kmeansnormalizeprocinfo, buildstate->collation, &value, buildstate->normvec); if (buildstate->kmeansnormprocinfo != NULL)
{
if (!IvfflatNormValue(buildstate->kmeansnormprocinfo, buildstate->collation, &value, buildstate->normvec))
return;
}
if (samples->length < targsamples) if (samples->length < targsamples)
{ {
@@ -172,7 +176,11 @@ AddTupleToSort(Relation index, ItemPointer tid, Datum *values, IvfflatBuildState
Datum value = PointerGetDatum(PG_DETOAST_DATUM(values[0])); Datum value = PointerGetDatum(PG_DETOAST_DATUM(values[0]));
/* Normalize if needed */ /* Normalize if needed */
IvfflatNormValue(buildstate->normprocinfo, buildstate->normalizeprocinfo, buildstate->collation, &value, buildstate->normvec); if (buildstate->normprocinfo != NULL)
{
if (!IvfflatNormValue(buildstate->normprocinfo, buildstate->collation, &value, buildstate->normvec))
return;
}
/* Find the list that minimizes the distance */ /* Find the list that minimizes the distance */
for (int i = 0; i < centers->length; i++) for (int i = 0; i < centers->length; i++)
@@ -360,8 +368,6 @@ InitBuildState(IvfflatBuildState * buildstate, Relation heap, Relation index, In
buildstate->procinfo = index_getprocinfo(index, 1, IVFFLAT_DISTANCE_PROC); buildstate->procinfo = index_getprocinfo(index, 1, IVFFLAT_DISTANCE_PROC);
buildstate->normprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_NORM_PROC); buildstate->normprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_NORM_PROC);
buildstate->kmeansnormprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_KMEANS_NORM_PROC); buildstate->kmeansnormprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_KMEANS_NORM_PROC);
buildstate->normalizeprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_NORMALIZE_PROC);
buildstate->kmeansnormalizeprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_KMEANS_NORMALIZE_PROC);
buildstate->collation = index->rd_indcollation[0]; buildstate->collation = index->rd_indcollation[0];
/* Require more than one dimension for spherical k-means */ /* Require more than one dimension for spherical k-means */

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);
} }
/* /*
@@ -194,7 +194,7 @@ ivfflathandler(PG_FUNCTION_ARGS)
IndexAmRoutine *amroutine = makeNode(IndexAmRoutine); IndexAmRoutine *amroutine = makeNode(IndexAmRoutine);
amroutine->amstrategies = 0; amroutine->amstrategies = 0;
amroutine->amsupport = 6; amroutine->amsupport = 4;
#if PG_VERSION_NUM >= 130000 #if PG_VERSION_NUM >= 130000
amroutine->amoptsprocnum = 0; amroutine->amoptsprocnum = 0;
#endif #endif

View File

@@ -31,8 +31,6 @@
#define IVFFLAT_NORM_PROC 2 #define IVFFLAT_NORM_PROC 2
#define IVFFLAT_KMEANS_DISTANCE_PROC 3 #define IVFFLAT_KMEANS_DISTANCE_PROC 3
#define IVFFLAT_KMEANS_NORM_PROC 4 #define IVFFLAT_KMEANS_NORM_PROC 4
#define IVFFLAT_NORMALIZE_PROC 5
#define IVFFLAT_KMEANS_NORMALIZE_PROC 6
#define IVFFLAT_VERSION 1 #define IVFFLAT_VERSION 1
#define IVFFLAT_MAGIC_NUMBER 0x14FF1A7 #define IVFFLAT_MAGIC_NUMBER 0x14FF1A7
@@ -42,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 */
@@ -174,8 +175,6 @@ typedef struct IvfflatBuildState
FmgrInfo *procinfo; FmgrInfo *procinfo;
FmgrInfo *normprocinfo; FmgrInfo *normprocinfo;
FmgrInfo *kmeansnormprocinfo; FmgrInfo *kmeansnormprocinfo;
FmgrInfo *normalizeprocinfo;
FmgrInfo *kmeansnormalizeprocinfo;
Oid collation; Oid collation;
/* Variables */ /* Variables */
@@ -257,7 +256,6 @@ typedef struct IvfflatScanOpaqueData
/* Support functions */ /* Support functions */
FmgrInfo *procinfo; FmgrInfo *procinfo;
FmgrInfo *normprocinfo; FmgrInfo *normprocinfo;
FmgrInfo *normalizeprocinfo;
Oid collation; Oid collation;
/* Lists */ /* Lists */
@@ -278,7 +276,7 @@ void VectorArrayFree(VectorArray arr);
void PrintVectorArray(char *msg, VectorArray arr); void PrintVectorArray(char *msg, VectorArray arr);
void IvfflatKmeans(Relation index, VectorArray samples, VectorArray centers); void IvfflatKmeans(Relation index, VectorArray samples, VectorArray centers);
FmgrInfo *IvfflatOptionalProcInfo(Relation rel, uint16 procnum); FmgrInfo *IvfflatOptionalProcInfo(Relation rel, uint16 procnum);
void IvfflatNormValue(FmgrInfo *procinfo, FmgrInfo *normalizeprocinfo, Oid collation, Datum *value, Vector * result); bool IvfflatNormValue(FmgrInfo *procinfo, Oid collation, Datum *value, Vector * result);
int IvfflatGetLists(Relation index); int IvfflatGetLists(Relation index);
void IvfflatUpdateList(Relation index, ListInfo listInfo, BlockNumber insertPage, BlockNumber originalInsertPage, BlockNumber startPage, ForkNumber forkNum); void IvfflatUpdateList(Relation index, ListInfo listInfo, BlockNumber insertPage, BlockNumber originalInsertPage, BlockNumber startPage, ForkNumber forkNum);
void IvfflatCommitBuffer(Buffer buf, GenericXLogState *state); void IvfflatCommitBuffer(Buffer buf, GenericXLogState *state);

View File

@@ -68,7 +68,6 @@ InsertTuple(Relation rel, Datum *values, bool *isnull, ItemPointer heap_tid, Rel
IndexTuple itup; IndexTuple itup;
Datum value; Datum value;
FmgrInfo *normprocinfo; FmgrInfo *normprocinfo;
FmgrInfo *normalizeprocinfo;
Buffer buf; Buffer buf;
Page page; Page page;
GenericXLogState *state; GenericXLogState *state;
@@ -82,8 +81,11 @@ InsertTuple(Relation rel, Datum *values, bool *isnull, ItemPointer heap_tid, Rel
/* Normalize if needed */ /* Normalize if needed */
normprocinfo = IvfflatOptionalProcInfo(rel, IVFFLAT_NORM_PROC); normprocinfo = IvfflatOptionalProcInfo(rel, IVFFLAT_NORM_PROC);
normalizeprocinfo = IvfflatOptionalProcInfo(rel, IVFFLAT_NORMALIZE_PROC); if (normprocinfo != NULL)
IvfflatNormValue(normprocinfo, normalizeprocinfo, rel->rd_indcollation[0], &value, NULL); {
if (!IvfflatNormValue(normprocinfo, rel->rd_indcollation[0], &value, NULL))
return;
}
/* Find the insert page - sets the page and list info */ /* Find the insert page - sets the page and list info */
FindInsertPage(rel, values, &insertPage, &listInfo); FindInsertPage(rel, values, &insertPage, &listInfo);

View File

@@ -232,7 +232,6 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys)
/* Set support functions */ /* Set support functions */
so->procinfo = index_getprocinfo(index, 1, IVFFLAT_DISTANCE_PROC); so->procinfo = index_getprocinfo(index, 1, IVFFLAT_DISTANCE_PROC);
so->normprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_NORM_PROC); so->normprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_NORM_PROC);
so->normalizeprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_NORMALIZE_PROC);
so->collation = index->rd_indcollation[0]; so->collation = index->rd_indcollation[0];
/* Create tuple description for sorting */ /* Create tuple description for sorting */
@@ -320,7 +319,8 @@ ivfflatgettuple(IndexScanDesc scan, ScanDirection dir)
Assert(!VARATT_IS_EXTENDED(DatumGetPointer(value))); Assert(!VARATT_IS_EXTENDED(DatumGetPointer(value)));
/* Fine if normalization fails */ /* Fine if normalization fails */
IvfflatNormValue(so->normprocinfo, so->normalizeprocinfo, so->collation, &value, NULL); if (so->normprocinfo != NULL)
IvfflatNormValue(so->normprocinfo, so->collation, &value, NULL);
} }
IvfflatBench("GetScanLists", GetScanLists(scan, value)); IvfflatBench("GetScanLists", GetScanLists(scan, value));

View File

@@ -66,26 +66,17 @@ IvfflatOptionalProcInfo(Relation rel, uint16 procnum)
} }
/* /*
* Normalize a vector * Divide by the norm
*
* Returns false if value should not be indexed
* *
* The caller needs to free the pointer stored in value * The caller needs to free the pointer stored in value
* if it's different than the original value * if it's different than the original value
*/ */
void bool
IvfflatNormValue(FmgrInfo *procinfo, FmgrInfo *normalizeprocinfo, Oid collation, Datum *value, Vector * result) IvfflatNormValue(FmgrInfo *procinfo, Oid collation, Datum *value, Vector * result)
{ {
double norm; double norm = DatumGetFloat8(FunctionCall1Coll(procinfo, collation, *value));
if (normalizeprocinfo != NULL)
{
*value = FunctionCall1Coll(normalizeprocinfo, collation, *value);
return;
}
if (procinfo == NULL)
return;
norm = DatumGetFloat8(FunctionCall1Coll(procinfo, collation, *value));
if (norm > 0) if (norm > 0)
{ {
@@ -98,7 +89,11 @@ IvfflatNormValue(FmgrInfo *procinfo, FmgrInfo *normalizeprocinfo, Oid collation,
result->x[i] = v->x[i] / norm; result->x[i] = v->x[i] / norm;
*value = PointerGetDatum(result); *value = PointerGetDatum(result);
return true;
} }
return false;
} }
/* /*

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,62 +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;
#ifdef _MSC_VER
/* Fix precision on Windows */
double normf;
#else
float normf;
#endif
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));
normf = norm;
if (normf > 0)
{
/* Auto-vectorized */
for (int i = 0, imax = a->dim; i < imax; i++)
rx[i] = ax[i] / normf;
/* 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

@@ -9,19 +9,18 @@ SELECT * FROM t ORDER BY val <=> '[3,3,3]';
[1,1,1] [1,1,1]
[1,2,3] [1,2,3]
[1,2,4] [1,2,4]
[0,0,0] (3 rows)
(4 rows)
SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <=> '[0,0,0]') t2; SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <=> '[0,0,0]') t2;
count count
------- -------
4 3
(1 row) (1 row)
SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <=> (SELECT NULL::vector)) t2; SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <=> (SELECT NULL::vector)) t2;
count count
------- -------
4 3
(1 row) (1 row)
DROP TABLE t; DROP TABLE t;

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

@@ -9,19 +9,18 @@ SELECT * FROM t ORDER BY val <=> '[3,3,3]';
[1,1,1] [1,1,1]
[1,2,3] [1,2,3]
[1,2,4] [1,2,4]
[0,0,0] (3 rows)
(4 rows)
SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <=> '[0,0,0]') t2; SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <=> '[0,0,0]') t2;
count count
------- -------
4 3
(1 row) (1 row)
SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <=> (SELECT NULL::vector)) t2; SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <=> (SELECT NULL::vector)) t2;
count count
------- -------
4 3
(1 row) (1 row)
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();

View File

@@ -1,4 +1,4 @@
comment = 'vector data type and ivfflat access method' comment = 'vector data type and ivfflat access method'
default_version = '0.4.4' default_version = '0.5.0'
module_pathname = '$libdir/vector' module_pathname = '$libdir/vector'
relocatable = true relocatable = true