Compare commits

..

17 Commits

Author SHA1 Message Date
Andrew Kane
e77410a1c7 Improved name [skip ci] 2024-09-24 11:48:34 -07:00
Andrew Kane
33506a645a Added comment [skip ci] 2024-09-24 11:46:44 -07:00
Andrew Kane
8e68625f1c Removed unneeded header [skip ci] 2024-09-24 01:25:48 -07:00
Andrew Kane
d1e2dc8cd4 Fixed lookup table [skip ci] 2024-09-24 01:22:49 -07:00
Andrew Kane
8d75f33eb4 Fixed code [skip ci] 2024-09-24 00:41:34 -07:00
Andrew Kane
d50125c95f Fixed rounding [skip ci] 2024-09-24 00:22:13 -07:00
Andrew Kane
d805378471 Fixed NaN [skip ci] 2024-09-24 00:07:02 -07:00
Andrew Kane
0dd7ace4de Added e2m5 version 2024-09-23 22:04:21 -07:00
Andrew Kane
261ba4a0d9 Added test for IVFFlat [skip ci] 2024-09-23 18:57:13 -07:00
Andrew Kane
4293bc439e Exclude zero vectors for cosine distance to be consistent with other types [skip ci] 2024-09-23 18:55:10 -07:00
Andrew Kane
044f0c6441 Improved precision for cosine distance [skip ci] 2024-09-23 18:53:23 -07:00
Andrew Kane
22efa8ec22 Improved precision for cosine distance [skip ci] 2024-09-23 18:50:10 -07:00
Andrew Kane
0a2803130f Added support for IVFFlat [skip ci] 2024-09-23 18:40:49 -07:00
Andrew Kane
6105a01881 Added more tests [skip ci] 2024-09-23 18:33:08 -07:00
Andrew Kane
9925b92d73 Added more minivec tests [skip ci] 2024-09-23 18:29:17 -07:00
Andrew Kane
e49bae9dc3 Fixed CI 2024-09-23 18:24:21 -07:00
Andrew Kane
eea2c44fae Added minivec type 2024-09-23 18:12:26 -07:00
36 changed files with 3280 additions and 250 deletions

View File

@@ -1,7 +1,6 @@
## 0.8.0 (unreleased)
- Added casts for arrays to `sparsevec`
- Improved cost estimation
- Reduced memory usage for HNSW index scans
- Dropped support for Postgres 12

View File

@@ -1,4 +1,4 @@
ARG PG_MAJOR=17
ARG PG_MAJOR=16
FROM postgres:$PG_MAJOR
ARG PG_MAJOR

View File

@@ -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/ivfvacuum.o src/sparsevec.o src/vector.o
HEADERS = src/halfvec.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/minivec.o src/ivfvacuum.o src/sparsevec.o src/vector.o
HEADERS = src/halfvec.h src/minivec.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 ?= 17
PG_MAJOR ?= 16
.PHONY: docker

View File

@@ -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\sparsevec.obj src\vector.obj
HEADERS = src\halfvec.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\minivec.obj src\sparsevec.obj src\vector.obj
HEADERS = src\halfvec.h src\minivec.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 sparsevec vector_type
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_OPTS = --inputdir=test --load-extension=$(EXTENSION)
# For /arch flags

View File

