mirror of
https://github.com/pgvector/pgvector.git
synced 2026-07-23 04:20:56 +08:00
Compare commits
115 Commits
minivec
...
guc-explai
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc0d3e7fdb | ||
|
|
c530a3c490 | ||
|
|
d8b9e8ef73 | ||
|
|
00894efed5 | ||
|
|
0aa0f6619b | ||
|
|
38d053001e | ||
|
|
ccb95407e7 | ||
|
|
04d5e934a1 | ||
|
|
b163b5b196 | ||
|
|
6a30c1e824 | ||
|
|
2db1b19644 | ||
|
|
305d62146e | ||
|
|
f9d627c9a9 | ||
|
|
38f42820be | ||
|
|
15c8245b42 | ||
|
|
572a9ab404 | ||
|
|
00492d7e57 | ||
|
|
857d716d9e | ||
|
|
c5dd2af750 | ||
|
|
78b877bdaf | ||
|
|
7043cce893 | ||
|
|
62039d74f6 | ||
|
|
ac6576e53a | ||
|
|
67eff41c44 | ||
|
|
1291b12090 | ||
|
|
24522700b8 | ||
|
|
bfb3a45b31 | ||
|
|
e718eb8da4 | ||
|
|
049972a4a3 | ||
|
|
61027645e9 | ||
|
|
a41b327b33 | ||
|
|
7f735ebd9b | ||
|
|
02b01e1ca9 | ||
|
|
388e42f6e6 | ||
|
|
bf379eed86 | ||
|
|
e1bc929429 | ||
|
|
38285aacc7 | ||
|
|
a2408e60fa | ||
|
|
53a8734bac | ||
|
|
7484625227 | ||
|
|
d1ebb8db73 | ||
|
|
42af8aa1d1 | ||
|
|
9d15a76b60 | ||
|
|
a3a20f9816 | ||
|
|
b26a21b848 | ||
|
|
2dc392ed6c | ||
|
|
960d2848cb | ||
|
|
8e88b481a6 | ||
|
|
124018b8dd | ||
|
|
35b252a3e3 | ||
|
|
2832e746f0 | ||
|
|
961cb17d80 | ||
|
|
c91ed7b2c3 | ||
|
|
48fe70c219 | ||
|
|
29908405ab | ||
|
|
08d0340655 | ||
|
|
7d2eb49c2a | ||
|
|
772ab69de6 | ||
|
|
e13e9a9614 | ||
|
|
e2fab306ac | ||
|
|
edc2126a4a | ||
|
|
2cbd08b6c0 | ||
|
|
fa6782985a | ||
|
|
32ab27d72a | ||
|
|
064db12de7 | ||
|
|
45a6eef9e0 | ||
|
|
17266ed409 | ||
|
|
a98534e5ab | ||
|
|
57c05c59a2 | ||
|
|
3126fbdb6f | ||
|
|
f4b67b078f | ||
|
|
77688b4309 | ||
|
|
d5f4a0e435 | ||
|
|
57248ba128 | ||
|
|
ff6da4fcea | ||
|
|
a8b4b6675a | ||
|
|
d148b4e61b | ||
|
|
658d74e2f6 | ||
|
|
7ba593c492 | ||
|
|
525e3b81e1 | ||
|
|
8eb8cdf0f3 | ||
|
|
4c72f91206 | ||
|
|
4ac86f62a1 | ||
|
|
648dd8af78 | ||
|
|
ee43ee9b16 | ||
|
|
5ce367e18b | ||
|
|
f371eb119b | ||
|
|
382a25aefb | ||
|
|
0b6214aad6 | ||
|
|
f2afd11257 | ||
|
|
cae3458329 | ||
|
|
dc23752618 | ||
|
|
158d9340bc | ||
|
|
5ee0471ead | ||
|
|
54f8d9733d | ||
|
|
cf419f448b | ||
|
|
8a2eebd6a4 | ||
|
|
daf9c5c743 | ||
|
|
2bca4e406b | ||
|
|
74020a90da | ||
|
|
44d8d28b40 | ||
|
|
54fa16e3e3 | ||
|
|
46de265a24 | ||
|
|
b8c27914d4 | ||
|
|
2d85af51a8 | ||
|
|
e0ad441306 | ||
|
|
5776a4d937 | ||
|
|
242a12b7d5 | ||
|
|
1370dd6e86 | ||
|
|
a100dc67e5 | ||
|
|
2df9f24aad | ||
|
|
8e979ed377 | ||
|
|
77b3d1f2a8 | ||
|
|
ecd0738728 | ||
|
|
62ffc3641c |
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -8,8 +8,8 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# - postgres: 18
|
||||
# os: ubuntu-24.04
|
||||
- postgres: 18
|
||||
os: ubuntu-24.04
|
||||
- postgres: 17
|
||||
os: ubuntu-24.04
|
||||
- postgres: 16
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
- postgres: 16
|
||||
os: macos-14
|
||||
- postgres: 14
|
||||
os: macos-12
|
||||
os: macos-13
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ankane/setup-postgres@v1
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
## 0.8.0 (unreleased)
|
||||
|
||||
- Added support for iterative index scans
|
||||
- Added casts for arrays to `sparsevec`
|
||||
- Improved cost estimation
|
||||
- Improved performance of HNSW inserts and on-disk index builds
|
||||
- Reduced memory usage for HNSW index scans
|
||||
- Dropped support for Postgres 12
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
ARG PG_MAJOR=16
|
||||
ARG PG_MAJOR=17
|
||||
FROM postgres:$PG_MAJOR
|
||||
ARG PG_MAJOR
|
||||
|
||||
|
||||
6
Makefile
6
Makefile
@@ -4,8 +4,8 @@ EXTVERSION = 0.7.4
|
||||
MODULE_big = vector
|
||||
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/minivec.o src/ivfvacuum.o src/sparsevec.o src/vector.o
|
||||
HEADERS = src/halfvec.h src/minivec.h src/sparsevec.h src/vector.h
|
||||
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
|
||||
|
||||
TESTS = $(wildcard test/sql/*.sql)
|
||||
REGRESS = $(patsubst test/sql/%.sql,%,$(TESTS))
|
||||
@@ -66,7 +66,7 @@ dist:
|
||||
git archive --format zip --prefix=$(EXTENSION)-$(EXTVERSION)/ --output dist/$(EXTENSION)-$(EXTVERSION).zip master
|
||||
|
||||
# for Docker
|
||||
PG_MAJOR ?= 16
|
||||
PG_MAJOR ?= 17
|
||||
|
||||
.PHONY: docker
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@ EXTENSION = vector
|
||||
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\minivec.obj src\sparsevec.obj src\vector.obj
|
||||
HEADERS = src\halfvec.h src\minivec.h src\sparsevec.h src\vector.h
|
||||
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
|
||||
|
||||
REGRESS = bit btree cast copy halfvec hnsw_bit hnsw_halfvec hnsw_sparsevec hnsw_vector ivfflat_bit ivfflat_halfvec ivfflat_vector minivec sparsevec vector_type
|
||||
REGRESS = bit btree cast copy halfvec hnsw_bit hnsw_halfvec hnsw_sparsevec hnsw_vector ivfflat_bit ivfflat_halfvec ivfflat_vector sparsevec vector_type
|
||||
REGRESS_OPTS = --inputdir=test --load-extension=$(EXTENSION)
|
||||
|
||||
# For /arch flags
|
||||
|
||||
152
README.md
152
README.md
@@ -52,6 +52,8 @@ nmake /F Makefile.win
|
||||
nmake /F Makefile.win install
|
||||
```
|
||||
|
||||
Note: Postgres 17 is not supported yet due to an upstream issue
|
||||
|
||||
See the [installation notes](#installation-notes---windows) if you run into issues
|
||||
|
||||
You can also install it with [Docker](#docker) or [conda-forge](#conda-forge).
|
||||
@@ -100,6 +102,8 @@ Or add a vector column to an existing table
|
||||
ALTER TABLE items ADD COLUMN embedding vector(3);
|
||||
```
|
||||
|
||||
Also supports [half-precision](#half-precision-vectors), [binary](#binary-vectors), and [sparse](#sparse-vectors) vectors
|
||||
|
||||
Insert vectors
|
||||
|
||||
```sql
|
||||
@@ -145,6 +149,8 @@ Supported distance functions are:
|
||||
- `<#>` - (negative) inner product
|
||||
- `<=>` - cosine distance
|
||||
- `<+>` - L1 distance (added in 0.7.0)
|
||||
- `<~>` - Hamming distance (binary vectors, added in 0.7.0)
|
||||
- `<%>` - Jaccard distance (binary vectors, added in 0.7.0)
|
||||
|
||||
Get the nearest neighbors to a row
|
||||
|
||||
@@ -445,6 +451,89 @@ 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);
|
||||
```
|
||||
|
||||
## Iterative Index Scans
|
||||
|
||||
*Unreleased*
|
||||
|
||||
With approximate indexes, queries with filtering can return less results (due to post-filtering). Starting with 0.8.0, you can enable iterative index scans. If too few results from the initial scan match the filters, the scan will resume until enough results are found (or it reaches `hnsw.max_scan_tuples` or `ivfflat.max_probes`). This can significantly improve recall.
|
||||
|
||||
There are two modes for iterative scans: strict and relaxed.
|
||||
|
||||
Strict ensures results are in the exact order by distance
|
||||
|
||||
```sql
|
||||
SET hnsw.iterative_scan = strict_order;
|
||||
```
|
||||
|
||||
Relaxed allows results to be slightly out of order by distance, but provides better recall
|
||||
|
||||
```sql
|
||||
SET hnsw.iterative_scan = relaxed_order;
|
||||
# or
|
||||
SET ivfflat.iterative_scan = relaxed_order;
|
||||
```
|
||||
|
||||
With relaxed ordering, you can use a [materialized CTE](https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-CTE-MATERIALIZATION) to get strict ordering
|
||||
|
||||
```sql
|
||||
WITH relaxed_results AS MATERIALIZED (
|
||||
SELECT id, embedding <-> '[1,2,3]' AS distance FROM items WHERE category_id = 123 ORDER BY distance LIMIT 5
|
||||
) SELECT * FROM relaxed_results ORDER BY distance;
|
||||
```
|
||||
|
||||
For queries that filter by distance, use a materialized CTE and place the distance filter outside of it for best performance (due to the [current behavior](https://www.postgresql.org/message-id/flat/CAOdR5yGUoMQ6j7M5hNUXrySzaqZVGf_Ne%2B8fwZMRKTFxU1nbJg%40mail.gmail.com) of the Postgres executor)
|
||||
|
||||
```sql
|
||||
WITH nearest_results AS MATERIALIZED (
|
||||
SELECT id, embedding <-> '[1,2,3]' AS distance FROM items ORDER BY distance LIMIT 5
|
||||
) SELECT * FROM nearest_results WHERE distance < 5 ORDER BY distance;
|
||||
```
|
||||
|
||||
Note: Place any other filters inside the CTE
|
||||
|
||||
### Iterative Scan Options
|
||||
|
||||
Since scanning a large portion of an approximate index is expensive, there are options to control when a scan ends
|
||||
|
||||
#### HNSW
|
||||
|
||||
Specify the max number of tuples to visit (20,000 by default)
|
||||
|
||||
```sql
|
||||
SET hnsw.max_scan_tuples = 20000;
|
||||
```
|
||||
|
||||
Note: This is approximate and does not affect the initial scan
|
||||
|
||||
Specify the max amount of memory to use, as a multiple of `work_mem` (1 by default)
|
||||
|
||||
```sql
|
||||
SET hnsw.scan_mem_multiplier = 2;
|
||||
```
|
||||
|
||||
You can see when increasing this is needed by enabling debug messages
|
||||
|
||||
```sql
|
||||
SET client_min_messages = debug1;
|
||||
```
|
||||
|
||||
which will show when a scan reaches the memory limit
|
||||
|
||||
```text
|
||||
DEBUG: hnsw index scan reached memory limit after 20000 tuples
|
||||
HINT: Increase hnsw.scan_mem_multiplier to scan more tuples.
|
||||
```
|
||||
|
||||
#### IVFFlat
|
||||
|
||||
Specify the max number of probes
|
||||
|
||||
```sql
|
||||
SET ivfflat.max_probes = 100;
|
||||
```
|
||||
|
||||
Note: If this is lower than `ivfflat.probes`, `ivfflat.probes` will be used
|
||||
|
||||
## Half-Precision Vectors
|
||||
|
||||
*Added in 0.7.0*
|
||||
@@ -934,37 +1023,6 @@ Function | Description | Added
|
||||
avg(halfvec) → halfvec | average | 0.7.0
|
||||
sum(halfvec) → halfvec | sum | 0.7.0
|
||||
|
||||
### Minivec Type
|
||||
|
||||
Each mini vector takes `dimensions + 8` bytes of storage. Each element is a E4M3 8-bit floating-point number, and all elements must be finite (no `NaN`). Mini vectors can have up to 16,000 dimensions.
|
||||
|
||||
### Minivec Operators
|
||||
|
||||
Operator | Description | Added
|
||||
--- | --- | ---
|
||||
\+ | element-wise addition | 0.8.0
|
||||
\- | element-wise subtraction | 0.8.0
|
||||
\* | element-wise multiplication | 0.8.0
|
||||
\|\| | concatenate | 0.8.0
|
||||
<-> | Euclidean distance | 0.8.0
|
||||
<#> | negative inner product | 0.8.0
|
||||
<=> | cosine distance | 0.8.0
|
||||
<+> | taxicab distance | 0.8.0
|
||||
|
||||
### Minivec Functions
|
||||
|
||||
Function | Description | Added
|
||||
--- | --- | ---
|
||||
binary_quantize(minivec) → bit | binary quantize | 0.8.0
|
||||
cosine_distance(minivec, minivec) → double precision | cosine distance | 0.8.0
|
||||
inner_product(minivec, minivec) → double precision | inner product | 0.8.0
|
||||
l1_distance(minivec, minivec) → double precision | taxicab distance | 0.8.0
|
||||
l2_distance(minivec, minivec) → double precision | Euclidean distance | 0.8.0
|
||||
l2_norm(minivec) → double precision | Euclidean norm | 0.8.0
|
||||
l2_normalize(minivec) → minivec | Normalize with Euclidean norm | 0.8.0
|
||||
subvector(minivec, integer, integer) → minivec | subvector | 0.8.0
|
||||
vector_dims(minivec) → integer | number of dimensions | 0.8.0
|
||||
|
||||
### Bit Type
|
||||
|
||||
Each bit vector takes `dimensions / 8 + 8` bytes of storage. See the [Postgres docs](https://www.postgresql.org/docs/current/datatype-bit.html) for more info.
|
||||
@@ -1014,7 +1072,7 @@ l2_normalize(sparsevec) → sparsevec | Normalize with Euclidean norm | 0.7.0
|
||||
If your machine has multiple Postgres installations, specify the path to [pg_config](https://www.postgresql.org/docs/current/app-pgconfig.html) with:
|
||||
|
||||
```sh
|
||||
export PG_CONFIG=/Library/PostgreSQL/16/bin/pg_config
|
||||
export PG_CONFIG=/Library/PostgreSQL/17/bin/pg_config
|
||||
```
|
||||
|
||||
Then re-run the installation instructions (run `make clean` before `make` if needed). If `sudo` is needed for `make install`, use:
|
||||
@@ -1025,11 +1083,11 @@ sudo --preserve-env=PG_CONFIG make install
|
||||
|
||||
A few common paths on Mac are:
|
||||
|
||||
- EDB installer - `/Library/PostgreSQL/16/bin/pg_config`
|
||||
- Homebrew (arm64) - `/opt/homebrew/opt/postgresql@16/bin/pg_config`
|
||||
- Homebrew (x86-64) - `/usr/local/opt/postgresql@16/bin/pg_config`
|
||||
- EDB installer - `/Library/PostgreSQL/17/bin/pg_config`
|
||||
- Homebrew (arm64) - `/opt/homebrew/opt/postgresql@17/bin/pg_config`
|
||||
- Homebrew (x86-64) - `/usr/local/opt/postgresql@17/bin/pg_config`
|
||||
|
||||
Note: Replace `16` with your Postgres server version
|
||||
Note: Replace `17` with your Postgres server version
|
||||
|
||||
### Missing Header
|
||||
|
||||
@@ -1038,10 +1096,10 @@ If compilation fails with `fatal error: postgres.h: No such file or directory`,
|
||||
For Ubuntu and Debian, use:
|
||||
|
||||
```sh
|
||||
sudo apt install postgresql-server-dev-16
|
||||
sudo apt install postgresql-server-dev-17
|
||||
```
|
||||
|
||||
Note: Replace `16` with your Postgres server version
|
||||
Note: Replace `17` with your Postgres server version
|
||||
|
||||
### Missing SDK
|
||||
|
||||
@@ -1074,17 +1132,17 @@ If installation fails with `Access is denied`, re-run the installation instructi
|
||||
Get the [Docker image](https://hub.docker.com/r/pgvector/pgvector) with:
|
||||
|
||||
```sh
|
||||
docker pull pgvector/pgvector:pg16
|
||||
docker pull pgvector/pgvector:pg17
|
||||
```
|
||||
|
||||
This adds pgvector to the [Postgres image](https://hub.docker.com/_/postgres) (replace `16` with your Postgres server version, and run it the same way).
|
||||
This adds pgvector to the [Postgres image](https://hub.docker.com/_/postgres) (replace `17` with your Postgres server version, and run it the same way).
|
||||
|
||||
You can also build the image manually:
|
||||
|
||||
```sh
|
||||
git clone --branch v0.7.4 https://github.com/pgvector/pgvector.git
|
||||
cd pgvector
|
||||
docker build --pull --build-arg PG_MAJOR=16 -t myuser/pgvector .
|
||||
docker build --pull --build-arg PG_MAJOR=17 -t myuser/pgvector .
|
||||
```
|
||||
|
||||
### Homebrew
|
||||
@@ -1095,7 +1153,7 @@ With Homebrew Postgres, you can use:
|
||||
brew install pgvector
|
||||
```
|
||||
|
||||
Note: This only adds it to the `postgresql@14` formula
|
||||
Note: This only adds it to the `postgresql@17` and `postgresql@14` formulas
|
||||
|
||||
### PGXN
|
||||
|
||||
@@ -1110,22 +1168,22 @@ pgxn install vector
|
||||
Debian and Ubuntu packages are available from the [PostgreSQL APT Repository](https://wiki.postgresql.org/wiki/Apt). Follow the [setup instructions](https://wiki.postgresql.org/wiki/Apt#Quickstart) and run:
|
||||
|
||||
```sh
|
||||
sudo apt install postgresql-16-pgvector
|
||||
sudo apt install postgresql-17-pgvector
|
||||
```
|
||||
|
||||
Note: Replace `16` with your Postgres server version
|
||||
Note: Replace `17` with your Postgres server version
|
||||
|
||||
### Yum
|
||||
|
||||
RPM packages are available from the [PostgreSQL Yum Repository](https://yum.postgresql.org/). Follow the [setup instructions](https://www.postgresql.org/download/linux/redhat/) for your distribution and run:
|
||||
|
||||
```sh
|
||||
sudo yum install pgvector_16
|
||||
sudo yum install pgvector_17
|
||||
# or
|
||||
sudo dnf install pgvector_16
|
||||
sudo dnf install pgvector_17
|
||||
```
|
||||
|
||||
Note: Replace `16` with your Postgres server version
|
||||
Note: Replace `17` with your Postgres server version
|
||||
|
||||
### pkg
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
-- 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
|
||||
|
||||
-- TODO minivec functions
|
||||
|
||||
CREATE FUNCTION array_to_sparsevec(integer[], integer, boolean) RETURNS sparsevec
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
|
||||
295
sql/vector.sql
295
sql/vector.sql
@@ -266,18 +266,12 @@ COMMENT ON ACCESS METHOD hnsw IS 'hnsw index access method';
|
||||
CREATE FUNCTION ivfflat_halfvec_support(internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C;
|
||||
|
||||
CREATE FUNCTION ivfflat_minivec_support(internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C;
|
||||
|
||||
CREATE FUNCTION ivfflat_bit_support(internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C;
|
||||
|
||||
CREATE FUNCTION hnsw_halfvec_support(internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C;
|
||||
|
||||
CREATE FUNCTION hnsw_minivec_support(internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C;
|
||||
|
||||
CREATE FUNCTION hnsw_bit_support(internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C;
|
||||
|
||||
@@ -653,295 +647,6 @@ CREATE OPERATOR CLASS halfvec_l1_ops
|
||||
FUNCTION 1 l1_distance(halfvec, halfvec),
|
||||
FUNCTION 3 hnsw_halfvec_support(internal);
|
||||
|
||||
-- minivec type
|
||||
|
||||
CREATE TYPE minivec;
|
||||
|
||||
CREATE FUNCTION minivec_in(cstring, oid, integer) RETURNS minivec
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION minivec_out(minivec) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION minivec_typmod_in(cstring[]) RETURNS integer
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION minivec_recv(internal, oid, integer) RETURNS minivec
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION minivec_send(minivec) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE minivec (
|
||||
INPUT = minivec_in,
|
||||
OUTPUT = minivec_out,
|
||||
TYPMOD_IN = minivec_typmod_in,
|
||||
RECEIVE = minivec_recv,
|
||||
SEND = minivec_send,
|
||||
STORAGE = external
|
||||
);
|
||||
|
||||
-- minivec functions
|
||||
|
||||
CREATE FUNCTION l2_distance(minivec, minivec) RETURNS float8
|
||||
AS 'MODULE_PATHNAME', 'minivec_l2_distance' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION inner_product(minivec, minivec) RETURNS float8
|
||||
AS 'MODULE_PATHNAME', 'minivec_inner_product' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION cosine_distance(minivec, minivec) RETURNS float8
|
||||
AS 'MODULE_PATHNAME', 'minivec_cosine_distance' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION l1_distance(minivec, minivec) RETURNS float8
|
||||
AS 'MODULE_PATHNAME', 'minivec_l1_distance' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION vector_dims(minivec) RETURNS integer
|
||||
AS 'MODULE_PATHNAME', 'minivec_vector_dims' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION l2_norm(minivec) RETURNS float8
|
||||
AS 'MODULE_PATHNAME', 'minivec_l2_norm' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION l2_normalize(minivec) RETURNS minivec
|
||||
AS 'MODULE_PATHNAME', 'minivec_l2_normalize' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION binary_quantize(minivec) RETURNS bit
|
||||
AS 'MODULE_PATHNAME', 'minivec_binary_quantize' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION subvector(minivec, int, int) RETURNS minivec
|
||||
AS 'MODULE_PATHNAME', 'minivec_subvector' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
-- minivec private functions
|
||||
|
||||
CREATE FUNCTION minivec_add(minivec, minivec) RETURNS minivec
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION minivec_sub(minivec, minivec) RETURNS minivec
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION minivec_mul(minivec, minivec) RETURNS minivec
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION minivec_concat(minivec, minivec) RETURNS minivec
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION minivec_lt(minivec, minivec) RETURNS bool
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION minivec_le(minivec, minivec) RETURNS bool
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION minivec_eq(minivec, minivec) RETURNS bool
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION minivec_ne(minivec, minivec) RETURNS bool
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION minivec_ge(minivec, minivec) RETURNS bool
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION minivec_gt(minivec, minivec) RETURNS bool
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION minivec_cmp(minivec, minivec) RETURNS int4
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION minivec_l2_squared_distance(minivec, minivec) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION minivec_negative_inner_product(minivec, minivec) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION minivec_spherical_distance(minivec, minivec) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
-- minivec cast functions
|
||||
|
||||
CREATE FUNCTION minivec(minivec, integer, boolean) RETURNS minivec
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION minivec_to_vector(minivec, integer, boolean) RETURNS vector
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION vector_to_minivec(vector, integer, boolean) RETURNS minivec
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION array_to_minivec(integer[], integer, boolean) RETURNS minivec
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION array_to_minivec(real[], integer, boolean) RETURNS minivec
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION array_to_minivec(double precision[], integer, boolean) RETURNS minivec
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION array_to_minivec(numeric[], integer, boolean) RETURNS minivec
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION minivec_to_float4(minivec, integer, boolean) RETURNS real[]
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
-- minivec casts
|
||||
|
||||
CREATE CAST (minivec AS minivec)
|
||||
WITH FUNCTION minivec(minivec, integer, boolean) AS IMPLICIT;
|
||||
|
||||
CREATE CAST (minivec AS vector)
|
||||
WITH FUNCTION minivec_to_vector(minivec, integer, boolean) AS ASSIGNMENT;
|
||||
|
||||
CREATE CAST (vector AS minivec)
|
||||
WITH FUNCTION vector_to_minivec(vector, integer, boolean) AS IMPLICIT;
|
||||
|
||||
CREATE CAST (minivec AS real[])
|
||||
WITH FUNCTION minivec_to_float4(minivec, integer, boolean) AS ASSIGNMENT;
|
||||
|
||||
CREATE CAST (integer[] AS minivec)
|
||||
WITH FUNCTION array_to_minivec(integer[], integer, boolean) AS ASSIGNMENT;
|
||||
|
||||
CREATE CAST (real[] AS minivec)
|
||||
WITH FUNCTION array_to_minivec(real[], integer, boolean) AS ASSIGNMENT;
|
||||
|
||||
CREATE CAST (double precision[] AS minivec)
|
||||
WITH FUNCTION array_to_minivec(double precision[], integer, boolean) AS ASSIGNMENT;
|
||||
|
||||
CREATE CAST (numeric[] AS minivec)
|
||||
WITH FUNCTION array_to_minivec(numeric[], integer, boolean) AS ASSIGNMENT;
|
||||
|
||||
-- minivec operators
|
||||
|
||||
CREATE OPERATOR <-> (
|
||||
LEFTARG = minivec, RIGHTARG = minivec, PROCEDURE = l2_distance,
|
||||
COMMUTATOR = '<->'
|
||||
);
|
||||
|
||||
CREATE OPERATOR <#> (
|
||||
LEFTARG = minivec, RIGHTARG = minivec, PROCEDURE = minivec_negative_inner_product,
|
||||
COMMUTATOR = '<#>'
|
||||
);
|
||||
|
||||
CREATE OPERATOR <=> (
|
||||
LEFTARG = minivec, RIGHTARG = minivec, PROCEDURE = cosine_distance,
|
||||
COMMUTATOR = '<=>'
|
||||
);
|
||||
|
||||
CREATE OPERATOR <+> (
|
||||
LEFTARG = minivec, RIGHTARG = minivec, PROCEDURE = l1_distance,
|
||||
COMMUTATOR = '<+>'
|
||||
);
|
||||
|
||||
CREATE OPERATOR + (
|
||||
LEFTARG = minivec, RIGHTARG = minivec, PROCEDURE = minivec_add,
|
||||
COMMUTATOR = +
|
||||
);
|
||||
|
||||
CREATE OPERATOR - (
|
||||
LEFTARG = minivec, RIGHTARG = minivec, PROCEDURE = minivec_sub
|
||||
);
|
||||
|
||||
CREATE OPERATOR * (
|
||||
LEFTARG = minivec, RIGHTARG = minivec, PROCEDURE = minivec_mul,
|
||||
COMMUTATOR = *
|
||||
);
|
||||
|
||||
CREATE OPERATOR || (
|
||||
LEFTARG = minivec, RIGHTARG = minivec, PROCEDURE = minivec_concat
|
||||
);
|
||||
|
||||
CREATE OPERATOR < (
|
||||
LEFTARG = minivec, RIGHTARG = minivec, PROCEDURE = minivec_lt,
|
||||
COMMUTATOR = > , NEGATOR = >= ,
|
||||
RESTRICT = scalarltsel, JOIN = scalarltjoinsel
|
||||
);
|
||||
|
||||
CREATE OPERATOR <= (
|
||||
LEFTARG = minivec, RIGHTARG = minivec, PROCEDURE = minivec_le,
|
||||
COMMUTATOR = >= , NEGATOR = > ,
|
||||
RESTRICT = scalarlesel, JOIN = scalarlejoinsel
|
||||
);
|
||||
|
||||
CREATE OPERATOR = (
|
||||
LEFTARG = minivec, RIGHTARG = minivec, PROCEDURE = minivec_eq,
|
||||
COMMUTATOR = = , NEGATOR = <> ,
|
||||
RESTRICT = eqsel, JOIN = eqjoinsel
|
||||
);
|
||||
|
||||
CREATE OPERATOR <> (
|
||||
LEFTARG = minivec, RIGHTARG = minivec, PROCEDURE = minivec_ne,
|
||||
COMMUTATOR = <> , NEGATOR = = ,
|
||||
RESTRICT = eqsel, JOIN = eqjoinsel
|
||||
);
|
||||
|
||||
CREATE OPERATOR >= (
|
||||
LEFTARG = minivec, RIGHTARG = minivec, PROCEDURE = minivec_ge,
|
||||
COMMUTATOR = <= , NEGATOR = < ,
|
||||
RESTRICT = scalargesel, JOIN = scalargejoinsel
|
||||
);
|
||||
|
||||
CREATE OPERATOR > (
|
||||
LEFTARG = minivec, RIGHTARG = minivec, PROCEDURE = minivec_gt,
|
||||
COMMUTATOR = < , NEGATOR = <= ,
|
||||
RESTRICT = scalargtsel, JOIN = scalargtjoinsel
|
||||
);
|
||||
|
||||
-- minivec op classes
|
||||
|
||||
CREATE OPERATOR CLASS minivec_ops
|
||||
DEFAULT FOR TYPE minivec USING btree AS
|
||||
OPERATOR 1 < ,
|
||||
OPERATOR 2 <= ,
|
||||
OPERATOR 3 = ,
|
||||
OPERATOR 4 >= ,
|
||||
OPERATOR 5 > ,
|
||||
FUNCTION 1 minivec_cmp(minivec, minivec);
|
||||
|
||||
CREATE OPERATOR CLASS minivec_l2_ops
|
||||
FOR TYPE minivec USING ivfflat AS
|
||||
OPERATOR 1 <-> (minivec, minivec) FOR ORDER BY float_ops,
|
||||
FUNCTION 1 minivec_l2_squared_distance(minivec, minivec),
|
||||
FUNCTION 3 l2_distance(minivec, minivec),
|
||||
FUNCTION 5 ivfflat_minivec_support(internal);
|
||||
|
||||
CREATE OPERATOR CLASS minivec_ip_ops
|
||||
FOR TYPE minivec USING ivfflat AS
|
||||
OPERATOR 1 <#> (minivec, minivec) FOR ORDER BY float_ops,
|
||||
FUNCTION 1 minivec_negative_inner_product(minivec, minivec),
|
||||
FUNCTION 3 minivec_spherical_distance(minivec, minivec),
|
||||
FUNCTION 4 l2_norm(minivec),
|
||||
FUNCTION 5 ivfflat_minivec_support(internal);
|
||||
|
||||
CREATE OPERATOR CLASS minivec_cosine_ops
|
||||
FOR TYPE minivec USING ivfflat AS
|
||||
OPERATOR 1 <=> (minivec, minivec) FOR ORDER BY float_ops,
|
||||
FUNCTION 1 cosine_distance(minivec, minivec),
|
||||
FUNCTION 2 l2_norm(minivec),
|
||||
FUNCTION 3 minivec_spherical_distance(minivec, minivec),
|
||||
FUNCTION 4 l2_norm(minivec),
|
||||
FUNCTION 5 ivfflat_minivec_support(internal);
|
||||
|
||||
CREATE OPERATOR CLASS minivec_l2_ops
|
||||
FOR TYPE minivec USING hnsw AS
|
||||
OPERATOR 1 <-> (minivec, minivec) FOR ORDER BY float_ops,
|
||||
FUNCTION 1 minivec_l2_squared_distance(minivec, minivec),
|
||||
FUNCTION 3 hnsw_minivec_support(internal);
|
||||
|
||||
CREATE OPERATOR CLASS minivec_ip_ops
|
||||
FOR TYPE minivec USING hnsw AS
|
||||
OPERATOR 1 <#> (minivec, minivec) FOR ORDER BY float_ops,
|
||||
FUNCTION 1 minivec_negative_inner_product(minivec, minivec),
|
||||
FUNCTION 3 hnsw_minivec_support(internal);
|
||||
|
||||
CREATE OPERATOR CLASS minivec_cosine_ops
|
||||
FOR TYPE minivec USING hnsw AS
|
||||
OPERATOR 1 <=> (minivec, minivec) FOR ORDER BY float_ops,
|
||||
FUNCTION 1 cosine_distance(minivec, minivec),
|
||||
FUNCTION 2 l2_norm(minivec),
|
||||
FUNCTION 3 hnsw_minivec_support(internal);
|
||||
|
||||
CREATE OPERATOR CLASS minivec_l1_ops
|
||||
FOR TYPE minivec USING hnsw AS
|
||||
OPERATOR 1 <+> (minivec, minivec) FOR ORDER BY float_ops,
|
||||
FUNCTION 1 l1_distance(minivec, minivec),
|
||||
FUNCTION 3 hnsw_minivec_support(internal);
|
||||
|
||||
-- bit functions
|
||||
|
||||
CREATE FUNCTION hamming_distance(bit, bit) RETURNS float8
|
||||
|
||||
101
src/hnsw.c
101
src/hnsw.c
@@ -12,12 +12,23 @@
|
||||
#include "utils/float.h"
|
||||
#include "utils/guc.h"
|
||||
#include "utils/selfuncs.h"
|
||||
#include "utils/spccache.h"
|
||||
|
||||
#if PG_VERSION_NUM < 150000
|
||||
#define MarkGUCPrefixReserved(x) EmitWarningsOnPlaceholders(x)
|
||||
#endif
|
||||
|
||||
static const struct config_enum_entry hnsw_iterative_scan_options[] = {
|
||||
{"off", HNSW_ITERATIVE_SCAN_OFF, false},
|
||||
{"relaxed_order", HNSW_ITERATIVE_SCAN_RELAXED, false},
|
||||
{"strict_order", HNSW_ITERATIVE_SCAN_STRICT, false},
|
||||
{NULL, 0, false}
|
||||
};
|
||||
|
||||
int hnsw_ef_search;
|
||||
int hnsw_iterative_scan;
|
||||
int hnsw_max_scan_tuples;
|
||||
double hnsw_scan_mem_multiplier;
|
||||
int hnsw_lock_tranche_id;
|
||||
static relopt_kind hnsw_relopt_kind;
|
||||
|
||||
@@ -66,7 +77,21 @@ HnswInit(void)
|
||||
|
||||
DefineCustomIntVariable("hnsw.ef_search", "Sets the size of the dynamic candidate list for 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, GUC_EXPLAIN, NULL, NULL, NULL);
|
||||
|
||||
DefineCustomEnumVariable("hnsw.iterative_scan", "Sets the mode for iterative scans",
|
||||
NULL, &hnsw_iterative_scan,
|
||||
HNSW_ITERATIVE_SCAN_OFF, hnsw_iterative_scan_options, PGC_USERSET, GUC_EXPLAIN, NULL, NULL, NULL);
|
||||
|
||||
/* This is approximate and does not affect the initial scan */
|
||||
DefineCustomIntVariable("hnsw.max_scan_tuples", "Sets the max number of tuples to visit for iterative scans",
|
||||
NULL, &hnsw_max_scan_tuples,
|
||||
20000, 1, INT_MAX, PGC_USERSET, GUC_EXPLAIN, NULL, NULL, NULL);
|
||||
|
||||
/* Same range as hash_mem_multiplier */
|
||||
DefineCustomRealVariable("hnsw.scan_mem_multiplier", "Sets the multiple of work_mem to use for iterative scans",
|
||||
NULL, &hnsw_scan_mem_multiplier,
|
||||
1, 1, 1000, PGC_USERSET, GUC_EXPLAIN, NULL, NULL, NULL);
|
||||
|
||||
MarkGUCPrefixReserved("hnsw");
|
||||
}
|
||||
@@ -99,7 +124,9 @@ hnswcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
|
||||
{
|
||||
GenericCosts costs;
|
||||
int m;
|
||||
int entryLevel;
|
||||
double ratio;
|
||||
double startupPages;
|
||||
double spc_seq_page_cost;
|
||||
Relation index;
|
||||
|
||||
/* Never use index without order */
|
||||
@@ -110,26 +137,80 @@ hnswcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
|
||||
*indexSelectivity = 0;
|
||||
*indexCorrelation = 0;
|
||||
*indexPages = 0;
|
||||
#if PG_VERSION_NUM >= 180000
|
||||
/* See "On disable_cost" thread on pgsql-hackers */
|
||||
path->path.disabled_nodes = 2;
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
MemSet(&costs, 0, sizeof(costs));
|
||||
|
||||
genericcostestimate(root, path, loop_count, &costs);
|
||||
|
||||
index = index_open(path->indexinfo->indexoid, NoLock);
|
||||
HnswGetMetaPageInfo(index, &m, NULL);
|
||||
index_close(index, NoLock);
|
||||
|
||||
/* Approximate entry level */
|
||||
entryLevel = (int) -log(1.0 / path->indexinfo->tuples) * HnswGetMl(m);
|
||||
/*
|
||||
* HNSW cost estimation follows a formula that accounts for the total
|
||||
* number of tuples indexed combined with the parameters that most
|
||||
* influence the duration of the index scan, namely: m - the number of
|
||||
* tuples that are scanned in each step of the HNSW graph traversal
|
||||
* ef_search - which influences the total number of steps taken at layer 0
|
||||
*
|
||||
* The source of the vector data can impact how many steps it takes to
|
||||
* converge on the set of vectors to return to the executor. Currently, we
|
||||
* use a hardcoded scaling factor (HNSWScanScalingFactor) to help
|
||||
* influence that, but this could later become a configurable parameter
|
||||
* based on the cost estimations.
|
||||
*
|
||||
* The tuple estimator formula is below:
|
||||
*
|
||||
* numIndexTuples = entryLevel * m + layer0TuplesMax * layer0Selectivity
|
||||
*
|
||||
* "entryLevel * m" represents the floor of tuples we need to scan to get
|
||||
* to layer 0 (L0).
|
||||
*
|
||||
* "layer0TuplesMax" is the estimated total number of tuples we'd scan at
|
||||
* L0 if we weren't discarding already visited tuples as part of the scan.
|
||||
*
|
||||
* "layer0Selectivity" estimates the percentage of tuples that are scanned
|
||||
* at L0, accounting for previously visited tuples, multiplied by the
|
||||
* "scalingFactor" (currently hardcoded).
|
||||
*/
|
||||
if (path->indexinfo->tuples > 0)
|
||||
{
|
||||
double scalingFactor = 0.55;
|
||||
int entryLevel = (int) (log(path->indexinfo->tuples) * HnswGetMl(m));
|
||||
int layer0TuplesMax = HnswGetLayerM(m, 0) * hnsw_ef_search;
|
||||
double layer0Selectivity = scalingFactor * log(path->indexinfo->tuples) / (log(m) * (1 + log(hnsw_ef_search)));
|
||||
|
||||
/* TODO Improve estimate of visited tuples (currently underestimates) */
|
||||
/* Account for number of tuples (or entry level), m, and ef_search */
|
||||
costs.numIndexTuples = (entryLevel + 2) * m;
|
||||
ratio = (entryLevel * m + layer0TuplesMax * layer0Selectivity) / path->indexinfo->tuples;
|
||||
|
||||
genericcostestimate(root, path, loop_count, &costs);
|
||||
if (ratio > 1)
|
||||
ratio = 1;
|
||||
}
|
||||
else
|
||||
ratio = 1;
|
||||
|
||||
/* Use total cost since most work happens before first tuple is returned */
|
||||
*indexStartupCost = costs.indexTotalCost;
|
||||
get_tablespace_page_costs(path->indexinfo->reltablespace, NULL, &spc_seq_page_cost);
|
||||
|
||||
/* Startup cost is cost before returning the first row */
|
||||
costs.indexStartupCost = costs.indexTotalCost * ratio;
|
||||
|
||||
/* Adjust cost if needed since TOAST not included in seq scan cost */
|
||||
startupPages = costs.numIndexPages * ratio;
|
||||
if (startupPages > path->indexinfo->rel->pages && ratio < 0.5)
|
||||
{
|
||||
/* Change all page cost from random to sequential */
|
||||
costs.indexStartupCost -= startupPages * (costs.spc_random_page_cost - spc_seq_page_cost);
|
||||
|
||||
/* Remove cost of extra pages */
|
||||
costs.indexStartupCost -= (startupPages - path->indexinfo->rel->pages) * spc_seq_page_cost;
|
||||
}
|
||||
|
||||
*indexStartupCost = costs.indexStartupCost;
|
||||
*indexTotalCost = costs.indexTotalCost;
|
||||
*indexSelectivity = costs.indexSelectivity;
|
||||
*indexCorrelation = costs.indexCorrelation;
|
||||
|
||||
90
src/hnsw.h
90
src/hnsw.h
@@ -88,6 +88,9 @@
|
||||
/* Ensure fits on page and in uint8 */
|
||||
#define HnswGetMaxLevel(m) Min(((BLCKSZ - MAXALIGN(SizeOfPageHeaderData) - MAXALIGN(sizeof(HnswPageOpaqueData)) - offsetof(HnswNeighborTupleData, indextids) - sizeof(ItemIdData)) / (sizeof(ItemPointerData)) / (m)) - 2, 255)
|
||||
|
||||
#define HnswGetSearchCandidate(membername, ptr) pairingheap_container(HnswSearchCandidate, membername, ptr)
|
||||
#define HnswGetSearchCandidateConst(membername, ptr) pairingheap_const_container(HnswSearchCandidate, membername, ptr)
|
||||
|
||||
#define HnswGetValue(base, element) PointerGetDatum(HnswPtrAccess(base, (element)->value))
|
||||
|
||||
#if PG_VERSION_NUM < 140005
|
||||
@@ -106,8 +109,18 @@
|
||||
|
||||
/* Variables */
|
||||
extern int hnsw_ef_search;
|
||||
extern int hnsw_iterative_scan;
|
||||
extern int hnsw_max_scan_tuples;
|
||||
extern double hnsw_scan_mem_multiplier;
|
||||
extern int hnsw_lock_tranche_id;
|
||||
|
||||
typedef enum HnswIterativeScanMode
|
||||
{
|
||||
HNSW_ITERATIVE_SCAN_OFF,
|
||||
HNSW_ITERATIVE_SCAN_RELAXED,
|
||||
HNSW_ITERATIVE_SCAN_STRICT
|
||||
} HnswIterativeScanMode;
|
||||
|
||||
typedef struct HnswElementData HnswElementData;
|
||||
typedef struct HnswNeighborArray HnswNeighborArray;
|
||||
|
||||
@@ -129,6 +142,7 @@ struct HnswElementData
|
||||
uint8 heaptidsLength;
|
||||
uint8 level;
|
||||
uint8 deleted;
|
||||
uint8 version;
|
||||
uint32 hash;
|
||||
HnswNeighborsPtr neighbors;
|
||||
BlockNumber blkno;
|
||||
@@ -160,7 +174,7 @@ typedef struct HnswSearchCandidate
|
||||
pairingheap_node c_node;
|
||||
pairingheap_node w_node;
|
||||
HnswElementPtr element;
|
||||
float distance;
|
||||
double distance;
|
||||
} HnswSearchCandidate;
|
||||
|
||||
/* HNSW index options */
|
||||
@@ -185,8 +199,8 @@ typedef struct HnswGraph
|
||||
|
||||
/* Allocations state */
|
||||
LWLock allocatorLock;
|
||||
long memoryUsed;
|
||||
long memoryTotal;
|
||||
Size memoryUsed;
|
||||
Size memoryTotal;
|
||||
|
||||
/* Flushed state */
|
||||
LWLock flushLock;
|
||||
@@ -237,6 +251,18 @@ typedef struct HnswTypeInfo
|
||||
void (*checkValue) (Pointer v);
|
||||
} HnswTypeInfo;
|
||||
|
||||
typedef struct HnswSupport
|
||||
{
|
||||
FmgrInfo *procinfo;
|
||||
FmgrInfo *normprocinfo;
|
||||
Oid collation;
|
||||
} HnswSupport;
|
||||
|
||||
typedef struct HnswQuery
|
||||
{
|
||||
Datum value;
|
||||
} HnswQuery;
|
||||
|
||||
typedef struct HnswBuildState
|
||||
{
|
||||
/* Info */
|
||||
@@ -256,9 +282,7 @@ typedef struct HnswBuildState
|
||||
double reltuples;
|
||||
|
||||
/* Support functions */
|
||||
FmgrInfo *procinfo;
|
||||
FmgrInfo *normprocinfo;
|
||||
Oid collation;
|
||||
HnswSupport support;
|
||||
|
||||
/* Variables */
|
||||
HnswGraph graphData;
|
||||
@@ -306,10 +330,10 @@ typedef struct HnswElementTupleData
|
||||
uint8 type;
|
||||
uint8 level;
|
||||
uint8 deleted;
|
||||
uint8 unused;
|
||||
uint8 version;
|
||||
ItemPointerData heaptids[HNSW_HEAPTIDS];
|
||||
ItemPointerData neighbortid;
|
||||
uint16 unused2;
|
||||
uint16 unused;
|
||||
Vector data;
|
||||
} HnswElementTupleData;
|
||||
|
||||
@@ -318,24 +342,42 @@ typedef HnswElementTupleData * HnswElementTuple;
|
||||
typedef struct HnswNeighborTupleData
|
||||
{
|
||||
uint8 type;
|
||||
uint8 unused;
|
||||
uint8 version;
|
||||
uint16 count;
|
||||
ItemPointerData indextids[FLEXIBLE_ARRAY_MEMBER];
|
||||
} HnswNeighborTupleData;
|
||||
|
||||
typedef HnswNeighborTupleData * HnswNeighborTuple;
|
||||
|
||||
typedef union
|
||||
{
|
||||
struct pointerhash_hash *pointers;
|
||||
struct offsethash_hash *offsets;
|
||||
struct tidhash_hash *tids;
|
||||
} visited_hash;
|
||||
|
||||
typedef union
|
||||
{
|
||||
HnswElement element;
|
||||
ItemPointerData indextid;
|
||||
} HnswUnvisited;
|
||||
|
||||
typedef struct HnswScanOpaqueData
|
||||
{
|
||||
const HnswTypeInfo *typeInfo;
|
||||
bool first;
|
||||
List *w;
|
||||
visited_hash v;
|
||||
pairingheap *discarded;
|
||||
HnswQuery q;
|
||||
int m;
|
||||
int64 tuples;
|
||||
double previousDistance;
|
||||
Size maxMemory;
|
||||
MemoryContext tmpCtx;
|
||||
|
||||
/* Support functions */
|
||||
FmgrInfo *procinfo;
|
||||
FmgrInfo *normprocinfo;
|
||||
Oid collation;
|
||||
HnswSupport support;
|
||||
} HnswScanOpaqueData;
|
||||
|
||||
typedef HnswScanOpaqueData * HnswScanOpaque;
|
||||
@@ -353,8 +395,7 @@ typedef struct HnswVacuumState
|
||||
int efConstruction;
|
||||
|
||||
/* Support functions */
|
||||
FmgrInfo *procinfo;
|
||||
Oid collation;
|
||||
HnswSupport support;
|
||||
|
||||
/* Variables */
|
||||
struct tidhash_hash *deleted;
|
||||
@@ -370,30 +411,33 @@ typedef struct HnswVacuumState
|
||||
int HnswGetM(Relation index);
|
||||
int HnswGetEfConstruction(Relation index);
|
||||
FmgrInfo *HnswOptionalProcInfo(Relation index, uint16 procnum);
|
||||
void HnswInitSupport(HnswSupport * support, Relation index);
|
||||
Datum HnswNormValue(const HnswTypeInfo * typeInfo, Oid collation, Datum value);
|
||||
bool HnswCheckNorm(FmgrInfo *procinfo, Oid collation, Datum value);
|
||||
bool HnswCheckNorm(HnswSupport * support, Datum value);
|
||||
Buffer HnswNewBuffer(Relation index, ForkNumber forkNum);
|
||||
void HnswInitPage(Buffer buf, Page page);
|
||||
void HnswInit(void);
|
||||
List *HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, FmgrInfo *procinfo, Oid collation, int m, bool inserting, HnswElement skipElement);
|
||||
List *HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation index, HnswSupport * support, int m, bool inserting, HnswElement skipElement, visited_hash * v, pairingheap **discarded, bool initVisited, int64 *tuples);
|
||||
HnswElement HnswGetEntryPoint(Relation index);
|
||||
void HnswGetMetaPageInfo(Relation index, int *m, HnswElement * entryPoint);
|
||||
void *HnswAlloc(HnswAllocator * allocator, Size size);
|
||||
HnswElement HnswInitElement(char *base, ItemPointer tid, int m, double ml, int maxLevel, HnswAllocator * alloc);
|
||||
HnswElement HnswInitElementFromBlock(BlockNumber blkno, OffsetNumber offno);
|
||||
void HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint, Relation index, FmgrInfo *procinfo, Oid collation, int m, int efConstruction, bool existing);
|
||||
HnswSearchCandidate *HnswEntryCandidate(char *base, HnswElement em, Datum q, Relation rel, FmgrInfo *procinfo, Oid collation, bool loadVec);
|
||||
void HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint, Relation index, HnswSupport * support, int m, int efConstruction, bool existing);
|
||||
HnswSearchCandidate *HnswEntryCandidate(char *base, HnswElement em, HnswQuery * q, Relation rel, HnswSupport * support, bool loadVec);
|
||||
void HnswUpdateMetaPage(Relation index, int updateEntry, HnswElement entryPoint, BlockNumber insertPage, ForkNumber forkNum, bool building);
|
||||
void HnswSetNeighborTuple(char *base, HnswNeighborTuple ntup, HnswElement e, int m);
|
||||
void HnswAddHeapTid(HnswElement element, ItemPointer heaptid);
|
||||
HnswNeighborArray *HnswInitNeighborArray(int lm, HnswAllocator * allocator);
|
||||
void HnswInitNeighbors(char *base, HnswElement element, int m, HnswAllocator * alloc);
|
||||
bool HnswInsertTupleOnDisk(Relation index, Datum value, Datum *values, bool *isnull, ItemPointer heap_tid, bool building);
|
||||
void HnswUpdateNeighborsOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, HnswElement e, int m, bool checkExisting, bool building);
|
||||
bool HnswInsertTupleOnDisk(Relation index, HnswSupport * support, Datum value, ItemPointer heaptid, bool building);
|
||||
void HnswUpdateNeighborsOnDisk(Relation index, HnswSupport * support, HnswElement e, int m, bool checkExisting, bool building);
|
||||
void HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool loadHeaptids, bool loadVec);
|
||||
void HnswLoadElement(HnswElement element, float *distance, Datum *q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec, float *maxDistance);
|
||||
void HnswLoadElement(HnswElement element, double *distance, HnswQuery * q, Relation index, HnswSupport * support, bool loadVec, double *maxDistance);
|
||||
bool HnswFormIndexValue(Datum *out, Datum *values, bool *isnull, const HnswTypeInfo * typeInfo, HnswSupport * support);
|
||||
void HnswSetElementTuple(char *base, HnswElementTuple etup, HnswElement element);
|
||||
void HnswUpdateConnection(char *base, HnswElement element, HnswCandidate * hc, int lm, int lc, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation);
|
||||
void HnswLoadNeighbors(HnswElement element, Relation index, int m);
|
||||
void HnswUpdateConnection(char *base, HnswNeighborArray * neighbors, HnswElement newElement, float distance, int lm, int *updateIdx, Relation index, HnswSupport * support);
|
||||
bool HnswLoadNeighborTids(HnswElement element, ItemPointerData *indextids, Relation index, int m, int lm, int lc);
|
||||
void HnswInitLockTranche(void);
|
||||
const HnswTypeInfo *HnswGetTypeInfo(Relation index);
|
||||
PGDLLEXPORT void HnswParallelBuildMain(dsm_segment *seg, shm_toc *toc);
|
||||
|
||||
@@ -366,7 +366,7 @@ AddElementInMemory(char *base, HnswGraph * graph, HnswElement element)
|
||||
* Update neighbors
|
||||
*/
|
||||
static void
|
||||
UpdateNeighborsInMemory(char *base, FmgrInfo *procinfo, Oid collation, HnswElement e, int m)
|
||||
UpdateNeighborsInMemory(char *base, HnswSupport * support, HnswElement e, int m)
|
||||
{
|
||||
for (int lc = e->level; lc >= 0; lc--)
|
||||
{
|
||||
@@ -388,7 +388,7 @@ UpdateNeighborsInMemory(char *base, FmgrInfo *procinfo, Oid collation, HnswEleme
|
||||
Assert(neighborElement);
|
||||
|
||||
LWLockAcquire(&neighborElement->lock, LW_EXCLUSIVE);
|
||||
HnswUpdateConnection(base, e, hc, lm, lc, NULL, NULL, procinfo, collation);
|
||||
HnswUpdateConnection(base, HnswGetNeighbors(base, neighborElement, lc), e, hc->distance, lm, NULL, NULL, support);
|
||||
LWLockRelease(&neighborElement->lock);
|
||||
}
|
||||
}
|
||||
@@ -398,7 +398,7 @@ UpdateNeighborsInMemory(char *base, FmgrInfo *procinfo, Oid collation, HnswEleme
|
||||
* Update graph in memory
|
||||
*/
|
||||
static void
|
||||
UpdateGraphInMemory(FmgrInfo *procinfo, Oid collation, HnswElement element, int m, int efConstruction, HnswElement entryPoint, HnswBuildState * buildstate)
|
||||
UpdateGraphInMemory(HnswSupport * support, HnswElement element, int m, int efConstruction, HnswElement entryPoint, HnswBuildState * buildstate)
|
||||
{
|
||||
HnswGraph *graph = buildstate->graph;
|
||||
char *base = buildstate->hnswarea;
|
||||
@@ -411,7 +411,7 @@ UpdateGraphInMemory(FmgrInfo *procinfo, Oid collation, HnswElement element, int
|
||||
AddElementInMemory(base, graph, element);
|
||||
|
||||
/* Update neighbors */
|
||||
UpdateNeighborsInMemory(base, procinfo, collation, element, m);
|
||||
UpdateNeighborsInMemory(base, support, element, m);
|
||||
|
||||
/* Update entry point if needed (already have lock) */
|
||||
if (entryPoint == NULL || element->level > entryPoint->level)
|
||||
@@ -424,9 +424,8 @@ UpdateGraphInMemory(FmgrInfo *procinfo, Oid collation, HnswElement element, int
|
||||
static void
|
||||
InsertTupleInMemory(HnswBuildState * buildstate, HnswElement element)
|
||||
{
|
||||
FmgrInfo *procinfo = buildstate->procinfo;
|
||||
Oid collation = buildstate->collation;
|
||||
HnswGraph *graph = buildstate->graph;
|
||||
HnswSupport *support = &buildstate->support;
|
||||
HnswElement entryPoint;
|
||||
LWLock *entryLock = &graph->entryLock;
|
||||
LWLock *entryWaitLock = &graph->entryWaitLock;
|
||||
@@ -458,10 +457,10 @@ InsertTupleInMemory(HnswBuildState * buildstate, HnswElement element)
|
||||
}
|
||||
|
||||
/* Find neighbors for element */
|
||||
HnswFindElementNeighbors(base, element, entryPoint, NULL, procinfo, collation, m, efConstruction, false);
|
||||
HnswFindElementNeighbors(base, element, entryPoint, NULL, support, m, efConstruction, false);
|
||||
|
||||
/* Update graph in memory */
|
||||
UpdateGraphInMemory(procinfo, collation, element, m, efConstruction, entryPoint, buildstate);
|
||||
UpdateGraphInMemory(support, element, m, efConstruction, entryPoint, buildstate);
|
||||
|
||||
/* Release entry lock */
|
||||
LWLockRelease(entryLock);
|
||||
@@ -473,30 +472,19 @@ InsertTupleInMemory(HnswBuildState * buildstate, HnswElement element)
|
||||
static bool
|
||||
InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid, HnswBuildState * buildstate)
|
||||
{
|
||||
const HnswTypeInfo *typeInfo = buildstate->typeInfo;
|
||||
HnswGraph *graph = buildstate->graph;
|
||||
HnswElement element;
|
||||
HnswAllocator *allocator = &buildstate->allocator;
|
||||
HnswSupport *support = &buildstate->support;
|
||||
Size valueSize;
|
||||
Pointer valuePtr;
|
||||
LWLock *flushLock = &graph->flushLock;
|
||||
char *base = buildstate->hnswarea;
|
||||
Datum value;
|
||||
|
||||
/* Detoast once for all calls */
|
||||
Datum value = PointerGetDatum(PG_DETOAST_DATUM(values[0]));
|
||||
|
||||
/* Check value */
|
||||
if (typeInfo->checkValue != NULL)
|
||||
typeInfo->checkValue(DatumGetPointer(value));
|
||||
|
||||
/* Normalize if needed */
|
||||
if (buildstate->normprocinfo != NULL)
|
||||
{
|
||||
if (!HnswCheckNorm(buildstate->normprocinfo, buildstate->collation, value))
|
||||
return false;
|
||||
|
||||
value = HnswNormValue(typeInfo, buildstate->collation, value);
|
||||
}
|
||||
/* Form index value */
|
||||
if (!HnswFormIndexValue(&value, values, isnull, buildstate->typeInfo, support))
|
||||
return false;
|
||||
|
||||
/* Get datum size */
|
||||
valueSize = VARSIZE_ANY(DatumGetPointer(value));
|
||||
@@ -509,7 +497,7 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid, Hn
|
||||
{
|
||||
LWLockRelease(flushLock);
|
||||
|
||||
return HnswInsertTupleOnDisk(index, value, values, isnull, heaptid, true);
|
||||
return HnswInsertTupleOnDisk(index, support, value, heaptid, true);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -541,7 +529,7 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid, Hn
|
||||
|
||||
LWLockRelease(flushLock);
|
||||
|
||||
return HnswInsertTupleOnDisk(index, value, values, isnull, heaptid, true);
|
||||
return HnswInsertTupleOnDisk(index, support, value, heaptid, true);
|
||||
}
|
||||
|
||||
/* Ok, we can proceed to allocate the element */
|
||||
@@ -607,7 +595,7 @@ BuildCallback(Relation index, ItemPointer tid, Datum *values,
|
||||
* Initialize the graph
|
||||
*/
|
||||
static void
|
||||
InitGraph(HnswGraph * graph, char *base, long memoryTotal)
|
||||
InitGraph(HnswGraph * graph, char *base, Size memoryTotal)
|
||||
{
|
||||
/* Initialize the lock tranche if needed */
|
||||
HnswInitLockTranche();
|
||||
@@ -704,11 +692,9 @@ InitBuildState(HnswBuildState * buildstate, Relation heap, Relation index, Index
|
||||
buildstate->indtuples = 0;
|
||||
|
||||
/* Get support functions */
|
||||
buildstate->procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC);
|
||||
buildstate->normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC);
|
||||
buildstate->collation = index->rd_indcollation[0];
|
||||
HnswInitSupport(&buildstate->support, index);
|
||||
|
||||
InitGraph(&buildstate->graphData, NULL, maintenance_work_mem * 1024L);
|
||||
InitGraph(&buildstate->graphData, NULL, (Size) maintenance_work_mem * 1024L);
|
||||
buildstate->graph = &buildstate->graphData;
|
||||
buildstate->ml = HnswGetMl(buildstate->m);
|
||||
buildstate->maxLevel = HnswGetMaxLevel(buildstate->m);
|
||||
|
||||
319
src/hnswinsert.c
319
src/hnswinsert.c
@@ -36,7 +36,7 @@ GetInsertPage(Relation index)
|
||||
* Check for a free offset
|
||||
*/
|
||||
static bool
|
||||
HnswFreeOffset(Relation index, Buffer buf, Page page, HnswElement element, Size etupSize, Size ntupSize, Buffer *nbuf, Page *npage, OffsetNumber *freeOffno, OffsetNumber *freeNeighborOffno, BlockNumber *newInsertPage)
|
||||
HnswFreeOffset(Relation index, Buffer buf, Page page, HnswElement element, Size etupSize, Size ntupSize, Buffer *nbuf, Page *npage, OffsetNumber *freeOffno, OffsetNumber *freeNeighborOffno, BlockNumber *newInsertPage, uint8 *tupleVersion)
|
||||
{
|
||||
OffsetNumber offno;
|
||||
OffsetNumber maxoffno = PageGetMaxOffsetNumber(page);
|
||||
@@ -98,6 +98,7 @@ HnswFreeOffset(Relation index, Buffer buf, Page page, HnswElement element, Size
|
||||
{
|
||||
*freeOffno = offno;
|
||||
*freeNeighborOffno = neighborOffno;
|
||||
*tupleVersion = etup->version;
|
||||
return true;
|
||||
}
|
||||
else if (*nbuf != buf)
|
||||
@@ -153,6 +154,7 @@ AddElementOnDisk(Relation index, HnswElement e, int m, BlockNumber insertPage, B
|
||||
OffsetNumber freeOffno = InvalidOffsetNumber;
|
||||
OffsetNumber freeNeighborOffno = InvalidOffsetNumber;
|
||||
BlockNumber newInsertPage = InvalidBlockNumber;
|
||||
uint8 tupleVersion;
|
||||
char *base = NULL;
|
||||
|
||||
/* Calculate sizes */
|
||||
@@ -202,7 +204,7 @@ AddElementOnDisk(Relation index, HnswElement e, int m, BlockNumber insertPage, B
|
||||
}
|
||||
|
||||
/* Next, try space from a deleted element */
|
||||
if (HnswFreeOffset(index, buf, page, e, etupSize, ntupSize, &nbuf, &npage, &freeOffno, &freeNeighborOffno, &newInsertPage))
|
||||
if (HnswFreeOffset(index, buf, page, e, etupSize, ntupSize, &nbuf, &npage, &freeOffno, &freeNeighborOffno, &newInsertPage, &tupleVersion))
|
||||
{
|
||||
if (nbuf != buf)
|
||||
{
|
||||
@@ -212,6 +214,10 @@ AddElementOnDisk(Relation index, HnswElement e, int m, BlockNumber insertPage, B
|
||||
npage = GenericXLogRegisterBuffer(state, nbuf, 0);
|
||||
}
|
||||
|
||||
/* Set tuple version */
|
||||
etup->version = tupleVersion;
|
||||
ntup->version = tupleVersion;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -334,6 +340,107 @@ AddElementOnDisk(Relation index, HnswElement e, int m, BlockNumber insertPage, B
|
||||
*updatedInsertPage = newInsertPage;
|
||||
}
|
||||
|
||||
/*
|
||||
* Load neighbors
|
||||
*/
|
||||
static HnswNeighborArray *
|
||||
HnswLoadNeighbors(HnswElement element, Relation index, int m, int lm, int lc)
|
||||
{
|
||||
char *base = NULL;
|
||||
HnswNeighborArray *neighbors = HnswInitNeighborArray(lm, NULL);
|
||||
ItemPointerData indextids[HNSW_MAX_M * 2];
|
||||
|
||||
if (!HnswLoadNeighborTids(element, indextids, index, m, lm, lc))
|
||||
return neighbors;
|
||||
|
||||
for (int i = 0; i < lm; i++)
|
||||
{
|
||||
ItemPointer indextid = &indextids[i];
|
||||
HnswElement e;
|
||||
HnswCandidate *hc;
|
||||
|
||||
if (!ItemPointerIsValid(indextid))
|
||||
break;
|
||||
|
||||
e = HnswInitElementFromBlock(ItemPointerGetBlockNumber(indextid), ItemPointerGetOffsetNumber(indextid));
|
||||
hc = &neighbors->items[neighbors->length++];
|
||||
HnswPtrStore(base, hc->element, e);
|
||||
}
|
||||
|
||||
return neighbors;
|
||||
}
|
||||
|
||||
/*
|
||||
* Load elements for insert
|
||||
*/
|
||||
static void
|
||||
LoadElementsForInsert(HnswNeighborArray * neighbors, HnswQuery * q, int *idx, Relation index, HnswSupport * support)
|
||||
{
|
||||
char *base = NULL;
|
||||
|
||||
for (int i = 0; i < neighbors->length; i++)
|
||||
{
|
||||
HnswCandidate *hc = &neighbors->items[i];
|
||||
HnswElement element = HnswPtrAccess(base, hc->element);
|
||||
double distance;
|
||||
|
||||
HnswLoadElement(element, &distance, q, index, support, true, NULL);
|
||||
hc->distance = distance;
|
||||
|
||||
/* Prune element if being deleted */
|
||||
if (element->heaptidsLength == 0)
|
||||
{
|
||||
*idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Get update index
|
||||
*/
|
||||
static int
|
||||
GetUpdateIndex(HnswElement element, HnswElement newElement, float distance, int m, int lm, int lc, Relation index, HnswSupport * support, MemoryContext updateCtx)
|
||||
{
|
||||
char *base = NULL;
|
||||
int idx = -1;
|
||||
HnswNeighborArray *neighbors;
|
||||
MemoryContext oldCtx = MemoryContextSwitchTo(updateCtx);
|
||||
|
||||
/*
|
||||
* Get latest neighbors since they may have changed. Do not lock yet since
|
||||
* selecting neighbors can take time. Could use optimistic locking to
|
||||
* retry if another update occurs before getting exclusive lock.
|
||||
*/
|
||||
neighbors = HnswLoadNeighbors(element, index, m, lm, lc);
|
||||
|
||||
/*
|
||||
* Could improve performance for vacuuming by checking neighbors against
|
||||
* list of elements being deleted to find index. It's important to exclude
|
||||
* already deleted elements for this since they can be replaced at any
|
||||
* time.
|
||||
*/
|
||||
|
||||
if (neighbors->length < lm)
|
||||
idx = -2;
|
||||
else
|
||||
{
|
||||
HnswQuery q;
|
||||
|
||||
q.value = HnswGetValue(base, element);
|
||||
|
||||
LoadElementsForInsert(neighbors, &q, &idx, index, support);
|
||||
|
||||
if (idx == -1)
|
||||
HnswUpdateConnection(base, neighbors, newElement, distance, lm, &idx, index, support);
|
||||
}
|
||||
|
||||
MemoryContextSwitchTo(oldCtx);
|
||||
MemoryContextReset(updateCtx);
|
||||
|
||||
return idx;
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if connection already exists
|
||||
*/
|
||||
@@ -354,14 +461,94 @@ ConnectionExists(HnswElement e, HnswNeighborTuple ntup, int startIdx, int lm)
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Update neighbor
|
||||
*/
|
||||
static void
|
||||
UpdateNeighborOnDisk(HnswElement element, HnswElement newElement, int idx, int m, int lm, int lc, Relation index, bool checkExisting, bool building)
|
||||
{
|
||||
Buffer buf;
|
||||
Page page;
|
||||
GenericXLogState *state;
|
||||
HnswNeighborTuple ntup;
|
||||
int startIdx;
|
||||
OffsetNumber offno = element->neighborOffno;
|
||||
|
||||
/* Register page */
|
||||
buf = ReadBuffer(index, element->neighborPage);
|
||||
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
|
||||
if (building)
|
||||
{
|
||||
state = NULL;
|
||||
page = BufferGetPage(buf);
|
||||
}
|
||||
else
|
||||
{
|
||||
state = GenericXLogStart(index);
|
||||
page = GenericXLogRegisterBuffer(state, buf, 0);
|
||||
}
|
||||
|
||||
/* Get tuple */
|
||||
ntup = (HnswNeighborTuple) PageGetItem(page, PageGetItemId(page, offno));
|
||||
|
||||
/* Calculate index for update */
|
||||
startIdx = (element->level - lc) * m;
|
||||
|
||||
/* Check for existing connection */
|
||||
if (checkExisting && ConnectionExists(newElement, ntup, startIdx, lm))
|
||||
idx = -1;
|
||||
else if (idx == -2)
|
||||
{
|
||||
/* Find free offset if still exists */
|
||||
/* TODO Retry updating connections if not */
|
||||
for (int j = 0; j < lm; j++)
|
||||
{
|
||||
if (!ItemPointerIsValid(&ntup->indextids[startIdx + j]))
|
||||
{
|
||||
idx = startIdx + j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
idx += startIdx;
|
||||
|
||||
/* Make robust to issues */
|
||||
if (idx >= 0 && idx < ntup->count)
|
||||
{
|
||||
ItemPointer indextid = &ntup->indextids[idx];
|
||||
|
||||
/* Update neighbor on the buffer */
|
||||
ItemPointerSet(indextid, newElement->blkno, newElement->offno);
|
||||
|
||||
/* Commit */
|
||||
if (building)
|
||||
MarkBufferDirty(buf);
|
||||
else
|
||||
GenericXLogFinish(state);
|
||||
}
|
||||
else if (!building)
|
||||
GenericXLogAbort(state);
|
||||
|
||||
UnlockReleaseBuffer(buf);
|
||||
}
|
||||
|
||||
/*
|
||||
* Update neighbors
|
||||
*/
|
||||
void
|
||||
HnswUpdateNeighborsOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, HnswElement e, int m, bool checkExisting, bool building)
|
||||
HnswUpdateNeighborsOnDisk(Relation index, HnswSupport * support, HnswElement e, int m, bool checkExisting, bool building)
|
||||
{
|
||||
char *base = NULL;
|
||||
|
||||
/* Use separate memory context to improve performance for larger vectors */
|
||||
MemoryContext updateCtx = GenerationContextCreate(CurrentMemoryContext,
|
||||
"Hnsw insert update context",
|
||||
#if PG_VERSION_NUM >= 150000
|
||||
128 * 1024, 128 * 1024,
|
||||
#endif
|
||||
128 * 1024);
|
||||
|
||||
for (int lc = e->level; lc >= 0; lc--)
|
||||
{
|
||||
int lm = HnswGetLayerM(m, lc);
|
||||
@@ -370,96 +557,20 @@ HnswUpdateNeighborsOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, Hns
|
||||
for (int i = 0; i < neighbors->length; i++)
|
||||
{
|
||||
HnswCandidate *hc = &neighbors->items[i];
|
||||
Buffer buf;
|
||||
Page page;
|
||||
GenericXLogState *state;
|
||||
HnswNeighborTuple ntup;
|
||||
int idx = -1;
|
||||
int startIdx;
|
||||
HnswElement neighborElement = HnswPtrAccess(base, hc->element);
|
||||
OffsetNumber offno = neighborElement->neighborOffno;
|
||||
int idx;
|
||||
|
||||
/*
|
||||
* 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);
|
||||
|
||||
/*
|
||||
* Could improve performance for vacuuming by checking neighbors
|
||||
* against list of elements being deleted to find index. It's
|
||||
* important to exclude already deleted elements for this since
|
||||
* they can be replaced at any time.
|
||||
*/
|
||||
|
||||
/* Select neighbors */
|
||||
HnswUpdateConnection(NULL, e, hc, lm, lc, &idx, index, procinfo, collation);
|
||||
idx = GetUpdateIndex(neighborElement, e, hc->distance, m, lm, lc, index, support, updateCtx);
|
||||
|
||||
/* New element was not selected as a neighbor */
|
||||
if (idx == -1)
|
||||
continue;
|
||||
|
||||
/* Register page */
|
||||
buf = ReadBuffer(index, neighborElement->neighborPage);
|
||||
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
|
||||
if (building)
|
||||
{
|
||||
state = NULL;
|
||||
page = BufferGetPage(buf);
|
||||
}
|
||||
else
|
||||
{
|
||||
state = GenericXLogStart(index);
|
||||
page = GenericXLogRegisterBuffer(state, buf, 0);
|
||||
}
|
||||
|
||||
/* Get tuple */
|
||||
ntup = (HnswNeighborTuple) PageGetItem(page, PageGetItemId(page, offno));
|
||||
|
||||
/* Calculate index for update */
|
||||
startIdx = (neighborElement->level - lc) * m;
|
||||
|
||||
/* Check for existing connection */
|
||||
if (checkExisting && ConnectionExists(e, ntup, startIdx, lm))
|
||||
idx = -1;
|
||||
else if (idx == -2)
|
||||
{
|
||||
/* Find free offset if still exists */
|
||||
/* TODO Retry updating connections if not */
|
||||
for (int j = 0; j < lm; j++)
|
||||
{
|
||||
if (!ItemPointerIsValid(&ntup->indextids[startIdx + j]))
|
||||
{
|
||||
idx = startIdx + j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
idx += startIdx;
|
||||
|
||||
/* Make robust to issues */
|
||||
if (idx >= 0 && idx < ntup->count)
|
||||
{
|
||||
ItemPointer indextid = &ntup->indextids[idx];
|
||||
|
||||
/* Update neighbor on the buffer */
|
||||
ItemPointerSet(indextid, e->blkno, e->offno);
|
||||
|
||||
/* Commit */
|
||||
if (building)
|
||||
MarkBufferDirty(buf);
|
||||
else
|
||||
GenericXLogFinish(state);
|
||||
}
|
||||
else if (!building)
|
||||
GenericXLogAbort(state);
|
||||
|
||||
UnlockReleaseBuffer(buf);
|
||||
UpdateNeighborOnDisk(neighborElement, e, idx, m, lm, lc, index, checkExisting, building);
|
||||
}
|
||||
}
|
||||
|
||||
MemoryContextDelete(updateCtx);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -549,7 +660,7 @@ FindDuplicateOnDisk(Relation index, HnswElement element, bool building)
|
||||
* Update graph on disk
|
||||
*/
|
||||
static void
|
||||
UpdateGraphOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, HnswElement element, int m, int efConstruction, HnswElement entryPoint, bool building)
|
||||
UpdateGraphOnDisk(Relation index, HnswSupport * support, HnswElement element, int m, int efConstruction, HnswElement entryPoint, bool building)
|
||||
{
|
||||
BlockNumber newInsertPage = InvalidBlockNumber;
|
||||
|
||||
@@ -565,7 +676,7 @@ UpdateGraphOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, HnswElement
|
||||
HnswUpdateMetaPage(index, 0, NULL, newInsertPage, MAIN_FORKNUM, building);
|
||||
|
||||
/* Update neighbors */
|
||||
HnswUpdateNeighborsOnDisk(index, procinfo, collation, element, m, false, building);
|
||||
HnswUpdateNeighborsOnDisk(index, support, element, m, false, building);
|
||||
|
||||
/* Update entry point if needed */
|
||||
if (entryPoint == NULL || element->level > entryPoint->level)
|
||||
@@ -576,14 +687,12 @@ UpdateGraphOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, HnswElement
|
||||
* Insert a tuple into the index
|
||||
*/
|
||||
bool
|
||||
HnswInsertTupleOnDisk(Relation index, Datum value, Datum *values, bool *isnull, ItemPointer heap_tid, bool building)
|
||||
HnswInsertTupleOnDisk(Relation index, HnswSupport * support, Datum value, ItemPointer heaptid, bool building)
|
||||
{
|
||||
HnswElement entryPoint;
|
||||
HnswElement element;
|
||||
int m;
|
||||
int efConstruction = HnswGetEfConstruction(index);
|
||||
FmgrInfo *procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC);
|
||||
Oid collation = index->rd_indcollation[0];
|
||||
LOCKMODE lockmode = ShareLock;
|
||||
char *base = NULL;
|
||||
|
||||
@@ -598,7 +707,7 @@ HnswInsertTupleOnDisk(Relation index, Datum value, Datum *values, bool *isnull,
|
||||
HnswGetMetaPageInfo(index, &m, &entryPoint);
|
||||
|
||||
/* Create an element */
|
||||
element = HnswInitElement(base, heap_tid, m, HnswGetMl(m), HnswGetMaxLevel(m), NULL);
|
||||
element = HnswInitElement(base, heaptid, m, HnswGetMl(m), HnswGetMaxLevel(m), NULL);
|
||||
HnswPtrStore(base, element->value, DatumGetPointer(value));
|
||||
|
||||
/* Prevent concurrent inserts when likely updating entry point */
|
||||
@@ -616,10 +725,10 @@ HnswInsertTupleOnDisk(Relation index, Datum value, Datum *values, bool *isnull,
|
||||
}
|
||||
|
||||
/* Find neighbors for element */
|
||||
HnswFindElementNeighbors(base, element, entryPoint, index, procinfo, collation, m, efConstruction, false);
|
||||
HnswFindElementNeighbors(base, element, entryPoint, index, support, m, efConstruction, false);
|
||||
|
||||
/* Update graph on disk */
|
||||
UpdateGraphOnDisk(index, procinfo, collation, element, m, efConstruction, entryPoint, building);
|
||||
UpdateGraphOnDisk(index, support, element, m, efConstruction, entryPoint, building);
|
||||
|
||||
/* Release lock */
|
||||
UnlockPage(index, HNSW_UPDATE_LOCK, lockmode);
|
||||
@@ -631,31 +740,19 @@ HnswInsertTupleOnDisk(Relation index, Datum value, Datum *values, bool *isnull,
|
||||
* Insert a tuple into the index
|
||||
*/
|
||||
static void
|
||||
HnswInsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heap_tid)
|
||||
HnswInsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid)
|
||||
{
|
||||
Datum value;
|
||||
const HnswTypeInfo *typeInfo = HnswGetTypeInfo(index);
|
||||
FmgrInfo *normprocinfo;
|
||||
Oid collation = index->rd_indcollation[0];
|
||||
HnswSupport support;
|
||||
|
||||
/* Detoast once for all calls */
|
||||
value = PointerGetDatum(PG_DETOAST_DATUM(values[0]));
|
||||
HnswInitSupport(&support, index);
|
||||
|
||||
/* Check value */
|
||||
if (typeInfo->checkValue != NULL)
|
||||
typeInfo->checkValue(DatumGetPointer(value));
|
||||
/* Form index value */
|
||||
if (!HnswFormIndexValue(&value, values, isnull, typeInfo, &support))
|
||||
return;
|
||||
|
||||
/* Normalize if needed */
|
||||
normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC);
|
||||
if (normprocinfo != NULL)
|
||||
{
|
||||
if (!HnswCheckNorm(normprocinfo, collation, value))
|
||||
return;
|
||||
|
||||
value = HnswNormValue(typeInfo, collation, value);
|
||||
}
|
||||
|
||||
HnswInsertTupleOnDisk(index, value, values, isnull, heap_tid, false);
|
||||
HnswInsertTupleOnDisk(index, &support, value, heaptid, false);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
175
src/hnswscan.c
175
src/hnswscan.c
@@ -5,39 +5,74 @@
|
||||
#include "pgstat.h"
|
||||
#include "storage/bufmgr.h"
|
||||
#include "storage/lmgr.h"
|
||||
#include "utils/float.h"
|
||||
#include "utils/memutils.h"
|
||||
|
||||
/*
|
||||
* Algorithm 5 from paper
|
||||
*/
|
||||
static List *
|
||||
GetScanItems(IndexScanDesc scan, Datum q)
|
||||
GetScanItems(IndexScanDesc scan, Datum value)
|
||||
{
|
||||
HnswScanOpaque so = (HnswScanOpaque) scan->opaque;
|
||||
Relation index = scan->indexRelation;
|
||||
FmgrInfo *procinfo = so->procinfo;
|
||||
Oid collation = so->collation;
|
||||
HnswSupport *support = &so->support;
|
||||
List *ep;
|
||||
List *w;
|
||||
int m;
|
||||
HnswElement entryPoint;
|
||||
char *base = NULL;
|
||||
HnswQuery *q = &so->q;
|
||||
|
||||
/* Get m and entry point */
|
||||
HnswGetMetaPageInfo(index, &m, &entryPoint);
|
||||
|
||||
q->value = value;
|
||||
so->m = m;
|
||||
|
||||
if (entryPoint == NULL)
|
||||
return NIL;
|
||||
|
||||
ep = list_make1(HnswEntryCandidate(base, entryPoint, q, index, procinfo, collation, false));
|
||||
ep = list_make1(HnswEntryCandidate(base, entryPoint, q, index, support, false));
|
||||
|
||||
for (int lc = entryPoint->level; lc >= 1; lc--)
|
||||
{
|
||||
w = HnswSearchLayer(base, q, ep, 1, lc, index, procinfo, collation, m, false, NULL);
|
||||
w = HnswSearchLayer(base, q, ep, 1, lc, index, support, m, false, NULL, NULL, NULL, true, NULL);
|
||||
ep = w;
|
||||
}
|
||||
|
||||
return HnswSearchLayer(base, q, ep, hnsw_ef_search, 0, index, procinfo, collation, m, false, NULL);
|
||||
return HnswSearchLayer(base, q, ep, hnsw_ef_search, 0, index, support, m, false, NULL, &so->v, hnsw_iterative_scan != HNSW_ITERATIVE_SCAN_OFF ? &so->discarded : NULL, true, &so->tuples);
|
||||
}
|
||||
|
||||
/*
|
||||
* Resume scan at ground level with discarded candidates
|
||||
*/
|
||||
static List *
|
||||
ResumeScanItems(IndexScanDesc scan)
|
||||
{
|
||||
HnswScanOpaque so = (HnswScanOpaque) scan->opaque;
|
||||
Relation index = scan->indexRelation;
|
||||
List *ep = NIL;
|
||||
char *base = NULL;
|
||||
int batch_size = hnsw_ef_search;
|
||||
|
||||
if (pairingheap_is_empty(so->discarded))
|
||||
return NIL;
|
||||
|
||||
/* Get next batch of candidates */
|
||||
for (int i = 0; i < batch_size; i++)
|
||||
{
|
||||
HnswSearchCandidate *sc;
|
||||
|
||||
if (pairingheap_is_empty(so->discarded))
|
||||
break;
|
||||
|
||||
sc = HnswGetSearchCandidate(w_node, pairingheap_remove_first(so->discarded));
|
||||
|
||||
ep = lappend(ep, sc);
|
||||
}
|
||||
|
||||
return HnswSearchLayer(base, &so->q, ep, batch_size, 0, index, &so->support, so->m, false, NULL, &so->v, &so->discarded, false, &so->tuples);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -60,13 +95,24 @@ GetScanValue(IndexScanDesc scan)
|
||||
Assert(!VARATT_IS_EXTENDED(DatumGetPointer(value)));
|
||||
|
||||
/* Normalize if needed */
|
||||
if (so->normprocinfo != NULL)
|
||||
value = HnswNormValue(so->typeInfo, so->collation, value);
|
||||
if (so->support.normprocinfo != NULL)
|
||||
value = HnswNormValue(so->typeInfo, so->support.collation, value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
#if defined(HNSW_MEMORY)
|
||||
/*
|
||||
* Show memory usage
|
||||
*/
|
||||
static void
|
||||
ShowMemoryUsage(HnswScanOpaque so)
|
||||
{
|
||||
elog(INFO, "memory: %zu KB, tuples: " INT64_FORMAT, MemoryContextMemAllocated(so->tmpCtx, false) / 1024, so->tuples);
|
||||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Prepare for an index scan
|
||||
*/
|
||||
@@ -75,20 +121,28 @@ hnswbeginscan(Relation index, int nkeys, int norderbys)
|
||||
{
|
||||
IndexScanDesc scan;
|
||||
HnswScanOpaque so;
|
||||
double maxMemory;
|
||||
|
||||
scan = RelationGetIndexScan(index, nkeys, norderbys);
|
||||
|
||||
so = (HnswScanOpaque) palloc(sizeof(HnswScanOpaqueData));
|
||||
so->typeInfo = HnswGetTypeInfo(index);
|
||||
so->first = true;
|
||||
so->tmpCtx = AllocSetContextCreate(CurrentMemoryContext,
|
||||
"Hnsw scan temporary context",
|
||||
ALLOCSET_DEFAULT_SIZES);
|
||||
|
||||
/* Set support functions */
|
||||
so->procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC);
|
||||
so->normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC);
|
||||
so->collation = index->rd_indcollation[0];
|
||||
HnswInitSupport(&so->support, index);
|
||||
|
||||
/*
|
||||
* Use a lower max allocation size than default to allow scanning more
|
||||
* tuples for iterative search before exceeding work_mem
|
||||
*/
|
||||
so->tmpCtx = AllocSetContextCreate(CurrentMemoryContext,
|
||||
"Hnsw scan temporary context",
|
||||
0, 8 * 1024, 256 * 1024);
|
||||
|
||||
/* Calculate max memory */
|
||||
/* Add 256 extra bytes to fill last block when close */
|
||||
maxMemory = (double) work_mem * hnsw_scan_mem_multiplier * 1024.0 + 256;
|
||||
so->maxMemory = Min(maxMemory, (double) SIZE_MAX);
|
||||
|
||||
scan->opaque = so;
|
||||
|
||||
@@ -104,6 +158,11 @@ hnswrescan(IndexScanDesc scan, ScanKey keys, int nkeys, ScanKey orderbys, int no
|
||||
HnswScanOpaque so = (HnswScanOpaque) scan->opaque;
|
||||
|
||||
so->first = true;
|
||||
/* v and discarded are allocated in tmpCtx */
|
||||
so->v.tids = NULL;
|
||||
so->discarded = NULL;
|
||||
so->tuples = 0;
|
||||
so->previousDistance = -get_float8_infinity();
|
||||
MemoryContextReset(so->tmpCtx);
|
||||
|
||||
if (keys && scan->numberOfKeys > 0)
|
||||
@@ -161,26 +220,104 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir)
|
||||
so->first = false;
|
||||
|
||||
#if defined(HNSW_MEMORY)
|
||||
elog(INFO, "memory: %zu KB", MemoryContextMemAllocated(so->tmpCtx, false) / 1024);
|
||||
ShowMemoryUsage(so);
|
||||
#endif
|
||||
}
|
||||
|
||||
while (list_length(so->w) > 0)
|
||||
for (;;)
|
||||
{
|
||||
char *base = NULL;
|
||||
HnswSearchCandidate *hc = llast(so->w);
|
||||
HnswElement element = HnswPtrAccess(base, hc->element);
|
||||
HnswSearchCandidate *sc;
|
||||
HnswElement element;
|
||||
ItemPointer heaptid;
|
||||
|
||||
if (list_length(so->w) == 0)
|
||||
{
|
||||
if (hnsw_iterative_scan == HNSW_ITERATIVE_SCAN_OFF)
|
||||
break;
|
||||
|
||||
/* Empty index */
|
||||
if (so->discarded == NULL)
|
||||
break;
|
||||
|
||||
/* Reached max number of tuples */
|
||||
if (so->tuples >= hnsw_max_scan_tuples)
|
||||
{
|
||||
if (pairingheap_is_empty(so->discarded))
|
||||
break;
|
||||
|
||||
/* Return remaining tuples */
|
||||
so->w = lappend(so->w, HnswGetSearchCandidate(w_node, pairingheap_remove_first(so->discarded)));
|
||||
}
|
||||
/* Prevent scans from consuming too much memory */
|
||||
else if (MemoryContextMemAllocated(so->tmpCtx, false) > so->maxMemory)
|
||||
{
|
||||
if (pairingheap_is_empty(so->discarded))
|
||||
{
|
||||
ereport(DEBUG1,
|
||||
(errmsg("hnsw index scan reached memory limit after " INT64_FORMAT " tuples", so->tuples),
|
||||
errhint("Increase hnsw.scan_mem_multiplier to scan more tuples.")));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
/* Return remaining tuples */
|
||||
so->w = lappend(so->w, HnswGetSearchCandidate(w_node, pairingheap_remove_first(so->discarded)));
|
||||
}
|
||||
else
|
||||
{
|
||||
/*
|
||||
* Locking ensures when neighbors are read, the elements they
|
||||
* reference will not be deleted (and replaced) during the
|
||||
* iteration.
|
||||
*
|
||||
* Elements loaded into memory on previous iterations may have
|
||||
* been deleted (and replaced), so when reading neighbors, the
|
||||
* element version must be checked.
|
||||
*/
|
||||
LockPage(scan->indexRelation, HNSW_SCAN_LOCK, ShareLock);
|
||||
|
||||
so->w = ResumeScanItems(scan);
|
||||
|
||||
UnlockPage(scan->indexRelation, HNSW_SCAN_LOCK, ShareLock);
|
||||
|
||||
#if defined(HNSW_MEMORY)
|
||||
ShowMemoryUsage(so);
|
||||
#endif
|
||||
}
|
||||
|
||||
if (list_length(so->w) == 0)
|
||||
break;
|
||||
}
|
||||
|
||||
sc = llast(so->w);
|
||||
element = HnswPtrAccess(base, sc->element);
|
||||
|
||||
/* Move to next element if no valid heap TIDs */
|
||||
if (element->heaptidsLength == 0)
|
||||
{
|
||||
so->w = list_delete_last(so->w);
|
||||
|
||||
/* Mark memory as free for next iteration */
|
||||
if (hnsw_iterative_scan != HNSW_ITERATIVE_SCAN_OFF)
|
||||
{
|
||||
pfree(element);
|
||||
pfree(sc);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
heaptid = &element->heaptids[--element->heaptidsLength];
|
||||
|
||||
if (hnsw_iterative_scan == HNSW_ITERATIVE_SCAN_STRICT)
|
||||
{
|
||||
if (sc->distance < so->previousDistance)
|
||||
continue;
|
||||
|
||||
so->previousDistance = sc->distance;
|
||||
}
|
||||
|
||||
MemoryContextSwitchTo(oldCtx);
|
||||
|
||||
scan->xs_heaptid = *heaptid;
|
||||
|
||||
478
src/hnswutils.c
478
src/hnswutils.c
@@ -100,19 +100,6 @@ hash_offset(Size offset)
|
||||
#define SH_DEFINE
|
||||
#include "lib/simplehash.h"
|
||||
|
||||
typedef union
|
||||
{
|
||||
pointerhash_hash *pointers;
|
||||
offsethash_hash *offsets;
|
||||
tidhash_hash *tids;
|
||||
} visited_hash;
|
||||
|
||||
typedef union
|
||||
{
|
||||
HnswElement element;
|
||||
ItemPointerData indextid;
|
||||
} HnswUnvisited;
|
||||
|
||||
/*
|
||||
* Get the max number of connections in an upper layer for each element in the index
|
||||
*/
|
||||
@@ -153,15 +140,23 @@ HnswOptionalProcInfo(Relation index, uint16 procnum)
|
||||
return index_getprocinfo(index, 1, procnum);
|
||||
}
|
||||
|
||||
/*
|
||||
* Init support functions
|
||||
*/
|
||||
void
|
||||
HnswInitSupport(HnswSupport * support, Relation index)
|
||||
{
|
||||
support->procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC);
|
||||
support->collation = index->rd_indcollation[0];
|
||||
support->normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC);
|
||||
}
|
||||
|
||||
/*
|
||||
* Normalize value
|
||||
*/
|
||||
Datum
|
||||
HnswNormValue(const HnswTypeInfo * typeInfo, Oid collation, Datum value)
|
||||
{
|
||||
if (!typeInfo->normalize)
|
||||
return value;
|
||||
|
||||
return DirectFunctionCall1Coll(typeInfo->normalize, collation, value);
|
||||
}
|
||||
|
||||
@@ -169,9 +164,9 @@ HnswNormValue(const HnswTypeInfo * typeInfo, Oid collation, Datum value)
|
||||
* Check if non-zero norm
|
||||
*/
|
||||
bool
|
||||
HnswCheckNorm(FmgrInfo *procinfo, Oid collation, Datum value)
|
||||
HnswCheckNorm(HnswSupport * support, Datum value)
|
||||
{
|
||||
return DatumGetFloat8(FunctionCall1Coll(procinfo, collation, value)) > 0;
|
||||
return DatumGetFloat8(FunctionCall1Coll(support->normprocinfo, support->collation, value)) > 0;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -200,7 +195,7 @@ HnswInitPage(Buffer buf, Page page)
|
||||
/*
|
||||
* Allocate a neighbor array
|
||||
*/
|
||||
static HnswNeighborArray *
|
||||
HnswNeighborArray *
|
||||
HnswInitNeighborArray(int lm, HnswAllocator * allocator)
|
||||
{
|
||||
HnswNeighborArray *a = HnswAlloc(allocator, HNSW_NEIGHBOR_ARRAY_SIZE(lm));
|
||||
@@ -256,6 +251,8 @@ HnswInitElement(char *base, ItemPointer heaptid, int m, double ml, int maxLevel,
|
||||
|
||||
element->level = level;
|
||||
element->deleted = 0;
|
||||
/* Start at one to make it easier to find issues */
|
||||
element->version = 1;
|
||||
|
||||
HnswInitNeighbors(base, element, m, allocator);
|
||||
|
||||
@@ -397,6 +394,33 @@ HnswUpdateMetaPage(Relation index, int updateEntry, HnswElement entryPoint, Bloc
|
||||
UnlockReleaseBuffer(buf);
|
||||
}
|
||||
|
||||
/*
|
||||
* Form index value
|
||||
*/
|
||||
bool
|
||||
HnswFormIndexValue(Datum *out, Datum *values, bool *isnull, const HnswTypeInfo * typeInfo, HnswSupport * support)
|
||||
{
|
||||
/* Detoast once for all calls */
|
||||
Datum value = PointerGetDatum(PG_DETOAST_DATUM(values[0]));
|
||||
|
||||
/* Check value */
|
||||
if (typeInfo->checkValue != NULL)
|
||||
typeInfo->checkValue(DatumGetPointer(value));
|
||||
|
||||
/* Normalize if needed */
|
||||
if (support->normprocinfo != NULL)
|
||||
{
|
||||
if (!HnswCheckNorm(support, value))
|
||||
return false;
|
||||
|
||||
value = HnswNormValue(typeInfo, support->collation, value);
|
||||
}
|
||||
|
||||
*out = value;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Set element tuple, except for neighbor info
|
||||
*/
|
||||
@@ -408,6 +432,7 @@ HnswSetElementTuple(char *base, HnswElementTuple etup, HnswElement element)
|
||||
etup->type = HNSW_ELEMENT_TUPLE_TYPE;
|
||||
etup->level = element->level;
|
||||
etup->deleted = 0;
|
||||
etup->version = element->version;
|
||||
for (int i = 0; i < HNSW_HEAPTIDS; i++)
|
||||
{
|
||||
if (i < element->heaptidsLength)
|
||||
@@ -450,69 +475,7 @@ HnswSetNeighborTuple(char *base, HnswNeighborTuple ntup, HnswElement e, int m)
|
||||
}
|
||||
|
||||
ntup->count = idx;
|
||||
}
|
||||
|
||||
/*
|
||||
* Load neighbors from page
|
||||
*/
|
||||
static void
|
||||
LoadNeighborsFromPage(HnswElement element, Relation index, Page page, int m)
|
||||
{
|
||||
char *base = NULL;
|
||||
|
||||
HnswNeighborTuple ntup = (HnswNeighborTuple) PageGetItem(page, PageGetItemId(page, element->neighborOffno));
|
||||
int neighborCount = (element->level + 2) * m;
|
||||
|
||||
Assert(HnswIsNeighborTuple(ntup));
|
||||
|
||||
HnswInitNeighbors(base, element, m, NULL);
|
||||
|
||||
/* Ensure expected neighbors */
|
||||
if (ntup->count != neighborCount)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < neighborCount; i++)
|
||||
{
|
||||
HnswElement e;
|
||||
int level;
|
||||
HnswCandidate *hc;
|
||||
ItemPointer indextid;
|
||||
HnswNeighborArray *neighbors;
|
||||
|
||||
indextid = &ntup->indextids[i];
|
||||
|
||||
if (!ItemPointerIsValid(indextid))
|
||||
continue;
|
||||
|
||||
e = HnswInitElementFromBlock(ItemPointerGetBlockNumber(indextid), ItemPointerGetOffsetNumber(indextid));
|
||||
|
||||
/* Calculate level based on offset */
|
||||
level = element->level - i / m;
|
||||
if (level < 0)
|
||||
level = 0;
|
||||
|
||||
neighbors = HnswGetNeighbors(base, element, level);
|
||||
hc = &neighbors->items[neighbors->length++];
|
||||
HnswPtrStore(base, hc->element, e);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Load neighbors
|
||||
*/
|
||||
void
|
||||
HnswLoadNeighbors(HnswElement element, Relation index, int m)
|
||||
{
|
||||
Buffer buf;
|
||||
Page page;
|
||||
|
||||
buf = ReadBuffer(index, element->neighborPage);
|
||||
LockBuffer(buf, BUFFER_LOCK_SHARE);
|
||||
page = BufferGetPage(buf);
|
||||
|
||||
LoadNeighborsFromPage(element, index, page, m);
|
||||
|
||||
UnlockReleaseBuffer(buf);
|
||||
ntup->version = e->version;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -523,6 +486,7 @@ HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool loadHe
|
||||
{
|
||||
element->level = etup->level;
|
||||
element->deleted = etup->deleted;
|
||||
element->version = etup->version;
|
||||
element->neighborPage = ItemPointerGetBlockNumber(&etup->neighbortid);
|
||||
element->neighborOffno = ItemPointerGetOffsetNumber(&etup->neighbortid);
|
||||
element->heaptidsLength = 0;
|
||||
@@ -548,11 +512,20 @@ HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool loadHe
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Calculate the distance between values
|
||||
*/
|
||||
static inline double
|
||||
HnswGetDistance(Datum a, Datum b, HnswSupport * support)
|
||||
{
|
||||
return DatumGetFloat8(FunctionCall2Coll(support->procinfo, support->collation, a, b));
|
||||
}
|
||||
|
||||
/*
|
||||
* Load an element and optionally get its distance from q
|
||||
*/
|
||||
static void
|
||||
HnswLoadElementImpl(BlockNumber blkno, OffsetNumber offno, float *distance, Datum *q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec, float *maxDistance, HnswElement * element)
|
||||
HnswLoadElementImpl(BlockNumber blkno, OffsetNumber offno, double *distance, HnswQuery * q, Relation index, HnswSupport * support, bool loadVec, double *maxDistance, HnswElement * element)
|
||||
{
|
||||
Buffer buf;
|
||||
Page page;
|
||||
@@ -570,10 +543,10 @@ HnswLoadElementImpl(BlockNumber blkno, OffsetNumber offno, float *distance, Datu
|
||||
/* Calculate distance */
|
||||
if (distance != NULL)
|
||||
{
|
||||
if (DatumGetPointer(*q) == NULL)
|
||||
if (DatumGetPointer(q->value) == NULL)
|
||||
*distance = 0;
|
||||
else
|
||||
*distance = (float) DatumGetFloat8(FunctionCall2Coll(procinfo, collation, *q, PointerGetDatum(&etup->data)));
|
||||
*distance = HnswGetDistance(q->value, PointerGetDatum(&etup->data), support);
|
||||
}
|
||||
|
||||
/* Load element */
|
||||
@@ -592,40 +565,51 @@ HnswLoadElementImpl(BlockNumber blkno, OffsetNumber offno, float *distance, Datu
|
||||
* Load an element and optionally get its distance from q
|
||||
*/
|
||||
void
|
||||
HnswLoadElement(HnswElement element, float *distance, Datum *q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec, float *maxDistance)
|
||||
HnswLoadElement(HnswElement element, double *distance, HnswQuery * q, Relation index, HnswSupport * support, bool loadVec, double *maxDistance)
|
||||
{
|
||||
HnswLoadElementImpl(element->blkno, element->offno, distance, q, index, procinfo, collation, loadVec, maxDistance, &element);
|
||||
HnswLoadElementImpl(element->blkno, element->offno, distance, q, index, support, loadVec, maxDistance, &element);
|
||||
}
|
||||
|
||||
/*
|
||||
* Get the distance for an element
|
||||
*/
|
||||
static float
|
||||
GetElementDistance(char *base, HnswElement element, Datum q, FmgrInfo *procinfo, Oid collation)
|
||||
static double
|
||||
GetElementDistance(char *base, HnswElement element, HnswQuery * q, HnswSupport * support)
|
||||
{
|
||||
Datum value = HnswGetValue(base, element);
|
||||
|
||||
return DatumGetFloat8(FunctionCall2Coll(procinfo, collation, q, value));
|
||||
return HnswGetDistance(q->value, value, support);
|
||||
}
|
||||
|
||||
/*
|
||||
* Allocate a search candidate
|
||||
*/
|
||||
static HnswSearchCandidate *
|
||||
HnswInitSearchCandidate(char *base, HnswElement element, double distance)
|
||||
{
|
||||
HnswSearchCandidate *sc = palloc(sizeof(HnswSearchCandidate));
|
||||
|
||||
HnswPtrStore(base, sc->element, element);
|
||||
sc->distance = distance;
|
||||
return sc;
|
||||
}
|
||||
|
||||
/*
|
||||
* Create a candidate for the entry point
|
||||
*/
|
||||
HnswSearchCandidate *
|
||||
HnswEntryCandidate(char *base, HnswElement entryPoint, Datum q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec)
|
||||
HnswEntryCandidate(char *base, HnswElement entryPoint, HnswQuery * q, Relation index, HnswSupport * support, bool loadVec)
|
||||
{
|
||||
HnswSearchCandidate *hc = palloc(sizeof(HnswSearchCandidate));
|
||||
bool inMemory = index == NULL;
|
||||
double distance;
|
||||
|
||||
HnswPtrStore(base, hc->element, entryPoint);
|
||||
if (index == NULL)
|
||||
hc->distance = GetElementDistance(base, entryPoint, q, procinfo, collation);
|
||||
if (inMemory)
|
||||
distance = GetElementDistance(base, entryPoint, q, support);
|
||||
else
|
||||
HnswLoadElement(entryPoint, &hc->distance, &q, index, procinfo, collation, loadVec, NULL);
|
||||
return hc;
|
||||
}
|
||||
HnswLoadElement(entryPoint, &distance, q, index, support, loadVec, NULL);
|
||||
|
||||
#define HnswGetSearchCandidate(membername, ptr) pairingheap_container(HnswSearchCandidate, membername, ptr)
|
||||
#define HnswGetSearchCandidateConst(membername, ptr) pairingheap_const_container(HnswSearchCandidate, membername, ptr)
|
||||
return HnswInitSearchCandidate(base, entryPoint, distance);
|
||||
}
|
||||
|
||||
/*
|
||||
* Compare candidate distances
|
||||
@@ -642,6 +626,21 @@ CompareNearestCandidates(const pairingheap_node *a, const pairingheap_node *b, v
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Compare discarded candidate distances
|
||||
*/
|
||||
static int
|
||||
CompareNearestDiscardedCandidates(const pairingheap_node *a, const pairingheap_node *b, void *arg)
|
||||
{
|
||||
if (HnswGetSearchCandidateConst(w_node, a)->distance < HnswGetSearchCandidateConst(w_node, b)->distance)
|
||||
return 1;
|
||||
|
||||
if (HnswGetSearchCandidateConst(w_node, a)->distance > HnswGetSearchCandidateConst(w_node, b)->distance)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Compare candidate distances
|
||||
*/
|
||||
@@ -661,9 +660,9 @@ CompareFurthestCandidates(const pairingheap_node *a, const pairingheap_node *b,
|
||||
* Init visited
|
||||
*/
|
||||
static inline void
|
||||
InitVisited(char *base, visited_hash * v, Relation index, int ef, int m)
|
||||
InitVisited(char *base, visited_hash * v, bool inMemory, int ef, int m)
|
||||
{
|
||||
if (index != NULL)
|
||||
if (!inMemory)
|
||||
v->tids = tidhash_create(CurrentMemoryContext, ef * m * 2, NULL);
|
||||
else if (base != NULL)
|
||||
v->offsets = offsethash_create(CurrentMemoryContext, ef * m * 2, NULL);
|
||||
@@ -675,9 +674,9 @@ InitVisited(char *base, visited_hash * v, Relation index, int ef, int m)
|
||||
* Add to visited
|
||||
*/
|
||||
static inline void
|
||||
AddToVisited(char *base, visited_hash * v, HnswElementPtr elementPtr, Relation index, bool *found)
|
||||
AddToVisited(char *base, visited_hash * v, HnswElementPtr elementPtr, bool inMemory, bool *found)
|
||||
{
|
||||
if (index != NULL)
|
||||
if (!inMemory)
|
||||
{
|
||||
HnswElement element = HnswPtrAccess(base, elementPtr);
|
||||
ItemPointerData indextid;
|
||||
@@ -738,39 +737,61 @@ HnswLoadUnvisitedFromMemory(char *base, HnswElement element, HnswUnvisited * unv
|
||||
HnswCandidate *hc = &localNeighborhood->items[i];
|
||||
bool found;
|
||||
|
||||
AddToVisited(base, v, hc->element, NULL, &found);
|
||||
AddToVisited(base, v, hc->element, true, &found);
|
||||
|
||||
if (!found)
|
||||
unvisited[(*unvisitedLength)++].element = HnswPtrAccess(base, hc->element);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Load neighbor index TIDs
|
||||
*/
|
||||
bool
|
||||
HnswLoadNeighborTids(HnswElement element, ItemPointerData *indextids, Relation index, int m, int lm, int lc)
|
||||
{
|
||||
Buffer buf;
|
||||
Page page;
|
||||
HnswNeighborTuple ntup;
|
||||
int start;
|
||||
|
||||
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 false;
|
||||
}
|
||||
|
||||
/* Copy to minimize lock time */
|
||||
start = (element->level - lc) * m;
|
||||
memcpy(indextids, ntup->indextids + start, lm * sizeof(ItemPointerData));
|
||||
|
||||
UnlockReleaseBuffer(buf);
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Load unvisited neighbors from disk
|
||||
*/
|
||||
static void
|
||||
HnswLoadUnvisitedFromDisk(HnswElement element, HnswUnvisited * unvisited, int *unvisitedLength, visited_hash * v, Relation index, int m, int lm, int lc)
|
||||
{
|
||||
Buffer buf;
|
||||
Page page;
|
||||
HnswNeighborTuple ntup;
|
||||
int start;
|
||||
ItemPointerData indextids[HNSW_MAX_M * 2];
|
||||
|
||||
buf = ReadBuffer(index, element->neighborPage);
|
||||
LockBuffer(buf, BUFFER_LOCK_SHARE);
|
||||
page = BufferGetPage(buf);
|
||||
|
||||
ntup = (HnswNeighborTuple) PageGetItem(page, PageGetItemId(page, element->neighborOffno));
|
||||
start = (element->level - lc) * m;
|
||||
|
||||
/* Copy to minimize lock time */
|
||||
memcpy(&indextids, ntup->indextids + start, lm * sizeof(ItemPointerData));
|
||||
|
||||
UnlockReleaseBuffer(buf);
|
||||
|
||||
*unvisitedLength = 0;
|
||||
|
||||
if (!HnswLoadNeighborTids(element, indextids, index, m, lm, lc))
|
||||
return;
|
||||
|
||||
for (int i = 0; i < lm; i++)
|
||||
{
|
||||
ItemPointer indextid = &indextids[i];
|
||||
@@ -790,24 +811,37 @@ HnswLoadUnvisitedFromDisk(HnswElement element, HnswUnvisited * unvisited, int *u
|
||||
* Algorithm 2 from paper
|
||||
*/
|
||||
List *
|
||||
HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, FmgrInfo *procinfo, Oid collation, int m, bool inserting, HnswElement skipElement)
|
||||
HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation index, HnswSupport * support, int m, bool inserting, HnswElement skipElement, visited_hash * v, pairingheap **discarded, bool initVisited, int64 *tuples)
|
||||
{
|
||||
List *w = NIL;
|
||||
pairingheap *C = pairingheap_allocate(CompareNearestCandidates, NULL);
|
||||
pairingheap *W = pairingheap_allocate(CompareFurthestCandidates, NULL);
|
||||
int wlen = 0;
|
||||
visited_hash v;
|
||||
visited_hash vh;
|
||||
ListCell *lc2;
|
||||
HnswNeighborArray *localNeighborhood = NULL;
|
||||
Size neighborhoodSize = 0;
|
||||
int lm = HnswGetLayerM(m, lc);
|
||||
HnswUnvisited *unvisited = palloc(lm * sizeof(HnswUnvisited));
|
||||
int unvisitedLength;
|
||||
bool inMemory = index == NULL;
|
||||
|
||||
InitVisited(base, &v, index, ef, m);
|
||||
if (v == NULL)
|
||||
{
|
||||
v = &vh;
|
||||
initVisited = true;
|
||||
}
|
||||
|
||||
if (initVisited)
|
||||
{
|
||||
InitVisited(base, v, inMemory, ef, m);
|
||||
|
||||
if (discarded != NULL)
|
||||
*discarded = pairingheap_allocate(CompareNearestDiscardedCandidates, NULL);
|
||||
}
|
||||
|
||||
/* Create local memory for neighborhood if needed */
|
||||
if (index == NULL)
|
||||
if (inMemory)
|
||||
{
|
||||
neighborhoodSize = HNSW_NEIGHBOR_ARRAY_SIZE(lm);
|
||||
localNeighborhood = palloc(neighborhoodSize);
|
||||
@@ -816,20 +850,27 @@ HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, F
|
||||
/* Add entry points to v, C, and W */
|
||||
foreach(lc2, ep)
|
||||
{
|
||||
HnswSearchCandidate *hc = (HnswSearchCandidate *) lfirst(lc2);
|
||||
HnswSearchCandidate *sc = (HnswSearchCandidate *) lfirst(lc2);
|
||||
bool found;
|
||||
|
||||
AddToVisited(base, &v, hc->element, index, &found);
|
||||
if (initVisited)
|
||||
{
|
||||
AddToVisited(base, v, sc->element, inMemory, &found);
|
||||
|
||||
pairingheap_add(C, &hc->c_node);
|
||||
pairingheap_add(W, &hc->w_node);
|
||||
/* OK to count elements instead of tuples */
|
||||
if (tuples != NULL)
|
||||
(*tuples)++;
|
||||
}
|
||||
|
||||
pairingheap_add(C, &sc->c_node);
|
||||
pairingheap_add(W, &sc->w_node);
|
||||
|
||||
/*
|
||||
* Do not count elements being deleted towards ef when vacuuming. It
|
||||
* would be ideal to do this for inserts as well, but this could
|
||||
* affect insert performance.
|
||||
*/
|
||||
if (CountElement(skipElement, HnswPtrAccess(base, hc->element)))
|
||||
if (CountElement(skipElement, HnswPtrAccess(base, sc->element)))
|
||||
wlen++;
|
||||
}
|
||||
|
||||
@@ -844,24 +885,28 @@ HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, F
|
||||
|
||||
cElement = HnswPtrAccess(base, c->element);
|
||||
|
||||
if (index == NULL)
|
||||
HnswLoadUnvisitedFromMemory(base, cElement, unvisited, &unvisitedLength, &v, lc, localNeighborhood, neighborhoodSize);
|
||||
if (inMemory)
|
||||
HnswLoadUnvisitedFromMemory(base, cElement, unvisited, &unvisitedLength, v, lc, localNeighborhood, neighborhoodSize);
|
||||
else
|
||||
HnswLoadUnvisitedFromDisk(cElement, unvisited, &unvisitedLength, &v, index, m, lm, lc);
|
||||
HnswLoadUnvisitedFromDisk(cElement, unvisited, &unvisitedLength, v, index, m, lm, lc);
|
||||
|
||||
/* OK to count elements instead of tuples */
|
||||
if (tuples != NULL)
|
||||
(*tuples) += unvisitedLength;
|
||||
|
||||
for (int i = 0; i < unvisitedLength; i++)
|
||||
{
|
||||
HnswElement eElement;
|
||||
HnswSearchCandidate *e;
|
||||
float eDistance;
|
||||
double eDistance;
|
||||
bool alwaysAdd = wlen < ef;
|
||||
|
||||
f = HnswGetSearchCandidate(w_node, pairingheap_first(W));
|
||||
|
||||
if (index == NULL)
|
||||
if (inMemory)
|
||||
{
|
||||
eElement = unvisited[i].element;
|
||||
eDistance = GetElementDistance(base, eElement, q, procinfo, collation);
|
||||
eDistance = GetElementDistance(base, eElement, q, support);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -871,25 +916,30 @@ HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, F
|
||||
|
||||
/* Avoid any allocations if not adding */
|
||||
eElement = NULL;
|
||||
HnswLoadElementImpl(blkno, offno, &eDistance, &q, index, procinfo, collation, inserting, alwaysAdd ? NULL : &f->distance, &eElement);
|
||||
HnswLoadElementImpl(blkno, offno, &eDistance, q, index, support, inserting, alwaysAdd || discarded != NULL ? NULL : &f->distance, &eElement);
|
||||
|
||||
if (eElement == NULL)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!(eDistance < f->distance || alwaysAdd))
|
||||
continue;
|
||||
if (eElement == NULL || !(eDistance < f->distance || alwaysAdd))
|
||||
{
|
||||
if (discarded != NULL)
|
||||
{
|
||||
/* Create a new candidate */
|
||||
e = HnswInitSearchCandidate(base, eElement, eDistance);
|
||||
pairingheap_add(*discarded, &e->w_node);
|
||||
}
|
||||
|
||||
Assert(!eElement->deleted);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Make robust to issues */
|
||||
if (eElement->level < lc)
|
||||
continue;
|
||||
|
||||
/* Create a new candidate */
|
||||
e = palloc(sizeof(HnswSearchCandidate));
|
||||
HnswPtrStore(base, e->element, eElement);
|
||||
e->distance = eDistance;
|
||||
e = HnswInitSearchCandidate(base, eElement, eDistance);
|
||||
pairingheap_add(C, &e->c_node);
|
||||
pairingheap_add(W, &e->w_node);
|
||||
|
||||
@@ -904,7 +954,12 @@ HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, F
|
||||
|
||||
/* No need to decrement wlen */
|
||||
if (wlen > ef)
|
||||
pairingheap_remove_first(W);
|
||||
{
|
||||
HnswSearchCandidate *d = HnswGetSearchCandidate(w_node, pairingheap_remove_first(W));
|
||||
|
||||
if (discarded != NULL)
|
||||
pairingheap_add(*discarded, &d->w_node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -912,9 +967,9 @@ HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, F
|
||||
/* Add each element of W to w */
|
||||
while (!pairingheap_is_empty(W))
|
||||
{
|
||||
HnswSearchCandidate *hc = HnswGetSearchCandidate(w_node, pairingheap_remove_first(W));
|
||||
HnswSearchCandidate *sc = HnswGetSearchCandidate(w_node, pairingheap_remove_first(W));
|
||||
|
||||
w = lappend(w, hc);
|
||||
w = lappend(w, sc);
|
||||
}
|
||||
|
||||
return w;
|
||||
@@ -968,32 +1023,22 @@ CompareCandidateDistancesOffset(const ListCell *a, const ListCell *b)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Calculate the distance between elements
|
||||
*/
|
||||
static float
|
||||
HnswGetDistance(char *base, HnswElement a, HnswElement b, FmgrInfo *procinfo, Oid collation)
|
||||
{
|
||||
Datum aValue = HnswGetValue(base, a);
|
||||
Datum bValue = HnswGetValue(base, b);
|
||||
|
||||
return DatumGetFloat8(FunctionCall2Coll(procinfo, collation, aValue, bValue));
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if an element is closer to q than any element from R
|
||||
*/
|
||||
static bool
|
||||
CheckElementCloser(char *base, HnswCandidate * e, List *r, FmgrInfo *procinfo, Oid collation)
|
||||
CheckElementCloser(char *base, HnswCandidate * e, List *r, HnswSupport * support)
|
||||
{
|
||||
HnswElement eElement = HnswPtrAccess(base, e->element);
|
||||
Datum eValue = HnswGetValue(base, eElement);
|
||||
ListCell *lc2;
|
||||
|
||||
foreach(lc2, r)
|
||||
{
|
||||
HnswCandidate *ri = lfirst(lc2);
|
||||
HnswElement riElement = HnswPtrAccess(base, ri->element);
|
||||
float distance = HnswGetDistance(base, eElement, riElement, procinfo, collation);
|
||||
Datum riValue = HnswGetValue(base, riElement);
|
||||
float distance = HnswGetDistance(eValue, riValue, support);
|
||||
|
||||
if (distance <= e->distance)
|
||||
return false;
|
||||
@@ -1006,15 +1051,14 @@ CheckElementCloser(char *base, HnswCandidate * e, List *r, FmgrInfo *procinfo, O
|
||||
* Algorithm 4 from paper
|
||||
*/
|
||||
static List *
|
||||
SelectNeighbors(char *base, List *c, int lm, int lc, FmgrInfo *procinfo, Oid collation, HnswElement e2, HnswCandidate * newCandidate, HnswCandidate * *pruned, bool sortCandidates)
|
||||
SelectNeighbors(char *base, List *c, int lm, HnswSupport * support, bool *closerSet, HnswCandidate * newCandidate, HnswCandidate * *pruned, bool sortCandidates)
|
||||
{
|
||||
List *r = NIL;
|
||||
List *w = list_copy(c);
|
||||
HnswCandidate **wd;
|
||||
int wdlen = 0;
|
||||
int wdoff = 0;
|
||||
HnswNeighborArray *neighbors = HnswGetNeighbors(base, e2, lc);
|
||||
bool mustCalculate = !neighbors->closerSet;
|
||||
bool mustCalculate = !(*closerSet);
|
||||
List *added = NIL;
|
||||
bool removedAny = false;
|
||||
|
||||
@@ -1041,7 +1085,7 @@ SelectNeighbors(char *base, List *c, int lm, int lc, FmgrInfo *procinfo, Oid col
|
||||
|
||||
/* Use previous state of r and wd to skip work when possible */
|
||||
if (mustCalculate)
|
||||
e->closer = CheckElementCloser(base, e, r, procinfo, collation);
|
||||
e->closer = CheckElementCloser(base, e, r, support);
|
||||
else if (list_length(added) > 0)
|
||||
{
|
||||
/* Keep Valgrind happy for in-memory, parallel builds */
|
||||
@@ -1054,7 +1098,7 @@ SelectNeighbors(char *base, List *c, int lm, int lc, FmgrInfo *procinfo, Oid col
|
||||
*/
|
||||
if (e->closer)
|
||||
{
|
||||
e->closer = CheckElementCloser(base, e, added, procinfo, collation);
|
||||
e->closer = CheckElementCloser(base, e, added, support);
|
||||
|
||||
if (!e->closer)
|
||||
removedAny = true;
|
||||
@@ -1067,7 +1111,7 @@ SelectNeighbors(char *base, List *c, int lm, int lc, FmgrInfo *procinfo, Oid col
|
||||
*/
|
||||
if (removedAny)
|
||||
{
|
||||
e->closer = CheckElementCloser(base, e, r, procinfo, collation);
|
||||
e->closer = CheckElementCloser(base, e, r, support);
|
||||
if (e->closer)
|
||||
added = lappend(added, e);
|
||||
}
|
||||
@@ -1075,7 +1119,7 @@ SelectNeighbors(char *base, List *c, int lm, int lc, FmgrInfo *procinfo, Oid col
|
||||
}
|
||||
else if (e == newCandidate)
|
||||
{
|
||||
e->closer = CheckElementCloser(base, e, r, procinfo, collation);
|
||||
e->closer = CheckElementCloser(base, e, r, support);
|
||||
if (e->closer)
|
||||
added = lappend(added, e);
|
||||
}
|
||||
@@ -1091,7 +1135,7 @@ SelectNeighbors(char *base, List *c, int lm, int lc, FmgrInfo *procinfo, Oid col
|
||||
}
|
||||
|
||||
/* Cached value can only be used in future if sorted deterministically */
|
||||
neighbors->closerSet = sortCandidates;
|
||||
*closerSet = sortCandidates;
|
||||
|
||||
/* Keep pruned connections */
|
||||
while (wdoff < wdlen && list_length(r) < lm)
|
||||
@@ -1126,18 +1170,16 @@ AddConnections(char *base, HnswElement element, List *neighbors, int lc)
|
||||
* Update connections
|
||||
*/
|
||||
void
|
||||
HnswUpdateConnection(char *base, HnswElement element, HnswCandidate * hc, int lm, int lc, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation)
|
||||
HnswUpdateConnection(char *base, HnswNeighborArray * neighbors, HnswElement newElement, float distance, int lm, int *updateIdx, Relation index, HnswSupport * support)
|
||||
{
|
||||
HnswElement hce = HnswPtrAccess(base, hc->element);
|
||||
HnswNeighborArray *currentNeighbors = HnswGetNeighbors(base, hce, lc);
|
||||
HnswCandidate hc2;
|
||||
HnswCandidate newHc;
|
||||
|
||||
HnswPtrStore(base, hc2.element, element);
|
||||
hc2.distance = hc->distance;
|
||||
HnswPtrStore(base, newHc.element, newElement);
|
||||
newHc.distance = distance;
|
||||
|
||||
if (currentNeighbors->length < lm)
|
||||
if (neighbors->length < lm)
|
||||
{
|
||||
currentNeighbors->items[currentNeighbors->length++] = hc2;
|
||||
neighbors->items[neighbors->length++] = newHc;
|
||||
|
||||
/* Track update */
|
||||
if (updateIdx != NULL)
|
||||
@@ -1146,54 +1188,26 @@ HnswUpdateConnection(char *base, HnswElement element, HnswCandidate * hc, int lm
|
||||
else
|
||||
{
|
||||
/* Shrink connections */
|
||||
List *c = NIL;
|
||||
HnswCandidate *pruned = NULL;
|
||||
|
||||
/* Load elements on insert */
|
||||
if (index != NULL)
|
||||
{
|
||||
Datum q = HnswGetValue(base, hce);
|
||||
/* Add candidates */
|
||||
for (int i = 0; i < neighbors->length; i++)
|
||||
c = lappend(c, &neighbors->items[i]);
|
||||
c = lappend(c, &newHc);
|
||||
|
||||
for (int i = 0; i < currentNeighbors->length; i++)
|
||||
{
|
||||
HnswCandidate *hc3 = ¤tNeighbors->items[i];
|
||||
HnswElement hc3Element = HnswPtrAccess(base, hc3->element);
|
||||
|
||||
if (HnswPtrIsNull(base, hc3Element->value))
|
||||
HnswLoadElement(hc3Element, &hc3->distance, &q, index, procinfo, collation, true, NULL);
|
||||
else
|
||||
hc3->distance = GetElementDistance(base, hc3Element, q, procinfo, collation);
|
||||
|
||||
/* Prune element if being deleted */
|
||||
if (hc3Element->heaptidsLength == 0)
|
||||
{
|
||||
pruned = ¤tNeighbors->items[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
SelectNeighbors(base, c, lm, support, &neighbors->closerSet, &newHc, &pruned, true);
|
||||
|
||||
/* Should not happen */
|
||||
if (pruned == NULL)
|
||||
{
|
||||
List *c = NIL;
|
||||
|
||||
/* Add candidates */
|
||||
for (int i = 0; i < currentNeighbors->length; i++)
|
||||
c = lappend(c, ¤tNeighbors->items[i]);
|
||||
c = lappend(c, &hc2);
|
||||
|
||||
SelectNeighbors(base, c, lm, lc, procinfo, collation, hce, &hc2, &pruned, true);
|
||||
|
||||
/* Should not happen */
|
||||
if (pruned == NULL)
|
||||
return;
|
||||
}
|
||||
return;
|
||||
|
||||
/* Find and replace the pruned element */
|
||||
for (int i = 0; i < currentNeighbors->length; i++)
|
||||
for (int i = 0; i < neighbors->length; i++)
|
||||
{
|
||||
if (HnswPtrEqual(base, currentNeighbors->items[i].element, pruned->element))
|
||||
if (HnswPtrEqual(base, neighbors->items[i].element, pruned->element))
|
||||
{
|
||||
currentNeighbors->items[i] = hc2;
|
||||
neighbors->items[i] = newHc;
|
||||
|
||||
/* Track update */
|
||||
if (updateIdx != NULL)
|
||||
@@ -1253,17 +1267,20 @@ PrecomputeHash(char *base, HnswElement element)
|
||||
* Algorithm 1 from paper
|
||||
*/
|
||||
void
|
||||
HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint, Relation index, FmgrInfo *procinfo, Oid collation, int m, int efConstruction, bool existing)
|
||||
HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint, Relation index, HnswSupport * support, int m, int efConstruction, bool existing)
|
||||
{
|
||||
List *ep;
|
||||
List *w;
|
||||
int level = element->level;
|
||||
int entryLevel;
|
||||
Datum q = HnswGetValue(base, element);
|
||||
HnswQuery q;
|
||||
HnswElement skipElement = existing ? element : NULL;
|
||||
bool inMemory = index == NULL;
|
||||
|
||||
q.value = HnswGetValue(base, element);
|
||||
|
||||
/* Precompute hash */
|
||||
if (index == NULL)
|
||||
if (inMemory)
|
||||
PrecomputeHash(base, element);
|
||||
|
||||
/* No neighbors if no entry point */
|
||||
@@ -1271,13 +1288,13 @@ HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint
|
||||
return;
|
||||
|
||||
/* Get entry point and level */
|
||||
ep = list_make1(HnswEntryCandidate(base, entryPoint, q, index, procinfo, collation, true));
|
||||
ep = list_make1(HnswEntryCandidate(base, entryPoint, &q, index, support, true));
|
||||
entryLevel = entryPoint->level;
|
||||
|
||||
/* 1st phase: greedy search to insert level */
|
||||
for (int lc = entryLevel; lc >= level + 1; lc--)
|
||||
{
|
||||
w = HnswSearchLayer(base, q, ep, 1, lc, index, procinfo, collation, m, true, skipElement);
|
||||
w = HnswSearchLayer(base, &q, ep, 1, lc, index, support, m, true, skipElement, NULL, NULL, true, NULL);
|
||||
ep = w;
|
||||
}
|
||||
|
||||
@@ -1296,7 +1313,7 @@ HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint
|
||||
List *lw = NIL;
|
||||
ListCell *lc2;
|
||||
|
||||
w = HnswSearchLayer(base, q, ep, efConstruction, lc, index, procinfo, collation, m, true, skipElement);
|
||||
w = HnswSearchLayer(base, &q, ep, efConstruction, lc, index, support, m, true, skipElement, NULL, NULL, true, NULL);
|
||||
|
||||
/* Convert search candidates to candidates */
|
||||
foreach(lc2, w)
|
||||
@@ -1312,7 +1329,7 @@ HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint
|
||||
|
||||
/* Elements being deleted or skipped can help with search */
|
||||
/* but should be removed before selecting neighbors */
|
||||
if (index != NULL)
|
||||
if (!inMemory)
|
||||
lw = RemoveElements(base, lw, skipElement);
|
||||
|
||||
/*
|
||||
@@ -1320,7 +1337,7 @@ HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint
|
||||
* sortCandidates to true for in-memory builds to enable closer
|
||||
* caching, but there does not seem to be a difference in performance.
|
||||
*/
|
||||
neighbors = SelectNeighbors(base, lw, lm, lc, procinfo, collation, element, NULL, NULL, false);
|
||||
neighbors = SelectNeighbors(base, lw, lm, support, &HnswGetNeighbors(base, element, lc)->closerSet, NULL, NULL, false);
|
||||
|
||||
AddConnections(base, element, neighbors, lc);
|
||||
|
||||
@@ -1330,7 +1347,6 @@ HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint
|
||||
|
||||
PGDLLEXPORT Datum l2_normalize(PG_FUNCTION_ARGS);
|
||||
PGDLLEXPORT Datum halfvec_l2_normalize(PG_FUNCTION_ARGS);
|
||||
PGDLLEXPORT Datum minivec_l2_normalize(PG_FUNCTION_ARGS);
|
||||
PGDLLEXPORT Datum sparsevec_l2_normalize(PG_FUNCTION_ARGS);
|
||||
|
||||
static void
|
||||
@@ -1379,20 +1395,6 @@ hnsw_halfvec_support(PG_FUNCTION_ARGS)
|
||||
PG_RETURN_POINTER(&typeInfo);
|
||||
};
|
||||
|
||||
FUNCTION_PREFIX PG_FUNCTION_INFO_V1(hnsw_minivec_support);
|
||||
Datum
|
||||
hnsw_minivec_support(PG_FUNCTION_ARGS)
|
||||
{
|
||||
static const HnswTypeInfo typeInfo = {
|
||||
.maxDimensions = HNSW_MAX_DIM * 4,
|
||||
/* Do not normalize to maximize precision */
|
||||
.normalize = NULL,
|
||||
.checkValue = NULL
|
||||
};
|
||||
|
||||
PG_RETURN_POINTER(&typeInfo);
|
||||
};
|
||||
|
||||
FUNCTION_PREFIX PG_FUNCTION_INFO_V1(hnsw_bit_support);
|
||||
Datum
|
||||
hnsw_bit_support(PG_FUNCTION_ARGS)
|
||||
|
||||
@@ -184,13 +184,12 @@ static void
|
||||
RepairGraphElement(HnswVacuumState * vacuumstate, HnswElement element, HnswElement entryPoint)
|
||||
{
|
||||
Relation index = vacuumstate->index;
|
||||
HnswSupport *support = &vacuumstate->support;
|
||||
Buffer buf;
|
||||
Page page;
|
||||
GenericXLogState *state;
|
||||
int m = vacuumstate->m;
|
||||
int efConstruction = vacuumstate->efConstruction;
|
||||
FmgrInfo *procinfo = vacuumstate->procinfo;
|
||||
Oid collation = vacuumstate->collation;
|
||||
BufferAccessStrategy bas = vacuumstate->bas;
|
||||
HnswNeighborTuple ntup = vacuumstate->ntup;
|
||||
Size ntupSize = HNSW_NEIGHBOR_TUPLE_SIZE(element->level, m);
|
||||
@@ -205,7 +204,7 @@ RepairGraphElement(HnswVacuumState * vacuumstate, HnswElement element, HnswEleme
|
||||
element->heaptidsLength = 0;
|
||||
|
||||
/* Find neighbors for element, skipping itself */
|
||||
HnswFindElementNeighbors(base, element, entryPoint, index, procinfo, collation, m, efConstruction, true);
|
||||
HnswFindElementNeighbors(base, element, entryPoint, index, support, m, efConstruction, true);
|
||||
|
||||
/* Zero memory for each element */
|
||||
MemSet(ntup, 0, HNSW_TUPLE_ALLOC_SIZE);
|
||||
@@ -229,7 +228,7 @@ RepairGraphElement(HnswVacuumState * vacuumstate, HnswElement element, HnswEleme
|
||||
UnlockReleaseBuffer(buf);
|
||||
|
||||
/* Update neighbors */
|
||||
HnswUpdateNeighborsOnDisk(index, procinfo, collation, element, m, true, false);
|
||||
HnswUpdateNeighborsOnDisk(index, support, element, m, true, false);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -239,6 +238,7 @@ static void
|
||||
RepairGraphEntryPoint(HnswVacuumState * vacuumstate)
|
||||
{
|
||||
Relation index = vacuumstate->index;
|
||||
HnswSupport *support = &vacuumstate->support;
|
||||
HnswElement highestPoint = &vacuumstate->highestPoint;
|
||||
HnswElement entryPoint;
|
||||
MemoryContext oldCtx = MemoryContextSwitchTo(vacuumstate->tmpCtx);
|
||||
@@ -256,7 +256,7 @@ RepairGraphEntryPoint(HnswVacuumState * vacuumstate)
|
||||
LockPage(index, HNSW_UPDATE_LOCK, ShareLock);
|
||||
|
||||
/* Load element */
|
||||
HnswLoadElement(highestPoint, NULL, NULL, index, vacuumstate->procinfo, vacuumstate->collation, true, NULL);
|
||||
HnswLoadElement(highestPoint, NULL, NULL, index, support, true, NULL);
|
||||
|
||||
/* Repair if needed */
|
||||
if (NeedsUpdated(vacuumstate, highestPoint))
|
||||
@@ -294,7 +294,7 @@ RepairGraphEntryPoint(HnswVacuumState * vacuumstate)
|
||||
* is outdated, this can remove connections at higher levels in
|
||||
* the graph until they are repaired, but this should be fine.
|
||||
*/
|
||||
HnswLoadElement(entryPoint, NULL, NULL, index, vacuumstate->procinfo, vacuumstate->collation, true, NULL);
|
||||
HnswLoadElement(entryPoint, NULL, NULL, index, support, true, NULL);
|
||||
|
||||
if (NeedsUpdated(vacuumstate, entryPoint))
|
||||
{
|
||||
@@ -527,6 +527,14 @@ MarkDeleted(HnswVacuumState * vacuumstate)
|
||||
for (int i = 0; i < ntup->count; i++)
|
||||
ItemPointerSetInvalid(&ntup->indextids[i]);
|
||||
|
||||
/* Increment version */
|
||||
/* This is used to avoid incorrect reads for iterative scans */
|
||||
/* Reserve some bits for future use */
|
||||
etup->version++;
|
||||
if (etup->version > 15)
|
||||
etup->version = 1;
|
||||
ntup->version = etup->version;
|
||||
|
||||
/*
|
||||
* We modified the tuples in place, no need to call
|
||||
* PageIndexTupleOverwrite
|
||||
@@ -573,13 +581,13 @@ InitVacuumState(HnswVacuumState * vacuumstate, IndexVacuumInfo *info, IndexBulkD
|
||||
vacuumstate->callback_state = callback_state;
|
||||
vacuumstate->efConstruction = HnswGetEfConstruction(index);
|
||||
vacuumstate->bas = GetAccessStrategy(BAS_BULKREAD);
|
||||
vacuumstate->procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC);
|
||||
vacuumstate->collation = index->rd_indcollation[0];
|
||||
vacuumstate->ntup = palloc0(HNSW_TUPLE_ALLOC_SIZE);
|
||||
vacuumstate->tmpCtx = AllocSetContextCreate(CurrentMemoryContext,
|
||||
"Hnsw vacuum temporary context",
|
||||
ALLOCSET_DEFAULT_SIZES);
|
||||
|
||||
HnswInitSupport(&vacuumstate->support, index);
|
||||
|
||||
/* Get m from metapage */
|
||||
HnswGetMetaPageInfo(index, &vacuumstate->m, NULL);
|
||||
|
||||
|
||||
@@ -228,11 +228,11 @@ BuildCallback(Relation index, ItemPointer tid, Datum *values,
|
||||
static inline void
|
||||
GetNextTuple(Tuplesortstate *sortstate, TupleDesc tupdesc, TupleTableSlot *slot, IndexTuple *itup, int *list)
|
||||
{
|
||||
Datum value;
|
||||
bool isnull;
|
||||
|
||||
if (tuplesort_gettupleslot(sortstate, true, false, slot, NULL))
|
||||
{
|
||||
Datum value;
|
||||
bool isnull;
|
||||
|
||||
*list = DatumGetInt32(slot_getattr(slot, 1, &isnull));
|
||||
value = slot_getattr(slot, 3, &isnull);
|
||||
|
||||
@@ -254,8 +254,8 @@ InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum)
|
||||
IndexTuple itup = NULL; /* silence compiler warning */
|
||||
int64 inserted = 0;
|
||||
|
||||
TupleTableSlot *slot = MakeSingleTupleTableSlot(buildstate->tupdesc, &TTSOpsMinimalTuple);
|
||||
TupleDesc tupdesc = RelationGetDescr(index);
|
||||
TupleTableSlot *slot = MakeSingleTupleTableSlot(buildstate->sortdesc, &TTSOpsMinimalTuple);
|
||||
TupleDesc tupdesc = buildstate->tupdesc;
|
||||
|
||||
pgstat_progress_update_param(PROGRESS_CREATEIDX_SUBPHASE, PROGRESS_IVFFLAT_PHASE_LOAD);
|
||||
|
||||
@@ -319,6 +319,7 @@ InitBuildState(IvfflatBuildState * buildstate, Relation heap, Relation index, In
|
||||
buildstate->index = index;
|
||||
buildstate->indexInfo = indexInfo;
|
||||
buildstate->typeInfo = IvfflatGetTypeInfo(index);
|
||||
buildstate->tupdesc = RelationGetDescr(index);
|
||||
|
||||
buildstate->lists = IvfflatGetLists(index);
|
||||
buildstate->dimensions = TupleDescAttr(index->rd_att, 0)->atttypmod;
|
||||
@@ -356,12 +357,12 @@ InitBuildState(IvfflatBuildState * buildstate, Relation heap, Relation index, In
|
||||
errmsg("dimensions must be greater than one for this opclass")));
|
||||
|
||||
/* Create tuple description for sorting */
|
||||
buildstate->tupdesc = CreateTemplateTupleDesc(3);
|
||||
TupleDescInitEntry(buildstate->tupdesc, (AttrNumber) 1, "list", INT4OID, -1, 0);
|
||||
TupleDescInitEntry(buildstate->tupdesc, (AttrNumber) 2, "tid", TIDOID, -1, 0);
|
||||
TupleDescInitEntry(buildstate->tupdesc, (AttrNumber) 3, "vector", RelationGetDescr(index)->attrs[0].atttypid, -1, 0);
|
||||
buildstate->sortdesc = CreateTemplateTupleDesc(3);
|
||||
TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 1, "list", INT4OID, -1, 0);
|
||||
TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 2, "tid", TIDOID, -1, 0);
|
||||
TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 3, "vector", buildstate->tupdesc->attrs[0].atttypid, -1, 0);
|
||||
|
||||
buildstate->slot = MakeSingleTupleTableSlot(buildstate->tupdesc, &TTSOpsVirtual);
|
||||
buildstate->slot = MakeSingleTupleTableSlot(buildstate->sortdesc, &TTSOpsVirtual);
|
||||
|
||||
buildstate->centers = VectorArrayInit(buildstate->lists, buildstate->dimensions, buildstate->typeInfo->itemSize(buildstate->dimensions));
|
||||
buildstate->listInfo = palloc(sizeof(ListInfo) * buildstate->lists);
|
||||
@@ -633,7 +634,7 @@ IvfflatParallelScanAndSort(IvfflatSpool * ivfspool, IvfflatShared * ivfshared, S
|
||||
InitBuildState(&buildstate, ivfspool->heap, ivfspool->index, indexInfo);
|
||||
memcpy(buildstate.centers->items, ivfcenters, buildstate.centers->itemsize * buildstate.centers->maxlen);
|
||||
buildstate.centers->length = buildstate.centers->maxlen;
|
||||
ivfspool->sortstate = InitBuildSortState(buildstate.tupdesc, sortmem, coordinate);
|
||||
ivfspool->sortstate = InitBuildSortState(buildstate.sortdesc, sortmem, coordinate);
|
||||
buildstate.sortstate = ivfspool->sortstate;
|
||||
scan = table_beginscan_parallel(ivfspool->heap,
|
||||
ParallelTableScanFromIvfflatShared(ivfshared));
|
||||
@@ -950,7 +951,7 @@ AssignTuples(IvfflatBuildState * buildstate)
|
||||
}
|
||||
|
||||
/* Begin serial/leader tuplesort */
|
||||
buildstate->sortstate = InitBuildSortState(buildstate->tupdesc, maintenance_work_mem, coordinate);
|
||||
buildstate->sortstate = InitBuildSortState(buildstate->sortdesc, maintenance_work_mem, coordinate);
|
||||
|
||||
/* Add tuples to sort */
|
||||
if (buildstate->heap != NULL)
|
||||
|
||||
@@ -17,8 +17,16 @@
|
||||
#endif
|
||||
|
||||
int ivfflat_probes;
|
||||
int ivfflat_iterative_scan;
|
||||
int ivfflat_max_probes;
|
||||
static relopt_kind ivfflat_relopt_kind;
|
||||
|
||||
static const struct config_enum_entry ivfflat_iterative_scan_options[] = {
|
||||
{"off", IVFFLAT_ITERATIVE_SCAN_OFF, false},
|
||||
{"relaxed_order", IVFFLAT_ITERATIVE_SCAN_RELAXED, false},
|
||||
{NULL, 0, false}
|
||||
};
|
||||
|
||||
/*
|
||||
* Initialize index options and variables
|
||||
*/
|
||||
@@ -31,7 +39,16 @@ IvfflatInit(void)
|
||||
|
||||
DefineCustomIntVariable("ivfflat.probes", "Sets the number of probes",
|
||||
"Valid range is 1..lists.", &ivfflat_probes,
|
||||
IVFFLAT_DEFAULT_PROBES, IVFFLAT_MIN_LISTS, IVFFLAT_MAX_LISTS, PGC_USERSET, 0, NULL, NULL, NULL);
|
||||
IVFFLAT_DEFAULT_PROBES, IVFFLAT_MIN_LISTS, IVFFLAT_MAX_LISTS, PGC_USERSET, GUC_EXPLAIN, NULL, NULL, NULL);
|
||||
|
||||
DefineCustomEnumVariable("ivfflat.iterative_scan", "Sets the mode for iterative scans",
|
||||
NULL, &ivfflat_iterative_scan,
|
||||
IVFFLAT_ITERATIVE_SCAN_OFF, ivfflat_iterative_scan_options, PGC_USERSET, GUC_EXPLAIN, NULL, NULL, NULL);
|
||||
|
||||
/* If this is less than probes, probes is used */
|
||||
DefineCustomIntVariable("ivfflat.max_probes", "Sets the max number of probes for iterative scans",
|
||||
NULL, &ivfflat_max_probes,
|
||||
IVFFLAT_MAX_LISTS, IVFFLAT_MIN_LISTS, IVFFLAT_MAX_LISTS, PGC_USERSET, GUC_EXPLAIN, NULL, NULL, NULL);
|
||||
|
||||
MarkGUCPrefixReserved("ivfflat");
|
||||
}
|
||||
@@ -69,6 +86,8 @@ ivfflatcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
|
||||
GenericCosts costs;
|
||||
int lists;
|
||||
double ratio;
|
||||
double sequentialRatio = 0.5;
|
||||
double startupPages;
|
||||
double spc_seq_page_cost;
|
||||
Relation index;
|
||||
|
||||
@@ -80,11 +99,17 @@ ivfflatcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
|
||||
*indexSelectivity = 0;
|
||||
*indexCorrelation = 0;
|
||||
*indexPages = 0;
|
||||
#if PG_VERSION_NUM >= 180000
|
||||
/* See "On disable_cost" thread on pgsql-hackers */
|
||||
path->path.disabled_nodes = 2;
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
MemSet(&costs, 0, sizeof(costs));
|
||||
|
||||
genericcostestimate(root, path, loop_count, &costs);
|
||||
|
||||
index = index_open(path->indexinfo->indexoid, NoLock);
|
||||
IvfflatGetMetaPageInfo(index, &lists, NULL);
|
||||
index_close(index, NoLock);
|
||||
@@ -94,41 +119,26 @@ ivfflatcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
|
||||
if (ratio > 1.0)
|
||||
ratio = 1.0;
|
||||
|
||||
/*
|
||||
* This gives us the subset of tuples to visit. This value is passed into
|
||||
* the generic cost estimator to determine the number of pages to visit
|
||||
* during the index scan.
|
||||
*/
|
||||
costs.numIndexTuples = path->indexinfo->tuples * ratio;
|
||||
|
||||
genericcostestimate(root, path, loop_count, &costs);
|
||||
|
||||
get_tablespace_page_costs(path->indexinfo->reltablespace, NULL, &spc_seq_page_cost);
|
||||
|
||||
/* Change some page cost from random to sequential */
|
||||
costs.indexTotalCost -= sequentialRatio * costs.numIndexPages * (costs.spc_random_page_cost - spc_seq_page_cost);
|
||||
|
||||
/* Startup cost is cost before returning the first row */
|
||||
costs.indexStartupCost = costs.indexTotalCost * ratio;
|
||||
|
||||
/* Adjust cost if needed since TOAST not included in seq scan cost */
|
||||
if (costs.numIndexPages > path->indexinfo->rel->pages && ratio < 0.5)
|
||||
startupPages = costs.numIndexPages * ratio;
|
||||
if (startupPages > path->indexinfo->rel->pages && ratio < 0.5)
|
||||
{
|
||||
/* Change all page cost from random to sequential */
|
||||
costs.indexTotalCost -= costs.numIndexPages * (costs.spc_random_page_cost - spc_seq_page_cost);
|
||||
/* Change rest of page cost from random to sequential */
|
||||
costs.indexStartupCost -= (1 - sequentialRatio) * startupPages * (costs.spc_random_page_cost - spc_seq_page_cost);
|
||||
|
||||
/* Remove cost of extra pages */
|
||||
costs.indexTotalCost -= (costs.numIndexPages - path->indexinfo->rel->pages) * spc_seq_page_cost;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Change some page cost from random to sequential */
|
||||
costs.indexTotalCost -= 0.5 * costs.numIndexPages * (costs.spc_random_page_cost - spc_seq_page_cost);
|
||||
costs.indexStartupCost -= (startupPages - path->indexinfo->rel->pages) * spc_seq_page_cost;
|
||||
}
|
||||
|
||||
/*
|
||||
* If the list selectivity is lower than what is returned from the generic
|
||||
* cost estimator, use that.
|
||||
*/
|
||||
if (ratio < costs.indexSelectivity)
|
||||
costs.indexSelectivity = ratio;
|
||||
|
||||
/* Use total cost since most work happens before first tuple is returned */
|
||||
*indexStartupCost = costs.indexTotalCost;
|
||||
*indexStartupCost = costs.indexStartupCost;
|
||||
*indexTotalCost = costs.indexTotalCost;
|
||||
*indexSelectivity = costs.indexSelectivity;
|
||||
*indexCorrelation = costs.indexCorrelation;
|
||||
|
||||
@@ -80,6 +80,14 @@
|
||||
|
||||
/* Variables */
|
||||
extern int ivfflat_probes;
|
||||
extern int ivfflat_iterative_scan;
|
||||
extern int ivfflat_max_probes;
|
||||
|
||||
typedef enum IvfflatIterativeScanMode
|
||||
{
|
||||
IVFFLAT_ITERATIVE_SCAN_OFF,
|
||||
IVFFLAT_ITERATIVE_SCAN_RELAXED
|
||||
} IvfflatIterativeScanMode;
|
||||
|
||||
typedef struct VectorArrayData
|
||||
{
|
||||
@@ -165,6 +173,7 @@ typedef struct IvfflatBuildState
|
||||
Relation index;
|
||||
IndexInfo *indexInfo;
|
||||
const IvfflatTypeInfo *typeInfo;
|
||||
TupleDesc tupdesc;
|
||||
|
||||
/* Settings */
|
||||
int dimensions;
|
||||
@@ -198,7 +207,7 @@ typedef struct IvfflatBuildState
|
||||
|
||||
/* Sorting */
|
||||
Tuplesortstate *sortstate;
|
||||
TupleDesc tupdesc;
|
||||
TupleDesc sortdesc;
|
||||
TupleTableSlot *slot;
|
||||
|
||||
/* Memory */
|
||||
@@ -247,8 +256,11 @@ typedef struct IvfflatScanOpaqueData
|
||||
{
|
||||
const IvfflatTypeInfo *typeInfo;
|
||||
int probes;
|
||||
int maxProbes;
|
||||
int dimensions;
|
||||
bool first;
|
||||
Datum value;
|
||||
MemoryContext tmpCtx;
|
||||
|
||||
/* Sorting */
|
||||
Tuplesortstate *sortstate;
|
||||
@@ -265,7 +277,9 @@ typedef struct IvfflatScanOpaqueData
|
||||
|
||||
/* Lists */
|
||||
pairingheap *listQueue;
|
||||
IvfflatScanList lists[FLEXIBLE_ARRAY_MEMBER]; /* must come last */
|
||||
BlockNumber *listPages;
|
||||
int listIndex;
|
||||
IvfflatScanList *lists;
|
||||
} IvfflatScanOpaqueData;
|
||||
|
||||
typedef IvfflatScanOpaqueData * IvfflatScanOpaque;
|
||||
|
||||
@@ -98,7 +98,7 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heap_tid, R
|
||||
IvfflatGetMetaPageInfo(index, NULL, NULL);
|
||||
|
||||
/* Find the insert page - sets the page and list info */
|
||||
FindInsertPage(index, values, &insertPage, &listInfo);
|
||||
FindInsertPage(index, &value, &insertPage, &listInfo);
|
||||
Assert(BlockNumberIsValid(insertPage));
|
||||
originalInsertPage = insertPage;
|
||||
|
||||
|
||||
@@ -10,10 +10,7 @@
|
||||
#include "miscadmin.h"
|
||||
#include "pgstat.h"
|
||||
#include "storage/bufmgr.h"
|
||||
|
||||
#ifdef IVFFLAT_MEMORY
|
||||
#include "utils/memutils.h"
|
||||
#endif
|
||||
|
||||
#define GetScanList(ptr) pairingheap_container(IvfflatScanList, ph_node, ptr)
|
||||
#define GetScanListConst(ptr) pairingheap_const_container(IvfflatScanList, ph_node, ptr)
|
||||
@@ -65,7 +62,7 @@ GetScanLists(IndexScanDesc scan, Datum value)
|
||||
/* Use procinfo from the index instead of scan key for performance */
|
||||
distance = DatumGetFloat8(so->distfunc(so->procinfo, so->collation, PointerGetDatum(&list->center), value));
|
||||
|
||||
if (listCount < so->probes)
|
||||
if (listCount < so->maxProbes)
|
||||
{
|
||||
IvfflatScanList *scanlist;
|
||||
|
||||
@@ -78,7 +75,7 @@ GetScanLists(IndexScanDesc scan, Datum value)
|
||||
pairingheap_add(so->listQueue, &scanlist->ph_node);
|
||||
|
||||
/* Calculate max distance */
|
||||
if (listCount == so->probes)
|
||||
if (listCount == so->maxProbes)
|
||||
maxDistance = GetScanList(pairingheap_first(so->listQueue))->distance;
|
||||
}
|
||||
else if (distance < maxDistance)
|
||||
@@ -102,6 +99,11 @@ GetScanLists(IndexScanDesc scan, Datum value)
|
||||
|
||||
UnlockReleaseBuffer(cbuf);
|
||||
}
|
||||
|
||||
for (int i = listCount - 1; i >= 0; i--)
|
||||
so->listPages[i] = GetScanList(pairingheap_remove_first(so->listQueue))->startPage;
|
||||
|
||||
Assert(pairingheap_is_empty(so->listQueue));
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -114,11 +116,14 @@ GetScanItems(IndexScanDesc scan, Datum value)
|
||||
TupleDesc tupdesc = RelationGetDescr(scan->indexRelation);
|
||||
double tuples = 0;
|
||||
TupleTableSlot *slot = so->vslot;
|
||||
int batchProbes = 0;
|
||||
|
||||
tuplesort_reset(so->sortstate);
|
||||
|
||||
/* Search closest probes lists */
|
||||
while (!pairingheap_is_empty(so->listQueue))
|
||||
while (so->listIndex < so->maxProbes && (++batchProbes) <= so->probes)
|
||||
{
|
||||
BlockNumber searchPage = GetScanList(pairingheap_remove_first(so->listQueue))->startPage;
|
||||
BlockNumber searchPage = so->listPages[so->listIndex++];
|
||||
|
||||
/* Search all entry pages for list */
|
||||
while (BlockNumberIsValid(searchPage))
|
||||
@@ -166,13 +171,17 @@ GetScanItems(IndexScanDesc scan, Datum value)
|
||||
}
|
||||
}
|
||||
|
||||
if (tuples < 100)
|
||||
if (tuples < 100 && ivfflat_iterative_scan == IVFFLAT_ITERATIVE_SCAN_OFF)
|
||||
ereport(DEBUG1,
|
||||
(errmsg("index scan found few tuples"),
|
||||
errdetail("Index may have been created with little data."),
|
||||
errhint("Recreate the index and possibly decrease lists.")));
|
||||
|
||||
tuplesort_performsort(so->sortstate);
|
||||
|
||||
#if defined(IVFFLAT_MEMORY)
|
||||
elog(INFO, "memory: %zu MB", MemoryContextMemAllocated(CurrentMemoryContext, true) / (1024 * 1024));
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -209,7 +218,13 @@ GetScanValue(IndexScanDesc scan)
|
||||
|
||||
/* Normalize if needed */
|
||||
if (so->normprocinfo != NULL)
|
||||
{
|
||||
MemoryContext oldCtx = MemoryContextSwitchTo(so->tmpCtx);
|
||||
|
||||
value = IvfflatNormValue(so->typeInfo, so->collation, value);
|
||||
|
||||
MemoryContextSwitchTo(oldCtx);
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
@@ -240,19 +255,30 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys)
|
||||
int lists;
|
||||
int dimensions;
|
||||
int probes = ivfflat_probes;
|
||||
int maxProbes;
|
||||
MemoryContext oldCtx;
|
||||
|
||||
scan = RelationGetIndexScan(index, nkeys, norderbys);
|
||||
|
||||
/* Get lists and dimensions from metapage */
|
||||
IvfflatGetMetaPageInfo(index, &lists, &dimensions);
|
||||
|
||||
if (ivfflat_iterative_scan != IVFFLAT_ITERATIVE_SCAN_OFF)
|
||||
maxProbes = Max(ivfflat_max_probes, probes);
|
||||
else
|
||||
maxProbes = probes;
|
||||
|
||||
if (probes > lists)
|
||||
probes = lists;
|
||||
|
||||
so = (IvfflatScanOpaque) palloc(offsetof(IvfflatScanOpaqueData, lists) + probes * sizeof(IvfflatScanList));
|
||||
if (maxProbes > lists)
|
||||
maxProbes = lists;
|
||||
|
||||
so = (IvfflatScanOpaque) palloc(sizeof(IvfflatScanOpaqueData));
|
||||
so->typeInfo = IvfflatGetTypeInfo(index);
|
||||
so->first = true;
|
||||
so->probes = probes;
|
||||
so->maxProbes = maxProbes;
|
||||
so->dimensions = dimensions;
|
||||
|
||||
/* Set support functions */
|
||||
@@ -260,6 +286,12 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys)
|
||||
so->normprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_NORM_PROC);
|
||||
so->collation = index->rd_indcollation[0];
|
||||
|
||||
so->tmpCtx = AllocSetContextCreate(CurrentMemoryContext,
|
||||
"Ivfflat scan temporary context",
|
||||
ALLOCSET_DEFAULT_SIZES);
|
||||
|
||||
oldCtx = MemoryContextSwitchTo(so->tmpCtx);
|
||||
|
||||
/* Create tuple description for sorting */
|
||||
so->tupdesc = CreateTemplateTupleDesc(2);
|
||||
TupleDescInitEntry(so->tupdesc, (AttrNumber) 1, "distance", FLOAT8OID, -1, 0);
|
||||
@@ -280,6 +312,11 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys)
|
||||
so->bas = GetAccessStrategy(BAS_BULKREAD);
|
||||
|
||||
so->listQueue = pairingheap_allocate(CompareLists, scan);
|
||||
so->listPages = palloc(maxProbes * sizeof(BlockNumber));
|
||||
so->listIndex = 0;
|
||||
so->lists = palloc(maxProbes * sizeof(IvfflatScanList));
|
||||
|
||||
MemoryContextSwitchTo(oldCtx);
|
||||
|
||||
scan->opaque = so;
|
||||
|
||||
@@ -294,11 +331,9 @@ ivfflatrescan(IndexScanDesc scan, ScanKey keys, int nkeys, ScanKey orderbys, int
|
||||
{
|
||||
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
|
||||
|
||||
if (!so->first)
|
||||
tuplesort_reset(so->sortstate);
|
||||
|
||||
so->first = true;
|
||||
pairingheap_reset(so->listQueue);
|
||||
so->listIndex = 0;
|
||||
|
||||
if (keys && scan->numberOfKeys > 0)
|
||||
memmove(scan->keyData, keys, scan->numberOfKeys * sizeof(ScanKeyData));
|
||||
@@ -314,6 +349,8 @@ bool
|
||||
ivfflatgettuple(IndexScanDesc scan, ScanDirection dir)
|
||||
{
|
||||
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
|
||||
ItemPointer heaptid;
|
||||
bool isnull;
|
||||
|
||||
/*
|
||||
* Index can be used to scan backward, but Postgres doesn't support
|
||||
@@ -341,28 +378,23 @@ ivfflatgettuple(IndexScanDesc scan, ScanDirection dir)
|
||||
IvfflatBench("GetScanLists", GetScanLists(scan, value));
|
||||
IvfflatBench("GetScanItems", GetScanItems(scan, value));
|
||||
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 */
|
||||
if (value != scan->orderByData->sk_argument)
|
||||
pfree(DatumGetPointer(value));
|
||||
so->value = value;
|
||||
}
|
||||
|
||||
if (tuplesort_gettupleslot(so->sortstate, true, false, so->mslot, NULL))
|
||||
while (!tuplesort_gettupleslot(so->sortstate, true, false, so->mslot, NULL))
|
||||
{
|
||||
bool isnull;
|
||||
ItemPointer heaptid = (ItemPointer) DatumGetPointer(slot_getattr(so->mslot, 2, &isnull));
|
||||
if (so->listIndex == so->maxProbes)
|
||||
return false;
|
||||
|
||||
scan->xs_heaptid = *heaptid;
|
||||
scan->xs_recheck = false;
|
||||
scan->xs_recheckorderby = false;
|
||||
return true;
|
||||
IvfflatBench("GetScanItems", GetScanItems(scan, so->value));
|
||||
}
|
||||
|
||||
return false;
|
||||
heaptid = (ItemPointer) DatumGetPointer(slot_getattr(so->mslot, 2, &isnull));
|
||||
|
||||
scan->xs_heaptid = *heaptid;
|
||||
scan->xs_recheck = false;
|
||||
scan->xs_recheckorderby = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -373,12 +405,10 @@ ivfflatendscan(IndexScanDesc scan)
|
||||
{
|
||||
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
|
||||
|
||||
pairingheap_free(so->listQueue);
|
||||
/* Free any temporary files */
|
||||
tuplesort_end(so->sortstate);
|
||||
FreeAccessStrategy(so->bas);
|
||||
FreeTupleDesc(so->tupdesc);
|
||||
|
||||
/* TODO Free vslot and mslot without freeing TupleDesc */
|
||||
MemoryContextDelete(so->tmpCtx);
|
||||
|
||||
pfree(so);
|
||||
scan->opaque = NULL;
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include "halfutils.h"
|
||||
#include "halfvec.h"
|
||||
#include "ivfflat.h"
|
||||
#include "minivec.h"
|
||||
#include "storage/bufmgr.h"
|
||||
|
||||
/*
|
||||
@@ -71,9 +70,6 @@ IvfflatOptionalProcInfo(Relation index, uint16 procnum)
|
||||
Datum
|
||||
IvfflatNormValue(const IvfflatTypeInfo * typeInfo, Oid collation, Datum value)
|
||||
{
|
||||
if (!typeInfo->normalize)
|
||||
return value;
|
||||
|
||||
return DirectFunctionCall1Coll(typeInfo->normalize, collation, value);
|
||||
}
|
||||
|
||||
@@ -235,7 +231,6 @@ IvfflatUpdateList(Relation index, ListInfo listInfo,
|
||||
|
||||
PGDLLEXPORT Datum l2_normalize(PG_FUNCTION_ARGS);
|
||||
PGDLLEXPORT Datum halfvec_l2_normalize(PG_FUNCTION_ARGS);
|
||||
PGDLLEXPORT Datum minivec_l2_normalize(PG_FUNCTION_ARGS);
|
||||
PGDLLEXPORT Datum sparsevec_l2_normalize(PG_FUNCTION_ARGS);
|
||||
|
||||
static Size
|
||||
@@ -250,12 +245,6 @@ HalfvecItemSize(int dimensions)
|
||||
return HALFVEC_SIZE(dimensions);
|
||||
}
|
||||
|
||||
static Size
|
||||
MinivecItemSize(int dimensions)
|
||||
{
|
||||
return MINIVEC_SIZE(dimensions);
|
||||
}
|
||||
|
||||
static Size
|
||||
BitItemSize(int dimensions)
|
||||
{
|
||||
@@ -286,18 +275,6 @@ HalfvecUpdateCenter(Pointer v, int dimensions, float *x)
|
||||
vec->x[k] = Float4ToHalfUnchecked(x[k]);
|
||||
}
|
||||
|
||||
static void
|
||||
MinivecUpdateCenter(Pointer v, int dimensions, float *x)
|
||||
{
|
||||
MiniVector *vec = (MiniVector *) v;
|
||||
|
||||
SET_VARSIZE(vec, MINIVEC_SIZE(dimensions));
|
||||
vec->dim = dimensions;
|
||||
|
||||
for (int k = 0; k < dimensions; k++)
|
||||
vec->x[k] = Float4ToFp8Unchecked(x[k]);
|
||||
}
|
||||
|
||||
static void
|
||||
BitUpdateCenter(Pointer v, int dimensions, float *x)
|
||||
{
|
||||
@@ -332,15 +309,6 @@ HalfvecSumCenter(Pointer v, float *x)
|
||||
x[k] += HalfToFloat4(vec->x[k]);
|
||||
}
|
||||
|
||||
static void
|
||||
MinivecSumCenter(Pointer v, float *x)
|
||||
{
|
||||
MiniVector *vec = (MiniVector *) v;
|
||||
|
||||
for (int k = 0; k < vec->dim; k++)
|
||||
x[k] += Fp8ToFloat4(vec->x[k]);
|
||||
}
|
||||
|
||||
static void
|
||||
BitSumCenter(Pointer v, float *x)
|
||||
{
|
||||
@@ -389,22 +357,6 @@ ivfflat_halfvec_support(PG_FUNCTION_ARGS)
|
||||
PG_RETURN_POINTER(&typeInfo);
|
||||
};
|
||||
|
||||
FUNCTION_PREFIX PG_FUNCTION_INFO_V1(ivfflat_minivec_support);
|
||||
Datum
|
||||
ivfflat_minivec_support(PG_FUNCTION_ARGS)
|
||||
{
|
||||
static const IvfflatTypeInfo typeInfo = {
|
||||
.maxDimensions = IVFFLAT_MAX_DIM * 4,
|
||||
/* Do not normalize to maximize precision */
|
||||
.normalize = NULL,
|
||||
.itemSize = MinivecItemSize,
|
||||
.updateCenter = MinivecUpdateCenter,
|
||||
.sumCenter = MinivecSumCenter
|
||||
};
|
||||
|
||||
PG_RETURN_POINTER(&typeInfo);
|
||||
};
|
||||
|
||||
FUNCTION_PREFIX PG_FUNCTION_INFO_V1(ivfflat_bit_support);
|
||||
Datum
|
||||
ivfflat_bit_support(PG_FUNCTION_ARGS)
|
||||
|
||||
@@ -26,7 +26,7 @@ ivfflatbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
|
||||
Page cpage;
|
||||
OffsetNumber coffno;
|
||||
OffsetNumber cmaxoffno;
|
||||
BlockNumber startPages[MaxOffsetNumber];
|
||||
BlockNumber listPages[MaxOffsetNumber];
|
||||
ListInfo listInfo;
|
||||
|
||||
cbuf = ReadBuffer(index, blkno);
|
||||
@@ -40,7 +40,7 @@ ivfflatbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
|
||||
{
|
||||
IvfflatList list = (IvfflatList) PageGetItem(cpage, PageGetItemId(cpage, coffno));
|
||||
|
||||
startPages[coffno - FirstOffsetNumber] = list->startPage;
|
||||
listPages[coffno - FirstOffsetNumber] = list->startPage;
|
||||
}
|
||||
|
||||
listInfo.blkno = blkno;
|
||||
@@ -50,7 +50,7 @@ ivfflatbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
|
||||
|
||||
for (coffno = FirstOffsetNumber; coffno <= cmaxoffno; coffno = OffsetNumberNext(coffno))
|
||||
{
|
||||
BlockNumber searchPage = startPages[coffno - FirstOffsetNumber];
|
||||
BlockNumber searchPage = listPages[coffno - FirstOffsetNumber];
|
||||
BlockNumber insertPage = InvalidBlockNumber;
|
||||
|
||||
/* Iterate over entry pages */
|
||||
|
||||
1051
src/minivec.c
1051
src/minivec.c
File diff suppressed because it is too large
Load Diff
163
src/minivec.h
163
src/minivec.h
@@ -1,163 +0,0 @@
|
||||
#ifndef MINIVEC_H
|
||||
#define MINIVEC_H
|
||||
|
||||
#define MINIVEC_MAX_DIM 16000
|
||||
|
||||
#define fp8 uint8
|
||||
|
||||
#define MINIVEC_SIZE(_dim) (offsetof(MiniVector, x) + sizeof(fp8)*(_dim))
|
||||
#define DatumGetMiniVector(x) ((MiniVector *) PG_DETOAST_DATUM(x))
|
||||
#define PG_GETARG_MINIVEC_P(x) DatumGetMiniVector(PG_GETARG_DATUM(x))
|
||||
#define PG_RETURN_MINIVEC_P(x) PG_RETURN_POINTER(x)
|
||||
|
||||
typedef struct MiniVector
|
||||
{
|
||||
int32 vl_len_; /* varlena header (do not touch directly!) */
|
||||
int16 dim; /* number of dimensions */
|
||||
int16 unused; /* reserved for future use, always zero */
|
||||
fp8 x[FLEXIBLE_ARRAY_MEMBER];
|
||||
} MiniVector;
|
||||
|
||||
MiniVector *InitMiniVector(int dim);
|
||||
|
||||
/*
|
||||
* Check if fp8 is NaN
|
||||
*/
|
||||
static inline bool
|
||||
Fp8IsNan(fp8 num)
|
||||
{
|
||||
return (num & 0x7F) == 0x7F;
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if fp8 is zero
|
||||
*/
|
||||
static inline bool
|
||||
Fp8IsZero(fp8 num)
|
||||
{
|
||||
return num == 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Convert a fp8 to a float4
|
||||
*/
|
||||
static inline float
|
||||
Fp8ToFloat4(fp8 num)
|
||||
{
|
||||
/* Lookup table for non-sign bits */
|
||||
/* Uses uint32 for correctness */
|
||||
uint32 lookup[128] = {0, 989855744, 998244352, 1002438656, 1006632960, 1008730112, 1010827264, 1012924416, 1015021568, 1016070144, 1017118720, 1018167296, 1019215872, 1020264448, 1021313024, 1022361600, 1023410176, 1024458752, 1025507328, 1026555904, 1027604480, 1028653056, 1029701632, 1030750208, 1031798784, 1032847360, 1033895936, 1034944512, 1035993088, 1037041664, 1038090240, 1039138816, 1040187392, 1041235968, 1042284544, 1043333120, 1044381696, 1045430272, 1046478848, 1047527424, 1048576000, 1049624576, 1050673152, 1051721728, 1052770304, 1053818880, 1054867456, 1055916032, 1056964608, 1058013184, 1059061760, 1060110336, 1061158912, 1062207488, 1063256064, 1064304640, 1065353216, 1066401792, 1067450368, 1068498944, 1069547520, 1070596096, 1071644672, 1072693248, 1073741824, 1074790400, 1075838976, 1076887552, 1077936128, 1078984704, 1080033280, 1081081856, 1082130432, 1083179008, 1084227584, 1085276160, 1086324736, 1087373312, 1088421888, 1089470464, 1090519040, 1091567616, 1092616192, 1093664768, 1094713344, 1095761920, 1096810496, 1097859072, 1098907648, 1099956224, 1101004800, 1102053376, 1103101952, 1104150528, 1105199104, 1106247680, 1107296256, 1108344832, 1109393408, 1110441984, 1111490560, 1112539136, 1113587712, 1114636288, 1115684864, 1116733440, 1117782016, 1118830592, 1119879168, 1120927744, 1121976320, 1123024896, 1124073472, 1125122048, 1126170624, 1127219200, 1128267776, 1129316352, 1130364928, 1131413504, 1132462080, 1133510656, 1134559232, 1135607808, 1136656384, 1137704960, 1138753536, 2146435072};
|
||||
|
||||
union
|
||||
{
|
||||
float f;
|
||||
uint32 i;
|
||||
} swap;
|
||||
|
||||
swap.i = lookup[num & 0x7F];
|
||||
|
||||
return (num & 0x80) == 0x80 ? -swap.f : swap.f;
|
||||
}
|
||||
|
||||
/*
|
||||
* Convert a float4 to a fp8
|
||||
*/
|
||||
static inline fp8
|
||||
Float4ToFp8Unchecked(float num)
|
||||
{
|
||||
union
|
||||
{
|
||||
float f;
|
||||
uint32 i;
|
||||
} swap;
|
||||
|
||||
uint32 bin;
|
||||
int exponent;
|
||||
int mantissa;
|
||||
uint8 result;
|
||||
|
||||
swap.f = num;
|
||||
bin = swap.i;
|
||||
exponent = (bin & 0x7F800000) >> 23;
|
||||
mantissa = bin & 0x007FFFFF;
|
||||
|
||||
/* Sign */
|
||||
result = (bin & 0x80000000) >> 24;
|
||||
|
||||
if (isinf(num) || isnan(num))
|
||||
{
|
||||
/* NaN */
|
||||
result |= 0x7F;
|
||||
}
|
||||
else if (exponent > 114)
|
||||
{
|
||||
int m;
|
||||
int gr;
|
||||
int s;
|
||||
|
||||
exponent -= 127;
|
||||
s = mantissa & 0x0007FFFF;
|
||||
|
||||
/* Subnormal */
|
||||
if (exponent < -6)
|
||||
{
|
||||
int diff = -exponent - 6;
|
||||
|
||||
mantissa >>= diff;
|
||||
mantissa += 1 << (23 - diff);
|
||||
s |= mantissa & 0x0007FFFF;
|
||||
}
|
||||
|
||||
m = mantissa >> 20;
|
||||
|
||||
/* Round */
|
||||
gr = (mantissa >> 19) % 4;
|
||||
if (gr == 3 || (gr == 1 && s != 0))
|
||||
m += 1;
|
||||
|
||||
if (m == 8)
|
||||
{
|
||||
m = 0;
|
||||
exponent += 1;
|
||||
}
|
||||
|
||||
if (exponent > 8)
|
||||
{
|
||||
/* Infinite, which is NaN */
|
||||
result |= 0x7F;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (exponent >= -6)
|
||||
result |= (exponent + 7) << 3;
|
||||
|
||||
result |= m;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
* Convert a float4 to a fp8
|
||||
*/
|
||||
static inline fp8
|
||||
Float4ToFp8(float num)
|
||||
{
|
||||
fp8 result = Float4ToFp8Unchecked(num);
|
||||
|
||||
if (unlikely(Fp8IsNan(result)) && !isinf(num))
|
||||
{
|
||||
char *buf = palloc(FLOAT_SHORTEST_DECIMAL_LEN);
|
||||
|
||||
float_to_shortest_decimal_buf(num, buf);
|
||||
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||
errmsg("\"%s\" is out of range for type minivec", buf)));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endif
|
||||
23
src/vector.c
23
src/vector.c
@@ -13,7 +13,6 @@
|
||||
#include "ivfflat.h"
|
||||
#include "lib/stringinfo.h"
|
||||
#include "libpq/pqformat.h"
|
||||
#include "minivec.h"
|
||||
#include "port.h" /* for strtof() */
|
||||
#include "sparsevec.h"
|
||||
#include "utils/array.h"
|
||||
@@ -543,28 +542,6 @@ halfvec_to_vector(PG_FUNCTION_ARGS)
|
||||
PG_RETURN_POINTER(result);
|
||||
}
|
||||
|
||||
/*
|
||||
* Convert fp8 vector to vector
|
||||
*/
|
||||
FUNCTION_PREFIX PG_FUNCTION_INFO_V1(minivec_to_vector);
|
||||
Datum
|
||||
minivec_to_vector(PG_FUNCTION_ARGS)
|
||||
{
|
||||
MiniVector *vec = PG_GETARG_MINIVEC_P(0);
|
||||
int32 typmod = PG_GETARG_INT32(1);
|
||||
Vector *result;
|
||||
|
||||
CheckDim(vec->dim);
|
||||
CheckExpectedDim(typmod, vec->dim);
|
||||
|
||||
result = InitVector(vec->dim);
|
||||
|
||||
for (int i = 0; i < vec->dim; i++)
|
||||
result->x[i] = Fp8ToFloat4(vec->x[i]);
|
||||
|
||||
PG_RETURN_POINTER(result);
|
||||
}
|
||||
|
||||
VECTOR_TARGET_CLONES static float
|
||||
VectorL2SquaredDistance(int dim, float *ax, float *bx)
|
||||
{
|
||||
|
||||
@@ -38,26 +38,6 @@ SELECT * FROM t ORDER BY val;
|
||||
|
||||
(4 rows)
|
||||
|
||||
DROP TABLE t;
|
||||
-- minivec
|
||||
CREATE TABLE t (val minivec(3));
|
||||
INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL);
|
||||
CREATE INDEX ON t (val);
|
||||
SELECT * FROM t WHERE val = '[1,2,3]';
|
||||
val
|
||||
---------
|
||||
[1,2,3]
|
||||
(1 row)
|
||||
|
||||
SELECT * FROM t ORDER BY val;
|
||||
val
|
||||
---------
|
||||
[0,0,0]
|
||||
[1,1,1]
|
||||
[1,2,3]
|
||||
|
||||
(4 rows)
|
||||
|
||||
DROP TABLE t;
|
||||
-- sparsevec
|
||||
CREATE TABLE t (val sparsevec(3));
|
||||
|
||||
@@ -140,64 +140,6 @@ SELECT '{1e-8,-1e-8}'::real[]::halfvec;
|
||||
[0,-0]
|
||||
(1 row)
|
||||
|
||||
SELECT '[1,2,3]'::vector::minivec;
|
||||
minivec
|
||||
---------
|
||||
[1,2,3]
|
||||
(1 row)
|
||||
|
||||
SELECT '[1,2,3]'::vector::minivec(3);
|
||||
minivec
|
||||
---------
|
||||
[1,2,3]
|
||||
(1 row)
|
||||
|
||||
SELECT '[1,2,3]'::vector::minivec(2);
|
||||
ERROR: expected 2 dimensions, not 3
|
||||
SELECT '[465]'::vector::minivec;
|
||||
ERROR: "465" is out of range for type minivec
|
||||
SELECT '[1e-8]'::vector::minivec;
|
||||
minivec
|
||||
---------
|
||||
[0]
|
||||
(1 row)
|
||||
|
||||
SELECT '[1,2,3]'::minivec::vector;
|
||||
vector
|
||||
---------
|
||||
[1,2,3]
|
||||
(1 row)
|
||||
|
||||
SELECT '[1,2,3]'::minivec::vector(3);
|
||||
vector
|
||||
---------
|
||||
[1,2,3]
|
||||
(1 row)
|
||||
|
||||
SELECT '[1,2,3]'::minivec::vector(2);
|
||||
ERROR: expected 2 dimensions, not 3
|
||||
SELECT '{1,2,3}'::real[]::minivec;
|
||||
minivec
|
||||
---------
|
||||
[1,2,3]
|
||||
(1 row)
|
||||
|
||||
SELECT '{1,2,3}'::real[]::minivec(3);
|
||||
minivec
|
||||
---------
|
||||
[1,2,3]
|
||||
(1 row)
|
||||
|
||||
SELECT '{1,2,3}'::real[]::minivec(2);
|
||||
ERROR: expected 2 dimensions, not 3
|
||||
SELECT '{465,-465}'::real[]::minivec;
|
||||
ERROR: "465" is out of range for type minivec
|
||||
SELECT '{1e-8,-1e-8}'::real[]::minivec;
|
||||
minivec
|
||||
---------
|
||||
[0,-0]
|
||||
(1 row)
|
||||
|
||||
SELECT '[0,1.5,0,3.5,0]'::vector::sparsevec;
|
||||
sparsevec
|
||||
-----------------
|
||||
|
||||
@@ -30,23 +30,6 @@ SELECT * FROM t2 ORDER BY val;
|
||||
|
||||
(4 rows)
|
||||
|
||||
DROP TABLE t;
|
||||
DROP TABLE t2;
|
||||
-- minivec
|
||||
CREATE TABLE t (val minivec(3));
|
||||
INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL);
|
||||
CREATE TABLE t2 (val minivec(3));
|
||||
\copy t TO 'results/minivec.bin' WITH (FORMAT binary)
|
||||
\copy t2 FROM 'results/minivec.bin' WITH (FORMAT binary)
|
||||
SELECT * FROM t2 ORDER BY val;
|
||||
val
|
||||
---------
|
||||
[0,0,0]
|
||||
[1,1,1]
|
||||
[1,2,3]
|
||||
|
||||
(4 rows)
|
||||
|
||||
DROP TABLE t;
|
||||
DROP TABLE t2;
|
||||
-- sparsevec
|
||||
|
||||
@@ -99,6 +99,32 @@ SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <+> (SELECT NULL::vector)) t2
|
||||
4
|
||||
(1 row)
|
||||
|
||||
DROP TABLE t;
|
||||
-- iterative
|
||||
CREATE TABLE t (val vector(3));
|
||||
INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL);
|
||||
CREATE INDEX ON t USING hnsw (val vector_l2_ops);
|
||||
SET hnsw.iterative_scan = strict_order;
|
||||
SET hnsw.ef_search = 1;
|
||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
||||
val
|
||||
---------
|
||||
[1,2,3]
|
||||
[1,1,1]
|
||||
[0,0,0]
|
||||
(3 rows)
|
||||
|
||||
SET hnsw.iterative_scan = relaxed_order;
|
||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
||||
val
|
||||
---------
|
||||
[1,2,3]
|
||||
[1,1,1]
|
||||
[0,0,0]
|
||||
(3 rows)
|
||||
|
||||
RESET hnsw.iterative_scan;
|
||||
RESET hnsw.ef_search;
|
||||
DROP TABLE t;
|
||||
-- unlogged
|
||||
CREATE UNLOGGED TABLE t (val vector(3));
|
||||
@@ -139,4 +165,29 @@ SET hnsw.ef_search = 0;
|
||||
ERROR: 0 is outside the valid range for parameter "hnsw.ef_search" (1 .. 1000)
|
||||
SET hnsw.ef_search = 1001;
|
||||
ERROR: 1001 is outside the valid range for parameter "hnsw.ef_search" (1 .. 1000)
|
||||
SHOW hnsw.iterative_scan;
|
||||
hnsw.iterative_scan
|
||||
---------------------
|
||||
off
|
||||
(1 row)
|
||||
|
||||
SET hnsw.iterative_scan = on;
|
||||
ERROR: invalid value for parameter "hnsw.iterative_scan": "on"
|
||||
HINT: Available values: off, relaxed_order, strict_order.
|
||||
SHOW hnsw.max_scan_tuples;
|
||||
hnsw.max_scan_tuples
|
||||
----------------------
|
||||
20000
|
||||
(1 row)
|
||||
|
||||
SET hnsw.max_scan_tuples = 0;
|
||||
ERROR: 0 is outside the valid range for parameter "hnsw.max_scan_tuples" (1 .. 2147483647)
|
||||
SHOW hnsw.scan_mem_multiplier;
|
||||
hnsw.scan_mem_multiplier
|
||||
--------------------------
|
||||
1
|
||||
(1 row)
|
||||
|
||||
SET hnsw.scan_mem_multiplier = 0;
|
||||
ERROR: 0 is outside the valid range for parameter "hnsw.scan_mem_multiplier" (1 .. 1000)
|
||||
DROP TABLE t;
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
SET enable_seqscan = off;
|
||||
-- L2
|
||||
CREATE TABLE t (val minivec(3));
|
||||
INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL);
|
||||
CREATE INDEX ON t USING ivfflat (val minivec_l2_ops) WITH (lists = 1);
|
||||
INSERT INTO t (val) VALUES ('[1,2,4]');
|
||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
||||
val
|
||||
---------
|
||||
[1,2,3]
|
||||
[1,2,4]
|
||||
[1,1,1]
|
||||
[0,0,0]
|
||||
(4 rows)
|
||||
|
||||
SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <-> (SELECT NULL::minivec)) t2;
|
||||
count
|
||||
-------
|
||||
4
|
||||
(1 row)
|
||||
|
||||
SELECT COUNT(*) FROM t;
|
||||
count
|
||||
-------
|
||||
5
|
||||
(1 row)
|
||||
|
||||
TRUNCATE t;
|
||||
NOTICE: ivfflat index created with little data
|
||||
DETAIL: This will cause low recall.
|
||||
HINT: Drop the index until the table has more data.
|
||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
||||
val
|
||||
-----
|
||||
(0 rows)
|
||||
|
||||
DROP TABLE t;
|
||||
-- inner product
|
||||
CREATE TABLE t (val minivec(3));
|
||||
INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL);
|
||||
CREATE INDEX ON t USING ivfflat (val minivec_ip_ops) WITH (lists = 1);
|
||||
INSERT INTO t (val) VALUES ('[1,2,4]');
|
||||
SELECT * FROM t ORDER BY val <#> '[3,3,3]';
|
||||
val
|
||||
---------
|
||||
[1,2,4]
|
||||
[1,2,3]
|
||||
[1,1,1]
|
||||
[0,0,0]
|
||||
(4 rows)
|
||||
|
||||
SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <#> (SELECT NULL::minivec)) t2;
|
||||
count
|
||||
-------
|
||||
4
|
||||
(1 row)
|
||||
|
||||
DROP TABLE t;
|
||||
-- cosine
|
||||
CREATE TABLE t (val minivec(3));
|
||||
INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL);
|
||||
CREATE INDEX ON t USING ivfflat (val minivec_cosine_ops) WITH (lists = 1);
|
||||
INSERT INTO t (val) VALUES ('[1,2,4]');
|
||||
SELECT * FROM t ORDER BY val <=> '[3,3,3]';
|
||||
val
|
||||
---------
|
||||
[1,1,1]
|
||||
[1,2,3]
|
||||
[1,2,4]
|
||||
(3 rows)
|
||||
|
||||
SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <=> '[0,0,0]') t2;
|
||||
count
|
||||
-------
|
||||
3
|
||||
(1 row)
|
||||
|
||||
SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <=> (SELECT NULL::minivec)) t2;
|
||||
count
|
||||
-------
|
||||
3
|
||||
(1 row)
|
||||
|
||||
DROP TABLE t;
|
||||
@@ -81,6 +81,37 @@ SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <=> (SELECT NULL::vector)) t2
|
||||
3
|
||||
(1 row)
|
||||
|
||||
DROP TABLE t;
|
||||
-- iterative
|
||||
CREATE TABLE t (val vector(3));
|
||||
INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL);
|
||||
CREATE INDEX ON t USING ivfflat (val vector_l2_ops) WITH (lists = 3);
|
||||
SET ivfflat.iterative_scan = relaxed_order;
|
||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
||||
val
|
||||
---------
|
||||
[1,2,3]
|
||||
[1,1,1]
|
||||
[0,0,0]
|
||||
(3 rows)
|
||||
|
||||
SET ivfflat.max_probes = 1;
|
||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
||||
val
|
||||
---------
|
||||
[1,2,3]
|
||||
(1 row)
|
||||
|
||||
SET ivfflat.max_probes = 2;
|
||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
||||
val
|
||||
---------
|
||||
[1,2,3]
|
||||
[1,1,1]
|
||||
(2 rows)
|
||||
|
||||
RESET ivfflat.iterative_scan;
|
||||
RESET ivfflat.max_probes;
|
||||
DROP TABLE t;
|
||||
-- unlogged
|
||||
CREATE UNLOGGED TABLE t (val vector(3));
|
||||
@@ -109,4 +140,27 @@ SHOW ivfflat.probes;
|
||||
1
|
||||
(1 row)
|
||||
|
||||
SET ivfflat.probes = 0;
|
||||
ERROR: 0 is outside the valid range for parameter "ivfflat.probes" (1 .. 32768)
|
||||
SET ivfflat.probes = 32769;
|
||||
ERROR: 32769 is outside the valid range for parameter "ivfflat.probes" (1 .. 32768)
|
||||
SHOW ivfflat.iterative_scan;
|
||||
ivfflat.iterative_scan
|
||||
------------------------
|
||||
off
|
||||
(1 row)
|
||||
|
||||
SET ivfflat.iterative_scan = on;
|
||||
ERROR: invalid value for parameter "ivfflat.iterative_scan": "on"
|
||||
HINT: Available values: off, relaxed_order.
|
||||
SHOW ivfflat.max_probes;
|
||||
ivfflat.max_probes
|
||||
--------------------
|
||||
32768
|
||||
(1 row)
|
||||
|
||||
SET ivfflat.max_probes = 0;
|
||||
ERROR: 0 is outside the valid range for parameter "ivfflat.max_probes" (1 .. 32768)
|
||||
SET ivfflat.max_probes = 32769;
|
||||
ERROR: 32769 is outside the valid range for parameter "ivfflat.max_probes" (1 .. 32768)
|
||||
DROP TABLE t;
|
||||
|
||||
@@ -1,588 +0,0 @@
|
||||
SELECT '[1,2,3]'::minivec;
|
||||
minivec
|
||||
---------
|
||||
[1,2,3]
|
||||
(1 row)
|
||||
|
||||
SELECT '[-1,-2,-3]'::minivec;
|
||||
minivec
|
||||
------------
|
||||
[-1,-2,-3]
|
||||
(1 row)
|
||||
|
||||
SELECT '[1.,2.,3.]'::minivec;
|
||||
minivec
|
||||
---------
|
||||
[1,2,3]
|
||||
(1 row)
|
||||
|
||||
SELECT ' [ 1, 2 , 3 ] '::minivec;
|
||||
minivec
|
||||
---------
|
||||
[1,2,3]
|
||||
(1 row)
|
||||
|
||||
SELECT '[1.23456]'::minivec;
|
||||
minivec
|
||||
---------
|
||||
[1.25]
|
||||
(1 row)
|
||||
|
||||
SELECT '[hello,1]'::minivec;
|
||||
ERROR: invalid input syntax for type minivec: "[hello,1]"
|
||||
LINE 1: SELECT '[hello,1]'::minivec;
|
||||
^
|
||||
SELECT '[NaN,1]'::minivec;
|
||||
ERROR: NaN not allowed in minivec
|
||||
LINE 1: SELECT '[NaN,1]'::minivec;
|
||||
^
|
||||
SELECT '[Infinity,1]'::minivec;
|
||||
ERROR: "Infinity" is out of range for type minivec
|
||||
LINE 1: SELECT '[Infinity,1]'::minivec;
|
||||
^
|
||||
SELECT '[-Infinity,1]'::minivec;
|
||||
ERROR: "-Infinity" is out of range for type minivec
|
||||
LINE 1: SELECT '[-Infinity,1]'::minivec;
|
||||
^
|
||||
SELECT '[65519,-65519]'::minivec;
|
||||
ERROR: "65519" is out of range for type minivec
|
||||
LINE 1: SELECT '[65519,-65519]'::minivec;
|
||||
^
|
||||
SELECT '[65520,-65520]'::minivec;
|
||||
ERROR: "65520" is out of range for type minivec
|
||||
LINE 1: SELECT '[65520,-65520]'::minivec;
|
||||
^
|
||||
SELECT '[1e-8,-1e-8]'::minivec;
|
||||
minivec
|
||||
---------
|
||||
[0,-0]
|
||||
(1 row)
|
||||
|
||||
SELECT '[4e38,1]'::minivec;
|
||||
ERROR: "4e38" is out of range for type minivec
|
||||
LINE 1: SELECT '[4e38,1]'::minivec;
|
||||
^
|
||||
SELECT '[1e-46,1]'::minivec;
|
||||
minivec
|
||||
---------
|
||||
[0,1]
|
||||
(1 row)
|
||||
|
||||
SELECT '[1,2,3'::minivec;
|
||||
ERROR: invalid input syntax for type minivec: "[1,2,3"
|
||||
LINE 1: SELECT '[1,2,3'::minivec;
|
||||
^
|
||||
SELECT '[1,2,3]9'::minivec;
|
||||
ERROR: invalid input syntax for type minivec: "[1,2,3]9"
|
||||
LINE 1: SELECT '[1,2,3]9'::minivec;
|
||||
^
|
||||
DETAIL: Junk after closing right brace.
|
||||
SELECT '1,2,3'::minivec;
|
||||
ERROR: invalid input syntax for type minivec: "1,2,3"
|
||||
LINE 1: SELECT '1,2,3'::minivec;
|
||||
^
|
||||
DETAIL: Vector contents must start with "[".
|
||||
SELECT ''::minivec;
|
||||
ERROR: invalid input syntax for type minivec: ""
|
||||
LINE 1: SELECT ''::minivec;
|
||||
^
|
||||
DETAIL: Vector contents must start with "[".
|
||||
SELECT '['::minivec;
|
||||
ERROR: invalid input syntax for type minivec: "["
|
||||
LINE 1: SELECT '['::minivec;
|
||||
^
|
||||
SELECT '[ '::minivec;
|
||||
ERROR: invalid input syntax for type minivec: "[ "
|
||||
LINE 1: SELECT '[ '::minivec;
|
||||
^
|
||||
SELECT '[,'::minivec;
|
||||
ERROR: invalid input syntax for type minivec: "[,"
|
||||
LINE 1: SELECT '[,'::minivec;
|
||||
^
|
||||
SELECT '[]'::minivec;
|
||||
ERROR: minivec must have at least 1 dimension
|
||||
LINE 1: SELECT '[]'::minivec;
|
||||
^
|
||||
SELECT '[ ]'::minivec;
|
||||
ERROR: minivec must have at least 1 dimension
|
||||
LINE 1: SELECT '[ ]'::minivec;
|
||||
^
|
||||
SELECT '[,]'::minivec;
|
||||
ERROR: invalid input syntax for type minivec: "[,]"
|
||||
LINE 1: SELECT '[,]'::minivec;
|
||||
^
|
||||
SELECT '[1,]'::minivec;
|
||||
ERROR: invalid input syntax for type minivec: "[1,]"
|
||||
LINE 1: SELECT '[1,]'::minivec;
|
||||
^
|
||||
SELECT '[1a]'::minivec;
|
||||
ERROR: invalid input syntax for type minivec: "[1a]"
|
||||
LINE 1: SELECT '[1a]'::minivec;
|
||||
^
|
||||
SELECT '[1,,3]'::minivec;
|
||||
ERROR: invalid input syntax for type minivec: "[1,,3]"
|
||||
LINE 1: SELECT '[1,,3]'::minivec;
|
||||
^
|
||||
SELECT '[1, ,3]'::minivec;
|
||||
ERROR: invalid input syntax for type minivec: "[1, ,3]"
|
||||
LINE 1: SELECT '[1, ,3]'::minivec;
|
||||
^
|
||||
SELECT '[1,2,3]'::minivec(3);
|
||||
minivec
|
||||
---------
|
||||
[1,2,3]
|
||||
(1 row)
|
||||
|
||||
SELECT '[1,2,3]'::minivec(2);
|
||||
ERROR: expected 2 dimensions, not 3
|
||||
SELECT '[1,2,3]'::minivec(3, 2);
|
||||
ERROR: invalid type modifier
|
||||
LINE 1: SELECT '[1,2,3]'::minivec(3, 2);
|
||||
^
|
||||
SELECT '[1,2,3]'::minivec('a');
|
||||
ERROR: invalid input syntax for type integer: "a"
|
||||
LINE 1: SELECT '[1,2,3]'::minivec('a');
|
||||
^
|
||||
SELECT '[1,2,3]'::minivec(0);
|
||||
ERROR: dimensions for type minivec must be at least 1
|
||||
LINE 1: SELECT '[1,2,3]'::minivec(0);
|
||||
^
|
||||
SELECT '[1,2,3]'::minivec(16001);
|
||||
ERROR: dimensions for type minivec cannot exceed 16000
|
||||
LINE 1: SELECT '[1,2,3]'::minivec(16001);
|
||||
^
|
||||
SELECT unnest('{"[1,2,3]", "[4,5,6]"}'::minivec[]);
|
||||
unnest
|
||||
---------
|
||||
[1,2,3]
|
||||
[4,5,6]
|
||||
(2 rows)
|
||||
|
||||
SELECT '{"[1,2,3]"}'::minivec(2)[];
|
||||
ERROR: expected 2 dimensions, not 3
|
||||
SELECT '[1,2,3]'::minivec + '[4,5,6]';
|
||||
?column?
|
||||
----------
|
||||
[5,7,9]
|
||||
(1 row)
|
||||
|
||||
SELECT '[448]'::minivec + '[448]';
|
||||
ERROR: value out of range: overflow
|
||||
SELECT '[1,2]'::minivec + '[3]';
|
||||
ERROR: different minivec dimensions 2 and 1
|
||||
SELECT '[1,2,3]'::minivec - '[4,5,6]';
|
||||
?column?
|
||||
------------
|
||||
[-3,-3,-3]
|
||||
(1 row)
|
||||
|
||||
SELECT '[-448]'::minivec - '[448]';
|
||||
ERROR: value out of range: overflow
|
||||
SELECT '[1,2]'::minivec - '[3]';
|
||||
ERROR: different minivec dimensions 2 and 1
|
||||
SELECT '[1,2,3]'::minivec * '[4,5,6]';
|
||||
?column?
|
||||
-----------
|
||||
[4,10,18]
|
||||
(1 row)
|
||||
|
||||
SELECT '[448]'::minivec * '[448]';
|
||||
ERROR: value out of range: overflow
|
||||
SELECT '[1e-7]'::minivec * '[1e-7]';
|
||||
?column?
|
||||
----------
|
||||
[0]
|
||||
(1 row)
|
||||
|
||||
SELECT '[1,2]'::minivec * '[3]';
|
||||
ERROR: different minivec dimensions 2 and 1
|
||||
SELECT '[1,2,3]'::minivec || '[4,5]';
|
||||
?column?
|
||||
-------------
|
||||
[1,2,3,4,5]
|
||||
(1 row)
|
||||
|
||||
SELECT array_fill(0, ARRAY[16000])::minivec || '[1]';
|
||||
ERROR: minivec cannot have more than 16000 dimensions
|
||||
SELECT '[1,2,3]'::minivec < '[1,2,3]';
|
||||
?column?
|
||||
----------
|
||||
f
|
||||
(1 row)
|
||||
|
||||
SELECT '[1,2,3]'::minivec < '[1,2]';
|
||||
?column?
|
||||
----------
|
||||
f
|
||||
(1 row)
|
||||
|
||||
SELECT '[1,2,3]'::minivec <= '[1,2,3]';
|
||||
?column?
|
||||
----------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
SELECT '[1,2,3]'::minivec <= '[1,2]';
|
||||
?column?
|
||||
----------
|
||||
f
|
||||
(1 row)
|
||||
|
||||
SELECT '[1,2,3]'::minivec = '[1,2,3]';
|
||||
?column?
|
||||
----------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
SELECT '[1,2,3]'::minivec = '[1,2]';
|
||||
?column?
|
||||
----------
|
||||
f
|
||||
(1 row)
|
||||
|
||||
SELECT '[1,2,3]'::minivec != '[1,2,3]';
|
||||
?column?
|
||||
----------
|
||||
f
|
||||
(1 row)
|
||||
|
||||
SELECT '[1,2,3]'::minivec != '[1,2]';
|
||||
?column?
|
||||
----------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
SELECT '[1,2,3]'::minivec >= '[1,2,3]';
|
||||
?column?
|
||||
----------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
SELECT '[1,2,3]'::minivec >= '[1,2]';
|
||||
?column?
|
||||
----------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
SELECT '[1,2,3]'::minivec > '[1,2,3]';
|
||||
?column?
|
||||
----------
|
||||
f
|
||||
(1 row)
|
||||
|
||||
SELECT '[1,2,3]'::minivec > '[1,2]';
|
||||
?column?
|
||||
----------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
SELECT minivec_cmp('[1,2,3]', '[1,2,3]');
|
||||
minivec_cmp
|
||||
-------------
|
||||
0
|
||||
(1 row)
|
||||
|
||||
SELECT minivec_cmp('[1,2,3]', '[0,0,0]');
|
||||
minivec_cmp
|
||||
-------------
|
||||
1
|
||||
(1 row)
|
||||
|
||||
SELECT minivec_cmp('[0,0,0]', '[1,2,3]');
|
||||
minivec_cmp
|
||||
-------------
|
||||
-1
|
||||
(1 row)
|
||||
|
||||
SELECT minivec_cmp('[1,2]', '[1,2,3]');
|
||||
minivec_cmp
|
||||
-------------
|
||||
-1
|
||||
(1 row)
|
||||
|
||||
SELECT minivec_cmp('[1,2,3]', '[1,2]');
|
||||
minivec_cmp
|
||||
-------------
|
||||
1
|
||||
(1 row)
|
||||
|
||||
SELECT minivec_cmp('[1,2]', '[2,3,4]');
|
||||
minivec_cmp
|
||||
-------------
|
||||
-1
|
||||
(1 row)
|
||||
|
||||
SELECT minivec_cmp('[2,3]', '[1,2,3]');
|
||||
minivec_cmp
|
||||
-------------
|
||||
1
|
||||
(1 row)
|
||||
|
||||
SELECT vector_dims('[1,2,3]'::minivec);
|
||||
vector_dims
|
||||
-------------
|
||||
3
|
||||
(1 row)
|
||||
|
||||
SELECT round(l2_norm('[1,1]'::minivec)::numeric, 5);
|
||||
round
|
||||
---------
|
||||
1.41421
|
||||
(1 row)
|
||||
|
||||
SELECT l2_norm('[3,4]'::minivec);
|
||||
l2_norm
|
||||
---------
|
||||
5
|
||||
(1 row)
|
||||
|
||||
SELECT l2_norm('[0,1]'::minivec);
|
||||
l2_norm
|
||||
---------
|
||||
1
|
||||
(1 row)
|
||||
|
||||
SELECT l2_norm('[0,0]'::minivec);
|
||||
l2_norm
|
||||
---------
|
||||
0
|
||||
(1 row)
|
||||
|
||||
SELECT l2_norm('[2]'::minivec);
|
||||
l2_norm
|
||||
---------
|
||||
2
|
||||
(1 row)
|
||||
|
||||
SELECT l2_distance('[0,0]'::minivec, '[3,4]');
|
||||
l2_distance
|
||||
-------------
|
||||
5
|
||||
(1 row)
|
||||
|
||||
SELECT l2_distance('[0,0]'::minivec, '[0,1]');
|
||||
l2_distance
|
||||
-------------
|
||||
1
|
||||
(1 row)
|
||||
|
||||
SELECT l2_distance('[1,2]'::minivec, '[3]');
|
||||
ERROR: different minivec dimensions 2 and 1
|
||||
SELECT l2_distance('[1,1,1,1,1,1,1,1,1]'::minivec, '[1,1,1,1,1,1,1,4,5]');
|
||||
l2_distance
|
||||
-------------
|
||||
5
|
||||
(1 row)
|
||||
|
||||
SELECT '[0,0]'::minivec <-> '[3,4]';
|
||||
?column?
|
||||
----------
|
||||
5
|
||||
(1 row)
|
||||
|
||||
SELECT inner_product('[1,2]'::minivec, '[3,4]');
|
||||
inner_product
|
||||
---------------
|
||||
11
|
||||
(1 row)
|
||||
|
||||
SELECT inner_product('[1,2]'::minivec, '[3]');
|
||||
ERROR: different minivec dimensions 2 and 1
|
||||
SELECT inner_product('[448]'::minivec, '[448]');
|
||||
inner_product
|
||||
---------------
|
||||
200704
|
||||
(1 row)
|
||||
|
||||
SELECT inner_product('[1,1,1,1,1,1,1,1,1]'::minivec, '[1,2,3,4,5,6,7,8,9]');
|
||||
inner_product
|
||||
---------------
|
||||
45
|
||||
(1 row)
|
||||
|
||||
SELECT '[1,2]'::minivec <#> '[3,4]';
|
||||
?column?
|
||||
----------
|
||||
-11
|
||||
(1 row)
|
||||
|
||||
SELECT cosine_distance('[1,2]'::minivec, '[2,4]');
|
||||
cosine_distance
|
||||
-----------------
|
||||
0
|
||||
(1 row)
|
||||
|
||||
SELECT cosine_distance('[1,2]'::minivec, '[0,0]');
|
||||
cosine_distance
|
||||
-----------------
|
||||
NaN
|
||||
(1 row)
|
||||
|
||||
SELECT cosine_distance('[1,1]'::minivec, '[1,1]');
|
||||
cosine_distance
|
||||
-----------------
|
||||
0
|
||||
(1 row)
|
||||
|
||||
SELECT cosine_distance('[1,0]'::minivec, '[0,2]');
|
||||
cosine_distance
|
||||
-----------------
|
||||
1
|
||||
(1 row)
|
||||
|
||||
SELECT cosine_distance('[1,1]'::minivec, '[-1,-1]');
|
||||
cosine_distance
|
||||
-----------------
|
||||
2
|
||||
(1 row)
|
||||
|
||||
SELECT cosine_distance('[1,2]'::minivec, '[3]');
|
||||
ERROR: different minivec dimensions 2 and 1
|
||||
SELECT cosine_distance('[1,1]'::minivec, '[1.1,1.1]');
|
||||
cosine_distance
|
||||
-----------------
|
||||
0
|
||||
(1 row)
|
||||
|
||||
SELECT cosine_distance('[1,1]'::minivec, '[-1.1,-1.1]');
|
||||
cosine_distance
|
||||
-----------------
|
||||
2
|
||||
(1 row)
|
||||
|
||||
SELECT cosine_distance('[1,2,3,4,5,6,7,8,9]'::minivec, '[1,2,3,4,5,6,7,8,9]');
|
||||
cosine_distance
|
||||
-----------------
|
||||
0
|
||||
(1 row)
|
||||
|
||||
SELECT cosine_distance('[1,2,3,4,5,6,7,8,9]'::minivec, '[-1,-2,-3,-4,-5,-6,-7,-8,-9]');
|
||||
cosine_distance
|
||||
-----------------
|
||||
2
|
||||
(1 row)
|
||||
|
||||
SELECT '[1,2]'::minivec <=> '[2,4]';
|
||||
?column?
|
||||
----------
|
||||
0
|
||||
(1 row)
|
||||
|
||||
SELECT l1_distance('[0,0]'::minivec, '[3,4]');
|
||||
l1_distance
|
||||
-------------
|
||||
7
|
||||
(1 row)
|
||||
|
||||
SELECT l1_distance('[0,0]'::minivec, '[0,1]');
|
||||
l1_distance
|
||||
-------------
|
||||
1
|
||||
(1 row)
|
||||
|
||||
SELECT l1_distance('[1,2]'::minivec, '[3]');
|
||||
ERROR: different minivec dimensions 2 and 1
|
||||
SELECT l1_distance('[1,2,3,4,5,6,7,8,9]'::minivec, '[1,2,3,4,5,6,7,8,9]');
|
||||
l1_distance
|
||||
-------------
|
||||
0
|
||||
(1 row)
|
||||
|
||||
SELECT l1_distance('[1,2,3,4,5,6,7,8,9]'::minivec, '[0,3,2,5,4,7,6,9,8]');
|
||||
l1_distance
|
||||
-------------
|
||||
9
|
||||
(1 row)
|
||||
|
||||
SELECT '[0,0]'::minivec <+> '[3,4]';
|
||||
?column?
|
||||
----------
|
||||
7
|
||||
(1 row)
|
||||
|
||||
SELECT l2_normalize('[3,4]'::minivec);
|
||||
l2_normalize
|
||||
----------------
|
||||
[0.625,0.8125]
|
||||
(1 row)
|
||||
|
||||
SELECT l2_normalize('[3,0]'::minivec);
|
||||
l2_normalize
|
||||
--------------
|
||||
[1,0]
|
||||
(1 row)
|
||||
|
||||
SELECT l2_normalize('[0,0.1]'::minivec);
|
||||
l2_normalize
|
||||
--------------
|
||||
[0,1]
|
||||
(1 row)
|
||||
|
||||
SELECT l2_normalize('[0,0]'::minivec);
|
||||
l2_normalize
|
||||
--------------
|
||||
[0,0]
|
||||
(1 row)
|
||||
|
||||
SELECT l2_normalize('[448]'::minivec);
|
||||
l2_normalize
|
||||
--------------
|
||||
[1]
|
||||
(1 row)
|
||||
|
||||
SELECT binary_quantize('[1,0,-1]'::minivec);
|
||||
binary_quantize
|
||||
-----------------
|
||||
100
|
||||
(1 row)
|
||||
|
||||
SELECT binary_quantize('[0,0.1,-0.2,-0.3,0.4,0.5,0.6,-0.7,0.8,-0.9,1]'::minivec);
|
||||
binary_quantize
|
||||
-----------------
|
||||
01001110101
|
||||
(1 row)
|
||||
|
||||
SELECT subvector('[1,2,3,4,5]'::minivec, 1, 3);
|
||||
subvector
|
||||
-----------
|
||||
[1,2,3]
|
||||
(1 row)
|
||||
|
||||
SELECT subvector('[1,2,3,4,5]'::minivec, 3, 2);
|
||||
subvector
|
||||
-----------
|
||||
[3,4]
|
||||
(1 row)
|
||||
|
||||
SELECT subvector('[1,2,3,4,5]'::minivec, -1, 3);
|
||||
subvector
|
||||
-----------
|
||||
[1]
|
||||
(1 row)
|
||||
|
||||
SELECT subvector('[1,2,3,4,5]'::minivec, 3, 9);
|
||||
subvector
|
||||
-----------
|
||||
[3,4,5]
|
||||
(1 row)
|
||||
|
||||
SELECT subvector('[1,2,3,4,5]'::minivec, 1, 0);
|
||||
ERROR: minivec must have at least 1 dimension
|
||||
SELECT subvector('[1,2,3,4,5]'::minivec, 3, -1);
|
||||
ERROR: minivec must have at least 1 dimension
|
||||
SELECT subvector('[1,2,3,4,5]'::minivec, -1, 2);
|
||||
ERROR: minivec must have at least 1 dimension
|
||||
SELECT subvector('[1,2,3,4,5]'::minivec, 2147483647, 10);
|
||||
ERROR: minivec must have at least 1 dimension
|
||||
SELECT subvector('[1,2,3,4,5]'::minivec, 3, 2147483647);
|
||||
subvector
|
||||
-----------
|
||||
[3,4,5]
|
||||
(1 row)
|
||||
|
||||
SELECT subvector('[1,2,3,4,5]'::minivec, -2147483644, 2147483647);
|
||||
subvector
|
||||
-----------
|
||||
[1,2]
|
||||
(1 row)
|
||||
|
||||
@@ -22,17 +22,6 @@ SELECT * FROM t ORDER BY val;
|
||||
|
||||
DROP TABLE t;
|
||||
|
||||
-- minivec
|
||||
|
||||
CREATE TABLE t (val minivec(3));
|
||||
INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL);
|
||||
CREATE INDEX ON t (val);
|
||||
|
||||
SELECT * FROM t WHERE val = '[1,2,3]';
|
||||
SELECT * FROM t ORDER BY val;
|
||||
|
||||
DROP TABLE t;
|
||||
|
||||
-- sparsevec
|
||||
|
||||
CREATE TABLE t (val sparsevec(3));
|
||||
|
||||
@@ -38,22 +38,6 @@ SELECT '{1,2,3}'::real[]::halfvec(2);
|
||||
SELECT '{65520,-65520}'::real[]::halfvec;
|
||||
SELECT '{1e-8,-1e-8}'::real[]::halfvec;
|
||||
|
||||
SELECT '[1,2,3]'::vector::minivec;
|
||||
SELECT '[1,2,3]'::vector::minivec(3);
|
||||
SELECT '[1,2,3]'::vector::minivec(2);
|
||||
SELECT '[465]'::vector::minivec;
|
||||
SELECT '[1e-8]'::vector::minivec;
|
||||
|
||||
SELECT '[1,2,3]'::minivec::vector;
|
||||
SELECT '[1,2,3]'::minivec::vector(3);
|
||||
SELECT '[1,2,3]'::minivec::vector(2);
|
||||
|
||||
SELECT '{1,2,3}'::real[]::minivec;
|
||||
SELECT '{1,2,3}'::real[]::minivec(3);
|
||||
SELECT '{1,2,3}'::real[]::minivec(2);
|
||||
SELECT '{465,-465}'::real[]::minivec;
|
||||
SELECT '{1e-8,-1e-8}'::real[]::minivec;
|
||||
|
||||
SELECT '[0,1.5,0,3.5,0]'::vector::sparsevec;
|
||||
SELECT '[0,1.5,0,3.5,0]'::vector::sparsevec(5);
|
||||
SELECT '[0,1.5,0,3.5,0]'::vector::sparsevec(4);
|
||||
|
||||
@@ -28,21 +28,6 @@ SELECT * FROM t2 ORDER BY val;
|
||||
DROP TABLE t;
|
||||
DROP TABLE t2;
|
||||
|
||||
-- minivec
|
||||
|
||||
CREATE TABLE t (val minivec(3));
|
||||
INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL);
|
||||
|
||||
CREATE TABLE t2 (val minivec(3));
|
||||
|
||||
\copy t TO 'results/minivec.bin' WITH (FORMAT binary)
|
||||
\copy t2 FROM 'results/minivec.bin' WITH (FORMAT binary)
|
||||
|
||||
SELECT * FROM t2 ORDER BY val;
|
||||
|
||||
DROP TABLE t;
|
||||
DROP TABLE t2;
|
||||
|
||||
-- sparsevec
|
||||
|
||||
CREATE TABLE t (val sparsevec(3));
|
||||
|
||||
@@ -57,6 +57,23 @@ SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <+> (SELECT NULL::vector)) t2
|
||||
|
||||
DROP TABLE t;
|
||||
|
||||
-- iterative
|
||||
|
||||
CREATE TABLE t (val vector(3));
|
||||
INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL);
|
||||
CREATE INDEX ON t USING hnsw (val vector_l2_ops);
|
||||
|
||||
SET hnsw.iterative_scan = strict_order;
|
||||
SET hnsw.ef_search = 1;
|
||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
||||
|
||||
SET hnsw.iterative_scan = relaxed_order;
|
||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
||||
|
||||
RESET hnsw.iterative_scan;
|
||||
RESET hnsw.ef_search;
|
||||
DROP TABLE t;
|
||||
|
||||
-- unlogged
|
||||
|
||||
CREATE UNLOGGED TABLE t (val vector(3));
|
||||
@@ -81,4 +98,16 @@ SHOW hnsw.ef_search;
|
||||
SET hnsw.ef_search = 0;
|
||||
SET hnsw.ef_search = 1001;
|
||||
|
||||
SHOW hnsw.iterative_scan;
|
||||
|
||||
SET hnsw.iterative_scan = on;
|
||||
|
||||
SHOW hnsw.max_scan_tuples;
|
||||
|
||||
SET hnsw.max_scan_tuples = 0;
|
||||
|
||||
SHOW hnsw.scan_mem_multiplier;
|
||||
|
||||
SET hnsw.scan_mem_multiplier = 0;
|
||||
|
||||
DROP TABLE t;
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
SET enable_seqscan = off;
|
||||
|
||||
-- L2
|
||||
|
||||
CREATE TABLE t (val minivec(3));
|
||||
INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL);
|
||||
CREATE INDEX ON t USING ivfflat (val minivec_l2_ops) WITH (lists = 1);
|
||||
|
||||
INSERT INTO t (val) VALUES ('[1,2,4]');
|
||||
|
||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
||||
SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <-> (SELECT NULL::minivec)) t2;
|
||||
SELECT COUNT(*) FROM t;
|
||||
|
||||
TRUNCATE t;
|
||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
||||
|
||||
DROP TABLE t;
|
||||
|
||||
-- inner product
|
||||
|
||||
CREATE TABLE t (val minivec(3));
|
||||
INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL);
|
||||
CREATE INDEX ON t USING ivfflat (val minivec_ip_ops) WITH (lists = 1);
|
||||
|
||||
INSERT INTO t (val) VALUES ('[1,2,4]');
|
||||
|
||||
SELECT * FROM t ORDER BY val <#> '[3,3,3]';
|
||||
SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <#> (SELECT NULL::minivec)) t2;
|
||||
|
||||
DROP TABLE t;
|
||||
|
||||
-- cosine
|
||||
|
||||
CREATE TABLE t (val minivec(3));
|
||||
INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL);
|
||||
CREATE INDEX ON t USING ivfflat (val minivec_cosine_ops) WITH (lists = 1);
|
||||
|
||||
INSERT INTO t (val) VALUES ('[1,2,4]');
|
||||
|
||||
SELECT * FROM t ORDER BY val <=> '[3,3,3]';
|
||||
SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <=> '[0,0,0]') t2;
|
||||
SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <=> (SELECT NULL::minivec)) t2;
|
||||
|
||||
DROP TABLE t;
|
||||
@@ -44,6 +44,25 @@ SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <=> (SELECT NULL::vector)) t2
|
||||
|
||||
DROP TABLE t;
|
||||
|
||||
-- iterative
|
||||
|
||||
CREATE TABLE t (val vector(3));
|
||||
INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL);
|
||||
CREATE INDEX ON t USING ivfflat (val vector_l2_ops) WITH (lists = 3);
|
||||
|
||||
SET ivfflat.iterative_scan = relaxed_order;
|
||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
||||
|
||||
SET ivfflat.max_probes = 1;
|
||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
||||
|
||||
SET ivfflat.max_probes = 2;
|
||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
||||
|
||||
RESET ivfflat.iterative_scan;
|
||||
RESET ivfflat.max_probes;
|
||||
DROP TABLE t;
|
||||
|
||||
-- unlogged
|
||||
|
||||
CREATE UNLOGGED TABLE t (val vector(3));
|
||||
@@ -62,4 +81,16 @@ CREATE INDEX ON t USING ivfflat (val vector_l2_ops) WITH (lists = 32769);
|
||||
|
||||
SHOW ivfflat.probes;
|
||||
|
||||
SET ivfflat.probes = 0;
|
||||
SET ivfflat.probes = 32769;
|
||||
|
||||
SHOW ivfflat.iterative_scan;
|
||||
|
||||
SET ivfflat.iterative_scan = on;
|
||||
|
||||
SHOW ivfflat.max_probes;
|
||||
|
||||
SET ivfflat.max_probes = 0;
|
||||
SET ivfflat.max_probes = 32769;
|
||||
|
||||
DROP TABLE t;
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
SELECT '[1,2,3]'::minivec;
|
||||
SELECT '[-1,-2,-3]'::minivec;
|
||||
SELECT '[1.,2.,3.]'::minivec;
|
||||
SELECT ' [ 1, 2 , 3 ] '::minivec;
|
||||
SELECT '[1.23456]'::minivec;
|
||||
SELECT '[hello,1]'::minivec;
|
||||
SELECT '[NaN,1]'::minivec;
|
||||
SELECT '[Infinity,1]'::minivec;
|
||||
SELECT '[-Infinity,1]'::minivec;
|
||||
SELECT '[65519,-65519]'::minivec;
|
||||
SELECT '[65520,-65520]'::minivec;
|
||||
SELECT '[1e-8,-1e-8]'::minivec;
|
||||
SELECT '[4e38,1]'::minivec;
|
||||
SELECT '[1e-46,1]'::minivec;
|
||||
SELECT '[1,2,3'::minivec;
|
||||
SELECT '[1,2,3]9'::minivec;
|
||||
SELECT '1,2,3'::minivec;
|
||||
SELECT ''::minivec;
|
||||
SELECT '['::minivec;
|
||||
SELECT '[ '::minivec;
|
||||
SELECT '[,'::minivec;
|
||||
SELECT '[]'::minivec;
|
||||
SELECT '[ ]'::minivec;
|
||||
SELECT '[,]'::minivec;
|
||||
SELECT '[1,]'::minivec;
|
||||
SELECT '[1a]'::minivec;
|
||||
SELECT '[1,,3]'::minivec;
|
||||
SELECT '[1, ,3]'::minivec;
|
||||
|
||||
SELECT '[1,2,3]'::minivec(3);
|
||||
SELECT '[1,2,3]'::minivec(2);
|
||||
SELECT '[1,2,3]'::minivec(3, 2);
|
||||
SELECT '[1,2,3]'::minivec('a');
|
||||
SELECT '[1,2,3]'::minivec(0);
|
||||
SELECT '[1,2,3]'::minivec(16001);
|
||||
|
||||
SELECT unnest('{"[1,2,3]", "[4,5,6]"}'::minivec[]);
|
||||
SELECT '{"[1,2,3]"}'::minivec(2)[];
|
||||
|
||||
SELECT '[1,2,3]'::minivec + '[4,5,6]';
|
||||
SELECT '[448]'::minivec + '[448]';
|
||||
SELECT '[1,2]'::minivec + '[3]';
|
||||
|
||||
SELECT '[1,2,3]'::minivec - '[4,5,6]';
|
||||
SELECT '[-448]'::minivec - '[448]';
|
||||
SELECT '[1,2]'::minivec - '[3]';
|
||||
|
||||
SELECT '[1,2,3]'::minivec * '[4,5,6]';
|
||||
SELECT '[448]'::minivec * '[448]';
|
||||
SELECT '[1e-7]'::minivec * '[1e-7]';
|
||||
SELECT '[1,2]'::minivec * '[3]';
|
||||
|
||||
SELECT '[1,2,3]'::minivec || '[4,5]';
|
||||
SELECT array_fill(0, ARRAY[16000])::minivec || '[1]';
|
||||
|
||||
SELECT '[1,2,3]'::minivec < '[1,2,3]';
|
||||
SELECT '[1,2,3]'::minivec < '[1,2]';
|
||||
SELECT '[1,2,3]'::minivec <= '[1,2,3]';
|
||||
SELECT '[1,2,3]'::minivec <= '[1,2]';
|
||||
SELECT '[1,2,3]'::minivec = '[1,2,3]';
|
||||
SELECT '[1,2,3]'::minivec = '[1,2]';
|
||||
SELECT '[1,2,3]'::minivec != '[1,2,3]';
|
||||
SELECT '[1,2,3]'::minivec != '[1,2]';
|
||||
SELECT '[1,2,3]'::minivec >= '[1,2,3]';
|
||||
SELECT '[1,2,3]'::minivec >= '[1,2]';
|
||||
SELECT '[1,2,3]'::minivec > '[1,2,3]';
|
||||
SELECT '[1,2,3]'::minivec > '[1,2]';
|
||||
|
||||
SELECT minivec_cmp('[1,2,3]', '[1,2,3]');
|
||||
SELECT minivec_cmp('[1,2,3]', '[0,0,0]');
|
||||
SELECT minivec_cmp('[0,0,0]', '[1,2,3]');
|
||||
SELECT minivec_cmp('[1,2]', '[1,2,3]');
|
||||
SELECT minivec_cmp('[1,2,3]', '[1,2]');
|
||||
SELECT minivec_cmp('[1,2]', '[2,3,4]');
|
||||
SELECT minivec_cmp('[2,3]', '[1,2,3]');
|
||||
|
||||
SELECT vector_dims('[1,2,3]'::minivec);
|
||||
|
||||
SELECT round(l2_norm('[1,1]'::minivec)::numeric, 5);
|
||||
SELECT l2_norm('[3,4]'::minivec);
|
||||
SELECT l2_norm('[0,1]'::minivec);
|
||||
SELECT l2_norm('[0,0]'::minivec);
|
||||
SELECT l2_norm('[2]'::minivec);
|
||||
|
||||
SELECT l2_distance('[0,0]'::minivec, '[3,4]');
|
||||
SELECT l2_distance('[0,0]'::minivec, '[0,1]');
|
||||
SELECT l2_distance('[1,2]'::minivec, '[3]');
|
||||
SELECT l2_distance('[1,1,1,1,1,1,1,1,1]'::minivec, '[1,1,1,1,1,1,1,4,5]');
|
||||
SELECT '[0,0]'::minivec <-> '[3,4]';
|
||||
|
||||
SELECT inner_product('[1,2]'::minivec, '[3,4]');
|
||||
SELECT inner_product('[1,2]'::minivec, '[3]');
|
||||
SELECT inner_product('[448]'::minivec, '[448]');
|
||||
SELECT inner_product('[1,1,1,1,1,1,1,1,1]'::minivec, '[1,2,3,4,5,6,7,8,9]');
|
||||
SELECT '[1,2]'::minivec <#> '[3,4]';
|
||||
|
||||
SELECT cosine_distance('[1,2]'::minivec, '[2,4]');
|
||||
SELECT cosine_distance('[1,2]'::minivec, '[0,0]');
|
||||
SELECT cosine_distance('[1,1]'::minivec, '[1,1]');
|
||||
SELECT cosine_distance('[1,0]'::minivec, '[0,2]');
|
||||
SELECT cosine_distance('[1,1]'::minivec, '[-1,-1]');
|
||||
SELECT cosine_distance('[1,2]'::minivec, '[3]');
|
||||
SELECT cosine_distance('[1,1]'::minivec, '[1.1,1.1]');
|
||||
SELECT cosine_distance('[1,1]'::minivec, '[-1.1,-1.1]');
|
||||
SELECT cosine_distance('[1,2,3,4,5,6,7,8,9]'::minivec, '[1,2,3,4,5,6,7,8,9]');
|
||||
SELECT cosine_distance('[1,2,3,4,5,6,7,8,9]'::minivec, '[-1,-2,-3,-4,-5,-6,-7,-8,-9]');
|
||||
SELECT '[1,2]'::minivec <=> '[2,4]';
|
||||
|
||||
SELECT l1_distance('[0,0]'::minivec, '[3,4]');
|
||||
SELECT l1_distance('[0,0]'::minivec, '[0,1]');
|
||||
SELECT l1_distance('[1,2]'::minivec, '[3]');
|
||||
SELECT l1_distance('[1,2,3,4,5,6,7,8,9]'::minivec, '[1,2,3,4,5,6,7,8,9]');
|
||||
SELECT l1_distance('[1,2,3,4,5,6,7,8,9]'::minivec, '[0,3,2,5,4,7,6,9,8]');
|
||||
SELECT '[0,0]'::minivec <+> '[3,4]';
|
||||
|
||||
SELECT l2_normalize('[3,4]'::minivec);
|
||||
SELECT l2_normalize('[3,0]'::minivec);
|
||||
SELECT l2_normalize('[0,0.1]'::minivec);
|
||||
SELECT l2_normalize('[0,0]'::minivec);
|
||||
SELECT l2_normalize('[448]'::minivec);
|
||||
|
||||
SELECT binary_quantize('[1,0,-1]'::minivec);
|
||||
SELECT binary_quantize('[0,0.1,-0.2,-0.3,0.4,0.5,0.6,-0.7,0.8,-0.9,1]'::minivec);
|
||||
|
||||
SELECT subvector('[1,2,3,4,5]'::minivec, 1, 3);
|
||||
SELECT subvector('[1,2,3,4,5]'::minivec, 3, 2);
|
||||
SELECT subvector('[1,2,3,4,5]'::minivec, -1, 3);
|
||||
SELECT subvector('[1,2,3,4,5]'::minivec, 3, 9);
|
||||
SELECT subvector('[1,2,3,4,5]'::minivec, 1, 0);
|
||||
SELECT subvector('[1,2,3,4,5]'::minivec, 3, -1);
|
||||
SELECT subvector('[1,2,3,4,5]'::minivec, -1, 2);
|
||||
SELECT subvector('[1,2,3,4,5]'::minivec, 2147483647, 10);
|
||||
SELECT subvector('[1,2,3,4,5]'::minivec, 3, 2147483647);
|
||||
SELECT subvector('[1,2,3,4,5]'::minivec, -2147483644, 2147483647);
|
||||
@@ -6,13 +6,7 @@ use Test::More;
|
||||
|
||||
my $dim = 3;
|
||||
|
||||
my @r = ();
|
||||
for (1 .. $dim)
|
||||
{
|
||||
my $v = int(rand(1000)) + 1;
|
||||
push(@r, "i % $v");
|
||||
}
|
||||
my $array_sql = join(", ", @r);
|
||||
my $array_sql = join(",", ('random()') x $dim);
|
||||
|
||||
# Initialize node
|
||||
my $node = PostgreSQL::Test::Cluster->new('node');
|
||||
@@ -23,19 +17,20 @@ $node->start;
|
||||
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
|
||||
$node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector($dim));");
|
||||
$node->safe_psql("postgres",
|
||||
"INSERT INTO tst SELECT i % 10, ARRAY[$array_sql] FROM generate_series(1, 100000) i;"
|
||||
"INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(1, 100000) i;"
|
||||
);
|
||||
$node->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v vector_l2_ops);");
|
||||
|
||||
# Get size
|
||||
my $size = $node->safe_psql("postgres", "SELECT pg_total_relation_size('tst_v_idx');");
|
||||
|
||||
# Store values
|
||||
$node->safe_psql("postgres", "CREATE TABLE tmp AS SELECT * FROM tst;");
|
||||
|
||||
# Delete all, vacuum, and insert same data
|
||||
$node->safe_psql("postgres", "DELETE FROM tst;");
|
||||
$node->safe_psql("postgres", "VACUUM tst;");
|
||||
$node->safe_psql("postgres",
|
||||
"INSERT INTO tst SELECT i % 10, ARRAY[$array_sql] FROM generate_series(1, 100000) i;"
|
||||
);
|
||||
$node->safe_psql("postgres", "INSERT INTO tst SELECT * FROM tmp;");
|
||||
|
||||
# Check size
|
||||
my $new_size = $node->safe_psql("postgres", "SELECT pg_total_relation_size('tst_v_idx');");
|
||||
|
||||
@@ -94,8 +94,7 @@ like($explain, qr/Seq Scan/);
|
||||
$explain = $node->safe_psql("postgres", qq(
|
||||
EXPLAIN ANALYZE SELECT i FROM tst WHERE v <-> '$query' < 1 ORDER BY v <-> '$query';
|
||||
));
|
||||
# TODO Do not use index
|
||||
like($explain, qr/Index Scan using idx/);
|
||||
like($explain, qr/Seq Scan/);
|
||||
|
||||
# Test attribute index
|
||||
$node->safe_psql("postgres", "CREATE INDEX attribute_idx ON tst (c);");
|
||||
@@ -110,7 +109,6 @@ $node->safe_psql("postgres", "CREATE INDEX partial_idx ON tst USING ivfflat (v v
|
||||
$explain = $node->safe_psql("postgres", qq(
|
||||
EXPLAIN ANALYZE SELECT i FROM tst WHERE c = $c ORDER BY v <-> '$query' LIMIT $limit;
|
||||
));
|
||||
# TODO Use partial index
|
||||
like($explain, qr/Index Scan using idx/);
|
||||
like($explain, qr/Index Scan using partial_idx/);
|
||||
|
||||
done_testing();
|
||||
|
||||
@@ -18,9 +18,13 @@ $node->start;
|
||||
# Create table and index
|
||||
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
|
||||
$node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector($dim), c int4, t text);");
|
||||
$node->safe_psql("postgres", "CREATE TABLE cat (i int4 PRIMARY KEY, t text, b boolean);");
|
||||
$node->safe_psql("postgres",
|
||||
"INSERT INTO tst SELECT i, ARRAY[$array_sql], i % $nc, 'test ' || i FROM generate_series(1, 10000) i;"
|
||||
);
|
||||
$node->safe_psql("postgres",
|
||||
"INSERT INTO cat SELECT i, 'cat ' || i, i % 5 = 0 FROM generate_series(1, $nc) i;"
|
||||
);
|
||||
$node->safe_psql("postgres", "CREATE INDEX idx ON tst USING hnsw (v vector_l2_ops);");
|
||||
$node->safe_psql("postgres", "ANALYZE tst;");
|
||||
|
||||
@@ -37,8 +41,7 @@ my $c = int(rand() * $nc);
|
||||
my $explain = $node->safe_psql("postgres", qq(
|
||||
EXPLAIN ANALYZE SELECT i FROM tst WHERE c = $c ORDER BY v <-> '$query' LIMIT $limit;
|
||||
));
|
||||
# TODO Do not use index
|
||||
like($explain, qr/Index Scan using idx/);
|
||||
like($explain, qr/Seq Scan/);
|
||||
|
||||
# Test attribute filtering with few rows removed
|
||||
$explain = $node->safe_psql("postgres", qq(
|
||||
@@ -56,8 +59,7 @@ like($explain, qr/Index Scan using idx/);
|
||||
$explain = $node->safe_psql("postgres", qq(
|
||||
EXPLAIN ANALYZE SELECT i FROM tst WHERE c < 1 ORDER BY v <-> '$query' LIMIT $limit;
|
||||
));
|
||||
# TODO Do not use index
|
||||
like($explain, qr/Index Scan using idx/);
|
||||
like($explain, qr/Seq Scan/);
|
||||
|
||||
# Test attribute filtering with few rows removed like
|
||||
$explain = $node->safe_psql("postgres", qq(
|
||||
@@ -96,13 +98,25 @@ $explain = $node->safe_psql("postgres", qq(
|
||||
));
|
||||
like($explain, qr/Seq Scan/);
|
||||
|
||||
# Test join
|
||||
$explain = $node->safe_psql("postgres", qq(
|
||||
EXPLAIN ANALYZE SELECT cat.t FROM cat INNER JOIN tst ON cat.i = tst.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
|
||||
$node->safe_psql("postgres", "CREATE INDEX attribute_idx ON tst (c);");
|
||||
$explain = $node->safe_psql("postgres", qq(
|
||||
EXPLAIN ANALYZE SELECT i FROM tst WHERE c = $c ORDER BY v <-> '$query' LIMIT $limit;
|
||||
));
|
||||
# TODO Use attribute index
|
||||
like($explain, qr/Index Scan using idx/);
|
||||
# Use attribute index
|
||||
like($explain, qr/Bitmap Index Scan on attribute_idx/);
|
||||
|
||||
# Test partial index
|
||||
$node->safe_psql("postgres", "CREATE INDEX partial_idx ON tst USING hnsw (v vector_l2_ops) WHERE (c = $c);");
|
||||
|
||||
@@ -40,10 +40,6 @@ for (1 .. 50)
|
||||
$actual = $node->safe_psql("postgres", "SELECT halfvec_cmp(v::halfvec, '$query'::real[]::halfvec) FROM tst");
|
||||
is($expected, $actual);
|
||||
|
||||
# Test minivec
|
||||
$actual = $node->safe_psql("postgres", "SELECT minivec_cmp(v::minivec, '$query'::real[]::minivec) FROM tst");
|
||||
is($expected, $actual);
|
||||
|
||||
# Test sparsevec
|
||||
$actual = $node->safe_psql("postgres", "SELECT sparsevec_cmp(v::vector::sparsevec, '$query'::real[]::vector::sparsevec) FROM tst");
|
||||
is($expected, $actual);
|
||||
|
||||
@@ -45,10 +45,6 @@ for my $function (@functions)
|
||||
my $actual = $node->safe_psql("postgres", "SELECT $function(v::halfvec, '$query'::vector::halfvec) FROM tst");
|
||||
is($expected, $actual, "halfvec $function");
|
||||
|
||||
# Test minivec
|
||||
$actual = $node->safe_psql("postgres", "SELECT $function(v::minivec, '$query'::vector::minivec) FROM tst");
|
||||
is($expected, $actual, "minivec $function");
|
||||
|
||||
# Test sparsevec
|
||||
$actual = $node->safe_psql("postgres", "SELECT $function(v::sparsevec, '$query'::vector::sparsevec) FROM tst");
|
||||
is($expected, $actual, "sparsevec $function");
|
||||
|
||||
@@ -12,8 +12,8 @@ $node->start;
|
||||
# Create extension
|
||||
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
|
||||
|
||||
my @types = ("vector", "halfvec", "minivec", "sparsevec");
|
||||
my @inputs = ("[1.23,4.56,7.89]", "[1.23,4.56,7.89]", "[1.23,4.56,7.89]", "{1:1.23,2:4.56,3:7.89}/3");
|
||||
my @types = ("vector", "halfvec", "sparsevec");
|
||||
my @inputs = ("[1.23,4.56,7.89]", "[1.23,4.56,7.89]", "{1:1.23,2:4.56,3:7.89}/3");
|
||||
my @subs = (" ", " ", ",", ":", "-", "1", "9", "\0", "2147483648", "-2147483649");
|
||||
|
||||
for my $i (0 .. $#types)
|
||||
|
||||
60
test/t/039_hnsw_cost.pl
Normal file
60
test/t/039_hnsw_cost.pl
Normal file
@@ -0,0 +1,60 @@
|
||||
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, 2000) 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/);
|
||||
|
||||
# 3x the rows are needed for distance filters
|
||||
# since the planner uses DEFAULT_INEQ_SEL for the selectivity (should be 1)
|
||||
# Recreate index for performance
|
||||
$node->safe_psql("postgres", "DROP INDEX idx;");
|
||||
$node->safe_psql("postgres",
|
||||
"INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(2001, 6000) i;"
|
||||
);
|
||||
$node->safe_psql("postgres", "CREATE INDEX idx ON tst USING hnsw (v vector_l2_ops);");
|
||||
$node->safe_psql("postgres", "ANALYZE tst;");
|
||||
|
||||
$explain = $node->safe_psql("postgres", qq(
|
||||
EXPLAIN ANALYZE SELECT i FROM tst WHERE v <-> '$query' < 1 ORDER BY v <-> '$query' LIMIT $limit;
|
||||
));
|
||||
like($explain, qr/Index Scan using idx/);
|
||||
|
||||
$node->safe_psql("postgres", "DROP TABLE tst;");
|
||||
}
|
||||
|
||||
done_testing();
|
||||
@@ -1,132 +0,0 @@
|
||||
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 = 10;
|
||||
my $array_sql = join(",", ('2 * random() * random()') x $dim);
|
||||
|
||||
sub test_recall
|
||||
{
|
||||
my ($min, $operator) = @_;
|
||||
my $correct = 0;
|
||||
my $total = 0;
|
||||
|
||||
my $explain = $node->safe_psql("postgres", qq(
|
||||
SET enable_seqscan = off;
|
||||
EXPLAIN ANALYZE SELECT i FROM tst ORDER BY v $operator '$queries[0]' LIMIT $limit;
|
||||
));
|
||||
like($explain, qr/Index Scan/);
|
||||
|
||||
for my $i (0 .. $#queries)
|
||||
{
|
||||
my $actual = $node->safe_psql("postgres", qq(
|
||||
SET enable_seqscan = off;
|
||||
SELECT i FROM tst ORDER BY v $operator '$queries[$i]' LIMIT $limit;
|
||||
));
|
||||
my @actual_ids = split("\n", $actual);
|
||||
my %actual_set = map { $_ => 1 } @actual_ids;
|
||||
|
||||
my @expected_ids = split("\n", $expected[$i]);
|
||||
|
||||
foreach (@expected_ids)
|
||||
{
|
||||
if (exists($actual_set{$_}))
|
||||
{
|
||||
$correct++;
|
||||
}
|
||||
$total++;
|
||||
}
|
||||
}
|
||||
|
||||
cmp_ok($correct / $total, ">=", $min, $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 minivec($dim));");
|
||||
$node->safe_psql("postgres",
|
||||
"INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(1, 10000) 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 = ("minivec_l2_ops", "minivec_ip_ops", "minivec_cosine_ops", "minivec_l1_ops");
|
||||
|
||||
for my $i (0 .. $#operators)
|
||||
{
|
||||
my $operator = $operators[$i];
|
||||
my $opclass = $opclasses[$i];
|
||||
|
||||
# Get exact results
|
||||
@expected = ();
|
||||
foreach (@queries)
|
||||
{
|
||||
my $res = $node->safe_psql("postgres", "SELECT i FROM tst ORDER BY v $operator '$_' LIMIT $limit;");
|
||||
push(@expected, $res);
|
||||
}
|
||||
|
||||
# Build index serially
|
||||
$node->safe_psql("postgres", qq(
|
||||
SET max_parallel_maintenance_workers = 0;
|
||||
CREATE INDEX idx ON tst USING hnsw (v $opclass);
|
||||
));
|
||||
|
||||
# Test approximate results
|
||||
my $min = 0.98;
|
||||
test_recall($min, $operator);
|
||||
|
||||
$node->safe_psql("postgres", "DROP INDEX idx;");
|
||||
|
||||
# Build index in parallel in memory
|
||||
my ($ret, $stdout, $stderr) = $node->psql("postgres", qq(
|
||||
SET client_min_messages = DEBUG;
|
||||
SET min_parallel_table_scan_size = 1;
|
||||
CREATE INDEX idx ON tst USING hnsw (v $opclass);
|
||||
));
|
||||
is($ret, 0, $stderr);
|
||||
like($stderr, qr/using \d+ parallel workers/);
|
||||
|
||||
# Test approximate results
|
||||
test_recall($min, $operator);
|
||||
|
||||
$node->safe_psql("postgres", "DROP INDEX idx;");
|
||||
|
||||
# Build index in parallel on disk
|
||||
# Set parallel_workers on table to use workers with low maintenance_work_mem
|
||||
($ret, $stdout, $stderr) = $node->psql("postgres", qq(
|
||||
ALTER TABLE tst SET (parallel_workers = 2);
|
||||
SET client_min_messages = DEBUG;
|
||||
SET maintenance_work_mem = '4MB';
|
||||
CREATE INDEX idx ON tst USING hnsw (v $opclass);
|
||||
ALTER TABLE tst RESET (parallel_workers);
|
||||
));
|
||||
is($ret, 0, $stderr);
|
||||
like($stderr, qr/using \d+ parallel workers/);
|
||||
like($stderr, qr/hnsw graph no longer fits into maintenance_work_mem/);
|
||||
|
||||
$node->safe_psql("postgres", "DROP INDEX idx;");
|
||||
}
|
||||
|
||||
done_testing();
|
||||
@@ -1,113 +0,0 @@
|
||||
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 = 10;
|
||||
my $array_sql = join(",", ('2 * random() * random()') x $dim);
|
||||
|
||||
sub test_recall
|
||||
{
|
||||
my ($min, $operator) = @_;
|
||||
my $correct = 0;
|
||||
my $total = 0;
|
||||
|
||||
my $explain = $node->safe_psql("postgres", qq(
|
||||
SET enable_seqscan = off;
|
||||
EXPLAIN ANALYZE SELECT i FROM tst ORDER BY v $operator '$queries[0]' LIMIT $limit;
|
||||
));
|
||||
like($explain, qr/Index Scan/);
|
||||
|
||||
for my $i (0 .. $#queries)
|
||||
{
|
||||
my $actual = $node->safe_psql("postgres", qq(
|
||||
SET enable_seqscan = off;
|
||||
SELECT i FROM tst ORDER BY v $operator '$queries[$i]' LIMIT $limit;
|
||||
));
|
||||
my @actual_ids = split("\n", $actual);
|
||||
my %actual_set = map { $_ => 1 } @actual_ids;
|
||||
|
||||
my @expected_ids = split("\n", $expected[$i]);
|
||||
|
||||
foreach (@expected_ids)
|
||||
{
|
||||
if (exists($actual_set{$_}))
|
||||
{
|
||||
$correct++;
|
||||
}
|
||||
$total++;
|
||||
}
|
||||
}
|
||||
|
||||
cmp_ok($correct / $total, ">=", $min, $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 serial, v minivec($dim));");
|
||||
|
||||
# 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 = ("minivec_l2_ops", "minivec_ip_ops", "minivec_cosine_ops", "minivec_l1_ops");
|
||||
|
||||
for my $i (0 .. $#operators)
|
||||
{
|
||||
my $operator = $operators[$i];
|
||||
my $opclass = $opclasses[$i];
|
||||
|
||||
# Add index
|
||||
$node->safe_psql("postgres", "CREATE INDEX idx ON tst USING hnsw (v $opclass);");
|
||||
|
||||
# Use concurrent inserts
|
||||
$node->pgbench(
|
||||
"--no-vacuum --client=10 --transactions=1000",
|
||||
0,
|
||||
[qr{actually processed}],
|
||||
[qr{^$}],
|
||||
"concurrent INSERTs",
|
||||
{
|
||||
"040_hnsw_minivec_insert_recall_$opclass" => "INSERT INTO tst (v) VALUES (ARRAY[$array_sql]);"
|
||||
}
|
||||
);
|
||||
|
||||
# Get exact results
|
||||
@expected = ();
|
||||
foreach (@queries)
|
||||
{
|
||||
my $res = $node->safe_psql("postgres", qq(
|
||||
SET enable_indexscan = off;
|
||||
SELECT i FROM tst ORDER BY v $operator '$_' LIMIT $limit;
|
||||
));
|
||||
push(@expected, $res);
|
||||
}
|
||||
|
||||
# Test approximate results
|
||||
my $min = 0.98;
|
||||
test_recall($min, $operator);
|
||||
|
||||
$node->safe_psql("postgres", "DROP INDEX idx;");
|
||||
$node->safe_psql("postgres", "TRUNCATE tst;");
|
||||
}
|
||||
|
||||
done_testing();
|
||||
50
test/t/040_ivfflat_cost.pl
Normal file
50
test/t/040_ivfflat_cost.pl
Normal file
@@ -0,0 +1,50 @@
|
||||
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/);
|
||||
|
||||
$explain = $node->safe_psql("postgres", qq(
|
||||
EXPLAIN ANALYZE SELECT i FROM tst WHERE v <-> '$query' < 1 ORDER BY v <-> '$query' LIMIT $limit;
|
||||
));
|
||||
like($explain, qr/Index Scan using idx/);
|
||||
|
||||
$node->safe_psql("postgres", "DROP TABLE tst;");
|
||||
}
|
||||
|
||||
done_testing();
|
||||
@@ -1,97 +0,0 @@
|
||||
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;
|
||||
|
||||
sub test_recall
|
||||
{
|
||||
my ($min, $ef_search, $test_name) = @_;
|
||||
my $correct = 0;
|
||||
my $total = 0;
|
||||
|
||||
my $explain = $node->safe_psql("postgres", qq(
|
||||
SET enable_seqscan = off;
|
||||
SET hnsw.ef_search = $ef_search;
|
||||
EXPLAIN ANALYZE SELECT i FROM tst ORDER BY v <-> '$queries[0]' LIMIT $limit;
|
||||
));
|
||||
like($explain, qr/Index Scan/);
|
||||
|
||||
for my $i (0 .. $#queries)
|
||||
{
|
||||
my $actual = $node->safe_psql("postgres", qq(
|
||||
SET enable_seqscan = off;
|
||||
SET hnsw.ef_search = $ef_search;
|
||||
SELECT i FROM tst ORDER BY v <-> '$queries[$i]' LIMIT $limit;
|
||||
));
|
||||
my @actual_ids = split("\n", $actual);
|
||||
my %actual_set = map { $_ => 1 } @actual_ids;
|
||||
|
||||
my @expected_ids = split("\n", $expected[$i]);
|
||||
|
||||
foreach (@expected_ids)
|
||||
{
|
||||
if (exists($actual_set{$_}))
|
||||
{
|
||||
$correct++;
|
||||
}
|
||||
$total++;
|
||||
}
|
||||
}
|
||||
|
||||
cmp_ok($correct / $total, ">=", $min, $test_name);
|
||||
}
|
||||
|
||||
# Initialize node
|
||||
$node = 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 minivec(3));");
|
||||
$node->safe_psql("postgres", "ALTER TABLE tst SET (autovacuum_enabled = false);");
|
||||
$node->safe_psql("postgres",
|
||||
"INSERT INTO tst SELECT i, ARRAY[random(), random(), random()] FROM generate_series(1, 10000) i;"
|
||||
);
|
||||
|
||||
# Add index
|
||||
$node->safe_psql("postgres", "CREATE INDEX ON tst USING hnsw (v minivec_l2_ops) WITH (m = 4, ef_construction = 8);");
|
||||
|
||||
# Delete data
|
||||
$node->safe_psql("postgres", "DELETE FROM tst WHERE i > 2500;");
|
||||
|
||||
# Generate queries
|
||||
for (1 .. 20)
|
||||
{
|
||||
my $r1 = rand();
|
||||
my $r2 = rand();
|
||||
my $r3 = rand();
|
||||
push(@queries, "[$r1,$r2,$r3]");
|
||||
}
|
||||
|
||||
# Get exact results
|
||||
@expected = ();
|
||||
foreach (@queries)
|
||||
{
|
||||
my $res = $node->safe_psql("postgres", qq(
|
||||
SET enable_indexscan = off;
|
||||
SELECT i FROM tst ORDER BY v <-> '$_' LIMIT $limit;
|
||||
));
|
||||
push(@expected, $res);
|
||||
}
|
||||
|
||||
test_recall(0.18, $limit, "before vacuum");
|
||||
test_recall(0.93, 100, "before vacuum");
|
||||
|
||||
# TODO Test concurrent inserts with vacuum
|
||||
$node->safe_psql("postgres", "VACUUM tst;");
|
||||
|
||||
test_recall(0.95, $limit, "after vacuum");
|
||||
|
||||
done_testing();
|
||||
54
test/t/041_ivfflat_iterative_scan.pl
Normal file
54
test/t/041_ivfflat_iterative_scan.pl
Normal file
@@ -0,0 +1,54 @@
|
||||
use strict;
|
||||
use warnings FATAL => 'all';
|
||||
use PostgreSQL::Test::Cluster;
|
||||
use PostgreSQL::Test::Utils;
|
||||
use Test::More;
|
||||
|
||||
my $dim = 3;
|
||||
my $array_sql = join(",", ('random()') x $dim);
|
||||
|
||||
# Initialize node
|
||||
my $node = PostgreSQL::Test::Cluster->new('node');
|
||||
$node->init;
|
||||
$node->start;
|
||||
|
||||
# Create table
|
||||
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
|
||||
$node->safe_psql("postgres", "CREATE TABLE tst (i int4 PRIMARY KEY, v vector($dim));");
|
||||
$node->safe_psql("postgres",
|
||||
"INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(1, 100000) i;"
|
||||
);
|
||||
$node->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v vector_l2_ops);");
|
||||
|
||||
my $count = $node->safe_psql("postgres", qq(
|
||||
SET enable_seqscan = off;
|
||||
SET ivfflat.probes = 10;
|
||||
SET ivfflat.iterative_scan = relaxed_order;
|
||||
SELECT COUNT(*) FROM (SELECT v FROM tst WHERE i % 10000 = 0 ORDER BY v <-> (SELECT v FROM tst LIMIT 1) LIMIT 11) t;
|
||||
));
|
||||
is($count, 10);
|
||||
|
||||
foreach ((30, 50, 70))
|
||||
{
|
||||
my $max_probes = $_;
|
||||
my $expected = $max_probes / 10;
|
||||
my $sum = 0;
|
||||
|
||||
for my $i (1 .. 20)
|
||||
{
|
||||
$count = $node->safe_psql("postgres", qq(
|
||||
SET enable_seqscan = off;
|
||||
SET ivfflat.probes = 10;
|
||||
SET ivfflat.iterative_scan = relaxed_order;
|
||||
SET ivfflat.max_probes = $max_probes;
|
||||
SELECT COUNT(*) FROM (SELECT v FROM tst WHERE i % 10000 = 0 ORDER BY v <-> (SELECT v FROM tst WHERE i = $i) LIMIT 11) t;
|
||||
));
|
||||
$sum += $count;
|
||||
}
|
||||
|
||||
my $avg = $sum / 20;
|
||||
cmp_ok($avg, '>', $expected - 2);
|
||||
cmp_ok($avg, '<', $expected + 2);
|
||||
}
|
||||
|
||||
done_testing();
|
||||
@@ -1,58 +0,0 @@
|
||||
use strict;
|
||||
use warnings FATAL => 'all';
|
||||
use PostgreSQL::Test::Cluster;
|
||||
use PostgreSQL::Test::Utils;
|
||||
use Test::More;
|
||||
|
||||
# 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 (v minivec(3));");
|
||||
|
||||
sub insert_vectors
|
||||
{
|
||||
for my $i (1 .. 20)
|
||||
{
|
||||
$node->safe_psql("postgres", "INSERT INTO tst VALUES ('[1,1,1]');");
|
||||
}
|
||||
}
|
||||
|
||||
sub test_duplicates
|
||||
{
|
||||
my $res = $node->safe_psql("postgres", qq(
|
||||
SET enable_seqscan = off;
|
||||
SET hnsw.ef_search = 1;
|
||||
SELECT COUNT(*) FROM (SELECT * FROM tst ORDER BY v <-> '[1,1,1]') t;
|
||||
));
|
||||
is($res, 10);
|
||||
}
|
||||
|
||||
# Test duplicates with build
|
||||
insert_vectors();
|
||||
$node->safe_psql("postgres", "CREATE INDEX idx ON tst USING hnsw (v minivec_l2_ops);");
|
||||
test_duplicates();
|
||||
|
||||
# Reset
|
||||
$node->safe_psql("postgres", "TRUNCATE tst;");
|
||||
|
||||
# Test duplicates with inserts
|
||||
insert_vectors();
|
||||
test_duplicates();
|
||||
|
||||
# Test fallback path for inserts
|
||||
$node->pgbench(
|
||||
"--no-vacuum --client=5 --transactions=100",
|
||||
0,
|
||||
[qr{actually processed}],
|
||||
[qr{^$}],
|
||||
"concurrent INSERTs",
|
||||
{
|
||||
"042_hnsw_minivec_duplicates" => "INSERT INTO tst VALUES ('[1,1,1]');"
|
||||
}
|
||||
);
|
||||
|
||||
done_testing();
|
||||
125
test/t/042_ivfflat_iterative_scan_recall.pl
Normal file
125
test/t/042_ivfflat_iterative_scan_recall.pl
Normal file
@@ -0,0 +1,125 @@
|
||||
use strict;
|
||||
use warnings FATAL => 'all';
|
||||
use PostgreSQL::Test::Cluster;
|
||||
use PostgreSQL::Test::Utils;
|
||||
use Test::More;
|
||||
|
||||
my $node;
|
||||
my @queries = ();
|
||||
my @expected;
|
||||
my $limit = 20;
|
||||
my @cs = (100, 1000);
|
||||
|
||||
sub test_recall
|
||||
{
|
||||
my ($c, $probes, $min, $operator) = @_;
|
||||
my $correct = 0;
|
||||
my $total = 0;
|
||||
|
||||
my $explain = $node->safe_psql("postgres", qq(
|
||||
SET enable_seqscan = off;
|
||||
SET ivfflat.probes = $probes;
|
||||
SET ivfflat.iterative_scan = relaxed_order;
|
||||
EXPLAIN ANALYZE SELECT i FROM tst WHERE i % $c = 0 ORDER BY v $operator '$queries[0]' LIMIT $limit;
|
||||
));
|
||||
like($explain, qr/Index Scan using idx on tst/);
|
||||
|
||||
for my $i (0 .. $#queries)
|
||||
{
|
||||
my $actual = $node->safe_psql("postgres", qq(
|
||||
SET enable_seqscan = off;
|
||||
SET ivfflat.probes = $probes;
|
||||
SET ivfflat.iterative_scan = relaxed_order;
|
||||
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 $c");
|
||||
}
|
||||
|
||||
# Initialize node
|
||||
$node = PostgreSQL::Test::Cluster->new('node');
|
||||
$node->init;
|
||||
$node->start;
|
||||
|
||||
# Create table
|
||||
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
|
||||
$node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector(3));");
|
||||
$node->safe_psql("postgres",
|
||||
"INSERT INTO tst SELECT i, ARRAY[random(), random(), random()] FROM generate_series(1, 100000) i;"
|
||||
);
|
||||
|
||||
# Generate queries
|
||||
for (1 .. 20)
|
||||
{
|
||||
my $r1 = rand();
|
||||
my $r2 = rand();
|
||||
my $r3 = rand();
|
||||
push(@queries, "[$r1,$r2,$r3]");
|
||||
}
|
||||
|
||||
# Check each index type
|
||||
my @operators = ("<->", "<=>");
|
||||
my @opclasses = ("vector_l2_ops", "vector_cosine_ops");
|
||||
|
||||
for my $i (0 .. $#operators)
|
||||
{
|
||||
my $operator = $operators[$i];
|
||||
my $opclass = $opclasses[$i];
|
||||
|
||||
$node->safe_psql("postgres", "CREATE INDEX idx ON tst USING ivfflat (v $opclass);");
|
||||
|
||||
foreach (@cs)
|
||||
{
|
||||
my $c = $_;
|
||||
|
||||
# Get exact results
|
||||
@expected = ();
|
||||
foreach (@queries)
|
||||
{
|
||||
my $res = $node->safe_psql("postgres", qq(
|
||||
SET enable_indexscan = off;
|
||||
WITH top AS (
|
||||
SELECT v $operator '$_' AS distance FROM tst WHERE i % $c = 0 ORDER BY distance LIMIT $limit
|
||||
)
|
||||
SELECT i FROM tst WHERE (v $operator '$_') <= (SELECT MAX(distance) FROM top)
|
||||
));
|
||||
push(@expected, $res);
|
||||
}
|
||||
|
||||
if ($c == 100)
|
||||
{
|
||||
test_recall($c, 1, 0.57, $operator);
|
||||
test_recall($c, 10, 0.98, $operator);
|
||||
}
|
||||
else
|
||||
{
|
||||
if ($operator eq "<->")
|
||||
{
|
||||
test_recall($c, 1, 0.80, $operator);
|
||||
}
|
||||
else
|
||||
{
|
||||
test_recall($c, 1, 0.88, $operator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$node->safe_psql("postgres", "DROP INDEX idx;");
|
||||
}
|
||||
|
||||
done_testing();
|
||||
68
test/t/043_hnsw_iterative_scan.pl
Normal file
68
test/t/043_hnsw_iterative_scan.pl
Normal file
@@ -0,0 +1,68 @@
|
||||
use strict;
|
||||
use warnings FATAL => 'all';
|
||||
use PostgreSQL::Test::Cluster;
|
||||
use PostgreSQL::Test::Utils;
|
||||
use Test::More;
|
||||
|
||||
my $dim = 3;
|
||||
my $array_sql = join(",", ('random()') x $dim);
|
||||
|
||||
# Initialize node
|
||||
my $node = PostgreSQL::Test::Cluster->new('node');
|
||||
$node->init;
|
||||
$node->start;
|
||||
|
||||
# Create table
|
||||
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
|
||||
$node->safe_psql("postgres", "CREATE TABLE tst (i int4 PRIMARY KEY, v vector($dim));");
|
||||
$node->safe_psql("postgres",
|
||||
"INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(1, 100000) i;"
|
||||
);
|
||||
$node->safe_psql("postgres", qq(
|
||||
SET maintenance_work_mem = '128MB';
|
||||
SET max_parallel_maintenance_workers = 2;
|
||||
CREATE INDEX ON tst USING hnsw (v vector_l2_ops)
|
||||
));
|
||||
|
||||
my $count = $node->safe_psql("postgres", qq(
|
||||
SET enable_seqscan = off;
|
||||
SET hnsw.iterative_scan = relaxed_order;
|
||||
SET hnsw.max_scan_tuples = 100000;
|
||||
SET hnsw.scan_mem_multiplier = 2;
|
||||
SELECT COUNT(*) FROM (SELECT v FROM tst WHERE i % 10000 = 0 ORDER BY v <-> (SELECT v FROM tst LIMIT 1) LIMIT 11) t;
|
||||
));
|
||||
is($count, 10);
|
||||
|
||||
foreach ((30000, 50000, 70000))
|
||||
{
|
||||
my $max_tuples = $_;
|
||||
my $expected = $max_tuples / 10000;
|
||||
my $sum = 0;
|
||||
|
||||
for my $i (1 .. 20)
|
||||
{
|
||||
$count = $node->safe_psql("postgres", qq(
|
||||
SET enable_seqscan = off;
|
||||
SET hnsw.iterative_scan = relaxed_order;
|
||||
SET hnsw.max_scan_tuples = $max_tuples;
|
||||
SET hnsw.scan_mem_multiplier = 2;
|
||||
SELECT COUNT(*) FROM (SELECT v FROM tst WHERE i % 10000 = 0 ORDER BY v <-> (SELECT v FROM tst WHERE i = $i) LIMIT 11) t;
|
||||
));
|
||||
$sum += $count;
|
||||
}
|
||||
|
||||
my $avg = $sum / 20;
|
||||
cmp_ok($avg, '>', $expected - 2);
|
||||
cmp_ok($avg, '<', $expected + 2);
|
||||
}
|
||||
|
||||
my ($ret, $stdout, $stderr) = $node->psql("postgres", qq(
|
||||
SET enable_seqscan = off;
|
||||
SET hnsw.iterative_scan = relaxed_order;
|
||||
SET client_min_messages = debug1;
|
||||
SET work_mem = '1MB';
|
||||
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 reached memory limit after \d+ tuples/);
|
||||
|
||||
done_testing();
|
||||
@@ -1,154 +0,0 @@
|
||||
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 = 10;
|
||||
my $array_sql = join(",", ('random()') x $dim);
|
||||
|
||||
sub test_recall
|
||||
{
|
||||
my ($probes, $min, $operator) = @_;
|
||||
my $correct = 0;
|
||||
my $total = 0;
|
||||
|
||||
my $explain = $node->safe_psql("postgres", qq(
|
||||
SET enable_seqscan = off;
|
||||
SET ivfflat.probes = $probes;
|
||||
EXPLAIN ANALYZE SELECT i FROM tst ORDER BY v $operator '$queries[0]' LIMIT $limit;
|
||||
));
|
||||
like($explain, qr/Index Scan using idx on tst/);
|
||||
|
||||
for my $i (0 .. $#queries)
|
||||
{
|
||||
my $actual = $node->safe_psql("postgres", qq(
|
||||
SET enable_seqscan = off;
|
||||
SET ivfflat.probes = $probes;
|
||||
SELECT i FROM tst 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 minivec($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 = ("minivec_l2_ops", "minivec_ip_ops", "minivec_cosine_ops");
|
||||
|
||||
for my $i (0 .. $#operators)
|
||||
{
|
||||
my $operator = $operators[$i];
|
||||
my $opclass = $opclasses[$i];
|
||||
|
||||
# Get exact results
|
||||
@expected = ();
|
||||
foreach (@queries)
|
||||
{
|
||||
my $res = $node->safe_psql("postgres", qq(
|
||||
WITH top AS (
|
||||
SELECT v $operator '$_' AS distance FROM tst ORDER BY distance LIMIT $limit
|
||||
)
|
||||
SELECT i FROM tst WHERE (v $operator '$_') <= (SELECT MAX(distance) FROM top)
|
||||
));
|
||||
push(@expected, $res);
|
||||
}
|
||||
|
||||
# Build index serially
|
||||
$node->safe_psql("postgres", qq(
|
||||
SET max_parallel_maintenance_workers = 0;
|
||||
CREATE INDEX idx ON tst USING ivfflat (v $opclass);
|
||||
));
|
||||
|
||||
# Test approximate results
|
||||
if ($operator ne "<#>")
|
||||
{
|
||||
# TODO Fix test (uniform random vectors all have similar inner product)
|
||||
test_recall(1, 0.33, $operator);
|
||||
test_recall(10, 0.93, $operator);
|
||||
}
|
||||
|
||||
# Test probes equals lists
|
||||
if ($operator eq "<=>")
|
||||
{
|
||||
test_recall(100, 0.98, $operator);
|
||||
}
|
||||
else
|
||||
{
|
||||
test_recall(100, 1.00, $operator);
|
||||
}
|
||||
|
||||
$node->safe_psql("postgres", "DROP INDEX idx;");
|
||||
|
||||
# Build index in parallel
|
||||
my ($ret, $stdout, $stderr) = $node->psql("postgres", qq(
|
||||
SET client_min_messages = DEBUG;
|
||||
SET min_parallel_table_scan_size = 1;
|
||||
CREATE INDEX idx ON tst USING ivfflat (v $opclass);
|
||||
));
|
||||
is($ret, 0, $stderr);
|
||||
like($stderr, qr/using \d+ parallel workers/);
|
||||
|
||||
# Test approximate results
|
||||
if ($operator ne "<#>")
|
||||
{
|
||||
# TODO Fix test (uniform random vectors all have similar inner product)
|
||||
test_recall(1, 0.33, $operator);
|
||||
test_recall(10, 0.93, $operator);
|
||||
}
|
||||
|
||||
# Test probes equals lists
|
||||
if ($operator eq "<=>")
|
||||
{
|
||||
test_recall(100, 0.98, $operator);
|
||||
}
|
||||
else
|
||||
{
|
||||
test_recall(100, 1.00, $operator);
|
||||
}
|
||||
|
||||
$node->safe_psql("postgres", "DROP INDEX idx;");
|
||||
}
|
||||
|
||||
done_testing();
|
||||
118
test/t/044_hnsw_iterative_scan_recall.pl
Normal file
118
test/t/044_hnsw_iterative_scan_recall.pl
Normal file
@@ -0,0 +1,118 @@
|
||||
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 = (50, 500);
|
||||
|
||||
sub test_recall
|
||||
{
|
||||
my ($c, $ef_search, $min, $operator, $mode) = @_;
|
||||
my $correct = 0;
|
||||
my $total = 0;
|
||||
|
||||
my $explain = $node->safe_psql("postgres", qq(
|
||||
SET enable_seqscan = off;
|
||||
SET hnsw.ef_search = $ef_search;
|
||||
SET hnsw.iterative_scan = $mode;
|
||||
EXPLAIN ANALYZE SELECT i FROM tst WHERE i % $c = 0 ORDER BY v $operator '$queries[0]' LIMIT $limit;
|
||||
));
|
||||
like($explain, qr/Index Scan using idx on tst/);
|
||||
|
||||
for my $i (0 .. $#queries)
|
||||
{
|
||||
my $actual = $node->safe_psql("postgres", qq(
|
||||
SET enable_seqscan = off;
|
||||
SET hnsw.ef_search = $ef_search;
|
||||
SET hnsw.iterative_scan = $mode;
|
||||
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 $mode $c");
|
||||
}
|
||||
|
||||
# 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, 50000) 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);
|
||||
}
|
||||
|
||||
test_recall($c, 40, 0.99, $operator, "strict_order");
|
||||
test_recall($c, 40, 0.99, $operator, "relaxed_order");
|
||||
}
|
||||
|
||||
$node->safe_psql("postgres", "DROP INDEX idx;");
|
||||
}
|
||||
|
||||
done_testing();
|
||||
Reference in New Issue
Block a user