mirror of
https://github.com/pgvector/pgvector.git
synced 2026-07-22 12:07:34 +08:00
Compare commits
84 Commits
v0.7.3
...
hnsw-strea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c01e76f2fa | ||
|
|
ab57217f48 | ||
|
|
5f6e031ccc | ||
|
|
1a1221f905 | ||
|
|
54fa16e3e3 | ||
|
|
40c3e402c7 | ||
|
|
058248fdcc | ||
|
|
73c5145b77 | ||
|
|
ec4a23fe49 | ||
|
|
38207f5640 | ||
|
|
46de265a24 | ||
|
|
b8c27914d4 | ||
|
|
2d85af51a8 | ||
|
|
e0ad441306 | ||
|
|
5776a4d937 | ||
|
|
242a12b7d5 | ||
|
|
1370dd6e86 | ||
|
|
a100dc67e5 | ||
|
|
2df9f24aad | ||
|
|
8e979ed377 | ||
|
|
4e35c6abe3 | ||
|
|
77b3d1f2a8 | ||
|
|
ecd0738728 | ||
|
|
62ffc3641c | ||
|
|
11e4d040d9 | ||
|
|
87ac108bf7 | ||
|
|
b2fa625255 | ||
|
|
a8e699c927 | ||
|
|
91541fece6 | ||
|
|
f3de487da2 | ||
|
|
721d4b7e3f | ||
|
|
28066d8fe4 | ||
|
|
495041e43b | ||
|
|
52c385c03a | ||
|
|
80cbd32dab | ||
|
|
97cf990e0f | ||
|
|
55dc735e1a | ||
|
|
be4e9a9df2 | ||
|
|
d5e8fc96a5 | ||
|
|
6d2af6d3f9 | ||
|
|
a6ab5d07c0 | ||
|
|
aa77346103 | ||
|
|
b0da2d95d9 | ||
|
|
3fb05eb847 | ||
|
|
b738ffecc1 | ||
|
|
7117513532 | ||
|
|
85d877d540 | ||
|
|
05fb382031 | ||
|
|
8e1853fbf3 | ||
|
|
f9d68a061a | ||
|
|
4f8ab574c9 | ||
|
|
a15806196e | ||
|
|
5c9429a0f8 | ||
|
|
4b44d6e745 | ||
|
|
16ca608f42 | ||
|
|
8dde14a736 | ||
|
|
d74d3065bc | ||
|
|
a1b80faa67 | ||
|
|
4af5a127e0 | ||
|
|
d02d71a398 | ||
|
|
2aca04b8de | ||
|
|
e47984e616 | ||
|
|
da7d3959a3 | ||
|
|
dadbbc3758 | ||
|
|
6af0a43d62 | ||
|
|
ffcb90d094 | ||
|
|
8a312c3c8e | ||
|
|
5d86b177ab | ||
|
|
ea99957fae | ||
|
|
4cede1a9c9 | ||
|
|
d0dbc8b4d1 | ||
|
|
bb855e6cb4 | ||
|
|
103ac50f1a | ||
|
|
57fb706242 | ||
|
|
020d3edaa9 | ||
|
|
1e9e355175 | ||
|
|
f1d283f565 | ||
|
|
8684c2ba62 | ||
|
|
6c692ef23f | ||
|
|
bb424e96e7 | ||
|
|
30911edb7a | ||
|
|
5ae2bd9efb | ||
|
|
19215317a4 | ||
|
|
c7ca7f05de |
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -8,17 +8,17 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
|
# - postgres: 18
|
||||||
|
# os: ubuntu-24.04
|
||||||
- postgres: 17
|
- postgres: 17
|
||||||
os: ubuntu-24.04
|
os: ubuntu-24.04
|
||||||
- postgres: 16
|
- postgres: 16
|
||||||
os: ubuntu-24.04
|
os: ubuntu-22.04
|
||||||
- postgres: 15
|
- postgres: 15
|
||||||
os: ubuntu-22.04
|
os: ubuntu-22.04
|
||||||
- postgres: 14
|
- postgres: 14
|
||||||
os: ubuntu-22.04
|
|
||||||
- postgres: 13
|
|
||||||
os: ubuntu-20.04
|
os: ubuntu-20.04
|
||||||
- postgres: 12
|
- postgres: 13
|
||||||
os: ubuntu-20.04
|
os: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,3 +1,16 @@
|
|||||||
|
## 0.8.0 (unreleased)
|
||||||
|
|
||||||
|
- Added support for iterative index scans
|
||||||
|
- Added casts for arrays to `sparsevec`
|
||||||
|
- Improved cost estimation
|
||||||
|
- Reduced memory usage for HNSW index scans
|
||||||
|
- Dropped support for Postgres 12
|
||||||
|
|
||||||
|
## 0.7.4 (2024-08-05)
|
||||||
|
|
||||||
|
- Fixed locking for parallel HNSW index builds
|
||||||
|
- Fixed compilation error with GCC 14 on i386 when SSE2 is not enabled
|
||||||
|
|
||||||
## 0.7.3 (2024-07-22)
|
## 0.7.3 (2024-07-22)
|
||||||
|
|
||||||
- Fixed `failed to add index item` error with `sparsevec`
|
- Fixed `failed to add index item` error with `sparsevec`
|
||||||
|
|||||||
@@ -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.7.3",
|
"version": "0.7.4",
|
||||||
"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.7.3",
|
"version": "0.7.4",
|
||||||
"abstract": "Open-source vector similarity search for Postgres"
|
"abstract": "Open-source vector similarity search for Postgres"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
9
Makefile
9
Makefile
@@ -1,8 +1,9 @@
|
|||||||
EXTENSION = vector
|
EXTENSION = vector
|
||||||
EXTVERSION = 0.7.3
|
EXTVERSION = 0.7.4
|
||||||
|
|
||||||
MODULE_big = vector
|
MODULE_big = vector
|
||||||
DATA = $(wildcard sql/*--*.sql)
|
DATA = $(wildcard sql/*--*--*.sql)
|
||||||
|
DATA_built = sql/$(EXTENSION)--$(EXTVERSION).sql
|
||||||
OBJS = src/bitutils.o src/bitvec.o src/halfutils.o src/halfvec.o 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/sparsevec.o src/vector.o
|
OBJS = src/bitutils.o src/bitvec.o src/halfutils.o src/halfvec.o 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/sparsevec.o src/vector.o
|
||||||
HEADERS = src/halfvec.h src/sparsevec.h src/vector.h
|
HEADERS = src/halfvec.h src/sparsevec.h src/vector.h
|
||||||
|
|
||||||
@@ -42,8 +43,6 @@ all: sql/$(EXTENSION)--$(EXTVERSION).sql
|
|||||||
sql/$(EXTENSION)--$(EXTVERSION).sql: sql/$(EXTENSION).sql
|
sql/$(EXTENSION)--$(EXTVERSION).sql: sql/$(EXTENSION).sql
|
||||||
cp $< $@
|
cp $< $@
|
||||||
|
|
||||||
EXTRA_CLEAN = sql/$(EXTENSION)--$(EXTVERSION).sql
|
|
||||||
|
|
||||||
PG_CONFIG ?= pg_config
|
PG_CONFIG ?= pg_config
|
||||||
PGXS := $(shell $(PG_CONFIG) --pgxs)
|
PGXS := $(shell $(PG_CONFIG) --pgxs)
|
||||||
include $(PGXS)
|
include $(PGXS)
|
||||||
@@ -53,7 +52,7 @@ ifeq ($(PROVE),)
|
|||||||
PROVE = prove
|
PROVE = prove
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# for Postgres 15
|
# for Postgres < 15
|
||||||
PROVE_FLAGS += -I ./test/perl
|
PROVE_FLAGS += -I ./test/perl
|
||||||
|
|
||||||
prove_installcheck:
|
prove_installcheck:
|
||||||
|
|||||||
17
Makefile.win
17
Makefile.win
@@ -1,6 +1,7 @@
|
|||||||
EXTENSION = vector
|
EXTENSION = vector
|
||||||
EXTVERSION = 0.7.3
|
EXTVERSION = 0.7.4
|
||||||
|
|
||||||
|
DATA_built = sql\$(EXTENSION)--$(EXTVERSION).sql
|
||||||
OBJS = src\bitutils.obj src\bitvec.obj src\halfutils.obj src\halfvec.obj 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\sparsevec.obj src\vector.obj
|
OBJS = src\bitutils.obj src\bitvec.obj src\halfutils.obj src\halfvec.obj 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\sparsevec.obj src\vector.obj
|
||||||
HEADERS = src\halfvec.h src\sparsevec.h src\vector.h
|
HEADERS = src\halfvec.h src\sparsevec.h src\vector.h
|
||||||
|
|
||||||
@@ -19,11 +20,6 @@ PG_CFLAGS = $(PG_CFLAGS) $(OPTFLAGS) /O2 /fp:fast
|
|||||||
# https://learn.microsoft.com/en-us/cpp/error-messages/tool-errors/vectorizer-and-parallelizer-messages
|
# https://learn.microsoft.com/en-us/cpp/error-messages/tool-errors/vectorizer-and-parallelizer-messages
|
||||||
# PG_CFLAGS = $(PG_CFLAGS) /Qvec-report:2
|
# PG_CFLAGS = $(PG_CFLAGS) /Qvec-report:2
|
||||||
|
|
||||||
all: sql\$(EXTENSION)--$(EXTVERSION).sql
|
|
||||||
|
|
||||||
sql\$(EXTENSION)--$(EXTVERSION).sql: sql\$(EXTENSION).sql
|
|
||||||
copy sql\$(EXTENSION).sql $@
|
|
||||||
|
|
||||||
# TODO use pg_config
|
# TODO use pg_config
|
||||||
!ifndef PGROOT
|
!ifndef PGROOT
|
||||||
!error PGROOT is not set
|
!error PGROOT is not set
|
||||||
@@ -43,15 +39,18 @@ SHLIB = $(EXTENSION).dll
|
|||||||
|
|
||||||
LIBS = "$(LIBDIR)\postgres.lib"
|
LIBS = "$(LIBDIR)\postgres.lib"
|
||||||
|
|
||||||
|
all: $(SHLIB) $(DATA_built)
|
||||||
|
|
||||||
.c.obj:
|
.c.obj:
|
||||||
$(CC) $(CFLAGS) /c $< /Fo$@
|
$(CC) $(CFLAGS) /c $< /Fo$@
|
||||||
|
|
||||||
$(SHLIB): $(OBJS)
|
$(SHLIB): $(OBJS)
|
||||||
$(CC) $(CFLAGS) $(OBJS) $(LIBS) /link /DLL /OUT:$(SHLIB)
|
$(CC) $(CFLAGS) $(OBJS) $(LIBS) /link /DLL /OUT:$(SHLIB)
|
||||||
|
|
||||||
all: $(SHLIB)
|
sql\$(EXTENSION)--$(EXTVERSION).sql: sql\$(EXTENSION).sql
|
||||||
|
copy sql\$(EXTENSION).sql $@
|
||||||
|
|
||||||
install:
|
install: all
|
||||||
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"
|
||||||
@@ -70,6 +69,6 @@ uninstall:
|
|||||||
|
|
||||||
clean:
|
clean:
|
||||||
del /f $(SHLIB) $(EXTENSION).lib $(EXTENSION).exp
|
del /f $(SHLIB) $(EXTENSION).lib $(EXTENSION).exp
|
||||||
|
del /f $(DATA_built)
|
||||||
del /f $(OBJS)
|
del /f $(OBJS)
|
||||||
del /f sql\$(EXTENSION)--$(EXTVERSION).sql
|
|
||||||
del /f /s /q results regression.diffs regression.out tmp_check tmp_check_iso log output_iso
|
del /f /s /q results regression.diffs regression.out tmp_check tmp_check_iso log output_iso
|
||||||
|
|||||||
75
README.md
75
README.md
@@ -21,7 +21,7 @@ Compile and install the extension (supports Postgres 12+)
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd /tmp
|
cd /tmp
|
||||||
git clone --branch v0.7.3 https://github.com/pgvector/pgvector.git
|
git clone --branch v0.7.4 https://github.com/pgvector/pgvector.git
|
||||||
cd pgvector
|
cd pgvector
|
||||||
make
|
make
|
||||||
make install # may need sudo
|
make install # may need sudo
|
||||||
@@ -46,7 +46,7 @@ Then use `nmake` to build:
|
|||||||
```cmd
|
```cmd
|
||||||
set "PGROOT=C:\Program Files\PostgreSQL\16"
|
set "PGROOT=C:\Program Files\PostgreSQL\16"
|
||||||
cd %TEMP%
|
cd %TEMP%
|
||||||
git clone --branch v0.7.3 https://github.com/pgvector/pgvector.git
|
git clone --branch v0.7.4 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
|
||||||
@@ -106,7 +106,7 @@ Insert vectors
|
|||||||
INSERT INTO items (embedding) VALUES ('[1,2,3]'), ('[4,5,6]');
|
INSERT INTO items (embedding) VALUES ('[1,2,3]'), ('[4,5,6]');
|
||||||
```
|
```
|
||||||
|
|
||||||
Or load vectors in bulk using `COPY` ([example](https://github.com/pgvector/pgvector-python/blob/master/examples/bulk_loading.py))
|
Or load vectors in bulk using `COPY` ([example](https://github.com/pgvector/pgvector-python/blob/master/examples/loading/example.py))
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
COPY items (embedding) FROM STDIN WITH (FORMAT BINARY);
|
COPY items (embedding) FROM STDIN WITH (FORMAT BINARY);
|
||||||
@@ -202,7 +202,7 @@ You can add an index to use approximate nearest neighbor search, which trades so
|
|||||||
|
|
||||||
Supported index types are:
|
Supported index types are:
|
||||||
|
|
||||||
- [HNSW](#hnsw) - added in 0.5.0
|
- [HNSW](#hnsw)
|
||||||
- [IVFFlat](#ivfflat)
|
- [IVFFlat](#ivfflat)
|
||||||
|
|
||||||
## HNSW
|
## HNSW
|
||||||
@@ -445,6 +445,63 @@ Use [partitioning](https://www.postgresql.org/docs/current/ddl-partitioning.html
|
|||||||
CREATE TABLE items (embedding vector(3), category_id int) PARTITION BY LIST(category_id);
|
CREATE TABLE items (embedding vector(3), category_id int) PARTITION BY LIST(category_id);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Streaming Queries [unreleased]
|
||||||
|
|
||||||
|
*Added in 0.8.0*
|
||||||
|
|
||||||
|
With approximate indexes, you can end up with less results than expected due to filtering conditions in the query.
|
||||||
|
|
||||||
|
Starting with 0.8.0, you can enable streaming queries. If too few results from the initial index scan match the query filters, it will resume scanning until enough results are found. This can significantly improve recall (at the cost of speed).
|
||||||
|
|
||||||
|
```tsql
|
||||||
|
SET hnsw.streaming = on;
|
||||||
|
-- or
|
||||||
|
SET ivfflat.streaming = on;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Streaming Options
|
||||||
|
|
||||||
|
Since scanning a large portion of the index is expensive, there are options to control when the scan ends.
|
||||||
|
|
||||||
|
#### HNSW
|
||||||
|
|
||||||
|
Specify the max number of additional tuples visited
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SET hnsw.ef_stream = 10000;
|
||||||
|
```
|
||||||
|
|
||||||
|
The scan will also end if reaches `work_mem`, at which point a notice is shown
|
||||||
|
|
||||||
|
```text
|
||||||
|
NOTICE: hnsw index scan exceeded work_mem after 50000 tuples
|
||||||
|
HINT: Increase work_mem to scan more tuples.
|
||||||
|
```
|
||||||
|
|
||||||
|
Adjust this with:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SET work_mem = '8MB';
|
||||||
|
```
|
||||||
|
|
||||||
|
#### IVFFlat
|
||||||
|
|
||||||
|
Specify the max number of probes
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SET ivfflat.max_probes = 100;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Streaming Order
|
||||||
|
|
||||||
|
With streaming queries, it’s possible for rows to be slightly out of order by distance. For strict ordering, use:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
WITH approx_order AS MATERIALIZED (
|
||||||
|
SELECT *, embedding <-> '[1,2,3]' AS distance FROM items WHERE ... ORDER BY distance LIMIT 5
|
||||||
|
) SELECT * FROM approx_order ORDER BY distance;
|
||||||
|
```
|
||||||
|
|
||||||
## Half-Precision Vectors
|
## Half-Precision Vectors
|
||||||
|
|
||||||
*Added in 0.7.0*
|
*Added in 0.7.0*
|
||||||
@@ -473,7 +530,7 @@ SELECT * FROM items ORDER BY embedding::halfvec(3) <-> '[1,2,3]' LIMIT 5;
|
|||||||
|
|
||||||
## Binary Vectors
|
## Binary Vectors
|
||||||
|
|
||||||
Use the `bit` type to store binary vectors ([example](https://github.com/pgvector/pgvector-python/blob/master/examples/hash_image_search.py))
|
Use the `bit` type to store binary vectors ([example](https://github.com/pgvector/pgvector-python/blob/master/examples/imagehash/example.py))
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
CREATE TABLE items (id bigserial PRIMARY KEY, embedding bit(3));
|
CREATE TABLE items (id bigserial PRIMARY KEY, embedding bit(3));
|
||||||
@@ -551,7 +608,7 @@ SELECT id, content FROM items, plainto_tsquery('hello search') query
|
|||||||
WHERE textsearch @@ query ORDER BY ts_rank_cd(textsearch, query) DESC LIMIT 5;
|
WHERE textsearch @@ query ORDER BY ts_rank_cd(textsearch, query) DESC LIMIT 5;
|
||||||
```
|
```
|
||||||
|
|
||||||
You can use [Reciprocal Rank Fusion](https://github.com/pgvector/pgvector-python/blob/master/examples/hybrid_search_rrf.py) or a [cross-encoder](https://github.com/pgvector/pgvector-python/blob/master/examples/hybrid_search.py) to combine results.
|
You can use [Reciprocal Rank Fusion](https://github.com/pgvector/pgvector-python/blob/master/examples/hybrid_search/rrf.py) or a [cross-encoder](https://github.com/pgvector/pgvector-python/blob/master/examples/hybrid_search/cross_encoder.py) to combine results.
|
||||||
|
|
||||||
## Indexing Subvectors
|
## Indexing Subvectors
|
||||||
|
|
||||||
@@ -597,7 +654,7 @@ Be sure to restart Postgres for changes to take effect.
|
|||||||
|
|
||||||
### Loading
|
### Loading
|
||||||
|
|
||||||
Use `COPY` for bulk loading data ([example](https://github.com/pgvector/pgvector-python/blob/master/examples/bulk_loading.py)).
|
Use `COPY` for bulk loading data ([example](https://github.com/pgvector/pgvector-python/blob/master/examples/loading/example.py)).
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
COPY items (embedding) FROM STDIN WITH (FORMAT BINARY);
|
COPY items (embedding) FROM STDIN WITH (FORMAT BINARY);
|
||||||
@@ -687,7 +744,7 @@ Scale pgvector the same way you scale Postgres.
|
|||||||
|
|
||||||
Scale vertically by increasing memory, CPU, and storage on a single instance. Use existing tools to [tune parameters](#tuning) and [monitor performance](#monitoring).
|
Scale vertically by increasing memory, CPU, and storage on a single instance. Use existing tools to [tune parameters](#tuning) and [monitor performance](#monitoring).
|
||||||
|
|
||||||
Scale horizontally with [replicas](https://www.postgresql.org/docs/current/hot-standby.html), or use [Citus](https://github.com/citusdata/citus) or another approach for sharding ([example](https://github.com/pgvector/pgvector-python/blob/master/examples/citus.py)).
|
Scale horizontally with [replicas](https://www.postgresql.org/docs/current/hot-standby.html), or use [Citus](https://github.com/citusdata/citus) or another approach for sharding ([example](https://github.com/pgvector/pgvector-python/blob/master/examples/citus/example.py)).
|
||||||
|
|
||||||
## Languages
|
## Languages
|
||||||
|
|
||||||
@@ -1051,7 +1108,7 @@ 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.7.3 https://github.com/pgvector/pgvector.git
|
git clone --branch v0.7.4 https://github.com/pgvector/pgvector.git
|
||||||
cd pgvector
|
cd pgvector
|
||||||
docker build --pull --build-arg PG_MAJOR=16 -t myuser/pgvector .
|
docker build --pull --build-arg PG_MAJOR=16 -t myuser/pgvector .
|
||||||
```
|
```
|
||||||
|
|||||||
2
sql/vector--0.7.3--0.7.4.sql
Normal file
2
sql/vector--0.7.3--0.7.4.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
|
||||||
|
\echo Use "ALTER EXTENSION vector UPDATE TO '0.7.4'" to load this file. \quit
|
||||||
26
sql/vector--0.7.4--0.8.0.sql
Normal file
26
sql/vector--0.7.4--0.8.0.sql
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
|
||||||
|
\echo Use "ALTER EXTENSION vector UPDATE TO '0.8.0'" to load this file. \quit
|
||||||
|
|
||||||
|
CREATE FUNCTION array_to_sparsevec(integer[], integer, boolean) RETURNS sparsevec
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION array_to_sparsevec(real[], integer, boolean) RETURNS sparsevec
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION array_to_sparsevec(double precision[], integer, boolean) RETURNS sparsevec
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION array_to_sparsevec(numeric[], integer, boolean) RETURNS sparsevec
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE CAST (integer[] AS sparsevec)
|
||||||
|
WITH FUNCTION array_to_sparsevec(integer[], integer, boolean) AS ASSIGNMENT;
|
||||||
|
|
||||||
|
CREATE CAST (real[] AS sparsevec)
|
||||||
|
WITH FUNCTION array_to_sparsevec(real[], integer, boolean) AS ASSIGNMENT;
|
||||||
|
|
||||||
|
CREATE CAST (double precision[] AS sparsevec)
|
||||||
|
WITH FUNCTION array_to_sparsevec(double precision[], integer, boolean) AS ASSIGNMENT;
|
||||||
|
|
||||||
|
CREATE CAST (numeric[] AS sparsevec)
|
||||||
|
WITH FUNCTION array_to_sparsevec(numeric[], integer, boolean) AS ASSIGNMENT;
|
||||||
@@ -782,6 +782,18 @@ CREATE FUNCTION halfvec_to_sparsevec(halfvec, integer, boolean) RETURNS sparseve
|
|||||||
CREATE FUNCTION sparsevec_to_halfvec(sparsevec, integer, boolean) RETURNS halfvec
|
CREATE FUNCTION sparsevec_to_halfvec(sparsevec, integer, boolean) RETURNS halfvec
|
||||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION array_to_sparsevec(integer[], integer, boolean) RETURNS sparsevec
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION array_to_sparsevec(real[], integer, boolean) RETURNS sparsevec
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION array_to_sparsevec(double precision[], integer, boolean) RETURNS sparsevec
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION array_to_sparsevec(numeric[], integer, boolean) RETURNS sparsevec
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
-- sparsevec casts
|
-- sparsevec casts
|
||||||
|
|
||||||
CREATE CAST (sparsevec AS sparsevec)
|
CREATE CAST (sparsevec AS sparsevec)
|
||||||
@@ -799,6 +811,18 @@ CREATE CAST (sparsevec AS halfvec)
|
|||||||
CREATE CAST (halfvec AS sparsevec)
|
CREATE CAST (halfvec AS sparsevec)
|
||||||
WITH FUNCTION halfvec_to_sparsevec(halfvec, integer, boolean) AS IMPLICIT;
|
WITH FUNCTION halfvec_to_sparsevec(halfvec, integer, boolean) AS IMPLICIT;
|
||||||
|
|
||||||
|
CREATE CAST (integer[] AS sparsevec)
|
||||||
|
WITH FUNCTION array_to_sparsevec(integer[], integer, boolean) AS ASSIGNMENT;
|
||||||
|
|
||||||
|
CREATE CAST (real[] AS sparsevec)
|
||||||
|
WITH FUNCTION array_to_sparsevec(real[], integer, boolean) AS ASSIGNMENT;
|
||||||
|
|
||||||
|
CREATE CAST (double precision[] AS sparsevec)
|
||||||
|
WITH FUNCTION array_to_sparsevec(double precision[], integer, boolean) AS ASSIGNMENT;
|
||||||
|
|
||||||
|
CREATE CAST (numeric[] AS sparsevec)
|
||||||
|
WITH FUNCTION array_to_sparsevec(numeric[], integer, boolean) AS ASSIGNMENT;
|
||||||
|
|
||||||
-- sparsevec operators
|
-- sparsevec operators
|
||||||
|
|
||||||
CREATE OPERATOR <-> (
|
CREATE OPERATOR <-> (
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#include "postgres.h"
|
#include "postgres.h"
|
||||||
|
|
||||||
/* Check version in first header */
|
/* Check version in first header */
|
||||||
#if PG_VERSION_NUM < 120000
|
#if PG_VERSION_NUM < 130000
|
||||||
#error "Requires PostgreSQL 12+"
|
#error "Requires PostgreSQL 13+"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
extern uint64 (*BitHammingDistance) (uint32 bytes, unsigned char *ax, unsigned char *bx, uint64 distance);
|
extern uint64 (*BitHammingDistance) (uint32 bytes, unsigned char *ax, unsigned char *bx, uint64 distance);
|
||||||
|
|||||||
@@ -19,11 +19,6 @@
|
|||||||
#include "utils/numeric.h"
|
#include "utils/numeric.h"
|
||||||
#include "vector.h"
|
#include "vector.h"
|
||||||
|
|
||||||
#if PG_VERSION_NUM < 130000
|
|
||||||
#define TYPALIGN_DOUBLE 'd'
|
|
||||||
#define TYPALIGN_INT 'i'
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define STATE_DIMS(x) (ARR_DIMS(x)[0] - 1)
|
#define STATE_DIMS(x) (ARR_DIMS(x)[0] - 1)
|
||||||
#define CreateStateDatums(dim) palloc(sizeof(Datum) * (dim + 1))
|
#define CreateStateDatums(dim) palloc(sizeof(Datum) * (dim + 1))
|
||||||
|
|
||||||
@@ -164,24 +159,6 @@ CheckStateArray(ArrayType *statearray, const char *caller)
|
|||||||
return (float8 *) ARR_DATA_PTR(statearray);
|
return (float8 *) ARR_DATA_PTR(statearray);
|
||||||
}
|
}
|
||||||
|
|
||||||
#if PG_VERSION_NUM < 120003
|
|
||||||
static pg_noinline void
|
|
||||||
float_overflow_error(void)
|
|
||||||
{
|
|
||||||
ereport(ERROR,
|
|
||||||
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
|
||||||
errmsg("value out of range: overflow")));
|
|
||||||
}
|
|
||||||
|
|
||||||
static pg_noinline void
|
|
||||||
float_underflow_error(void)
|
|
||||||
{
|
|
||||||
ereport(ERROR,
|
|
||||||
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
|
||||||
errmsg("value out of range: underflow")));
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Convert textual representation to internal representation
|
* Convert textual representation to internal representation
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
/* F16C has better performance than _Float16 (on x86-64) */
|
/* F16C has better performance than _Float16 (on x86-64) */
|
||||||
#if defined(__F16C__)
|
#if defined(__F16C__)
|
||||||
#define F16C_SUPPORT
|
#define F16C_SUPPORT
|
||||||
#elif defined(__FLT16_MAX__) && !defined(HALFVEC_DISPATCH) && !defined(__FreeBSD__)
|
#elif defined(__FLT16_MAX__) && !defined(HALFVEC_DISPATCH) && !defined(__FreeBSD__) && (!defined(__i386__) || defined(__SSE2__))
|
||||||
#define FLT16_SUPPORT
|
#define FLT16_SUPPORT
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
145
src/hnsw.c
145
src/hnsw.c
@@ -9,14 +9,18 @@
|
|||||||
#include "commands/vacuum.h"
|
#include "commands/vacuum.h"
|
||||||
#include "hnsw.h"
|
#include "hnsw.h"
|
||||||
#include "miscadmin.h"
|
#include "miscadmin.h"
|
||||||
|
#include "utils/float.h"
|
||||||
#include "utils/guc.h"
|
#include "utils/guc.h"
|
||||||
#include "utils/selfuncs.h"
|
#include "utils/selfuncs.h"
|
||||||
|
#include "utils/spccache.h"
|
||||||
|
|
||||||
#if PG_VERSION_NUM < 150000
|
#if PG_VERSION_NUM < 150000
|
||||||
#define MarkGUCPrefixReserved(x) EmitWarningsOnPlaceholders(x)
|
#define MarkGUCPrefixReserved(x) EmitWarningsOnPlaceholders(x)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
int hnsw_ef_search;
|
int hnsw_ef_search;
|
||||||
|
int hnsw_ef_stream;
|
||||||
|
bool hnsw_streaming;
|
||||||
int hnsw_lock_tranche_id;
|
int hnsw_lock_tranche_id;
|
||||||
static relopt_kind hnsw_relopt_kind;
|
static relopt_kind hnsw_relopt_kind;
|
||||||
|
|
||||||
@@ -59,22 +63,25 @@ HnswInit(void)
|
|||||||
|
|
||||||
hnsw_relopt_kind = add_reloption_kind();
|
hnsw_relopt_kind = add_reloption_kind();
|
||||||
add_int_reloption(hnsw_relopt_kind, "m", "Max number of connections",
|
add_int_reloption(hnsw_relopt_kind, "m", "Max number of connections",
|
||||||
HNSW_DEFAULT_M, HNSW_MIN_M, HNSW_MAX_M
|
HNSW_DEFAULT_M, HNSW_MIN_M, HNSW_MAX_M, AccessExclusiveLock);
|
||||||
#if PG_VERSION_NUM >= 130000
|
|
||||||
,AccessExclusiveLock
|
|
||||||
#endif
|
|
||||||
);
|
|
||||||
add_int_reloption(hnsw_relopt_kind, "ef_construction", "Size of the dynamic candidate list for construction",
|
add_int_reloption(hnsw_relopt_kind, "ef_construction", "Size of the dynamic candidate list for construction",
|
||||||
HNSW_DEFAULT_EF_CONSTRUCTION, HNSW_MIN_EF_CONSTRUCTION, HNSW_MAX_EF_CONSTRUCTION
|
HNSW_DEFAULT_EF_CONSTRUCTION, HNSW_MIN_EF_CONSTRUCTION, HNSW_MAX_EF_CONSTRUCTION, AccessExclusiveLock);
|
||||||
#if PG_VERSION_NUM >= 130000
|
|
||||||
,AccessExclusiveLock
|
|
||||||
#endif
|
|
||||||
);
|
|
||||||
|
|
||||||
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 1..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);
|
||||||
|
|
||||||
|
/* TODO Figure out name */
|
||||||
|
DefineCustomBoolVariable("hnsw.streaming", "Use streaming mode",
|
||||||
|
NULL, &hnsw_streaming,
|
||||||
|
HNSW_DEFAULT_STREAMING, PGC_USERSET, 0, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
/* TODO Figure out name */
|
||||||
|
/* TODO Use same value as ivfflat.max_probes for "all" */
|
||||||
|
DefineCustomIntVariable("hnsw.ef_stream", "Sets the max number of additional candidates to visit for streaming search",
|
||||||
|
"-1 means all", &hnsw_ef_stream,
|
||||||
|
HNSW_DEFAULT_EF_STREAM, HNSW_MIN_EF_STREAM, HNSW_MAX_EF_STREAM, PGC_USERSET, 0, NULL, NULL, NULL);
|
||||||
|
|
||||||
MarkGUCPrefixReserved("hnsw");
|
MarkGUCPrefixReserved("hnsw");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +102,33 @@ hnswbuildphasename(int64 phasenum)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Estimate ef needed for iterative scans
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
EstimateEf(PlannerInfo *root, IndexPath *path)
|
||||||
|
{
|
||||||
|
double selectivity = 1;
|
||||||
|
ListCell *lc;
|
||||||
|
|
||||||
|
/* Cannot estimate without limit */
|
||||||
|
/* limit_tuples includes offset */
|
||||||
|
if (root->limit_tuples < 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
/* Get the selectivity of non-index conditions */
|
||||||
|
foreach(lc, path->indexinfo->indrestrictinfo)
|
||||||
|
{
|
||||||
|
RestrictInfo *rinfo = lfirst(lc);
|
||||||
|
|
||||||
|
/* Skip DEFAULT_INEQ_SEL since it may be a distance filter */
|
||||||
|
if (rinfo->norm_selec >= 0 && rinfo->norm_selec <= 1 && rinfo->norm_selec != (Selectivity) DEFAULT_INEQ_SEL)
|
||||||
|
selectivity *= rinfo->norm_selec;
|
||||||
|
}
|
||||||
|
|
||||||
|
return root->limit_tuples / Max(selectivity, 0.00001);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Estimate the cost of an index scan
|
* Estimate the cost of an index scan
|
||||||
*/
|
*/
|
||||||
@@ -106,14 +140,19 @@ hnswcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
|
|||||||
{
|
{
|
||||||
GenericCosts costs;
|
GenericCosts costs;
|
||||||
int m;
|
int m;
|
||||||
|
int ef;
|
||||||
int entryLevel;
|
int entryLevel;
|
||||||
|
int layer0TuplesMax;
|
||||||
|
double layer0Selectivity;
|
||||||
|
double scalingFactor = 0.55;
|
||||||
|
double spc_seq_page_cost;
|
||||||
Relation index;
|
Relation index;
|
||||||
|
|
||||||
/* Never use index without order */
|
/* Never use index without order */
|
||||||
if (path->indexorderbys == NULL)
|
if (path->indexorderbys == NULL)
|
||||||
{
|
{
|
||||||
*indexStartupCost = DBL_MAX;
|
*indexStartupCost = get_float8_infinity();
|
||||||
*indexTotalCost = DBL_MAX;
|
*indexTotalCost = get_float8_infinity();
|
||||||
*indexSelectivity = 0;
|
*indexSelectivity = 0;
|
||||||
*indexCorrelation = 0;
|
*indexCorrelation = 0;
|
||||||
*indexPages = 0;
|
*indexPages = 0;
|
||||||
@@ -126,15 +165,57 @@ hnswcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
|
|||||||
HnswGetMetaPageInfo(index, &m, NULL);
|
HnswGetMetaPageInfo(index, &m, NULL);
|
||||||
index_close(index, NoLock);
|
index_close(index, NoLock);
|
||||||
|
|
||||||
/* Approximate entry level */
|
ef = hnsw_streaming ? Max(hnsw_ef_search, EstimateEf(root, path)) : hnsw_ef_search;
|
||||||
entryLevel = (int) -log(1.0 / path->indexinfo->tuples) * HnswGetMl(m);
|
|
||||||
|
|
||||||
/* TODO Improve estimate of visited tuples (currently underestimates) */
|
/*
|
||||||
/* Account for number of tuples (or entry level), m, and ef_search */
|
* HNSW cost estimation follows a formula that accounts for the total
|
||||||
costs.numIndexTuples = (entryLevel + 2) * m;
|
* number of tuples indexed combined with the parameters that most
|
||||||
|
* influence the duration of the index scan, namely: m - the number of
|
||||||
|
* tuples that are scanned in each step of the HNSW graph traversal
|
||||||
|
* ef_search - which influences the total number of steps taken at layer 0
|
||||||
|
*
|
||||||
|
* The source of the vector data can impact how many steps it takes to
|
||||||
|
* converge on the set of vectors to return to the executor. Currently, we
|
||||||
|
* use a hardcoded scaling factor (HNSWScanScalingFactor) to help
|
||||||
|
* influence that, but this could later become a configurable parameter
|
||||||
|
* based on the cost estimations.
|
||||||
|
*
|
||||||
|
* The tuple estimator formula is below:
|
||||||
|
*
|
||||||
|
* numIndexTuples = entryLevel * m + layer0TuplesMax * layer0Selectivity
|
||||||
|
*
|
||||||
|
* "entryLevel * m" represents the floor of tuples we need to scan to get
|
||||||
|
* to layer 0 (L0).
|
||||||
|
*
|
||||||
|
* "layer0TuplesMax" is the estimated total number of tuples we'd scan at
|
||||||
|
* L0 if we weren't discarding already visited tuples as part of the scan.
|
||||||
|
*
|
||||||
|
* "layer0Selectivity" estimates the percentage of tuples that are scanned
|
||||||
|
* at L0, accounting for previously visited tuples, multiplied by the
|
||||||
|
* "scalingFactor" (currently hardcoded).
|
||||||
|
*/
|
||||||
|
entryLevel = (int) (log(path->indexinfo->tuples + 1) * HnswGetMl(m));
|
||||||
|
layer0TuplesMax = HnswGetLayerM(m, 0) * ef;
|
||||||
|
layer0Selectivity = (scalingFactor * log(path->indexinfo->tuples + 1)) /
|
||||||
|
(log(m) * (1 + log(ef)));
|
||||||
|
|
||||||
|
costs.numIndexTuples = (entryLevel * m) +
|
||||||
|
(layer0TuplesMax * layer0Selectivity);
|
||||||
|
|
||||||
genericcostestimate(root, path, loop_count, &costs);
|
genericcostestimate(root, path, loop_count, &costs);
|
||||||
|
|
||||||
|
get_tablespace_page_costs(path->indexinfo->reltablespace, NULL, &spc_seq_page_cost);
|
||||||
|
|
||||||
|
/* Adjust cost if needed since TOAST not included in seq scan cost */
|
||||||
|
if (costs.numIndexPages > path->indexinfo->rel->pages)
|
||||||
|
{
|
||||||
|
/* Change all page cost from random to sequential */
|
||||||
|
costs.indexTotalCost -= costs.numIndexPages * (costs.spc_random_page_cost - spc_seq_page_cost);
|
||||||
|
|
||||||
|
/* Remove cost of extra pages */
|
||||||
|
costs.indexTotalCost -= (costs.numIndexPages - path->indexinfo->rel->pages) * spc_seq_page_cost;
|
||||||
|
}
|
||||||
|
|
||||||
/* Use total cost since most work happens before first tuple is returned */
|
/* Use total cost since most work happens before first tuple is returned */
|
||||||
*indexStartupCost = costs.indexTotalCost;
|
*indexStartupCost = costs.indexTotalCost;
|
||||||
*indexTotalCost = costs.indexTotalCost;
|
*indexTotalCost = costs.indexTotalCost;
|
||||||
@@ -154,23 +235,10 @@ hnswoptions(Datum reloptions, bool validate)
|
|||||||
{"ef_construction", RELOPT_TYPE_INT, offsetof(HnswOptions, efConstruction)},
|
{"ef_construction", RELOPT_TYPE_INT, offsetof(HnswOptions, efConstruction)},
|
||||||
};
|
};
|
||||||
|
|
||||||
#if PG_VERSION_NUM >= 130000
|
|
||||||
return (bytea *) build_reloptions(reloptions, validate,
|
return (bytea *) build_reloptions(reloptions, validate,
|
||||||
hnsw_relopt_kind,
|
hnsw_relopt_kind,
|
||||||
sizeof(HnswOptions),
|
sizeof(HnswOptions),
|
||||||
tab, lengthof(tab));
|
tab, lengthof(tab));
|
||||||
#else
|
|
||||||
relopt_value *options;
|
|
||||||
int numoptions;
|
|
||||||
HnswOptions *rdopts;
|
|
||||||
|
|
||||||
options = parseRelOptions(reloptions, validate, hnsw_relopt_kind, &numoptions);
|
|
||||||
rdopts = allocateReloptStruct(sizeof(HnswOptions), options, numoptions);
|
|
||||||
fillRelOptions((void *) rdopts, sizeof(HnswOptions), options, numoptions,
|
|
||||||
validate, tab, lengthof(tab));
|
|
||||||
|
|
||||||
return (bytea *) rdopts;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -195,9 +263,7 @@ hnswhandler(PG_FUNCTION_ARGS)
|
|||||||
|
|
||||||
amroutine->amstrategies = 0;
|
amroutine->amstrategies = 0;
|
||||||
amroutine->amsupport = 3;
|
amroutine->amsupport = 3;
|
||||||
#if PG_VERSION_NUM >= 130000
|
|
||||||
amroutine->amoptsprocnum = 0;
|
amroutine->amoptsprocnum = 0;
|
||||||
#endif
|
|
||||||
amroutine->amcanorder = false;
|
amroutine->amcanorder = false;
|
||||||
amroutine->amcanorderbyop = true;
|
amroutine->amcanorderbyop = true;
|
||||||
amroutine->amcanbackward = false; /* can change direction mid-scan */
|
amroutine->amcanbackward = false; /* can change direction mid-scan */
|
||||||
@@ -210,17 +276,24 @@ hnswhandler(PG_FUNCTION_ARGS)
|
|||||||
amroutine->amclusterable = false;
|
amroutine->amclusterable = false;
|
||||||
amroutine->ampredlocks = false;
|
amroutine->ampredlocks = false;
|
||||||
amroutine->amcanparallel = false;
|
amroutine->amcanparallel = false;
|
||||||
amroutine->amcaninclude = false;
|
#if PG_VERSION_NUM >= 170000
|
||||||
#if PG_VERSION_NUM >= 130000
|
amroutine->amcanbuildparallel = true;
|
||||||
amroutine->amusemaintenanceworkmem = false; /* not used during VACUUM */
|
|
||||||
amroutine->amparallelvacuumoptions = VACUUM_OPTION_PARALLEL_BULKDEL;
|
|
||||||
#endif
|
#endif
|
||||||
|
amroutine->amcaninclude = false;
|
||||||
|
amroutine->amusemaintenanceworkmem = false; /* not used during VACUUM */
|
||||||
|
#if PG_VERSION_NUM >= 160000
|
||||||
|
amroutine->amsummarizing = false;
|
||||||
|
#endif
|
||||||
|
amroutine->amparallelvacuumoptions = VACUUM_OPTION_PARALLEL_BULKDEL;
|
||||||
amroutine->amkeytype = InvalidOid;
|
amroutine->amkeytype = InvalidOid;
|
||||||
|
|
||||||
/* Interface functions */
|
/* Interface functions */
|
||||||
amroutine->ambuild = hnswbuild;
|
amroutine->ambuild = hnswbuild;
|
||||||
amroutine->ambuildempty = hnswbuildempty;
|
amroutine->ambuildempty = hnswbuildempty;
|
||||||
amroutine->aminsert = hnswinsert;
|
amroutine->aminsert = hnswinsert;
|
||||||
|
#if PG_VERSION_NUM >= 170000
|
||||||
|
amroutine->aminsertcleanup = NULL;
|
||||||
|
#endif
|
||||||
amroutine->ambulkdelete = hnswbulkdelete;
|
amroutine->ambulkdelete = hnswbulkdelete;
|
||||||
amroutine->amvacuumcleanup = hnswvacuumcleanup;
|
amroutine->amvacuumcleanup = hnswvacuumcleanup;
|
||||||
amroutine->amcanreturn = NULL;
|
amroutine->amcanreturn = NULL;
|
||||||
|
|||||||
67
src/hnsw.h
67
src/hnsw.h
@@ -12,6 +12,10 @@
|
|||||||
#include "utils/sampling.h"
|
#include "utils/sampling.h"
|
||||||
#include "vector.h"
|
#include "vector.h"
|
||||||
|
|
||||||
|
#ifdef HNSW_BENCH
|
||||||
|
#include "portability/instr_time.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#define HNSW_MAX_DIM 2000
|
#define HNSW_MAX_DIM 2000
|
||||||
#define HNSW_MAX_NNZ 1000
|
#define HNSW_MAX_NNZ 1000
|
||||||
|
|
||||||
@@ -42,6 +46,10 @@
|
|||||||
#define HNSW_DEFAULT_EF_SEARCH 40
|
#define HNSW_DEFAULT_EF_SEARCH 40
|
||||||
#define HNSW_MIN_EF_SEARCH 1
|
#define HNSW_MIN_EF_SEARCH 1
|
||||||
#define HNSW_MAX_EF_SEARCH 1000
|
#define HNSW_MAX_EF_SEARCH 1000
|
||||||
|
#define HNSW_DEFAULT_STREAMING false
|
||||||
|
#define HNSW_DEFAULT_EF_STREAM -1
|
||||||
|
#define HNSW_MIN_EF_STREAM -1
|
||||||
|
#define HNSW_MAX_EF_STREAM INT_MAX
|
||||||
|
|
||||||
/* Tuple types */
|
/* Tuple types */
|
||||||
#define HNSW_ELEMENT_TUPLE_TYPE 1
|
#define HNSW_ELEMENT_TUPLE_TYPE 1
|
||||||
@@ -68,6 +76,21 @@
|
|||||||
#define HnswPageGetOpaque(page) ((HnswPageOpaque) PageGetSpecialPointer(page))
|
#define HnswPageGetOpaque(page) ((HnswPageOpaque) PageGetSpecialPointer(page))
|
||||||
#define HnswPageGetMeta(page) ((HnswMetaPageData *) PageGetContents(page))
|
#define HnswPageGetMeta(page) ((HnswMetaPageData *) PageGetContents(page))
|
||||||
|
|
||||||
|
#ifdef HNSW_BENCH
|
||||||
|
#define HnswBench(name, code) \
|
||||||
|
do { \
|
||||||
|
instr_time start; \
|
||||||
|
instr_time duration; \
|
||||||
|
INSTR_TIME_SET_CURRENT(start); \
|
||||||
|
(code); \
|
||||||
|
INSTR_TIME_SET_CURRENT(duration); \
|
||||||
|
INSTR_TIME_SUBTRACT(duration, start); \
|
||||||
|
elog(INFO, "%s: %.3f ms", name, INSTR_TIME_GET_MILLISEC(duration)); \
|
||||||
|
} while (0)
|
||||||
|
#else
|
||||||
|
#define HnswBench(name, code) (code)
|
||||||
|
#endif
|
||||||
|
|
||||||
#if PG_VERSION_NUM >= 150000
|
#if PG_VERSION_NUM >= 150000
|
||||||
#define RandomDouble() pg_prng_double(&pg_global_prng_state)
|
#define RandomDouble() pg_prng_double(&pg_global_prng_state)
|
||||||
#define SeedRandom(seed) pg_prng_seed(&pg_global_prng_state, seed)
|
#define SeedRandom(seed) pg_prng_seed(&pg_global_prng_state, seed)
|
||||||
@@ -76,11 +99,6 @@
|
|||||||
#define SeedRandom(seed) srandom(seed)
|
#define SeedRandom(seed) srandom(seed)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if PG_VERSION_NUM < 130000
|
|
||||||
#define list_delete_last(list) list_truncate(list, list_length(list) - 1)
|
|
||||||
#define list_sort(list, cmp) ((list) = list_qsort(list, cmp))
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#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)
|
||||||
|
|
||||||
@@ -111,6 +129,8 @@
|
|||||||
|
|
||||||
/* Variables */
|
/* Variables */
|
||||||
extern int hnsw_ef_search;
|
extern int hnsw_ef_search;
|
||||||
|
extern int hnsw_ef_stream;
|
||||||
|
extern bool hnsw_streaming;
|
||||||
extern int hnsw_lock_tranche_id;
|
extern int hnsw_lock_tranche_id;
|
||||||
|
|
||||||
typedef struct HnswElementData HnswElementData;
|
typedef struct HnswElementData HnswElementData;
|
||||||
@@ -134,6 +154,7 @@ struct HnswElementData
|
|||||||
uint8 heaptidsLength;
|
uint8 heaptidsLength;
|
||||||
uint8 level;
|
uint8 level;
|
||||||
uint8 deleted;
|
uint8 deleted;
|
||||||
|
uint8 version;
|
||||||
uint32 hash;
|
uint32 hash;
|
||||||
HnswNeighborsPtr neighbors;
|
HnswNeighborsPtr neighbors;
|
||||||
BlockNumber blkno;
|
BlockNumber blkno;
|
||||||
@@ -160,11 +181,16 @@ struct HnswNeighborArray
|
|||||||
HnswCandidate items[FLEXIBLE_ARRAY_MEMBER];
|
HnswCandidate items[FLEXIBLE_ARRAY_MEMBER];
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef struct HnswPairingHeapNode
|
typedef struct HnswSearchCandidate
|
||||||
{
|
{
|
||||||
pairingheap_node ph_node;
|
pairingheap_node c_node;
|
||||||
HnswCandidate *inner;
|
pairingheap_node w_node;
|
||||||
} HnswPairingHeapNode;
|
HnswElementPtr element;
|
||||||
|
float distance;
|
||||||
|
} HnswSearchCandidate;
|
||||||
|
|
||||||
|
#define HnswGetSearchCandidate(membername, ptr) pairingheap_container(HnswSearchCandidate, membername, ptr)
|
||||||
|
#define HnswGetSearchCandidateConst(membername, ptr) pairingheap_const_container(HnswSearchCandidate, membername, ptr)
|
||||||
|
|
||||||
/* HNSW index options */
|
/* HNSW index options */
|
||||||
typedef struct HnswOptions
|
typedef struct HnswOptions
|
||||||
@@ -309,10 +335,10 @@ typedef struct HnswElementTupleData
|
|||||||
uint8 type;
|
uint8 type;
|
||||||
uint8 level;
|
uint8 level;
|
||||||
uint8 deleted;
|
uint8 deleted;
|
||||||
uint8 unused;
|
uint8 version;
|
||||||
ItemPointerData heaptids[HNSW_HEAPTIDS];
|
ItemPointerData heaptids[HNSW_HEAPTIDS];
|
||||||
ItemPointerData neighbortid;
|
ItemPointerData neighbortid;
|
||||||
uint16 unused2;
|
uint16 unused;
|
||||||
Vector data;
|
Vector data;
|
||||||
} HnswElementTupleData;
|
} HnswElementTupleData;
|
||||||
|
|
||||||
@@ -321,18 +347,31 @@ typedef HnswElementTupleData * HnswElementTuple;
|
|||||||
typedef struct HnswNeighborTupleData
|
typedef struct HnswNeighborTupleData
|
||||||
{
|
{
|
||||||
uint8 type;
|
uint8 type;
|
||||||
uint8 unused;
|
uint8 version;
|
||||||
uint16 count;
|
uint16 count;
|
||||||
ItemPointerData indextids[FLEXIBLE_ARRAY_MEMBER];
|
ItemPointerData indextids[FLEXIBLE_ARRAY_MEMBER];
|
||||||
} HnswNeighborTupleData;
|
} HnswNeighborTupleData;
|
||||||
|
|
||||||
typedef HnswNeighborTupleData * HnswNeighborTuple;
|
typedef HnswNeighborTupleData * HnswNeighborTuple;
|
||||||
|
|
||||||
|
typedef union
|
||||||
|
{
|
||||||
|
struct pointerhash_hash *pointers;
|
||||||
|
struct offsethash_hash *offsets;
|
||||||
|
struct tidhash_hash *tids;
|
||||||
|
} visited_hash;
|
||||||
|
|
||||||
typedef struct HnswScanOpaqueData
|
typedef struct HnswScanOpaqueData
|
||||||
{
|
{
|
||||||
const HnswTypeInfo *typeInfo;
|
const HnswTypeInfo *typeInfo;
|
||||||
bool first;
|
bool first;
|
||||||
List *w;
|
List *w;
|
||||||
|
visited_hash v;
|
||||||
|
pairingheap *discarded;
|
||||||
|
Datum q;
|
||||||
|
int m;
|
||||||
|
int64 tuples;
|
||||||
|
double previousDistance;
|
||||||
MemoryContext tmpCtx;
|
MemoryContext tmpCtx;
|
||||||
|
|
||||||
/* Support functions */
|
/* Support functions */
|
||||||
@@ -378,14 +417,14 @@ bool HnswCheckNorm(FmgrInfo *procinfo, Oid collation, Datum value);
|
|||||||
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 HnswInit(void);
|
void HnswInit(void);
|
||||||
List *HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, FmgrInfo *procinfo, Oid collation, int m, bool inserting, HnswElement skipElement);
|
List *HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, FmgrInfo *procinfo, Oid collation, int m, bool inserting, HnswElement skipElement, visited_hash * v, pairingheap **discarded, bool initVisited, int64 *tuples);
|
||||||
HnswElement HnswGetEntryPoint(Relation index);
|
HnswElement HnswGetEntryPoint(Relation index);
|
||||||
void HnswGetMetaPageInfo(Relation index, int *m, HnswElement * entryPoint);
|
void HnswGetMetaPageInfo(Relation index, int *m, HnswElement * entryPoint);
|
||||||
void *HnswAlloc(HnswAllocator * allocator, Size size);
|
void *HnswAlloc(HnswAllocator * allocator, Size size);
|
||||||
HnswElement HnswInitElement(char *base, ItemPointer tid, int m, double ml, int maxLevel, HnswAllocator * alloc);
|
HnswElement HnswInitElement(char *base, ItemPointer tid, int m, double ml, int maxLevel, HnswAllocator * alloc);
|
||||||
HnswElement HnswInitElementFromBlock(BlockNumber blkno, OffsetNumber offno);
|
HnswElement HnswInitElementFromBlock(BlockNumber blkno, OffsetNumber offno);
|
||||||
void HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint, Relation index, FmgrInfo *procinfo, Oid collation, int m, int efConstruction, bool existing);
|
void HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint, Relation index, FmgrInfo *procinfo, Oid collation, int m, int efConstruction, bool existing);
|
||||||
HnswCandidate *HnswEntryCandidate(char *base, HnswElement em, Datum q, Relation rel, FmgrInfo *procinfo, Oid collation, bool loadVec);
|
HnswSearchCandidate *HnswEntryCandidate(char *base, HnswElement em, Datum q, Relation rel, FmgrInfo *procinfo, Oid collation, bool loadVec);
|
||||||
void HnswUpdateMetaPage(Relation index, int updateEntry, HnswElement entryPoint, BlockNumber insertPage, ForkNumber forkNum, bool building);
|
void HnswUpdateMetaPage(Relation index, int updateEntry, HnswElement entryPoint, BlockNumber insertPage, ForkNumber forkNum, bool building);
|
||||||
void HnswSetNeighborTuple(char *base, HnswNeighborTuple ntup, HnswElement e, int m);
|
void HnswSetNeighborTuple(char *base, HnswNeighborTuple ntup, HnswElement e, int m);
|
||||||
void HnswAddHeapTid(HnswElement element, ItemPointer heaptid);
|
void HnswAddHeapTid(HnswElement element, ItemPointer heaptid);
|
||||||
|
|||||||
@@ -60,12 +60,6 @@
|
|||||||
#include "pgstat.h"
|
#include "pgstat.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if PG_VERSION_NUM >= 130000
|
|
||||||
#define CALLBACK_ITEM_POINTER ItemPointer tid
|
|
||||||
#else
|
|
||||||
#define CALLBACK_ITEM_POINTER HeapTuple hup
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if PG_VERSION_NUM >= 140000
|
#if PG_VERSION_NUM >= 140000
|
||||||
#include "utils/backend_status.h"
|
#include "utils/backend_status.h"
|
||||||
#include "utils/wait_event.h"
|
#include "utils/wait_event.h"
|
||||||
@@ -75,10 +69,6 @@
|
|||||||
#define PARALLEL_KEY_HNSW_AREA UINT64CONST(0xA000000000000002)
|
#define PARALLEL_KEY_HNSW_AREA UINT64CONST(0xA000000000000002)
|
||||||
#define PARALLEL_KEY_QUERY_TEXT UINT64CONST(0xA000000000000003)
|
#define PARALLEL_KEY_QUERY_TEXT UINT64CONST(0xA000000000000003)
|
||||||
|
|
||||||
#if PG_VERSION_NUM < 130000
|
|
||||||
#define GENERATIONCHUNK_RAWSIZE (SIZEOF_SIZE_T + SIZEOF_VOID_P * 2)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Create the metapage
|
* Create the metapage
|
||||||
*/
|
*/
|
||||||
@@ -192,7 +182,9 @@ CreateGraphPages(HnswBuildState * buildstate)
|
|||||||
|
|
||||||
/* Initial size check */
|
/* Initial size check */
|
||||||
if (etupSize > HNSW_TUPLE_ALLOC_SIZE)
|
if (etupSize > HNSW_TUPLE_ALLOC_SIZE)
|
||||||
elog(ERROR, "index tuple too large");
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
|
||||||
|
errmsg("index tuple too large")));
|
||||||
|
|
||||||
HnswSetElementTuple(base, etup, element);
|
HnswSetElementTuple(base, etup, element);
|
||||||
|
|
||||||
@@ -379,7 +371,13 @@ UpdateNeighborsInMemory(char *base, FmgrInfo *procinfo, Oid collation, HnswEleme
|
|||||||
for (int lc = e->level; lc >= 0; lc--)
|
for (int lc = e->level; lc >= 0; lc--)
|
||||||
{
|
{
|
||||||
int lm = HnswGetLayerM(m, lc);
|
int lm = HnswGetLayerM(m, lc);
|
||||||
HnswNeighborArray *neighbors = HnswGetNeighbors(base, e, lc);
|
Size neighborsSize = HNSW_NEIGHBOR_ARRAY_SIZE(lm);
|
||||||
|
HnswNeighborArray *neighbors = palloc(neighborsSize);
|
||||||
|
|
||||||
|
/* Copy neighbors to local memory */
|
||||||
|
LWLockAcquire(&e->lock, LW_SHARED);
|
||||||
|
memcpy(neighbors, HnswGetNeighbors(base, e, lc), neighborsSize);
|
||||||
|
LWLockRelease(&e->lock);
|
||||||
|
|
||||||
for (int i = 0; i < neighbors->length; i++)
|
for (int i = 0; i < neighbors->length; i++)
|
||||||
{
|
{
|
||||||
@@ -389,7 +387,6 @@ UpdateNeighborsInMemory(char *base, FmgrInfo *procinfo, Oid collation, HnswEleme
|
|||||||
/* Keep scan-build happy on Mac x86-64 */
|
/* Keep scan-build happy on Mac x86-64 */
|
||||||
Assert(neighborElement);
|
Assert(neighborElement);
|
||||||
|
|
||||||
/* Use element for lock instead of hc since hc can be replaced */
|
|
||||||
LWLockAcquire(&neighborElement->lock, LW_EXCLUSIVE);
|
LWLockAcquire(&neighborElement->lock, LW_EXCLUSIVE);
|
||||||
HnswUpdateConnection(base, e, hc, lm, lc, NULL, NULL, procinfo, collation);
|
HnswUpdateConnection(base, e, hc, lm, lc, NULL, NULL, procinfo, collation);
|
||||||
LWLockRelease(&neighborElement->lock);
|
LWLockRelease(&neighborElement->lock);
|
||||||
@@ -578,17 +575,13 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid, Hn
|
|||||||
* Callback for table_index_build_scan
|
* Callback for table_index_build_scan
|
||||||
*/
|
*/
|
||||||
static void
|
static void
|
||||||
BuildCallback(Relation index, CALLBACK_ITEM_POINTER, Datum *values,
|
BuildCallback(Relation index, ItemPointer tid, Datum *values,
|
||||||
bool *isnull, bool tupleIsAlive, void *state)
|
bool *isnull, bool tupleIsAlive, void *state)
|
||||||
{
|
{
|
||||||
HnswBuildState *buildstate = (HnswBuildState *) state;
|
HnswBuildState *buildstate = (HnswBuildState *) state;
|
||||||
HnswGraph *graph = buildstate->graph;
|
HnswGraph *graph = buildstate->graph;
|
||||||
MemoryContext oldCtx;
|
MemoryContext oldCtx;
|
||||||
|
|
||||||
#if PG_VERSION_NUM < 130000
|
|
||||||
ItemPointer tid = &hup->t_self;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* Skip nulls */
|
/* Skip nulls */
|
||||||
if (isnull[0])
|
if (isnull[0])
|
||||||
return;
|
return;
|
||||||
@@ -651,11 +644,7 @@ HnswMemoryContextAlloc(Size size, void *state)
|
|||||||
HnswBuildState *buildstate = (HnswBuildState *) state;
|
HnswBuildState *buildstate = (HnswBuildState *) state;
|
||||||
void *chunk = MemoryContextAlloc(buildstate->graphCtx, size);
|
void *chunk = MemoryContextAlloc(buildstate->graphCtx, size);
|
||||||
|
|
||||||
#if PG_VERSION_NUM >= 130000
|
|
||||||
buildstate->graphData.memoryUsed = MemoryContextMemAllocated(buildstate->graphCtx, false);
|
buildstate->graphData.memoryUsed = MemoryContextMemAllocated(buildstate->graphCtx, false);
|
||||||
#else
|
|
||||||
buildstate->graphData.memoryUsed += MAXALIGN(size);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return chunk;
|
return chunk;
|
||||||
}
|
}
|
||||||
@@ -691,17 +680,25 @@ InitBuildState(HnswBuildState * buildstate, Relation heap, Relation index, Index
|
|||||||
|
|
||||||
/* Disallow varbit since require fixed dimensions */
|
/* Disallow varbit since require fixed dimensions */
|
||||||
if (TupleDescAttr(index->rd_att, 0)->atttypid == VARBITOID)
|
if (TupleDescAttr(index->rd_att, 0)->atttypid == VARBITOID)
|
||||||
elog(ERROR, "type not supported for hnsw index");
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||||
|
errmsg("type not supported for hnsw index")));
|
||||||
|
|
||||||
/* Require column to have dimensions to be indexed */
|
/* Require column to have dimensions to be indexed */
|
||||||
if (buildstate->dimensions < 0)
|
if (buildstate->dimensions < 0)
|
||||||
elog(ERROR, "column does not have dimensions");
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||||
|
errmsg("column does not have dimensions")));
|
||||||
|
|
||||||
if (buildstate->dimensions > buildstate->typeInfo->maxDimensions)
|
if (buildstate->dimensions > buildstate->typeInfo->maxDimensions)
|
||||||
elog(ERROR, "column cannot have more than %d dimensions for hnsw index", buildstate->typeInfo->maxDimensions);
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
|
||||||
|
errmsg("column cannot have more than %d dimensions for hnsw index", buildstate->typeInfo->maxDimensions)));
|
||||||
|
|
||||||
if (buildstate->efConstruction < 2 * buildstate->m)
|
if (buildstate->efConstruction < 2 * buildstate->m)
|
||||||
elog(ERROR, "ef_construction must be greater than or equal to 2 * m");
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||||
|
errmsg("ef_construction must be greater than or equal to 2 * m")));
|
||||||
|
|
||||||
buildstate->reltuples = 0;
|
buildstate->reltuples = 0;
|
||||||
buildstate->indtuples = 0;
|
buildstate->indtuples = 0;
|
||||||
|
|||||||
@@ -36,7 +36,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 etupSize, Size ntupSize, Buffer *nbuf, Page *npage, OffsetNumber *freeOffno, OffsetNumber *freeNeighborOffno, BlockNumber *newInsertPage)
|
HnswFreeOffset(Relation index, Buffer buf, Page page, HnswElement element, Size etupSize, Size ntupSize, Buffer *nbuf, Page *npage, OffsetNumber *freeOffno, OffsetNumber *freeNeighborOffno, BlockNumber *newInsertPage, uint8 *tupleVersion)
|
||||||
{
|
{
|
||||||
OffsetNumber offno;
|
OffsetNumber offno;
|
||||||
OffsetNumber maxoffno = PageGetMaxOffsetNumber(page);
|
OffsetNumber maxoffno = PageGetMaxOffsetNumber(page);
|
||||||
@@ -98,6 +98,7 @@ HnswFreeOffset(Relation index, Buffer buf, Page page, HnswElement element, Size
|
|||||||
{
|
{
|
||||||
*freeOffno = offno;
|
*freeOffno = offno;
|
||||||
*freeNeighborOffno = neighborOffno;
|
*freeNeighborOffno = neighborOffno;
|
||||||
|
*tupleVersion = etup->version;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else if (*nbuf != buf)
|
else if (*nbuf != buf)
|
||||||
@@ -153,6 +154,7 @@ AddElementOnDisk(Relation index, HnswElement e, int m, BlockNumber insertPage, B
|
|||||||
OffsetNumber freeOffno = InvalidOffsetNumber;
|
OffsetNumber freeOffno = InvalidOffsetNumber;
|
||||||
OffsetNumber freeNeighborOffno = InvalidOffsetNumber;
|
OffsetNumber freeNeighborOffno = InvalidOffsetNumber;
|
||||||
BlockNumber newInsertPage = InvalidBlockNumber;
|
BlockNumber newInsertPage = InvalidBlockNumber;
|
||||||
|
uint8 tupleVersion;
|
||||||
char *base = NULL;
|
char *base = NULL;
|
||||||
|
|
||||||
/* Calculate sizes */
|
/* Calculate sizes */
|
||||||
@@ -202,7 +204,7 @@ AddElementOnDisk(Relation index, HnswElement e, int m, BlockNumber insertPage, B
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Next, try space from a deleted element */
|
/* Next, try space from a deleted element */
|
||||||
if (HnswFreeOffset(index, buf, page, e, etupSize, ntupSize, &nbuf, &npage, &freeOffno, &freeNeighborOffno, &newInsertPage))
|
if (HnswFreeOffset(index, buf, page, e, etupSize, ntupSize, &nbuf, &npage, &freeOffno, &freeNeighborOffno, &newInsertPage, &tupleVersion))
|
||||||
{
|
{
|
||||||
if (nbuf != buf)
|
if (nbuf != buf)
|
||||||
{
|
{
|
||||||
@@ -212,6 +214,10 @@ AddElementOnDisk(Relation index, HnswElement e, int m, BlockNumber insertPage, B
|
|||||||
npage = GenericXLogRegisterBuffer(state, nbuf, 0);
|
npage = GenericXLogRegisterBuffer(state, nbuf, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Set tuple version */
|
||||||
|
etup->version = tupleVersion;
|
||||||
|
ntup->version = tupleVersion;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,8 +385,12 @@ HnswUpdateNeighborsOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, Hns
|
|||||||
HnswElement neighborElement = HnswPtrAccess(base, hc->element);
|
HnswElement neighborElement = HnswPtrAccess(base, hc->element);
|
||||||
OffsetNumber offno = neighborElement->neighborOffno;
|
OffsetNumber offno = neighborElement->neighborOffno;
|
||||||
|
|
||||||
/* Get latest neighbors since they may have changed */
|
/*
|
||||||
/* Do not lock yet since selecting neighbors can take time */
|
* Get latest neighbors since they may have changed. Do not lock
|
||||||
|
* yet since selecting neighbors can take time. Could use
|
||||||
|
* optimistic locking to retry if another update occurs before
|
||||||
|
* getting exclusive lock.
|
||||||
|
*/
|
||||||
HnswLoadNeighbors(neighborElement, index, m);
|
HnswLoadNeighbors(neighborElement, index, m);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
139
src/hnswscan.c
139
src/hnswscan.c
@@ -1,5 +1,7 @@
|
|||||||
#include "postgres.h"
|
#include "postgres.h"
|
||||||
|
|
||||||
|
#include <float.h>
|
||||||
|
|
||||||
#include "access/relscan.h"
|
#include "access/relscan.h"
|
||||||
#include "hnsw.h"
|
#include "hnsw.h"
|
||||||
#include "pgstat.h"
|
#include "pgstat.h"
|
||||||
@@ -26,6 +28,9 @@ GetScanItems(IndexScanDesc scan, Datum q)
|
|||||||
/* Get m and entry point */
|
/* Get m and entry point */
|
||||||
HnswGetMetaPageInfo(index, &m, &entryPoint);
|
HnswGetMetaPageInfo(index, &m, &entryPoint);
|
||||||
|
|
||||||
|
so->q = q;
|
||||||
|
so->m = m;
|
||||||
|
|
||||||
if (entryPoint == NULL)
|
if (entryPoint == NULL)
|
||||||
return NIL;
|
return NIL;
|
||||||
|
|
||||||
@@ -33,11 +38,44 @@ GetScanItems(IndexScanDesc scan, Datum q)
|
|||||||
|
|
||||||
for (int lc = entryPoint->level; lc >= 1; lc--)
|
for (int lc = entryPoint->level; lc >= 1; lc--)
|
||||||
{
|
{
|
||||||
w = HnswSearchLayer(base, q, ep, 1, lc, index, procinfo, collation, m, false, NULL);
|
w = HnswSearchLayer(base, q, ep, 1, lc, index, procinfo, collation, m, false, NULL, NULL, NULL, true, NULL);
|
||||||
ep = w;
|
ep = w;
|
||||||
}
|
}
|
||||||
|
|
||||||
return HnswSearchLayer(base, q, ep, hnsw_ef_search, 0, index, procinfo, collation, m, false, NULL);
|
return HnswSearchLayer(base, q, ep, hnsw_ef_search, 0, index, procinfo, collation, m, false, NULL, &so->v, hnsw_streaming ? &so->discarded : NULL, true, &so->tuples);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Resume scan at ground level with discarded candidates
|
||||||
|
*/
|
||||||
|
static List *
|
||||||
|
ResumeScanItems(IndexScanDesc scan)
|
||||||
|
{
|
||||||
|
HnswScanOpaque so = (HnswScanOpaque) scan->opaque;
|
||||||
|
Relation index = scan->indexRelation;
|
||||||
|
FmgrInfo *procinfo = so->procinfo;
|
||||||
|
Oid collation = so->collation;
|
||||||
|
List *ep = NIL;
|
||||||
|
char *base = NULL;
|
||||||
|
int batch_size = hnsw_ef_search;
|
||||||
|
|
||||||
|
if (pairingheap_is_empty(so->discarded))
|
||||||
|
return NIL;
|
||||||
|
|
||||||
|
/* Get next batch of candidates */
|
||||||
|
for (int i = 0; i < batch_size; i++)
|
||||||
|
{
|
||||||
|
HnswSearchCandidate *hc;
|
||||||
|
|
||||||
|
if (pairingheap_is_empty(so->discarded))
|
||||||
|
break;
|
||||||
|
|
||||||
|
hc = HnswGetSearchCandidate(w_node, pairingheap_remove_first(so->discarded));
|
||||||
|
|
||||||
|
ep = lappend(ep, hc);
|
||||||
|
}
|
||||||
|
|
||||||
|
return HnswSearchLayer(base, so->q, ep, batch_size, 0, index, procinfo, collation, so->m, false, NULL, &so->v, &so->discarded, false, &so->tuples);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -81,6 +119,8 @@ hnswbeginscan(Relation index, int nkeys, int norderbys)
|
|||||||
so = (HnswScanOpaque) palloc(sizeof(HnswScanOpaqueData));
|
so = (HnswScanOpaque) palloc(sizeof(HnswScanOpaqueData));
|
||||||
so->typeInfo = HnswGetTypeInfo(index);
|
so->typeInfo = HnswGetTypeInfo(index);
|
||||||
so->first = true;
|
so->first = true;
|
||||||
|
so->v.tids = NULL;
|
||||||
|
so->discarded = NULL;
|
||||||
so->tmpCtx = AllocSetContextCreate(CurrentMemoryContext,
|
so->tmpCtx = AllocSetContextCreate(CurrentMemoryContext,
|
||||||
"Hnsw scan temporary context",
|
"Hnsw scan temporary context",
|
||||||
ALLOCSET_DEFAULT_SIZES);
|
ALLOCSET_DEFAULT_SIZES);
|
||||||
@@ -103,7 +143,15 @@ hnswrescan(IndexScanDesc scan, ScanKey keys, int nkeys, ScanKey orderbys, int no
|
|||||||
{
|
{
|
||||||
HnswScanOpaque so = (HnswScanOpaque) scan->opaque;
|
HnswScanOpaque so = (HnswScanOpaque) scan->opaque;
|
||||||
|
|
||||||
|
if (so->v.tids != NULL)
|
||||||
|
tidhash_reset(so->v.tids);
|
||||||
|
|
||||||
|
if (so->discarded != NULL)
|
||||||
|
pairingheap_reset(so->discarded);
|
||||||
|
|
||||||
so->first = true;
|
so->first = true;
|
||||||
|
so->tuples = 0;
|
||||||
|
so->previousDistance = -INFINITY;
|
||||||
MemoryContextReset(so->tmpCtx);
|
MemoryContextReset(so->tmpCtx);
|
||||||
|
|
||||||
if (keys && scan->numberOfKeys > 0)
|
if (keys && scan->numberOfKeys > 0)
|
||||||
@@ -153,34 +201,109 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir)
|
|||||||
*/
|
*/
|
||||||
LockPage(scan->indexRelation, HNSW_SCAN_LOCK, ShareLock);
|
LockPage(scan->indexRelation, HNSW_SCAN_LOCK, ShareLock);
|
||||||
|
|
||||||
so->w = GetScanItems(scan, value);
|
HnswBench("scan iteration", so->w = GetScanItems(scan, value));
|
||||||
|
|
||||||
/* Release shared lock */
|
/* Release shared lock */
|
||||||
UnlockPage(scan->indexRelation, HNSW_SCAN_LOCK, ShareLock);
|
UnlockPage(scan->indexRelation, HNSW_SCAN_LOCK, ShareLock);
|
||||||
|
|
||||||
so->first = false;
|
so->first = false;
|
||||||
|
|
||||||
#if defined(HNSW_MEMORY) && PG_VERSION_NUM >= 130000
|
#if defined(HNSW_MEMORY)
|
||||||
elog(INFO, "memory: %zu MB", MemoryContextMemAllocated(so->tmpCtx, false) / (1024 * 1024));
|
elog(INFO, "memory: %zu KB", MemoryContextMemAllocated(so->tmpCtx, false) / 1024);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
while (list_length(so->w) > 0)
|
for (;;)
|
||||||
{
|
{
|
||||||
char *base = NULL;
|
char *base = NULL;
|
||||||
HnswCandidate *hc = llast(so->w);
|
HnswSearchCandidate *hc;
|
||||||
HnswElement element = HnswPtrAccess(base, hc->element);
|
HnswElement element;
|
||||||
ItemPointer heaptid;
|
ItemPointer heaptid;
|
||||||
|
|
||||||
|
if (list_length(so->w) == 0)
|
||||||
|
{
|
||||||
|
if (!hnsw_streaming)
|
||||||
|
break;
|
||||||
|
|
||||||
|
/* Empty index */
|
||||||
|
if (so->discarded == NULL)
|
||||||
|
break;
|
||||||
|
|
||||||
|
/* Reached max number of additional tuples */
|
||||||
|
if (hnsw_ef_stream != -1 && so->tuples >= hnsw_ef_search + hnsw_ef_stream)
|
||||||
|
{
|
||||||
|
if (pairingheap_is_empty(so->discarded))
|
||||||
|
break;
|
||||||
|
|
||||||
|
/* Return remaining tuples */
|
||||||
|
so->w = lappend(so->w, HnswGetSearchCandidate(w_node, pairingheap_remove_first(so->discarded)));
|
||||||
|
}
|
||||||
|
/* Prevent scans from consuming too much memory */
|
||||||
|
else if (MemoryContextMemAllocated(so->tmpCtx, false) > (Size) work_mem * 1024L)
|
||||||
|
{
|
||||||
|
if (pairingheap_is_empty(so->discarded))
|
||||||
|
{
|
||||||
|
ereport(NOTICE,
|
||||||
|
(errmsg("hnsw index scan exceeded work_mem after " INT64_FORMAT " tuples", so->tuples),
|
||||||
|
errhint("Increase work_mem to scan more tuples.")));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Return remaining tuples */
|
||||||
|
so->w = lappend(so->w, HnswGetSearchCandidate(w_node, pairingheap_remove_first(so->discarded)));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* Locking ensures when neighbors are read, the elements they
|
||||||
|
* reference will not be deleted (and replaced) during the
|
||||||
|
* iteration.
|
||||||
|
*
|
||||||
|
* Elements loaded into memory on previous iterations may have
|
||||||
|
* been deleted (and replaced), so when reading neighbors, the
|
||||||
|
* element version must be checked.
|
||||||
|
*/
|
||||||
|
LockPage(scan->indexRelation, HNSW_SCAN_LOCK, ShareLock);
|
||||||
|
|
||||||
|
HnswBench("scan iteration", so->w = ResumeScanItems(scan));
|
||||||
|
|
||||||
|
UnlockPage(scan->indexRelation, HNSW_SCAN_LOCK, ShareLock);
|
||||||
|
|
||||||
|
#if defined(HNSW_MEMORY)
|
||||||
|
elog(INFO, "memory: %zu KB", MemoryContextMemAllocated(so->tmpCtx, false) / 1024);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
if (list_length(so->w) == 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
hc = llast(so->w);
|
||||||
|
element = HnswPtrAccess(base, hc->element);
|
||||||
|
|
||||||
/* Move to next element if no valid heap TIDs */
|
/* Move to next element if no valid heap TIDs */
|
||||||
if (element->heaptidsLength == 0)
|
if (element->heaptidsLength == 0)
|
||||||
{
|
{
|
||||||
so->w = list_delete_last(so->w);
|
so->w = list_delete_last(so->w);
|
||||||
|
|
||||||
|
/* Mark memory as free for next iteration */
|
||||||
|
if (hnsw_streaming)
|
||||||
|
{
|
||||||
|
pfree(element);
|
||||||
|
pfree(hc);
|
||||||
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
heaptid = &element->heaptids[--element->heaptidsLength];
|
heaptid = &element->heaptids[--element->heaptidsLength];
|
||||||
|
|
||||||
|
if (hc->distance < so->previousDistance)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
so->previousDistance = hc->distance;
|
||||||
|
|
||||||
MemoryContextSwitchTo(oldCtx);
|
MemoryContextSwitchTo(oldCtx);
|
||||||
|
|
||||||
scan->xs_heaptid = *heaptid;
|
scan->xs_heaptid = *heaptid;
|
||||||
|
|||||||
409
src/hnswutils.c
409
src/hnswutils.c
@@ -5,6 +5,7 @@
|
|||||||
#include "access/generic_xlog.h"
|
#include "access/generic_xlog.h"
|
||||||
#include "catalog/pg_type.h"
|
#include "catalog/pg_type.h"
|
||||||
#include "catalog/pg_type_d.h"
|
#include "catalog/pg_type_d.h"
|
||||||
|
#include "common/hashfn.h"
|
||||||
#include "fmgr.h"
|
#include "fmgr.h"
|
||||||
#include "hnsw.h"
|
#include "hnsw.h"
|
||||||
#include "lib/pairingheap.h"
|
#include "lib/pairingheap.h"
|
||||||
@@ -14,12 +15,6 @@
|
|||||||
#include "utils/memdebug.h"
|
#include "utils/memdebug.h"
|
||||||
#include "utils/rel.h"
|
#include "utils/rel.h"
|
||||||
|
|
||||||
#if PG_VERSION_NUM >= 130000
|
|
||||||
#include "common/hashfn.h"
|
|
||||||
#else
|
|
||||||
#include "utils/hashutils.h"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if PG_VERSION_NUM < 170000
|
#if PG_VERSION_NUM < 170000
|
||||||
static inline uint64
|
static inline uint64
|
||||||
murmurhash64(uint64 data)
|
murmurhash64(uint64 data)
|
||||||
@@ -107,10 +102,9 @@ hash_offset(Size offset)
|
|||||||
|
|
||||||
typedef union
|
typedef union
|
||||||
{
|
{
|
||||||
pointerhash_hash *pointers;
|
HnswElement element;
|
||||||
offsethash_hash *offsets;
|
ItemPointerData indextid;
|
||||||
tidhash_hash *tids;
|
} HnswUnvisited;
|
||||||
} visited_hash;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Get the max number of connections in an upper layer for each element in the index
|
* Get the max number of connections in an upper layer for each element in the index
|
||||||
@@ -252,6 +246,8 @@ HnswInitElement(char *base, ItemPointer heaptid, int m, double ml, int maxLevel,
|
|||||||
|
|
||||||
element->level = level;
|
element->level = level;
|
||||||
element->deleted = 0;
|
element->deleted = 0;
|
||||||
|
/* Start at one to make it easier to find issues */
|
||||||
|
element->version = 1;
|
||||||
|
|
||||||
HnswInitNeighbors(base, element, m, allocator);
|
HnswInitNeighbors(base, element, m, allocator);
|
||||||
|
|
||||||
@@ -404,6 +400,7 @@ HnswSetElementTuple(char *base, HnswElementTuple etup, HnswElement element)
|
|||||||
etup->type = HNSW_ELEMENT_TUPLE_TYPE;
|
etup->type = HNSW_ELEMENT_TUPLE_TYPE;
|
||||||
etup->level = element->level;
|
etup->level = element->level;
|
||||||
etup->deleted = 0;
|
etup->deleted = 0;
|
||||||
|
etup->version = element->version;
|
||||||
for (int i = 0; i < HNSW_HEAPTIDS; i++)
|
for (int i = 0; i < HNSW_HEAPTIDS; i++)
|
||||||
{
|
{
|
||||||
if (i < element->heaptidsLength)
|
if (i < element->heaptidsLength)
|
||||||
@@ -446,6 +443,7 @@ HnswSetNeighborTuple(char *base, HnswNeighborTuple ntup, HnswElement e, int m)
|
|||||||
}
|
}
|
||||||
|
|
||||||
ntup->count = idx;
|
ntup->count = idx;
|
||||||
|
ntup->version = e->version;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -519,6 +517,7 @@ HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool loadHe
|
|||||||
{
|
{
|
||||||
element->level = etup->level;
|
element->level = etup->level;
|
||||||
element->deleted = etup->deleted;
|
element->deleted = etup->deleted;
|
||||||
|
element->version = etup->version;
|
||||||
element->neighborPage = ItemPointerGetBlockNumber(&etup->neighbortid);
|
element->neighborPage = ItemPointerGetBlockNumber(&etup->neighbortid);
|
||||||
element->neighborOffno = ItemPointerGetOffsetNumber(&etup->neighbortid);
|
element->neighborOffno = ItemPointerGetOffsetNumber(&etup->neighbortid);
|
||||||
element->heaptidsLength = 0;
|
element->heaptidsLength = 0;
|
||||||
@@ -547,19 +546,19 @@ HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool loadHe
|
|||||||
/*
|
/*
|
||||||
* Load an element and optionally get its distance from q
|
* Load an element and optionally get its distance from q
|
||||||
*/
|
*/
|
||||||
void
|
static void
|
||||||
HnswLoadElement(HnswElement element, float *distance, Datum *q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec, float *maxDistance)
|
HnswLoadElementImpl(BlockNumber blkno, OffsetNumber offno, float *distance, Datum *q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec, float *maxDistance, HnswElement * element)
|
||||||
{
|
{
|
||||||
Buffer buf;
|
Buffer buf;
|
||||||
Page page;
|
Page page;
|
||||||
HnswElementTuple etup;
|
HnswElementTuple etup;
|
||||||
|
|
||||||
/* Read vector */
|
/* Read vector */
|
||||||
buf = ReadBuffer(index, element->blkno);
|
buf = ReadBuffer(index, blkno);
|
||||||
LockBuffer(buf, BUFFER_LOCK_SHARE);
|
LockBuffer(buf, BUFFER_LOCK_SHARE);
|
||||||
page = BufferGetPage(buf);
|
page = BufferGetPage(buf);
|
||||||
|
|
||||||
etup = (HnswElementTuple) PageGetItem(page, PageGetItemId(page, element->offno));
|
etup = (HnswElementTuple) PageGetItem(page, PageGetItemId(page, offno));
|
||||||
|
|
||||||
Assert(HnswIsElementTuple(etup));
|
Assert(HnswIsElementTuple(etup));
|
||||||
|
|
||||||
@@ -574,19 +573,32 @@ HnswLoadElement(HnswElement element, float *distance, Datum *q, Relation index,
|
|||||||
|
|
||||||
/* Load element */
|
/* Load element */
|
||||||
if (distance == NULL || maxDistance == NULL || *distance < *maxDistance)
|
if (distance == NULL || maxDistance == NULL || *distance < *maxDistance)
|
||||||
HnswLoadElementFromTuple(element, etup, true, loadVec);
|
{
|
||||||
|
if (*element == NULL)
|
||||||
|
*element = HnswInitElementFromBlock(blkno, offno);
|
||||||
|
|
||||||
|
HnswLoadElementFromTuple(*element, etup, true, loadVec);
|
||||||
|
}
|
||||||
|
|
||||||
UnlockReleaseBuffer(buf);
|
UnlockReleaseBuffer(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Get the distance for a candidate
|
* Load an element and optionally get its distance from q
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
HnswLoadElement(HnswElement element, float *distance, Datum *q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec, float *maxDistance)
|
||||||
|
{
|
||||||
|
HnswLoadElementImpl(element->blkno, element->offno, distance, q, index, procinfo, collation, loadVec, maxDistance, &element);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Get the distance for an element
|
||||||
*/
|
*/
|
||||||
static float
|
static float
|
||||||
GetCandidateDistance(char *base, HnswCandidate * hc, Datum q, FmgrInfo *procinfo, Oid collation)
|
GetElementDistance(char *base, HnswElement element, Datum q, FmgrInfo *procinfo, Oid collation)
|
||||||
{
|
{
|
||||||
HnswElement hce = HnswPtrAccess(base, hc->element);
|
Datum value = HnswGetValue(base, element);
|
||||||
Datum value = HnswGetValue(base, hce);
|
|
||||||
|
|
||||||
return DatumGetFloat8(FunctionCall2Coll(procinfo, collation, q, value));
|
return DatumGetFloat8(FunctionCall2Coll(procinfo, collation, q, value));
|
||||||
}
|
}
|
||||||
@@ -594,14 +606,14 @@ GetCandidateDistance(char *base, HnswCandidate * hc, Datum q, FmgrInfo *procinfo
|
|||||||
/*
|
/*
|
||||||
* Create a candidate for the entry point
|
* Create a candidate for the entry point
|
||||||
*/
|
*/
|
||||||
HnswCandidate *
|
HnswSearchCandidate *
|
||||||
HnswEntryCandidate(char *base, HnswElement entryPoint, Datum q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec)
|
HnswEntryCandidate(char *base, HnswElement entryPoint, Datum q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec)
|
||||||
{
|
{
|
||||||
HnswCandidate *hc = palloc(sizeof(HnswCandidate));
|
HnswSearchCandidate *hc = palloc(sizeof(HnswSearchCandidate));
|
||||||
|
|
||||||
HnswPtrStore(base, hc->element, entryPoint);
|
HnswPtrStore(base, hc->element, entryPoint);
|
||||||
if (index == NULL)
|
if (index == NULL)
|
||||||
hc->distance = GetCandidateDistance(base, hc, q, procinfo, collation);
|
hc->distance = GetElementDistance(base, entryPoint, q, procinfo, collation);
|
||||||
else
|
else
|
||||||
HnswLoadElement(entryPoint, &hc->distance, &q, index, procinfo, collation, loadVec, NULL);
|
HnswLoadElement(entryPoint, &hc->distance, &q, index, procinfo, collation, loadVec, NULL);
|
||||||
return hc;
|
return hc;
|
||||||
@@ -613,10 +625,25 @@ HnswEntryCandidate(char *base, HnswElement entryPoint, Datum q, Relation index,
|
|||||||
static int
|
static int
|
||||||
CompareNearestCandidates(const pairingheap_node *a, const pairingheap_node *b, void *arg)
|
CompareNearestCandidates(const pairingheap_node *a, const pairingheap_node *b, void *arg)
|
||||||
{
|
{
|
||||||
if (((const HnswPairingHeapNode *) a)->inner->distance < ((const HnswPairingHeapNode *) b)->inner->distance)
|
if (HnswGetSearchCandidateConst(c_node, a)->distance < HnswGetSearchCandidateConst(c_node, b)->distance)
|
||||||
return 1;
|
return 1;
|
||||||
|
|
||||||
if (((const HnswPairingHeapNode *) a)->inner->distance > ((const HnswPairingHeapNode *) b)->inner->distance)
|
if (HnswGetSearchCandidateConst(c_node, a)->distance > HnswGetSearchCandidateConst(c_node, b)->distance)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Compare discarded candidate distances
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
CompareNearestDiscardedCandidates(const pairingheap_node *a, const pairingheap_node *b, void *arg)
|
||||||
|
{
|
||||||
|
if (HnswGetSearchCandidateConst(w_node, a)->distance < HnswGetSearchCandidateConst(w_node, b)->distance)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
if (HnswGetSearchCandidateConst(w_node, a)->distance > HnswGetSearchCandidateConst(w_node, b)->distance)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
@@ -628,27 +655,15 @@ CompareNearestCandidates(const pairingheap_node *a, const pairingheap_node *b, v
|
|||||||
static int
|
static int
|
||||||
CompareFurthestCandidates(const pairingheap_node *a, const pairingheap_node *b, void *arg)
|
CompareFurthestCandidates(const pairingheap_node *a, const pairingheap_node *b, void *arg)
|
||||||
{
|
{
|
||||||
if (((const HnswPairingHeapNode *) a)->inner->distance < ((const HnswPairingHeapNode *) b)->inner->distance)
|
if (HnswGetSearchCandidateConst(w_node, a)->distance < HnswGetSearchCandidateConst(w_node, b)->distance)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
if (((const HnswPairingHeapNode *) a)->inner->distance > ((const HnswPairingHeapNode *) b)->inner->distance)
|
if (HnswGetSearchCandidateConst(w_node, a)->distance > HnswGetSearchCandidateConst(w_node, b)->distance)
|
||||||
return 1;
|
return 1;
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Create a pairing heap node for a candidate
|
|
||||||
*/
|
|
||||||
static HnswPairingHeapNode *
|
|
||||||
CreatePairingHeapNode(HnswCandidate * c)
|
|
||||||
{
|
|
||||||
HnswPairingHeapNode *node = palloc(sizeof(HnswPairingHeapNode));
|
|
||||||
|
|
||||||
node->inner = c;
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Init visited
|
* Init visited
|
||||||
*/
|
*/
|
||||||
@@ -667,11 +682,11 @@ InitVisited(char *base, visited_hash * v, Relation index, int ef, int m)
|
|||||||
* Add to visited
|
* Add to visited
|
||||||
*/
|
*/
|
||||||
static inline void
|
static inline void
|
||||||
AddToVisited(char *base, visited_hash * v, HnswCandidate * hc, Relation index, bool *found)
|
AddToVisited(char *base, visited_hash * v, HnswElementPtr elementPtr, Relation index, bool *found)
|
||||||
{
|
{
|
||||||
if (index != NULL)
|
if (index != NULL)
|
||||||
{
|
{
|
||||||
HnswElement element = HnswPtrAccess(base, hc->element);
|
HnswElement element = HnswPtrAccess(base, elementPtr);
|
||||||
ItemPointerData indextid;
|
ItemPointerData indextid;
|
||||||
|
|
||||||
ItemPointerSet(&indextid, element->blkno, element->offno);
|
ItemPointerSet(&indextid, element->blkno, element->offno);
|
||||||
@@ -679,23 +694,15 @@ AddToVisited(char *base, visited_hash * v, HnswCandidate * hc, Relation index, b
|
|||||||
}
|
}
|
||||||
else if (base != NULL)
|
else if (base != NULL)
|
||||||
{
|
{
|
||||||
#if PG_VERSION_NUM >= 130000
|
HnswElement element = HnswPtrAccess(base, elementPtr);
|
||||||
HnswElement element = HnswPtrAccess(base, hc->element);
|
|
||||||
|
|
||||||
offsethash_insert_hash(v->offsets, HnswPtrOffset(hc->element), element->hash, found);
|
offsethash_insert_hash(v->offsets, HnswPtrOffset(elementPtr), element->hash, found);
|
||||||
#else
|
|
||||||
offsethash_insert(v->offsets, HnswPtrOffset(hc->element), found);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
#if PG_VERSION_NUM >= 130000
|
HnswElement element = HnswPtrAccess(base, elementPtr);
|
||||||
HnswElement element = HnswPtrAccess(base, hc->element);
|
|
||||||
|
|
||||||
pointerhash_insert_hash(v->pointers, (uintptr_t) HnswPtrPointer(hc->element), element->hash, found);
|
pointerhash_insert_hash(v->pointers, (uintptr_t) HnswPtrPointer(elementPtr), element->hash, found);
|
||||||
#else
|
|
||||||
pointerhash_insert(v->pointers, (uintptr_t) HnswPtrPointer(hc->element), found);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -703,69 +710,168 @@ AddToVisited(char *base, visited_hash * v, HnswCandidate * hc, Relation index, b
|
|||||||
* Count element towards ef
|
* Count element towards ef
|
||||||
*/
|
*/
|
||||||
static inline bool
|
static inline bool
|
||||||
CountElement(char *base, HnswElement skipElement, HnswCandidate * hc)
|
CountElement(HnswElement skipElement, HnswElement e)
|
||||||
{
|
{
|
||||||
HnswElement e;
|
|
||||||
|
|
||||||
if (skipElement == NULL)
|
if (skipElement == NULL)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
/* Ensure does not access heaptidsLength during in-memory build */
|
/* Ensure does not access heaptidsLength during in-memory build */
|
||||||
pg_memory_barrier();
|
pg_memory_barrier();
|
||||||
|
|
||||||
e = HnswPtrAccess(base, hc->element);
|
/* Keep scan-build happy on Mac x86-64 */
|
||||||
|
Assert(e);
|
||||||
|
|
||||||
return e->heaptidsLength != 0;
|
return e->heaptidsLength != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Load unvisited neighbors from memory
|
||||||
|
*/
|
||||||
|
static void
|
||||||
|
HnswLoadUnvisitedFromMemory(char *base, HnswElement element, HnswUnvisited * unvisited, int *unvisitedLength, visited_hash * v, int lc, HnswNeighborArray * localNeighborhood, Size neighborhoodSize)
|
||||||
|
{
|
||||||
|
/* Get the neighborhood at layer lc */
|
||||||
|
HnswNeighborArray *neighborhood = HnswGetNeighbors(base, element, lc);
|
||||||
|
|
||||||
|
/* Copy neighborhood to local memory */
|
||||||
|
LWLockAcquire(&element->lock, LW_SHARED);
|
||||||
|
memcpy(localNeighborhood, neighborhood, neighborhoodSize);
|
||||||
|
LWLockRelease(&element->lock);
|
||||||
|
|
||||||
|
*unvisitedLength = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < localNeighborhood->length; i++)
|
||||||
|
{
|
||||||
|
HnswCandidate *hc = &localNeighborhood->items[i];
|
||||||
|
bool found;
|
||||||
|
|
||||||
|
AddToVisited(base, v, hc->element, NULL, &found);
|
||||||
|
|
||||||
|
if (!found)
|
||||||
|
unvisited[(*unvisitedLength)++].element = HnswPtrAccess(base, hc->element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Load unvisited neighbors from disk
|
||||||
|
*/
|
||||||
|
static void
|
||||||
|
HnswLoadUnvisitedFromDisk(HnswElement element, HnswUnvisited * unvisited, int *unvisitedLength, visited_hash * v, Relation index, int m, int lm, int lc)
|
||||||
|
{
|
||||||
|
Buffer buf;
|
||||||
|
Page page;
|
||||||
|
HnswNeighborTuple ntup;
|
||||||
|
int start;
|
||||||
|
ItemPointerData indextids[HNSW_MAX_M * 2];
|
||||||
|
|
||||||
|
*unvisitedLength = 0;
|
||||||
|
|
||||||
|
buf = ReadBuffer(index, element->neighborPage);
|
||||||
|
LockBuffer(buf, BUFFER_LOCK_SHARE);
|
||||||
|
page = BufferGetPage(buf);
|
||||||
|
|
||||||
|
ntup = (HnswNeighborTuple) PageGetItem(page, PageGetItemId(page, element->neighborOffno));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Ensure the neighbor tuple has not been deleted or replaced between
|
||||||
|
* index scan iterations
|
||||||
|
*/
|
||||||
|
if (ntup->version != element->version || ntup->count != (element->level + 2) * m)
|
||||||
|
{
|
||||||
|
UnlockReleaseBuffer(buf);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Copy to minimize lock time */
|
||||||
|
start = (element->level - lc) * m;
|
||||||
|
memcpy(&indextids, ntup->indextids + start, lm * sizeof(ItemPointerData));
|
||||||
|
|
||||||
|
UnlockReleaseBuffer(buf);
|
||||||
|
|
||||||
|
for (int i = 0; i < lm; i++)
|
||||||
|
{
|
||||||
|
ItemPointer indextid = &indextids[i];
|
||||||
|
bool found;
|
||||||
|
|
||||||
|
if (!ItemPointerIsValid(indextid))
|
||||||
|
break;
|
||||||
|
|
||||||
|
tidhash_insert(v->tids, *indextid, &found);
|
||||||
|
|
||||||
|
if (!found)
|
||||||
|
unvisited[(*unvisitedLength)++].indextid = *indextid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Algorithm 2 from paper
|
* Algorithm 2 from paper
|
||||||
*/
|
*/
|
||||||
List *
|
List *
|
||||||
HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, FmgrInfo *procinfo, Oid collation, int m, bool inserting, HnswElement skipElement)
|
HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, FmgrInfo *procinfo, Oid collation, int m, bool inserting, HnswElement skipElement, visited_hash * v, pairingheap **discarded, bool initVisited, int64 *tuples)
|
||||||
{
|
{
|
||||||
List *w = NIL;
|
List *w = NIL;
|
||||||
pairingheap *C = pairingheap_allocate(CompareNearestCandidates, NULL);
|
pairingheap *C = pairingheap_allocate(CompareNearestCandidates, NULL);
|
||||||
pairingheap *W = pairingheap_allocate(CompareFurthestCandidates, NULL);
|
pairingheap *W = pairingheap_allocate(CompareFurthestCandidates, NULL);
|
||||||
int wlen = 0;
|
int wlen = 0;
|
||||||
visited_hash v;
|
visited_hash vh;
|
||||||
ListCell *lc2;
|
ListCell *lc2;
|
||||||
HnswNeighborArray *neighborhoodData = NULL;
|
HnswNeighborArray *localNeighborhood = NULL;
|
||||||
Size neighborhoodSize;
|
Size neighborhoodSize = 0;
|
||||||
|
int lm = HnswGetLayerM(m, lc);
|
||||||
|
HnswUnvisited *unvisited = palloc(lm * sizeof(HnswUnvisited));
|
||||||
|
int unvisitedLength;
|
||||||
|
|
||||||
InitVisited(base, &v, index, ef, m);
|
if (v == NULL)
|
||||||
|
{
|
||||||
|
v = &vh;
|
||||||
|
initVisited = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initVisited)
|
||||||
|
{
|
||||||
|
InitVisited(base, v, index, ef, m);
|
||||||
|
|
||||||
|
if (discarded != NULL)
|
||||||
|
*discarded = pairingheap_allocate(CompareNearestDiscardedCandidates, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
/* Create local memory for neighborhood if needed */
|
/* Create local memory for neighborhood if needed */
|
||||||
if (index == NULL)
|
if (index == NULL)
|
||||||
{
|
{
|
||||||
neighborhoodSize = HNSW_NEIGHBOR_ARRAY_SIZE(HnswGetLayerM(m, lc));
|
neighborhoodSize = HNSW_NEIGHBOR_ARRAY_SIZE(lm);
|
||||||
neighborhoodData = palloc(neighborhoodSize);
|
localNeighborhood = palloc(neighborhoodSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Add entry points to v, C, and W */
|
/* Add entry points to v, C, and W */
|
||||||
foreach(lc2, ep)
|
foreach(lc2, ep)
|
||||||
{
|
{
|
||||||
HnswCandidate *hc = (HnswCandidate *) lfirst(lc2);
|
HnswSearchCandidate *hc = (HnswSearchCandidate *) lfirst(lc2);
|
||||||
bool found;
|
bool found;
|
||||||
|
|
||||||
AddToVisited(base, &v, hc, index, &found);
|
if (initVisited)
|
||||||
|
{
|
||||||
|
AddToVisited(base, v, hc->element, index, &found);
|
||||||
|
|
||||||
pairingheap_add(C, &(CreatePairingHeapNode(hc)->ph_node));
|
if (tuples != NULL)
|
||||||
pairingheap_add(W, &(CreatePairingHeapNode(hc)->ph_node));
|
(*tuples)++;
|
||||||
|
}
|
||||||
|
|
||||||
|
pairingheap_add(C, &hc->c_node);
|
||||||
|
pairingheap_add(W, &hc->w_node);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Do not count elements being deleted towards ef when vacuuming. It
|
* Do not count elements being deleted towards ef when vacuuming. It
|
||||||
* would be ideal to do this for inserts as well, but this could
|
* would be ideal to do this for inserts as well, but this could
|
||||||
* affect insert performance.
|
* affect insert performance.
|
||||||
*/
|
*/
|
||||||
if (CountElement(base, skipElement, hc))
|
if (CountElement(skipElement, HnswPtrAccess(base, hc->element)))
|
||||||
wlen++;
|
wlen++;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (!pairingheap_is_empty(C))
|
while (!pairingheap_is_empty(C))
|
||||||
{
|
{
|
||||||
HnswNeighborArray *neighborhood;
|
HnswSearchCandidate *c = HnswGetSearchCandidate(c_node, pairingheap_remove_first(C));
|
||||||
HnswCandidate *c = ((HnswPairingHeapNode *) pairingheap_remove_first(C))->inner;
|
HnswSearchCandidate *f = HnswGetSearchCandidate(w_node, pairingheap_first(W));
|
||||||
HnswCandidate *f = ((HnswPairingHeapNode *) pairingheap_first(W))->inner;
|
|
||||||
HnswElement cElement;
|
HnswElement cElement;
|
||||||
|
|
||||||
if (c->distance > f->distance)
|
if (c->distance > f->distance)
|
||||||
@@ -773,72 +879,80 @@ HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, F
|
|||||||
|
|
||||||
cElement = HnswPtrAccess(base, c->element);
|
cElement = HnswPtrAccess(base, c->element);
|
||||||
|
|
||||||
if (HnswPtrIsNull(base, cElement->neighbors))
|
|
||||||
HnswLoadNeighbors(cElement, index, m);
|
|
||||||
|
|
||||||
/* Get the neighborhood at layer lc */
|
|
||||||
neighborhood = HnswGetNeighbors(base, cElement, lc);
|
|
||||||
|
|
||||||
/* Copy neighborhood to local memory if needed */
|
|
||||||
if (index == NULL)
|
if (index == NULL)
|
||||||
|
HnswLoadUnvisitedFromMemory(base, cElement, unvisited, &unvisitedLength, v, lc, localNeighborhood, neighborhoodSize);
|
||||||
|
else
|
||||||
|
HnswLoadUnvisitedFromDisk(cElement, unvisited, &unvisitedLength, v, index, m, lm, lc);
|
||||||
|
|
||||||
|
if (tuples != NULL)
|
||||||
|
(*tuples) += unvisitedLength;
|
||||||
|
|
||||||
|
for (int i = 0; i < unvisitedLength; i++)
|
||||||
{
|
{
|
||||||
LWLockAcquire(&cElement->lock, LW_SHARED);
|
HnswElement eElement;
|
||||||
memcpy(neighborhoodData, neighborhood, neighborhoodSize);
|
HnswSearchCandidate *e;
|
||||||
LWLockRelease(&cElement->lock);
|
float eDistance;
|
||||||
neighborhood = neighborhoodData;
|
bool alwaysAdd = wlen < ef;
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < neighborhood->length; i++)
|
f = HnswGetSearchCandidate(w_node, pairingheap_first(W));
|
||||||
{
|
|
||||||
HnswCandidate *e = &neighborhood->items[i];
|
|
||||||
bool visited;
|
|
||||||
|
|
||||||
AddToVisited(base, &v, e, index, &visited);
|
if (index == NULL)
|
||||||
|
|
||||||
if (!visited)
|
|
||||||
{
|
{
|
||||||
float eDistance;
|
eElement = unvisited[i].element;
|
||||||
HnswElement eElement = HnswPtrAccess(base, e->element);
|
eDistance = GetElementDistance(base, eElement, q, procinfo, collation);
|
||||||
bool alwaysAdd = wlen < ef;
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ItemPointer indextid = &unvisited[i].indextid;
|
||||||
|
BlockNumber blkno = ItemPointerGetBlockNumber(indextid);
|
||||||
|
OffsetNumber offno = ItemPointerGetOffsetNumber(indextid);
|
||||||
|
|
||||||
f = ((HnswPairingHeapNode *) pairingheap_first(W))->inner;
|
/* Avoid any allocations if not adding */
|
||||||
|
eElement = NULL;
|
||||||
|
HnswLoadElementImpl(blkno, offno, &eDistance, &q, index, procinfo, collation, inserting, alwaysAdd || discarded != NULL ? NULL : &f->distance, &eElement);
|
||||||
|
}
|
||||||
|
|
||||||
if (index == NULL)
|
if (eElement == NULL || !(eDistance < f->distance || alwaysAdd))
|
||||||
eDistance = GetCandidateDistance(base, e, q, procinfo, collation);
|
{
|
||||||
else
|
if (discarded != NULL)
|
||||||
HnswLoadElement(eElement, &eDistance, &q, index, procinfo, collation, inserting, alwaysAdd ? NULL : &f->distance);
|
|
||||||
|
|
||||||
if (eDistance < f->distance || alwaysAdd)
|
|
||||||
{
|
{
|
||||||
HnswCandidate *ec;
|
/* Create a new candidate */
|
||||||
|
e = palloc(sizeof(HnswSearchCandidate));
|
||||||
|
HnswPtrStore(base, e->element, eElement);
|
||||||
|
e->distance = eDistance;
|
||||||
|
pairingheap_add(*discarded, &e->w_node);
|
||||||
|
}
|
||||||
|
|
||||||
Assert(!eElement->deleted);
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
/* Make robust to issues */
|
/* Make robust to issues */
|
||||||
if (eElement->level < lc)
|
if (eElement->level < lc)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
/* Copy e */
|
/* Create a new candidate */
|
||||||
ec = palloc(sizeof(HnswCandidate));
|
e = palloc(sizeof(HnswSearchCandidate));
|
||||||
HnswPtrStore(base, ec->element, eElement);
|
HnswPtrStore(base, e->element, eElement);
|
||||||
ec->distance = eDistance;
|
e->distance = eDistance;
|
||||||
|
pairingheap_add(C, &e->c_node);
|
||||||
|
pairingheap_add(W, &e->w_node);
|
||||||
|
|
||||||
pairingheap_add(C, &(CreatePairingHeapNode(ec)->ph_node));
|
/*
|
||||||
pairingheap_add(W, &(CreatePairingHeapNode(ec)->ph_node));
|
* Do not count elements being deleted towards ef when vacuuming.
|
||||||
|
* It would be ideal to do this for inserts as well, but this
|
||||||
|
* could affect insert performance.
|
||||||
|
*/
|
||||||
|
if (CountElement(skipElement, eElement))
|
||||||
|
{
|
||||||
|
wlen++;
|
||||||
|
|
||||||
/*
|
/* No need to decrement wlen */
|
||||||
* Do not count elements being deleted towards ef when
|
if (wlen > ef)
|
||||||
* vacuuming. It would be ideal to do this for inserts as
|
{
|
||||||
* well, but this could affect insert performance.
|
HnswSearchCandidate *d = HnswGetSearchCandidate(w_node, pairingheap_remove_first(W));
|
||||||
*/
|
|
||||||
if (CountElement(base, skipElement, e))
|
|
||||||
{
|
|
||||||
wlen++;
|
|
||||||
|
|
||||||
/* No need to decrement wlen */
|
if (discarded != NULL)
|
||||||
if (wlen > ef)
|
pairingheap_add(*discarded, &d->w_node);
|
||||||
pairingheap_remove_first(W);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -847,7 +961,7 @@ HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, F
|
|||||||
/* Add each element of W to w */
|
/* Add each element of W to w */
|
||||||
while (!pairingheap_is_empty(W))
|
while (!pairingheap_is_empty(W))
|
||||||
{
|
{
|
||||||
HnswCandidate *hc = ((HnswPairingHeapNode *) pairingheap_remove_first(W))->inner;
|
HnswSearchCandidate *hc = HnswGetSearchCandidate(w_node, pairingheap_remove_first(W));
|
||||||
|
|
||||||
w = lappend(w, hc);
|
w = lappend(w, hc);
|
||||||
}
|
}
|
||||||
@@ -859,17 +973,10 @@ HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, F
|
|||||||
* Compare candidate distances with pointer tie-breaker
|
* Compare candidate distances with pointer tie-breaker
|
||||||
*/
|
*/
|
||||||
static int
|
static int
|
||||||
#if PG_VERSION_NUM >= 130000
|
|
||||||
CompareCandidateDistances(const ListCell *a, const ListCell *b)
|
CompareCandidateDistances(const ListCell *a, const ListCell *b)
|
||||||
{
|
{
|
||||||
HnswCandidate *hca = lfirst(a);
|
HnswCandidate *hca = lfirst(a);
|
||||||
HnswCandidate *hcb = lfirst(b);
|
HnswCandidate *hcb = lfirst(b);
|
||||||
#else
|
|
||||||
CompareCandidateDistances(const void *a, const void *b)
|
|
||||||
{
|
|
||||||
HnswCandidate *hca = lfirst(*(ListCell **) a);
|
|
||||||
HnswCandidate *hcb = lfirst(*(ListCell **) b);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (hca->distance < hcb->distance)
|
if (hca->distance < hcb->distance)
|
||||||
return 1;
|
return 1;
|
||||||
@@ -890,17 +997,10 @@ CompareCandidateDistances(const void *a, const void *b)
|
|||||||
* Compare candidate distances with offset tie-breaker
|
* Compare candidate distances with offset tie-breaker
|
||||||
*/
|
*/
|
||||||
static int
|
static int
|
||||||
#if PG_VERSION_NUM >= 130000
|
|
||||||
CompareCandidateDistancesOffset(const ListCell *a, const ListCell *b)
|
CompareCandidateDistancesOffset(const ListCell *a, const ListCell *b)
|
||||||
{
|
{
|
||||||
HnswCandidate *hca = lfirst(a);
|
HnswCandidate *hca = lfirst(a);
|
||||||
HnswCandidate *hcb = lfirst(b);
|
HnswCandidate *hcb = lfirst(b);
|
||||||
#else
|
|
||||||
CompareCandidateDistancesOffset(const void *a, const void *b)
|
|
||||||
{
|
|
||||||
HnswCandidate *hca = lfirst(*(ListCell **) a);
|
|
||||||
HnswCandidate *hcb = lfirst(*(ListCell **) b);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (hca->distance < hcb->distance)
|
if (hca->distance < hcb->distance)
|
||||||
return 1;
|
return 1;
|
||||||
@@ -1110,7 +1210,7 @@ HnswUpdateConnection(char *base, HnswElement element, HnswCandidate * hc, int lm
|
|||||||
if (HnswPtrIsNull(base, hc3Element->value))
|
if (HnswPtrIsNull(base, hc3Element->value))
|
||||||
HnswLoadElement(hc3Element, &hc3->distance, &q, index, procinfo, collation, true, NULL);
|
HnswLoadElement(hc3Element, &hc3->distance, &q, index, procinfo, collation, true, NULL);
|
||||||
else
|
else
|
||||||
hc3->distance = GetCandidateDistance(base, hc3, q, procinfo, collation);
|
hc3->distance = GetElementDistance(base, hc3Element, q, procinfo, collation);
|
||||||
|
|
||||||
/* Prune element if being deleted */
|
/* Prune element if being deleted */
|
||||||
if (hc3Element->heaptidsLength == 0)
|
if (hc3Element->heaptidsLength == 0)
|
||||||
@@ -1182,7 +1282,6 @@ RemoveElements(char *base, List *w, HnswElement skipElement)
|
|||||||
return w2;
|
return w2;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if PG_VERSION_NUM >= 130000
|
|
||||||
/*
|
/*
|
||||||
* Precompute hash
|
* Precompute hash
|
||||||
*/
|
*/
|
||||||
@@ -1198,7 +1297,6 @@ PrecomputeHash(char *base, HnswElement element)
|
|||||||
else
|
else
|
||||||
element->hash = hash_offset(HnswPtrOffset(ptr));
|
element->hash = hash_offset(HnswPtrOffset(ptr));
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Algorithm 1 from paper
|
* Algorithm 1 from paper
|
||||||
@@ -1213,11 +1311,9 @@ HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint
|
|||||||
Datum q = HnswGetValue(base, element);
|
Datum q = HnswGetValue(base, element);
|
||||||
HnswElement skipElement = existing ? element : NULL;
|
HnswElement skipElement = existing ? element : NULL;
|
||||||
|
|
||||||
#if PG_VERSION_NUM >= 130000
|
|
||||||
/* Precompute hash */
|
/* Precompute hash */
|
||||||
if (index == NULL)
|
if (index == NULL)
|
||||||
PrecomputeHash(base, element);
|
PrecomputeHash(base, element);
|
||||||
#endif
|
|
||||||
|
|
||||||
/* No neighbors if no entry point */
|
/* No neighbors if no entry point */
|
||||||
if (entryPoint == NULL)
|
if (entryPoint == NULL)
|
||||||
@@ -1230,7 +1326,7 @@ HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint
|
|||||||
/* 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(base, q, ep, 1, lc, index, procinfo, collation, m, true, skipElement);
|
w = HnswSearchLayer(base, q, ep, 1, lc, index, procinfo, collation, m, true, skipElement, NULL, NULL, true, NULL);
|
||||||
ep = w;
|
ep = w;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1246,16 +1342,27 @@ HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint
|
|||||||
{
|
{
|
||||||
int lm = HnswGetLayerM(m, lc);
|
int lm = HnswGetLayerM(m, lc);
|
||||||
List *neighbors;
|
List *neighbors;
|
||||||
List *lw;
|
List *lw = NIL;
|
||||||
|
ListCell *lc2;
|
||||||
|
|
||||||
w = HnswSearchLayer(base, q, ep, efConstruction, lc, index, procinfo, collation, m, true, skipElement);
|
w = HnswSearchLayer(base, q, ep, efConstruction, lc, index, procinfo, collation, m, true, skipElement, NULL, NULL, true, NULL);
|
||||||
|
|
||||||
|
/* Convert search candidates to candidates */
|
||||||
|
foreach(lc2, w)
|
||||||
|
{
|
||||||
|
HnswSearchCandidate *sc = lfirst(lc2);
|
||||||
|
HnswCandidate *hc = palloc(sizeof(HnswCandidate));
|
||||||
|
|
||||||
|
hc->element = sc->element;
|
||||||
|
hc->distance = sc->distance;
|
||||||
|
|
||||||
|
lw = lappend(lw, hc);
|
||||||
|
}
|
||||||
|
|
||||||
/* Elements being deleted or skipped can help with search */
|
/* Elements being deleted or skipped can help with search */
|
||||||
/* but should be removed before selecting neighbors */
|
/* but should be removed before selecting neighbors */
|
||||||
if (index != NULL)
|
if (index != NULL)
|
||||||
lw = RemoveElements(base, w, skipElement);
|
lw = RemoveElements(base, lw, skipElement);
|
||||||
else
|
|
||||||
lw = w;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Candidates are sorted, but not deterministically. Could set
|
* Candidates are sorted, but not deterministically. Could set
|
||||||
@@ -1280,7 +1387,9 @@ SparsevecCheckValue(Pointer v)
|
|||||||
SparseVector *vec = (SparseVector *) v;
|
SparseVector *vec = (SparseVector *) v;
|
||||||
|
|
||||||
if (vec->nnz > HNSW_MAX_NNZ)
|
if (vec->nnz > HNSW_MAX_NNZ)
|
||||||
elog(ERROR, "sparsevec cannot have more than %d non-zero elements for hnsw index", HNSW_MAX_NNZ);
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
|
||||||
|
errmsg("sparsevec cannot have more than %d non-zero elements for hnsw index", HNSW_MAX_NNZ)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -527,6 +527,14 @@ MarkDeleted(HnswVacuumState * vacuumstate)
|
|||||||
for (int i = 0; i < ntup->count; i++)
|
for (int i = 0; i < ntup->count; i++)
|
||||||
ItemPointerSetInvalid(&ntup->indextids[i]);
|
ItemPointerSetInvalid(&ntup->indextids[i]);
|
||||||
|
|
||||||
|
/* Increment version */
|
||||||
|
/* This is used to avoid incorrect reads for iterative scans */
|
||||||
|
/* Reserve some bits for future use */
|
||||||
|
etup->version++;
|
||||||
|
if (etup->version > 15)
|
||||||
|
etup->version = 1;
|
||||||
|
ntup->version = etup->version;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* We modified the tuples in place, no need to call
|
* We modified the tuples in place, no need to call
|
||||||
* PageIndexTupleOverwrite
|
* PageIndexTupleOverwrite
|
||||||
|
|||||||
@@ -26,12 +26,6 @@
|
|||||||
#include "pgstat.h"
|
#include "pgstat.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if PG_VERSION_NUM >= 130000
|
|
||||||
#define CALLBACK_ITEM_POINTER ItemPointer tid
|
|
||||||
#else
|
|
||||||
#define CALLBACK_ITEM_POINTER HeapTuple hup
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if PG_VERSION_NUM >= 140000
|
#if PG_VERSION_NUM >= 140000
|
||||||
#include "utils/backend_status.h"
|
#include "utils/backend_status.h"
|
||||||
#include "utils/wait_event.h"
|
#include "utils/wait_event.h"
|
||||||
@@ -96,7 +90,7 @@ AddSample(Datum *values, IvfflatBuildState * buildstate)
|
|||||||
* Callback for sampling
|
* Callback for sampling
|
||||||
*/
|
*/
|
||||||
static void
|
static void
|
||||||
SampleCallback(Relation index, CALLBACK_ITEM_POINTER, Datum *values,
|
SampleCallback(Relation index, ItemPointer tid, Datum *values,
|
||||||
bool *isnull, bool tupleIsAlive, void *state)
|
bool *isnull, bool tupleIsAlive, void *state)
|
||||||
{
|
{
|
||||||
IvfflatBuildState *buildstate = (IvfflatBuildState *) state;
|
IvfflatBuildState *buildstate = (IvfflatBuildState *) state;
|
||||||
@@ -207,16 +201,12 @@ AddTupleToSort(Relation index, ItemPointer tid, Datum *values, IvfflatBuildState
|
|||||||
* Callback for table_index_build_scan
|
* Callback for table_index_build_scan
|
||||||
*/
|
*/
|
||||||
static void
|
static void
|
||||||
BuildCallback(Relation index, CALLBACK_ITEM_POINTER, Datum *values,
|
BuildCallback(Relation index, ItemPointer tid, Datum *values,
|
||||||
bool *isnull, bool tupleIsAlive, void *state)
|
bool *isnull, bool tupleIsAlive, void *state)
|
||||||
{
|
{
|
||||||
IvfflatBuildState *buildstate = (IvfflatBuildState *) state;
|
IvfflatBuildState *buildstate = (IvfflatBuildState *) state;
|
||||||
MemoryContext oldCtx;
|
MemoryContext oldCtx;
|
||||||
|
|
||||||
#if PG_VERSION_NUM < 130000
|
|
||||||
ItemPointer tid = &hup->t_self;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* Skip nulls */
|
/* Skip nulls */
|
||||||
if (isnull[0])
|
if (isnull[0])
|
||||||
return;
|
return;
|
||||||
@@ -335,14 +325,20 @@ InitBuildState(IvfflatBuildState * buildstate, Relation heap, Relation index, In
|
|||||||
|
|
||||||
/* Disallow varbit since require fixed dimensions */
|
/* Disallow varbit since require fixed dimensions */
|
||||||
if (TupleDescAttr(index->rd_att, 0)->atttypid == VARBITOID)
|
if (TupleDescAttr(index->rd_att, 0)->atttypid == VARBITOID)
|
||||||
elog(ERROR, "type not supported for ivfflat index");
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||||
|
errmsg("type not supported for ivfflat index")));
|
||||||
|
|
||||||
/* Require column to have dimensions to be indexed */
|
/* Require column to have dimensions to be indexed */
|
||||||
if (buildstate->dimensions < 0)
|
if (buildstate->dimensions < 0)
|
||||||
elog(ERROR, "column does not have dimensions");
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||||
|
errmsg("column does not have dimensions")));
|
||||||
|
|
||||||
if (buildstate->dimensions > buildstate->typeInfo->maxDimensions)
|
if (buildstate->dimensions > buildstate->typeInfo->maxDimensions)
|
||||||
elog(ERROR, "column cannot have more than %d dimensions for ivfflat index", buildstate->typeInfo->maxDimensions);
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
|
||||||
|
errmsg("column cannot have more than %d dimensions for ivfflat index", buildstate->typeInfo->maxDimensions)));
|
||||||
|
|
||||||
buildstate->reltuples = 0;
|
buildstate->reltuples = 0;
|
||||||
buildstate->indtuples = 0;
|
buildstate->indtuples = 0;
|
||||||
@@ -355,7 +351,9 @@ InitBuildState(IvfflatBuildState * buildstate, Relation heap, Relation index, In
|
|||||||
|
|
||||||
/* Require more than one dimension for spherical k-means */
|
/* Require more than one dimension for spherical k-means */
|
||||||
if (buildstate->kmeansnormprocinfo != NULL && buildstate->dimensions == 1)
|
if (buildstate->kmeansnormprocinfo != NULL && buildstate->dimensions == 1)
|
||||||
elog(ERROR, "dimensions must be greater than one for this opclass");
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||||
|
errmsg("dimensions must be greater than one for this opclass")));
|
||||||
|
|
||||||
/* Create tuple description for sorting */
|
/* Create tuple description for sorting */
|
||||||
buildstate->tupdesc = CreateTemplateTupleDesc(3);
|
buildstate->tupdesc = CreateTemplateTupleDesc(3);
|
||||||
@@ -562,6 +560,20 @@ PrintKmeansMetrics(IvfflatBuildState * buildstate)
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Initialize build sort state
|
||||||
|
*/
|
||||||
|
static Tuplesortstate *
|
||||||
|
InitBuildSortState(TupleDesc tupdesc, int memory, SortCoordinate coordinate)
|
||||||
|
{
|
||||||
|
AttrNumber attNums[] = {1};
|
||||||
|
Oid sortOperators[] = {Int4LessOperator};
|
||||||
|
Oid sortCollations[] = {InvalidOid};
|
||||||
|
bool nullsFirstFlags[] = {false};
|
||||||
|
|
||||||
|
return tuplesort_begin_heap(tupdesc, 1, attNums, sortOperators, sortCollations, nullsFirstFlags, memory, coordinate, false);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Within leader, wait for end of heap scan
|
* Within leader, wait for end of heap scan
|
||||||
*/
|
*/
|
||||||
@@ -609,12 +621,6 @@ IvfflatParallelScanAndSort(IvfflatSpool * ivfspool, IvfflatShared * ivfshared, S
|
|||||||
double reltuples;
|
double reltuples;
|
||||||
IndexInfo *indexInfo;
|
IndexInfo *indexInfo;
|
||||||
|
|
||||||
/* Sort options, which must match AssignTuples */
|
|
||||||
AttrNumber attNums[] = {1};
|
|
||||||
Oid sortOperators[] = {Int4LessOperator};
|
|
||||||
Oid sortCollations[] = {InvalidOid};
|
|
||||||
bool nullsFirstFlags[] = {false};
|
|
||||||
|
|
||||||
/* Initialize local tuplesort coordination state */
|
/* Initialize local tuplesort coordination state */
|
||||||
coordinate = palloc0(sizeof(SortCoordinateData));
|
coordinate = palloc0(sizeof(SortCoordinateData));
|
||||||
coordinate->isWorker = true;
|
coordinate->isWorker = true;
|
||||||
@@ -627,7 +633,7 @@ IvfflatParallelScanAndSort(IvfflatSpool * ivfspool, IvfflatShared * ivfshared, S
|
|||||||
InitBuildState(&buildstate, ivfspool->heap, ivfspool->index, indexInfo);
|
InitBuildState(&buildstate, ivfspool->heap, ivfspool->index, indexInfo);
|
||||||
memcpy(buildstate.centers->items, ivfcenters, buildstate.centers->itemsize * buildstate.centers->maxlen);
|
memcpy(buildstate.centers->items, ivfcenters, buildstate.centers->itemsize * buildstate.centers->maxlen);
|
||||||
buildstate.centers->length = buildstate.centers->maxlen;
|
buildstate.centers->length = buildstate.centers->maxlen;
|
||||||
ivfspool->sortstate = tuplesort_begin_heap(buildstate.tupdesc, 1, attNums, sortOperators, sortCollations, nullsFirstFlags, sortmem, coordinate, false);
|
ivfspool->sortstate = InitBuildSortState(buildstate.tupdesc, sortmem, coordinate);
|
||||||
buildstate.sortstate = ivfspool->sortstate;
|
buildstate.sortstate = ivfspool->sortstate;
|
||||||
scan = table_beginscan_parallel(ivfspool->heap,
|
scan = table_beginscan_parallel(ivfspool->heap,
|
||||||
ParallelTableScanFromIvfflatShared(ivfshared));
|
ParallelTableScanFromIvfflatShared(ivfshared));
|
||||||
@@ -924,12 +930,6 @@ AssignTuples(IvfflatBuildState * buildstate)
|
|||||||
int parallel_workers = 0;
|
int parallel_workers = 0;
|
||||||
SortCoordinate coordinate = NULL;
|
SortCoordinate coordinate = NULL;
|
||||||
|
|
||||||
/* Sort options, which must match IvfflatParallelScanAndSort */
|
|
||||||
AttrNumber attNums[] = {1};
|
|
||||||
Oid sortOperators[] = {Int4LessOperator};
|
|
||||||
Oid sortCollations[] = {InvalidOid};
|
|
||||||
bool nullsFirstFlags[] = {false};
|
|
||||||
|
|
||||||
pgstat_progress_update_param(PROGRESS_CREATEIDX_SUBPHASE, PROGRESS_IVFFLAT_PHASE_ASSIGN);
|
pgstat_progress_update_param(PROGRESS_CREATEIDX_SUBPHASE, PROGRESS_IVFFLAT_PHASE_ASSIGN);
|
||||||
|
|
||||||
/* Calculate parallel workers */
|
/* Calculate parallel workers */
|
||||||
@@ -950,7 +950,7 @@ AssignTuples(IvfflatBuildState * buildstate)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Begin serial/leader tuplesort */
|
/* Begin serial/leader tuplesort */
|
||||||
buildstate->sortstate = tuplesort_begin_heap(buildstate->tupdesc, 1, attNums, sortOperators, sortCollations, nullsFirstFlags, maintenance_work_mem, coordinate, false);
|
buildstate->sortstate = InitBuildSortState(buildstate->tupdesc, maintenance_work_mem, coordinate);
|
||||||
|
|
||||||
/* Add tuples to sort */
|
/* Add tuples to sort */
|
||||||
if (buildstate->heap != NULL)
|
if (buildstate->heap != NULL)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "commands/progress.h"
|
#include "commands/progress.h"
|
||||||
#include "commands/vacuum.h"
|
#include "commands/vacuum.h"
|
||||||
#include "ivfflat.h"
|
#include "ivfflat.h"
|
||||||
|
#include "utils/float.h"
|
||||||
#include "utils/guc.h"
|
#include "utils/guc.h"
|
||||||
#include "utils/selfuncs.h"
|
#include "utils/selfuncs.h"
|
||||||
#include "utils/spccache.h"
|
#include "utils/spccache.h"
|
||||||
@@ -26,11 +27,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, IVFFLAT_MIN_LISTS, IVFFLAT_MAX_LISTS
|
IVFFLAT_DEFAULT_LISTS, IVFFLAT_MIN_LISTS, IVFFLAT_MAX_LISTS, AccessExclusiveLock);
|
||||||
#if PG_VERSION_NUM >= 130000
|
|
||||||
,AccessExclusiveLock
|
|
||||||
#endif
|
|
||||||
);
|
|
||||||
|
|
||||||
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,
|
||||||
@@ -78,8 +75,8 @@ ivfflatcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
|
|||||||
/* Never use index without order */
|
/* Never use index without order */
|
||||||
if (path->indexorderbys == NULL)
|
if (path->indexorderbys == NULL)
|
||||||
{
|
{
|
||||||
*indexStartupCost = DBL_MAX;
|
*indexStartupCost = get_float8_infinity();
|
||||||
*indexTotalCost = DBL_MAX;
|
*indexTotalCost = get_float8_infinity();
|
||||||
*indexSelectivity = 0;
|
*indexSelectivity = 0;
|
||||||
*indexCorrelation = 0;
|
*indexCorrelation = 0;
|
||||||
*indexPages = 0;
|
*indexPages = 0;
|
||||||
@@ -123,13 +120,6 @@ ivfflatcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
|
|||||||
costs.indexTotalCost -= 0.5 * costs.numIndexPages * (costs.spc_random_page_cost - spc_seq_page_cost);
|
costs.indexTotalCost -= 0.5 * costs.numIndexPages * (costs.spc_random_page_cost - spc_seq_page_cost);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* If the list selectivity is lower than what is returned from the generic
|
|
||||||
* cost estimator, use that.
|
|
||||||
*/
|
|
||||||
if (ratio < costs.indexSelectivity)
|
|
||||||
costs.indexSelectivity = ratio;
|
|
||||||
|
|
||||||
/* Use total cost since most work happens before first tuple is returned */
|
/* Use total cost since most work happens before first tuple is returned */
|
||||||
*indexStartupCost = costs.indexTotalCost;
|
*indexStartupCost = costs.indexTotalCost;
|
||||||
*indexTotalCost = costs.indexTotalCost;
|
*indexTotalCost = costs.indexTotalCost;
|
||||||
@@ -148,23 +138,10 @@ ivfflatoptions(Datum reloptions, bool validate)
|
|||||||
{"lists", RELOPT_TYPE_INT, offsetof(IvfflatOptions, lists)},
|
{"lists", RELOPT_TYPE_INT, offsetof(IvfflatOptions, lists)},
|
||||||
};
|
};
|
||||||
|
|
||||||
#if PG_VERSION_NUM >= 130000
|
|
||||||
return (bytea *) build_reloptions(reloptions, validate,
|
return (bytea *) build_reloptions(reloptions, validate,
|
||||||
ivfflat_relopt_kind,
|
ivfflat_relopt_kind,
|
||||||
sizeof(IvfflatOptions),
|
sizeof(IvfflatOptions),
|
||||||
tab, lengthof(tab));
|
tab, lengthof(tab));
|
||||||
#else
|
|
||||||
relopt_value *options;
|
|
||||||
int numoptions;
|
|
||||||
IvfflatOptions *rdopts;
|
|
||||||
|
|
||||||
options = parseRelOptions(reloptions, validate, ivfflat_relopt_kind, &numoptions);
|
|
||||||
rdopts = allocateReloptStruct(sizeof(IvfflatOptions), options, numoptions);
|
|
||||||
fillRelOptions((void *) rdopts, sizeof(IvfflatOptions), options, numoptions,
|
|
||||||
validate, tab, lengthof(tab));
|
|
||||||
|
|
||||||
return (bytea *) rdopts;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -189,9 +166,7 @@ ivfflathandler(PG_FUNCTION_ARGS)
|
|||||||
|
|
||||||
amroutine->amstrategies = 0;
|
amroutine->amstrategies = 0;
|
||||||
amroutine->amsupport = 5;
|
amroutine->amsupport = 5;
|
||||||
#if PG_VERSION_NUM >= 130000
|
|
||||||
amroutine->amoptsprocnum = 0;
|
amroutine->amoptsprocnum = 0;
|
||||||
#endif
|
|
||||||
amroutine->amcanorder = false;
|
amroutine->amcanorder = false;
|
||||||
amroutine->amcanorderbyop = true;
|
amroutine->amcanorderbyop = true;
|
||||||
amroutine->amcanbackward = false; /* can change direction mid-scan */
|
amroutine->amcanbackward = false; /* can change direction mid-scan */
|
||||||
@@ -204,17 +179,24 @@ ivfflathandler(PG_FUNCTION_ARGS)
|
|||||||
amroutine->amclusterable = false;
|
amroutine->amclusterable = false;
|
||||||
amroutine->ampredlocks = false;
|
amroutine->ampredlocks = false;
|
||||||
amroutine->amcanparallel = false;
|
amroutine->amcanparallel = false;
|
||||||
amroutine->amcaninclude = false;
|
#if PG_VERSION_NUM >= 170000
|
||||||
#if PG_VERSION_NUM >= 130000
|
amroutine->amcanbuildparallel = true;
|
||||||
amroutine->amusemaintenanceworkmem = false; /* not used during VACUUM */
|
|
||||||
amroutine->amparallelvacuumoptions = VACUUM_OPTION_PARALLEL_BULKDEL;
|
|
||||||
#endif
|
#endif
|
||||||
|
amroutine->amcaninclude = false;
|
||||||
|
amroutine->amusemaintenanceworkmem = false; /* not used during VACUUM */
|
||||||
|
#if PG_VERSION_NUM >= 160000
|
||||||
|
amroutine->amsummarizing = false;
|
||||||
|
#endif
|
||||||
|
amroutine->amparallelvacuumoptions = VACUUM_OPTION_PARALLEL_BULKDEL;
|
||||||
amroutine->amkeytype = InvalidOid;
|
amroutine->amkeytype = InvalidOid;
|
||||||
|
|
||||||
/* Interface functions */
|
/* Interface functions */
|
||||||
amroutine->ambuild = ivfflatbuild;
|
amroutine->ambuild = ivfflatbuild;
|
||||||
amroutine->ambuildempty = ivfflatbuildempty;
|
amroutine->ambuildempty = ivfflatbuildempty;
|
||||||
amroutine->aminsert = ivfflatinsert;
|
amroutine->aminsert = ivfflatinsert;
|
||||||
|
#if PG_VERSION_NUM >= 170000
|
||||||
|
amroutine->aminsertcleanup = NULL;
|
||||||
|
#endif
|
||||||
amroutine->ambulkdelete = ivfflatbulkdelete;
|
amroutine->ambulkdelete = ivfflatbulkdelete;
|
||||||
amroutine->amvacuumcleanup = ivfflatvacuumcleanup;
|
amroutine->amvacuumcleanup = ivfflatvacuumcleanup;
|
||||||
amroutine->amcanreturn = NULL; /* tuple not included in heapsort */
|
amroutine->amcanreturn = NULL; /* tuple not included in heapsort */
|
||||||
|
|||||||
@@ -253,8 +253,9 @@ typedef struct IvfflatScanOpaqueData
|
|||||||
/* Sorting */
|
/* Sorting */
|
||||||
Tuplesortstate *sortstate;
|
Tuplesortstate *sortstate;
|
||||||
TupleDesc tupdesc;
|
TupleDesc tupdesc;
|
||||||
TupleTableSlot *slot;
|
TupleTableSlot *vslot;
|
||||||
bool isnull;
|
TupleTableSlot *mslot;
|
||||||
|
BufferAccessStrategy bas;
|
||||||
|
|
||||||
/* Support functions */
|
/* Support functions */
|
||||||
FmgrInfo *procinfo;
|
FmgrInfo *procinfo;
|
||||||
|
|||||||
@@ -151,12 +151,8 @@ RandomCenters(Relation index, VectorArray centers, const IvfflatTypeInfo * typeI
|
|||||||
static void
|
static void
|
||||||
ShowMemoryUsage(MemoryContext context, Size estimatedSize)
|
ShowMemoryUsage(MemoryContext context, Size estimatedSize)
|
||||||
{
|
{
|
||||||
#if PG_VERSION_NUM >= 130000
|
|
||||||
elog(INFO, "total memory: %zu MB",
|
elog(INFO, "total memory: %zu MB",
|
||||||
MemoryContextMemAllocated(context, true) / (1024 * 1024));
|
MemoryContextMemAllocated(context, true) / (1024 * 1024));
|
||||||
#else
|
|
||||||
MemoryContextStats(context);
|
|
||||||
#endif
|
|
||||||
elog(INFO, "estimated memory: %zu MB", estimatedSize / (1024 * 1024));
|
elog(INFO, "estimated memory: %zu MB", estimatedSize / (1024 * 1024));
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -327,7 +323,7 @@ ElkanKmeans(Relation index, VectorArray samples, VectorArray centers, const Ivff
|
|||||||
newCenters->length = numCenters;
|
newCenters->length = numCenters;
|
||||||
|
|
||||||
#ifdef IVFFLAT_MEMORY
|
#ifdef IVFFLAT_MEMORY
|
||||||
ShowMemoryUsage(MemoryContextGetParent(CurrentMemoryContext));
|
ShowMemoryUsage(MemoryContextGetParent(CurrentMemoryContext), totalSize);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/* Pick initial centers */
|
/* Pick initial centers */
|
||||||
|
|||||||
@@ -11,16 +11,23 @@
|
|||||||
#include "pgstat.h"
|
#include "pgstat.h"
|
||||||
#include "storage/bufmgr.h"
|
#include "storage/bufmgr.h"
|
||||||
|
|
||||||
|
#ifdef IVFFLAT_MEMORY
|
||||||
|
#include "utils/memutils.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define GetScanList(ptr) pairingheap_container(IvfflatScanList, ph_node, ptr)
|
||||||
|
#define GetScanListConst(ptr) pairingheap_const_container(IvfflatScanList, ph_node, ptr)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Compare list distances
|
* Compare list distances
|
||||||
*/
|
*/
|
||||||
static int
|
static int
|
||||||
CompareLists(const pairingheap_node *a, const pairingheap_node *b, void *arg)
|
CompareLists(const pairingheap_node *a, const pairingheap_node *b, void *arg)
|
||||||
{
|
{
|
||||||
if (((const IvfflatScanList *) a)->distance > ((const IvfflatScanList *) b)->distance)
|
if (GetScanListConst(a)->distance > GetScanListConst(b)->distance)
|
||||||
return 1;
|
return 1;
|
||||||
|
|
||||||
if (((const IvfflatScanList *) a)->distance < ((const IvfflatScanList *) b)->distance)
|
if (GetScanListConst(a)->distance < GetScanListConst(b)->distance)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
@@ -72,14 +79,14 @@ GetScanLists(IndexScanDesc scan, Datum value)
|
|||||||
|
|
||||||
/* Calculate max distance */
|
/* Calculate max distance */
|
||||||
if (listCount == so->probes)
|
if (listCount == so->probes)
|
||||||
maxDistance = ((IvfflatScanList *) pairingheap_first(so->listQueue))->distance;
|
maxDistance = GetScanList(pairingheap_first(so->listQueue))->distance;
|
||||||
}
|
}
|
||||||
else if (distance < maxDistance)
|
else if (distance < maxDistance)
|
||||||
{
|
{
|
||||||
IvfflatScanList *scanlist;
|
IvfflatScanList *scanlist;
|
||||||
|
|
||||||
/* Remove */
|
/* Remove */
|
||||||
scanlist = (IvfflatScanList *) pairingheap_remove_first(so->listQueue);
|
scanlist = GetScanList(pairingheap_remove_first(so->listQueue));
|
||||||
|
|
||||||
/* Reuse */
|
/* Reuse */
|
||||||
scanlist->startPage = list->startPage;
|
scanlist->startPage = list->startPage;
|
||||||
@@ -87,7 +94,7 @@ GetScanLists(IndexScanDesc scan, Datum value)
|
|||||||
pairingheap_add(so->listQueue, &scanlist->ph_node);
|
pairingheap_add(so->listQueue, &scanlist->ph_node);
|
||||||
|
|
||||||
/* Update max distance */
|
/* Update max distance */
|
||||||
maxDistance = ((IvfflatScanList *) pairingheap_first(so->listQueue))->distance;
|
maxDistance = GetScanList(pairingheap_first(so->listQueue))->distance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,19 +113,12 @@ GetScanItems(IndexScanDesc scan, Datum value)
|
|||||||
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
|
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
|
||||||
TupleDesc tupdesc = RelationGetDescr(scan->indexRelation);
|
TupleDesc tupdesc = RelationGetDescr(scan->indexRelation);
|
||||||
double tuples = 0;
|
double tuples = 0;
|
||||||
TupleTableSlot *slot = MakeSingleTupleTableSlot(so->tupdesc, &TTSOpsVirtual);
|
TupleTableSlot *slot = so->vslot;
|
||||||
|
|
||||||
/*
|
|
||||||
* Reuse same set of shared buffers for scan
|
|
||||||
*
|
|
||||||
* See postgres/src/backend/storage/buffer/README for description
|
|
||||||
*/
|
|
||||||
BufferAccessStrategy bas = GetAccessStrategy(BAS_BULKREAD);
|
|
||||||
|
|
||||||
/* Search closest probes lists */
|
/* Search closest probes lists */
|
||||||
while (!pairingheap_is_empty(so->listQueue))
|
while (!pairingheap_is_empty(so->listQueue))
|
||||||
{
|
{
|
||||||
BlockNumber searchPage = ((IvfflatScanList *) pairingheap_remove_first(so->listQueue))->startPage;
|
BlockNumber searchPage = GetScanList(pairingheap_remove_first(so->listQueue))->startPage;
|
||||||
|
|
||||||
/* Search all entry pages for list */
|
/* Search all entry pages for list */
|
||||||
while (BlockNumberIsValid(searchPage))
|
while (BlockNumberIsValid(searchPage))
|
||||||
@@ -127,7 +127,7 @@ GetScanItems(IndexScanDesc scan, Datum value)
|
|||||||
Page page;
|
Page page;
|
||||||
OffsetNumber maxoffno;
|
OffsetNumber maxoffno;
|
||||||
|
|
||||||
buf = ReadBufferExtended(scan->indexRelation, MAIN_FORKNUM, searchPage, RBM_NORMAL, bas);
|
buf = ReadBufferExtended(scan->indexRelation, MAIN_FORKNUM, searchPage, RBM_NORMAL, so->bas);
|
||||||
LockBuffer(buf, BUFFER_LOCK_SHARE);
|
LockBuffer(buf, BUFFER_LOCK_SHARE);
|
||||||
page = BufferGetPage(buf);
|
page = BufferGetPage(buf);
|
||||||
maxoffno = PageGetMaxOffsetNumber(page);
|
maxoffno = PageGetMaxOffsetNumber(page);
|
||||||
@@ -166,8 +166,6 @@ GetScanItems(IndexScanDesc scan, Datum value)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FreeAccessStrategy(bas);
|
|
||||||
|
|
||||||
if (tuples < 100)
|
if (tuples < 100)
|
||||||
ereport(DEBUG1,
|
ereport(DEBUG1,
|
||||||
(errmsg("index scan found few tuples"),
|
(errmsg("index scan found few tuples"),
|
||||||
@@ -217,6 +215,20 @@ GetScanValue(IndexScanDesc scan)
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Initialize scan sort state
|
||||||
|
*/
|
||||||
|
static Tuplesortstate *
|
||||||
|
InitScanSortState(TupleDesc tupdesc)
|
||||||
|
{
|
||||||
|
AttrNumber attNums[] = {1};
|
||||||
|
Oid sortOperators[] = {Float8LessOperator};
|
||||||
|
Oid sortCollations[] = {InvalidOid};
|
||||||
|
bool nullsFirstFlags[] = {false};
|
||||||
|
|
||||||
|
return tuplesort_begin_heap(tupdesc, 1, attNums, sortOperators, sortCollations, nullsFirstFlags, work_mem, NULL, false);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Prepare for an index scan
|
* Prepare for an index scan
|
||||||
*/
|
*/
|
||||||
@@ -227,10 +239,6 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys)
|
|||||||
IvfflatScanOpaque so;
|
IvfflatScanOpaque so;
|
||||||
int lists;
|
int lists;
|
||||||
int dimensions;
|
int dimensions;
|
||||||
AttrNumber attNums[] = {1};
|
|
||||||
Oid sortOperators[] = {Float8LessOperator};
|
|
||||||
Oid sortCollations[] = {InvalidOid};
|
|
||||||
bool nullsFirstFlags[] = {false};
|
|
||||||
int probes = ivfflat_probes;
|
int probes = ivfflat_probes;
|
||||||
|
|
||||||
scan = RelationGetIndexScan(index, nkeys, norderbys);
|
scan = RelationGetIndexScan(index, nkeys, norderbys);
|
||||||
@@ -258,9 +266,18 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys)
|
|||||||
TupleDescInitEntry(so->tupdesc, (AttrNumber) 2, "heaptid", TIDOID, -1, 0);
|
TupleDescInitEntry(so->tupdesc, (AttrNumber) 2, "heaptid", TIDOID, -1, 0);
|
||||||
|
|
||||||
/* Prep sort */
|
/* Prep sort */
|
||||||
so->sortstate = tuplesort_begin_heap(so->tupdesc, 1, attNums, sortOperators, sortCollations, nullsFirstFlags, work_mem, NULL, false);
|
so->sortstate = InitScanSortState(so->tupdesc);
|
||||||
|
|
||||||
so->slot = MakeSingleTupleTableSlot(so->tupdesc, &TTSOpsMinimalTuple);
|
/* Need separate slots for puttuple and gettuple */
|
||||||
|
so->vslot = MakeSingleTupleTableSlot(so->tupdesc, &TTSOpsVirtual);
|
||||||
|
so->mslot = MakeSingleTupleTableSlot(so->tupdesc, &TTSOpsMinimalTuple);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Reuse same set of shared buffers for scan
|
||||||
|
*
|
||||||
|
* See postgres/src/backend/storage/buffer/README for description
|
||||||
|
*/
|
||||||
|
so->bas = GetAccessStrategy(BAS_BULKREAD);
|
||||||
|
|
||||||
so->listQueue = pairingheap_allocate(CompareLists, scan);
|
so->listQueue = pairingheap_allocate(CompareLists, scan);
|
||||||
|
|
||||||
@@ -277,10 +294,8 @@ ivfflatrescan(IndexScanDesc scan, ScanKey keys, int nkeys, ScanKey orderbys, int
|
|||||||
{
|
{
|
||||||
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
|
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
|
||||||
|
|
||||||
#if PG_VERSION_NUM >= 130000
|
|
||||||
if (!so->first)
|
if (!so->first)
|
||||||
tuplesort_reset(so->sortstate);
|
tuplesort_reset(so->sortstate);
|
||||||
#endif
|
|
||||||
|
|
||||||
so->first = true;
|
so->first = true;
|
||||||
pairingheap_reset(so->listQueue);
|
pairingheap_reset(so->listQueue);
|
||||||
@@ -327,14 +342,19 @@ ivfflatgettuple(IndexScanDesc scan, ScanDirection dir)
|
|||||||
IvfflatBench("GetScanItems", GetScanItems(scan, value));
|
IvfflatBench("GetScanItems", GetScanItems(scan, value));
|
||||||
so->first = false;
|
so->first = false;
|
||||||
|
|
||||||
|
#if defined(IVFFLAT_MEMORY)
|
||||||
|
elog(INFO, "memory: %zu MB", MemoryContextMemAllocated(CurrentMemoryContext, true) / (1024 * 1024));
|
||||||
|
#endif
|
||||||
|
|
||||||
/* Clean up if we allocated a new value */
|
/* Clean up if we allocated a new value */
|
||||||
if (value != scan->orderByData->sk_argument)
|
if (value != scan->orderByData->sk_argument)
|
||||||
pfree(DatumGetPointer(value));
|
pfree(DatumGetPointer(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tuplesort_gettupleslot(so->sortstate, true, false, so->slot, NULL))
|
if (tuplesort_gettupleslot(so->sortstate, true, false, so->mslot, NULL))
|
||||||
{
|
{
|
||||||
ItemPointer heaptid = (ItemPointer) DatumGetPointer(slot_getattr(so->slot, 2, &so->isnull));
|
bool isnull;
|
||||||
|
ItemPointer heaptid = (ItemPointer) DatumGetPointer(slot_getattr(so->mslot, 2, &isnull));
|
||||||
|
|
||||||
scan->xs_heaptid = *heaptid;
|
scan->xs_heaptid = *heaptid;
|
||||||
scan->xs_recheck = false;
|
scan->xs_recheck = false;
|
||||||
@@ -355,6 +375,10 @@ ivfflatendscan(IndexScanDesc scan)
|
|||||||
|
|
||||||
pairingheap_free(so->listQueue);
|
pairingheap_free(so->listQueue);
|
||||||
tuplesort_end(so->sortstate);
|
tuplesort_end(so->sortstate);
|
||||||
|
FreeAccessStrategy(so->bas);
|
||||||
|
FreeTupleDesc(so->tupdesc);
|
||||||
|
|
||||||
|
/* TODO Free vslot and mslot without freeing TupleDesc */
|
||||||
|
|
||||||
pfree(so);
|
pfree(so);
|
||||||
scan->opaque = NULL;
|
scan->opaque = NULL;
|
||||||
|
|||||||
133
src/sparsevec.c
133
src/sparsevec.c
@@ -3,6 +3,7 @@
|
|||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
|
|
||||||
|
#include "catalog/pg_type.h"
|
||||||
#include "common/string.h"
|
#include "common/string.h"
|
||||||
#include "fmgr.h"
|
#include "fmgr.h"
|
||||||
#include "halfutils.h"
|
#include "halfutils.h"
|
||||||
@@ -11,6 +12,7 @@
|
|||||||
#include "sparsevec.h"
|
#include "sparsevec.h"
|
||||||
#include "utils/array.h"
|
#include "utils/array.h"
|
||||||
#include "utils/builtins.h"
|
#include "utils/builtins.h"
|
||||||
|
#include "utils/lsyscache.h"
|
||||||
#include "vector.h"
|
#include "vector.h"
|
||||||
|
|
||||||
#if PG_VERSION_NUM >= 120000
|
#if PG_VERSION_NUM >= 120000
|
||||||
@@ -670,6 +672,137 @@ halfvec_to_sparsevec(PG_FUNCTION_ARGS)
|
|||||||
PG_RETURN_POINTER(result);
|
PG_RETURN_POINTER(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Convert array to sparse vector
|
||||||
|
*/
|
||||||
|
FUNCTION_PREFIX PG_FUNCTION_INFO_V1(array_to_sparsevec);
|
||||||
|
Datum
|
||||||
|
array_to_sparsevec(PG_FUNCTION_ARGS)
|
||||||
|
{
|
||||||
|
ArrayType *array = PG_GETARG_ARRAYTYPE_P(0);
|
||||||
|
int32 typmod = PG_GETARG_INT32(1);
|
||||||
|
SparseVector *result;
|
||||||
|
int16 typlen;
|
||||||
|
bool typbyval;
|
||||||
|
char typalign;
|
||||||
|
Datum *elemsp;
|
||||||
|
int nelemsp;
|
||||||
|
int nnz = 0;
|
||||||
|
float *values;
|
||||||
|
int j = 0;
|
||||||
|
|
||||||
|
if (ARR_NDIM(array) > 1)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_DATA_EXCEPTION),
|
||||||
|
errmsg("array must be 1-D")));
|
||||||
|
|
||||||
|
if (ARR_HASNULL(array) && array_contains_nulls(array))
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
|
||||||
|
errmsg("array must not contain nulls")));
|
||||||
|
|
||||||
|
get_typlenbyvalalign(ARR_ELEMTYPE(array), &typlen, &typbyval, &typalign);
|
||||||
|
deconstruct_array(array, ARR_ELEMTYPE(array), typlen, typbyval, typalign, &elemsp, NULL, &nelemsp);
|
||||||
|
|
||||||
|
CheckDim(nelemsp);
|
||||||
|
CheckExpectedDim(typmod, nelemsp);
|
||||||
|
|
||||||
|
#ifdef _MSC_VER
|
||||||
|
/* /fp:fast may not propagate +/-Infinity or NaN */
|
||||||
|
#define IS_NOT_ZERO(v) (isnan((float) (v)) || isinf((float) (v)) || ((float) (v)) != 0)
|
||||||
|
#else
|
||||||
|
#define IS_NOT_ZERO(v) (((float) (v)) != 0)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (ARR_ELEMTYPE(array) == INT4OID)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < nelemsp; i++)
|
||||||
|
nnz += IS_NOT_ZERO(DatumGetInt32(elemsp[i]));
|
||||||
|
}
|
||||||
|
else if (ARR_ELEMTYPE(array) == FLOAT8OID)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < nelemsp; i++)
|
||||||
|
nnz += IS_NOT_ZERO(DatumGetFloat8(elemsp[i]));
|
||||||
|
}
|
||||||
|
else if (ARR_ELEMTYPE(array) == FLOAT4OID)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < nelemsp; i++)
|
||||||
|
nnz += IS_NOT_ZERO(DatumGetFloat4(elemsp[i]));
|
||||||
|
}
|
||||||
|
else if (ARR_ELEMTYPE(array) == NUMERICOID)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < nelemsp; i++)
|
||||||
|
nnz += IS_NOT_ZERO(DirectFunctionCall1(numeric_float4, elemsp[i]));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_DATA_EXCEPTION),
|
||||||
|
errmsg("unsupported array type")));
|
||||||
|
}
|
||||||
|
|
||||||
|
result = InitSparseVector(nelemsp, nnz);
|
||||||
|
values = SPARSEVEC_VALUES(result);
|
||||||
|
|
||||||
|
#define PROCESS_ARRAY_ELEM(elem) \
|
||||||
|
do { \
|
||||||
|
float v = (float) (elem); \
|
||||||
|
if (IS_NOT_ZERO(v)) { \
|
||||||
|
/* Safety check */ \
|
||||||
|
if (j >= result->nnz) \
|
||||||
|
elog(ERROR, "safety check failed"); \
|
||||||
|
result->indices[j] = i; \
|
||||||
|
values[j] = v; \
|
||||||
|
j++; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
if (ARR_ELEMTYPE(array) == INT4OID)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < nelemsp; i++)
|
||||||
|
PROCESS_ARRAY_ELEM(DatumGetInt32(elemsp[i]));
|
||||||
|
}
|
||||||
|
else if (ARR_ELEMTYPE(array) == FLOAT8OID)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < nelemsp; i++)
|
||||||
|
PROCESS_ARRAY_ELEM(DatumGetFloat8(elemsp[i]));
|
||||||
|
}
|
||||||
|
else if (ARR_ELEMTYPE(array) == FLOAT4OID)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < nelemsp; i++)
|
||||||
|
PROCESS_ARRAY_ELEM(DatumGetFloat4(elemsp[i]));
|
||||||
|
}
|
||||||
|
else if (ARR_ELEMTYPE(array) == NUMERICOID)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < nelemsp; i++)
|
||||||
|
PROCESS_ARRAY_ELEM(DatumGetFloat4(DirectFunctionCall1(numeric_float4, elemsp[i])));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_DATA_EXCEPTION),
|
||||||
|
errmsg("unsupported array type")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef PROCESS_ARRAY_ELEM
|
||||||
|
#undef IS_NOT_ZERO
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Free allocation from deconstruct_array. Do not free individual elements
|
||||||
|
* when pass-by-reference since they point to original array.
|
||||||
|
*/
|
||||||
|
pfree(elemsp);
|
||||||
|
|
||||||
|
if (j != result->nnz)
|
||||||
|
elog(ERROR, "correctness check failed");
|
||||||
|
|
||||||
|
/* Check elements */
|
||||||
|
for (int i = 0; i < result->nnz; i++)
|
||||||
|
CheckElement(values[i]);
|
||||||
|
|
||||||
|
PG_RETURN_POINTER(result);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Get the L2 squared distance between sparse vectors
|
* Get the L2 squared distance between sparse vectors
|
||||||
*/
|
*/
|
||||||
|
|||||||
23
src/vector.c
23
src/vector.c
@@ -26,11 +26,6 @@
|
|||||||
#include "varatt.h"
|
#include "varatt.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if PG_VERSION_NUM < 130000
|
|
||||||
#define TYPALIGN_DOUBLE 'd'
|
|
||||||
#define TYPALIGN_INT 'i'
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define STATE_DIMS(x) (ARR_DIMS(x)[0] - 1)
|
#define STATE_DIMS(x) (ARR_DIMS(x)[0] - 1)
|
||||||
#define CreateStateDatums(dim) palloc(sizeof(Datum) * (dim + 1))
|
#define CreateStateDatums(dim) palloc(sizeof(Datum) * (dim + 1))
|
||||||
|
|
||||||
@@ -160,24 +155,6 @@ CheckStateArray(ArrayType *statearray, const char *caller)
|
|||||||
return (float8 *) ARR_DATA_PTR(statearray);
|
return (float8 *) ARR_DATA_PTR(statearray);
|
||||||
}
|
}
|
||||||
|
|
||||||
#if PG_VERSION_NUM < 120003
|
|
||||||
static pg_noinline void
|
|
||||||
float_overflow_error(void)
|
|
||||||
{
|
|
||||||
ereport(ERROR,
|
|
||||||
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
|
||||||
errmsg("value out of range: overflow")));
|
|
||||||
}
|
|
||||||
|
|
||||||
static pg_noinline void
|
|
||||||
float_underflow_error(void)
|
|
||||||
{
|
|
||||||
ereport(ERROR,
|
|
||||||
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
|
||||||
errmsg("value out of range: underflow")));
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Convert textual representation to internal representation
|
* Convert textual representation to internal representation
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -208,6 +208,62 @@ SELECT '{1:1e-8}/1'::sparsevec::halfvec;
|
|||||||
[0]
|
[0]
|
||||||
(1 row)
|
(1 row)
|
||||||
|
|
||||||
|
SELECT ARRAY[1,0,2,0,3,0]::sparsevec;
|
||||||
|
array
|
||||||
|
-----------------
|
||||||
|
{1:1,3:2,5:3}/6
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
SELECT ARRAY[1.0,0.0,2.0,0.0,3.0,0.0]::sparsevec;
|
||||||
|
array
|
||||||
|
-----------------
|
||||||
|
{1:1,3:2,5:3}/6
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
SELECT ARRAY[1,0,2,0,3,0]::float4[]::sparsevec;
|
||||||
|
array
|
||||||
|
-----------------
|
||||||
|
{1:1,3:2,5:3}/6
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
SELECT ARRAY[1,0,2,0,3,0]::float8[]::sparsevec;
|
||||||
|
array
|
||||||
|
-----------------
|
||||||
|
{1:1,3:2,5:3}/6
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
SELECT ARRAY[1,0,2,0,3,0]::numeric[]::sparsevec;
|
||||||
|
array
|
||||||
|
-----------------
|
||||||
|
{1:1,3:2,5:3}/6
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
SELECT '{1,0,2,0,3,0}'::real[]::sparsevec;
|
||||||
|
sparsevec
|
||||||
|
-----------------
|
||||||
|
{1:1,3:2,5:3}/6
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
SELECT '{1,0,2,0,3,0}'::real[]::sparsevec(6);
|
||||||
|
sparsevec
|
||||||
|
-----------------
|
||||||
|
{1:1,3:2,5:3}/6
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
SELECT '{1,0,2,0,3,0}'::real[]::sparsevec(5);
|
||||||
|
ERROR: expected 5 dimensions, not 6
|
||||||
|
SELECT '{NULL}'::real[]::sparsevec;
|
||||||
|
ERROR: array must not contain nulls
|
||||||
|
SELECT '{NaN}'::real[]::sparsevec;
|
||||||
|
ERROR: NaN not allowed in sparsevec
|
||||||
|
SELECT '{Infinity}'::real[]::sparsevec;
|
||||||
|
ERROR: infinite value not allowed in sparsevec
|
||||||
|
SELECT '{-Infinity}'::real[]::sparsevec;
|
||||||
|
ERROR: infinite value not allowed in sparsevec
|
||||||
|
SELECT '{}'::real[]::sparsevec;
|
||||||
|
ERROR: sparsevec must have at least 1 dimension
|
||||||
|
SELECT '{{1}}'::real[]::sparsevec;
|
||||||
|
ERROR: array must be 1-D
|
||||||
SELECT array_agg(n)::vector FROM generate_series(1, 16001) n;
|
SELECT array_agg(n)::vector FROM generate_series(1, 16001) n;
|
||||||
ERROR: vector cannot have more than 16000 dimensions
|
ERROR: vector cannot have more than 16000 dimensions
|
||||||
SELECT array_to_vector(array_agg(n), 16001, false) FROM generate_series(1, 16001) n;
|
SELECT array_to_vector(array_agg(n), 16001, false) FROM generate_series(1, 16001) n;
|
||||||
|
|||||||
11
test/perl/PostgreSQL/Test/Cluster.pm
Normal file
11
test/perl/PostgreSQL/Test/Cluster.pm
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package PostgreSQL::Test::Cluster;
|
||||||
|
|
||||||
|
use PostgresNode;
|
||||||
|
|
||||||
|
sub new
|
||||||
|
{
|
||||||
|
my ($class, $name) = @_;
|
||||||
|
return get_new_node($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
1;
|
||||||
5
test/perl/PostgreSQL/Test/Utils.pm
Normal file
5
test/perl/PostgreSQL/Test/Utils.pm
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package PostgreSQL::Test::Utils;
|
||||||
|
|
||||||
|
use TestLib;
|
||||||
|
|
||||||
|
1;
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
use PostgreSQL::Test::Cluster;
|
|
||||||
|
|
||||||
sub get_new_node
|
|
||||||
{
|
|
||||||
return PostgreSQL::Test::Cluster->new(@_);
|
|
||||||
}
|
|
||||||
|
|
||||||
1;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
use PostgreSQL::Test::Utils;
|
|
||||||
|
|
||||||
1;
|
|
||||||
@@ -58,6 +58,22 @@ SELECT '{}/16001'::sparsevec::halfvec;
|
|||||||
SELECT '{1:65520}/1'::sparsevec::halfvec;
|
SELECT '{1:65520}/1'::sparsevec::halfvec;
|
||||||
SELECT '{1:1e-8}/1'::sparsevec::halfvec;
|
SELECT '{1:1e-8}/1'::sparsevec::halfvec;
|
||||||
|
|
||||||
|
SELECT ARRAY[1,0,2,0,3,0]::sparsevec;
|
||||||
|
SELECT ARRAY[1.0,0.0,2.0,0.0,3.0,0.0]::sparsevec;
|
||||||
|
SELECT ARRAY[1,0,2,0,3,0]::float4[]::sparsevec;
|
||||||
|
SELECT ARRAY[1,0,2,0,3,0]::float8[]::sparsevec;
|
||||||
|
SELECT ARRAY[1,0,2,0,3,0]::numeric[]::sparsevec;
|
||||||
|
|
||||||
|
SELECT '{1,0,2,0,3,0}'::real[]::sparsevec;
|
||||||
|
SELECT '{1,0,2,0,3,0}'::real[]::sparsevec(6);
|
||||||
|
SELECT '{1,0,2,0,3,0}'::real[]::sparsevec(5);
|
||||||
|
SELECT '{NULL}'::real[]::sparsevec;
|
||||||
|
SELECT '{NaN}'::real[]::sparsevec;
|
||||||
|
SELECT '{Infinity}'::real[]::sparsevec;
|
||||||
|
SELECT '{-Infinity}'::real[]::sparsevec;
|
||||||
|
SELECT '{}'::real[]::sparsevec;
|
||||||
|
SELECT '{{1}}'::real[]::sparsevec;
|
||||||
|
|
||||||
SELECT array_agg(n)::vector FROM generate_series(1, 16001) n;
|
SELECT array_agg(n)::vector FROM generate_series(1, 16001) n;
|
||||||
SELECT array_to_vector(array_agg(n), 16001, false) FROM generate_series(1, 16001) n;
|
SELECT array_to_vector(array_agg(n), 16001, false) FROM generate_series(1, 16001) n;
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
# Test generic xlog record work for ivfflat index replication.
|
# Test generic xlog record work for ivfflat index replication.
|
||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
my $dim = 32;
|
my $dim = 32;
|
||||||
@@ -49,7 +49,7 @@ sub test_index_replay
|
|||||||
my $array_sql = join(",", ('random()') x $dim);
|
my $array_sql = join(",", ('random()') x $dim);
|
||||||
|
|
||||||
# Initialize primary node
|
# Initialize primary node
|
||||||
$node_primary = get_new_node('primary');
|
$node_primary = PostgreSQL::Test::Cluster->new('primary');
|
||||||
$node_primary->init(allows_streaming => 1);
|
$node_primary->init(allows_streaming => 1);
|
||||||
if ($dim > 32)
|
if ($dim > 32)
|
||||||
{
|
{
|
||||||
@@ -67,7 +67,7 @@ my $backup_name = 'my_backup';
|
|||||||
$node_primary->backup($backup_name);
|
$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 = PostgreSQL::Test::Cluster->new('replica');
|
||||||
$node_replica->init_from_backup($node_primary, $backup_name, has_streaming => 1);
|
$node_replica->init_from_backup($node_primary, $backup_name, has_streaming => 1);
|
||||||
$node_replica->start;
|
$node_replica->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
my $dim = 3;
|
my $dim = 3;
|
||||||
@@ -15,7 +15,7 @@ for (1 .. $dim)
|
|||||||
my $array_sql = join(", ", @r);
|
my $array_sql = join(", ", @r);
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
my $node = get_new_node('node');
|
my $node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
my $node;
|
my $node;
|
||||||
@@ -49,7 +49,7 @@ sub test_recall
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
$node = get_new_node('node');
|
$node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
my $node;
|
my $node;
|
||||||
@@ -48,7 +48,7 @@ sub test_recall
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
$node = get_new_node('node');
|
$node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
my $node = get_new_node('node');
|
my $node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
my $node = get_new_node('node');
|
my $node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
my $dim = 768;
|
my $dim = 768;
|
||||||
@@ -9,7 +9,7 @@ my $dim = 768;
|
|||||||
my $array_sql = join(",", ('random()') x $dim);
|
my $array_sql = join(",", ('random()') x $dim);
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
my $node = get_new_node('node');
|
my $node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
my $node = get_new_node('node');
|
my $node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
my $dim = 3;
|
my $dim = 3;
|
||||||
@@ -11,7 +11,7 @@ my $limit = 20;
|
|||||||
my $array_sql = join(",", ('random()') x $dim);
|
my $array_sql = join(",", ('random()') x $dim);
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
my $node = get_new_node('node');
|
my $node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
@@ -94,8 +94,7 @@ like($explain, qr/Seq Scan/);
|
|||||||
$explain = $node->safe_psql("postgres", qq(
|
$explain = $node->safe_psql("postgres", qq(
|
||||||
EXPLAIN ANALYZE SELECT i FROM tst WHERE v <-> '$query' < 1 ORDER BY v <-> '$query';
|
EXPLAIN ANALYZE SELECT i FROM tst WHERE v <-> '$query' < 1 ORDER BY v <-> '$query';
|
||||||
));
|
));
|
||||||
# TODO Do not use index
|
like($explain, qr/Seq Scan/);
|
||||||
like($explain, qr/Index Scan using idx/);
|
|
||||||
|
|
||||||
# Test attribute index
|
# Test attribute index
|
||||||
$node->safe_psql("postgres", "CREATE INDEX attribute_idx ON tst (c);");
|
$node->safe_psql("postgres", "CREATE INDEX attribute_idx ON tst (c);");
|
||||||
@@ -110,7 +109,6 @@ $node->safe_psql("postgres", "CREATE INDEX partial_idx ON tst USING ivfflat (v v
|
|||||||
$explain = $node->safe_psql("postgres", qq(
|
$explain = $node->safe_psql("postgres", qq(
|
||||||
EXPLAIN ANALYZE SELECT i FROM tst WHERE c = $c ORDER BY v <-> '$query' LIMIT $limit;
|
EXPLAIN ANALYZE SELECT i FROM tst WHERE c = $c ORDER BY v <-> '$query' LIMIT $limit;
|
||||||
));
|
));
|
||||||
# TODO Use partial index
|
like($explain, qr/Index Scan using partial_idx/);
|
||||||
like($explain, qr/Index Scan using idx/);
|
|
||||||
|
|
||||||
done_testing();
|
done_testing();
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
# Test generic xlog record work for hnsw index replication.
|
# Test generic xlog record work for hnsw index replication.
|
||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
my $dim = 32;
|
my $dim = 32;
|
||||||
@@ -49,7 +49,7 @@ sub test_index_replay
|
|||||||
my $array_sql = join(",", ('random()') x $dim);
|
my $array_sql = join(",", ('random()') x $dim);
|
||||||
|
|
||||||
# Initialize primary node
|
# Initialize primary node
|
||||||
$node_primary = get_new_node('primary');
|
$node_primary = PostgreSQL::Test::Cluster->new('primary');
|
||||||
$node_primary->init(allows_streaming => 1);
|
$node_primary->init(allows_streaming => 1);
|
||||||
if ($dim > 32)
|
if ($dim > 32)
|
||||||
{
|
{
|
||||||
@@ -67,7 +67,7 @@ my $backup_name = 'my_backup';
|
|||||||
$node_primary->backup($backup_name);
|
$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 = PostgreSQL::Test::Cluster->new('replica');
|
||||||
$node_replica->init_from_backup($node_primary, $backup_name, has_streaming => 1);
|
$node_replica->init_from_backup($node_primary, $backup_name, has_streaming => 1);
|
||||||
$node_replica->start;
|
$node_replica->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
my $dim = 3;
|
my $dim = 3;
|
||||||
@@ -15,7 +15,7 @@ for (1 .. $dim)
|
|||||||
my $array_sql = join(", ", @r);
|
my $array_sql = join(", ", @r);
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
my $node = get_new_node('node');
|
my $node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
my $node;
|
my $node;
|
||||||
@@ -47,7 +47,7 @@ sub test_recall
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
$node = get_new_node('node');
|
$node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
my $node;
|
my $node;
|
||||||
@@ -47,7 +47,7 @@ sub test_recall
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
$node = get_new_node('node');
|
$node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
my $node;
|
my $node;
|
||||||
@@ -48,7 +48,7 @@ sub test_recall
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
$node = get_new_node('node');
|
$node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
my $node = get_new_node('node');
|
my $node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
# Ensures elements and neighbors on both same and different pages
|
# Ensures elements and neighbors on both same and different pages
|
||||||
@@ -10,7 +10,7 @@ my $dim = 1900;
|
|||||||
my $array_sql = join(",", ('random()') x $dim);
|
my $array_sql = join(",", ('random()') x $dim);
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
my $node = get_new_node('node');
|
my $node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
my $dim = 3;
|
my $dim = 3;
|
||||||
@@ -11,16 +11,20 @@ my $limit = 20;
|
|||||||
my $array_sql = join(",", ('random()') x $dim);
|
my $array_sql = join(",", ('random()') x $dim);
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
my $node = get_new_node('node');
|
my $node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$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 (i int4, v vector($dim), c int4, t text);");
|
$node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector($dim), c int4, t text);");
|
||||||
|
$node->safe_psql("postgres", "CREATE TABLE cat (i int4 PRIMARY KEY, t text, b boolean);");
|
||||||
$node->safe_psql("postgres",
|
$node->safe_psql("postgres",
|
||||||
"INSERT INTO tst SELECT i, ARRAY[$array_sql], i % $nc, 'test ' || i FROM generate_series(1, 10000) i;"
|
"INSERT INTO tst SELECT i, ARRAY[$array_sql], i % $nc, 'test ' || i FROM generate_series(1, 10000) i;"
|
||||||
);
|
);
|
||||||
|
$node->safe_psql("postgres",
|
||||||
|
"INSERT INTO cat SELECT i, 'cat ' || i, i % 5 = 0 FROM generate_series(1, $nc) i;"
|
||||||
|
);
|
||||||
$node->safe_psql("postgres", "CREATE INDEX idx ON tst USING hnsw (v vector_l2_ops);");
|
$node->safe_psql("postgres", "CREATE INDEX idx ON tst USING hnsw (v vector_l2_ops);");
|
||||||
$node->safe_psql("postgres", "ANALYZE tst;");
|
$node->safe_psql("postgres", "ANALYZE tst;");
|
||||||
|
|
||||||
@@ -96,13 +100,25 @@ $explain = $node->safe_psql("postgres", qq(
|
|||||||
));
|
));
|
||||||
like($explain, qr/Seq Scan/);
|
like($explain, qr/Seq Scan/);
|
||||||
|
|
||||||
|
# Test join
|
||||||
|
$explain = $node->safe_psql("postgres", qq(
|
||||||
|
EXPLAIN ANALYZE SELECT cat.t FROM cat INNER JOIN tst ON cat.i = tst.c ORDER BY v <-> '$query' LIMIT $limit;
|
||||||
|
));
|
||||||
|
like($explain, qr/Index Scan using idx/);
|
||||||
|
|
||||||
|
# Test join with attribute filtering
|
||||||
|
$explain = $node->safe_psql("postgres", qq(
|
||||||
|
EXPLAIN ANALYZE SELECT cat.t FROM cat INNER JOIN tst ON cat.i = tst.c WHERE cat.b = 't' ORDER BY v <-> '$query' LIMIT $limit;
|
||||||
|
));
|
||||||
|
like($explain, qr/Index Scan using idx/);
|
||||||
|
|
||||||
# Test attribute index
|
# Test attribute index
|
||||||
$node->safe_psql("postgres", "CREATE INDEX attribute_idx ON tst (c);");
|
$node->safe_psql("postgres", "CREATE INDEX attribute_idx ON tst (c);");
|
||||||
$explain = $node->safe_psql("postgres", qq(
|
$explain = $node->safe_psql("postgres", qq(
|
||||||
EXPLAIN ANALYZE SELECT i FROM tst WHERE c = $c ORDER BY v <-> '$query' LIMIT $limit;
|
EXPLAIN ANALYZE SELECT i FROM tst WHERE c = $c ORDER BY v <-> '$query' LIMIT $limit;
|
||||||
));
|
));
|
||||||
# TODO Use attribute index
|
# Use attribute index
|
||||||
like($explain, qr/Index Scan using idx/);
|
like($explain, qr/Bitmap Index Scan on attribute_idx/);
|
||||||
|
|
||||||
# Test partial index
|
# Test partial index
|
||||||
$node->safe_psql("postgres", "CREATE INDEX partial_idx ON tst USING hnsw (v vector_l2_ops) WHERE (c = $c);");
|
$node->safe_psql("postgres", "CREATE INDEX partial_idx ON tst USING hnsw (v vector_l2_ops) WHERE (c = $c);");
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
my $node = get_new_node('node');
|
my $node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ sub test_aggregate
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
# Does not raise overflow error in this instance due to loss of precision
|
# Does not raise overflow error in this instance due to loss of precision
|
||||||
is($res, "[24576,24576,49152]")
|
is($res, "[24576,24576,49152]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
my $dim = 1024;
|
my $dim = 1024;
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
my $node = get_new_node('node');
|
my $node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
my $node;
|
my $node;
|
||||||
@@ -51,7 +51,7 @@ sub test_recall
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
$node = get_new_node('node');
|
$node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
my $node;
|
my $node;
|
||||||
@@ -51,7 +51,7 @@ sub test_recall
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
$node = get_new_node('node');
|
$node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
my $node;
|
my $node;
|
||||||
@@ -51,7 +51,7 @@ sub test_recall
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
$node = get_new_node('node');
|
$node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
my $node = get_new_node('node');
|
my $node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
my $node;
|
my $node;
|
||||||
@@ -48,7 +48,7 @@ sub test_recall
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
$node = get_new_node('node');
|
$node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
my $node;
|
my $node;
|
||||||
@@ -48,7 +48,7 @@ sub test_recall
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
$node = get_new_node('node');
|
$node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
my $node;
|
my $node;
|
||||||
@@ -48,7 +48,7 @@ sub test_recall
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
$node = get_new_node('node');
|
$node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
my $node = get_new_node('node');
|
my $node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
my $node;
|
my $node;
|
||||||
@@ -47,7 +47,7 @@ sub test_recall
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
$node = get_new_node('node');
|
$node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
my $node;
|
my $node;
|
||||||
@@ -47,7 +47,7 @@ sub test_recall
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
$node = get_new_node('node');
|
$node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
my $node;
|
my $node;
|
||||||
@@ -48,7 +48,7 @@ sub test_recall
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
$node = get_new_node('node');
|
$node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
my $node = get_new_node('node');
|
my $node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
my $node;
|
my $node;
|
||||||
@@ -51,7 +51,7 @@ sub test_recall
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
$node = get_new_node('node');
|
$node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
my $node;
|
my $node;
|
||||||
my $array_sql = join(",", ('floor(random() * 2)::int - 1') x 3);
|
my $array_sql = join(",", ('floor(random() * 2)::int - 1') x 3);
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
$node = get_new_node('node');
|
$node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
my $node;
|
my $node;
|
||||||
@@ -10,7 +10,7 @@ my $dim = 5;
|
|||||||
my $array_sql = join(",", ('floor(random() * 4)::int - 2') x $dim);
|
my $array_sql = join(",", ('floor(random() * 4)::int - 2') x $dim);
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
$node = get_new_node('node');
|
$node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
my $node;
|
my $node;
|
||||||
@@ -51,7 +51,7 @@ sub test_recall
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
$node = get_new_node('node');
|
$node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
my $node = get_new_node('node');
|
my $node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
my $node = get_new_node('node');
|
my $node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings FATAL => 'all';
|
||||||
use PostgresNode;
|
use PostgreSQL::Test::Cluster;
|
||||||
use TestLib;
|
use PostgreSQL::Test::Utils;
|
||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
my $node = get_new_node('node');
|
my $node = PostgreSQL::Test::Cluster->new('node');
|
||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$node->start;
|
||||||
|
|
||||||
@@ -14,25 +14,29 @@ $node->safe_psql("postgres", "CREATE EXTENSION vector;");
|
|||||||
$node->safe_psql("postgres", "CREATE TABLE tst (i serial, v sparsevec(100000));");
|
$node->safe_psql("postgres", "CREATE TABLE tst (i serial, v sparsevec(100000));");
|
||||||
$node->safe_psql("postgres", "CREATE INDEX ON tst USING hnsw (v sparsevec_l2_ops);");
|
$node->safe_psql("postgres", "CREATE INDEX ON tst USING hnsw (v sparsevec_l2_ops);");
|
||||||
|
|
||||||
for (1 .. 3) {
|
for (1 .. 3)
|
||||||
for (1 .. 100) {
|
{
|
||||||
my @elements;
|
for (1 .. 100)
|
||||||
my %indices;
|
{
|
||||||
for (1 .. int(rand() * 100)) {
|
my @elements;
|
||||||
my $index = int(rand() * (100000 - 1)) + 1;
|
my %indices;
|
||||||
if (!exists($indices{$index})) {
|
for (1 .. int(rand() * 100))
|
||||||
my $value = rand();
|
{
|
||||||
push(@elements, "$index:$value");
|
my $index = int(rand() * (100000 - 1)) + 1;
|
||||||
$indices{$index} = 1;
|
if (!exists($indices{$index}))
|
||||||
}
|
{
|
||||||
}
|
my $value = rand();
|
||||||
my $embedding = "{" . join(",", @elements) . "}/100000";
|
push(@elements, "$index:$value");
|
||||||
$node->safe_psql("postgres", "INSERT INTO tst (v) VALUES ('$embedding');");
|
$indices{$index} = 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
my $embedding = "{" . join(",", @elements) . "}/100000";
|
||||||
|
$node->safe_psql("postgres", "INSERT INTO tst (v) VALUES ('$embedding');");
|
||||||
|
}
|
||||||
|
|
||||||
$node->safe_psql("postgres", "DELETE FROM tst WHERE i % 2 = 0;");
|
$node->safe_psql("postgres", "DELETE FROM tst WHERE i % 2 = 0;");
|
||||||
$node->safe_psql("postgres", "VACUUM tst;");
|
$node->safe_psql("postgres", "VACUUM tst;");
|
||||||
is(1, 1);
|
is(1, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
done_testing();
|
done_testing();
|
||||||
|
|||||||
46
test/t/039_hnsw_cost.pl
Normal file
46
test/t/039_hnsw_cost.pl
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
use strict;
|
||||||
|
use warnings FATAL => 'all';
|
||||||
|
use PostgreSQL::Test::Cluster;
|
||||||
|
use PostgreSQL::Test::Utils;
|
||||||
|
use Test::More;
|
||||||
|
|
||||||
|
my @dims = (384, 1536);
|
||||||
|
my $limit = 10;
|
||||||
|
|
||||||
|
# Initialize node
|
||||||
|
my $node = PostgreSQL::Test::Cluster->new('node');
|
||||||
|
$node->init;
|
||||||
|
$node->start;
|
||||||
|
|
||||||
|
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
|
||||||
|
|
||||||
|
for my $dim (@dims)
|
||||||
|
{
|
||||||
|
my $array_sql = join(",", ('random()') x $dim);
|
||||||
|
my $n = $dim == 384 ? 2000 : 1000;
|
||||||
|
|
||||||
|
# Create table and index
|
||||||
|
$node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector($dim));");
|
||||||
|
$node->safe_psql("postgres",
|
||||||
|
"INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(1, $n) i;"
|
||||||
|
);
|
||||||
|
$node->safe_psql("postgres", "CREATE INDEX idx ON tst USING hnsw (v vector_l2_ops);");
|
||||||
|
$node->safe_psql("postgres", "ANALYZE tst;");
|
||||||
|
|
||||||
|
# Generate query
|
||||||
|
my @r = ();
|
||||||
|
for (1 .. $dim)
|
||||||
|
{
|
||||||
|
push(@r, rand());
|
||||||
|
}
|
||||||
|
my $query = "[" . join(",", @r) . "]";
|
||||||
|
|
||||||
|
my $explain = $node->safe_psql("postgres", qq(
|
||||||
|
EXPLAIN ANALYZE SELECT i FROM tst ORDER BY v <-> '$query' LIMIT $limit;
|
||||||
|
));
|
||||||
|
like($explain, qr/Index Scan using idx/);
|
||||||
|
|
||||||
|
$node->safe_psql("postgres", "DROP TABLE tst;");
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
||||||
45
test/t/040_ivfflat_cost.pl
Normal file
45
test/t/040_ivfflat_cost.pl
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
use strict;
|
||||||
|
use warnings FATAL => 'all';
|
||||||
|
use PostgreSQL::Test::Cluster;
|
||||||
|
use PostgreSQL::Test::Utils;
|
||||||
|
use Test::More;
|
||||||
|
|
||||||
|
my @dims = (384, 1536);
|
||||||
|
my $limit = 10;
|
||||||
|
|
||||||
|
# Initialize node
|
||||||
|
my $node = PostgreSQL::Test::Cluster->new('node');
|
||||||
|
$node->init;
|
||||||
|
$node->start;
|
||||||
|
|
||||||
|
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
|
||||||
|
|
||||||
|
for my $dim (@dims)
|
||||||
|
{
|
||||||
|
my $array_sql = join(",", ('random()') x $dim);
|
||||||
|
|
||||||
|
# Create table and index
|
||||||
|
$node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector($dim));");
|
||||||
|
$node->safe_psql("postgres",
|
||||||
|
"INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(1, 5000) i;"
|
||||||
|
);
|
||||||
|
$node->safe_psql("postgres", "CREATE INDEX idx ON tst USING ivfflat (v vector_l2_ops) WITH (lists = 5);");
|
||||||
|
$node->safe_psql("postgres", "ANALYZE tst;");
|
||||||
|
|
||||||
|
# Generate query
|
||||||
|
my @r = ();
|
||||||
|
for (1 .. $dim)
|
||||||
|
{
|
||||||
|
push(@r, rand());
|
||||||
|
}
|
||||||
|
my $query = "[" . join(",", @r) . "]";
|
||||||
|
|
||||||
|
my $explain = $node->safe_psql("postgres", qq(
|
||||||
|
EXPLAIN ANALYZE SELECT i FROM tst ORDER BY v <-> '$query' LIMIT $limit;
|
||||||
|
));
|
||||||
|
like($explain, qr/Index Scan using idx/);
|
||||||
|
|
||||||
|
$node->safe_psql("postgres", "DROP TABLE tst;");
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
||||||
66
test/t/041_hnsw_streaming.pl
Normal file
66
test/t/041_hnsw_streaming.pl
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
use strict;
|
||||||
|
use warnings FATAL => 'all';
|
||||||
|
use PostgreSQL::Test::Cluster;
|
||||||
|
use PostgreSQL::Test::Utils;
|
||||||
|
use Test::More;
|
||||||
|
|
||||||
|
my $dim = 3;
|
||||||
|
my $array_sql = join(",", ('random()') x $dim);
|
||||||
|
|
||||||
|
# Initialize node
|
||||||
|
my $node = PostgreSQL::Test::Cluster->new('node');
|
||||||
|
$node->init;
|
||||||
|
$node->start;
|
||||||
|
|
||||||
|
# Create table
|
||||||
|
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
|
||||||
|
$node->safe_psql("postgres", "CREATE TABLE tst (i int4 PRIMARY KEY, v vector($dim));");
|
||||||
|
$node->safe_psql("postgres",
|
||||||
|
"INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(1, 100000) i;"
|
||||||
|
);
|
||||||
|
$node->safe_psql("postgres", qq(
|
||||||
|
SET maintenance_work_mem = '128MB';
|
||||||
|
SET max_parallel_maintenance_workers = 2;
|
||||||
|
CREATE INDEX ON tst USING hnsw (v vector_l2_ops)
|
||||||
|
));
|
||||||
|
|
||||||
|
my $count = $node->safe_psql("postgres", qq(
|
||||||
|
SET enable_seqscan = off;
|
||||||
|
SET hnsw.streaming = on;
|
||||||
|
SET work_mem = '8MB';
|
||||||
|
SELECT COUNT(*) FROM (SELECT v FROM tst WHERE i % 10000 = 0 ORDER BY v <-> (SELECT v FROM tst LIMIT 1) LIMIT 11) t;
|
||||||
|
));
|
||||||
|
is($count, 10);
|
||||||
|
|
||||||
|
foreach ((30000, 50000, 70000))
|
||||||
|
{
|
||||||
|
my $ef_stream = $_;
|
||||||
|
my $expected = $ef_stream / 10000;
|
||||||
|
my $sum = 0;
|
||||||
|
|
||||||
|
for my $i (1 .. 20)
|
||||||
|
{
|
||||||
|
$count = $node->safe_psql("postgres", qq(
|
||||||
|
SET enable_seqscan = off;
|
||||||
|
SET hnsw.streaming = on;
|
||||||
|
SET hnsw.ef_stream = $ef_stream;
|
||||||
|
SET work_mem = '8MB';
|
||||||
|
SELECT COUNT(*) FROM (SELECT v FROM tst WHERE i % 10000 = 0 ORDER BY v <-> (SELECT v FROM tst WHERE i = $i) LIMIT 11) t;
|
||||||
|
));
|
||||||
|
$sum += $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
my $avg = $sum / 20;
|
||||||
|
cmp_ok($avg, '>', $expected - 2);
|
||||||
|
cmp_ok($avg, '<', $expected + 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
my ($ret, $stdout, $stderr) = $node->psql("postgres", qq(
|
||||||
|
SET enable_seqscan = off;
|
||||||
|
SET hnsw.streaming = on;
|
||||||
|
SET work_mem = '2MB';
|
||||||
|
SELECT COUNT(*) FROM (SELECT v FROM tst WHERE i % 10000 = 0 ORDER BY v <-> (SELECT v FROM tst LIMIT 1) LIMIT 11) t;
|
||||||
|
));
|
||||||
|
like($stderr, qr/hnsw index scan exceeded work_mem after \d+ tuples/);
|
||||||
|
|
||||||
|
done_testing();
|
||||||
131
test/t/042_hnsw_streaming_recall.pl
Normal file
131
test/t/042_hnsw_streaming_recall.pl
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
use strict;
|
||||||
|
use warnings FATAL => 'all';
|
||||||
|
use PostgreSQL::Test::Cluster;
|
||||||
|
use PostgreSQL::Test::Utils;
|
||||||
|
use Test::More;
|
||||||
|
|
||||||
|
my $node;
|
||||||
|
my @queries = ();
|
||||||
|
my @expected;
|
||||||
|
my $limit = 20;
|
||||||
|
my $dim = 3;
|
||||||
|
my $array_sql = join(",", ('random()') x $dim);
|
||||||
|
my @cs = (100, 1000);
|
||||||
|
|
||||||
|
sub test_recall
|
||||||
|
{
|
||||||
|
my ($c, $ef_search, $min, $operator) = @_;
|
||||||
|
my $correct = 0;
|
||||||
|
my $total = 0;
|
||||||
|
|
||||||
|
my $explain = $node->safe_psql("postgres", qq(
|
||||||
|
SET enable_seqscan = off;
|
||||||
|
SET hnsw.ef_search = $ef_search;
|
||||||
|
SET hnsw.streaming = on;
|
||||||
|
EXPLAIN ANALYZE SELECT i FROM tst WHERE i % $c = 0 ORDER BY v $operator '$queries[0]' LIMIT $limit;
|
||||||
|
));
|
||||||
|
like($explain, qr/Index Scan using idx on tst/);
|
||||||
|
|
||||||
|
for my $i (0 .. $#queries)
|
||||||
|
{
|
||||||
|
my $actual = $node->safe_psql("postgres", qq(
|
||||||
|
SET enable_seqscan = off;
|
||||||
|
SET hnsw.ef_search = $ef_search;
|
||||||
|
SET hnsw.streaming = on;
|
||||||
|
SELECT i FROM tst WHERE i % $c = 0 ORDER BY v $operator '$queries[$i]' LIMIT $limit;
|
||||||
|
));
|
||||||
|
my @actual_ids = split("\n", $actual);
|
||||||
|
|
||||||
|
my @expected_ids = split("\n", $expected[$i]);
|
||||||
|
my %expected_set = map { $_ => 1 } @expected_ids;
|
||||||
|
|
||||||
|
foreach (@actual_ids)
|
||||||
|
{
|
||||||
|
if (exists($expected_set{$_}))
|
||||||
|
{
|
||||||
|
$correct++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$total += $limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
cmp_ok($correct / $total, ">=", $min, $operator);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Initialize node
|
||||||
|
$node = PostgreSQL::Test::Cluster->new('node');
|
||||||
|
$node->init;
|
||||||
|
$node->start;
|
||||||
|
|
||||||
|
# Create table
|
||||||
|
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
|
||||||
|
$node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector($dim));");
|
||||||
|
$node->safe_psql("postgres",
|
||||||
|
"INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(1, 100000) i;"
|
||||||
|
);
|
||||||
|
|
||||||
|
# Generate queries
|
||||||
|
for (1 .. 20)
|
||||||
|
{
|
||||||
|
my @r = ();
|
||||||
|
for (1 .. $dim)
|
||||||
|
{
|
||||||
|
push(@r, rand());
|
||||||
|
}
|
||||||
|
push(@queries, "[" . join(",", @r) . "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check each index type
|
||||||
|
my @operators = ("<->", "<=>");
|
||||||
|
my @opclasses = ("vector_l2_ops", "vector_cosine_ops");
|
||||||
|
|
||||||
|
for my $i (0 .. $#operators)
|
||||||
|
{
|
||||||
|
my $operator = $operators[$i];
|
||||||
|
my $opclass = $opclasses[$i];
|
||||||
|
|
||||||
|
$node->safe_psql("postgres", qq(
|
||||||
|
SET maintenance_work_mem = '128MB';
|
||||||
|
CREATE INDEX idx ON tst USING hnsw (v $opclass);
|
||||||
|
));
|
||||||
|
|
||||||
|
foreach (@cs)
|
||||||
|
{
|
||||||
|
my $c = $_;
|
||||||
|
|
||||||
|
# Get exact results
|
||||||
|
@expected = ();
|
||||||
|
foreach (@queries)
|
||||||
|
{
|
||||||
|
my $res = $node->safe_psql("postgres", qq(
|
||||||
|
SET enable_indexscan = off;
|
||||||
|
WITH top AS (
|
||||||
|
SELECT v $operator '$_' AS distance FROM tst WHERE i % $c = 0 ORDER BY distance LIMIT $limit
|
||||||
|
)
|
||||||
|
SELECT i FROM tst WHERE (v $operator '$_') <= (SELECT MAX(distance) FROM top)
|
||||||
|
));
|
||||||
|
push(@expected, $res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($c == 100)
|
||||||
|
{
|
||||||
|
test_recall($c, 40, 0.99, $operator);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if ($operator eq "<->")
|
||||||
|
{
|
||||||
|
test_recall($c, 40, 0.99, $operator);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
test_recall($c, 40, 0.99, $operator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$node->safe_psql("postgres", "DROP INDEX idx;");
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
comment = 'vector data type and ivfflat and hnsw access methods'
|
comment = 'vector data type and ivfflat and hnsw access methods'
|
||||||
default_version = '0.7.3'
|
default_version = '0.7.4'
|
||||||
module_pathname = '$libdir/vector'
|
module_pathname = '$libdir/vector'
|
||||||
relocatable = true
|
relocatable = true
|
||||||
|
|||||||
Reference in New Issue
Block a user