@@ -52,8 +52,6 @@ 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).
@@ -102,8 +100,6 @@ 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
@@ -149,8 +145,6 @@ 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
@@ -940,6 +934,37 @@ 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.
@@ -989,7 +1014,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/17/bin/pg_config
export PG_CONFIG=/Library/PostgreSQL/16/bin/pg_config
```
Then re-run the installation instructions (run `make clean` before `make` if needed). If `sudo` is needed for `make install`, use:
@@ -1000,11 +1025,11 @@ sudo --preserve-env=PG_CONFIG make install
A few common paths on Mac are:
- 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`
- 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`
Note: Replace `17` with your Postgres server version
Note: Replace `16` with your Postgres server version
### Missing Header
@@ -1013,10 +1038,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-17
sudo apt install postgresql-server-dev-16
```
Note: Replace `17` with your Postgres server version
Note: Replace `16` with your Postgres server version
### Missing SDK
@@ -1049,17 +1074,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:pg17
docker pull pgvector/pgvector:pg16
```
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).
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).
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=17 -t myuser/pgvector .
docker build --pull --build-arg PG_MAJOR=16 -t myuser/pgvector .
```
### Homebrew
@@ -1070,7 +1095,7 @@ With Homebrew Postgres, you can use:
brew install pgvector
```
Note: This only adds it to the `postgresql@17` and `postgresql@14` formulas
Note: This only adds it to the `postgresql@14` formula
### PGXN
@@ -1085,22 +1110,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-17-pgvector
sudo apt install postgresql-16-pgvector
```
Note: Replace `17` with your Postgres server version
Note: Replace `16` 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_17
sudo yum install pgvector_16
# or
sudo dnf install pgvector_17
sudo dnf install pgvector_16
```
Note: Replace `17` with your Postgres server version
Note: Replace `16` with your Postgres server version
### pkg

View File

@@ -1,6 +1,8 @@
-- 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;

View File

@@ -266,12 +266,18 @@ 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;
@@ -647,6 +653,295 @@ 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

View File

@@ -12,7 +12,6 @@
#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)
@@ -100,9 +99,7 @@ hnswcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
{
GenericCosts costs;
int m;
double ratio;
double startupPages;
double spc_seq_page_cost;
int entryLevel;
Relation index;
/* Never use index without order */
@@ -118,71 +115,21 @@ hnswcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
MemSet(&costs, 0, sizeof(costs));
genericcostestimate(root, path, loop_count, &costs);
index = index_open(path->indexinfo->indexoid, NoLock);
HnswGetMetaPageInfo(index, &m, NULL);
index_close(index, NoLock);
/*
* 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)));
/* Approximate entry level */
entryLevel = (int) -log(1.0 / path->indexinfo->tuples) * HnswGetMl(m);
ratio = (entryLevel * m + layer0TuplesMax * layer0Selectivity) / path->indexinfo->tuples;
/* 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;
if (ratio > 1)
ratio = 1;
}
else
ratio = 1;
genericcostestimate(root, path, loop_count, &costs);
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;
/* Use total cost since most work happens before first tuple is returned */
*indexStartupCost = costs.indexTotalCost;
*indexTotalCost = costs.indexTotalCost;
*indexSelectivity = costs.indexSelectivity;
*indexCorrelation = costs.indexCorrelation;

View File

@@ -159,6 +159,9 @@ HnswOptionalProcInfo(Relation index, uint16 procnum)
Datum
HnswNormValue(const HnswTypeInfo * typeInfo, Oid collation, Datum value)
{
if (!typeInfo->normalize)
return value;
return DirectFunctionCall1Coll(typeInfo->normalize, collation, value);
}
@@ -759,16 +762,9 @@ HnswLoadUnvisitedFromDisk(HnswElement element, HnswUnvisited * unvisited, int *u
page = BufferGetPage(buf);
ntup = (HnswNeighborTuple) PageGetItem(page, PageGetItemId(page, element->neighborOffno));
/* Ensure expected neighbors */
if (ntup->count != (element->level + 2) * m)
{
UnlockReleaseBuffer(buf);
return;
}
start = (element->level - lc) * m;
/* Copy to minimize lock time */
start = (element->level - lc) * m;
memcpy(&indextids, ntup->indextids + start, lm * sizeof(ItemPointerData));
UnlockReleaseBuffer(buf);
@@ -1334,6 +1330,7 @@ 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
@@ -1382,6 +1379,20 @@ 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)

View File

@@ -69,8 +69,6 @@ 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;
@@ -87,8 +85,6 @@ ivfflatcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
MemSet(&costs, 0, sizeof(costs));
genericcostestimate(root, path, loop_count, &costs);
index = index_open(path->indexinfo->indexoid, NoLock);
IvfflatGetMetaPageInfo(index, &lists, NULL);
index_close(index, NoLock);
@@ -98,26 +94,41 @@ 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 */
startupPages = costs.numIndexPages * ratio;
if (startupPages > path->indexinfo->rel->pages && ratio < 0.5)
if (costs.numIndexPages > path->indexinfo->rel->pages && ratio < 0.5)
{
/* Change rest of page cost from random to sequential */
costs.indexStartupCost -= (1 - sequentialRatio) * startupPages * (costs.spc_random_page_cost - spc_seq_page_cost);
/* Change all page cost from random to sequential */
costs.indexTotalCost -= costs.numIndexPages * (costs.spc_random_page_cost - spc_seq_page_cost);
/* Remove cost of extra pages */
costs.indexStartupCost -= (startupPages - path->indexinfo->rel->pages) * spc_seq_page_cost;
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);
}
*indexStartupCost = costs.indexStartupCost;
/*
* If the list selectivity is lower than what is returned from the generic
* cost estimator, use that.
*/
if (ratio < costs.indexSelectivity)
costs.indexSelectivity = ratio;
/* Use total cost since most work happens before first tuple is returned */
*indexStartupCost = costs.indexTotalCost;
*indexTotalCost = costs.indexTotalCost;
*indexSelectivity = costs.indexSelectivity;
*indexCorrelation = costs.indexCorrelation;

View File

@@ -7,6 +7,7 @@
#include "halfutils.h"
#include "halfvec.h"
#include "ivfflat.h"
#include "minivec.h"
#include "storage/bufmgr.h"
/*
@@ -70,6 +71,9 @@ 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);
}
@@ -231,6 +235,7 @@ 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
@@ -245,6 +250,12 @@ HalfvecItemSize(int dimensions)
return HALFVEC_SIZE(dimensions);
}
static Size
MinivecItemSize(int dimensions)
{
return MINIVEC_SIZE(dimensions);
}
static Size
BitItemSize(int dimensions)
{
@@ -275,6 +286,18 @@ 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)
{
@@ -309,6 +332,15 @@ 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)
{
@@ -357,6 +389,22 @@ 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)

1062
src/minivec.c Normal file

File diff suppressed because it is too large Load Diff

177
src/minivec.h Normal file
View File

@@ -0,0 +1,177 @@
#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 & 0x7C) == 0x7C && (num & 0x7F) != 0x7C;
}
/*
* Check if fp8 is infinite
*/
static inline bool
Fp8IsInf(fp8 num)
{
return (num & 0x7F) == 0x7C;
}
/*
* 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, 931135488, 939524096, 943718400, 947912704, 950009856, 952107008, 954204160, 956301312, 958398464, 960495616, 962592768, 964689920, 966787072, 968884224, 970981376, 973078528, 975175680, 977272832, 979369984, 981467136, 983564288, 985661440, 987758592, 989855744, 991952896, 994050048, 996147200, 998244352, 1000341504, 1002438656, 1004535808, 1006632960, 1008730112, 1010827264, 1012924416, 1015021568, 1017118720, 1019215872, 1021313024, 1023410176, 1025507328, 1027604480, 1029701632, 1031798784, 1033895936, 1035993088, 1038090240, 1040187392, 1042284544, 1044381696, 1046478848, 1048576000, 1050673152, 1052770304, 1054867456, 1056964608, 1059061760, 1061158912, 1063256064, 1065353216, 1067450368, 1069547520, 1071644672, 1073741824, 1075838976, 1077936128, 1080033280, 1082130432, 1084227584, 1086324736, 1088421888, 1090519040, 1092616192, 1094713344, 1096810496, 1098907648, 1101004800, 1103101952, 1105199104, 1107296256, 1109393408, 1111490560, 1113587712, 1115684864, 1117782016, 1119879168, 1121976320, 1124073472, 1126170624, 1128267776, 1130364928, 1132462080, 1134559232, 1136656384, 1138753536, 1140850688, 1142947840, 1145044992, 1147142144, 1149239296, 1151336448, 1153433600, 1155530752, 1157627904, 1159725056, 1161822208, 1163919360, 1166016512, 1168113664, 1170210816, 1172307968, 1174405120, 1176502272, 1178599424, 1180696576, 1182793728, 1184890880, 1186988032, 1189085184, 1191182336, 1193279488, 1195376640, 1197473792, 2139095040, 2145386496, 2143289344, 2145386496};
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))
{
/* Infinite */
result |= 0x7C;
}
else if (isnan(num))
{
/* NaN */
result |= 0x7F;
}
else if (exponent > 98)
{
int m;
int gr;
int s;
exponent -= 127;
s = mantissa & 0x000FFFFF;
/* Subnormal */
if (exponent < -14)
{
int diff = -exponent - 14;
mantissa >>= diff;
mantissa += 1 << (23 - diff);
s |= mantissa & 0x000FFFFF;
}
m = mantissa >> 21;
/* Round */
gr = (mantissa >> 20) % 4;
if (gr == 3 || (gr == 1 && s != 0))
m += 1;
if (m == 4)
{
m = 0;
exponent += 1;
}
if (exponent > 15)
{
/* Infinite */
result |= 0x7C;
}
else
{
if (exponent >= -14)
result |= (exponent + 15) << 2;
result |= m;
}
}
return result;
}
/*
* Convert a float4 to a fp8
*/
static inline fp8
Float4ToFp8(float num)
{
fp8 result = Float4ToFp8Unchecked(num);
if (unlikely(Fp8IsInf(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

View File

@@ -13,6 +13,7 @@
#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"
@@ -542,6 +543,28 @@ 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)
{

View File

@@ -38,6 +38,26 @@ 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));

View File

@@ -140,6 +140,64 @@ 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 '[61440]'::vector::minivec;
ERROR: "61440" 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 '{61440,-61440}'::real[]::minivec;
ERROR: "61440" 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
-----------------

View File

@@ -30,6 +30,23 @@ 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

View File

@@ -0,0 +1,84 @@
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;

590
test/expected/minivec.out Normal file
View File

@@ -0,0 +1,590 @@
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: infinite value not allowed in minivec
LINE 1: SELECT '[Infinity,1]'::minivec;
^
SELECT '[-Infinity,1]'::minivec;
ERROR: infinite value not allowed in minivec
LINE 1: SELECT '[-Infinity,1]'::minivec;
^
SELECT '[61439,-61439]'::minivec;
minivec
----------------
[57344,-57344]
(1 row)
SELECT '[61440,-61440]'::minivec;
ERROR: "61440" is out of range for type minivec
LINE 1: SELECT '[61440,-61440]'::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,8]
(1 row)
SELECT '[61439]'::minivec + '[61439]';
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]';
ERROR: value out of range: overflow
SELECT '[-61439]'::minivec - '[61439]';
?column?
----------
[-0]
(1 row)
SELECT '[1,2]'::minivec - '[3]';
ERROR: different minivec dimensions 2 and 1
SELECT '[1,2,3]'::minivec * '[4,5,6]';
?column?
-----------
[4,10,16]
(1 row)
SELECT '[61439]'::minivec * '[61439]';
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
---------------
44
(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
-------------
7
(1 row)
SELECT '[0,0]'::minivec <+> '[3,4]';
?column?
----------
7
(1 row)
SELECT l2_normalize('[3,4]'::minivec);
l2_normalize
--------------
[0.625,0.75]
(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)

View File

@@ -22,6 +22,17 @@ 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));

View File

@@ -38,6 +38,22 @@ 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 '[61440]'::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 '{61440,-61440}'::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);

View File

@@ -28,6 +28,21 @@ 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));

View File

@@ -0,0 +1,45 @@
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;

134
test/sql/minivec.sql Normal file
View File

@@ -0,0 +1,134 @@
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 '[61439,-61439]'::minivec;
SELECT '[61440,-61440]'::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 '[61439]'::minivec + '[61439]';
SELECT '[1,2]'::minivec + '[3]';
SELECT '[1,2,3]'::minivec - '[4,5,6]';
SELECT '[-61439]'::minivec - '[61439]';
SELECT '[1,2]'::minivec - '[3]';
SELECT '[1,2,3]'::minivec * '[4,5,6]';
SELECT '[61439]'::minivec * '[61439]';
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);

View File

@@ -94,7 +94,8 @@ 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';
));
like($explain, qr/Seq Scan/);
# TODO Do not use index
like($explain, qr/Index Scan using idx/);
# Test attribute index
$node->safe_psql("postgres", "CREATE INDEX attribute_idx ON tst (c);");
@@ -109,6 +110,7 @@ $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;
));
like($explain, qr/Index Scan using partial_idx/);
# TODO Use partial index
like($explain, qr/Index Scan using idx/);
done_testing();

View File

@@ -18,13 +18,9 @@ $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;");
@@ -41,7 +37,8 @@ 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;
));
like($explain, qr/Seq Scan/);
# TODO Do not use index
like($explain, qr/Index Scan using idx/);
# Test attribute filtering with few rows removed
$explain = $node->safe_psql("postgres", qq(
@@ -59,7 +56,8 @@ 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;
));
like($explain, qr/Seq Scan/);
# TODO Do not use index
like($explain, qr/Index Scan using idx/);
# Test attribute filtering with few rows removed like
$explain = $node->safe_psql("postgres", qq(
@@ -98,25 +96,13 @@ $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;
));
# Use attribute index
like($explain, qr/Bitmap Index Scan on attribute_idx/);
# TODO Use attribute index
like($explain, qr/Index Scan using idx/);
# Test partial index
$node->safe_psql("postgres", "CREATE INDEX partial_idx ON tst USING hnsw (v vector_l2_ops) WHERE (c = $c);");

View File

@@ -40,6 +40,10 @@ 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);

View File

@@ -45,6 +45,10 @@ 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");

View File

@@ -12,8 +12,8 @@ $node->start;
# Create extension
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
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 @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 @subs = (" ", " ", ",", ":", "-", "1", "9", "\0", "2147483648", "-2147483649");
for my $i (0 .. $#types)

View File

@@ -1,60 +0,0 @@
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();

View File

@@ -0,0 +1,132 @@
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();

View File

@@ -0,0 +1,113 @@
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();

View File

@@ -1,50 +0,0 @@
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();

View File

@@ -0,0 +1,97 @@
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();

View File

@@ -0,0 +1,58 @@
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();

View File

@@ -0,0 +1,154 @@
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();