Compare commits

..

8 Commits

Author SHA1 Message Date
Andrew Kane
bb127fce8d Try again 2022-12-09 02:01:39 -08:00
Andrew Kane
7ce4e1f53e Skip IPC::Run 2022-12-09 01:57:10 -08:00
Andrew Kane
6e41c81412 Skip tests 2022-12-09 01:06:03 -08:00
Andrew Kane
98ec05145b Install IPC::Run 2022-12-09 00:57:28 -08:00
Andrew Kane
e302c99d4a Extract tar 2022-12-09 00:52:34 -08:00
Andrew Kane
d6ab29e772 Try again 2022-12-09 00:47:45 -08:00
Andrew Kane
6ad96fe8ca Use set 2022-12-08 23:05:11 -08:00
Andrew Kane
e52ef4f2b5 Added prove_installcheck for Windows 2022-12-08 22:57:46 -08:00
80 changed files with 654 additions and 7320 deletions

View File

@@ -1,6 +1,6 @@
root = true root = true
[*.{c,h,pl,pm,sql}] [*.{c,h,pl,pm}]
indent_style = tab indent_style = tab
indent_size = tab indent_size = tab
tab_width = 4 tab_width = 4

View File

@@ -2,24 +2,12 @@ name: build
on: [push, pull_request] on: [push, pull_request]
jobs: jobs:
ubuntu: ubuntu:
runs-on: ${{ matrix.os }} runs-on: ubuntu-latest
if: ${{ !startsWith(github.ref_name, 'mac') && !startsWith(github.ref_name, 'windows') }} if: ${{ !startsWith(github.ref_name, 'windows') }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: postgres: [15, 14, 13, 12, 11, 10]
- postgres: 16
os: ubuntu-22.04
- postgres: 15
os: ubuntu-22.04
- postgres: 14
os: ubuntu-22.04
- postgres: 13
os: ubuntu-20.04
- postgres: 12
os: ubuntu-20.04
- postgres: 11
os: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: ankane/setup-postgres@v1 - uses: ankane/setup-postgres@v1
@@ -27,8 +15,6 @@ jobs:
postgres-version: ${{ matrix.postgres }} postgres-version: ${{ matrix.postgres }}
dev-files: true dev-files: true
- run: make - run: make
env:
PG_CFLAGS: -Wall -Wextra -Werror -Wno-unused-parameter -Wno-sign-compare
- run: | - run: |
export PG_CONFIG=`which pg_config` export PG_CONFIG=`which pg_config`
sudo --preserve-env=PG_CONFIG make install sudo --preserve-env=PG_CONFIG make install
@@ -38,7 +24,7 @@ jobs:
- run: | - run: |
sudo apt-get update sudo apt-get update
sudo apt-get install libipc-run-perl sudo apt-get install libipc-run-perl
- run: make prove_installcheck make prove_installcheck
mac: mac:
runs-on: macos-latest runs-on: macos-latest
if: ${{ !startsWith(github.ref_name, 'windows') }} if: ${{ !startsWith(github.ref_name, 'windows') }}
@@ -48,53 +34,32 @@ jobs:
with: with:
postgres-version: 14 postgres-version: 14
- run: make - run: make
env:
PG_CFLAGS: -Wall -Wextra -Werror -Wno-unused-parameter
- run: make install - run: make install
- run: make installcheck - run: make installcheck
- if: ${{ failure() }} - if: ${{ failure() }}
run: cat regression.diffs run: cat regression.diffs
- run: | - run: |
brew install cpanm brew install cpanm
cpanm --notest IPC::Run cpanm IPC::Run
wget -q https://github.com/postgres/postgres/archive/refs/tags/REL_14_5.tar.gz wget -q https://github.com/postgres/postgres/archive/refs/tags/REL_14_5.tar.gz
tar xf REL_14_5.tar.gz tar xf REL_14_5.tar.gz
- run: make prove_installcheck PROVE_FLAGS="-I ./postgres-REL_14_5/src/test/perl" PERL5LIB="/Users/runner/perl5/lib/perl5" make prove_installcheck PROVE=prove PROVE_FLAGS="-I ./postgres-REL_14_5/src/test/perl" PERL5LIB="/Users/runner/perl5/lib/perl5"
- run: make clean && /usr/local/opt/llvm@15/bin/scan-build --status-bugs make
windows: windows:
runs-on: windows-latest runs-on: windows-latest
if: ${{ !startsWith(github.ref_name, 'mac') }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: ankane/setup-postgres@v1 - uses: ankane/setup-postgres@v1
with: with:
postgres-version: 14 postgres-version: 14
- run: | - run: |
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" && ^ call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
nmake /NOLOGO /F Makefile.win && ^ nmake /NOLOGO /F Makefile.win
nmake /NOLOGO /F Makefile.win install && ^ nmake /NOLOGO /F Makefile.win install
nmake /NOLOGO /F Makefile.win installcheck && ^ curl -Ls -o REL_14_5.tar.gz https://github.com/postgres/postgres/archive/refs/tags/REL_14_5.tar.gz
nmake /NOLOGO /F Makefile.win clean && ^ 7z x REL_14_5.tar.gz
nmake /NOLOGO /F Makefile.win uninstall 7z x REL_14_5.tar
ls ./postgres-REL_14_5/src/test/perl
set PROVE=prove
set PROVE_FLAGS="-I ./postgres-REL_14_5/src/test/perl"
nmake /NOLOGO /F Makefile.win prove_installcheck
shell: cmd shell: cmd
i386:
if: ${{ !startsWith(github.ref_name, 'mac') && !startsWith(github.ref_name, 'windows') }}
runs-on: ubuntu-latest
container:
image: debian:11
options: --platform linux/386
steps:
- run: apt-get update && apt-get install -y build-essential git libipc-run-perl postgresql-13 postgresql-server-dev-13 sudo
- run: service postgresql start
- run: |
git clone https://github.com/${{ github.repository }}.git pgvector
cd pgvector
git fetch origin ${{ github.ref }}
git reset --hard FETCH_HEAD
make
make install
chown -R postgres .
sudo -u postgres make installcheck
sudo -u postgres make prove_installcheck
env:
PG_CFLAGS: -Wall -Wextra -Werror -Wno-unused-parameter -Wno-sign-compare

6
.gitignore vendored
View File

@@ -1,5 +1,4 @@
/dist/ /dist/
/log/
/results/ /results/
/tmp_check/ /tmp_check/
/sql/vector--?.?.?.sql /sql/vector--?.?.?.sql
@@ -7,8 +6,3 @@ regression.*
*.o *.o
*.so *.so
*.bc *.bc
*.dll
*.dylib
*.obj
*.lib
*.exp

View File

@@ -1,58 +1,8 @@
## 0.5.1 (unreleased) ## 0.4.0 (unreleased)
- Improved performance of index scans for IVFFlat after updates and deletes
## 0.5.0 (2023-08-28)
- Added HNSW index type
- Added support for parallel index builds for IVFFlat
- Added `l1_distance` function
- Added element-wise multiplication for vectors
- Added `sum` aggregate
- Improved performance of distance functions
- Fixed out of range results for cosine distance
- Fixed results for NULL and NaN distances for IVFFlat
## 0.4.4 (2023-06-12)
- Improved error message for malformed vector literal
- Fixed segmentation fault with text input
- Fixed consecutive delimiters with text input
## 0.4.3 (2023-06-10)
- Improved cost estimation
- Improved support for spaces with text input
- Fixed infinite and NaN values with binary input
- Fixed infinite values with vector addition and subtraction
- Fixed infinite values with list centers
- Fixed compilation error when `float8` is pass by reference
- Fixed compilation error on PowerPC
- Fixed segmentation fault with index creation on i386
## 0.4.2 (2023-05-13)
- Added notice when index created with little data
- Fixed dimensions check for some direct function calls
- Fixed installation error with Postgres 12.0-12.2
## 0.4.1 (2023-03-21)
- Improved performance of cosine distance
- Fixed index scan count
## 0.4.0 (2023-01-11)
If upgrading with Postgres < 13, see [this note](https://github.com/pgvector/pgvector#040).
- Changed text representation for vector elements to match `real` - Changed text representation for vector elements to match `real`
- Changed storage for vector from `plain` to `extended`
- Increased max dimensions for vector from 1024 to 16000
- Increased max dimensions for index from 1024 to 2000
- Improved accuracy of text parsing for certain inputs - Improved accuracy of text parsing for certain inputs
- Added `avg` aggregate for vector
- Added experimental support for Windows - Added experimental support for Windows
- Dropped support for Postgres 10
## 0.3.2 (2022-11-22) ## 0.3.2 (2022-11-22)

View File

@@ -1,12 +1,9 @@
ARG PG_MAJOR=15 FROM postgres:15
FROM postgres:$PG_MAJOR
ARG PG_MAJOR
COPY . /tmp/pgvector COPY . /tmp/pgvector
RUN apt-get update && \ RUN apt-get update && \
apt-mark hold locales && \ apt-get install -y --no-install-recommends build-essential postgresql-server-dev-15 && \
apt-get install -y --no-install-recommends build-essential postgresql-server-dev-$PG_MAJOR && \
cd /tmp/pgvector && \ cd /tmp/pgvector && \
make clean && \ make clean && \
make OPTFLAGS="" && \ make OPTFLAGS="" && \
@@ -14,7 +11,6 @@ RUN apt-get update && \
mkdir /usr/share/doc/pgvector && \ mkdir /usr/share/doc/pgvector && \
cp LICENSE README.md /usr/share/doc/pgvector && \ cp LICENSE README.md /usr/share/doc/pgvector && \
rm -r /tmp/pgvector && \ rm -r /tmp/pgvector && \
apt-get remove -y build-essential postgresql-server-dev-$PG_MAJOR && \ apt-get remove -y build-essential postgresql-server-dev-15 && \
apt-get autoremove -y && \ apt-get autoremove -y && \
apt-mark unhold locales && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*

View File

@@ -1,4 +1,4 @@
Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
Portions Copyright (c) 1994, The Regents of the University of California Portions Copyright (c) 1994, The Regents of the University of California

View File

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

View File

@@ -1,10 +1,9 @@
EXTENSION = vector EXTENSION = vector
EXTVERSION = 0.5.0 EXTVERSION = 0.3.2
MODULE_big = vector MODULE_big = vector
DATA = $(wildcard sql/*--*.sql) DATA = $(wildcard sql/*--*.sql)
OBJS = src/hnsw.o src/hnswbuild.o src/hnswinsert.o src/hnswscan.o src/hnswutils.o src/hnswvacuum.o src/ivfbuild.o src/ivfflat.o src/ivfinsert.o src/ivfkmeans.o src/ivfscan.o src/ivfutils.o src/ivfvacuum.o src/vector.o OBJS = src/ivfbuild.o src/ivfflat.o src/ivfinsert.o src/ivfkmeans.o src/ivfscan.o src/ivfutils.o src/ivfvacuum.o src/vector.o
HEADERS = src/vector.h
TESTS = $(wildcard test/sql/*.sql) TESTS = $(wildcard test/sql/*.sql)
REGRESS = $(patsubst test/sql/%.sql,%,$(TESTS)) REGRESS = $(patsubst test/sql/%.sql,%,$(TESTS))
@@ -15,16 +14,10 @@ OPTFLAGS = -march=native
# Mac ARM doesn't support -march=native # Mac ARM doesn't support -march=native
ifeq ($(shell uname -s), Darwin) ifeq ($(shell uname -s), Darwin)
ifeq ($(shell uname -p), arm) ifeq ($(shell uname -p), arm)
# no difference with -march=armv8.5-a
OPTFLAGS = OPTFLAGS =
endif endif
endif endif
# PowerPC doesn't support -march=native
ifneq ($(filter ppc64%, $(shell uname -m)), )
OPTFLAGS =
endif
# For auto-vectorization: # For auto-vectorization:
# - GCC (needs -ftree-vectorize OR -O3) - https://gcc.gnu.org/projects/tree-ssa/vectorization.html # - GCC (needs -ftree-vectorize OR -O3) - https://gcc.gnu.org/projects/tree-ssa/vectorization.html
# - Clang (could use pragma instead) - https://llvm.org/docs/Vectorizers.html # - Clang (could use pragma instead) - https://llvm.org/docs/Vectorizers.html
@@ -47,11 +40,6 @@ PG_CONFIG ?= pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs) PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS) include $(PGXS)
# for Mac
ifeq ($(PROVE),)
PROVE = prove
endif
# for Postgres 15 # for Postgres 15
PROVE_FLAGS += -I ./test/perl PROVE_FLAGS += -I ./test/perl
@@ -68,10 +56,4 @@ dist:
.PHONY: docker .PHONY: docker
docker: docker:
docker build --pull --no-cache --platform linux/amd64 -t ankane/pgvector:latest . docker build --pull --no-cache -t ankane/pgvector:latest .
.PHONY: docker-release
docker-release:
docker buildx build --push --pull --no-cache --platform linux/amd64,linux/arm64 -t ankane/pgvector:latest .
docker buildx build --push --platform linux/amd64,linux/arm64 -t ankane/pgvector:v$(EXTVERSION) .

View File

@@ -1,8 +1,7 @@
EXTENSION = vector EXTENSION = vector
EXTVERSION = 0.5.0 EXTVERSION = 0.3.2
OBJS = src\hnsw.obj src\hnswbuild.obj src\hnswinsert.obj src\hnswscan.obj src\hnswutils.obj src\hnswvacuum.obj src\ivfbuild.obj src\ivfflat.obj src\ivfinsert.obj src\ivfkmeans.obj src\ivfscan.obj src\ivfutils.obj src\ivfvacuum.obj src\vector.obj OBJS = src\ivfbuild.obj src\ivfflat.obj src\ivfinsert.obj src\ivfkmeans.obj src\ivfscan.obj src\ivfutils.obj src\ivfvacuum.obj src\vector.obj
HEADERS = src\vector.h
REGRESS = btree cast copy functions input ivfflat_cosine ivfflat_ip ivfflat_l2 ivfflat_options ivfflat_unlogged REGRESS = btree cast copy functions input ivfflat_cosine ivfflat_ip ivfflat_l2 ivfflat_options ivfflat_unlogged
REGRESS_OPTS = --inputdir=test --load-extension=vector REGRESS_OPTS = --inputdir=test --load-extension=vector
@@ -25,9 +24,6 @@ sql\$(EXTENSION)--$(EXTVERSION).sql: sql\$(EXTENSION).sql
copy sql\$(EXTENSION).sql $@ copy sql\$(EXTENSION).sql $@
# TODO use pg_config # TODO use pg_config
!ifndef PGROOT
!error PGROOT is not set
!endif
BINDIR = $(PGROOT)\bin BINDIR = $(PGROOT)\bin
INCLUDEDIR = $(PGROOT)\include INCLUDEDIR = $(PGROOT)\include
INCLUDEDIR_SERVER = $(PGROOT)\include\server INCLUDEDIR_SERVER = $(PGROOT)\include\server
@@ -39,7 +35,7 @@ CFLAGS = /nologo /I"$(INCLUDEDIR_SERVER)\port\win32_msvc" /I"$(INCLUDEDIR_SERVER
CFLAGS = $(CFLAGS) $(PG_CFLAGS) CFLAGS = $(CFLAGS) $(PG_CFLAGS)
SHLIB = $(EXTENSION).dll SHLIB = src\$(EXTENSION).dll
LIBS = "$(LIBDIR)\postgres.lib" LIBS = "$(LIBDIR)\postgres.lib"
@@ -55,21 +51,12 @@ install:
copy $(SHLIB) "$(PKGLIBDIR)" copy $(SHLIB) "$(PKGLIBDIR)"
copy $(EXTENSION).control "$(SHAREDIR)\extension" copy $(EXTENSION).control "$(SHAREDIR)\extension"
copy sql\$(EXTENSION)--*.sql "$(SHAREDIR)\extension" copy sql\$(EXTENSION)--*.sql "$(SHAREDIR)\extension"
mkdir "$(INCLUDEDIR_SERVER)\extension\$(EXTENSION)"
copy $(HEADERS) "$(INCLUDEDIR_SERVER)\extension\$(EXTENSION)"
installcheck: installcheck:
"$(BINDIR)\pg_regress" --bindir="$(BINDIR)" $(REGRESS_OPTS) $(REGRESS) "$(BINDIR)\pg_regress" --bindir="$(BINDIR)" $(REGRESS_OPTS) $(REGRESS)
uninstall: prove_installcheck:
del /f "$(PKGLIBDIR)\$(SHLIB)" rm -rf tmp_check
del /f "$(SHAREDIR)\extension\$(EXTENSION).control" set PGPORT=65432
del /f "$(SHAREDIR)\extension\$(EXTENSION)--*.sql" set PG_REGRESS="$(BINDIR)\pg_regress"
del /f "$(INCLUDEDIR_SERVER)\extension\$(EXTENSION)\*.h" $(PROVE) $(PG_PROVE_FLAGS) $(PROVE_FLAGS) test/t/*.pl
rmdir "$(INCLUDEDIR_SERVER)\extension\$(EXTENSION)"
clean:
del /f $(SHLIB) $(EXTENSION).lib $(EXTENSION).exp
del /f $(OBJS)
del /f sql\$(EXTENSION)--$(EXTVERSION).sql
del /f /s /q results regression.diffs regression.out tmp_check tmp_check_iso log output_iso

552
README.md
View File

@@ -2,276 +2,113 @@
Open-source vector similarity search for Postgres Open-source vector similarity search for Postgres
Store your vectors with the rest of your data. Supports: ```sql
CREATE TABLE items (embedding vector(3));
CREATE INDEX ON items USING ivfflat (embedding vector_l2_ops);
SELECT * FROM items ORDER BY embedding <-> '[1,2,3]' LIMIT 5;
```
- exact and approximate nearest neighbor search Supports L2 distance, inner product, and cosine distance
- L2 distance, inner product, and cosine distance
- any [language](#languages) with a Postgres client
Plus [ACID](https://en.wikipedia.org/wiki/ACID) compliance, point-in-time recovery, JOINs, and all of the other [great features](https://www.postgresql.org/about/) of Postgres
[![Build Status](https://github.com/pgvector/pgvector/workflows/build/badge.svg?branch=master)](https://github.com/pgvector/pgvector/actions) [![Build Status](https://github.com/pgvector/pgvector/workflows/build/badge.svg?branch=master)](https://github.com/pgvector/pgvector/actions)
## Installation ## Installation
Compile and install the extension (supports Postgres 11+) Compile and install the extension (supports Postgres 10+)
```sh ```sh
cd /tmp git clone --branch v0.3.2 https://github.com/pgvector/pgvector.git
git clone --branch v0.5.0 https://github.com/pgvector/pgvector.git
cd pgvector cd pgvector
make make
make install # may need sudo make install # may need sudo
``` ```
See the [installation notes](#installation-notes) if you run into issues Then load it in databases where you want to use it
You can also install it with [Docker](#docker), [Homebrew](#homebrew), [PGXN](#pgxn), [APT](#apt), [Yum](#yum), or [conda-forge](#conda-forge), and it comes preinstalled with [Postgres.app](#postgresapp) and many [hosted providers](#hosted-postgres) ```sql
## Getting Started
Enable the extension (do this once in each database where you want to use it)
```tsql
CREATE EXTENSION vector; CREATE EXTENSION vector;
``` ```
You can also install it with [Docker](#docker), [Homebrew](#homebrew), or [PGXN](#pgxn)
## Getting Started
Create a vector column with 3 dimensions Create a vector column with 3 dimensions
```sql ```sql
CREATE TABLE items (id bigserial PRIMARY KEY, embedding vector(3)); CREATE TABLE items (embedding vector(3));
``` ```
Insert vectors Insert values
```sql ```sql
INSERT INTO items (embedding) VALUES ('[1,2,3]'), ('[4,5,6]'); INSERT INTO items VALUES ('[1,2,3]'), ('[4,5,6]');
``` ```
Get the nearest neighbors by L2 distance Get the nearest neighbor by L2 distance
```sql ```sql
SELECT * FROM items ORDER BY embedding <-> '[3,1,2]' LIMIT 5; SELECT * FROM items ORDER BY embedding <-> '[3,1,2]' LIMIT 1;
``` ```
Also supports inner product (`<#>`) and cosine distance (`<=>`) Also supports inner product (`<#>`) and cosine distance (`<=>`)
Note: `<#>` returns the negative inner product since Postgres only supports `ASC` order index scans on operators Note: `<#>` returns the negative inner product since Postgres only supports `ASC` order index scans on operators
## Storing
Create a new table with a vector column
```sql
CREATE TABLE items (id bigserial PRIMARY KEY, embedding vector(3));
```
Or add a vector column to an existing table
```sql
ALTER TABLE items ADD COLUMN embedding vector(3);
```
Insert vectors
```sql
INSERT INTO items (embedding) VALUES ('[1,2,3]'), ('[4,5,6]');
```
Upsert vectors
```sql
INSERT INTO items (id, embedding) VALUES (1, '[1,2,3]'), (2, '[4,5,6]')
ON CONFLICT (id) DO UPDATE SET embedding = EXCLUDED.embedding;
```
Update vectors
```sql
UPDATE items SET embedding = '[1,2,3]' WHERE id = 1;
```
Delete vectors
```sql
DELETE FROM items WHERE id = 1;
```
## Querying
Get the nearest neighbors to a vector
```sql
SELECT * FROM items ORDER BY embedding <-> '[3,1,2]' LIMIT 5;
```
Get the nearest neighbors to a row
```sql
SELECT * FROM items WHERE id != 1 ORDER BY embedding <-> (SELECT embedding FROM items WHERE id = 1) LIMIT 5;
```
Get rows within a certain distance
```sql
SELECT * FROM items WHERE embedding <-> '[3,1,2]' < 5;
```
Note: Combine with `ORDER BY` and `LIMIT` to use an index
#### Distances
Get the distance
```sql
SELECT embedding <-> '[3,1,2]' AS distance FROM items;
```
For inner product, multiply by -1 (since `<#>` returns the negative inner product)
```tsql
SELECT (embedding <#> '[3,1,2]') * -1 AS inner_product FROM items;
```
For cosine similarity, use 1 - cosine distance
```sql
SELECT 1 - (embedding <=> '[3,1,2]') AS cosine_similarity FROM items;
```
#### Aggregates
Average vectors
```sql
SELECT AVG(embedding) FROM items;
```
Average groups of vectors
```sql
SELECT category_id, AVG(embedding) FROM items GROUP BY category_id;
```
## Indexing ## Indexing
By default, pgvector performs exact nearest neighbor search, which provides perfect recall. Speed up queries with an approximate index. Add an index for each distance function you want to use.
You can add an index to use approximate nearest neighbor search, which trades some recall for speed. Unlike typical indexes, you will see different results for queries after adding an approximate index.
Supported index types are:
- [IVFFlat](#ivfflat)
- [HNSW](#hnsw) - added in 0.5.0
## IVFFlat
An IVFFlat index divides vectors into lists, and then searches a subset of those lists that are closest to the query vector. It has faster build times and uses less memory than HNSW, but has lower query performance (in terms of speed-recall tradeoff).
Three keys to achieving good recall are:
1. Create the index *after* the table has some data
2. Choose an appropriate number of lists - a good place to start is `rows / 1000` for up to 1M rows and `sqrt(rows)` for over 1M rows
3. When querying, specify an appropriate number of [probes](#query-options) (higher is better for recall, lower is better for speed) - a good place to start is `sqrt(lists)`
Add an index for each distance function you want to use.
L2 distance L2 distance
```sql ```sql
CREATE INDEX ON items USING ivfflat (embedding vector_l2_ops) WITH (lists = 100); CREATE INDEX ON items USING ivfflat (embedding vector_l2_ops);
``` ```
Inner product Inner product
```sql ```sql
CREATE INDEX ON items USING ivfflat (embedding vector_ip_ops) WITH (lists = 100); CREATE INDEX ON items USING ivfflat (embedding vector_ip_ops);
``` ```
Cosine distance Cosine distance
```sql ```sql
CREATE INDEX ON items USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); CREATE INDEX ON items USING ivfflat (embedding vector_cosine_ops);
``` ```
Vectors with up to 2,000 dimensions can be indexed. Indexes should be created after the table has some data for optimal clustering. Also, unlike typical indexes which only affect performance, you may see different results for queries after adding an approximate index.
### Index Options
Specify the number of inverted lists (100 by default)
```sql
CREATE INDEX ON items USING ivfflat (embedding vector_l2_ops) WITH (lists = 100);
```
A [good place to start](https://github.com/facebookresearch/faiss/issues/112) is `4 * sqrt(rows)`
### Query Options ### Query Options
Specify the number of probes (1 by default) Specify the number of probes (1 by default)
```sql ```sql
SET ivfflat.probes = 10; SET ivfflat.probes = 1;
``` ```
A higher value provides better recall at the cost of speed, and it can be set to the number of lists for exact nearest neighbor search (at which point the planner wont use the index) A higher value improves recall at the cost of speed.
Use `SET LOCAL` inside a transaction to set it for a single query Use `SET LOCAL` inside a transaction to set it for a single query
```sql ```sql
BEGIN; BEGIN;
SET LOCAL ivfflat.probes = 10; SET LOCAL ivfflat.probes = 1;
SELECT ... SELECT ...
COMMIT; COMMIT;
``` ```
## HNSW ### Indexing Progress
An HNSW index creates a multilayer graph. It has slower build times and uses more memory than IVFFlat, but has better query performance (in terms of speed-recall tradeoff). Theres no training step like IVFFlat, so the index can be created without any data in the table.
Add an index for each distance function you want to use.
L2 distance
```sql
CREATE INDEX ON items USING hnsw (embedding vector_l2_ops);
```
Inner product
```sql
CREATE INDEX ON items USING hnsw (embedding vector_ip_ops);
```
Cosine distance
```sql
CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops);
```
Vectors with up to 2,000 dimensions can be indexed.
### Index Options
Specify HNSW parameters
- `m` - the max number of connections per layer (16 by default)
- `ef_construction` - the size of the dynamic candidate list for constructing the graph (64 by default)
```sql
CREATE INDEX ON items USING hnsw (embedding vector_l2_ops) WITH (m = 16, ef_construction = 64);
```
### Query Options
Specify the size of the dynamic candidate list for search (40 by default)
```sql
SET hnsw.ef_search = 100;
```
A higher value provides better recall at the cost of speed.
Use `SET LOCAL` inside a transaction to set it for a single query
```sql
BEGIN;
SET LOCAL hnsw.ef_search = 100;
SELECT ...
COMMIT;
```
## Indexing Progress
Check [indexing progress](https://www.postgresql.org/docs/current/progress-reporting.html#CREATE-INDEX-PROGRESS-REPORTING) with Postgres 12+ Check [indexing progress](https://www.postgresql.org/docs/current/progress-reporting.html#CREATE-INDEX-PROGRESS-REPORTING) with Postgres 12+
@@ -282,102 +119,86 @@ SELECT phase, tuples_done, tuples_total FROM pg_stat_progress_create_index;
The phases are: The phases are:
1. `initializing` 1. `initializing`
2. `performing k-means` - IVFFlat only 2. `sampling table`
3. `assigning tuples` - IVFFlat only 3. `performing k-means`
4. `loading tuples` 4. `sorting tuples`
5. `loading tuples`
Note: `tuples_done` and `tuples_total` are only populated during the `loading tuples` phase Note: `tuples_done` and `tuples_total` are only populated during the `loading tuples` phase
## Filtering ### Partial Indexes
There are a few ways to index nearest neighbor queries with a `WHERE` clause Consider [partial indexes](https://www.postgresql.org/docs/current/indexes-partial.html) for queries with a `WHERE` clause
```sql ```sql
SELECT * FROM items WHERE category_id = 123 ORDER BY embedding <-> '[3,1,2]' LIMIT 5; SELECT * FROM items WHERE category_id = 123 ORDER BY embedding <-> '[3,1,2]' LIMIT 5;
``` ```
Create an index on one [or more](https://www.postgresql.org/docs/current/indexes-multicolumn.html) of the `WHERE` columns for exact search can be indexed with:
```sql ```sql
CREATE INDEX ON items (category_id); CREATE INDEX ON items USING ivfflat (embedding vector_l2_ops) WHERE (category_id = 123);
``` ```
Or a [partial index](https://www.postgresql.org/docs/current/indexes-partial.html) on the vector column for approximate search To index many different values of `category_id`, consider [partitioning](https://www.postgresql.org/docs/current/ddl-partitioning.html) on `category_id`.
```sql
CREATE INDEX ON items USING ivfflat (embedding vector_l2_ops) WITH (lists = 100)
WHERE (category_id = 123);
```
Use [partitioning](https://www.postgresql.org/docs/current/ddl-partitioning.html) for approximate search on many different values of the `WHERE` columns
```sql ```sql
CREATE TABLE items (embedding vector(3), category_id int) PARTITION BY LIST(category_id); CREATE TABLE items (embedding vector(3), category_id int) PARTITION BY LIST(category_id);
``` ```
## Hybrid Search
Use together with Postgres [full-text search](https://www.postgresql.org/docs/current/textsearch-intro.html) for hybrid search ([Python example](https://github.com/pgvector/pgvector-python/blob/master/examples/hybrid_search.py)).
```sql
SELECT id, content FROM items, plainto_tsquery('hello search') query
WHERE textsearch @@ query ORDER BY ts_rank_cd(textsearch, query) DESC LIMIT 5;
```
## Performance ## Performance
Use `EXPLAIN ANALYZE` to debug performance.
```sql
EXPLAIN ANALYZE SELECT * FROM items ORDER BY embedding <-> '[3,1,2]' LIMIT 5;
```
### Exact Search
To speed up queries without an index, increase `max_parallel_workers_per_gather`. To speed up queries without an index, increase `max_parallel_workers_per_gather`.
```sql ```sql
SET max_parallel_workers_per_gather = 4; SET max_parallel_workers_per_gather = 4;
``` ```
If vectors are normalized to length 1 (like [OpenAI embeddings](https://platform.openai.com/docs/guides/embeddings/which-distance-function-should-i-use)), use inner product for best performance. To speed up queries with an index, increase the number of inverted lists (at the expense of recall).
```tsql
SELECT * FROM items ORDER BY embedding <#> '[3,1,2]' LIMIT 5;
```
### Approximate Search
To speed up queries with an IVFFlat index, increase the number of inverted lists (at the expense of recall).
```sql ```sql
CREATE INDEX ON items USING ivfflat (embedding vector_l2_ops) WITH (lists = 1000); CREATE INDEX ON items USING ivfflat (embedding vector_l2_ops) WITH (lists = 1000);
``` ```
## Languages ## Reference
Use pgvector from any language with a Postgres client. You can even generate and store vectors in one language and query them in another. ### Vector Type
Language | Libraries / Examples Each vector takes `4 * dimensions + 8` bytes of storage. Each element is a single precision floating-point number (like the `real` type in Postgres), and all elements must be finite (no `NaN`, `Infinity` or `-Infinity`). Vectors can have up to 1024 dimensions.
### Vector Operators
Operator | Description
--- | --- --- | ---
C++ | [pgvector-cpp](https://github.com/pgvector/pgvector-cpp) \+ | element-wise addition
C# | [pgvector-dotnet](https://github.com/pgvector/pgvector-dotnet) \- | element-wise subtraction
Crystal | [pgvector-crystal](https://github.com/pgvector/pgvector-crystal) <-> | Euclidean distance
Dart | [pgvector-dart](https://github.com/pgvector/pgvector-dart) <#> | negative inner product
Elixir | [pgvector-elixir](https://github.com/pgvector/pgvector-elixir) <=> | cosine distance
Go | [pgvector-go](https://github.com/pgvector/pgvector-go)
Haskell | [pgvector-haskell](https://github.com/pgvector/pgvector-haskell) ### Vector Functions
Java, Scala | [pgvector-java](https://github.com/pgvector/pgvector-java)
Julia | [pgvector-julia](https://github.com/pgvector/pgvector-julia) Function | Description
Lua | [pgvector-lua](https://github.com/pgvector/pgvector-lua) --- | ---
Node.js | [pgvector-node](https://github.com/pgvector/pgvector-node) cosine_distance(vector, vector) | cosine distance
Perl | [pgvector-perl](https://github.com/pgvector/pgvector-perl) inner_product(vector, vector) | inner product
PHP | [pgvector-php](https://github.com/pgvector/pgvector-php) l2_distance(vector, vector) | Euclidean distance
Python | [pgvector-python](https://github.com/pgvector/pgvector-python) vector_dims(vector) | number of dimensions
R | [pgvector-r](https://github.com/pgvector/pgvector-r) vector_norm(vector) | Euclidean norm
Ruby | [pgvector-ruby](https://github.com/pgvector/pgvector-ruby), [Neighbor](https://github.com/ankane/neighbor)
Rust | [pgvector-rust](https://github.com/pgvector/pgvector-rust) ## Libraries
Swift | [pgvector-swift](https://github.com/pgvector/pgvector-swift)
Libraries that use pgvector:
- [pgvector-python](https://github.com/pgvector/pgvector-python) (Python)
- [Neighbor](https://github.com/ankane/neighbor) (Ruby)
- [pgvector-ruby](https://github.com/pgvector/pgvector-ruby) (Ruby)
- [pgvector-node](https://github.com/pgvector/pgvector-node) (Node.js)
- [pgvector-go](https://github.com/pgvector/pgvector-go) (Go)
- [pgvector-php](https://github.com/pgvector/pgvector-php) (PHP)
- [pgvector-rust](https://github.com/pgvector/pgvector-rust) (Rust)
- [pgvector-cpp](https://github.com/pgvector/pgvector-cpp) (C++)
- [pgvector-elixir](https://github.com/pgvector/pgvector-elixir) (Elixir)
## Frequently Asked Questions ## Frequently Asked Questions
@@ -389,131 +210,12 @@ A non-partitioned table has a limit of 32 TB by default in Postgres. A partition
Yes, pgvector uses the write-ahead log (WAL), which allows for replication and point-in-time recovery. Yes, pgvector uses the write-ahead log (WAL), which allows for replication and point-in-time recovery.
#### What if I want to index vectors with more than 2,000 dimensions? #### What if my data has more than 1024 dimensions?
Youll need to use [dimensionality reduction](https://en.wikipedia.org/wiki/Dimensionality_reduction) at the moment. Two things you can try are:
## Troubleshooting 1. use dimensionality reduction
2. compile Postgres with a larger block size (`./configure --with-blocksize=32`) and edit the limit in `src/vector.h`
#### Why isnt a query using an index?
The cost estimation in pgvector < 0.4.3 does not always work well with the planner. You can encourage the planner to use an index for a query with:
```sql
BEGIN;
SET LOCAL enable_seqscan = off;
SELECT ...
COMMIT;
```
#### Why isnt a query using a parallel table scan?
The planner doesnt consider [out-of-line storage](https://www.postgresql.org/docs/current/storage-toast.html) in cost estimates, which can make a serial scan look cheaper. You can reduce the cost of a parallel scan for a query with:
```sql
BEGIN;
SET LOCAL min_parallel_table_scan_size = 1;
SET LOCAL parallel_setup_cost = 1;
SELECT ...
COMMIT;
```
or choose to store vectors inline:
```sql
ALTER TABLE items ALTER COLUMN embedding SET STORAGE PLAIN;
```
#### Why are there less results for a query after adding an IVFFlat index?
The index was likely created with too little data for the number of lists. Drop the index until the table has more data.
```sql
DROP INDEX index_name;
```
## Reference
### Vector Type
Each vector takes `4 * dimensions + 8` bytes of storage. Each element is a single precision floating-point number (like the `real` type in Postgres), and all elements must be finite (no `NaN`, `Infinity` or `-Infinity`). Vectors can have up to 16,000 dimensions.
### Vector Operators
Operator | Description | Added
--- | --- | ---
\+ | element-wise addition |
\- | element-wise subtraction |
\* | element-wise multiplication | 0.5.0
<-> | Euclidean distance |
<#> | negative inner product |
<=> | cosine distance |
### Vector Functions
Function | Description | Added
--- | --- | ---
cosine_distance(vector, vector) → double precision | cosine distance |
inner_product(vector, vector) → double precision | inner product |
l2_distance(vector, vector) → double precision | Euclidean distance |
l1_distance(vector, vector) → double precision | taxicab distance | 0.5.0
vector_dims(vector) → integer | number of dimensions |
vector_norm(vector) → double precision | Euclidean norm |
### Aggregate Functions
Function | Description | Added
--- | --- | ---
avg(vector) → vector | average |
sum(vector) → vector | sum | 0.5.0
## Installation Notes
### Postgres Location
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=/Applications/Postgres.app/Contents/Versions/latest/bin/pg_config
```
Then re-run the installation instructions (run `make clean` before `make` if needed). If `sudo` is needed for `make install`, use:
```sh
sudo --preserve-env=PG_CONFIG make install
```
### Missing Header
If compilation fails with `fatal error: postgres.h: No such file or directory`, make sure Postgres development files are installed on the server.
For Ubuntu and Debian, use:
```sh
sudo apt install postgresql-server-dev-15
```
Note: Replace `15` with your Postgres server version
### Windows
Support for Windows is currently experimental. Ensure [C++ support in Visual Studio](https://learn.microsoft.com/en-us/cpp/build/building-on-the-command-line?view=msvc-170#download-and-install-the-tools) is installed, and run:
```cmd
call "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat"
```
Note: The exact path will vary depending on your Visual Studio version and edition
Then use `nmake` to build:
```cmd
set "PGROOT=C:\Program Files\PostgreSQL\15"
git clone --branch v0.5.0 https://github.com/pgvector/pgvector.git
cd pgvector
nmake /F Makefile.win
nmake /F Makefile.win install
```
## Additional Installation Methods ## Additional Installation Methods
@@ -525,14 +227,14 @@ Get the [Docker image](https://hub.docker.com/r/ankane/pgvector) with:
docker pull ankane/pgvector docker pull ankane/pgvector
``` ```
This adds pgvector to the [Postgres image](https://hub.docker.com/_/postgres) (run it the same way). This adds pgvector to the [Postgres image](https://hub.docker.com/_/postgres).
You can also build the image manually: You can also build the image manually
```sh ```sh
git clone --branch v0.5.0 https://github.com/pgvector/pgvector.git git clone --branch v0.3.2 https://github.com/pgvector/pgvector.git
cd pgvector cd pgvector
docker build --build-arg PG_MAJOR=15 -t myuser/pgvector . docker build -t pgvector .
``` ```
### Homebrew ### Homebrew
@@ -540,11 +242,9 @@ docker build --build-arg PG_MAJOR=15 -t myuser/pgvector .
With Homebrew Postgres, you can use: With Homebrew Postgres, you can use:
```sh ```sh
brew install pgvector brew install pgvector/brew/pgvector
``` ```
Note: This only adds it to the `postgresql@14` formula
### PGXN ### PGXN
Install from the [PostgreSQL Extension Network](https://pgxn.org/dist/vector) with: Install from the [PostgreSQL Extension Network](https://pgxn.org/dist/vector) with:
@@ -553,72 +253,25 @@ Install from the [PostgreSQL Extension Network](https://pgxn.org/dist/vector) wi
pgxn install vector pgxn install vector
``` ```
### APT
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-15-pgvector
```
Note: Replace `15` 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_15
# or
sudo dnf install pgvector_15
```
Note: Replace `15` with your Postgres server version
### conda-forge
With Conda Postgres, install from [conda-forge](https://anaconda.org/conda-forge/pgvector) with:
```sh
conda install -c conda-forge pgvector
```
This method is [community-maintained](https://github.com/conda-forge/pgvector-feedstock) by [@mmcauliffe](https://github.com/mmcauliffe)
### Postgres.app
Download the [latest release](https://postgresapp.com/downloads.html) with Postgres 15+.
## Hosted Postgres ## Hosted Postgres
pgvector is available on [these providers](https://github.com/pgvector/pgvector/issues/54). Some Postgres providers only support specific extensions. To request a new extension:
- Amazon RDS - follow the instructions on [this page](https://aws.amazon.com/rds/postgresql/faqs/)
- Google Cloud SQL - follow the instructions on [this page](https://cloud.google.com/sql/docs/postgres/extensions#requesting-support-for-a-new-extension)
- DigitalOcean Managed Databases - vote or comment on [this page](https://ideas.digitalocean.com/app-framework-services/p/pgvector-extension-for-postgresql)
- Azure Database for PostgreSQL - follow the instructions on [this page](https://docs.microsoft.com/en-us/azure/postgresql/concepts-extensions#next-steps)
## Upgrading ## Upgrading
Install the latest version. Then in each database you want to upgrade, run: Install the latest version and run:
```sql ```sql
ALTER EXTENSION vector UPDATE; ALTER EXTENSION vector UPDATE;
``` ```
You can check the version in the current database with:
```sql
SELECT extversion FROM pg_extension WHERE extname = 'vector';
```
## Upgrade Notes ## Upgrade Notes
### 0.4.0
If upgrading with Postgres < 13, remove this line from `sql/vector--0.3.2--0.4.0.sql`:
```sql
ALTER TYPE vector SET (STORAGE = extended);
```
Then run `make install` and `ALTER EXTENSION vector UPDATE;`.
### 0.3.1 ### 0.3.1
If upgrading from 0.2.7 or 0.3.0, recreate all `ivfflat` indexes after upgrading to ensure all data is indexed. If upgrading from 0.2.7 or 0.3.0, recreate all `ivfflat` indexes after upgrading to ensure all data is indexed.
@@ -639,10 +292,9 @@ Thanks to:
- [PASE: PostgreSQL Ultra-High-Dimensional Approximate Nearest Neighbor Search Extension](https://dl.acm.org/doi/pdf/10.1145/3318464.3386131) - [PASE: PostgreSQL Ultra-High-Dimensional Approximate Nearest Neighbor Search Extension](https://dl.acm.org/doi/pdf/10.1145/3318464.3386131)
- [Faiss: A Library for Efficient Similarity Search and Clustering of Dense Vectors](https://github.com/facebookresearch/faiss) - [Faiss: A Library for Efficient Similarity Search and Clustering of Dense Vectors](https://github.com/facebookresearch/faiss)
- [Using the Triangle Inequality to Accelerate k-means](https://cdn.aaai.org/ICML/2003/ICML03-022.pdf) - [Using the Triangle Inequality to Accelerate k-means](https://www.aaai.org/Papers/ICML/2003/ICML03-022.pdf)
- [k-means++: The Advantage of Careful Seeding](https://theory.stanford.edu/~sergei/papers/kMeansPP-soda.pdf) - [k-means++: The Advantage of Careful Seeding](https://theory.stanford.edu/~sergei/papers/kMeansPP-soda.pdf)
- [Concept Decompositions for Large Sparse Text Data using Clustering](https://www.cs.utexas.edu/users/inderjit/public_papers/concept_mlj.pdf) - [Concept Decompositions for Large Sparse Text Data using Clustering](https://www.cs.utexas.edu/users/inderjit/public_papers/concept_mlj.pdf)
- [Efficient and Robust Approximate Nearest Neighbor Search using Hierarchical Navigable Small World Graphs](https://arxiv.org/ftp/arxiv/papers/1603/1603.09320.pdf)
## History ## History
@@ -690,4 +342,4 @@ Resources for contributors
- [Extension Building Infrastructure](https://www.postgresql.org/docs/current/extend-pgxs.html) - [Extension Building Infrastructure](https://www.postgresql.org/docs/current/extend-pgxs.html)
- [Index Access Method Interface Definition](https://www.postgresql.org/docs/current/indexam.html) - [Index Access Method Interface Definition](https://www.postgresql.org/docs/current/indexam.html)
- [Generic WAL Records](https://www.postgresql.org/docs/current/generic-wal.html) - [Generic WAL Records](https://www.postgresql.org/docs/13/generic-wal.html)

View File

@@ -7,13 +7,13 @@ DROP CAST (double precision[] AS vector);
DROP CAST (numeric[] AS vector); DROP CAST (numeric[] AS vector);
CREATE CAST (integer[] AS vector) CREATE CAST (integer[] AS vector)
WITH FUNCTION array_to_vector(integer[], integer, boolean) AS ASSIGNMENT; WITH FUNCTION array_to_vector(integer[], integer, boolean) AS ASSIGNMENT;
CREATE CAST (real[] AS vector) CREATE CAST (real[] AS vector)
WITH FUNCTION array_to_vector(real[], integer, boolean) AS ASSIGNMENT; WITH FUNCTION array_to_vector(real[], integer, boolean) AS ASSIGNMENT;
CREATE CAST (double precision[] AS vector) CREATE CAST (double precision[] AS vector)
WITH FUNCTION array_to_vector(double precision[], integer, boolean) AS ASSIGNMENT; WITH FUNCTION array_to_vector(double precision[], integer, boolean) AS ASSIGNMENT;
CREATE CAST (numeric[] AS vector) CREATE CAST (numeric[] AS vector)
WITH FUNCTION array_to_vector(numeric[], integer, boolean) AS ASSIGNMENT; WITH FUNCTION array_to_vector(numeric[], integer, boolean) AS ASSIGNMENT;

View File

@@ -1,23 +0,0 @@
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
\echo Use "ALTER EXTENSION vector UPDATE TO '0.4.0'" to load this file. \quit
-- remove this single line for Postgres < 13
ALTER TYPE vector SET (STORAGE = extended);
CREATE FUNCTION vector_accum(double precision[], vector) RETURNS double precision[]
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
CREATE FUNCTION vector_avg(double precision[]) RETURNS vector
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
CREATE FUNCTION vector_combine(double precision[], double precision[]) RETURNS double precision[]
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
CREATE AGGREGATE avg(vector) (
SFUNC = vector_accum,
STYPE = double precision[],
FINALFUNC = vector_avg,
COMBINEFUNC = vector_combine,
INITCOND = '{0}',
PARALLEL = SAFE
);

View File

@@ -1,2 +0,0 @@
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
\echo Use "ALTER EXTENSION vector UPDATE TO '0.4.1'" to load this file. \quit

View File

@@ -1,2 +0,0 @@
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
\echo Use "ALTER EXTENSION vector UPDATE TO '0.4.2'" to load this file. \quit

View File

@@ -1,2 +0,0 @@
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
\echo Use "ALTER EXTENSION vector UPDATE TO '0.4.3'" to load this file. \quit

View File

@@ -1,2 +0,0 @@
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
\echo Use "ALTER EXTENSION vector UPDATE TO '0.4.4'" to load this file. \quit

View File

@@ -1,43 +0,0 @@
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
\echo Use "ALTER EXTENSION vector UPDATE TO '0.5.0'" to load this file. \quit
CREATE FUNCTION l1_distance(vector, vector) RETURNS float8
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
CREATE FUNCTION vector_mul(vector, vector) RETURNS vector
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
CREATE OPERATOR * (
LEFTARG = vector, RIGHTARG = vector, PROCEDURE = vector_mul,
COMMUTATOR = *
);
CREATE AGGREGATE sum(vector) (
SFUNC = vector_add,
STYPE = vector,
COMBINEFUNC = vector_add,
PARALLEL = SAFE
);
CREATE FUNCTION hnswhandler(internal) RETURNS index_am_handler
AS 'MODULE_PATHNAME' LANGUAGE C;
CREATE ACCESS METHOD hnsw TYPE INDEX HANDLER hnswhandler;
COMMENT ON ACCESS METHOD hnsw IS 'hnsw index access method';
CREATE OPERATOR CLASS vector_l2_ops
FOR TYPE vector USING hnsw AS
OPERATOR 1 <-> (vector, vector) FOR ORDER BY float_ops,
FUNCTION 1 vector_l2_squared_distance(vector, vector);
CREATE OPERATOR CLASS vector_ip_ops
FOR TYPE vector USING hnsw AS
OPERATOR 1 <#> (vector, vector) FOR ORDER BY float_ops,
FUNCTION 1 vector_negative_inner_product(vector, vector);
CREATE OPERATOR CLASS vector_cosine_ops
FOR TYPE vector USING hnsw AS
OPERATOR 1 <=> (vector, vector) FOR ORDER BY float_ops,
FUNCTION 1 vector_negative_inner_product(vector, vector),
FUNCTION 2 vector_norm(vector);

View File

@@ -25,8 +25,7 @@ CREATE TYPE vector (
OUTPUT = vector_out, OUTPUT = vector_out,
TYPMOD_IN = vector_typmod_in, TYPMOD_IN = vector_typmod_in,
RECEIVE = vector_recv, RECEIVE = vector_recv,
SEND = vector_send, SEND = vector_send
STORAGE = extended
); );
-- functions -- functions
@@ -40,9 +39,6 @@ CREATE FUNCTION inner_product(vector, vector) RETURNS float8
CREATE FUNCTION cosine_distance(vector, vector) RETURNS float8 CREATE FUNCTION cosine_distance(vector, vector) RETURNS float8
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
CREATE FUNCTION l1_distance(vector, vector) RETURNS float8
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
CREATE FUNCTION vector_dims(vector) RETURNS integer CREATE FUNCTION vector_dims(vector) RETURNS integer
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
@@ -55,9 +51,6 @@ CREATE FUNCTION vector_add(vector, vector) RETURNS vector
CREATE FUNCTION vector_sub(vector, vector) RETURNS vector CREATE FUNCTION vector_sub(vector, vector) RETURNS vector
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
CREATE FUNCTION vector_mul(vector, vector) RETURNS vector
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
-- private functions -- private functions
CREATE FUNCTION vector_lt(vector, vector) RETURNS bool CREATE FUNCTION vector_lt(vector, vector) RETURNS bool
@@ -90,33 +83,6 @@ CREATE FUNCTION vector_negative_inner_product(vector, vector) RETURNS float8
CREATE FUNCTION vector_spherical_distance(vector, vector) RETURNS float8 CREATE FUNCTION vector_spherical_distance(vector, vector) RETURNS float8
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
CREATE FUNCTION vector_accum(double precision[], vector) RETURNS double precision[]
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
CREATE FUNCTION vector_avg(double precision[]) RETURNS vector
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
CREATE FUNCTION vector_combine(double precision[], double precision[]) RETURNS double precision[]
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
-- aggregates
CREATE AGGREGATE avg(vector) (
SFUNC = vector_accum,
STYPE = double precision[],
FINALFUNC = vector_avg,
COMBINEFUNC = vector_combine,
INITCOND = '{0}',
PARALLEL = SAFE
);
CREATE AGGREGATE sum(vector) (
SFUNC = vector_add,
STYPE = vector,
COMBINEFUNC = vector_add,
PARALLEL = SAFE
);
-- cast functions -- cast functions
CREATE FUNCTION vector(vector, integer, boolean) RETURNS vector CREATE FUNCTION vector(vector, integer, boolean) RETURNS vector
@@ -184,11 +150,6 @@ CREATE OPERATOR - (
COMMUTATOR = - COMMUTATOR = -
); );
CREATE OPERATOR * (
LEFTARG = vector, RIGHTARG = vector, PROCEDURE = vector_mul,
COMMUTATOR = *
);
CREATE OPERATOR < ( CREATE OPERATOR < (
LEFTARG = vector, RIGHTARG = vector, PROCEDURE = vector_lt, LEFTARG = vector, RIGHTARG = vector, PROCEDURE = vector_lt,
COMMUTATOR = > , NEGATOR = >= , COMMUTATOR = > , NEGATOR = >= ,
@@ -227,7 +188,7 @@ CREATE OPERATOR > (
RESTRICT = scalargtsel, JOIN = scalargtjoinsel RESTRICT = scalargtsel, JOIN = scalargtjoinsel
); );
-- access methods -- access method
CREATE FUNCTION ivfflathandler(internal) RETURNS index_am_handler CREATE FUNCTION ivfflathandler(internal) RETURNS index_am_handler
AS 'MODULE_PATHNAME' LANGUAGE C; AS 'MODULE_PATHNAME' LANGUAGE C;
@@ -236,13 +197,6 @@ CREATE ACCESS METHOD ivfflat TYPE INDEX HANDLER ivfflathandler;
COMMENT ON ACCESS METHOD ivfflat IS 'ivfflat index access method'; COMMENT ON ACCESS METHOD ivfflat IS 'ivfflat index access method';
CREATE FUNCTION hnswhandler(internal) RETURNS index_am_handler
AS 'MODULE_PATHNAME' LANGUAGE C;
CREATE ACCESS METHOD hnsw TYPE INDEX HANDLER hnswhandler;
COMMENT ON ACCESS METHOD hnsw IS 'hnsw index access method';
-- opclasses -- opclasses
CREATE OPERATOR CLASS vector_ops CREATE OPERATOR CLASS vector_ops
@@ -274,19 +228,3 @@ CREATE OPERATOR CLASS vector_cosine_ops
FUNCTION 2 vector_norm(vector), FUNCTION 2 vector_norm(vector),
FUNCTION 3 vector_spherical_distance(vector, vector), FUNCTION 3 vector_spherical_distance(vector, vector),
FUNCTION 4 vector_norm(vector); FUNCTION 4 vector_norm(vector);
CREATE OPERATOR CLASS vector_l2_ops
FOR TYPE vector USING hnsw AS
OPERATOR 1 <-> (vector, vector) FOR ORDER BY float_ops,
FUNCTION 1 vector_l2_squared_distance(vector, vector);
CREATE OPERATOR CLASS vector_ip_ops
FOR TYPE vector USING hnsw AS
OPERATOR 1 <#> (vector, vector) FOR ORDER BY float_ops,
FUNCTION 1 vector_negative_inner_product(vector, vector);
CREATE OPERATOR CLASS vector_cosine_ops
FOR TYPE vector USING hnsw AS
OPERATOR 1 <=> (vector, vector) FOR ORDER BY float_ops,
FUNCTION 1 vector_negative_inner_product(vector, vector),
FUNCTION 2 vector_norm(vector);

View File

@@ -1,224 +0,0 @@
#include "postgres.h"
#include <float.h>
#include <math.h>
#include "access/amapi.h"
#include "commands/vacuum.h"
#include "hnsw.h"
#include "utils/guc.h"
#include "utils/selfuncs.h"
#if PG_VERSION_NUM >= 120000
#include "commands/progress.h"
#endif
int hnsw_ef_search;
static relopt_kind hnsw_relopt_kind;
/*
* Initialize index options and variables
*/
void
HnswInit(void)
{
hnsw_relopt_kind = add_reloption_kind();
add_int_reloption(hnsw_relopt_kind, "m", "Max number of connections",
HNSW_DEFAULT_M, HNSW_MIN_M, HNSW_MAX_M
#if PG_VERSION_NUM >= 130000
,AccessExclusiveLock
#endif
);
add_int_reloption(hnsw_relopt_kind, "ef_construction", "Size of the dynamic candidate list for construction",
HNSW_DEFAULT_EF_CONSTRUCTION, HNSW_MIN_EF_CONSTRUCTION, HNSW_MAX_EF_CONSTRUCTION
#if PG_VERSION_NUM >= 130000
,AccessExclusiveLock
#endif
);
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);
}
/*
* Get the name of index build phase
*/
#if PG_VERSION_NUM >= 120000
static char *
hnswbuildphasename(int64 phasenum)
{
switch (phasenum)
{
case PROGRESS_CREATEIDX_SUBPHASE_INITIALIZE:
return "initializing";
case PROGRESS_HNSW_PHASE_LOAD:
return "loading tuples";
default:
return NULL;
}
}
#endif
/*
* Estimate the cost of an index scan
*/
static void
hnswcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
Cost *indexStartupCost, Cost *indexTotalCost,
Selectivity *indexSelectivity, double *indexCorrelation,
double *indexPages)
{
GenericCosts costs;
int m;
int entryLevel;
Relation index;
#if PG_VERSION_NUM < 120000
List *qinfos;
#endif
/* Never use index without order */
if (path->indexorderbys == NULL)
{
*indexStartupCost = DBL_MAX;
*indexTotalCost = DBL_MAX;
*indexSelectivity = 0;
*indexCorrelation = 0;
*indexPages = 0;
return;
}
MemSet(&costs, 0, sizeof(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);
/* 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 PG_VERSION_NUM >= 120000
genericcostestimate(root, path, loop_count, &costs);
#else
qinfos = deconstruct_indexquals(path);
genericcostestimate(root, path, loop_count, qinfos, &costs);
#endif
/* Use total cost since most work happens before first tuple is returned */
*indexStartupCost = costs.indexTotalCost;
*indexTotalCost = costs.indexTotalCost;
*indexSelectivity = costs.indexSelectivity;
*indexCorrelation = costs.indexCorrelation;
*indexPages = costs.numIndexPages;
}
/*
* Parse and validate the reloptions
*/
static bytea *
hnswoptions(Datum reloptions, bool validate)
{
static const relopt_parse_elt tab[] = {
{"m", RELOPT_TYPE_INT, offsetof(HnswOptions, m)},
{"ef_construction", RELOPT_TYPE_INT, offsetof(HnswOptions, efConstruction)},
};
#if PG_VERSION_NUM >= 130000
return (bytea *) build_reloptions(reloptions, validate,
hnsw_relopt_kind,
sizeof(HnswOptions),
tab, lengthof(tab));
#else
relopt_value *options;
int numoptions;
HnswOptions *rdopts;
options = parseRelOptions(reloptions, validate, hnsw_relopt_kind, &numoptions);
rdopts = allocateReloptStruct(sizeof(HnswOptions), options, numoptions);
fillRelOptions((void *) rdopts, sizeof(HnswOptions), options, numoptions,
validate, tab, lengthof(tab));
return (bytea *) rdopts;
#endif
}
/*
* Validate catalog entries for the specified operator class
*/
static bool
hnswvalidate(Oid opclassoid)
{
return true;
}
/*
* Define index handler
*
* See https://www.postgresql.org/docs/current/index-api.html
*/
PGDLLEXPORT PG_FUNCTION_INFO_V1(hnswhandler);
Datum
hnswhandler(PG_FUNCTION_ARGS)
{
IndexAmRoutine *amroutine = makeNode(IndexAmRoutine);
amroutine->amstrategies = 0;
amroutine->amsupport = 2;
#if PG_VERSION_NUM >= 130000
amroutine->amoptsprocnum = 0;
#endif
amroutine->amcanorder = false;
amroutine->amcanorderbyop = true;
amroutine->amcanbackward = false; /* can change direction mid-scan */
amroutine->amcanunique = false;
amroutine->amcanmulticol = false;
amroutine->amoptionalkey = true;
amroutine->amsearcharray = false;
amroutine->amsearchnulls = false;
amroutine->amstorage = false;
amroutine->amclusterable = false;
amroutine->ampredlocks = false;
amroutine->amcanparallel = false;
amroutine->amcaninclude = false;
#if PG_VERSION_NUM >= 130000
amroutine->amusemaintenanceworkmem = false; /* not used during VACUUM */
amroutine->amparallelvacuumoptions = VACUUM_OPTION_PARALLEL_BULKDEL;
#endif
amroutine->amkeytype = InvalidOid;
/* Interface functions */
amroutine->ambuild = hnswbuild;
amroutine->ambuildempty = hnswbuildempty;
amroutine->aminsert = hnswinsert;
amroutine->ambulkdelete = hnswbulkdelete;
amroutine->amvacuumcleanup = hnswvacuumcleanup;
amroutine->amcanreturn = NULL;
amroutine->amcostestimate = hnswcostestimate;
amroutine->amoptions = hnswoptions;
amroutine->amproperty = NULL; /* TODO AMPROP_DISTANCE_ORDERABLE */
#if PG_VERSION_NUM >= 120000
amroutine->ambuildphasename = hnswbuildphasename;
#endif
amroutine->amvalidate = hnswvalidate;
#if PG_VERSION_NUM >= 140000
amroutine->amadjustmembers = NULL;
#endif
amroutine->ambeginscan = hnswbeginscan;
amroutine->amrescan = hnswrescan;
amroutine->amgettuple = hnswgettuple;
amroutine->amgetbitmap = NULL;
amroutine->amendscan = hnswendscan;
amroutine->ammarkpos = NULL;
amroutine->amrestrpos = NULL;
/* Interface functions to support parallel index scans */
amroutine->amestimateparallelscan = NULL;
amroutine->aminitparallelscan = NULL;
amroutine->amparallelrescan = NULL;
PG_RETURN_POINTER(amroutine);
}

View File

@@ -1,306 +0,0 @@
#ifndef HNSW_H
#define HNSW_H
#include "postgres.h"
#include "access/generic_xlog.h"
#include "access/reloptions.h"
#include "nodes/execnodes.h"
#include "port.h" /* for random() */
#include "utils/sampling.h"
#include "vector.h"
#if PG_VERSION_NUM < 110000
#error "Requires PostgreSQL 11+"
#endif
#define HNSW_MAX_DIM 2000
/* Support functions */
#define HNSW_DISTANCE_PROC 1
#define HNSW_NORM_PROC 2
#define HNSW_VERSION 1
#define HNSW_MAGIC_NUMBER 0xA953A953
#define HNSW_PAGE_ID 0xFF90
/* Preserved page numbers */
#define HNSW_METAPAGE_BLKNO 0
#define HNSW_HEAD_BLKNO 1 /* first element page */
/* Must correspond to page numbers since page lock is used */
#define HNSW_UPDATE_LOCK 0
#define HNSW_SCAN_LOCK 1
/* HNSW parameters */
#define HNSW_DEFAULT_M 16
#define HNSW_MIN_M 2
#define HNSW_MAX_M 100
#define HNSW_DEFAULT_EF_CONSTRUCTION 64
#define HNSW_MIN_EF_CONSTRUCTION 4
#define HNSW_MAX_EF_CONSTRUCTION 1000
#define HNSW_DEFAULT_EF_SEARCH 40
#define HNSW_MIN_EF_SEARCH 1
#define HNSW_MAX_EF_SEARCH 1000
/* Tuple types */
#define HNSW_ELEMENT_TUPLE_TYPE 1
#define HNSW_NEIGHBOR_TUPLE_TYPE 2
/* Make graph robust against non-HOT updates */
#define HNSW_HEAPTIDS 10
#define HNSW_UPDATE_ENTRY_GREATER 1
#define HNSW_UPDATE_ENTRY_ALWAYS 2
/* Build phases */
/* PROGRESS_CREATEIDX_SUBPHASE_INITIALIZE is 1 */
#define PROGRESS_HNSW_PHASE_LOAD 2
#define HNSW_ELEMENT_TUPLE_SIZE(_dim) MAXALIGN(offsetof(HnswElementTupleData, vec) + VECTOR_SIZE(_dim))
#define HNSW_NEIGHBOR_TUPLE_SIZE(level, m) MAXALIGN(offsetof(HnswNeighborTupleData, indextids) + ((level) + 2) * (m) * sizeof(ItemPointerData))
#define HnswPageGetOpaque(page) ((HnswPageOpaque) PageGetSpecialPointer(page))
#define HnswPageGetMeta(page) ((HnswMetaPageData *) PageGetContents(page))
#if PG_VERSION_NUM >= 150000
#define RandomDouble() pg_prng_double(&pg_global_prng_state)
#else
#define RandomDouble() (((double) random()) / MAX_RANDOM_VALUE)
#endif
#if PG_VERSION_NUM < 130000
#define list_delete_last(list) list_truncate(list, list_length(list) - 1)
#define list_sort(list, cmp) list_qsort(list, cmp)
#endif
#define HnswIsElementTuple(tup) ((tup)->type == HNSW_ELEMENT_TUPLE_TYPE)
#define HnswIsNeighborTuple(tup) ((tup)->type == HNSW_NEIGHBOR_TUPLE_TYPE)
/* 2 * M connections for ground layer */
#define HnswGetLayerM(m, layer) (layer == 0 ? (m) * 2 : (m))
/* Optimal ML from paper */
#define HnswGetMl(m) (1 / log(m))
/* 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)
/* Variables */
extern int hnsw_ef_search;
typedef struct HnswNeighborArray HnswNeighborArray;
typedef struct HnswElementData
{
List *heaptids;
uint8 level;
uint8 deleted;
HnswNeighborArray *neighbors;
BlockNumber blkno;
OffsetNumber offno;
OffsetNumber neighborOffno;
BlockNumber neighborPage;
Vector *vec;
} HnswElementData;
typedef HnswElementData * HnswElement;
typedef struct HnswCandidate
{
HnswElement element;
float distance;
} HnswCandidate;
typedef struct HnswNeighborArray
{
int length;
HnswCandidate *items;
} HnswNeighborArray;
typedef struct HnswPairingHeapNode
{
pairingheap_node ph_node;
HnswCandidate *inner;
} HnswPairingHeapNode;
/* HNSW index options */
typedef struct HnswOptions
{
int32 vl_len_; /* varlena header (do not touch directly!) */
int m; /* number of connections */
int efConstruction; /* size of dynamic candidate list */
} HnswOptions;
typedef struct HnswBuildState
{
/* Info */
Relation heap;
Relation index;
IndexInfo *indexInfo;
ForkNumber forkNum;
/* Settings */
int dimensions;
int m;
int efConstruction;
/* Statistics */
double indtuples;
double reltuples;
/* Support functions */
FmgrInfo *procinfo;
FmgrInfo *normprocinfo;
Oid collation;
/* Variables */
List *elements;
HnswElement entryPoint;
double ml;
int maxLevel;
double maxInMemoryElements;
bool flushed;
Vector *normvec;
/* Memory */
MemoryContext tmpCtx;
} HnswBuildState;
typedef struct HnswMetaPageData
{
uint32 magicNumber;
uint32 version;
uint32 dimensions;
uint16 m;
uint16 efConstruction;
BlockNumber entryBlkno;
OffsetNumber entryOffno;
int16 entryLevel;
BlockNumber insertPage;
} HnswMetaPageData;
typedef HnswMetaPageData * HnswMetaPage;
typedef struct HnswPageOpaqueData
{
BlockNumber nextblkno;
uint16 unused;
uint16 page_id; /* for identification of HNSW indexes */
} HnswPageOpaqueData;
typedef HnswPageOpaqueData * HnswPageOpaque;
typedef struct HnswElementTupleData
{
uint8 type;
uint8 level;
uint8 deleted;
uint8 unused;
ItemPointerData heaptids[HNSW_HEAPTIDS];
ItemPointerData neighbortid;
uint16 unused2;
Vector vec;
} HnswElementTupleData;
typedef HnswElementTupleData * HnswElementTuple;
typedef struct HnswNeighborTupleData
{
uint8 type;
uint8 unused;
uint16 count;
ItemPointerData indextids[FLEXIBLE_ARRAY_MEMBER];
} HnswNeighborTupleData;
typedef HnswNeighborTupleData * HnswNeighborTuple;
typedef struct HnswScanOpaqueData
{
bool first;
Buffer buf;
List *w;
MemoryContext tmpCtx;
/* Support functions */
FmgrInfo *procinfo;
FmgrInfo *normprocinfo;
Oid collation;
} HnswScanOpaqueData;
typedef HnswScanOpaqueData * HnswScanOpaque;
typedef struct HnswVacuumState
{
/* Info */
Relation index;
IndexBulkDeleteResult *stats;
IndexBulkDeleteCallback callback;
void *callback_state;
/* Settings */
int m;
int efConstruction;
/* Support functions */
FmgrInfo *procinfo;
Oid collation;
/* Variables */
HTAB *deleted;
BufferAccessStrategy bas;
HnswNeighborTuple ntup;
HnswElementData highestPoint;
/* Memory */
MemoryContext tmpCtx;
} HnswVacuumState;
/* Methods */
int HnswGetM(Relation index);
int HnswGetEfConstruction(Relation index);
FmgrInfo *HnswOptionalProcInfo(Relation rel, uint16 procnum);
bool HnswNormValue(FmgrInfo *procinfo, Oid collation, Datum *value, Vector * result);
void HnswCommitBuffer(Buffer buf, GenericXLogState *state);
Buffer HnswNewBuffer(Relation index, ForkNumber forkNum);
void HnswInitPage(Buffer buf, Page page);
void HnswInitRegisterPage(Relation index, Buffer *buf, Page *page, GenericXLogState **state);
void HnswInit(void);
List *HnswSearchLayer(Datum q, List *ep, int ef, int lc, Relation index, FmgrInfo *procinfo, Oid collation, int m, bool inserting, HnswElement skipElement);
HnswElement HnswGetEntryPoint(Relation index);
void HnswGetMetaPageInfo(Relation index, int *m, HnswElement * entryPoint);
HnswElement HnswInitElement(ItemPointer tid, int m, double ml, int maxLevel);
void HnswFreeElement(HnswElement element);
HnswElement HnswInitElementFromBlock(BlockNumber blkno, OffsetNumber offno);
void HnswInsertElement(HnswElement element, HnswElement entryPoint, Relation index, FmgrInfo *procinfo, Oid collation, int m, int efConstruction, bool existing);
HnswElement HnswFindDuplicate(HnswElement e);
HnswCandidate *HnswEntryCandidate(HnswElement em, Datum q, Relation rel, FmgrInfo *procinfo, Oid collation, bool loadVec);
void HnswUpdateMetaPage(Relation index, int updateEntry, HnswElement entryPoint, BlockNumber insertPage, ForkNumber forkNum);
void HnswSetNeighborTuple(HnswNeighborTuple ntup, HnswElement e, int m);
void HnswAddHeapTid(HnswElement element, ItemPointer heaptid);
void HnswInitNeighbors(HnswElement element, int m);
bool HnswInsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heap_tid, Relation heapRel);
void HnswUpdateNeighborPages(Relation index, FmgrInfo *procinfo, Oid collation, HnswElement e, int m, bool checkExisting);
void HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool loadHeaptids, bool loadVec);
void HnswLoadElement(HnswElement element, float *distance, Datum *q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec);
void HnswSetElementTuple(HnswElementTuple etup, HnswElement element);
void HnswUpdateConnection(HnswElement element, HnswCandidate * hc, int m, int lc, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation);
void HnswLoadNeighbors(HnswElement element, Relation index, int m);
/* Index access methods */
IndexBuildResult *hnswbuild(Relation heap, Relation index, IndexInfo *indexInfo);
void hnswbuildempty(Relation index);
bool hnswinsert(Relation index, Datum *values, bool *isnull, ItemPointer heap_tid, Relation heap, IndexUniqueCheck checkUnique
#if PG_VERSION_NUM >= 140000
,bool indexUnchanged
#endif
,IndexInfo *indexInfo
);
IndexBulkDeleteResult *hnswbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, IndexBulkDeleteCallback callback, void *callback_state);
IndexBulkDeleteResult *hnswvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats);
IndexScanDesc hnswbeginscan(Relation index, int nkeys, int norderbys);
void hnswrescan(IndexScanDesc scan, ScanKey keys, int nkeys, ScanKey orderbys, int norderbys);
bool hnswgettuple(IndexScanDesc scan, ScanDirection dir);
void hnswendscan(IndexScanDesc scan);
#endif

View File

@@ -1,526 +0,0 @@
#include "postgres.h"
#include <math.h>
#include "catalog/index.h"
#include "hnsw.h"
#include "miscadmin.h"
#include "lib/pairingheap.h"
#include "nodes/pg_list.h"
#include "storage/bufmgr.h"
#include "utils/memutils.h"
#if PG_VERSION_NUM >= 140000
#include "utils/backend_progress.h"
#elif PG_VERSION_NUM >= 120000
#include "pgstat.h"
#endif
#if PG_VERSION_NUM >= 120000
#include "access/tableam.h"
#include "commands/progress.h"
#else
#define PROGRESS_CREATEIDX_TUPLES_DONE 0
#endif
#if PG_VERSION_NUM >= 130000
#define CALLBACK_ITEM_POINTER ItemPointer tid
#else
#define CALLBACK_ITEM_POINTER HeapTuple hup
#endif
#if PG_VERSION_NUM >= 120000
#define UpdateProgress(index, val) pgstat_progress_update_param(index, val)
#else
#define UpdateProgress(index, val) ((void)val)
#endif
/*
* Create the metapage
*/
static void
CreateMetaPage(HnswBuildState * buildstate)
{
Relation index = buildstate->index;
ForkNumber forkNum = buildstate->forkNum;
Buffer buf;
Page page;
GenericXLogState *state;
HnswMetaPage metap;
buf = HnswNewBuffer(index, forkNum);
HnswInitRegisterPage(index, &buf, &page, &state);
/* Set metapage data */
metap = HnswPageGetMeta(page);
metap->magicNumber = HNSW_MAGIC_NUMBER;
metap->version = HNSW_VERSION;
metap->dimensions = buildstate->dimensions;
metap->m = buildstate->m;
metap->efConstruction = buildstate->efConstruction;
metap->entryBlkno = InvalidBlockNumber;
metap->entryOffno = InvalidOffsetNumber;
metap->entryLevel = -1;
metap->insertPage = InvalidBlockNumber;
((PageHeader) page)->pd_lower =
((char *) metap + sizeof(HnswMetaPageData)) - (char *) page;
HnswCommitBuffer(buf, state);
}
/*
* Add a new page
*/
static void
HnswBuildAppendPage(Relation index, Buffer *buf, Page *page, GenericXLogState **state, ForkNumber forkNum)
{
/* Add a new page */
Buffer newbuf = HnswNewBuffer(index, forkNum);
/* Update previous page */
HnswPageGetOpaque(*page)->nextblkno = BufferGetBlockNumber(newbuf);
/* Commit */
MarkBufferDirty(*buf);
GenericXLogFinish(*state);
UnlockReleaseBuffer(*buf);
/* Can take a while, so ensure we can interrupt */
/* Needs to be called when no buffer locks are held */
LockBuffer(newbuf, BUFFER_LOCK_UNLOCK);
CHECK_FOR_INTERRUPTS();
LockBuffer(newbuf, BUFFER_LOCK_EXCLUSIVE);
/* Prepare new page */
*buf = newbuf;
*state = GenericXLogStart(index);
*page = GenericXLogRegisterBuffer(*state, *buf, GENERIC_XLOG_FULL_IMAGE);
HnswInitPage(*buf, *page);
}
/*
* Create element pages
*/
static void
CreateElementPages(HnswBuildState * buildstate)
{
Relation index = buildstate->index;
ForkNumber forkNum = buildstate->forkNum;
int dimensions = buildstate->dimensions;
Size etupSize;
Size maxSize;
HnswElementTuple etup;
HnswNeighborTuple ntup;
BlockNumber insertPage;
Buffer buf;
Page page;
GenericXLogState *state;
ListCell *lc;
/* Calculate sizes */
maxSize = BLCKSZ - MAXALIGN(SizeOfPageHeaderData) - MAXALIGN(sizeof(HnswPageOpaqueData));
etupSize = HNSW_ELEMENT_TUPLE_SIZE(dimensions);
/* Allocate once */
etup = palloc0(etupSize);
ntup = palloc0(maxSize);
/* Prepare first page */
buf = HnswNewBuffer(index, forkNum);
state = GenericXLogStart(index);
page = GenericXLogRegisterBuffer(state, buf, GENERIC_XLOG_FULL_IMAGE);
HnswInitPage(buf, page);
foreach(lc, buildstate->elements)
{
HnswElement element = lfirst(lc);
Size ntupSize;
Size combinedSize;
HnswSetElementTuple(etup, element);
/* Calculate sizes */
ntupSize = HNSW_NEIGHBOR_TUPLE_SIZE(element->level, buildstate->m);
combinedSize = etupSize + ntupSize + sizeof(ItemIdData);
/* Keep element and neighbors on the same page if possible */
if (PageGetFreeSpace(page) < etupSize || (combinedSize <= maxSize && PageGetFreeSpace(page) < combinedSize))
HnswBuildAppendPage(index, &buf, &page, &state, forkNum);
/* Calculate offsets */
element->blkno = BufferGetBlockNumber(buf);
element->offno = OffsetNumberNext(PageGetMaxOffsetNumber(page));
if (combinedSize <= maxSize)
{
element->neighborPage = element->blkno;
element->neighborOffno = OffsetNumberNext(element->offno);
}
else
{
element->neighborPage = element->blkno + 1;
element->neighborOffno = FirstOffsetNumber;
}
ItemPointerSet(&etup->neighbortid, element->neighborPage, element->neighborOffno);
/* Add element */
if (PageAddItem(page, (Item) etup, etupSize, InvalidOffsetNumber, false, false) != element->offno)
elog(ERROR, "failed to add index item to \"%s\"", RelationGetRelationName(index));
/* Add new page if needed */
if (PageGetFreeSpace(page) < ntupSize)
HnswBuildAppendPage(index, &buf, &page, &state, forkNum);
/* Add placeholder for neighbors */
if (PageAddItem(page, (Item) ntup, ntupSize, InvalidOffsetNumber, false, false) != element->neighborOffno)
elog(ERROR, "failed to add index item to \"%s\"", RelationGetRelationName(index));
}
insertPage = BufferGetBlockNumber(buf);
/* Commit */
MarkBufferDirty(buf);
GenericXLogFinish(state);
UnlockReleaseBuffer(buf);
HnswUpdateMetaPage(index, HNSW_UPDATE_ENTRY_ALWAYS, buildstate->entryPoint, insertPage, forkNum);
pfree(etup);
pfree(ntup);
}
/*
* Create neighbor pages
*/
static void
CreateNeighborPages(HnswBuildState * buildstate)
{
Relation index = buildstate->index;
ForkNumber forkNum = buildstate->forkNum;
int m = buildstate->m;
ListCell *lc;
HnswNeighborTuple ntup;
/* Allocate once */
ntup = palloc0(BLCKSZ);
foreach(lc, buildstate->elements)
{
HnswElement e = lfirst(lc);
Buffer buf;
Page page;
GenericXLogState *state;
Size ntupSize = HNSW_NEIGHBOR_TUPLE_SIZE(e->level, m);
/* Can take a while, so ensure we can interrupt */
/* Needs to be called when no buffer locks are held */
CHECK_FOR_INTERRUPTS();
buf = ReadBufferExtended(index, forkNum, e->neighborPage, RBM_NORMAL, NULL);
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
state = GenericXLogStart(index);
page = GenericXLogRegisterBuffer(state, buf, 0);
HnswSetNeighborTuple(ntup, e, m);
if (!PageIndexTupleOverwrite(page, e->neighborOffno, (Item) ntup, ntupSize))
elog(ERROR, "failed to add index item to \"%s\"", RelationGetRelationName(index));
/* Commit */
MarkBufferDirty(buf);
GenericXLogFinish(state);
UnlockReleaseBuffer(buf);
}
pfree(ntup);
}
/*
* Free elements
*/
static void
FreeElements(HnswBuildState * buildstate)
{
ListCell *lc;
foreach(lc, buildstate->elements)
HnswFreeElement(lfirst(lc));
list_free(buildstate->elements);
}
/*
* Flush pages
*/
static void
FlushPages(HnswBuildState * buildstate)
{
CreateMetaPage(buildstate);
CreateElementPages(buildstate);
CreateNeighborPages(buildstate);
buildstate->flushed = true;
FreeElements(buildstate);
}
/*
* Insert tuple
*/
static bool
InsertTuple(Relation index, Datum *values, HnswElement element, HnswBuildState * buildstate, HnswElement * dup)
{
FmgrInfo *procinfo = buildstate->procinfo;
Oid collation = buildstate->collation;
HnswElement entryPoint = buildstate->entryPoint;
int efConstruction = buildstate->efConstruction;
int m = buildstate->m;
/* Detoast once for all calls */
Datum value = PointerGetDatum(PG_DETOAST_DATUM(values[0]));
/* Normalize if needed */
if (buildstate->normprocinfo != NULL)
{
if (!HnswNormValue(buildstate->normprocinfo, collation, &value, buildstate->normvec))
return false;
}
/* Copy value to element so accessible outside of memory context */
memcpy(element->vec, DatumGetVector(value), VECTOR_SIZE(buildstate->dimensions));
/* Insert element in graph */
HnswInsertElement(element, entryPoint, NULL, procinfo, collation, m, efConstruction, false);
/* Look for duplicate */
*dup = HnswFindDuplicate(element);
/* Update neighbors if needed */
if (*dup == NULL)
{
for (int lc = element->level; lc >= 0; lc--)
{
int lm = HnswGetLayerM(m, lc);
HnswNeighborArray *neighbors = &element->neighbors[lc];
for (int i = 0; i < neighbors->length; i++)
HnswUpdateConnection(element, &neighbors->items[i], lm, lc, NULL, NULL, procinfo, collation);
}
}
/* Update entry point if needed */
if (*dup == NULL && (entryPoint == NULL || element->level > entryPoint->level))
buildstate->entryPoint = element;
UpdateProgress(PROGRESS_CREATEIDX_TUPLES_DONE, ++buildstate->indtuples);
return *dup == NULL;
}
/*
* Callback for table_index_build_scan
*/
static void
BuildCallback(Relation index, CALLBACK_ITEM_POINTER, Datum *values,
bool *isnull, bool tupleIsAlive, void *state)
{
HnswBuildState *buildstate = (HnswBuildState *) state;
MemoryContext oldCtx;
HnswElement element;
HnswElement dup = NULL;
bool inserted;
#if PG_VERSION_NUM < 130000
ItemPointer tid = &hup->t_self;
#endif
/* Skip nulls */
if (isnull[0])
return;
if (buildstate->indtuples >= buildstate->maxInMemoryElements)
{
if (!buildstate->flushed)
{
ereport(NOTICE,
(errmsg("hnsw graph no longer fits into maintenance_work_mem after " INT64_FORMAT " tuples", (int64) buildstate->indtuples),
errdetail("Building will take significantly more time."),
errhint("Increase maintenance_work_mem to speed up builds.")));
FlushPages(buildstate);
}
oldCtx = MemoryContextSwitchTo(buildstate->tmpCtx);
if (HnswInsertTuple(buildstate->index, values, isnull, tid, buildstate->heap))
UpdateProgress(PROGRESS_CREATEIDX_TUPLES_DONE, ++buildstate->indtuples);
/* Reset memory context */
MemoryContextSwitchTo(oldCtx);
MemoryContextReset(buildstate->tmpCtx);
return;
}
/* Allocate necessary memory outside of memory context */
element = HnswInitElement(tid, buildstate->m, buildstate->ml, buildstate->maxLevel);
element->vec = palloc(VECTOR_SIZE(buildstate->dimensions));
/* Use memory context since detoast can allocate */
oldCtx = MemoryContextSwitchTo(buildstate->tmpCtx);
/* Insert tuple */
inserted = InsertTuple(index, values, element, buildstate, &dup);
/* Reset memory context */
MemoryContextSwitchTo(oldCtx);
MemoryContextReset(buildstate->tmpCtx);
/* Add outside memory context */
if (dup != NULL)
HnswAddHeapTid(dup, tid);
/* Add to buildstate or free */
if (inserted)
buildstate->elements = lappend(buildstate->elements, element);
else
HnswFreeElement(element);
}
/*
* Get the max number of elements that fit into maintenance_work_mem
*/
static double
HnswGetMaxInMemoryElements(int m, double ml, int dimensions)
{
Size elementSize = sizeof(HnswElementData);
double avgLevel = -log(0.5) * ml;
elementSize += sizeof(HnswNeighborArray) * (avgLevel + 1);
elementSize += sizeof(HnswCandidate) * (m * (avgLevel + 2));
elementSize += sizeof(ItemPointerData);
elementSize += VECTOR_SIZE(dimensions);
return (maintenance_work_mem * 1024L) / elementSize;
}
/*
* Initialize the build state
*/
static void
InitBuildState(HnswBuildState * buildstate, Relation heap, Relation index, IndexInfo *indexInfo, ForkNumber forkNum)
{
buildstate->heap = heap;
buildstate->index = index;
buildstate->indexInfo = indexInfo;
buildstate->forkNum = forkNum;
buildstate->m = HnswGetM(index);
buildstate->efConstruction = HnswGetEfConstruction(index);
buildstate->dimensions = TupleDescAttr(index->rd_att, 0)->atttypmod;
/* Require column to have dimensions to be indexed */
if (buildstate->dimensions < 0)
elog(ERROR, "column does not have dimensions");
if (buildstate->dimensions > HNSW_MAX_DIM)
elog(ERROR, "column cannot have more than %d dimensions for hnsw index", HNSW_MAX_DIM);
if (buildstate->efConstruction < 2 * buildstate->m)
elog(ERROR, "ef_construction must be greater than or equal to 2 * m");
buildstate->reltuples = 0;
buildstate->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];
buildstate->elements = NIL;
buildstate->entryPoint = NULL;
buildstate->ml = HnswGetMl(buildstate->m);
buildstate->maxLevel = HnswGetMaxLevel(buildstate->m);
buildstate->maxInMemoryElements = HnswGetMaxInMemoryElements(buildstate->m, buildstate->ml, buildstate->dimensions);
buildstate->flushed = false;
/* Reuse for each tuple */
buildstate->normvec = InitVector(buildstate->dimensions);
buildstate->tmpCtx = AllocSetContextCreate(CurrentMemoryContext,
"Hnsw build temporary context",
ALLOCSET_DEFAULT_SIZES);
}
/*
* Free resources
*/
static void
FreeBuildState(HnswBuildState * buildstate)
{
pfree(buildstate->normvec);
MemoryContextDelete(buildstate->tmpCtx);
}
/*
* Build graph
*/
static void
BuildGraph(HnswBuildState * buildstate, ForkNumber forkNum)
{
UpdateProgress(PROGRESS_CREATEIDX_SUBPHASE, PROGRESS_HNSW_PHASE_LOAD);
#if PG_VERSION_NUM >= 120000
buildstate->reltuples = table_index_build_scan(buildstate->heap, buildstate->index, buildstate->indexInfo,
true, true, BuildCallback, (void *) buildstate, NULL);
#else
buildstate->reltuples = IndexBuildHeapScan(buildstate->heap, buildstate->index, buildstate->indexInfo,
true, BuildCallback, (void *) buildstate, NULL);
#endif
}
/*
* Build the index
*/
static void
BuildIndex(Relation heap, Relation index, IndexInfo *indexInfo,
HnswBuildState * buildstate, ForkNumber forkNum)
{
InitBuildState(buildstate, heap, index, indexInfo, forkNum);
if (buildstate->heap != NULL)
BuildGraph(buildstate, forkNum);
if (!buildstate->flushed)
FlushPages(buildstate);
FreeBuildState(buildstate);
}
/*
* Build the index for a logged table
*/
IndexBuildResult *
hnswbuild(Relation heap, Relation index, IndexInfo *indexInfo)
{
IndexBuildResult *result;
HnswBuildState buildstate;
BuildIndex(heap, index, indexInfo, &buildstate, MAIN_FORKNUM);
result = (IndexBuildResult *) palloc(sizeof(IndexBuildResult));
result->heap_tuples = buildstate.reltuples;
result->index_tuples = buildstate.indtuples;
return result;
}
/*
* Build the index for an unlogged table
*/
void
hnswbuildempty(Relation index)
{
IndexInfo *indexInfo = BuildIndexInfo(index);
HnswBuildState buildstate;
BuildIndex(NULL, index, indexInfo, &buildstate, INIT_FORKNUM);
}

View File

@@ -1,589 +0,0 @@
#include "postgres.h"
#include <math.h>
#include "hnsw.h"
#include "storage/bufmgr.h"
#include "storage/lmgr.h"
#include "utils/memutils.h"
/*
* Get the insert page
*/
static BlockNumber
GetInsertPage(Relation index)
{
Buffer buf;
Page page;
HnswMetaPage metap;
BlockNumber insertPage;
buf = ReadBuffer(index, HNSW_METAPAGE_BLKNO);
LockBuffer(buf, BUFFER_LOCK_SHARE);
page = BufferGetPage(buf);
metap = HnswPageGetMeta(page);
insertPage = metap->insertPage;
UnlockReleaseBuffer(buf);
return insertPage;
}
/*
* Check for a free offset
*/
static bool
HnswFreeOffset(Relation index, Buffer buf, Page page, HnswElement element, Size ntupSize, Buffer *nbuf, Page *npage, OffsetNumber *freeOffno, OffsetNumber *freeNeighborOffno, BlockNumber *newInsertPage)
{
OffsetNumber offno;
OffsetNumber maxoffno = PageGetMaxOffsetNumber(page);
for (offno = FirstOffsetNumber; offno <= maxoffno; offno = OffsetNumberNext(offno))
{
HnswElementTuple etup = (HnswElementTuple) PageGetItem(page, PageGetItemId(page, offno));
/* Skip neighbor tuples */
if (!HnswIsElementTuple(etup))
continue;
if (etup->deleted)
{
BlockNumber elementPage = BufferGetBlockNumber(buf);
BlockNumber neighborPage = ItemPointerGetBlockNumber(&etup->neighbortid);
OffsetNumber neighborOffno = ItemPointerGetOffsetNumber(&etup->neighbortid);
ItemId itemid;
if (!BlockNumberIsValid(*newInsertPage))
*newInsertPage = elementPage;
if (neighborPage == elementPage)
{
*nbuf = buf;
*npage = page;
}
else
{
*nbuf = ReadBuffer(index, neighborPage);
LockBuffer(*nbuf, BUFFER_LOCK_EXCLUSIVE);
/* Skip WAL for now */
*npage = BufferGetPage(*nbuf);
}
itemid = PageGetItemId(*npage, neighborOffno);
/* Check for space on neighbor tuple page */
if (PageGetFreeSpace(*npage) + ItemIdGetLength(itemid) - sizeof(ItemIdData) >= ntupSize)
{
*freeOffno = offno;
*freeNeighborOffno = neighborOffno;
return true;
}
else if (*nbuf != buf)
UnlockReleaseBuffer(*nbuf);
}
}
return false;
}
/*
* Add a new page
*/
static void
HnswInsertAppendPage(Relation index, Buffer *nbuf, Page *npage, GenericXLogState *state, Page page)
{
/* Add a new page */
LockRelationForExtension(index, ExclusiveLock);
*nbuf = HnswNewBuffer(index, MAIN_FORKNUM);
UnlockRelationForExtension(index, ExclusiveLock);
/* Init new page */
*npage = GenericXLogRegisterBuffer(state, *nbuf, GENERIC_XLOG_FULL_IMAGE);
HnswInitPage(*nbuf, *npage);
/* Update previous buffer */
HnswPageGetOpaque(page)->nextblkno = BufferGetBlockNumber(*nbuf);
}
/*
* Add to element and neighbor pages
*/
static void
WriteNewElementPages(Relation index, HnswElement e, int m, BlockNumber insertPage, BlockNumber *updatedInsertPage)
{
Buffer buf;
Page page;
GenericXLogState *state;
Size etupSize;
Size ntupSize;
Size combinedSize;
Size maxSize;
Size minCombinedSize;
HnswElementTuple etup;
BlockNumber currentPage = insertPage;
int dimensions = e->vec->dim;
HnswNeighborTuple ntup;
Buffer nbuf;
Page npage;
OffsetNumber freeOffno = InvalidOffsetNumber;
OffsetNumber freeNeighborOffno = InvalidOffsetNumber;
BlockNumber newInsertPage = InvalidBlockNumber;
/* Calculate sizes */
etupSize = HNSW_ELEMENT_TUPLE_SIZE(dimensions);
ntupSize = HNSW_NEIGHBOR_TUPLE_SIZE(e->level, m);
combinedSize = etupSize + ntupSize + sizeof(ItemIdData);
maxSize = BLCKSZ - MAXALIGN(SizeOfPageHeaderData) - MAXALIGN(sizeof(HnswPageOpaqueData));
minCombinedSize = etupSize + HNSW_NEIGHBOR_TUPLE_SIZE(0, m) + sizeof(ItemIdData);
/* Prepare element tuple */
etup = palloc0(etupSize);
HnswSetElementTuple(etup, e);
/* Prepare neighbor tuple */
ntup = palloc0(ntupSize);
HnswSetNeighborTuple(ntup, e, m);
/* Find a page (or two if needed) to insert the tuples */
for (;;)
{
buf = ReadBuffer(index, currentPage);
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
state = GenericXLogStart(index);
page = GenericXLogRegisterBuffer(state, buf, 0);
/* Keep track of first page where element at level 0 can fit */
if (!BlockNumberIsValid(newInsertPage) && PageGetFreeSpace(page) >= minCombinedSize)
newInsertPage = currentPage;
/* First, try the fastest path */
/* Space for both tuples on the current page */
/* This can split existing tuples in rare cases */
if (PageGetFreeSpace(page) >= combinedSize)
{
nbuf = buf;
npage = page;
break;
}
/* Next, try space from a deleted element */
if (HnswFreeOffset(index, buf, page, e, ntupSize, &nbuf, &npage, &freeOffno, &freeNeighborOffno, &newInsertPage))
{
if (nbuf != buf)
npage = GenericXLogRegisterBuffer(state, nbuf, 0);
break;
}
/* Finally, try space for element only if last page */
/* Skip if both tuples can fit on the same page */
if (combinedSize > maxSize && PageGetFreeSpace(page) >= etupSize && !BlockNumberIsValid(HnswPageGetOpaque(page)->nextblkno))
{
HnswInsertAppendPage(index, &nbuf, &npage, state, page);
break;
}
currentPage = HnswPageGetOpaque(page)->nextblkno;
if (BlockNumberIsValid(currentPage))
{
/* Move to next page */
GenericXLogAbort(state);
UnlockReleaseBuffer(buf);
}
else
{
Buffer newbuf;
Page newpage;
HnswInsertAppendPage(index, &newbuf, &newpage, state, page);
/* Commit */
MarkBufferDirty(newbuf);
MarkBufferDirty(buf);
GenericXLogFinish(state);
/* Unlock previous buffer */
UnlockReleaseBuffer(buf);
/* Prepare new buffer */
state = GenericXLogStart(index);
buf = newbuf;
page = GenericXLogRegisterBuffer(state, buf, 0);
/* Create new page for neighbors if needed */
if (PageGetFreeSpace(page) < combinedSize)
HnswInsertAppendPage(index, &nbuf, &npage, state, page);
else
{
nbuf = buf;
npage = page;
}
break;
}
}
e->blkno = BufferGetBlockNumber(buf);
e->neighborPage = BufferGetBlockNumber(nbuf);
/* Added tuple to new page if newInsertPage is not set */
/* So can set to neighbor page instead of element page */
if (!BlockNumberIsValid(newInsertPage))
newInsertPage = e->neighborPage;
if (OffsetNumberIsValid(freeOffno))
{
e->offno = freeOffno;
e->neighborOffno = freeNeighborOffno;
}
else
{
e->offno = OffsetNumberNext(PageGetMaxOffsetNumber(page));
if (nbuf == buf)
e->neighborOffno = OffsetNumberNext(e->offno);
else
e->neighborOffno = FirstOffsetNumber;
}
ItemPointerSet(&etup->neighbortid, e->neighborPage, e->neighborOffno);
/* Add element and neighbors */
if (OffsetNumberIsValid(freeOffno))
{
if (!PageIndexTupleOverwrite(page, e->offno, (Item) etup, etupSize))
elog(ERROR, "failed to add index item to \"%s\"", RelationGetRelationName(index));
if (!PageIndexTupleOverwrite(npage, e->neighborOffno, (Item) ntup, ntupSize))
elog(ERROR, "failed to add index item to \"%s\"", RelationGetRelationName(index));
}
else
{
if (PageAddItem(page, (Item) etup, etupSize, InvalidOffsetNumber, false, false) != e->offno)
elog(ERROR, "failed to add index item to \"%s\"", RelationGetRelationName(index));
if (PageAddItem(npage, (Item) ntup, ntupSize, InvalidOffsetNumber, false, false) != e->neighborOffno)
elog(ERROR, "failed to add index item to \"%s\"", RelationGetRelationName(index));
}
/* Commit */
MarkBufferDirty(buf);
if (nbuf != buf)
MarkBufferDirty(nbuf);
GenericXLogFinish(state);
UnlockReleaseBuffer(buf);
if (nbuf != buf)
UnlockReleaseBuffer(nbuf);
/* Update the insert page */
if (BlockNumberIsValid(newInsertPage) && newInsertPage != insertPage)
*updatedInsertPage = newInsertPage;
}
/*
* Check if connection already exists
*/
static bool
ConnectionExists(HnswElement e, HnswNeighborTuple ntup, int startIdx, int lm)
{
for (int i = 0; i < lm; i++)
{
ItemPointer indextid = &ntup->indextids[startIdx + i];
if (!ItemPointerIsValid(indextid))
break;
if (ItemPointerGetBlockNumber(indextid) == e->blkno && ItemPointerGetOffsetNumber(indextid) == e->offno)
return true;
}
return false;
}
/*
* Update neighbors
*/
void
HnswUpdateNeighborPages(Relation index, FmgrInfo *procinfo, Oid collation, HnswElement e, int m, bool checkExisting)
{
for (int lc = e->level; lc >= 0; lc--)
{
int lm = HnswGetLayerM(m, lc);
HnswNeighborArray *neighbors = &e->neighbors[lc];
for (int i = 0; i < neighbors->length; i++)
{
HnswCandidate *hc = &neighbors->items[i];
Buffer buf;
Page page;
GenericXLogState *state;
ItemId itemid;
HnswNeighborTuple ntup;
Size ntupSize;
int idx = -1;
int startIdx;
OffsetNumber offno = hc->element->neighborOffno;
/* Get latest neighbors since they may have changed */
/* Do not lock yet since selecting neighbors can take time */
HnswLoadNeighbors(hc->element, 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(e, hc, lm, lc, &idx, index, procinfo, collation);
/* New element was not selected as a neighbor */
if (idx == -1)
continue;
/* Register page */
buf = ReadBuffer(index, hc->element->neighborPage);
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
state = GenericXLogStart(index);
page = GenericXLogRegisterBuffer(state, buf, 0);
/* Get tuple */
itemid = PageGetItemId(page, offno);
ntup = (HnswNeighborTuple) PageGetItem(page, itemid);
ntupSize = ItemIdGetLength(itemid);
/* Calculate index for update */
startIdx = (hc->element->level - lc) * m;
/* Check for existing connection */
if (checkExisting && ConnectionExists(e, ntup, startIdx, lm))
idx = -1;
else if (idx == -2)
{
/* Find free offset if still exists */
/* TODO Retry updating connections if not */
for (int j = 0; j < lm; j++)
{
if (!ItemPointerIsValid(&ntup->indextids[startIdx + j]))
{
idx = startIdx + j;
break;
}
}
}
else
idx += startIdx;
/* Make robust to issues */
if (idx >= 0 && idx < ntup->count)
{
ItemPointer indextid = &ntup->indextids[idx];
/* Update neighbor */
ItemPointerSet(indextid, e->blkno, e->offno);
/* Overwrite tuple */
if (!PageIndexTupleOverwrite(page, offno, (Item) ntup, ntupSize))
elog(ERROR, "failed to add index item to \"%s\"", RelationGetRelationName(index));
/* Commit */
MarkBufferDirty(buf);
GenericXLogFinish(state);
}
else
GenericXLogAbort(state);
UnlockReleaseBuffer(buf);
}
}
}
/*
* Add a heap TID to an existing element
*/
static bool
HnswAddDuplicate(Relation index, HnswElement element, HnswElement dup)
{
Buffer buf;
Page page;
GenericXLogState *state;
Size etupSize = HNSW_ELEMENT_TUPLE_SIZE(dup->vec->dim);
HnswElementTuple etup;
int i;
/* Read page */
buf = ReadBuffer(index, dup->blkno);
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
state = GenericXLogStart(index);
page = GenericXLogRegisterBuffer(state, buf, 0);
/* Find space */
etup = (HnswElementTuple) PageGetItem(page, PageGetItemId(page, dup->offno));
for (i = 0; i < HNSW_HEAPTIDS; i++)
{
if (!ItemPointerIsValid(&etup->heaptids[i]))
break;
}
/* Either being deleted or we lost our chance to another backend */
if (i == 0 || i == HNSW_HEAPTIDS)
{
GenericXLogAbort(state);
UnlockReleaseBuffer(buf);
return false;
}
/* Add heap TID */
etup->heaptids[i] = *((ItemPointer) linitial(element->heaptids));
/* Overwrite tuple */
if (!PageIndexTupleOverwrite(page, dup->offno, (Item) etup, etupSize))
elog(ERROR, "failed to add index item to \"%s\"", RelationGetRelationName(index));
/* Commit */
MarkBufferDirty(buf);
GenericXLogFinish(state);
UnlockReleaseBuffer(buf);
return true;
}
/*
* Write changes to disk
*/
static void
WriteElement(Relation index, FmgrInfo *procinfo, Oid collation, HnswElement element, int m, int efConstruction, HnswElement dup, HnswElement entryPoint)
{
BlockNumber newInsertPage = InvalidBlockNumber;
/* Try to add to existing page */
if (dup != NULL)
{
if (HnswAddDuplicate(index, element, dup))
return;
}
/* Write element and neighbor tuples */
WriteNewElementPages(index, element, m, GetInsertPage(index), &newInsertPage);
/* Update insert page if needed */
if (BlockNumberIsValid(newInsertPage))
HnswUpdateMetaPage(index, 0, NULL, newInsertPage, MAIN_FORKNUM);
/* Update neighbors */
HnswUpdateNeighborPages(index, procinfo, collation, element, m, false);
/* Update metapage if needed */
if (entryPoint == NULL || element->level > entryPoint->level)
HnswUpdateMetaPage(index, HNSW_UPDATE_ENTRY_GREATER, element, InvalidBlockNumber, MAIN_FORKNUM);
}
/*
* Insert a tuple into the index
*/
bool
HnswInsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heap_tid, Relation heapRel)
{
Datum value;
FmgrInfo *normprocinfo;
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];
HnswElement dup;
LOCKMODE lockmode = ShareLock;
/* Detoast once for all calls */
value = PointerGetDatum(PG_DETOAST_DATUM(values[0]));
/* Normalize if needed */
normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC);
if (normprocinfo != NULL)
{
if (!HnswNormValue(normprocinfo, collation, &value, NULL))
return false;
}
/*
* Get a shared lock. This allows vacuum to ensure no in-flight inserts
* before repairing graph. Use a page lock so it does not interfere with
* buffer lock (or reads when vacuuming).
*/
LockPage(index, HNSW_UPDATE_LOCK, lockmode);
/* Get m and entry point */
HnswGetMetaPageInfo(index, &m, &entryPoint);
/* Create an element */
element = HnswInitElement(heap_tid, m, HnswGetMl(m), HnswGetMaxLevel(m));
element->vec = DatumGetVector(value);
/* Prevent concurrent inserts when likely updating entry point */
if (entryPoint == NULL || element->level > entryPoint->level)
{
/* Release shared lock */
UnlockPage(index, HNSW_UPDATE_LOCK, lockmode);
/* Get exclusive lock */
lockmode = ExclusiveLock;
LockPage(index, HNSW_UPDATE_LOCK, lockmode);
/* Get latest entry point after lock is acquired */
entryPoint = HnswGetEntryPoint(index);
}
/* Insert element in graph */
HnswInsertElement(element, entryPoint, index, procinfo, collation, m, efConstruction, false);
/* Look for duplicate */
dup = HnswFindDuplicate(element);
/* Write to disk */
WriteElement(index, procinfo, collation, element, m, efConstruction, dup, entryPoint);
/* Release lock */
UnlockPage(index, HNSW_UPDATE_LOCK, lockmode);
return true;
}
/*
* Insert a tuple into the index
*/
bool
hnswinsert(Relation index, Datum *values, bool *isnull, ItemPointer heap_tid,
Relation heap, IndexUniqueCheck checkUnique
#if PG_VERSION_NUM >= 140000
,bool indexUnchanged
#endif
,IndexInfo *indexInfo
)
{
MemoryContext oldCtx;
MemoryContext insertCtx;
/* Skip nulls */
if (isnull[0])
return false;
/* Create memory context */
insertCtx = AllocSetContextCreate(CurrentMemoryContext,
"Hnsw insert temporary context",
ALLOCSET_DEFAULT_SIZES);
oldCtx = MemoryContextSwitchTo(insertCtx);
/* Insert tuple */
HnswInsertTuple(index, values, isnull, heap_tid, heap);
/* Delete memory context */
MemoryContextSwitchTo(oldCtx);
MemoryContextDelete(insertCtx);
return false;
}

View File

@@ -1,243 +0,0 @@
#include "postgres.h"
#include "access/relscan.h"
#include "hnsw.h"
#include "pgstat.h"
#include "storage/bufmgr.h"
#include "storage/lmgr.h"
#include "utils/memutils.h"
/*
* Algorithm 5 from paper
*/
static List *
GetScanItems(IndexScanDesc scan, Datum q)
{
HnswScanOpaque so = (HnswScanOpaque) scan->opaque;
Relation index = scan->indexRelation;
FmgrInfo *procinfo = so->procinfo;
Oid collation = so->collation;
List *ep;
List *w;
int m;
HnswElement entryPoint;
/* Get m and entry point */
HnswGetMetaPageInfo(index, &m, &entryPoint);
if (entryPoint == NULL)
return NIL;
ep = list_make1(HnswEntryCandidate(entryPoint, q, index, procinfo, collation, false));
for (int lc = entryPoint->level; lc >= 1; lc--)
{
w = HnswSearchLayer(q, ep, 1, lc, index, procinfo, collation, m, false, NULL);
ep = w;
}
return HnswSearchLayer(q, ep, hnsw_ef_search, 0, index, procinfo, collation, m, false, NULL);
}
/*
* Get dimensions from metapage
*/
static int
GetDimensions(Relation index)
{
Buffer buf;
Page page;
HnswMetaPage metap;
int dimensions;
buf = ReadBuffer(index, HNSW_METAPAGE_BLKNO);
LockBuffer(buf, BUFFER_LOCK_SHARE);
page = BufferGetPage(buf);
metap = HnswPageGetMeta(page);
dimensions = metap->dimensions;
UnlockReleaseBuffer(buf);
return dimensions;
}
/*
* Get scan value
*/
static Datum
GetScanValue(IndexScanDesc scan)
{
HnswScanOpaque so = (HnswScanOpaque) scan->opaque;
Datum value;
if (scan->orderByData->sk_flags & SK_ISNULL)
value = PointerGetDatum(InitVector(GetDimensions(scan->indexRelation)));
else
{
value = scan->orderByData->sk_argument;
/* Value should not be compressed or toasted */
Assert(!VARATT_IS_COMPRESSED(DatumGetPointer(value)));
Assert(!VARATT_IS_EXTENDED(DatumGetPointer(value)));
/* Fine if normalization fails */
if (so->normprocinfo != NULL)
HnswNormValue(so->normprocinfo, so->collation, &value, NULL);
}
return value;
}
/*
* Prepare for an index scan
*/
IndexScanDesc
hnswbeginscan(Relation index, int nkeys, int norderbys)
{
IndexScanDesc scan;
HnswScanOpaque so;
scan = RelationGetIndexScan(index, nkeys, norderbys);
so = (HnswScanOpaque) palloc(sizeof(HnswScanOpaqueData));
so->buf = InvalidBuffer;
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];
scan->opaque = so;
return scan;
}
/*
* Start or restart an index scan
*/
void
hnswrescan(IndexScanDesc scan, ScanKey keys, int nkeys, ScanKey orderbys, int norderbys)
{
HnswScanOpaque so = (HnswScanOpaque) scan->opaque;
so->first = true;
MemoryContextReset(so->tmpCtx);
if (keys && scan->numberOfKeys > 0)
memmove(scan->keyData, keys, scan->numberOfKeys * sizeof(ScanKeyData));
if (orderbys && scan->numberOfOrderBys > 0)
memmove(scan->orderByData, orderbys, scan->numberOfOrderBys * sizeof(ScanKeyData));
}
/*
* Fetch the next tuple in the given scan
*/
bool
hnswgettuple(IndexScanDesc scan, ScanDirection dir)
{
HnswScanOpaque so = (HnswScanOpaque) scan->opaque;
MemoryContext oldCtx = MemoryContextSwitchTo(so->tmpCtx);
/*
* Index can be used to scan backward, but Postgres doesn't support
* backward scan on operators
*/
Assert(ScanDirectionIsForward(dir));
if (so->first)
{
Datum value;
/* Count index scan for stats */
pgstat_count_index_scan(scan->indexRelation);
/* Safety check */
if (scan->orderByData == NULL)
elog(ERROR, "cannot scan hnsw index without order");
/* Get scan value */
value = GetScanValue(scan);
/*
* Get a shared lock. This allows vacuum to ensure no in-flight scans
* before marking tuples as deleted.
*/
LockPage(scan->indexRelation, HNSW_SCAN_LOCK, ShareLock);
so->w = GetScanItems(scan, value);
/* Release shared lock */
UnlockPage(scan->indexRelation, HNSW_SCAN_LOCK, ShareLock);
so->first = false;
}
while (list_length(so->w) > 0)
{
HnswCandidate *hc = llast(so->w);
ItemPointer heaptid;
BlockNumber indexblkno;
/* Move to next element if no valid heap TIDs */
if (list_length(hc->element->heaptids) == 0)
{
so->w = list_delete_last(so->w);
continue;
}
heaptid = llast(hc->element->heaptids);
indexblkno = hc->element->blkno;
hc->element->heaptids = list_delete_last(hc->element->heaptids);
MemoryContextSwitchTo(oldCtx);
#if PG_VERSION_NUM >= 120000
scan->xs_heaptid = *heaptid;
#else
scan->xs_ctup.t_self = *heaptid;
#endif
/* Unpin buffer */
if (BufferIsValid(so->buf))
ReleaseBuffer(so->buf);
/*
* An index scan must maintain a pin on the index page holding the
* item last returned by amgettuple
*
* https://www.postgresql.org/docs/current/index-locking.html
*/
so->buf = ReadBuffer(scan->indexRelation, indexblkno);
scan->xs_recheckorderby = false;
return true;
}
MemoryContextSwitchTo(oldCtx);
return false;
}
/*
* End a scan and release resources
*/
void
hnswendscan(IndexScanDesc scan)
{
HnswScanOpaque so = (HnswScanOpaque) scan->opaque;
/* Release pin */
if (BufferIsValid(so->buf))
ReleaseBuffer(so->buf);
MemoryContextDelete(so->tmpCtx);
pfree(so);
scan->opaque = NULL;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,660 +0,0 @@
#include "postgres.h"
#include <math.h>
#include "commands/vacuum.h"
#include "hnsw.h"
#include "storage/bufmgr.h"
#include "storage/lmgr.h"
#include "utils/memutils.h"
/*
* Check if deleted list contains an index TID
*/
static bool
DeletedContains(HTAB *deleted, ItemPointer indextid)
{
bool found;
hash_search(deleted, indextid, HASH_FIND, &found);
return found;
}
/*
* Remove deleted heap TIDs
*
* OK to remove for entry point, since always considered for searches and inserts
*/
static void
RemoveHeapTids(HnswVacuumState * vacuumstate)
{
BlockNumber blkno = HNSW_HEAD_BLKNO;
HnswElement highestPoint = &vacuumstate->highestPoint;
Relation index = vacuumstate->index;
BufferAccessStrategy bas = vacuumstate->bas;
HnswElement entryPoint = HnswGetEntryPoint(vacuumstate->index);
IndexBulkDeleteResult *stats = vacuumstate->stats;
/* Store separately since highestPoint.level is uint8 */
int highestLevel = -1;
/* Initialize highest point */
highestPoint->blkno = InvalidBlockNumber;
highestPoint->offno = InvalidOffsetNumber;
while (BlockNumberIsValid(blkno))
{
Buffer buf;
Page page;
GenericXLogState *state;
OffsetNumber offno;
OffsetNumber maxoffno;
bool updated = false;
vacuum_delay_point();
buf = ReadBufferExtended(index, MAIN_FORKNUM, blkno, RBM_NORMAL, bas);
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
state = GenericXLogStart(index);
page = GenericXLogRegisterBuffer(state, buf, 0);
maxoffno = PageGetMaxOffsetNumber(page);
/* Iterate over nodes */
for (offno = FirstOffsetNumber; offno <= maxoffno; offno = OffsetNumberNext(offno))
{
HnswElementTuple etup = (HnswElementTuple) PageGetItem(page, PageGetItemId(page, offno));
int idx = 0;
bool itemUpdated = false;
/* Skip neighbor tuples */
if (!HnswIsElementTuple(etup))
continue;
if (ItemPointerIsValid(&etup->heaptids[0]))
{
for (int i = 0; i < HNSW_HEAPTIDS; i++)
{
/* Stop at first unused */
if (!ItemPointerIsValid(&etup->heaptids[i]))
break;
if (vacuumstate->callback(&etup->heaptids[i], vacuumstate->callback_state))
{
itemUpdated = true;
stats->tuples_removed++;
}
else
{
/* Move to front of list */
etup->heaptids[idx++] = etup->heaptids[i];
stats->num_index_tuples++;
}
}
if (itemUpdated)
{
Size etupSize = HNSW_ELEMENT_TUPLE_SIZE(etup->vec.dim);
/* Mark rest as invalid */
for (int i = idx; i < HNSW_HEAPTIDS; i++)
ItemPointerSetInvalid(&etup->heaptids[i]);
if (!PageIndexTupleOverwrite(page, offno, (Item) etup, etupSize))
elog(ERROR, "failed to add index item to \"%s\"", RelationGetRelationName(index));
updated = true;
}
}
if (!ItemPointerIsValid(&etup->heaptids[0]))
{
ItemPointerData ip;
/* Add to deleted list */
ItemPointerSet(&ip, blkno, offno);
(void) hash_search(vacuumstate->deleted, &ip, HASH_ENTER, NULL);
}
else if (etup->level > highestLevel && !(entryPoint != NULL && blkno == entryPoint->blkno && offno == entryPoint->offno))
{
/* Keep track of highest non-entry point */
highestPoint->blkno = blkno;
highestPoint->offno = offno;
highestPoint->level = etup->level;
highestLevel = etup->level;
}
}
blkno = HnswPageGetOpaque(page)->nextblkno;
if (updated)
{
MarkBufferDirty(buf);
GenericXLogFinish(state);
}
else
GenericXLogAbort(state);
UnlockReleaseBuffer(buf);
}
}
/*
* Check for deleted neighbors
*/
static bool
NeedsUpdated(HnswVacuumState * vacuumstate, HnswElement element)
{
Relation index = vacuumstate->index;
BufferAccessStrategy bas = vacuumstate->bas;
Buffer buf;
Page page;
HnswNeighborTuple ntup;
bool needsUpdated = false;
buf = ReadBufferExtended(index, MAIN_FORKNUM, element->neighborPage, RBM_NORMAL, bas);
LockBuffer(buf, BUFFER_LOCK_SHARE);
page = BufferGetPage(buf);
ntup = (HnswNeighborTuple) PageGetItem(page, PageGetItemId(page, element->neighborOffno));
Assert(HnswIsNeighborTuple(ntup));
/* Check neighbors */
for (int i = 0; i < ntup->count; i++)
{
ItemPointer indextid = &ntup->indextids[i];
if (!ItemPointerIsValid(indextid))
continue;
/* Check if in deleted list */
if (DeletedContains(vacuumstate->deleted, indextid))
{
needsUpdated = true;
break;
}
}
/* Also update if layer 0 is not full */
/* This could indicate too many candidates being deleted during insert */
if (!needsUpdated)
needsUpdated = !ItemPointerIsValid(&ntup->indextids[ntup->count - 1]);
UnlockReleaseBuffer(buf);
return needsUpdated;
}
/*
* Repair graph for a single element
*/
static void
RepairGraphElement(HnswVacuumState * vacuumstate, HnswElement element, HnswElement entryPoint)
{
Relation index = vacuumstate->index;
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);
/* Skip if element is entry point */
if (entryPoint != NULL && element->blkno == entryPoint->blkno && element->offno == entryPoint->offno)
return;
/* Init fields */
HnswInitNeighbors(element, m);
element->heaptids = NIL;
/* Add element to graph, skipping itself */
HnswInsertElement(element, entryPoint, index, procinfo, collation, m, efConstruction, true);
/* Update neighbor tuple */
/* Do this before getting page to minimize locking */
HnswSetNeighborTuple(ntup, element, m);
/* Get neighbor page */
buf = ReadBufferExtended(index, MAIN_FORKNUM, element->neighborPage, RBM_NORMAL, bas);
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
state = GenericXLogStart(index);
page = GenericXLogRegisterBuffer(state, buf, 0);
/* Overwrite tuple */
if (!PageIndexTupleOverwrite(page, element->neighborOffno, (Item) ntup, ntupSize))
elog(ERROR, "failed to add index item to \"%s\"", RelationGetRelationName(index));
/* Commit */
MarkBufferDirty(buf);
GenericXLogFinish(state);
UnlockReleaseBuffer(buf);
/* Update neighbors */
HnswUpdateNeighborPages(index, procinfo, collation, element, m, true);
}
/*
* Repair graph entry point
*/
static void
RepairGraphEntryPoint(HnswVacuumState * vacuumstate)
{
Relation index = vacuumstate->index;
HnswElement highestPoint = &vacuumstate->highestPoint;
HnswElement entryPoint;
MemoryContext oldCtx = MemoryContextSwitchTo(vacuumstate->tmpCtx);
if (!BlockNumberIsValid(highestPoint->blkno))
highestPoint = NULL;
/*
* Repair graph for highest non-entry point. Highest point may be outdated
* due to inserts that happen during and after RemoveHeapTids.
*/
if (highestPoint != NULL)
{
/* Get a shared lock */
LockPage(index, HNSW_UPDATE_LOCK, ShareLock);
/* Load element */
HnswLoadElement(highestPoint, NULL, NULL, index, vacuumstate->procinfo, vacuumstate->collation, true);
/* Repair if needed */
if (NeedsUpdated(vacuumstate, highestPoint))
RepairGraphElement(vacuumstate, highestPoint, HnswGetEntryPoint(index));
/* Release lock */
UnlockPage(index, HNSW_UPDATE_LOCK, ShareLock);
}
/* Prevent concurrent inserts when possibly updating entry point */
LockPage(index, HNSW_UPDATE_LOCK, ExclusiveLock);
/* Get latest entry point */
entryPoint = HnswGetEntryPoint(index);
if (entryPoint != NULL)
{
ItemPointerData epData;
ItemPointerSet(&epData, entryPoint->blkno, entryPoint->offno);
if (DeletedContains(vacuumstate->deleted, &epData))
{
/*
* Replace the entry point with the highest point. If highest
* point is outdated and empty, the entry point will be empty
* until an element is repaired.
*/
HnswUpdateMetaPage(index, HNSW_UPDATE_ENTRY_ALWAYS, highestPoint, InvalidBlockNumber, MAIN_FORKNUM);
}
else
{
/*
* Repair the entry point with the highest point. If highest point
* is outdated, this can remove connections at higher levels in
* the graph until they are repaired, but this should be fine.
*/
HnswLoadElement(entryPoint, NULL, NULL, index, vacuumstate->procinfo, vacuumstate->collation, true);
if (NeedsUpdated(vacuumstate, entryPoint))
{
/* Reset neighbors from previous update */
if (highestPoint != NULL)
highestPoint->neighbors = NULL;
RepairGraphElement(vacuumstate, entryPoint, highestPoint);
}
}
}
/* Release lock */
UnlockPage(index, HNSW_UPDATE_LOCK, ExclusiveLock);
/* Reset memory context */
MemoryContextSwitchTo(oldCtx);
MemoryContextReset(vacuumstate->tmpCtx);
}
/*
* Repair graph for all elements
*/
static void
RepairGraph(HnswVacuumState * vacuumstate)
{
Relation index = vacuumstate->index;
BufferAccessStrategy bas = vacuumstate->bas;
BlockNumber blkno = HNSW_HEAD_BLKNO;
/* Wait for inserts to complete */
LockPage(index, HNSW_UPDATE_LOCK, ExclusiveLock);
UnlockPage(index, HNSW_UPDATE_LOCK, ExclusiveLock);
/* Repair entry point first */
RepairGraphEntryPoint(vacuumstate);
while (BlockNumberIsValid(blkno))
{
Buffer buf;
Page page;
OffsetNumber offno;
OffsetNumber maxoffno;
List *elements = NIL;
ListCell *lc2;
MemoryContext oldCtx;
vacuum_delay_point();
oldCtx = MemoryContextSwitchTo(vacuumstate->tmpCtx);
buf = ReadBufferExtended(index, MAIN_FORKNUM, blkno, RBM_NORMAL, bas);
LockBuffer(buf, BUFFER_LOCK_SHARE);
page = BufferGetPage(buf);
maxoffno = PageGetMaxOffsetNumber(page);
/* Load items into memory to minimize locking */
for (offno = FirstOffsetNumber; offno <= maxoffno; offno = OffsetNumberNext(offno))
{
HnswElementTuple etup = (HnswElementTuple) PageGetItem(page, PageGetItemId(page, offno));
HnswElement element;
/* Skip neighbor tuples */
if (!HnswIsElementTuple(etup))
continue;
/* Skip updating neighbors if being deleted */
if (!ItemPointerIsValid(&etup->heaptids[0]))
continue;
/* Create an element */
element = HnswInitElementFromBlock(blkno, offno);
HnswLoadElementFromTuple(element, etup, false, true);
elements = lappend(elements, element);
}
blkno = HnswPageGetOpaque(page)->nextblkno;
UnlockReleaseBuffer(buf);
/* Update neighbor pages */
foreach(lc2, elements)
{
HnswElement element = (HnswElement) lfirst(lc2);
HnswElement entryPoint;
LOCKMODE lockmode = ShareLock;
/* Check if any neighbors point to deleted values */
if (!NeedsUpdated(vacuumstate, element))
continue;
/* Get a shared lock */
LockPage(index, HNSW_UPDATE_LOCK, lockmode);
/* Refresh entry point for each element */
entryPoint = HnswGetEntryPoint(index);
/* Prevent concurrent inserts when likely updating entry point */
if (entryPoint == NULL || element->level > entryPoint->level)
{
/* Release shared lock */
UnlockPage(index, HNSW_UPDATE_LOCK, lockmode);
/* Get exclusive lock */
lockmode = ExclusiveLock;
LockPage(index, HNSW_UPDATE_LOCK, lockmode);
/* Get latest entry point after lock is acquired */
entryPoint = HnswGetEntryPoint(index);
}
/* Repair connections */
RepairGraphElement(vacuumstate, element, entryPoint);
/*
* Update metapage if needed. Should only happen if entry point
* was replaced and highest point was outdated.
*/
if (entryPoint == NULL || element->level > entryPoint->level)
HnswUpdateMetaPage(index, HNSW_UPDATE_ENTRY_GREATER, element, InvalidBlockNumber, MAIN_FORKNUM);
/* Release lock */
UnlockPage(index, HNSW_UPDATE_LOCK, lockmode);
}
/* Reset memory context */
MemoryContextSwitchTo(oldCtx);
MemoryContextReset(vacuumstate->tmpCtx);
}
}
/*
* Mark items as deleted
*/
static void
MarkDeleted(HnswVacuumState * vacuumstate)
{
BlockNumber blkno = HNSW_HEAD_BLKNO;
BlockNumber insertPage = InvalidBlockNumber;
Relation index = vacuumstate->index;
BufferAccessStrategy bas = vacuumstate->bas;
/* Wait for selects to complete */
LockPage(index, HNSW_SCAN_LOCK, ExclusiveLock);
UnlockPage(index, HNSW_SCAN_LOCK, ExclusiveLock);
while (BlockNumberIsValid(blkno))
{
Buffer buf;
Page page;
GenericXLogState *state;
OffsetNumber offno;
OffsetNumber maxoffno;
vacuum_delay_point();
buf = ReadBufferExtended(index, MAIN_FORKNUM, blkno, RBM_NORMAL, bas);
/*
* ambulkdelete cannot delete entries from pages that are pinned by
* other backends
*
* https://www.postgresql.org/docs/current/index-locking.html
*/
LockBufferForCleanup(buf);
state = GenericXLogStart(index);
page = GenericXLogRegisterBuffer(state, buf, 0);
maxoffno = PageGetMaxOffsetNumber(page);
/* Update element and neighbors together */
for (offno = FirstOffsetNumber; offno <= maxoffno; offno = OffsetNumberNext(offno))
{
HnswElementTuple etup = (HnswElementTuple) PageGetItem(page, PageGetItemId(page, offno));
HnswNeighborTuple ntup;
Size etupSize;
Size ntupSize;
Buffer nbuf;
Page npage;
BlockNumber neighborPage;
OffsetNumber neighborOffno;
/* Skip neighbor tuples */
if (!HnswIsElementTuple(etup))
continue;
/* Skip deleted tuples */
if (etup->deleted)
{
/* Set to first free page */
if (!BlockNumberIsValid(insertPage))
insertPage = blkno;
continue;
}
/* Skip live tuples */
if (ItemPointerIsValid(&etup->heaptids[0]))
continue;
/* Calculate sizes */
etupSize = HNSW_ELEMENT_TUPLE_SIZE(etup->vec.dim);
ntupSize = HNSW_NEIGHBOR_TUPLE_SIZE(etup->level, vacuumstate->m);
/* Get neighbor page */
neighborPage = ItemPointerGetBlockNumber(&etup->neighbortid);
neighborOffno = ItemPointerGetOffsetNumber(&etup->neighbortid);
if (neighborPage == blkno)
{
nbuf = buf;
npage = page;
}
else
{
nbuf = ReadBufferExtended(index, MAIN_FORKNUM, neighborPage, RBM_NORMAL, bas);
LockBuffer(nbuf, BUFFER_LOCK_EXCLUSIVE);
npage = GenericXLogRegisterBuffer(state, nbuf, 0);
}
ntup = (HnswNeighborTuple) PageGetItem(npage, PageGetItemId(npage, neighborOffno));
/* Overwrite element */
etup->deleted = 1;
MemSet(&etup->vec.x, 0, etup->vec.dim * sizeof(float));
/* Overwrite neighbors */
for (int i = 0; i < ntup->count; i++)
ItemPointerSetInvalid(&ntup->indextids[i]);
/* Overwrite element tuple */
if (!PageIndexTupleOverwrite(page, offno, (Item) etup, etupSize))
elog(ERROR, "failed to add index item to \"%s\"", RelationGetRelationName(index));
/* Overwrite neighbor tuple */
if (!PageIndexTupleOverwrite(npage, neighborOffno, (Item) ntup, ntupSize))
elog(ERROR, "failed to add index item to \"%s\"", RelationGetRelationName(index));
/* Commit */
MarkBufferDirty(buf);
if (nbuf != buf)
MarkBufferDirty(nbuf);
GenericXLogFinish(state);
if (nbuf != buf)
UnlockReleaseBuffer(nbuf);
/* Set to first free page */
if (!BlockNumberIsValid(insertPage))
insertPage = blkno;
/* Prepare new xlog */
state = GenericXLogStart(index);
page = GenericXLogRegisterBuffer(state, buf, 0);
}
blkno = HnswPageGetOpaque(page)->nextblkno;
GenericXLogAbort(state);
UnlockReleaseBuffer(buf);
}
/* Update insert page last, after everything has been marked as deleted */
HnswUpdateMetaPage(index, 0, NULL, insertPage, MAIN_FORKNUM);
}
/*
* Initialize the vacuum state
*/
static void
InitVacuumState(HnswVacuumState * vacuumstate, IndexVacuumInfo *info, IndexBulkDeleteResult *stats, IndexBulkDeleteCallback callback, void *callback_state)
{
Relation index = info->index;
HASHCTL hash_ctl;
if (stats == NULL)
stats = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult));
vacuumstate->index = index;
vacuumstate->stats = stats;
vacuumstate->callback = callback;
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(BLCKSZ);
vacuumstate->tmpCtx = AllocSetContextCreate(CurrentMemoryContext,
"Hnsw vacuum temporary context",
ALLOCSET_DEFAULT_SIZES);
/* Get m from metapage */
HnswGetMetaPageInfo(index, &vacuumstate->m, NULL);
/* Create hash table */
hash_ctl.keysize = sizeof(ItemPointerData);
hash_ctl.entrysize = sizeof(ItemPointerData);
hash_ctl.hcxt = CurrentMemoryContext;
vacuumstate->deleted = hash_create("hnswbulkdelete indextids", 256, &hash_ctl, HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
}
/*
* Free resources
*/
static void
FreeVacuumState(HnswVacuumState * vacuumstate)
{
hash_destroy(vacuumstate->deleted);
FreeAccessStrategy(vacuumstate->bas);
pfree(vacuumstate->ntup);
MemoryContextDelete(vacuumstate->tmpCtx);
}
/*
* Bulk delete tuples from the index
*/
IndexBulkDeleteResult *
hnswbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
IndexBulkDeleteCallback callback, void *callback_state)
{
HnswVacuumState vacuumstate;
InitVacuumState(&vacuumstate, info, stats, callback, callback_state);
/* Pass 1: Remove heap TIDs */
RemoveHeapTids(&vacuumstate);
/* Pass 2: Repair graph */
RepairGraph(&vacuumstate);
/* Pass 3: Mark as deleted */
MarkDeleted(&vacuumstate);
FreeVacuumState(&vacuumstate);
return vacuumstate.stats;
}
/*
* Clean up after a VACUUM operation
*/
IndexBulkDeleteResult *
hnswvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
{
Relation rel = info->index;
if (info->analyze_only)
return stats;
/* stats is NULL if ambulkdelete not called */
/* OK to return NULL if index not changed */
if (stats == NULL)
return NULL;
stats->num_pages = RelationGetNumberOfBlocks(rel);
return stats;
}

View File

@@ -2,16 +2,10 @@
#include <float.h> #include <float.h>
#include "access/parallel.h"
#include "access/xact.h"
#include "catalog/index.h" #include "catalog/index.h"
#include "catalog/pg_operator_d.h"
#include "catalog/pg_type_d.h"
#include "ivfflat.h" #include "ivfflat.h"
#include "miscadmin.h" #include "miscadmin.h"
#include "storage/bufmgr.h" #include "storage/bufmgr.h"
#include "tcop/tcopprot.h"
#include "utils/memutils.h"
#if PG_VERSION_NUM >= 140000 #if PG_VERSION_NUM >= 140000
#include "utils/backend_progress.h" #include "utils/backend_progress.h"
@@ -28,6 +22,14 @@
#define PROGRESS_CREATEIDX_TUPLES_DONE 0 #define PROGRESS_CREATEIDX_TUPLES_DONE 0
#endif #endif
#if PG_VERSION_NUM >= 110000
#include "catalog/pg_operator_d.h"
#include "catalog/pg_type_d.h"
#else
#include "catalog/pg_operator.h"
#include "catalog/pg_type.h"
#endif
#if PG_VERSION_NUM >= 130000 #if PG_VERSION_NUM >= 130000
#define CALLBACK_ITEM_POINTER ItemPointer tid #define CALLBACK_ITEM_POINTER ItemPointer tid
#else #else
@@ -40,36 +42,21 @@
#define UpdateProgress(index, val) ((void)val) #define UpdateProgress(index, val) ((void)val)
#endif #endif
#if PG_VERSION_NUM >= 140000
#include "utils/backend_status.h"
#include "utils/wait_event.h"
#endif
#if PG_VERSION_NUM >= 120000
#include "access/table.h"
#include "optimizer/optimizer.h"
#else
#include "access/heapam.h"
#include "optimizer/planner.h"
#include "pgstat.h"
#endif
#define PARALLEL_KEY_IVFFLAT_SHARED UINT64CONST(0xA000000000000001)
#define PARALLEL_KEY_TUPLESORT UINT64CONST(0xA000000000000002)
#define PARALLEL_KEY_IVFFLAT_CENTERS UINT64CONST(0xA000000000000003)
#define PARALLEL_KEY_QUERY_TEXT UINT64CONST(0xA000000000000004)
/* /*
* Add sample * Callback for sampling
*/ */
static void static void
AddSample(Datum *values, IvfflatBuildState * buildstate) SampleCallback(Relation index, CALLBACK_ITEM_POINTER, Datum *values,
bool *isnull, bool tupleIsAlive, void *state)
{ {
IvfflatBuildState *buildstate = (IvfflatBuildState *) state;
VectorArray samples = buildstate->samples; VectorArray samples = buildstate->samples;
int targsamples = samples->maxlen; int targsamples = samples->maxlen;
Datum value = values[0];
/* Detoast once for all calls */ /* Skip nulls */
Datum value = PointerGetDatum(PG_DETOAST_DATUM(values[0])); if (isnull[0])
return;
/* /*
* Normalize with KMEANS_NORM_PROC since spherical distance function * Normalize with KMEANS_NORM_PROC since spherical distance function
@@ -107,31 +94,6 @@ AddSample(Datum *values, IvfflatBuildState * buildstate)
} }
} }
/*
* Callback for sampling
*/
static void
SampleCallback(Relation index, CALLBACK_ITEM_POINTER, Datum *values,
bool *isnull, bool tupleIsAlive, void *state)
{
IvfflatBuildState *buildstate = (IvfflatBuildState *) state;
MemoryContext oldCtx;
/* Skip nulls */
if (isnull[0])
return;
/* Use memory context since detoast can allocate */
oldCtx = MemoryContextSwitchTo(buildstate->tmpCtx);
/* Add sample */
AddSample(values, state);
/* Reset memory context */
MemoryContextSwitchTo(oldCtx);
MemoryContextReset(buildstate->tmpCtx);
}
/* /*
* Sample rows with same logic as ANALYZE * Sample rows with same logic as ANALYZE
*/ */
@@ -141,6 +103,8 @@ SampleRows(IvfflatBuildState * buildstate)
int targsamples = buildstate->samples->maxlen; int targsamples = buildstate->samples->maxlen;
BlockNumber totalblocks = RelationGetNumberOfBlocks(buildstate->heap); BlockNumber totalblocks = RelationGetNumberOfBlocks(buildstate->heap);
UpdateProgress(PROGRESS_CREATEIDX_SUBPHASE, PROGRESS_IVFFLAT_PHASE_SAMPLE);
buildstate->rowstoskip = -1; buildstate->rowstoskip = -1;
BlockSampler_Init(&buildstate->bs, totalblocks, targsamples, RandomInt()); BlockSampler_Init(&buildstate->bs, totalblocks, targsamples, RandomInt());
@@ -153,27 +117,38 @@ SampleRows(IvfflatBuildState * buildstate)
#if PG_VERSION_NUM >= 120000 #if PG_VERSION_NUM >= 120000
table_index_build_range_scan(buildstate->heap, buildstate->index, buildstate->indexInfo, table_index_build_range_scan(buildstate->heap, buildstate->index, buildstate->indexInfo,
false, true, false, targblock, 1, SampleCallback, (void *) buildstate, NULL); false, true, false, targblock, 1, SampleCallback, (void *) buildstate, NULL);
#else #elif PG_VERSION_NUM >= 110000
IndexBuildHeapRangeScan(buildstate->heap, buildstate->index, buildstate->indexInfo, IndexBuildHeapRangeScan(buildstate->heap, buildstate->index, buildstate->indexInfo,
false, true, targblock, 1, SampleCallback, (void *) buildstate, NULL); false, true, targblock, 1, SampleCallback, (void *) buildstate, NULL);
#else
IndexBuildHeapRangeScan(buildstate->heap, buildstate->index, buildstate->indexInfo,
false, true, targblock, 1, SampleCallback, (void *) buildstate);
#endif #endif
} }
} }
/* /*
* Add tuple to sort * Callback for table_index_build_scan
*/ */
static void static void
AddTupleToSort(Relation index, ItemPointer tid, Datum *values, IvfflatBuildState * buildstate) BuildCallback(Relation index, CALLBACK_ITEM_POINTER, Datum *values,
bool *isnull, bool tupleIsAlive, void *state)
{ {
IvfflatBuildState *buildstate = (IvfflatBuildState *) state;
double distance; double distance;
double minDistance = DBL_MAX; double minDistance = DBL_MAX;
int closestCenter = 0; int closestCenter = -1;
VectorArray centers = buildstate->centers; VectorArray centers = buildstate->centers;
TupleTableSlot *slot = buildstate->slot; TupleTableSlot *slot = buildstate->slot;
Datum value = values[0];
int i;
/* Detoast once for all calls */ #if PG_VERSION_NUM < 130000
Datum value = PointerGetDatum(PG_DETOAST_DATUM(values[0])); ItemPointer tid = &hup->t_self;
#endif
if (isnull[0])
return;
/* Normalize if needed */ /* Normalize if needed */
if (buildstate->normprocinfo != NULL) if (buildstate->normprocinfo != NULL)
@@ -183,7 +158,7 @@ AddTupleToSort(Relation index, ItemPointer tid, Datum *values, IvfflatBuildState
} }
/* Find the list that minimizes the distance */ /* Find the list that minimizes the distance */
for (int i = 0; i < centers->length; i++) for (i = 0; i < centers->length; i++)
{ {
distance = DatumGetFloat8(FunctionCall2Coll(buildstate->procinfo, buildstate->collation, value, PointerGetDatum(VectorArrayGet(centers, i)))); distance = DatumGetFloat8(FunctionCall2Coll(buildstate->procinfo, buildstate->collation, value, PointerGetDatum(VectorArrayGet(centers, i))));
@@ -221,35 +196,6 @@ AddTupleToSort(Relation index, ItemPointer tid, Datum *values, IvfflatBuildState
buildstate->indtuples++; buildstate->indtuples++;
} }
/*
* Callback for table_index_build_scan
*/
static void
BuildCallback(Relation index, CALLBACK_ITEM_POINTER, Datum *values,
bool *isnull, bool tupleIsAlive, void *state)
{
IvfflatBuildState *buildstate = (IvfflatBuildState *) state;
MemoryContext oldCtx;
#if PG_VERSION_NUM < 130000
ItemPointer tid = &hup->t_self;
#endif
/* Skip nulls */
if (isnull[0])
return;
/* Use memory context since detoast can allocate */
oldCtx = MemoryContextSwitchTo(buildstate->tmpCtx);
/* Add tuple to sort */
AddTupleToSort(index, tid, values, buildstate);
/* Reset memory context */
MemoryContextSwitchTo(oldCtx);
MemoryContextReset(buildstate->tmpCtx);
}
/* /*
* Get index tuple from sort state * Get index tuple from sort state
*/ */
@@ -259,7 +205,11 @@ GetNextTuple(Tuplesortstate *sortstate, TupleDesc tupdesc, TupleTableSlot *slot,
Datum value; Datum value;
bool isnull; bool isnull;
#if PG_VERSION_NUM >= 100000
if (tuplesort_gettupleslot(sortstate, true, false, slot, NULL)) if (tuplesort_gettupleslot(sortstate, true, false, slot, NULL))
#else
if (tuplesort_gettupleslot(sortstate, true, slot, NULL))
#endif
{ {
*list = DatumGetInt32(slot_getattr(slot, 1, &isnull)); *list = DatumGetInt32(slot_getattr(slot, 1, &isnull));
value = slot_getattr(slot, 3, &isnull); value = slot_getattr(slot, 3, &isnull);
@@ -278,8 +228,15 @@ GetNextTuple(Tuplesortstate *sortstate, TupleDesc tupdesc, TupleTableSlot *slot,
static void static void
InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum) InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum)
{ {
Buffer buf;
Page page;
GenericXLogState *state;
int list; int list;
IndexTuple itup = NULL; /* silence compiler warning */ IndexTuple itup = NULL; /* silence compiler warning */
BlockNumber startPage;
BlockNumber insertPage;
Size itemsz;
int i;
int64 inserted = 0; int64 inserted = 0;
#if PG_VERSION_NUM >= 120000 #if PG_VERSION_NUM >= 120000
@@ -295,14 +252,8 @@ InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum)
GetNextTuple(buildstate->sortstate, tupdesc, slot, &itup, &list); GetNextTuple(buildstate->sortstate, tupdesc, slot, &itup, &list);
for (int i = 0; i < buildstate->centers->length; i++) for (i = 0; i < buildstate->centers->length; i++)
{ {
Buffer buf;
Page page;
GenericXLogState *state;
BlockNumber startPage;
BlockNumber insertPage;
/* Can take a while, so ensure we can interrupt */ /* Can take a while, so ensure we can interrupt */
/* Needs to be called when no buffer locks are held */ /* Needs to be called when no buffer locks are held */
CHECK_FOR_INTERRUPTS(); CHECK_FOR_INTERRUPTS();
@@ -316,8 +267,7 @@ InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum)
while (list == i) while (list == i)
{ {
/* Check for free space */ /* Check for free space */
Size itemsz = MAXALIGN(IndexTupleSize(itup)); itemsz = MAXALIGN(IndexTupleSize(itup));
if (PageGetFreeSpace(page) < itemsz) if (PageGetFreeSpace(page) < itemsz)
IvfflatAppendPage(index, &buf, &page, &state, forkNum); IvfflatAppendPage(index, &buf, &page, &state, forkNum);
@@ -337,7 +287,7 @@ InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum)
IvfflatCommitBuffer(buf, state); IvfflatCommitBuffer(buf, state);
/* Set the start and insert pages */ /* Set the start and insert pages */
IvfflatUpdateList(index, buildstate->listInfo[i], insertPage, InvalidBlockNumber, startPage, forkNum); IvfflatUpdateList(index, state, buildstate->listInfo[i], insertPage, InvalidBlockNumber, startPage, forkNum);
} }
} }
@@ -358,9 +308,6 @@ InitBuildState(IvfflatBuildState * buildstate, Relation heap, Relation index, In
if (buildstate->dimensions < 0) if (buildstate->dimensions < 0)
elog(ERROR, "column does not have dimensions"); elog(ERROR, "column does not have dimensions");
if (buildstate->dimensions > IVFFLAT_MAX_DIM)
elog(ERROR, "column cannot have more than %d dimensions for ivfflat index", IVFFLAT_MAX_DIM);
buildstate->reltuples = 0; buildstate->reltuples = 0;
buildstate->indtuples = 0; buildstate->indtuples = 0;
@@ -371,7 +318,9 @@ InitBuildState(IvfflatBuildState * buildstate, Relation heap, Relation index, In
buildstate->collation = index->rd_indcollation[0]; buildstate->collation = index->rd_indcollation[0];
/* Require more than one dimension for spherical k-means */ /* Require more than one dimension for spherical k-means */
if (buildstate->kmeansnormprocinfo != NULL && buildstate->dimensions == 1) /* Lists check for backwards compatibility */
/* TODO Remove lists check in 0.3.0 */
if (buildstate->kmeansnormprocinfo != NULL && buildstate->dimensions == 1 && buildstate->lists > 1)
elog(ERROR, "dimensions must be greater than one for this opclass"); elog(ERROR, "dimensions must be greater than one for this opclass");
/* Create tuple description for sorting */ /* Create tuple description for sorting */
@@ -382,7 +331,11 @@ InitBuildState(IvfflatBuildState * buildstate, Relation heap, Relation index, In
#endif #endif
TupleDescInitEntry(buildstate->tupdesc, (AttrNumber) 1, "list", INT4OID, -1, 0); TupleDescInitEntry(buildstate->tupdesc, (AttrNumber) 1, "list", INT4OID, -1, 0);
TupleDescInitEntry(buildstate->tupdesc, (AttrNumber) 2, "tid", TIDOID, -1, 0); TupleDescInitEntry(buildstate->tupdesc, (AttrNumber) 2, "tid", TIDOID, -1, 0);
#if PG_VERSION_NUM >= 110000
TupleDescInitEntry(buildstate->tupdesc, (AttrNumber) 3, "vector", RelationGetDescr(index)->attrs[0].atttypid, -1, 0); TupleDescInitEntry(buildstate->tupdesc, (AttrNumber) 3, "vector", RelationGetDescr(index)->attrs[0].atttypid, -1, 0);
#else
TupleDescInitEntry(buildstate->tupdesc, (AttrNumber) 3, "vector", RelationGetDescr(index)->attrs[0]->atttypid, -1, 0);
#endif
#if PG_VERSION_NUM >= 120000 #if PG_VERSION_NUM >= 120000
buildstate->slot = MakeSingleTupleTableSlot(buildstate->tupdesc, &TTSOpsVirtual); buildstate->slot = MakeSingleTupleTableSlot(buildstate->tupdesc, &TTSOpsVirtual);
@@ -396,17 +349,11 @@ InitBuildState(IvfflatBuildState * buildstate, Relation heap, Relation index, In
/* Reuse for each tuple */ /* Reuse for each tuple */
buildstate->normvec = InitVector(buildstate->dimensions); buildstate->normvec = InitVector(buildstate->dimensions);
buildstate->tmpCtx = AllocSetContextCreate(CurrentMemoryContext,
"Ivfflat build temporary context",
ALLOCSET_DEFAULT_SIZES);
#ifdef IVFFLAT_KMEANS_DEBUG #ifdef IVFFLAT_KMEANS_DEBUG
buildstate->inertia = 0; buildstate->inertia = 0;
buildstate->listSums = palloc0(sizeof(double) * buildstate->lists); buildstate->listSums = palloc0(sizeof(double) * buildstate->lists);
buildstate->listCounts = palloc0(sizeof(int) * buildstate->lists); buildstate->listCounts = palloc0(sizeof(int) * buildstate->lists);
#endif #endif
buildstate->ivfleader = NULL;
} }
/* /*
@@ -423,8 +370,6 @@ FreeBuildState(IvfflatBuildState * buildstate)
pfree(buildstate->listSums); pfree(buildstate->listSums);
pfree(buildstate->listCounts); pfree(buildstate->listCounts);
#endif #endif
MemoryContextDelete(buildstate->tmpCtx);
} }
/* /*
@@ -435,8 +380,6 @@ ComputeCenters(IvfflatBuildState * buildstate)
{ {
int numSamples; int numSamples;
UpdateProgress(PROGRESS_CREATEIDX_SUBPHASE, PROGRESS_IVFFLAT_PHASE_KMEANS);
/* Target 50 samples per list, with at least 10000 samples */ /* Target 50 samples per list, with at least 10000 samples */
/* The number of samples has a large effect on index build time */ /* The number of samples has a large effect on index build time */
numSamples = buildstate->lists * 50; numSamples = buildstate->lists * 50;
@@ -451,19 +394,10 @@ ComputeCenters(IvfflatBuildState * buildstate)
/* TODO Ensure within maintenance_work_mem */ /* TODO Ensure within maintenance_work_mem */
buildstate->samples = VectorArrayInit(numSamples, buildstate->dimensions); buildstate->samples = VectorArrayInit(numSamples, buildstate->dimensions);
if (buildstate->heap != NULL) if (buildstate->heap != NULL)
{
SampleRows(buildstate); SampleRows(buildstate);
if (buildstate->samples->length < buildstate->lists)
{
ereport(NOTICE,
(errmsg("ivfflat index created with little data"),
errdetail("This will cause low recall."),
errhint("Drop the index until the table has more data.")));
}
}
/* Calculate centers */ /* Calculate centers */
UpdateProgress(PROGRESS_CREATEIDX_SUBPHASE, PROGRESS_IVFFLAT_PHASE_KMEANS);
IvfflatBench("k-means", IvfflatKmeans(buildstate->index, buildstate->samples, buildstate->centers)); IvfflatBench("k-means", IvfflatKmeans(buildstate->index, buildstate->samples, buildstate->centers));
/* Free samples before we allocate more memory */ /* Free samples before we allocate more memory */
@@ -503,6 +437,7 @@ static void
CreateListPages(Relation index, VectorArray centers, int dimensions, CreateListPages(Relation index, VectorArray centers, int dimensions,
int lists, ForkNumber forkNum, ListInfo * *listInfo) int lists, ForkNumber forkNum, ListInfo * *listInfo)
{ {
int i;
Buffer buf; Buffer buf;
Page page; Page page;
GenericXLogState *state; GenericXLogState *state;
@@ -516,7 +451,7 @@ CreateListPages(Relation index, VectorArray centers, int dimensions,
buf = IvfflatNewBuffer(index, forkNum); buf = IvfflatNewBuffer(index, forkNum);
IvfflatInitRegisterPage(index, &buf, &page, &state); IvfflatInitRegisterPage(index, &buf, &page, &state);
for (int i = 0; i < lists; i++) for (i = 0; i < lists; i++)
{ {
/* Load list */ /* Load list */
list->startPage = InvalidBlockNumber; list->startPage = InvalidBlockNumber;
@@ -552,7 +487,7 @@ PrintKmeansMetrics(IvfflatBuildState * buildstate)
elog(INFO, "inertia: %.3e", buildstate->inertia); elog(INFO, "inertia: %.3e", buildstate->inertia);
/* Calculate Davies-Bouldin index */ /* Calculate Davies-Bouldin index */
if (buildstate->lists > 1 && !buildstate->ivfleader) if (buildstate->lists > 1)
{ {
double db = 0.0; double db = 0.0;
@@ -587,478 +522,50 @@ PrintKmeansMetrics(IvfflatBuildState * buildstate)
} }
#endif #endif
/*
* Within leader, wait for end of heap scan
*/
static double
ParallelHeapScan(IvfflatBuildState * buildstate)
{
IvfflatShared *ivfshared = buildstate->ivfleader->ivfshared;
int nparticipanttuplesorts;
double reltuples;
nparticipanttuplesorts = buildstate->ivfleader->nparticipanttuplesorts;
for (;;)
{
SpinLockAcquire(&ivfshared->mutex);
if (ivfshared->nparticipantsdone == nparticipanttuplesorts)
{
buildstate->indtuples = ivfshared->indtuples;
reltuples = ivfshared->reltuples;
#ifdef IVFFLAT_KMEANS_DEBUG
buildstate->inertia = ivfshared->inertia;
#endif
SpinLockRelease(&ivfshared->mutex);
break;
}
SpinLockRelease(&ivfshared->mutex);
ConditionVariableSleep(&ivfshared->workersdonecv,
WAIT_EVENT_PARALLEL_CREATE_INDEX_SCAN);
}
ConditionVariableCancelSleep();
return reltuples;
}
/*
* Perform a worker's portion of a parallel sort
*/
static void
IvfflatParallelScanAndSort(IvfflatSpool * ivfspool, IvfflatShared * ivfshared, Sharedsort *sharedsort, Vector * ivfcenters, int sortmem, bool progress)
{
SortCoordinate coordinate;
IvfflatBuildState buildstate;
#if PG_VERSION_NUM >= 120000
TableScanDesc scan;
#else
HeapScanDesc scan;
#endif
double reltuples;
IndexInfo *indexInfo;
/* Sort options, which must match AssignTuples */
AttrNumber attNums[] = {1};
Oid sortOperators[] = {Int4LessOperator};
Oid sortCollations[] = {InvalidOid};
bool nullsFirstFlags[] = {false};
/* Initialize local tuplesort coordination state */
coordinate = palloc0(sizeof(SortCoordinateData));
coordinate->isWorker = true;
coordinate->nParticipants = -1;
coordinate->sharedsort = sharedsort;
/* Join parallel scan */
indexInfo = BuildIndexInfo(ivfspool->index);
indexInfo->ii_Concurrent = ivfshared->isconcurrent;
InitBuildState(&buildstate, ivfspool->heap, ivfspool->index, indexInfo);
memcpy(buildstate.centers->items, ivfcenters, VECTOR_SIZE(buildstate.centers->dim) * buildstate.centers->maxlen);
buildstate.centers->length = buildstate.centers->maxlen;
ivfspool->sortstate = tuplesort_begin_heap(buildstate.tupdesc, 1, attNums, sortOperators, sortCollations, nullsFirstFlags, sortmem, coordinate, false);
buildstate.sortstate = ivfspool->sortstate;
#if PG_VERSION_NUM >= 120000
scan = table_beginscan_parallel(ivfspool->heap,
ParallelTableScanFromIvfflatShared(ivfshared));
reltuples = table_index_build_scan(ivfspool->heap, ivfspool->index, indexInfo,
true, progress, BuildCallback,
(void *) &buildstate, scan);
#else
scan = heap_beginscan_parallel(ivfspool->heap, &ivfshared->heapdesc);
reltuples = IndexBuildHeapScan(ivfspool->heap, ivfspool->index, indexInfo,
true, BuildCallback,
(void *) &buildstate, scan);
#endif
/* Execute this worker's part of the sort */
tuplesort_performsort(ivfspool->sortstate);
/* Record statistics */
SpinLockAcquire(&ivfshared->mutex);
ivfshared->nparticipantsdone++;
ivfshared->reltuples += reltuples;
ivfshared->indtuples += buildstate.indtuples;
#ifdef IVFFLAT_KMEANS_DEBUG
ivfshared->inertia += buildstate.inertia;
#endif
SpinLockRelease(&ivfshared->mutex);
/* Log statistics */
if (progress)
ereport(DEBUG1, (errmsg("leader processed " INT64_FORMAT " tuples", (int64) reltuples)));
else
ereport(DEBUG1, (errmsg("worker processed " INT64_FORMAT " tuples", (int64) reltuples)));
/* Notify leader */
ConditionVariableSignal(&ivfshared->workersdonecv);
/* We can end tuplesorts immediately */
tuplesort_end(ivfspool->sortstate);
FreeBuildState(&buildstate);
}
/*
* Perform work within a launched parallel process
*/
void
IvfflatParallelBuildMain(dsm_segment *seg, shm_toc *toc)
{
char *sharedquery;
IvfflatSpool *ivfspool;
IvfflatShared *ivfshared;
Sharedsort *sharedsort;
Vector *ivfcenters;
Relation heapRel;
Relation indexRel;
LOCKMODE heapLockmode;
LOCKMODE indexLockmode;
int sortmem;
/* Set debug_query_string for individual workers first */
sharedquery = shm_toc_lookup(toc, PARALLEL_KEY_QUERY_TEXT, true);
debug_query_string = sharedquery;
/* Report the query string from leader */
pgstat_report_activity(STATE_RUNNING, debug_query_string);
/* Look up shared state */
ivfshared = shm_toc_lookup(toc, PARALLEL_KEY_IVFFLAT_SHARED, false);
/* Open relations using lock modes known to be obtained by index.c */
if (!ivfshared->isconcurrent)
{
heapLockmode = ShareLock;
indexLockmode = AccessExclusiveLock;
}
else
{
heapLockmode = ShareUpdateExclusiveLock;
indexLockmode = RowExclusiveLock;
}
/* Open relations within worker */
#if PG_VERSION_NUM >= 120000
heapRel = table_open(ivfshared->heaprelid, heapLockmode);
#else
heapRel = heap_open(ivfshared->heaprelid, heapLockmode);
#endif
indexRel = index_open(ivfshared->indexrelid, indexLockmode);
/* Initialize worker's own spool */
ivfspool = (IvfflatSpool *) palloc0(sizeof(IvfflatSpool));
ivfspool->heap = heapRel;
ivfspool->index = indexRel;
/* Look up shared state private to tuplesort.c */
sharedsort = shm_toc_lookup(toc, PARALLEL_KEY_TUPLESORT, false);
tuplesort_attach_shared(sharedsort, seg);
ivfcenters = shm_toc_lookup(toc, PARALLEL_KEY_IVFFLAT_CENTERS, false);
/* Perform sorting */
sortmem = maintenance_work_mem / ivfshared->scantuplesortstates;
IvfflatParallelScanAndSort(ivfspool, ivfshared, sharedsort, ivfcenters, sortmem, false);
/* Close relations within worker */
index_close(indexRel, indexLockmode);
#if PG_VERSION_NUM >= 120000
table_close(heapRel, heapLockmode);
#else
heap_close(heapRel, heapLockmode);
#endif
}
/*
* End parallel build
*/
static void
IvfflatEndParallel(IvfflatLeader * ivfleader)
{
/* Shutdown worker processes */
WaitForParallelWorkersToFinish(ivfleader->pcxt);
/* Free last reference to MVCC snapshot, if one was used */
if (IsMVCCSnapshot(ivfleader->snapshot))
UnregisterSnapshot(ivfleader->snapshot);
DestroyParallelContext(ivfleader->pcxt);
ExitParallelMode();
}
/*
* Return size of shared memory required for parallel index build
*/
static Size
ParallelEstimateShared(Relation heap, Snapshot snapshot)
{
#if PG_VERSION_NUM >= 120000
return add_size(BUFFERALIGN(sizeof(IvfflatShared)), table_parallelscan_estimate(heap, snapshot));
#else
if (!IsMVCCSnapshot(snapshot))
{
Assert(snapshot == SnapshotAny);
return sizeof(IvfflatShared);
}
return add_size(offsetof(IvfflatShared, heapdesc) +
offsetof(ParallelHeapScanDescData, phs_snapshot_data),
EstimateSnapshotSpace(snapshot));
#endif
}
/*
* Within leader, participate as a parallel worker
*/
static void
IvfflatLeaderParticipateAsWorker(IvfflatBuildState * buildstate)
{
IvfflatLeader *ivfleader = buildstate->ivfleader;
IvfflatSpool *leaderworker;
int sortmem;
/* Allocate memory and initialize private spool */
leaderworker = (IvfflatSpool *) palloc0(sizeof(IvfflatSpool));
leaderworker->heap = buildstate->heap;
leaderworker->index = buildstate->index;
/* Perform work common to all participants */
sortmem = maintenance_work_mem / ivfleader->nparticipanttuplesorts;
IvfflatParallelScanAndSort(leaderworker, ivfleader->ivfshared,
ivfleader->sharedsort, ivfleader->ivfcenters,
sortmem, true);
}
/*
* Begin parallel build
*/
static void
IvfflatBeginParallel(IvfflatBuildState * buildstate, bool isconcurrent, int request)
{
ParallelContext *pcxt;
int scantuplesortstates;
Snapshot snapshot;
Size estivfshared;
Size estsort;
Size estcenters;
IvfflatShared *ivfshared;
Sharedsort *sharedsort;
Vector *ivfcenters;
IvfflatLeader *ivfleader = (IvfflatLeader *) palloc0(sizeof(IvfflatLeader));
bool leaderparticipates = true;
int querylen;
#ifdef DISABLE_LEADER_PARTICIPATION
leaderparticipates = false;
#endif
/* Enter parallel mode and create context */
EnterParallelMode();
Assert(request > 0);
#if PG_VERSION_NUM >= 120000
pcxt = CreateParallelContext("vector", "IvfflatParallelBuildMain", request);
#else
pcxt = CreateParallelContext("vector", "IvfflatParallelBuildMain", request, true);
#endif
scantuplesortstates = leaderparticipates ? request + 1 : request;
/* Get snapshot for table scan */
if (!isconcurrent)
snapshot = SnapshotAny;
else
snapshot = RegisterSnapshot(GetTransactionSnapshot());
/* Estimate size of workspaces */
estivfshared = ParallelEstimateShared(buildstate->heap, snapshot);
shm_toc_estimate_chunk(&pcxt->estimator, estivfshared);
estsort = tuplesort_estimate_shared(scantuplesortstates);
shm_toc_estimate_chunk(&pcxt->estimator, estsort);
estcenters = VECTOR_SIZE(buildstate->dimensions) * buildstate->lists;
shm_toc_estimate_chunk(&pcxt->estimator, estcenters);
shm_toc_estimate_keys(&pcxt->estimator, 3);
/* Finally, estimate PARALLEL_KEY_QUERY_TEXT space */
if (debug_query_string)
{
querylen = strlen(debug_query_string);
shm_toc_estimate_chunk(&pcxt->estimator, querylen + 1);
shm_toc_estimate_keys(&pcxt->estimator, 1);
}
else
querylen = 0; /* keep compiler quiet */
/* Everyone's had a chance to ask for space, so now create the DSM */
InitializeParallelDSM(pcxt);
/* If no DSM segment was available, back out (do serial build) */
if (pcxt->seg == NULL)
{
if (IsMVCCSnapshot(snapshot))
UnregisterSnapshot(snapshot);
DestroyParallelContext(pcxt);
ExitParallelMode();
return;
}
/* Store shared build state, for which we reserved space */
ivfshared = (IvfflatShared *) shm_toc_allocate(pcxt->toc, estivfshared);
/* Initialize immutable state */
ivfshared->heaprelid = RelationGetRelid(buildstate->heap);
ivfshared->indexrelid = RelationGetRelid(buildstate->index);
ivfshared->isconcurrent = isconcurrent;
ivfshared->scantuplesortstates = scantuplesortstates;
ConditionVariableInit(&ivfshared->workersdonecv);
SpinLockInit(&ivfshared->mutex);
/* Initialize mutable state */
ivfshared->nparticipantsdone = 0;
ivfshared->reltuples = 0;
ivfshared->indtuples = 0;
#ifdef IVFFLAT_KMEANS_DEBUG
ivfshared->inertia = 0;
#endif
#if PG_VERSION_NUM >= 120000
table_parallelscan_initialize(buildstate->heap,
ParallelTableScanFromIvfflatShared(ivfshared),
snapshot);
#else
heap_parallelscan_initialize(&ivfshared->heapdesc, buildstate->heap, snapshot);
#endif
/* Store shared tuplesort-private state, for which we reserved space */
sharedsort = (Sharedsort *) shm_toc_allocate(pcxt->toc, estsort);
tuplesort_initialize_shared(sharedsort, scantuplesortstates,
pcxt->seg);
ivfcenters = (Vector *) shm_toc_allocate(pcxt->toc, estcenters);
memcpy(ivfcenters, buildstate->centers->items, estcenters);
shm_toc_insert(pcxt->toc, PARALLEL_KEY_IVFFLAT_SHARED, ivfshared);
shm_toc_insert(pcxt->toc, PARALLEL_KEY_TUPLESORT, sharedsort);
shm_toc_insert(pcxt->toc, PARALLEL_KEY_IVFFLAT_CENTERS, ivfcenters);
/* Store query string for workers */
if (debug_query_string)
{
char *sharedquery;
sharedquery = (char *) shm_toc_allocate(pcxt->toc, querylen + 1);
memcpy(sharedquery, debug_query_string, querylen + 1);
shm_toc_insert(pcxt->toc, PARALLEL_KEY_QUERY_TEXT, sharedquery);
}
/* Launch workers, saving status for leader/caller */
LaunchParallelWorkers(pcxt);
ivfleader->pcxt = pcxt;
ivfleader->nparticipanttuplesorts = pcxt->nworkers_launched;
if (leaderparticipates)
ivfleader->nparticipanttuplesorts++;
ivfleader->ivfshared = ivfshared;
ivfleader->sharedsort = sharedsort;
ivfleader->snapshot = snapshot;
ivfleader->ivfcenters = ivfcenters;
/* If no workers were successfully launched, back out (do serial build) */
if (pcxt->nworkers_launched == 0)
{
IvfflatEndParallel(ivfleader);
return;
}
/* Log participants */
ereport(DEBUG1, (errmsg("using %d parallel workers", pcxt->nworkers_launched)));
/* Save leader state now that it's clear build will be parallel */
buildstate->ivfleader = ivfleader;
/* Join heap scan ourselves */
if (leaderparticipates)
IvfflatLeaderParticipateAsWorker(buildstate);
/* Wait for all launched workers */
WaitForParallelWorkersToAttach(pcxt);
}
/*
* Scan table for tuples to index
*/
static void
AssignTuples(IvfflatBuildState * buildstate)
{
int parallel_workers = 0;
SortCoordinate coordinate = NULL;
/* Sort options, which must match IvfflatParallelScanAndSort */
AttrNumber attNums[] = {1};
Oid sortOperators[] = {Int4LessOperator};
Oid sortCollations[] = {InvalidOid};
bool nullsFirstFlags[] = {false};
UpdateProgress(PROGRESS_CREATEIDX_SUBPHASE, PROGRESS_IVFFLAT_PHASE_ASSIGN);
/* Calculate parallel workers */
if (buildstate->heap != NULL)
parallel_workers = plan_create_index_workers(RelationGetRelid(buildstate->heap), RelationGetRelid(buildstate->index));
/* Attempt to launch parallel worker scan when required */
if (parallel_workers > 0)
IvfflatBeginParallel(buildstate, buildstate->indexInfo->ii_Concurrent, parallel_workers);
/* Set up coordination state if at least one worker launched */
if (buildstate->ivfleader)
{
coordinate = (SortCoordinate) palloc0(sizeof(SortCoordinateData));
coordinate->isWorker = false;
coordinate->nParticipants = buildstate->ivfleader->nparticipanttuplesorts;
coordinate->sharedsort = buildstate->ivfleader->sharedsort;
}
/* Begin serial/leader tuplesort */
buildstate->sortstate = tuplesort_begin_heap(buildstate->tupdesc, 1, attNums, sortOperators, sortCollations, nullsFirstFlags, maintenance_work_mem, coordinate, false);
/* Add tuples to sort */
if (buildstate->heap != NULL)
{
if (buildstate->ivfleader)
buildstate->reltuples = ParallelHeapScan(buildstate);
else
{
#if PG_VERSION_NUM >= 120000
buildstate->reltuples = table_index_build_scan(buildstate->heap, buildstate->index, buildstate->indexInfo,
true, true, BuildCallback, (void *) buildstate, NULL);
#else
buildstate->reltuples = IndexBuildHeapScan(buildstate->heap, buildstate->index, buildstate->indexInfo,
true, BuildCallback, (void *) buildstate, NULL);
#endif
}
#ifdef IVFFLAT_KMEANS_DEBUG
PrintKmeansMetrics(buildstate);
#endif
}
}
/* /*
* Create entry pages * Create entry pages
*/ */
static void static void
CreateEntryPages(IvfflatBuildState * buildstate, ForkNumber forkNum) CreateEntryPages(IvfflatBuildState * buildstate, ForkNumber forkNum)
{ {
/* Assign */ AttrNumber attNums[] = {1};
IvfflatBench("assign tuples", AssignTuples(buildstate)); Oid sortOperators[] = {Float8LessOperator};
Oid sortCollations[] = {InvalidOid};
bool nullsFirstFlags[] = {false};
UpdateProgress(PROGRESS_CREATEIDX_SUBPHASE, PROGRESS_IVFFLAT_PHASE_SORT);
#if PG_VERSION_NUM >= 110000
buildstate->sortstate = tuplesort_begin_heap(buildstate->tupdesc, 1, attNums, sortOperators, sortCollations, nullsFirstFlags, maintenance_work_mem, NULL, false);
#else
buildstate->sortstate = tuplesort_begin_heap(buildstate->tupdesc, 1, attNums, sortOperators, sortCollations, nullsFirstFlags, maintenance_work_mem, false);
#endif
/* Add tuples to sort */
if (buildstate->heap != NULL)
{
#if PG_VERSION_NUM >= 120000
buildstate->reltuples = table_index_build_scan(buildstate->heap, buildstate->index, buildstate->indexInfo,
true, true, BuildCallback, (void *) buildstate, NULL);
#elif PG_VERSION_NUM >= 110000
buildstate->reltuples = IndexBuildHeapScan(buildstate->heap, buildstate->index, buildstate->indexInfo,
true, BuildCallback, (void *) buildstate, NULL);
#else
buildstate->reltuples = IndexBuildHeapScan(buildstate->heap, buildstate->index, buildstate->indexInfo,
true, BuildCallback, (void *) buildstate);
#endif
}
/* Sort */ /* Sort */
IvfflatBench("sort tuples", tuplesort_performsort(buildstate->sortstate)); tuplesort_performsort(buildstate->sortstate);
/* Load */ #ifdef IVFFLAT_KMEANS_DEBUG
IvfflatBench("load tuples", InsertTuples(buildstate->index, buildstate, forkNum)); PrintKmeansMetrics(buildstate);
#endif
/* End sort */ /* Insert */
InsertTuples(buildstate->index, buildstate, forkNum);
tuplesort_end(buildstate->sortstate); tuplesort_end(buildstate->sortstate);
/* End parallel build */
if (buildstate->ivfleader)
IvfflatEndParallel(buildstate->ivfleader);
} }
/* /*
@@ -1075,7 +582,7 @@ BuildIndex(Relation heap, Relation index, IndexInfo *indexInfo,
/* Create pages */ /* Create pages */
CreateMetaPage(index, buildstate->dimensions, buildstate->lists, forkNum); CreateMetaPage(index, buildstate->dimensions, buildstate->lists, forkNum);
CreateListPages(index, buildstate->centers, buildstate->dimensions, buildstate->lists, forkNum, &buildstate->listInfo); CreateListPages(index, buildstate->centers, buildstate->dimensions, buildstate->lists, forkNum, &buildstate->listInfo);
CreateEntryPages(buildstate, forkNum); IvfflatBench("CreateEntryPages", CreateEntryPages(buildstate, forkNum));
FreeBuildState(buildstate); FreeBuildState(buildstate);
} }

View File

@@ -7,7 +7,6 @@
#include "ivfflat.h" #include "ivfflat.h"
#include "utils/guc.h" #include "utils/guc.h"
#include "utils/selfuncs.h" #include "utils/selfuncs.h"
#include "utils/spccache.h"
#if PG_VERSION_NUM >= 120000 #if PG_VERSION_NUM >= 120000
#include "commands/progress.h" #include "commands/progress.h"
@@ -20,11 +19,11 @@ static relopt_kind ivfflat_relopt_kind;
* Initialize index options and variables * Initialize index options and variables
*/ */
void void
IvfflatInit(void) _PG_init(void)
{ {
ivfflat_relopt_kind = add_reloption_kind(); ivfflat_relopt_kind = add_reloption_kind();
add_int_reloption(ivfflat_relopt_kind, "lists", "Number of inverted lists", add_int_reloption(ivfflat_relopt_kind, "lists", "Number of inverted lists",
IVFFLAT_DEFAULT_LISTS, IVFFLAT_MIN_LISTS, IVFFLAT_MAX_LISTS IVFFLAT_DEFAULT_LISTS, 1, IVFFLAT_MAX_LISTS
#if PG_VERSION_NUM >= 130000 #if PG_VERSION_NUM >= 130000
,AccessExclusiveLock ,AccessExclusiveLock
#endif #endif
@@ -32,7 +31,7 @@ IvfflatInit(void)
DefineCustomIntVariable("ivfflat.probes", "Sets the number of probes", DefineCustomIntVariable("ivfflat.probes", "Sets the number of probes",
"Valid range is 1..lists.", &ivfflat_probes, "Valid range is 1..lists.", &ivfflat_probes,
IVFFLAT_DEFAULT_PROBES, IVFFLAT_MIN_LISTS, IVFFLAT_MAX_LISTS, PGC_USERSET, 0, NULL, NULL, NULL); 1, 1, IVFFLAT_MAX_LISTS, PGC_USERSET, 0, NULL, NULL, NULL);
} }
/* /*
@@ -46,10 +45,12 @@ ivfflatbuildphasename(int64 phasenum)
{ {
case PROGRESS_CREATEIDX_SUBPHASE_INITIALIZE: case PROGRESS_CREATEIDX_SUBPHASE_INITIALIZE:
return "initializing"; return "initializing";
case PROGRESS_IVFFLAT_PHASE_SAMPLE:
return "sampling table";
case PROGRESS_IVFFLAT_PHASE_KMEANS: case PROGRESS_IVFFLAT_PHASE_KMEANS:
return "performing k-means"; return "performing k-means";
case PROGRESS_IVFFLAT_PHASE_ASSIGN: case PROGRESS_IVFFLAT_PHASE_SORT:
return "assigning tuples"; return "sorting tuples";
case PROGRESS_IVFFLAT_PHASE_LOAD: case PROGRESS_IVFFLAT_PHASE_LOAD:
return "loading tuples"; return "loading tuples";
default: default:
@@ -64,14 +65,16 @@ ivfflatbuildphasename(int64 phasenum)
static void static void
ivfflatcostestimate(PlannerInfo *root, IndexPath *path, double loop_count, ivfflatcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
Cost *indexStartupCost, Cost *indexTotalCost, Cost *indexStartupCost, Cost *indexTotalCost,
Selectivity *indexSelectivity, double *indexCorrelation, Selectivity *indexSelectivity, double *indexCorrelation
double *indexPages) #if PG_VERSION_NUM >= 100000
,double *indexPages
#endif
)
{ {
GenericCosts costs; GenericCosts costs;
int lists; int lists;
double ratio; double ratio;
double spc_seq_page_cost; Relation indexRel;
Relation index;
#if PG_VERSION_NUM < 120000 #if PG_VERSION_NUM < 120000
List *qinfos; List *qinfos;
#endif #endif
@@ -83,28 +86,14 @@ ivfflatcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
*indexTotalCost = DBL_MAX; *indexTotalCost = DBL_MAX;
*indexSelectivity = 0; *indexSelectivity = 0;
*indexCorrelation = 0; *indexCorrelation = 0;
#if PG_VERSION_NUM >= 100000
*indexPages = 0; *indexPages = 0;
#endif
return; return;
} }
MemSet(&costs, 0, sizeof(costs)); MemSet(&costs, 0, sizeof(costs));
index = index_open(path->indexinfo->indexoid, NoLock);
IvfflatGetMetaPageInfo(index, &lists, NULL);
index_close(index, NoLock);
/* Get the ratio of lists that we need to visit */
ratio = ((double) ivfflat_probes) / lists;
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;
#if PG_VERSION_NUM >= 120000 #if PG_VERSION_NUM >= 120000
genericcostestimate(root, path, loop_count, &costs); genericcostestimate(root, path, loop_count, &costs);
#else #else
@@ -112,36 +101,24 @@ ivfflatcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
genericcostestimate(root, path, loop_count, qinfos, &costs); genericcostestimate(root, path, loop_count, qinfos, &costs);
#endif #endif
get_tablespace_page_costs(path->indexinfo->reltablespace, NULL, &spc_seq_page_cost); indexRel = index_open(path->indexinfo->indexoid, NoLock);
lists = IvfflatGetLists(indexRel);
index_close(indexRel, NoLock);
/* Adjust cost if needed since TOAST not included in seq scan cost */ ratio = ((double) ivfflat_probes) / lists;
if (costs.numIndexPages > path->indexinfo->rel->pages && ratio < 0.5) if (ratio > 1)
{ ratio = 1;
/* Change all page cost from random to sequential */
costs.indexTotalCost -= costs.numIndexPages * (costs.spc_random_page_cost - spc_seq_page_cost);
/* Remove cost of extra pages */ costs.indexTotalCost *= ratio;
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);
}
/* /* Startup cost and total cost are same */
* 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.indexTotalCost;
*indexTotalCost = costs.indexTotalCost; *indexTotalCost = costs.indexTotalCost;
*indexSelectivity = costs.indexSelectivity; *indexSelectivity = costs.indexSelectivity;
*indexCorrelation = costs.indexCorrelation; *indexCorrelation = costs.indexCorrelation;
#if PG_VERSION_NUM >= 100000
*indexPages = costs.numIndexPages; *indexPages = costs.numIndexPages;
#endif
} }
/* /*
@@ -182,15 +159,6 @@ ivfflatvalidate(Oid opclassoid)
return true; return true;
} }
/*
* Checks if index-only scan is supported
*/
static bool
ivfflatcanreturn(Relation index, int attno)
{
return attno == 1 && IvfflatOptionalProcInfo(index, IVFFLAT_NORM_PROC) == NULL;
}
/* /*
* Define index handler * Define index handler
* *
@@ -218,8 +186,12 @@ ivfflathandler(PG_FUNCTION_ARGS)
amroutine->amstorage = false; amroutine->amstorage = false;
amroutine->amclusterable = false; amroutine->amclusterable = false;
amroutine->ampredlocks = false; amroutine->ampredlocks = false;
#if PG_VERSION_NUM >= 100000
amroutine->amcanparallel = false; amroutine->amcanparallel = false;
#endif
#if PG_VERSION_NUM >= 110000
amroutine->amcaninclude = false; amroutine->amcaninclude = false;
#endif
#if PG_VERSION_NUM >= 130000 #if PG_VERSION_NUM >= 130000
amroutine->amusemaintenanceworkmem = false; /* not used during VACUUM */ amroutine->amusemaintenanceworkmem = false; /* not used during VACUUM */
amroutine->amparallelvacuumoptions = VACUUM_OPTION_PARALLEL_BULKDEL; amroutine->amparallelvacuumoptions = VACUUM_OPTION_PARALLEL_BULKDEL;
@@ -232,7 +204,7 @@ ivfflathandler(PG_FUNCTION_ARGS)
amroutine->aminsert = ivfflatinsert; amroutine->aminsert = ivfflatinsert;
amroutine->ambulkdelete = ivfflatbulkdelete; amroutine->ambulkdelete = ivfflatbulkdelete;
amroutine->amvacuumcleanup = ivfflatvacuumcleanup; amroutine->amvacuumcleanup = ivfflatvacuumcleanup;
amroutine->amcanreturn = ivfflatcanreturn; amroutine->amcanreturn = NULL; /* tuple not included in heapsort */
amroutine->amcostestimate = ivfflatcostestimate; amroutine->amcostestimate = ivfflatcostestimate;
amroutine->amoptions = ivfflatoptions; amroutine->amoptions = ivfflatoptions;
amroutine->amproperty = NULL; /* TODO AMPROP_DISTANCE_ORDERABLE */ amroutine->amproperty = NULL; /* TODO AMPROP_DISTANCE_ORDERABLE */
@@ -252,9 +224,11 @@ ivfflathandler(PG_FUNCTION_ARGS)
amroutine->amrestrpos = NULL; amroutine->amrestrpos = NULL;
/* Interface functions to support parallel index scans */ /* Interface functions to support parallel index scans */
#if PG_VERSION_NUM >= 100000
amroutine->amestimateparallelscan = NULL; amroutine->amestimateparallelscan = NULL;
amroutine->aminitparallelscan = NULL; amroutine->aminitparallelscan = NULL;
amroutine->amparallelrescan = NULL; amroutine->amparallelrescan = NULL;
#endif
PG_RETURN_POINTER(amroutine); PG_RETURN_POINTER(amroutine);
} }

View File

@@ -3,11 +3,14 @@
#include "postgres.h" #include "postgres.h"
#if PG_VERSION_NUM < 100000
#error "Requires PostgreSQL 10+"
#endif
#include "access/generic_xlog.h" #include "access/generic_xlog.h"
#include "access/parallel.h"
#include "access/reloptions.h" #include "access/reloptions.h"
#include "nodes/execnodes.h" #include "nodes/execnodes.h"
#include "port.h" /* for random() */ #include "port.h" /* for strtof() and random() */
#include "utils/sampling.h" #include "utils/sampling.h"
#include "utils/tuplesort.h" #include "utils/tuplesort.h"
#include "vector.h" #include "vector.h"
@@ -16,16 +19,10 @@
#include "common/pg_prng.h" #include "common/pg_prng.h"
#endif #endif
#if PG_VERSION_NUM < 120000
#include "access/relscan.h"
#endif
#ifdef IVFFLAT_BENCH #ifdef IVFFLAT_BENCH
#include "portability/instr_time.h" #include "portability/instr_time.h"
#endif #endif
#define IVFFLAT_MAX_DIM 2000
/* Support functions */ /* Support functions */
#define IVFFLAT_DISTANCE_PROC 1 #define IVFFLAT_DISTANCE_PROC 1
#define IVFFLAT_NORM_PROC 2 #define IVFFLAT_NORM_PROC 2
@@ -40,17 +37,15 @@
#define IVFFLAT_METAPAGE_BLKNO 0 #define IVFFLAT_METAPAGE_BLKNO 0
#define IVFFLAT_HEAD_BLKNO 1 /* first list page */ #define IVFFLAT_HEAD_BLKNO 1 /* first list page */
/* IVFFlat parameters */
#define IVFFLAT_DEFAULT_LISTS 100 #define IVFFLAT_DEFAULT_LISTS 100
#define IVFFLAT_MIN_LISTS 1
#define IVFFLAT_MAX_LISTS 32768 #define IVFFLAT_MAX_LISTS 32768
#define IVFFLAT_DEFAULT_PROBES 1
/* Build phases */ /* Build phases */
/* PROGRESS_CREATEIDX_SUBPHASE_INITIALIZE is 1 */ /* PROGRESS_CREATEIDX_SUBPHASE_INITIALIZE is 1 */
#define PROGRESS_IVFFLAT_PHASE_KMEANS 2 #define PROGRESS_IVFFLAT_PHASE_SAMPLE 2
#define PROGRESS_IVFFLAT_PHASE_ASSIGN 3 #define PROGRESS_IVFFLAT_PHASE_KMEANS 3
#define PROGRESS_IVFFLAT_PHASE_LOAD 4 #define PROGRESS_IVFFLAT_PHASE_SORT 4
#define PROGRESS_IVFFLAT_PHASE_LOAD 5
#define IVFFLAT_LIST_SIZE(_dim) (offsetof(IvfflatListData, center) + VECTOR_SIZE(_dim)) #define IVFFLAT_LIST_SIZE(_dim) (offsetof(IvfflatListData, center) + VECTOR_SIZE(_dim))
@@ -83,6 +78,9 @@
/* Variables */ /* Variables */
extern int ivfflat_probes; extern int ivfflat_probes;
/* Exported functions */
PGDLLEXPORT void _PG_init(void);
typedef struct VectorArrayData typedef struct VectorArrayData
{ {
int length; int length;
@@ -106,56 +104,6 @@ typedef struct IvfflatOptions
int lists; /* number of lists */ int lists; /* number of lists */
} IvfflatOptions; } IvfflatOptions;
typedef struct IvfflatSpool
{
Tuplesortstate *sortstate;
Relation heap;
Relation index;
} IvfflatSpool;
typedef struct IvfflatShared
{
/* Immutable state */
Oid heaprelid;
Oid indexrelid;
bool isconcurrent;
int scantuplesortstates;
/* Worker progress */
ConditionVariable workersdonecv;
/* Mutex for mutable state */
slock_t mutex;
/* Mutable state */
int nparticipantsdone;
double reltuples;
double indtuples;
#ifdef IVFFLAT_KMEANS_DEBUG
double inertia;
#endif
#if PG_VERSION_NUM < 120000
ParallelHeapScanDescData heapdesc; /* must come last */
#endif
} IvfflatShared;
#if PG_VERSION_NUM >= 120000
#define ParallelTableScanFromIvfflatShared(shared) \
(ParallelTableScanDesc) ((char *) (shared) + BUFFERALIGN(sizeof(IvfflatShared)))
#endif
typedef struct IvfflatLeader
{
ParallelContext *pcxt;
int nparticipanttuplesorts;
IvfflatShared *ivfshared;
Sharedsort *sharedsort;
Snapshot snapshot;
Vector *ivfcenters;
} IvfflatLeader;
typedef struct IvfflatBuildState typedef struct IvfflatBuildState
{ {
/* Info */ /* Info */
@@ -198,12 +146,6 @@ typedef struct IvfflatBuildState
Tuplesortstate *sortstate; Tuplesortstate *sortstate;
TupleDesc tupdesc; TupleDesc tupdesc;
TupleTableSlot *slot; TupleTableSlot *slot;
/* Memory */
MemoryContext tmpCtx;
/* Parallel builds */
IvfflatLeader *ivfleader;
} IvfflatBuildState; } IvfflatBuildState;
typedef struct IvfflatMetaPageData typedef struct IvfflatMetaPageData
@@ -244,11 +186,8 @@ typedef struct IvfflatScanList
typedef struct IvfflatScanOpaqueData typedef struct IvfflatScanOpaqueData
{ {
int probes; int probes;
int dimensions;
bool first; bool first;
Buffer buf; Buffer buf;
ItemPointerData heaptid;
IndexTuple itup;
/* Sorting */ /* Sorting */
Tuplesortstate *sortstate; Tuplesortstate *sortstate;
@@ -281,15 +220,12 @@ void IvfflatKmeans(Relation index, VectorArray samples, VectorArray centers);
FmgrInfo *IvfflatOptionalProcInfo(Relation rel, uint16 procnum); FmgrInfo *IvfflatOptionalProcInfo(Relation rel, uint16 procnum);
bool IvfflatNormValue(FmgrInfo *procinfo, Oid collation, Datum *value, Vector * result); bool IvfflatNormValue(FmgrInfo *procinfo, Oid collation, Datum *value, Vector * result);
int IvfflatGetLists(Relation index); int IvfflatGetLists(Relation index);
void IvfflatGetMetaPageInfo(Relation index, int *lists, int *dimensions); void IvfflatUpdateList(Relation index, GenericXLogState *state, ListInfo listInfo, BlockNumber insertPage, BlockNumber originalInsertPage, BlockNumber startPage, ForkNumber forkNum);
void IvfflatUpdateList(Relation index, ListInfo listInfo, BlockNumber insertPage, BlockNumber originalInsertPage, BlockNumber startPage, ForkNumber forkNum);
void IvfflatCommitBuffer(Buffer buf, GenericXLogState *state); void IvfflatCommitBuffer(Buffer buf, GenericXLogState *state);
void IvfflatAppendPage(Relation index, Buffer *buf, Page *page, GenericXLogState **state, ForkNumber forkNum); void IvfflatAppendPage(Relation index, Buffer *buf, Page *page, GenericXLogState **state, ForkNumber forkNum);
Buffer IvfflatNewBuffer(Relation index, ForkNumber forkNum); Buffer IvfflatNewBuffer(Relation index, ForkNumber forkNum);
void IvfflatInitPage(Buffer buf, Page page); void IvfflatInitPage(Buffer buf, Page page);
void IvfflatInitRegisterPage(Relation index, Buffer *buf, Page *page, GenericXLogState **state); void IvfflatInitRegisterPage(Relation index, Buffer *buf, Page *page, GenericXLogState **state);
void IvfflatInit(void);
PGDLLEXPORT void IvfflatParallelBuildMain(dsm_segment *seg, shm_toc *toc);
/* Index access methods */ /* Index access methods */
IndexBuildResult *ivfflatbuild(Relation heap, Relation index, IndexInfo *indexInfo); IndexBuildResult *ivfflatbuild(Relation heap, Relation index, IndexInfo *indexInfo);
@@ -298,7 +234,9 @@ bool ivfflatinsert(Relation index, Datum *values, bool *isnull, ItemPointer hea
#if PG_VERSION_NUM >= 140000 #if PG_VERSION_NUM >= 140000
,bool indexUnchanged ,bool indexUnchanged
#endif #endif
#if PG_VERSION_NUM >= 100000
,IndexInfo *indexInfo ,IndexInfo *indexInfo
#endif
); );
IndexBulkDeleteResult *ivfflatbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, IndexBulkDeleteCallback callback, void *callback_state); IndexBulkDeleteResult *ivfflatbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, IndexBulkDeleteCallback callback, void *callback_state);
IndexBulkDeleteResult *ivfflatvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats); IndexBulkDeleteResult *ivfflatvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats);

View File

@@ -4,8 +4,6 @@
#include "ivfflat.h" #include "ivfflat.h"
#include "storage/bufmgr.h" #include "storage/bufmgr.h"
#include "storage/lmgr.h"
#include "utils/memutils.h"
/* /*
* Find the list that minimizes the distance function * Find the list that minimizes the distance function
@@ -24,10 +22,6 @@ FindInsertPage(Relation rel, Datum *values, BlockNumber *insertPage, ListInfo *
OffsetNumber offno; OffsetNumber offno;
OffsetNumber maxoffno; OffsetNumber maxoffno;
/* Avoid compiler warning */
listInfo->blkno = nextblkno;
listInfo->offno = FirstOffsetNumber;
procinfo = index_getprocinfo(rel, 1, IVFFLAT_DISTANCE_PROC); procinfo = index_getprocinfo(rel, 1, IVFFLAT_DISTANCE_PROC);
collation = rel->rd_indcollation[0]; collation = rel->rd_indcollation[0];
@@ -44,7 +38,7 @@ FindInsertPage(Relation rel, Datum *values, BlockNumber *insertPage, ListInfo *
list = (IvfflatList) PageGetItem(cpage, PageGetItemId(cpage, offno)); list = (IvfflatList) PageGetItem(cpage, PageGetItemId(cpage, offno));
distance = DatumGetFloat8(FunctionCall2Coll(procinfo, collation, values[0], PointerGetDatum(&list->center))); distance = DatumGetFloat8(FunctionCall2Coll(procinfo, collation, values[0], PointerGetDatum(&list->center)));
if (distance < minDistance || !BlockNumberIsValid(*insertPage)) if (distance < minDistance)
{ {
*insertPage = list->insertPage; *insertPage = list->insertPage;
listInfo->blkno = nextblkno; listInfo->blkno = nextblkno;
@@ -63,11 +57,8 @@ FindInsertPage(Relation rel, Datum *values, BlockNumber *insertPage, ListInfo *
* Insert a tuple into the index * Insert a tuple into the index
*/ */
static void static void
InsertTuple(Relation rel, Datum *values, bool *isnull, ItemPointer heap_tid, Relation heapRel) InsertTuple(Relation rel, IndexTuple itup, Relation heapRel, Datum *values)
{ {
IndexTuple itup;
Datum value;
FmgrInfo *normprocinfo;
Buffer buf; Buffer buf;
Page page; Page page;
GenericXLogState *state; GenericXLogState *state;
@@ -76,27 +67,11 @@ InsertTuple(Relation rel, Datum *values, bool *isnull, ItemPointer heap_tid, Rel
ListInfo listInfo; ListInfo listInfo;
BlockNumber originalInsertPage; BlockNumber originalInsertPage;
/* Detoast once for all calls */
value = PointerGetDatum(PG_DETOAST_DATUM(values[0]));
/* Normalize if needed */
normprocinfo = IvfflatOptionalProcInfo(rel, IVFFLAT_NORM_PROC);
if (normprocinfo != NULL)
{
if (!IvfflatNormValue(normprocinfo, rel->rd_indcollation[0], &value, NULL))
return;
}
/* Find the insert page - sets the page and list info */ /* Find the insert page - sets the page and list info */
FindInsertPage(rel, values, &insertPage, &listInfo); FindInsertPage(rel, values, &insertPage, &listInfo);
Assert(BlockNumberIsValid(insertPage)); Assert(BlockNumberIsValid(insertPage));
originalInsertPage = insertPage; originalInsertPage = insertPage;
/* Form tuple */
itup = index_form_tuple(RelationGetDescr(rel), &value, isnull);
itup->t_tid = *heap_tid;
/* Get tuple size */
itemsz = MAXALIGN(IndexTupleSize(itup)); itemsz = MAXALIGN(IndexTupleSize(itup));
Assert(itemsz <= BLCKSZ - MAXALIGN(SizeOfPageHeaderData) - MAXALIGN(sizeof(IvfflatPageOpaqueData))); Assert(itemsz <= BLCKSZ - MAXALIGN(SizeOfPageHeaderData) - MAXALIGN(sizeof(IvfflatPageOpaqueData)));
@@ -122,16 +97,23 @@ InsertTuple(Relation rel, Datum *values, bool *isnull, ItemPointer heap_tid, Rel
} }
else else
{ {
Buffer metabuf;
Buffer newbuf; Buffer newbuf;
Page newpage; Page newpage;
/*
* From ReadBufferExtended: Caller is responsible for ensuring
* that only one backend tries to extend a relation at the same
* time!
*/
metabuf = ReadBuffer(rel, IVFFLAT_METAPAGE_BLKNO);
LockBuffer(metabuf, BUFFER_LOCK_EXCLUSIVE);
/* Add a new page */ /* Add a new page */
LockRelationForExtension(rel, ExclusiveLock);
newbuf = IvfflatNewBuffer(rel, MAIN_FORKNUM); newbuf = IvfflatNewBuffer(rel, MAIN_FORKNUM);
UnlockRelationForExtension(rel, ExclusiveLock); newpage = GenericXLogRegisterBuffer(state, newbuf, GENERIC_XLOG_FULL_IMAGE);
/* Init new page */ /* Init new page */
newpage = GenericXLogRegisterBuffer(state, newbuf, GENERIC_XLOG_FULL_IMAGE);
IvfflatInitPage(newbuf, newpage); IvfflatInitPage(newbuf, newpage);
/* Update insert page */ /* Update insert page */
@@ -145,14 +127,12 @@ InsertTuple(Relation rel, Datum *values, bool *isnull, ItemPointer heap_tid, Rel
MarkBufferDirty(buf); MarkBufferDirty(buf);
GenericXLogFinish(state); GenericXLogFinish(state);
/* Unlock previous buffer */ /* Unlock extend relation lock as early as possible */
UnlockReleaseBuffer(buf); UnlockReleaseBuffer(metabuf);
/* Prepare new buffer */ /* Unlock rest */
state = GenericXLogStart(rel); UnlockReleaseBuffer(newbuf);
buf = newbuf; UnlockReleaseBuffer(buf);
page = GenericXLogRegisterBuffer(state, buf, 0);
break;
} }
} }
@@ -164,7 +144,7 @@ InsertTuple(Relation rel, Datum *values, bool *isnull, ItemPointer heap_tid, Rel
/* Update the insert page */ /* Update the insert page */
if (insertPage != originalInsertPage) if (insertPage != originalInsertPage)
IvfflatUpdateList(rel, listInfo, insertPage, originalInsertPage, InvalidBlockNumber, MAIN_FORKNUM); IvfflatUpdateList(rel, state, listInfo, insertPage, originalInsertPage, InvalidBlockNumber, MAIN_FORKNUM);
} }
/* /*
@@ -176,31 +156,36 @@ ivfflatinsert(Relation index, Datum *values, bool *isnull, ItemPointer heap_tid,
#if PG_VERSION_NUM >= 140000 #if PG_VERSION_NUM >= 140000
,bool indexUnchanged ,bool indexUnchanged
#endif #endif
#if PG_VERSION_NUM >= 100000
,IndexInfo *indexInfo ,IndexInfo *indexInfo
#endif
) )
{ {
MemoryContext oldCtx; IndexTuple itup;
MemoryContext insertCtx; Datum value;
FmgrInfo *normprocinfo;
/* Skip nulls */
if (isnull[0]) if (isnull[0])
return false; return false;
/* value = values[0];
* Use memory context since detoast, IvfflatNormValue, and
* index_form_tuple can allocate
*/
insertCtx = AllocSetContextCreate(CurrentMemoryContext,
"Ivfflat insert temporary context",
ALLOCSET_DEFAULT_SIZES);
oldCtx = MemoryContextSwitchTo(insertCtx);
/* Insert tuple */ /* Normalize if needed */
InsertTuple(index, values, isnull, heap_tid, heap); normprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_NORM_PROC);
if (normprocinfo != NULL)
{
if (!IvfflatNormValue(normprocinfo, index->rd_indcollation[0], &value, NULL))
return false;
}
/* Delete memory context */ itup = index_form_tuple(RelationGetDescr(index), &value, isnull);
MemoryContextSwitchTo(oldCtx); itup->t_tid = *heap_tid;
MemoryContextDelete(insertCtx); InsertTuple(index, itup, heap, &value);
pfree(itup);
/* Clean up if we allocated a new value */
if (value != values[0])
pfree(DatumGetPointer(value));
return false; return false;
} }

View File

@@ -1,7 +1,6 @@
#include "postgres.h" #include "postgres.h"
#include <float.h> #include <float.h>
#include <math.h>
#include "ivfflat.h" #include "ivfflat.h"
#include "miscadmin.h" #include "miscadmin.h"
@@ -16,6 +15,7 @@ InitCenters(Relation index, VectorArray samples, VectorArray centers, float *low
{ {
FmgrInfo *procinfo; FmgrInfo *procinfo;
Oid collation; Oid collation;
int i;
int64 j; int64 j;
double distance; double distance;
double sum; double sum;
@@ -35,7 +35,7 @@ InitCenters(Relation index, VectorArray samples, VectorArray centers, float *low
for (j = 0; j < numSamples; j++) for (j = 0; j < numSamples; j++)
weight[j] = DBL_MAX; weight[j] = DBL_MAX;
for (int i = 0; i < numCenters; i++) for (i = 0; i < numCenters; i++)
{ {
CHECK_FOR_INTERRUPTS(); CHECK_FOR_INTERRUPTS();
@@ -87,12 +87,13 @@ InitCenters(Relation index, VectorArray samples, VectorArray centers, float *low
static inline void static inline void
ApplyNorm(FmgrInfo *normprocinfo, Oid collation, Vector * vec) ApplyNorm(FmgrInfo *normprocinfo, Oid collation, Vector * vec)
{ {
int i;
double norm = DatumGetFloat8(FunctionCall1Coll(normprocinfo, collation, PointerGetDatum(vec))); double norm = DatumGetFloat8(FunctionCall1Coll(normprocinfo, collation, PointerGetDatum(vec)));
/* TODO Handle zero norm */ /* TODO Handle zero norm */
if (norm > 0) if (norm > 0)
{ {
for (int i = 0; i < vec->dim; i++) for (i = 0; i < vec->dim; i++)
vec->x[i] /= norm; vec->x[i] /= norm;
} }
} }
@@ -112,6 +113,8 @@ CompareVectors(const void *a, const void *b)
static void static void
QuickCenters(Relation index, VectorArray samples, VectorArray centers) QuickCenters(Relation index, VectorArray samples, VectorArray centers)
{ {
int i;
int j;
Vector *vec; Vector *vec;
int dimensions = centers->dim; int dimensions = centers->dim;
Oid collation = index->rd_indcollation[0]; Oid collation = index->rd_indcollation[0];
@@ -121,7 +124,7 @@ QuickCenters(Relation index, VectorArray samples, VectorArray centers)
if (samples->length > 0) if (samples->length > 0)
{ {
qsort(samples->items, samples->length, VECTOR_SIZE(samples->dim), CompareVectors); qsort(samples->items, samples->length, VECTOR_SIZE(samples->dim), CompareVectors);
for (int i = 0; i < samples->length; i++) for (i = 0; i < samples->length; i++)
{ {
vec = VectorArrayGet(samples, i); vec = VectorArrayGet(samples, i);
@@ -141,7 +144,7 @@ QuickCenters(Relation index, VectorArray samples, VectorArray centers)
SET_VARSIZE(vec, VECTOR_SIZE(dimensions)); SET_VARSIZE(vec, VECTOR_SIZE(dimensions));
vec->dim = dimensions; vec->dim = dimensions;
for (int j = 0; j < dimensions; j++) for (j = 0; j < dimensions; j++)
vec->x[j] = RandomDouble(); vec->x[j] = RandomDouble();
/* Normalize if needed (only needed for random centers) */ /* Normalize if needed (only needed for random centers) */
@@ -208,7 +211,7 @@ ElkanKmeans(Relation index, VectorArray samples, VectorArray centers)
/* Check memory requirements */ /* Check memory requirements */
/* Add one to error message to ceil */ /* Add one to error message to ceil */
if (totalSize > (Size) maintenance_work_mem * 1024L) if (totalSize / 1024 > maintenance_work_mem)
ereport(ERROR, ereport(ERROR,
(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
errmsg("memory required is %zu MB, maintenance_work_mem is %d MB", errmsg("memory required is %zu MB, maintenance_work_mem is %d MB",
@@ -248,7 +251,7 @@ ElkanKmeans(Relation index, VectorArray samples, VectorArray centers)
for (j = 0; j < numSamples; j++) for (j = 0; j < numSamples; j++)
{ {
minDistance = DBL_MAX; minDistance = DBL_MAX;
closestCenter = 0; closestCenter = -1;
/* Find closest center */ /* Find closest center */
for (k = 0; k < numCenters; k++) for (k = 0; k < numCenters; k++)
@@ -395,14 +398,6 @@ ElkanKmeans(Relation index, VectorArray samples, VectorArray centers)
if (centerCounts[j] > 0) if (centerCounts[j] > 0)
{ {
/* Double avoids overflow, but requires more memory */
/* TODO Update bounds */
for (k = 0; k < dimensions; k++)
{
if (isinf(vec->x[k]))
vec->x[k] = vec->x[k] > 0 ? FLT_MAX : -FLT_MAX;
}
for (k = 0; k < dimensions; k++) for (k = 0; k < dimensions; k++)
vec->x[k] /= centerCounts[j]; vec->x[k] /= centerCounts[j];
} }
@@ -466,31 +461,16 @@ CheckCenters(Relation index, VectorArray centers)
{ {
FmgrInfo *normprocinfo; FmgrInfo *normprocinfo;
Oid collation; Oid collation;
Vector *vec; int i;
double norm; double norm;
if (centers->length != centers->maxlen) if (centers->length != centers->maxlen)
elog(ERROR, "Not enough centers. Please report a bug."); elog(ERROR, "Not enough centers. Please report a bug.");
/* Ensure no NaN or infinite values */
for (int i = 0; i < centers->length; i++)
{
vec = VectorArrayGet(centers, i);
for (int j = 0; j < vec->dim; j++)
{
if (isnan(vec->x[j]))
elog(ERROR, "NaN detected. Please report a bug.");
if (isinf(vec->x[j]))
elog(ERROR, "Infinite value detected. Please report a bug.");
}
}
/* Ensure no duplicate centers */ /* Ensure no duplicate centers */
/* Fine to sort in-place */ /* Fine to sort in-place */
qsort(centers->items, centers->length, VECTOR_SIZE(centers->dim), CompareVectors); qsort(centers->items, centers->length, VECTOR_SIZE(centers->dim), CompareVectors);
for (int i = 1; i < centers->length; i++) for (i = 1; i < centers->length; i++)
{ {
if (CompareVectors(VectorArrayGet(centers, i), VectorArrayGet(centers, i - 1)) == 0) if (CompareVectors(VectorArrayGet(centers, i), VectorArrayGet(centers, i - 1)) == 0)
elog(ERROR, "Duplicate centers detected. Please report a bug."); elog(ERROR, "Duplicate centers detected. Please report a bug.");
@@ -503,7 +483,7 @@ CheckCenters(Relation index, VectorArray centers)
{ {
collation = index->rd_indcollation[0]; collation = index->rd_indcollation[0];
for (int i = 0; i < centers->length; i++) for (i = 0; i < centers->length; i++)
{ {
norm = DatumGetFloat8(FunctionCall1Coll(normprocinfo, collation, PointerGetDatum(VectorArrayGet(centers, i)))); norm = DatumGetFloat8(FunctionCall1Coll(normprocinfo, collation, PointerGetDatum(VectorArrayGet(centers, i))));
if (norm == 0) if (norm == 0)

View File

@@ -3,13 +3,18 @@
#include <float.h> #include <float.h>
#include "access/relscan.h" #include "access/relscan.h"
#include "catalog/pg_operator_d.h"
#include "catalog/pg_type_d.h"
#include "ivfflat.h" #include "ivfflat.h"
#include "miscadmin.h" #include "miscadmin.h"
#include "pgstat.h"
#include "storage/bufmgr.h" #include "storage/bufmgr.h"
#if PG_VERSION_NUM >= 110000
#include "catalog/pg_operator_d.h"
#include "catalog/pg_type_d.h"
#else
#include "catalog/pg_operator.h"
#include "catalog/pg_type.h"
#endif
/* /*
* Compare list distances * Compare list distances
*/ */
@@ -31,36 +36,36 @@ CompareLists(const pairingheap_node *a, const pairingheap_node *b, void *arg)
static void static void
GetScanLists(IndexScanDesc scan, Datum value) GetScanLists(IndexScanDesc scan, Datum value)
{ {
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque; Buffer cbuf;
Page cpage;
IvfflatList list;
OffsetNumber offno;
OffsetNumber maxoffno;
BlockNumber nextblkno = IVFFLAT_HEAD_BLKNO; BlockNumber nextblkno = IVFFLAT_HEAD_BLKNO;
int listCount = 0; int listCount = 0;
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
double distance;
IvfflatScanList *scanlist;
double maxDistance = DBL_MAX; double maxDistance = DBL_MAX;
/* Search all list pages */ /* Search all list pages */
while (BlockNumberIsValid(nextblkno)) while (BlockNumberIsValid(nextblkno))
{ {
Buffer cbuf;
Page cpage;
OffsetNumber maxoffno;
cbuf = ReadBuffer(scan->indexRelation, nextblkno); cbuf = ReadBuffer(scan->indexRelation, nextblkno);
LockBuffer(cbuf, BUFFER_LOCK_SHARE); LockBuffer(cbuf, BUFFER_LOCK_SHARE);
cpage = BufferGetPage(cbuf); cpage = BufferGetPage(cbuf);
maxoffno = PageGetMaxOffsetNumber(cpage); maxoffno = PageGetMaxOffsetNumber(cpage);
for (OffsetNumber offno = FirstOffsetNumber; offno <= maxoffno; offno = OffsetNumberNext(offno)) for (offno = FirstOffsetNumber; offno <= maxoffno; offno = OffsetNumberNext(offno))
{ {
IvfflatList list = (IvfflatList) PageGetItem(cpage, PageGetItemId(cpage, offno)); list = (IvfflatList) PageGetItem(cpage, PageGetItemId(cpage, offno));
double distance;
/* Use procinfo from the index instead of scan key for performance */ /* Use procinfo from the index instead of scan key for performance */
distance = DatumGetFloat8(FunctionCall2Coll(so->procinfo, so->collation, PointerGetDatum(&list->center), value)); distance = DatumGetFloat8(FunctionCall2Coll(so->procinfo, so->collation, PointerGetDatum(&list->center), value));
if (listCount < so->probes) if (listCount < so->probes)
{ {
IvfflatScanList *scanlist;
scanlist = &so->lists[listCount]; scanlist = &so->lists[listCount];
scanlist->startPage = list->startPage; scanlist->startPage = list->startPage;
scanlist->distance = distance; scanlist->distance = distance;
@@ -75,8 +80,6 @@ GetScanLists(IndexScanDesc scan, Datum value)
} }
else if (distance < maxDistance) else if (distance < maxDistance)
{ {
IvfflatScanList *scanlist;
/* Remove */ /* Remove */
scanlist = (IvfflatScanList *) pairingheap_remove_first(so->listQueue); scanlist = (IvfflatScanList *) pairingheap_remove_first(so->listQueue);
@@ -103,8 +106,15 @@ static void
GetScanItems(IndexScanDesc scan, Datum value) GetScanItems(IndexScanDesc scan, Datum value)
{ {
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque; IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
Buffer buf;
Page page;
IndexTuple itup;
BlockNumber searchPage;
OffsetNumber offno;
OffsetNumber maxoffno;
Datum datum;
bool isnull;
TupleDesc tupdesc = RelationGetDescr(scan->indexRelation); TupleDesc tupdesc = RelationGetDescr(scan->indexRelation);
double tuples = 0;
#if PG_VERSION_NUM >= 120000 #if PG_VERSION_NUM >= 120000
TupleTableSlot *slot = MakeSingleTupleTableSlot(so->tupdesc, &TTSOpsVirtual); TupleTableSlot *slot = MakeSingleTupleTableSlot(so->tupdesc, &TTSOpsVirtual);
@@ -122,35 +132,20 @@ GetScanItems(IndexScanDesc scan, Datum value)
/* Search closest probes lists */ /* Search closest probes lists */
while (!pairingheap_is_empty(so->listQueue)) while (!pairingheap_is_empty(so->listQueue))
{ {
BlockNumber searchPage = ((IvfflatScanList *) pairingheap_remove_first(so->listQueue))->startPage; searchPage = ((IvfflatScanList *) pairingheap_remove_first(so->listQueue))->startPage;
/* Search all entry pages for list */ /* Search all entry pages for list */
while (BlockNumberIsValid(searchPage)) while (BlockNumberIsValid(searchPage))
{ {
Buffer buf;
Page page;
OffsetNumber maxoffno;
buf = ReadBufferExtended(scan->indexRelation, MAIN_FORKNUM, searchPage, RBM_NORMAL, bas); buf = ReadBufferExtended(scan->indexRelation, MAIN_FORKNUM, searchPage, RBM_NORMAL, bas);
LockBuffer(buf, BUFFER_LOCK_SHARE); LockBuffer(buf, BUFFER_LOCK_SHARE);
page = BufferGetPage(buf); page = BufferGetPage(buf);
maxoffno = PageGetMaxOffsetNumber(page); maxoffno = PageGetMaxOffsetNumber(page);
for (OffsetNumber offno = FirstOffsetNumber; offno <= maxoffno; offno = OffsetNumberNext(offno)) for (offno = FirstOffsetNumber; offno <= maxoffno; offno = OffsetNumberNext(offno))
{ {
IndexTuple itup; itup = (IndexTuple) PageGetItem(page, PageGetItemId(page, offno));
Datum datum;
bool isnull;
ItemPointerData indextid;
ItemId itemid = PageGetItemId(page, offno);
/* Skip dead tuples */
if (scan->ignore_killed_tuples && ItemIdIsDead(itemid))
continue;
itup = (IndexTuple) PageGetItem(page, itemid);
datum = index_getattr(itup, 1, tupdesc, &isnull); datum = index_getattr(itup, 1, tupdesc, &isnull);
ItemPointerSet(&indextid, searchPage, offno);
/* /*
* Add virtual tuple * Add virtual tuple
@@ -163,13 +158,11 @@ GetScanItems(IndexScanDesc scan, Datum value)
slot->tts_isnull[0] = false; slot->tts_isnull[0] = false;
slot->tts_values[1] = PointerGetDatum(&itup->t_tid); slot->tts_values[1] = PointerGetDatum(&itup->t_tid);
slot->tts_isnull[1] = false; slot->tts_isnull[1] = false;
slot->tts_values[2] = PointerGetDatum(&indextid); slot->tts_values[2] = Int32GetDatum((int) searchPage);
slot->tts_isnull[2] = false; slot->tts_isnull[2] = false;
ExecStoreVirtualTuple(slot); ExecStoreVirtualTuple(slot);
tuplesort_puttupleslot(so->sortstate, slot); tuplesort_puttupleslot(so->sortstate, slot);
tuples++;
} }
searchPage = IvfflatPageGetOpaque(page)->nextblkno; searchPage = IvfflatPageGetOpaque(page)->nextblkno;
@@ -178,94 +171,9 @@ GetScanItems(IndexScanDesc scan, Datum value)
} }
} }
FreeAccessStrategy(bas);
if (tuples < 100)
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); tuplesort_performsort(so->sortstate);
} }
/*
* Mark prior tuple as dead
*/
static void
MarkPriorTupleDead(IndexScanDesc scan)
{
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
Buffer buf = so->buf;
Page page;
OffsetNumber maxoffno;
/* Safety check */
if (!BufferIsValid(so->buf) || !ItemPointerIsValid(&so->heaptid))
return;
/* Only a shared locked is needed for ItemIdMarkDead */
LockBuffer(buf, BUFFER_LOCK_SHARE);
page = BufferGetPage(buf);
maxoffno = PageGetMaxOffsetNumber(page);
for (OffsetNumber offno = FirstOffsetNumber; offno <= maxoffno; offno = OffsetNumberNext(offno))
{
ItemId itemid = PageGetItemId(page, offno);
IndexTuple itup = (IndexTuple) PageGetItem(page, itemid);
/*
* Find tuple. Since buffer has been pinned, tuple cannot have been
* vacuumed (and heap TID reused).
*/
if (ItemPointerEquals(&itup->t_tid, &so->heaptid))
{
/*
* Make sure tuple has not already been marked dead to avoid extra
* WAL if wal_log_hints or data checksums enabled
*/
if (!ItemIdIsDead(itemid))
{
ItemIdMarkDead(itemid);
MarkBufferDirtyHint(buf, true);
}
break;
}
}
/* Unlock buffer */
LockBuffer(buf, BUFFER_LOCK_UNLOCK);
}
/*
* Set tuple for index-only scan
*/
static void
SetIndexTuple(IndexScanDesc scan, ItemPointer indextid)
{
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
Buffer buf = so->buf;
Page page;
OffsetNumber offno = ItemPointerGetOffsetNumber(indextid);
IndexTuple itup;
Size itupSize;
LockBuffer(buf, BUFFER_LOCK_SHARE);
page = BufferGetPage(buf);
itup = (IndexTuple) PageGetItem(page, PageGetItemId(page, offno));
itupSize = IndexTupleSize(itup);
if (so->itup == NULL)
so->itup = palloc(BLCKSZ);
memcpy(so->itup, itup, itupSize);
scan->xs_itup = so->itup;
LockBuffer(buf, BUFFER_LOCK_UNLOCK);
}
/* /*
* Prepare for an index scan * Prepare for an index scan
*/ */
@@ -275,7 +183,6 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys)
IndexScanDesc scan; IndexScanDesc scan;
IvfflatScanOpaque so; IvfflatScanOpaque so;
int lists; int lists;
int dimensions;
AttrNumber attNums[] = {1}; AttrNumber attNums[] = {1};
Oid sortOperators[] = {Float8LessOperator}; Oid sortOperators[] = {Float8LessOperator};
Oid sortCollations[] = {InvalidOid}; Oid sortCollations[] = {InvalidOid};
@@ -283,9 +190,7 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys)
int probes = ivfflat_probes; int probes = ivfflat_probes;
scan = RelationGetIndexScan(index, nkeys, norderbys); scan = RelationGetIndexScan(index, nkeys, norderbys);
lists = IvfflatGetLists(scan->indexRelation);
/* Get lists and dimensions from metapage */
IvfflatGetMetaPageInfo(index, &lists, &dimensions);
if (probes > lists) if (probes > lists)
probes = lists; probes = lists;
@@ -293,10 +198,7 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys)
so = (IvfflatScanOpaque) palloc(offsetof(IvfflatScanOpaqueData, lists) + probes * sizeof(IvfflatScanList)); so = (IvfflatScanOpaque) palloc(offsetof(IvfflatScanOpaqueData, lists) + probes * sizeof(IvfflatScanList));
so->buf = InvalidBuffer; so->buf = InvalidBuffer;
so->first = true; so->first = true;
ItemPointerSetInvalid(&so->heaptid);
so->itup = NULL;
so->probes = probes; so->probes = probes;
so->dimensions = dimensions;
/* Set support functions */ /* Set support functions */
so->procinfo = index_getprocinfo(index, 1, IVFFLAT_DISTANCE_PROC); so->procinfo = index_getprocinfo(index, 1, IVFFLAT_DISTANCE_PROC);
@@ -310,11 +212,15 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys)
so->tupdesc = CreateTemplateTupleDesc(3, false); so->tupdesc = CreateTemplateTupleDesc(3, false);
#endif #endif
TupleDescInitEntry(so->tupdesc, (AttrNumber) 1, "distance", FLOAT8OID, -1, 0); TupleDescInitEntry(so->tupdesc, (AttrNumber) 1, "distance", FLOAT8OID, -1, 0);
TupleDescInitEntry(so->tupdesc, (AttrNumber) 2, "heaptid", TIDOID, -1, 0); TupleDescInitEntry(so->tupdesc, (AttrNumber) 2, "tid", TIDOID, -1, 0);
TupleDescInitEntry(so->tupdesc, (AttrNumber) 3, "indextid", TIDOID, -1, 0); TupleDescInitEntry(so->tupdesc, (AttrNumber) 3, "indexblkno", INT4OID, -1, 0);
/* Prep sort */ /* Prep sort */
#if PG_VERSION_NUM >= 110000
so->sortstate = tuplesort_begin_heap(so->tupdesc, 1, attNums, sortOperators, sortCollations, nullsFirstFlags, work_mem, NULL, false); so->sortstate = tuplesort_begin_heap(so->tupdesc, 1, attNums, sortOperators, sortCollations, nullsFirstFlags, work_mem, NULL, false);
#else
so->sortstate = tuplesort_begin_heap(so->tupdesc, 1, attNums, sortOperators, sortCollations, nullsFirstFlags, work_mem, false);
#endif
#if PG_VERSION_NUM >= 120000 #if PG_VERSION_NUM >= 120000
so->slot = MakeSingleTupleTableSlot(so->tupdesc, &TTSOpsMinimalTuple); so->slot = MakeSingleTupleTableSlot(so->tupdesc, &TTSOpsMinimalTuple);
@@ -326,8 +232,6 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys)
scan->opaque = so; scan->opaque = so;
scan->xs_itupdesc = RelationGetDescr(index);
return scan; return scan;
} }
@@ -345,7 +249,6 @@ ivfflatrescan(IndexScanDesc scan, ScanKey keys, int nkeys, ScanKey orderbys, int
#endif #endif
so->first = true; so->first = true;
ItemPointerSetInvalid(&so->heaptid);
pairingheap_reset(so->listQueue); pairingheap_reset(so->listQueue);
if (keys && scan->numberOfKeys > 0) if (keys && scan->numberOfKeys > 0)
@@ -373,26 +276,21 @@ ivfflatgettuple(IndexScanDesc scan, ScanDirection dir)
{ {
Datum value; Datum value;
/* Count index scan for stats */
pgstat_count_index_scan(scan->indexRelation);
/* Safety check */ /* Safety check */
if (scan->orderByData == NULL) if (scan->orderByData == NULL)
elog(ERROR, "cannot scan ivfflat index without order"); elog(ERROR, "cannot scan ivfflat index without order");
/* No items will match if null */
if (scan->orderByData->sk_flags & SK_ISNULL) if (scan->orderByData->sk_flags & SK_ISNULL)
value = PointerGetDatum(InitVector(so->dimensions)); return false;
else
value = scan->orderByData->sk_argument;
if (so->normprocinfo != NULL)
{ {
value = scan->orderByData->sk_argument; /* No items will match if normalization fails */
if (!IvfflatNormValue(so->normprocinfo, so->collation, &value, NULL))
/* Value should not be compressed or toasted */ return false;
Assert(!VARATT_IS_COMPRESSED(DatumGetPointer(value)));
Assert(!VARATT_IS_EXTENDED(DatumGetPointer(value)));
/* Fine if normalization fails */
if (so->normprocinfo != NULL)
IvfflatNormValue(so->normprocinfo, so->collation, &value, NULL);
} }
IvfflatBench("GetScanLists", GetScanLists(scan, value)); IvfflatBench("GetScanLists", GetScanLists(scan, value));
@@ -403,28 +301,22 @@ ivfflatgettuple(IndexScanDesc scan, ScanDirection dir)
if (value != scan->orderByData->sk_argument) if (value != scan->orderByData->sk_argument)
pfree(DatumGetPointer(value)); pfree(DatumGetPointer(value));
} }
else
{
/* Mark prior tuple as dead */
if (scan->kill_prior_tuple)
MarkPriorTupleDead(scan);
}
#if PG_VERSION_NUM >= 100000
if (tuplesort_gettupleslot(so->sortstate, true, false, so->slot, NULL)) if (tuplesort_gettupleslot(so->sortstate, true, false, so->slot, NULL))
#else
if (tuplesort_gettupleslot(so->sortstate, true, so->slot, NULL))
#endif
{ {
ItemPointer heaptid = (ItemPointer) DatumGetPointer(slot_getattr(so->slot, 2, &so->isnull)); ItemPointer tid = (ItemPointer) DatumGetPointer(slot_getattr(so->slot, 2, &so->isnull));
ItemPointer indextid = (ItemPointer) DatumGetPointer(slot_getattr(so->slot, 3, &so->isnull)); BlockNumber indexblkno = DatumGetInt32(slot_getattr(so->slot, 3, &so->isnull));
#if PG_VERSION_NUM >= 120000 #if PG_VERSION_NUM >= 120000
scan->xs_heaptid = *heaptid; scan->xs_heaptid = *tid;
#else #else
scan->xs_ctup.t_self = *heaptid; scan->xs_ctup.t_self = *tid;
#endif #endif
/* Keep track of info needed to mark tuple as dead */
so->heaptid = *heaptid;
/* Unpin buffer */
if (BufferIsValid(so->buf)) if (BufferIsValid(so->buf))
ReleaseBuffer(so->buf); ReleaseBuffer(so->buf);
@@ -434,11 +326,7 @@ ivfflatgettuple(IndexScanDesc scan, ScanDirection dir)
* *
* https://www.postgresql.org/docs/current/index-locking.html * https://www.postgresql.org/docs/current/index-locking.html
*/ */
so->buf = ReadBuffer(scan->indexRelation, ItemPointerGetBlockNumber(indextid)); so->buf = ReadBuffer(scan->indexRelation, indexblkno);
/* Set tuple for index-only scan */
if (scan->xs_want_itup)
SetIndexTuple(scan, indextid);
scan->xs_recheckorderby = false; scan->xs_recheckorderby = false;
return true; return true;
@@ -459,9 +347,6 @@ ivfflatendscan(IndexScanDesc scan)
if (BufferIsValid(so->buf)) if (BufferIsValid(so->buf))
ReleaseBuffer(so->buf); ReleaseBuffer(so->buf);
if (so->itup != NULL)
pfree(so->itup);
pairingheap_free(so->listQueue); pairingheap_free(so->listQueue);
tuplesort_end(so->sortstate); tuplesort_end(so->sortstate);

View File

@@ -35,7 +35,9 @@ VectorArrayFree(VectorArray arr)
void void
PrintVectorArray(char *msg, VectorArray arr) PrintVectorArray(char *msg, VectorArray arr)
{ {
for (int i = 0; i < arr->length; i++) int i;
for (i = 0; i < arr->length; i++)
PrintVector(msg, VectorArrayGet(arr, i)); PrintVector(msg, VectorArrayGet(arr, i));
} }
@@ -76,16 +78,20 @@ IvfflatOptionalProcInfo(Relation rel, uint16 procnum)
bool bool
IvfflatNormValue(FmgrInfo *procinfo, Oid collation, Datum *value, Vector * result) IvfflatNormValue(FmgrInfo *procinfo, Oid collation, Datum *value, Vector * result)
{ {
double norm = DatumGetFloat8(FunctionCall1Coll(procinfo, collation, *value)); Vector *v;
int i;
double norm;
norm = DatumGetFloat8(FunctionCall1Coll(procinfo, collation, *value));
if (norm > 0) if (norm > 0)
{ {
Vector *v = DatumGetVector(*value); v = (Vector *) DatumGetPointer(*value);
if (result == NULL) if (result == NULL)
result = InitVector(v->dim); result = InitVector(v->dim);
for (int i = 0; i < v->dim; i++) for (i = 0; i < v->dim; i++)
result->x[i] = v->x[i] / norm; result->x[i] = v->x[i] / norm;
*value = PointerGetDatum(result); *value = PointerGetDatum(result);
@@ -172,40 +178,16 @@ IvfflatAppendPage(Relation index, Buffer *buf, Page *page, GenericXLogState **st
*buf = newbuf; *buf = newbuf;
} }
/*
* Get the metapage info
*/
void
IvfflatGetMetaPageInfo(Relation index, int *lists, int *dimensions)
{
Buffer buf;
Page page;
IvfflatMetaPage metap;
buf = ReadBuffer(index, IVFFLAT_METAPAGE_BLKNO);
LockBuffer(buf, BUFFER_LOCK_SHARE);
page = BufferGetPage(buf);
metap = IvfflatPageGetMeta(page);
*lists = metap->lists;
if (dimensions != NULL)
*dimensions = metap->dimensions;
UnlockReleaseBuffer(buf);
}
/* /*
* Update the start or insert page of a list * Update the start or insert page of a list
*/ */
void void
IvfflatUpdateList(Relation index, ListInfo listInfo, IvfflatUpdateList(Relation index, GenericXLogState *state, ListInfo listInfo,
BlockNumber insertPage, BlockNumber originalInsertPage, BlockNumber insertPage, BlockNumber originalInsertPage,
BlockNumber startPage, ForkNumber forkNum) BlockNumber startPage, ForkNumber forkNum)
{ {
Buffer buf; Buffer buf;
Page page; Page page;
GenericXLogState *state;
IvfflatList list; IvfflatList list;
bool changed = false; bool changed = false;

View File

@@ -12,23 +12,34 @@ ivfflatbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
IndexBulkDeleteCallback callback, void *callback_state) IndexBulkDeleteCallback callback, void *callback_state)
{ {
Relation index = info->index; Relation index = info->index;
BlockNumber blkno = IVFFLAT_HEAD_BLKNO; Buffer cbuf;
Page cpage;
Buffer buf;
Page page;
IvfflatList list;
IndexTuple itup;
ItemPointer htup;
OffsetNumber deletable[MaxOffsetNumber];
int ndeletable;
BlockNumber startPages[MaxOffsetNumber];
BlockNumber nextblkno = IVFFLAT_HEAD_BLKNO;
BlockNumber searchPage;
BlockNumber insertPage;
GenericXLogState *state;
OffsetNumber coffno;
OffsetNumber cmaxoffno;
OffsetNumber offno;
OffsetNumber maxoffno;
ListInfo listInfo;
BufferAccessStrategy bas = GetAccessStrategy(BAS_BULKREAD); BufferAccessStrategy bas = GetAccessStrategy(BAS_BULKREAD);
if (stats == NULL) if (stats == NULL)
stats = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult)); stats = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult));
/* Iterate over list pages */ /* Iterate over list pages */
while (BlockNumberIsValid(blkno)) while (BlockNumberIsValid(nextblkno))
{ {
Buffer cbuf; cbuf = ReadBuffer(index, nextblkno);
Page cpage;
OffsetNumber coffno;
OffsetNumber cmaxoffno;
BlockNumber startPages[MaxOffsetNumber];
ListInfo listInfo;
cbuf = ReadBuffer(index, blkno);
LockBuffer(cbuf, BUFFER_LOCK_SHARE); LockBuffer(cbuf, BUFFER_LOCK_SHARE);
cpage = BufferGetPage(cbuf); cpage = BufferGetPage(cbuf);
@@ -37,32 +48,23 @@ ivfflatbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
/* Iterate over lists */ /* Iterate over lists */
for (coffno = FirstOffsetNumber; coffno <= cmaxoffno; coffno = OffsetNumberNext(coffno)) for (coffno = FirstOffsetNumber; coffno <= cmaxoffno; coffno = OffsetNumberNext(coffno))
{ {
IvfflatList list = (IvfflatList) PageGetItem(cpage, PageGetItemId(cpage, coffno)); list = (IvfflatList) PageGetItem(cpage, PageGetItemId(cpage, coffno));
startPages[coffno - FirstOffsetNumber] = list->startPage; startPages[coffno - FirstOffsetNumber] = list->startPage;
} }
listInfo.blkno = blkno; listInfo.blkno = nextblkno;
blkno = IvfflatPageGetOpaque(cpage)->nextblkno; nextblkno = IvfflatPageGetOpaque(cpage)->nextblkno;
UnlockReleaseBuffer(cbuf); UnlockReleaseBuffer(cbuf);
for (coffno = FirstOffsetNumber; coffno <= cmaxoffno; coffno = OffsetNumberNext(coffno)) for (coffno = FirstOffsetNumber; coffno <= cmaxoffno; coffno = OffsetNumberNext(coffno))
{ {
BlockNumber searchPage = startPages[coffno - FirstOffsetNumber]; searchPage = startPages[coffno - FirstOffsetNumber];
BlockNumber insertPage = InvalidBlockNumber; insertPage = InvalidBlockNumber;
/* Iterate over entry pages */ /* Iterate over entry pages */
while (BlockNumberIsValid(searchPage)) while (BlockNumberIsValid(searchPage))
{ {
Buffer buf;
Page page;
GenericXLogState *state;
OffsetNumber offno;
OffsetNumber maxoffno;
OffsetNumber deletable[MaxOffsetNumber];
int ndeletable;
vacuum_delay_point(); vacuum_delay_point();
buf = ReadBufferExtended(index, MAIN_FORKNUM, searchPage, RBM_NORMAL, bas); buf = ReadBufferExtended(index, MAIN_FORKNUM, searchPage, RBM_NORMAL, bas);
@@ -84,8 +86,8 @@ ivfflatbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
/* Find deleted tuples */ /* Find deleted tuples */
for (offno = FirstOffsetNumber; offno <= maxoffno; offno = OffsetNumberNext(offno)) for (offno = FirstOffsetNumber; offno <= maxoffno; offno = OffsetNumberNext(offno))
{ {
IndexTuple itup = (IndexTuple) PageGetItem(page, PageGetItemId(page, offno)); itup = (IndexTuple) PageGetItem(page, PageGetItemId(page, offno));
ItemPointer htup = &(itup->t_tid); htup = &(itup->t_tid);
if (callback(htup, callback_state)) if (callback(htup, callback_state))
{ {
@@ -125,13 +127,11 @@ ivfflatbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
if (BlockNumberIsValid(insertPage)) if (BlockNumberIsValid(insertPage))
{ {
listInfo.offno = coffno; listInfo.offno = coffno;
IvfflatUpdateList(index, listInfo, insertPage, InvalidBlockNumber, InvalidBlockNumber, MAIN_FORKNUM); IvfflatUpdateList(index, state, listInfo, insertPage, InvalidBlockNumber, InvalidBlockNumber, MAIN_FORKNUM);
} }
} }
} }
FreeAccessStrategy(bas);
return stats; return stats;
} }
@@ -143,11 +143,6 @@ ivfflatvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
{ {
Relation rel = info->index; Relation rel = info->index;
if (info->analyze_only)
return stats;
/* stats is NULL if ambulkdelete not called */
/* OK to return NULL if index not changed */
if (stats == NULL) if (stats == NULL)
return NULL; return NULL;

View File

@@ -2,22 +2,15 @@
#include <math.h> #include <math.h>
#include "catalog/pg_type.h" #include "vector.h"
#include "fmgr.h" #include "fmgr.h"
#include "hnsw.h" #include "catalog/pg_type.h"
#include "ivfflat.h"
#include "lib/stringinfo.h" #include "lib/stringinfo.h"
#include "libpq/pqformat.h" #include "libpq/pqformat.h"
#include "port.h" /* for strtof() */
#include "utils/array.h" #include "utils/array.h"
#include "utils/builtins.h" #include "utils/builtins.h"
#include "utils/lsyscache.h" #include "utils/lsyscache.h"
#include "utils/numeric.h" #include "utils/numeric.h"
#include "vector.h"
#if PG_VERSION_NUM >= 160000
#include "varatt.h"
#endif
#if PG_VERSION_NUM >= 120000 #if PG_VERSION_NUM >= 120000
#include "common/shortest_dec.h" #include "common/shortest_dec.h"
@@ -27,26 +20,11 @@
#endif #endif
#if PG_VERSION_NUM < 130000 #if PG_VERSION_NUM < 130000
#define TYPALIGN_DOUBLE 'd'
#define TYPALIGN_INT 'i' #define TYPALIGN_INT 'i'
#endif #endif
#define STATE_DIMS(x) (ARR_DIMS(x)[0] - 1)
#define CreateStateDatums(dim) palloc(sizeof(Datum) * (dim + 1))
PG_MODULE_MAGIC; PG_MODULE_MAGIC;
/*
* Initialize index options and variables
*/
PGDLLEXPORT void _PG_init(void);
void
_PG_init(void)
{
HnswInit();
IvfflatInit();
}
/* /*
* Ensure same dimensions * Ensure same dimensions
*/ */
@@ -60,7 +38,7 @@ CheckDims(Vector * a, Vector * b)
} }
/* /*
* Ensure expected dimensions * Ensure expected dimension
*/ */
static inline void static inline void
CheckExpectedDim(int32 typmod, int dim) CheckExpectedDim(int32 typmod, int dim)
@@ -71,9 +49,7 @@ CheckExpectedDim(int32 typmod, int dim)
errmsg("expected %d dimensions, not %d", typmod, dim))); errmsg("expected %d dimensions, not %d", typmod, dim)));
} }
/*
* Ensure valid dimensions
*/
static inline void static inline void
CheckDim(int dim) CheckDim(int dim)
{ {
@@ -99,6 +75,7 @@ CheckElement(float value)
(errcode(ERRCODE_DATA_EXCEPTION), (errcode(ERRCODE_DATA_EXCEPTION),
errmsg("NaN not allowed in vector"))); errmsg("NaN not allowed in vector")));
if (isinf(value)) if (isinf(value))
ereport(ERROR, ereport(ERROR,
(errcode(ERRCODE_DATA_EXCEPTION), (errcode(ERRCODE_DATA_EXCEPTION),
@@ -106,70 +83,29 @@ CheckElement(float value)
} }
/* /*
* Allocate and initialize a new vector * Print vector - useful for debugging
*/ */
Vector * void
InitVector(int dim) PrintVector(char *msg, Vector * vector)
{ {
Vector *result; StringInfoData buf;
int size; int dim = vector->dim;
int i;
size = VECTOR_SIZE(dim); initStringInfo(&buf);
result = (Vector *) palloc0(size);
SET_VARSIZE(result, size);
result->dim = dim;
return result; appendStringInfoChar(&buf, '[');
for (i = 0; i < dim; i++)
{
if (i > 0)
appendStringInfoString(&buf, ",");
appendStringInfoString(&buf, float8out_internal(vector->x[i]));
}
appendStringInfoChar(&buf, ']');
elog(INFO, "%s = %s", msg, buf.data);
} }
/*
* Check for whitespace, since array_isspace() is static
*/
static inline bool
vector_isspace(char ch)
{
if (ch == ' ' ||
ch == '\t' ||
ch == '\n' ||
ch == '\r' ||
ch == '\v' ||
ch == '\f')
return true;
return false;
}
/*
* Check state array
*/
static float8 *
CheckStateArray(ArrayType *statearray, const char *caller)
{
if (ARR_NDIM(statearray) != 1 ||
ARR_DIMS(statearray)[0] < 1 ||
ARR_HASNULL(statearray) ||
ARR_ELEMTYPE(statearray) != FLOAT8OID)
elog(ERROR, "%s: expected state array", caller);
return (float8 *) ARR_DATA_PTR(statearray);
}
#if PG_VERSION_NUM < 120003
static pg_noinline void
float_overflow_error(void)
{
ereport(ERROR,
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
errmsg("value out of range: overflow")));
}
static pg_noinline void
float_underflow_error(void)
{
ereport(ERROR,
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
errmsg("value out of range: underflow")));
}
#endif
/* /*
* Convert textual representation to internal representation * Convert textual representation to internal representation
*/ */
@@ -179,20 +115,17 @@ vector_in(PG_FUNCTION_ARGS)
{ {
char *str = PG_GETARG_CSTRING(0); char *str = PG_GETARG_CSTRING(0);
int32 typmod = PG_GETARG_INT32(2); int32 typmod = PG_GETARG_INT32(2);
int i;
float x[VECTOR_MAX_DIM]; float x[VECTOR_MAX_DIM];
int dim = 0; int dim = 0;
char *pt; char *pt;
char *stringEnd; char *stringEnd;
Vector *result; Vector *result;
char *lit = pstrdup(str);
while (vector_isspace(*str))
str++;
if (*str != '[') if (*str != '[')
ereport(ERROR, ereport(ERROR,
(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
errmsg("malformed vector literal: \"%s\"", lit), errmsg("malformed vector literal: \"%s\"", str),
errdetail("Vector contents must start with \"[\"."))); errdetail("Vector contents must start with \"[\".")));
str++; str++;
@@ -206,15 +139,6 @@ vector_in(PG_FUNCTION_ARGS)
(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
errmsg("vector cannot have more than %d dimensions", VECTOR_MAX_DIM))); errmsg("vector cannot have more than %d dimensions", VECTOR_MAX_DIM)));
while (vector_isspace(*pt))
pt++;
/* Check for empty string like float4in */
if (*pt == '\0')
ereport(ERROR,
(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
errmsg("invalid input syntax for type vector: \"%s\"", lit)));
/* Use strtof like float4in to avoid a double-rounding problem */ /* Use strtof like float4in to avoid a double-rounding problem */
x[dim] = strtof(pt, &stringEnd); x[dim] = strtof(pt, &stringEnd);
CheckElement(x[dim]); CheckElement(x[dim]);
@@ -223,57 +147,37 @@ vector_in(PG_FUNCTION_ARGS)
if (stringEnd == pt) if (stringEnd == pt)
ereport(ERROR, ereport(ERROR,
(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
errmsg("invalid input syntax for type vector: \"%s\"", lit))); errmsg("invalid input syntax for type vector: \"%s\"", pt)));
while (vector_isspace(*stringEnd))
stringEnd++;
if (*stringEnd != '\0' && *stringEnd != ']') if (*stringEnd != '\0' && *stringEnd != ']')
ereport(ERROR, ereport(ERROR,
(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
errmsg("invalid input syntax for type vector: \"%s\"", lit))); errmsg("invalid input syntax for type vector: \"%s\"", pt)));
pt = strtok(NULL, ","); pt = strtok(NULL, ",");
} }
if (stringEnd == NULL || *stringEnd != ']') if (*stringEnd != ']')
ereport(ERROR, ereport(ERROR,
(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
errmsg("malformed vector literal: \"%s\"", lit), errmsg("malformed vector literal"),
errdetail("Unexpected end of input."))); errdetail("Unexpected end of input.")));
stringEnd++; if (stringEnd[1] != '\0')
/* Only whitespace is allowed after the closing brace */
while (vector_isspace(*stringEnd))
stringEnd++;
if (*stringEnd != '\0')
ereport(ERROR, ereport(ERROR,
(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
errmsg("malformed vector literal: \"%s\"", lit), errmsg("malformed vector literal"),
errdetail("Junk after closing right brace."))); errdetail("Junk after closing right brace.")));
/* Ensure no consecutive delimiters since strtok skips */
for (pt = lit + 1; *pt != '\0'; pt++)
{
if (pt[-1] == ',' && *pt == ',')
ereport(ERROR,
(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
errmsg("malformed vector literal: \"%s\"", lit)));
}
if (dim < 1) if (dim < 1)
ereport(ERROR, ereport(ERROR,
(errcode(ERRCODE_DATA_EXCEPTION), (errcode(ERRCODE_DATA_EXCEPTION),
errmsg("vector must have at least 1 dimension"))); errmsg("vector must have at least 1 dimension")));
pfree(lit);
CheckExpectedDim(typmod, dim); CheckExpectedDim(typmod, dim);
result = InitVector(dim); result = InitVector(dim);
for (int i = 0; i < dim; i++) for (i = 0; i < dim; i++)
result->x[i] = x[i]; result->x[i] = x[i];
PG_RETURN_POINTER(result); PG_RETURN_POINTER(result);
@@ -290,6 +194,7 @@ vector_out(PG_FUNCTION_ARGS)
int dim = vector->dim; int dim = vector->dim;
char *buf; char *buf;
char *ptr; char *ptr;
int i;
int n; int n;
#if PG_VERSION_NUM < 120000 #if PG_VERSION_NUM < 120000
@@ -316,7 +221,7 @@ vector_out(PG_FUNCTION_ARGS)
*ptr = '['; *ptr = '[';
ptr++; ptr++;
for (int i = 0; i < dim; i++) for (i = 0; i < dim; i++)
{ {
if (i > 0) if (i > 0)
{ {
@@ -339,18 +244,6 @@ vector_out(PG_FUNCTION_ARGS)
PG_RETURN_CSTRING(buf); PG_RETURN_CSTRING(buf);
} }
/*
* Print vector - useful for debugging
*/
void
PrintVector(char *msg, Vector * vector)
{
char *out = DatumGetPointer(DirectFunctionCall1(vector_out, PointerGetDatum(vector)));
elog(INFO, "%s = %s", msg, out);
pfree(out);
}
/* /*
* Convert type modifier * Convert type modifier
*/ */
@@ -394,6 +287,7 @@ vector_recv(PG_FUNCTION_ARGS)
Vector *result; Vector *result;
int16 dim; int16 dim;
int16 unused; int16 unused;
int i;
dim = pq_getmsgint(buf, sizeof(int16)); dim = pq_getmsgint(buf, sizeof(int16));
unused = pq_getmsgint(buf, sizeof(int16)); unused = pq_getmsgint(buf, sizeof(int16));
@@ -407,11 +301,8 @@ vector_recv(PG_FUNCTION_ARGS)
errmsg("expected unused to be 0, not %d", unused))); errmsg("expected unused to be 0, not %d", unused)));
result = InitVector(dim); result = InitVector(dim);
for (int i = 0; i < dim; i++) for (i = 0; i < dim; i++)
{
result->x[i] = pq_getmsgfloat4(buf); result->x[i] = pq_getmsgfloat4(buf);
CheckElement(result->x[i]);
}
PG_RETURN_POINTER(result); PG_RETURN_POINTER(result);
} }
@@ -425,11 +316,12 @@ vector_send(PG_FUNCTION_ARGS)
{ {
Vector *vec = PG_GETARG_VECTOR_P(0); Vector *vec = PG_GETARG_VECTOR_P(0);
StringInfoData buf; StringInfoData buf;
int i;
pq_begintypsend(&buf); pq_begintypsend(&buf);
pq_sendint(&buf, vec->dim, sizeof(int16)); pq_sendint(&buf, vec->dim, sizeof(int16));
pq_sendint(&buf, vec->unused, sizeof(int16)); pq_sendint(&buf, vec->unused, sizeof(int16));
for (int i = 0; i < vec->dim; i++) for (i = 0; i < vec->dim; i++)
pq_sendfloat4(&buf, vec->x[i]); pq_sendfloat4(&buf, vec->x[i]);
PG_RETURN_BYTEA_P(pq_endtypsend(&buf)); PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
@@ -459,6 +351,7 @@ array_to_vector(PG_FUNCTION_ARGS)
{ {
ArrayType *array = PG_GETARG_ARRAYTYPE_P(0); ArrayType *array = PG_GETARG_ARRAYTYPE_P(0);
int32 typmod = PG_GETARG_INT32(1); int32 typmod = PG_GETARG_INT32(1);
int i;
Vector *result; Vector *result;
int16 typlen; int16 typlen;
bool typbyval; bool typbyval;
@@ -472,49 +365,37 @@ array_to_vector(PG_FUNCTION_ARGS)
(errcode(ERRCODE_DATA_EXCEPTION), (errcode(ERRCODE_DATA_EXCEPTION),
errmsg("array must be 1-D"))); errmsg("array must be 1-D")));
if (ARR_HASNULL(array) && array_contains_nulls(array))
ereport(ERROR,
(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
errmsg("array must not contain nulls")));
get_typlenbyvalalign(ARR_ELEMTYPE(array), &typlen, &typbyval, &typalign); get_typlenbyvalalign(ARR_ELEMTYPE(array), &typlen, &typbyval, &typalign);
deconstruct_array(array, ARR_ELEMTYPE(array), typlen, typbyval, typalign, &elemsp, &nullsp, &nelemsp); deconstruct_array(array, ARR_ELEMTYPE(array), typlen, typbyval, typalign, &elemsp, &nullsp, &nelemsp);
CheckDim(nelemsp); if (typmod == -1)
CheckExpectedDim(typmod, nelemsp); CheckDim(nelemsp);
else
CheckExpectedDim(typmod, nelemsp);
result = InitVector(nelemsp); result = InitVector(nelemsp);
for (i = 0; i < nelemsp; i++)
if (ARR_ELEMTYPE(array) == INT4OID)
{ {
for (int i = 0; i < nelemsp; i++) if (nullsp[i])
ereport(ERROR,
(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
errmsg("array must not containing NULLs")));
if (ARR_ELEMTYPE(array) == INT4OID)
result->x[i] = DatumGetInt32(elemsp[i]); result->x[i] = DatumGetInt32(elemsp[i]);
} else if (ARR_ELEMTYPE(array) == FLOAT8OID)
else if (ARR_ELEMTYPE(array) == FLOAT8OID)
{
for (int i = 0; i < nelemsp; i++)
result->x[i] = DatumGetFloat8(elemsp[i]); result->x[i] = DatumGetFloat8(elemsp[i]);
} else if (ARR_ELEMTYPE(array) == FLOAT4OID)
else if (ARR_ELEMTYPE(array) == FLOAT4OID)
{
for (int i = 0; i < nelemsp; i++)
result->x[i] = DatumGetFloat4(elemsp[i]); result->x[i] = DatumGetFloat4(elemsp[i]);
} else if (ARR_ELEMTYPE(array) == NUMERICOID)
else if (ARR_ELEMTYPE(array) == NUMERICOID) result->x[i] = DatumGetFloat4(DirectFunctionCall1(numeric_float4, NumericGetDatum(elemsp[i])));
{ else
for (int i = 0; i < nelemsp; i++) ereport(ERROR,
result->x[i] = DatumGetFloat4(DirectFunctionCall1(numeric_float4, elemsp[i])); (errcode(ERRCODE_DATA_EXCEPTION),
} errmsg("unsupported array type")));
else
{
ereport(ERROR,
(errcode(ERRCODE_DATA_EXCEPTION),
errmsg("unsupported array type")));
}
/* Check elements */
for (int i = 0; i < result->dim; i++)
CheckElement(result->x[i]); CheckElement(result->x[i]);
}
PG_RETURN_POINTER(result); PG_RETURN_POINTER(result);
} }
@@ -527,18 +408,17 @@ Datum
vector_to_float4(PG_FUNCTION_ARGS) vector_to_float4(PG_FUNCTION_ARGS)
{ {
Vector *vec = PG_GETARG_VECTOR_P(0); Vector *vec = PG_GETARG_VECTOR_P(0);
Datum *datums; Datum *d;
ArrayType *result; ArrayType *result;
int i;
datums = (Datum *) palloc(sizeof(Datum) * vec->dim); d = (Datum *) palloc(sizeof(Datum) * vec->dim);
for (int i = 0; i < vec->dim; i++) for (i = 0; i < vec->dim; i++)
datums[i] = Float4GetDatum(vec->x[i]); d[i] = Float4GetDatum(vec->x[i]);
/* Use TYPALIGN_INT for float4 */ /* Use TYPALIGN_INT for float4 */
result = construct_array(datums, vec->dim, FLOAT4OID, sizeof(float4), true, TYPALIGN_INT); result = construct_array(d, vec->dim, FLOAT4OID, sizeof(float4), true, TYPALIGN_INT);
pfree(datums);
PG_RETURN_POINTER(result); PG_RETURN_POINTER(result);
} }
@@ -554,19 +434,18 @@ l2_distance(PG_FUNCTION_ARGS)
Vector *b = PG_GETARG_VECTOR_P(1); Vector *b = PG_GETARG_VECTOR_P(1);
float *ax = a->x; float *ax = a->x;
float *bx = b->x; float *bx = b->x;
float distance = 0.0; double distance = 0.0;
float diff; double diff;
CheckDims(a, b); CheckDims(a, b);
/* Auto-vectorized */
for (int i = 0; i < a->dim; i++) for (int i = 0; i < a->dim; i++)
{ {
diff = ax[i] - bx[i]; diff = ax[i] - bx[i];
distance += diff * diff; distance += diff * diff;
} }
PG_RETURN_FLOAT8(sqrt((double) distance)); PG_RETURN_FLOAT8(sqrt(distance));
} }
/* /*
@@ -581,19 +460,18 @@ vector_l2_squared_distance(PG_FUNCTION_ARGS)
Vector *b = PG_GETARG_VECTOR_P(1); Vector *b = PG_GETARG_VECTOR_P(1);
float *ax = a->x; float *ax = a->x;
float *bx = b->x; float *bx = b->x;
float distance = 0.0; double distance = 0.0;
float diff; double diff;
CheckDims(a, b); CheckDims(a, b);
/* Auto-vectorized */
for (int i = 0; i < a->dim; i++) for (int i = 0; i < a->dim; i++)
{ {
diff = ax[i] - bx[i]; diff = ax[i] - bx[i];
distance += diff * diff; distance += diff * diff;
} }
PG_RETURN_FLOAT8((double) distance); PG_RETURN_FLOAT8(distance);
} }
/* /*
@@ -607,15 +485,14 @@ inner_product(PG_FUNCTION_ARGS)
Vector *b = PG_GETARG_VECTOR_P(1); Vector *b = PG_GETARG_VECTOR_P(1);
float *ax = a->x; float *ax = a->x;
float *bx = b->x; float *bx = b->x;
float distance = 0.0; double distance = 0.0;
CheckDims(a, b); CheckDims(a, b);
/* Auto-vectorized */
for (int i = 0; i < a->dim; i++) for (int i = 0; i < a->dim; i++)
distance += ax[i] * bx[i]; distance += ax[i] * bx[i];
PG_RETURN_FLOAT8((double) distance); PG_RETURN_FLOAT8(distance);
} }
/* /*
@@ -629,15 +506,14 @@ vector_negative_inner_product(PG_FUNCTION_ARGS)
Vector *b = PG_GETARG_VECTOR_P(1); Vector *b = PG_GETARG_VECTOR_P(1);
float *ax = a->x; float *ax = a->x;
float *bx = b->x; float *bx = b->x;
float distance = 0.0; double distance = 0.0;
CheckDims(a, b); CheckDims(a, b);
/* Auto-vectorized */
for (int i = 0; i < a->dim; i++) for (int i = 0; i < a->dim; i++)
distance += ax[i] * bx[i]; distance += ax[i] * bx[i];
PG_RETURN_FLOAT8((double) distance * -1); PG_RETURN_FLOAT8(distance * -1);
} }
/* /*
@@ -651,14 +527,12 @@ cosine_distance(PG_FUNCTION_ARGS)
Vector *b = PG_GETARG_VECTOR_P(1); Vector *b = PG_GETARG_VECTOR_P(1);
float *ax = a->x; float *ax = a->x;
float *bx = b->x; float *bx = b->x;
float distance = 0.0; double distance = 0.0;
float norma = 0.0; double norma = 0.0;
float normb = 0.0; double normb = 0.0;
double similarity;
CheckDims(a, b); CheckDims(a, b);
/* Auto-vectorized */
for (int i = 0; i < a->dim; i++) for (int i = 0; i < a->dim; i++)
{ {
distance += ax[i] * bx[i]; distance += ax[i] * bx[i];
@@ -666,22 +540,7 @@ cosine_distance(PG_FUNCTION_ARGS)
normb += bx[i] * bx[i]; normb += bx[i] * bx[i];
} }
/* Use sqrt(a * b) over sqrt(a) * sqrt(b) */ PG_RETURN_FLOAT8(1 - (distance / (sqrt(norma) * sqrt(normb))));
similarity = (double) distance / sqrt((double) norma * (double) normb);
#ifdef _MSC_VER
/* /fp:fast may not propagate NaN */
if (isnan(similarity))
PG_RETURN_FLOAT8(NAN);
#endif
/* Keep in range */
if (similarity > 1)
similarity = 1.0;
else if (similarity < -1)
similarity = -1.0;
PG_RETURN_FLOAT8(1.0 - similarity);
} }
/* /*
@@ -695,18 +554,12 @@ vector_spherical_distance(PG_FUNCTION_ARGS)
{ {
Vector *a = PG_GETARG_VECTOR_P(0); Vector *a = PG_GETARG_VECTOR_P(0);
Vector *b = PG_GETARG_VECTOR_P(1); Vector *b = PG_GETARG_VECTOR_P(1);
float *ax = a->x; double distance = 0.0;
float *bx = b->x;
float dp = 0.0;
double distance;
CheckDims(a, b); CheckDims(a, b);
/* Auto-vectorized */
for (int i = 0; i < a->dim; i++) for (int i = 0; i < a->dim; i++)
dp += ax[i] * bx[i]; distance += a->x[i] * b->x[i];
distance = (double) dp;
/* Prevent NaN with acos with loss of precision */ /* Prevent NaN with acos with loss of precision */
if (distance > 1) if (distance > 1)
@@ -717,28 +570,6 @@ vector_spherical_distance(PG_FUNCTION_ARGS)
PG_RETURN_FLOAT8(acos(distance) / M_PI); PG_RETURN_FLOAT8(acos(distance) / M_PI);
} }
/*
* Get the L1 distance between vectors
*/
PGDLLEXPORT PG_FUNCTION_INFO_V1(l1_distance);
Datum
l1_distance(PG_FUNCTION_ARGS)
{
Vector *a = PG_GETARG_VECTOR_P(0);
Vector *b = PG_GETARG_VECTOR_P(1);
float *ax = a->x;
float *bx = b->x;
float distance = 0.0;
CheckDims(a, b);
/* Auto-vectorized */
for (int i = 0; i < a->dim; i++)
distance += fabsf(ax[i] - bx[i]);
PG_RETURN_FLOAT8((double) distance);
}
/* /*
* Get the dimensions of a vector * Get the dimensions of a vector
*/ */
@@ -762,9 +593,8 @@ vector_norm(PG_FUNCTION_ARGS)
float *ax = a->x; float *ax = a->x;
double norm = 0.0; double norm = 0.0;
/* Auto-vectorized */
for (int i = 0; i < a->dim; i++) for (int i = 0; i < a->dim; i++)
norm += (double) ax[i] * (double) ax[i]; norm += ax[i] * ax[i];
PG_RETURN_FLOAT8(sqrt(norm)); PG_RETURN_FLOAT8(sqrt(norm));
} }
@@ -787,18 +617,9 @@ vector_add(PG_FUNCTION_ARGS)
result = InitVector(a->dim); result = InitVector(a->dim);
rx = result->x; rx = result->x;
/* Auto-vectorized */
for (int i = 0, imax = a->dim; i < imax; i++) for (int i = 0, imax = a->dim; i < imax; i++)
rx[i] = ax[i] + bx[i]; rx[i] = ax[i] + bx[i];
/* Check for overflow */
for (int i = 0, imax = a->dim; i < imax; i++)
{
if (isinf(rx[i]))
float_overflow_error();
}
PG_RETURN_POINTER(result); PG_RETURN_POINTER(result);
} }
@@ -820,54 +641,9 @@ vector_sub(PG_FUNCTION_ARGS)
result = InitVector(a->dim); result = InitVector(a->dim);
rx = result->x; rx = result->x;
/* Auto-vectorized */
for (int i = 0, imax = a->dim; i < imax; i++) for (int i = 0, imax = a->dim; i < imax; i++)
rx[i] = ax[i] - bx[i]; rx[i] = ax[i] - bx[i];
/* Check for overflow */
for (int i = 0, imax = a->dim; i < imax; i++)
{
if (isinf(rx[i]))
float_overflow_error();
}
PG_RETURN_POINTER(result);
}
/*
* Multiply vectors
*/
PGDLLEXPORT PG_FUNCTION_INFO_V1(vector_mul);
Datum
vector_mul(PG_FUNCTION_ARGS)
{
Vector *a = PG_GETARG_VECTOR_P(0);
Vector *b = PG_GETARG_VECTOR_P(1);
float *ax = a->x;
float *bx = b->x;
Vector *result;
float *rx;
CheckDims(a, b);
result = InitVector(a->dim);
rx = result->x;
/* Auto-vectorized */
for (int i = 0, imax = a->dim; i < imax; i++)
rx[i] = ax[i] * bx[i];
/* Check for overflow and underflow */
for (int i = 0, imax = a->dim; i < imax; i++)
{
if (isinf(rx[i]))
float_overflow_error();
if (rx[i] == 0 && !(ax[i] == 0 || bx[i] == 0))
float_underflow_error();
}
PG_RETURN_POINTER(result); PG_RETURN_POINTER(result);
} }
@@ -877,9 +653,11 @@ vector_mul(PG_FUNCTION_ARGS)
int int
vector_cmp_internal(Vector * a, Vector * b) vector_cmp_internal(Vector * a, Vector * b)
{ {
int i;
CheckDims(a, b); CheckDims(a, b);
for (int i = 0; i < a->dim; i++) for (i = 0; i < a->dim; i++)
{ {
if (a->x[i] < b->x[i]) if (a->x[i] < b->x[i])
return -1; return -1;
@@ -980,168 +758,3 @@ vector_cmp(PG_FUNCTION_ARGS)
PG_RETURN_INT32(vector_cmp_internal(a, b)); PG_RETURN_INT32(vector_cmp_internal(a, b));
} }
/*
* Accumulate vectors
*/
PGDLLEXPORT PG_FUNCTION_INFO_V1(vector_accum);
Datum
vector_accum(PG_FUNCTION_ARGS)
{
ArrayType *statearray = PG_GETARG_ARRAYTYPE_P(0);
Vector *newval = PG_GETARG_VECTOR_P(1);
float8 *statevalues;
int16 dim;
bool newarr;
float8 n;
Datum *statedatums;
float *x = newval->x;
ArrayType *result;
/* Check array before using */
statevalues = CheckStateArray(statearray, "vector_accum");
dim = STATE_DIMS(statearray);
newarr = dim == 0;
if (newarr)
dim = newval->dim;
else
CheckExpectedDim(dim, newval->dim);
n = statevalues[0] + 1.0;
statedatums = CreateStateDatums(dim);
statedatums[0] = Float8GetDatum(n);
if (newarr)
{
for (int i = 0; i < dim; i++)
statedatums[i + 1] = Float8GetDatum((double) x[i]);
}
else
{
for (int i = 0; i < dim; i++)
{
double v = statevalues[i + 1] + x[i];
/* Check for overflow */
if (isinf(v))
float_overflow_error();
statedatums[i + 1] = Float8GetDatum(v);
}
}
/* Use float8 array like float4_accum */
result = construct_array(statedatums, dim + 1,
FLOAT8OID,
sizeof(float8), FLOAT8PASSBYVAL, TYPALIGN_DOUBLE);
pfree(statedatums);
PG_RETURN_ARRAYTYPE_P(result);
}
/*
* Combine vectors
*/
PGDLLEXPORT PG_FUNCTION_INFO_V1(vector_combine);
Datum
vector_combine(PG_FUNCTION_ARGS)
{
ArrayType *statearray1 = PG_GETARG_ARRAYTYPE_P(0);
ArrayType *statearray2 = PG_GETARG_ARRAYTYPE_P(1);
float8 *statevalues1;
float8 *statevalues2;
float8 n;
float8 n1;
float8 n2;
int16 dim;
Datum *statedatums;
ArrayType *result;
/* Check arrays before using */
statevalues1 = CheckStateArray(statearray1, "vector_combine");
statevalues2 = CheckStateArray(statearray2, "vector_combine");
n1 = statevalues1[0];
n2 = statevalues2[0];
if (n1 == 0.0)
{
n = n2;
dim = STATE_DIMS(statearray2);
statedatums = CreateStateDatums(dim);
for (int i = 1; i <= dim; i++)
statedatums[i] = Float8GetDatum(statevalues2[i]);
}
else if (n2 == 0.0)
{
n = n1;
dim = STATE_DIMS(statearray1);
statedatums = CreateStateDatums(dim);
for (int i = 1; i <= dim; i++)
statedatums[i] = Float8GetDatum(statevalues1[i]);
}
else
{
n = n1 + n2;
dim = STATE_DIMS(statearray1);
CheckExpectedDim(dim, STATE_DIMS(statearray2));
statedatums = CreateStateDatums(dim);
for (int i = 1; i <= dim; i++)
{
double v = statevalues1[i] + statevalues2[i];
/* Check for overflow */
if (isinf(v))
float_overflow_error();
statedatums[i] = Float8GetDatum(v);
}
}
statedatums[0] = Float8GetDatum(n);
result = construct_array(statedatums, dim + 1,
FLOAT8OID,
sizeof(float8), FLOAT8PASSBYVAL, TYPALIGN_DOUBLE);
pfree(statedatums);
PG_RETURN_ARRAYTYPE_P(result);
}
/*
* Average vectors
*/
PGDLLEXPORT PG_FUNCTION_INFO_V1(vector_avg);
Datum
vector_avg(PG_FUNCTION_ARGS)
{
ArrayType *statearray = PG_GETARG_ARRAYTYPE_P(0);
float8 *statevalues;
float8 n;
uint16 dim;
Vector *result;
/* Check array before using */
statevalues = CheckStateArray(statearray, "vector_avg");
n = statevalues[0];
/* SQL defines AVG of no values to be NULL */
if (n == 0.0)
PG_RETURN_NULL();
/* Create vector */
dim = STATE_DIMS(statearray);
CheckDim(dim);
result = InitVector(dim);
for (int i = 0; i < dim; i++)
{
result->x[i] = statevalues[i + 1] / n;
CheckElement(result->x[i]);
}
PG_RETURN_POINTER(result);
}

View File

@@ -1,7 +1,9 @@
#ifndef VECTOR_H #ifndef VECTOR_H
#define VECTOR_H #define VECTOR_H
#define VECTOR_MAX_DIM 16000 #include "postgres.h"
#define VECTOR_MAX_DIM 1024
#define VECTOR_SIZE(_dim) (offsetof(Vector, x) + sizeof(float)*(_dim)) #define VECTOR_SIZE(_dim) (offsetof(Vector, x) + sizeof(float)*(_dim))
#define DatumGetVector(x) ((Vector *) PG_DETOAST_DATUM(x)) #define DatumGetVector(x) ((Vector *) PG_DETOAST_DATUM(x))
@@ -16,8 +18,24 @@ typedef struct Vector
float x[FLEXIBLE_ARRAY_MEMBER]; float x[FLEXIBLE_ARRAY_MEMBER];
} Vector; } Vector;
Vector *InitVector(int dim);
void PrintVector(char *msg, Vector * vector); void PrintVector(char *msg, Vector * vector);
int vector_cmp_internal(Vector * a, Vector * b); int vector_cmp_internal(Vector * a, Vector * b);
/*
* Allocate and initialize a new vector
*/
static inline Vector *
InitVector(int dim)
{
Vector *result;
int size;
size = VECTOR_SIZE(dim);
result = (Vector *) palloc0(size);
SET_VARSIZE(result, size);
result->dim = dim;
return result;
}
#endif #endif

View File

@@ -22,14 +22,8 @@ SELECT ARRAY[1,2,3]::float8[]::vector;
[1,2,3] [1,2,3]
(1 row) (1 row)
SELECT ARRAY[1,2,3]::numeric[]::vector;
array
---------
[1,2,3]
(1 row)
SELECT '{NULL}'::real[]::vector; SELECT '{NULL}'::real[]::vector;
ERROR: array must not contain nulls ERROR: array must not containing NULLs
SELECT '{NaN}'::real[]::vector; SELECT '{NaN}'::real[]::vector;
ERROR: NaN not allowed in vector ERROR: NaN not allowed in vector
SELECT '{Infinity}'::real[]::vector; SELECT '{Infinity}'::real[]::vector;
@@ -38,18 +32,14 @@ SELECT '{-Infinity}'::real[]::vector;
ERROR: infinite value not allowed in vector ERROR: infinite value not allowed in vector
SELECT '{}'::real[]::vector; SELECT '{}'::real[]::vector;
ERROR: vector must have at least 1 dimension ERROR: vector must have at least 1 dimension
SELECT '{{1}}'::real[]::vector;
ERROR: array must be 1-D
SELECT '[1,2,3]'::vector::real[]; SELECT '[1,2,3]'::vector::real[];
float4 float4
--------- ---------
{1,2,3} {1,2,3}
(1 row) (1 row)
SELECT array_agg(n)::vector FROM generate_series(1, 16001) n; SELECT array_agg(n)::vector FROM generate_series(1, 1025) n;
ERROR: vector cannot have more than 16000 dimensions ERROR: vector cannot have more than 1024 dimensions
SELECT array_to_vector(array_agg(n), 16001, false) FROM generate_series(1, 16001) n;
ERROR: vector cannot have more than 16000 dimensions
-- ensure no error -- ensure no error
SELECT ARRAY[1,2,3] = ARRAY[1,2,3]; SELECT ARRAY[1,2,3] = ARRAY[1,2,3];
?column? ?column?

View File

@@ -4,26 +4,12 @@ SELECT '[1,2,3]'::vector + '[4,5,6]';
[5,7,9] [5,7,9]
(1 row) (1 row)
SELECT '[3e38]'::vector + '[3e38]';
ERROR: value out of range: overflow
SELECT '[1,2,3]'::vector - '[4,5,6]'; SELECT '[1,2,3]'::vector - '[4,5,6]';
?column? ?column?
------------ ------------
[-3,-3,-3] [-3,-3,-3]
(1 row) (1 row)
SELECT '[-3e38]'::vector - '[3e38]';
ERROR: value out of range: overflow
SELECT '[1,2,3]'::vector * '[4,5,6]';
?column?
-----------
[4,10,18]
(1 row)
SELECT '[1e37]'::vector * '[1e37]';
ERROR: value out of range: overflow
SELECT '[1e-37]'::vector * '[1e-37]';
ERROR: value out of range: underflow
SELECT vector_dims('[1,2,3]'); SELECT vector_dims('[1,2,3]');
vector_dims vector_dims
------------- -------------
@@ -36,44 +22,14 @@ SELECT round(vector_norm('[1,1]')::numeric, 5);
1.41421 1.41421
(1 row) (1 row)
SELECT vector_norm('[3,4]'); SELECT round(l2_distance('[1,2]', '[0,0]')::numeric, 5);
vector_norm round
------------- ---------
5 2.23607
(1 row)
SELECT vector_norm('[0,1]');
vector_norm
-------------
1
(1 row)
SELECT vector_norm('[3e37,4e37]')::real;
vector_norm
-------------
5e+37
(1 row)
SELECT l2_distance('[0,0]', '[3,4]');
l2_distance
-------------
5
(1 row)
SELECT l2_distance('[0,0]', '[0,1]');
l2_distance
-------------
1
(1 row) (1 row)
SELECT l2_distance('[1,2]', '[3]'); SELECT l2_distance('[1,2]', '[3]');
ERROR: different vector dimensions 2 and 1 ERROR: different vector dimensions 2 and 1
SELECT l2_distance('[3e38]', '[-3e38]');
l2_distance
-------------
Infinity
(1 row)
SELECT inner_product('[1,2]', '[3,4]'); SELECT inner_product('[1,2]', '[3,4]');
inner_product inner_product
--------------- ---------------
@@ -82,16 +38,10 @@ SELECT inner_product('[1,2]', '[3,4]');
SELECT inner_product('[1,2]', '[3]'); SELECT inner_product('[1,2]', '[3]');
ERROR: different vector dimensions 2 and 1 ERROR: different vector dimensions 2 and 1
SELECT inner_product('[3e38]', '[3e38]'); SELECT round(cosine_distance('[1,2]', '[2,4]')::numeric, 5);
inner_product round
--------------- ---------
Infinity 0.00000
(1 row)
SELECT cosine_distance('[1,2]', '[2,4]');
cosine_distance
-----------------
0
(1 row) (1 row)
SELECT cosine_distance('[1,2]', '[0,0]'); SELECT cosine_distance('[1,2]', '[0,0]');
@@ -100,111 +50,5 @@ SELECT cosine_distance('[1,2]', '[0,0]');
NaN NaN
(1 row) (1 row)
SELECT cosine_distance('[1,1]', '[1,1]');
cosine_distance
-----------------
0
(1 row)
SELECT cosine_distance('[1,0]', '[0,2]');
cosine_distance
-----------------
1
(1 row)
SELECT cosine_distance('[1,1]', '[-1,-1]');
cosine_distance
-----------------
2
(1 row)
SELECT cosine_distance('[1,2]', '[3]'); SELECT cosine_distance('[1,2]', '[3]');
ERROR: different vector dimensions 2 and 1 ERROR: different vector dimensions 2 and 1
SELECT cosine_distance('[1,1]', '[1.1,1.1]');
cosine_distance
-----------------
0
(1 row)
SELECT cosine_distance('[1,1]', '[-1.1,-1.1]');
cosine_distance
-----------------
2
(1 row)
SELECT cosine_distance('[3e38]', '[3e38]');
cosine_distance
-----------------
NaN
(1 row)
SELECT l1_distance('[0,0]', '[3,4]');
l1_distance
-------------
7
(1 row)
SELECT l1_distance('[0,0]', '[0,1]');
l1_distance
-------------
1
(1 row)
SELECT l1_distance('[1,2]', '[3]');
ERROR: different vector dimensions 2 and 1
SELECT l1_distance('[3e38]', '[-3e38]');
l1_distance
-------------
Infinity
(1 row)
SELECT avg(v) FROM unnest(ARRAY['[1,2,3]'::vector, '[3,5,7]']) v;
avg
-----------
[2,3.5,5]
(1 row)
SELECT avg(v) FROM unnest(ARRAY['[1,2,3]'::vector, '[3,5,7]', NULL]) v;
avg
-----------
[2,3.5,5]
(1 row)
SELECT avg(v) FROM unnest(ARRAY[]::vector[]) v;
avg
-----
(1 row)
SELECT avg(v) FROM unnest(ARRAY['[1,2]'::vector, '[3]']) v;
ERROR: expected 2 dimensions, not 1
SELECT avg(v) FROM unnest(ARRAY['[3e38]'::vector, '[3e38]']) v;
avg
---------
[3e+38]
(1 row)
SELECT vector_avg(array_agg(n)) FROM generate_series(1, 16002) n;
ERROR: vector cannot have more than 16000 dimensions
SELECT sum(v) FROM unnest(ARRAY['[1,2,3]'::vector, '[3,5,7]']) v;
sum
----------
[4,7,10]
(1 row)
SELECT sum(v) FROM unnest(ARRAY['[1,2,3]'::vector, '[3,5,7]', NULL]) v;
sum
----------
[4,7,10]
(1 row)
SELECT sum(v) FROM unnest(ARRAY[]::vector[]) v;
sum
-----
(1 row)
SELECT sum(v) FROM unnest(ARRAY['[1,2]'::vector, '[3]']) v;
ERROR: different vector dimensions 2 and 1
SELECT sum(v) FROM unnest(ARRAY['[3e38]'::vector, '[3e38]']) v;
ERROR: value out of range: overflow

View File

@@ -1,26 +0,0 @@
SET enable_seqscan = off;
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_cosine_ops);
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::vector)) t2;
count
-------
3
(1 row)
DROP TABLE t;

View File

@@ -1,21 +0,0 @@
SET enable_seqscan = off;
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_ip_ops);
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::vector)) t2;
count
-------
4
(1 row)
DROP TABLE t;

View File

@@ -1,36 +0,0 @@
SET enable_seqscan = off;
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);
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 * FROM t ORDER BY val <-> (SELECT NULL::vector);
val
---------
[0,0,0]
[1,1,1]
[1,2,3]
[1,2,4]
(4 rows)
SELECT COUNT(*) FROM t;
count
-------
5
(1 row)
TRUNCATE t;
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
val
-----
(0 rows)
DROP TABLE t;

View File

@@ -1,26 +0,0 @@
CREATE TABLE t (val vector(3));
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (m = 1);
ERROR: value 1 out of bounds for option "m"
DETAIL: Valid values are between "2" and "100".
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (m = 101);
ERROR: value 101 out of bounds for option "m"
DETAIL: Valid values are between "2" and "100".
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (ef_construction = 3);
ERROR: value 3 out of bounds for option "ef_construction"
DETAIL: Valid values are between "4" and "1000".
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (ef_construction = 1001);
ERROR: value 1001 out of bounds for option "ef_construction"
DETAIL: Valid values are between "4" and "1000".
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (m = 16, ef_construction = 31);
ERROR: ef_construction must be greater than or equal to 2 * m
SHOW hnsw.ef_search;
hnsw.ef_search
----------------
40
(1 row)
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)
DROP TABLE t;

View File

@@ -1,13 +0,0 @@
SET enable_seqscan = off;
CREATE UNLOGGED 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);
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
val
---------
[1,2,3]
[1,1,1]
[0,0,0]
(3 rows)
DROP TABLE t;

View File

@@ -4,22 +4,10 @@ SELECT '[1,2,3]'::vector;
[1,2,3] [1,2,3]
(1 row) (1 row)
SELECT '[-1,-2,-3]'::vector; SELECT '[-1,2,3]'::vector;
vector vector
------------ ----------
[-1,-2,-3] [-1,2,3]
(1 row)
SELECT '[1.,2.,3.]'::vector;
vector
---------
[1,2,3]
(1 row)
SELECT ' [ 1, 2 , 3 ] '::vector;
vector
---------
[1,2,3]
(1 row) (1 row)
SELECT '[1.23456]'::vector; SELECT '[1.23456]'::vector;
@@ -29,7 +17,7 @@ SELECT '[1.23456]'::vector;
(1 row) (1 row)
SELECT '[hello,1]'::vector; SELECT '[hello,1]'::vector;
ERROR: invalid input syntax for type vector: "[hello,1]" ERROR: invalid input syntax for type vector: "hello"
LINE 1: SELECT '[hello,1]'::vector; LINE 1: SELECT '[hello,1]'::vector;
^ ^
SELECT '[NaN,1]'::vector; SELECT '[NaN,1]'::vector;
@@ -44,35 +32,13 @@ SELECT '[-Infinity,1]'::vector;
ERROR: infinite value not allowed in vector ERROR: infinite value not allowed in vector
LINE 1: SELECT '[-Infinity,1]'::vector; LINE 1: SELECT '[-Infinity,1]'::vector;
^ ^
SELECT '[1.5e38,-1.5e38]'::vector;
vector
--------------------
[1.5e+38,-1.5e+38]
(1 row)
SELECT '[1.5e+38,-1.5e+38]'::vector;
vector
--------------------
[1.5e+38,-1.5e+38]
(1 row)
SELECT '[1.5e-38,-1.5e-38]'::vector;
vector
--------------------
[1.5e-38,-1.5e-38]
(1 row)
SELECT '[4e38,1]'::vector;
ERROR: infinite value not allowed in vector
LINE 1: SELECT '[4e38,1]'::vector;
^
SELECT '[1,2,3'::vector; SELECT '[1,2,3'::vector;
ERROR: malformed vector literal: "[1,2,3" ERROR: malformed vector literal
LINE 1: SELECT '[1,2,3'::vector; LINE 1: SELECT '[1,2,3'::vector;
^ ^
DETAIL: Unexpected end of input. DETAIL: Unexpected end of input.
SELECT '[1,2,3]9'::vector; SELECT '[1,2,3]9'::vector;
ERROR: malformed vector literal: "[1,2,3]9" ERROR: malformed vector literal
LINE 1: SELECT '[1,2,3]9'::vector; LINE 1: SELECT '[1,2,3]9'::vector;
^ ^
DETAIL: Junk after closing right brace. DETAIL: Junk after closing right brace.
@@ -81,41 +47,14 @@ ERROR: malformed vector literal: "1,2,3"
LINE 1: SELECT '1,2,3'::vector; LINE 1: SELECT '1,2,3'::vector;
^ ^
DETAIL: Vector contents must start with "[". DETAIL: Vector contents must start with "[".
SELECT ''::vector;
ERROR: malformed vector literal: ""
LINE 1: SELECT ''::vector;
^
DETAIL: Vector contents must start with "[".
SELECT '['::vector;
ERROR: malformed vector literal: "["
LINE 1: SELECT '['::vector;
^
DETAIL: Unexpected end of input.
SELECT '[,'::vector;
ERROR: malformed vector literal: "[,"
LINE 1: SELECT '[,'::vector;
^
DETAIL: Unexpected end of input.
SELECT '[]'::vector; SELECT '[]'::vector;
ERROR: vector must have at least 1 dimension ERROR: vector must have at least 1 dimension
LINE 1: SELECT '[]'::vector; LINE 1: SELECT '[]'::vector;
^ ^
SELECT '[1,]'::vector; SELECT '[1,]'::vector;
ERROR: invalid input syntax for type vector: "[1,]" ERROR: invalid input syntax for type vector: "]"
LINE 1: SELECT '[1,]'::vector; LINE 1: SELECT '[1,]'::vector;
^ ^
SELECT '[1a]'::vector;
ERROR: invalid input syntax for type vector: "[1a]"
LINE 1: SELECT '[1a]'::vector;
^
SELECT '[1,,3]'::vector;
ERROR: malformed vector literal: "[1,,3]"
LINE 1: SELECT '[1,,3]'::vector;
^
SELECT '[1, ,3]'::vector;
ERROR: invalid input syntax for type vector: "[1, ,3]"
LINE 1: SELECT '[1, ,3]'::vector;
^
SELECT '[1,2,3]'::vector(2); SELECT '[1,2,3]'::vector(2);
ERROR: expected 2 dimensions, not 3 ERROR: expected 2 dimensions, not 3
SELECT unnest('{"[1,2,3]", "[4,5,6]"}'::vector[]); SELECT unnest('{"[1,2,3]", "[4,5,6]"}'::vector[]);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,16 +2,13 @@ SELECT ARRAY[1,2,3]::vector;
SELECT ARRAY[1.0,2.0,3.0]::vector; SELECT ARRAY[1.0,2.0,3.0]::vector;
SELECT ARRAY[1,2,3]::float4[]::vector; SELECT ARRAY[1,2,3]::float4[]::vector;
SELECT ARRAY[1,2,3]::float8[]::vector; SELECT ARRAY[1,2,3]::float8[]::vector;
SELECT ARRAY[1,2,3]::numeric[]::vector;
SELECT '{NULL}'::real[]::vector; SELECT '{NULL}'::real[]::vector;
SELECT '{NaN}'::real[]::vector; SELECT '{NaN}'::real[]::vector;
SELECT '{Infinity}'::real[]::vector; SELECT '{Infinity}'::real[]::vector;
SELECT '{-Infinity}'::real[]::vector; SELECT '{-Infinity}'::real[]::vector;
SELECT '{}'::real[]::vector; SELECT '{}'::real[]::vector;
SELECT '{{1}}'::real[]::vector;
SELECT '[1,2,3]'::vector::real[]; SELECT '[1,2,3]'::vector::real[];
SELECT array_agg(n)::vector FROM generate_series(1, 16001) n; SELECT array_agg(n)::vector FROM generate_series(1, 1025) n;
SELECT array_to_vector(array_agg(n), 16001, false) FROM generate_series(1, 16001) n;
-- ensure no error -- ensure no error
SELECT ARRAY[1,2,3] = ARRAY[1,2,3]; SELECT ARRAY[1,2,3] = ARRAY[1,2,3];

View File

@@ -1,51 +1,15 @@
SELECT '[1,2,3]'::vector + '[4,5,6]'; SELECT '[1,2,3]'::vector + '[4,5,6]';
SELECT '[3e38]'::vector + '[3e38]';
SELECT '[1,2,3]'::vector - '[4,5,6]'; SELECT '[1,2,3]'::vector - '[4,5,6]';
SELECT '[-3e38]'::vector - '[3e38]';
SELECT '[1,2,3]'::vector * '[4,5,6]';
SELECT '[1e37]'::vector * '[1e37]';
SELECT '[1e-37]'::vector * '[1e-37]';
SELECT vector_dims('[1,2,3]'); SELECT vector_dims('[1,2,3]');
SELECT round(vector_norm('[1,1]')::numeric, 5); SELECT round(vector_norm('[1,1]')::numeric, 5);
SELECT vector_norm('[3,4]');
SELECT vector_norm('[0,1]');
SELECT vector_norm('[3e37,4e37]')::real;
SELECT l2_distance('[0,0]', '[3,4]'); SELECT round(l2_distance('[1,2]', '[0,0]')::numeric, 5);
SELECT l2_distance('[0,0]', '[0,1]');
SELECT l2_distance('[1,2]', '[3]'); SELECT l2_distance('[1,2]', '[3]');
SELECT l2_distance('[3e38]', '[-3e38]');
SELECT inner_product('[1,2]', '[3,4]'); SELECT inner_product('[1,2]', '[3,4]');
SELECT inner_product('[1,2]', '[3]'); SELECT inner_product('[1,2]', '[3]');
SELECT inner_product('[3e38]', '[3e38]');
SELECT cosine_distance('[1,2]', '[2,4]'); SELECT round(cosine_distance('[1,2]', '[2,4]')::numeric, 5);
SELECT cosine_distance('[1,2]', '[0,0]'); SELECT cosine_distance('[1,2]', '[0,0]');
SELECT cosine_distance('[1,1]', '[1,1]');
SELECT cosine_distance('[1,0]', '[0,2]');
SELECT cosine_distance('[1,1]', '[-1,-1]');
SELECT cosine_distance('[1,2]', '[3]'); SELECT cosine_distance('[1,2]', '[3]');
SELECT cosine_distance('[1,1]', '[1.1,1.1]');
SELECT cosine_distance('[1,1]', '[-1.1,-1.1]');
SELECT cosine_distance('[3e38]', '[3e38]');
SELECT l1_distance('[0,0]', '[3,4]');
SELECT l1_distance('[0,0]', '[0,1]');
SELECT l1_distance('[1,2]', '[3]');
SELECT l1_distance('[3e38]', '[-3e38]');
SELECT avg(v) FROM unnest(ARRAY['[1,2,3]'::vector, '[3,5,7]']) v;
SELECT avg(v) FROM unnest(ARRAY['[1,2,3]'::vector, '[3,5,7]', NULL]) v;
SELECT avg(v) FROM unnest(ARRAY[]::vector[]) v;
SELECT avg(v) FROM unnest(ARRAY['[1,2]'::vector, '[3]']) v;
SELECT avg(v) FROM unnest(ARRAY['[3e38]'::vector, '[3e38]']) v;
SELECT vector_avg(array_agg(n)) FROM generate_series(1, 16002) n;
SELECT sum(v) FROM unnest(ARRAY['[1,2,3]'::vector, '[3,5,7]']) v;
SELECT sum(v) FROM unnest(ARRAY['[1,2,3]'::vector, '[3,5,7]', NULL]) v;
SELECT sum(v) FROM unnest(ARRAY[]::vector[]) v;
SELECT sum(v) FROM unnest(ARRAY['[1,2]'::vector, '[3]']) v;
SELECT sum(v) FROM unnest(ARRAY['[3e38]'::vector, '[3e38]']) v;

View File

@@ -1,13 +0,0 @@
SET enable_seqscan = off;
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_cosine_ops);
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::vector)) t2;
DROP TABLE t;

View File

@@ -1,12 +0,0 @@
SET enable_seqscan = off;
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_ip_ops);
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::vector)) t2;
DROP TABLE t;

View File

@@ -1,16 +0,0 @@
SET enable_seqscan = off;
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);
INSERT INTO t (val) VALUES ('[1,2,4]');
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
SELECT * FROM t ORDER BY val <-> (SELECT NULL::vector);
SELECT COUNT(*) FROM t;
TRUNCATE t;
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
DROP TABLE t;

View File

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

View File

@@ -1,9 +0,0 @@
SET enable_seqscan = off;
CREATE UNLOGGED 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);
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
DROP TABLE t;

View File

@@ -1,27 +1,15 @@
SELECT '[1,2,3]'::vector; SELECT '[1,2,3]'::vector;
SELECT '[-1,-2,-3]'::vector; SELECT '[-1,2,3]'::vector;
SELECT '[1.,2.,3.]'::vector;
SELECT ' [ 1, 2 , 3 ] '::vector;
SELECT '[1.23456]'::vector; SELECT '[1.23456]'::vector;
SELECT '[hello,1]'::vector; SELECT '[hello,1]'::vector;
SELECT '[NaN,1]'::vector; SELECT '[NaN,1]'::vector;
SELECT '[Infinity,1]'::vector; SELECT '[Infinity,1]'::vector;
SELECT '[-Infinity,1]'::vector; SELECT '[-Infinity,1]'::vector;
SELECT '[1.5e38,-1.5e38]'::vector;
SELECT '[1.5e+38,-1.5e+38]'::vector;
SELECT '[1.5e-38,-1.5e-38]'::vector;
SELECT '[4e38,1]'::vector;
SELECT '[1,2,3'::vector; SELECT '[1,2,3'::vector;
SELECT '[1,2,3]9'::vector; SELECT '[1,2,3]9'::vector;
SELECT '1,2,3'::vector; SELECT '1,2,3'::vector;
SELECT ''::vector;
SELECT '['::vector;
SELECT '[,'::vector;
SELECT '[]'::vector; SELECT '[]'::vector;
SELECT '[1,]'::vector; SELECT '[1,]'::vector;
SELECT '[1a]'::vector;
SELECT '[1,,3]'::vector;
SELECT '[1, ,3]'::vector;
SELECT '[1,2,3]'::vector(2); SELECT '[1,2,3]'::vector(2);
SELECT unnest('{"[1,2,3]", "[4,5,6]"}'::vector[]); SELECT unnest('{"[1,2,3]", "[4,5,6]"}'::vector[]);

View File

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

View File

@@ -7,6 +7,6 @@ CREATE INDEX ON t USING ivfflat (val vector_ip_ops) WITH (lists = 1);
INSERT INTO t (val) VALUES ('[1,2,4]'); INSERT INTO t (val) VALUES ('[1,2,4]');
SELECT * FROM t ORDER BY val <#> '[3,3,3]'; SELECT * FROM t ORDER BY val <#> '[3,3,3]';
SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <#> (SELECT NULL::vector)) t2; SELECT * FROM t ORDER BY val <#> (SELECT NULL::vector);
DROP TABLE t; DROP TABLE t;

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ use strict;
use warnings; use warnings;
use PostgresNode; use PostgresNode;
use TestLib; use TestLib;
use Test::More; use Test::More tests => 31;
my $dim = 32; my $dim = 32;
@@ -19,13 +19,21 @@ sub test_index_replay
# Wait for replica to catch up # Wait for replica to catch up
my $applname = $node_replica->name; my $applname = $node_replica->name;
my $caughtup_query = "SELECT pg_current_wal_lsn() <= replay_lsn FROM pg_stat_replication WHERE application_name = '$applname';";
my $caughtup_query;
my $server_version_num = $node_primary->safe_psql("postgres", "SHOW server_version_num");
if ($server_version_num >= 100000) {
$caughtup_query = "SELECT pg_current_wal_lsn() <= replay_lsn FROM pg_stat_replication WHERE application_name = '$applname';";
} else {
# TODO figure out why replay location doesn't work
$caughtup_query = "SELECT pg_current_xlog_location() <= write_location FROM pg_stat_replication WHERE application_name = '$applname';";
}
$node_primary->poll_query_until('postgres', $caughtup_query) $node_primary->poll_query_until('postgres', $caughtup_query)
or die "Timed out while waiting for replica 1 to catch up"; or die "Timed out while waiting for replica 1 to catch up";
my @r = (); my @r = ();
for (1 .. $dim) for (1 .. $dim) {
{
push(@r, rand()); push(@r, rand());
} }
my $sql = join(",", @r); my $sql = join(",", @r);
@@ -51,15 +59,10 @@ my $array_sql = join(",", ('random()') x $dim);
# Initialize primary node # Initialize primary node
$node_primary = get_new_node('primary'); $node_primary = get_new_node('primary');
$node_primary->init(allows_streaming => 1); $node_primary->init(allows_streaming => 1);
if ($dim > 32) if ($dim > 32) {
{
# TODO use wal_keep_segments for Postgres < 13 # TODO use wal_keep_segments for Postgres < 13
$node_primary->append_conf('postgresql.conf', qq(wal_keep_size = 1GB)); $node_primary->append_conf('postgresql.conf', qq(wal_keep_size = 1GB));
} }
if ($dim > 1500)
{
$node_primary->append_conf('postgresql.conf', qq(maintenance_work_mem = 128MB));
}
$node_primary->start; $node_primary->start;
my $backup_name = 'my_backup'; my $backup_name = 'my_backup';
@@ -68,7 +71,8 @@ $node_primary->backup($backup_name);
# Create streaming replica linking to primary # Create streaming replica linking to primary
$node_replica = get_new_node('replica'); $node_replica = get_new_node('replica');
$node_replica->init_from_backup($node_primary, $backup_name, has_streaming => 1); $node_replica->init_from_backup($node_primary, $backup_name,
has_streaming => 1);
$node_replica->start; $node_replica->start;
# Create ivfflat index on primary # Create ivfflat index on primary
@@ -77,7 +81,7 @@ $node_primary->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector($dim));
$node_primary->safe_psql("postgres", $node_primary->safe_psql("postgres",
"INSERT INTO tst SELECT i % 10, ARRAY[$array_sql] FROM generate_series(1, 100000) i;" "INSERT INTO tst SELECT i % 10, ARRAY[$array_sql] FROM generate_series(1, 100000) i;"
); );
$node_primary->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v vector_l2_ops);"); $node_primary->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v);");
# Test that queries give same result # Test that queries give same result
test_index_replay('initial'); test_index_replay('initial');
@@ -95,5 +99,3 @@ for my $i (1 .. 10)
); );
test_index_replay("insert $i"); test_index_replay("insert $i");
} }
done_testing();

View File

@@ -2,17 +2,7 @@ use strict;
use warnings; use warnings;
use PostgresNode; use PostgresNode;
use TestLib; use TestLib;
use Test::More; use Test::More tests => 1;
my $dim = 3;
my @r = ();
for (1 .. $dim)
{
my $v = int(rand(1000)) + 1;
push(@r, "i % $v");
}
my $array_sql = join(", ", @r);
# Initialize node # Initialize node
my $node = get_new_node('node'); my $node = get_new_node('node');
@@ -21,11 +11,11 @@ $node->start;
# Create table and index # Create table and index
$node->safe_psql("postgres", "CREATE EXTENSION vector;"); $node->safe_psql("postgres", "CREATE EXTENSION vector;");
$node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector($dim));"); $node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector(3));");
$node->safe_psql("postgres", $node->safe_psql("postgres",
"INSERT INTO tst SELECT i % 10, ARRAY[$array_sql] FROM generate_series(1, 100000) i;" "INSERT INTO tst SELECT i % 10, ARRAY[i % 1000, i % 333, i % 55] FROM generate_series(1, 100000) i;"
); );
$node->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v vector_l2_ops);"); $node->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v);");
# Get size # Get size
my $size = $node->safe_psql("postgres", "SELECT pg_total_relation_size('tst_v_idx');"); my $size = $node->safe_psql("postgres", "SELECT pg_total_relation_size('tst_v_idx');");
@@ -34,11 +24,9 @@ my $size = $node->safe_psql("postgres", "SELECT pg_total_relation_size('tst_v_id
$node->safe_psql("postgres", "DELETE FROM tst;"); $node->safe_psql("postgres", "DELETE FROM tst;");
$node->safe_psql("postgres", "VACUUM tst;"); $node->safe_psql("postgres", "VACUUM tst;");
$node->safe_psql("postgres", $node->safe_psql("postgres",
"INSERT INTO tst SELECT i % 10, ARRAY[$array_sql] FROM generate_series(1, 100000) i;" "INSERT INTO tst SELECT i % 10, ARRAY[i % 1000, i % 333, i % 55] FROM generate_series(1, 100000) i;"
); );
# Check size # Check size
my $new_size = $node->safe_psql("postgres", "SELECT pg_total_relation_size('tst_v_idx');"); my $new_size = $node->safe_psql("postgres", "SELECT pg_total_relation_size('tst_v_idx');");
is($size, $new_size, "size does not change"); is($size, $new_size, "size does not change");
done_testing();

View File

@@ -1,128 +0,0 @@
use strict;
use warnings;
use PostgresNode;
use TestLib;
use Test::More;
my $node;
my @queries = ();
my @expected;
my $limit = 20;
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 %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 = get_new_node('node');
$node->init;
$node->start;
# Create table
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
$node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector(3));");
$node->safe_psql("postgres",
"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_ip_ops", "vector_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", "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 ivfflat (v $opclass);
));
# Test approximate results
if ($operator ne "<#>")
{
# TODO Fix test (uniform random vectors all have similar inner product)
test_recall(1, 0.71, $operator);
test_recall(10, 0.95, $operator);
}
# Account for equal distances
test_recall(100, 0.9925, $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.71, $operator);
test_recall(10, 0.95, $operator);
}
# Account for equal distances
test_recall(100, 0.9925, $operator);
$node->safe_psql("postgres", "DROP INDEX idx;");
}
done_testing();

View File

@@ -2,7 +2,7 @@ use strict;
use warnings; use warnings;
use PostgresNode; use PostgresNode;
use TestLib; use TestLib;
use Test::More; use Test::More tests => 9;
my $node; my $node;
my @queries = (); my @queries = ();
@@ -11,20 +11,14 @@ my $limit = 20;
sub test_recall sub test_recall
{ {
my ($min, $operator) = @_; my ($probes, $min, $operator) = @_;
my $correct = 0; my $correct = 0;
my $total = 0; my $total = 0;
my $explain = $node->safe_psql("postgres", qq( for my $i (0 .. $#queries) {
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( my $actual = $node->safe_psql("postgres", qq(
SET enable_seqscan = off; SET enable_seqscan = off;
SET ivfflat.probes = $probes;
SELECT i FROM tst ORDER BY v $operator '$queries[$i]' LIMIT $limit; SELECT i FROM tst ORDER BY v $operator '$queries[$i]' LIMIT $limit;
)); ));
my @actual_ids = split("\n", $actual); my @actual_ids = split("\n", $actual);
@@ -32,10 +26,8 @@ sub test_recall
my @expected_ids = split("\n", $expected[$i]); my @expected_ids = split("\n", $expected[$i]);
foreach (@expected_ids) foreach (@expected_ids) {
{ if (exists($actual_set{$_})) {
if (exists($actual_set{$_}))
{
$correct++; $correct++;
} }
$total++; $total++;
@@ -54,12 +46,11 @@ $node->start;
$node->safe_psql("postgres", "CREATE EXTENSION vector;"); $node->safe_psql("postgres", "CREATE EXTENSION vector;");
$node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector(3));"); $node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector(3));");
$node->safe_psql("postgres", $node->safe_psql("postgres",
"INSERT INTO tst SELECT i, ARRAY[random(), random(), random()] FROM generate_series(1, 10000) i;" "INSERT INTO tst SELECT i, ARRAY[random(), random(), random()] FROM generate_series(1, 100000) i;"
); );
# Generate queries # Generate queries
for (1 .. 20) for (1..20) {
{
my $r1 = rand(); my $r1 = rand();
my $r2 = rand(); my $r2 = rand();
my $r3 = rand(); my $r3 = rand();
@@ -68,26 +59,30 @@ for (1 .. 20)
# Check each index type # Check each index type
my @operators = ("<->", "<#>", "<=>"); my @operators = ("<->", "<#>", "<=>");
my @opclasses = ("vector_l2_ops", "vector_ip_ops", "vector_cosine_ops");
for my $i (0 .. $#operators) foreach (@operators) {
{ my $operator = $_;
my $operator = $operators[$i];
my $opclass = $opclasses[$i];
# Get exact results # Get exact results
@expected = (); @expected = ();
foreach (@queries) foreach (@queries) {
{
my $res = $node->safe_psql("postgres", "SELECT i FROM tst ORDER BY v $operator '$_' LIMIT $limit;"); my $res = $node->safe_psql("postgres", "SELECT i FROM tst ORDER BY v $operator '$_' LIMIT $limit;");
push(@expected, $res); push(@expected, $res);
} }
# Add index # Add index
$node->safe_psql("postgres", "CREATE INDEX ON tst USING hnsw (v $opclass);"); my $opclass;
if ($operator == "<->") {
$opclass = "vector_l2_ops";
} elsif ($operator == "<#>") {
$opclass = "vector_ip_ops";
} else {
$opclass = "vector_cosine_ops";
}
$node->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v $opclass);");
my $min = $operator eq "<#>" ? 0.80 : 0.99; # Test approximate results
test_recall($min, $operator); test_recall(1, 0.75, $operator);
test_recall(10, 0.95, $operator);
test_recall(100, 1.0, $operator);
} }
done_testing();

View File

@@ -2,7 +2,7 @@ use strict;
use warnings; use warnings;
use PostgresNode; use PostgresNode;
use TestLib; use TestLib;
use Test::More; use Test::More tests => 3;
# Initialize node # Initialize node
my $node = get_new_node('node'); my $node = get_new_node('node');
@@ -20,7 +20,7 @@ sub test_centers
{ {
my ($lists, $min) = @_; my ($lists, $min) = @_;
my ($ret, $stdout, $stderr) = $node->psql("postgres", "CREATE INDEX ON tst USING ivfflat (v vector_l2_ops) WITH (lists = $lists);"); my ($ret, $stdout, $stderr) = $node->psql("postgres", "CREATE INDEX ON tst USING ivfflat (v) WITH (lists = $lists);");
is($ret, 0, $stderr); is($ret, 0, $stderr);
} }
@@ -34,5 +34,3 @@ $node->safe_psql("postgres",
# Test no error for duplicate centers # Test no error for duplicate centers
test_centers(10); test_centers(10);
done_testing();

View File

@@ -2,7 +2,7 @@ use strict;
use warnings; use warnings;
use PostgresNode; use PostgresNode;
use TestLib; use TestLib;
use Test::More; use Test::More tests => 60;
# Initialize node # Initialize node
my $node = get_new_node('node'); my $node = get_new_node('node');
@@ -18,21 +18,24 @@ $node->safe_psql("postgres",
# Check each index type # Check each index type
my @operators = ("<->", "<#>", "<=>"); my @operators = ("<->", "<#>", "<=>");
my @opclasses = ("vector_l2_ops", "vector_ip_ops", "vector_cosine_ops"); foreach (@operators) {
my $operator = $_;
for my $i (0 .. $#operators)
{
my $operator = $operators[$i];
my $opclass = $opclasses[$i];
# Add index # Add index
my $opclass;
if ($operator == "<->") {
$opclass = "vector_l2_ops";
} elsif ($operator == "<#>") {
$opclass = "vector_ip_ops";
} else {
$opclass = "vector_cosine_ops";
}
$node->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v $opclass);"); $node->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v $opclass);");
# Test 100% recall # Test 100% recall
for (1 .. 20) for (1..20) {
{ my $i = int(rand() * 100000);
my $id = int(rand() * 100000); my $query = $node->safe_psql("postgres", "SELECT v FROM tst WHERE i = $i;");
my $query = $node->safe_psql("postgres", "SELECT v FROM tst WHERE i = $id;");
my $res = $node->safe_psql("postgres", qq( my $res = $node->safe_psql("postgres", qq(
SET enable_seqscan = off; SET enable_seqscan = off;
SELECT v FROM tst ORDER BY v <-> '$query' LIMIT 1; SELECT v FROM tst ORDER BY v <-> '$query' LIMIT 1;
@@ -40,5 +43,3 @@ for my $i (0 .. $#operators)
is($res, $query); is($res, $query);
} }
} }
done_testing();

View File

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

View File

@@ -2,7 +2,7 @@ use strict;
use warnings; use warnings;
use PostgresNode; use PostgresNode;
use TestLib; use TestLib;
use Test::More; use Test::More tests => 5;
my $dim = 768; my $dim = 768;
@@ -19,7 +19,7 @@ $node->safe_psql("postgres", "CREATE TABLE tst (v vector($dim));");
$node->safe_psql("postgres", $node->safe_psql("postgres",
"INSERT INTO tst SELECT ARRAY[$array_sql] FROM generate_series(1, 10000) i;" "INSERT INTO tst SELECT ARRAY[$array_sql] FROM generate_series(1, 10000) i;"
); );
$node->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v vector_l2_ops);"); $node->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v);");
$node->pgbench( $node->pgbench(
"--no-vacuum --client=5 --transactions=100", "--no-vacuum --client=5 --transactions=100",
@@ -28,23 +28,14 @@ $node->pgbench(
[qr{^$}], [qr{^$}],
"concurrent INSERTs", "concurrent INSERTs",
{ {
"007_ivfflat_inserts" => "INSERT INTO tst SELECT ARRAY[$array_sql] FROM generate_series(1, 10) i;" "007_inserts" => "INSERT INTO tst SELECT ARRAY[$array_sql] FROM generate_series(1, 10) i;"
} }
); );
sub idx_scan
{
# Stats do not update instantaneously
# https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-STATS-VIEWS
sleep(1);
$node->safe_psql("postgres", "SELECT idx_scan FROM pg_stat_user_indexes WHERE indexrelid = 'tst_v_idx'::regclass;");
}
my $expected = 10000 + 5 * 100 * 10; my $expected = 10000 + 5 * 100 * 10;
my $count = $node->safe_psql("postgres", "SELECT COUNT(*) FROM tst;"); my $count = $node->safe_psql("postgres", "SELECT COUNT(*) FROM tst;");
is($count, $expected); is($count, $expected);
is(idx_scan(), 0);
$count = $node->safe_psql("postgres", qq( $count = $node->safe_psql("postgres", qq(
SET enable_seqscan = off; SET enable_seqscan = off;
@@ -52,6 +43,3 @@ $count = $node->safe_psql("postgres", qq(
SELECT COUNT(*) FROM (SELECT v FROM tst ORDER BY v <-> (SELECT v FROM tst LIMIT 1)) t; SELECT COUNT(*) FROM (SELECT v FROM tst ORDER BY v <-> (SELECT v FROM tst LIMIT 1)) t;
)); ));
is($count, $expected); is($count, $expected);
is(idx_scan(), 1);
done_testing();

View File

@@ -1,49 +0,0 @@
use strict;
use warnings;
use PostgresNode;
use TestLib;
use Test::More;
# Initialize node
my $node = get_new_node('node');
$node->init;
$node->start;
# Create table
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
$node->safe_psql("postgres", "CREATE TABLE tst (r1 real, r2 real, r3 real, v vector(3));");
$node->safe_psql("postgres", qq(
INSERT INTO tst SELECT r1, r2, r3, ARRAY[r1, r2, r3] FROM (
SELECT random() + 1.01 AS r1, random() + 2.01 AS r2, random() + 3.01 AS r3 FROM generate_series(1, 1000000) t
) i;
));
sub test_aggregate
{
my ($agg) = @_;
# Test value
my $res = $node->safe_psql("postgres", "SELECT $agg(v) FROM tst;");
like($res, qr/\[1\.5/);
like($res, qr/,2\.5/);
like($res, qr/,3\.5/);
# Test matches real for avg
# Cannot test sum since sum(real) varies between calls
if ($agg eq 'avg')
{
my $r1 = $node->safe_psql("postgres", "SELECT $agg(r1)::float4 FROM tst;");
my $r2 = $node->safe_psql("postgres", "SELECT $agg(r2)::float4 FROM tst;");
my $r3 = $node->safe_psql("postgres", "SELECT $agg(r3)::float4 FROM tst;");
is($res, "[$r1,$r2,$r3]");
}
# Test explain
my $explain = $node->safe_psql("postgres", "EXPLAIN SELECT $agg(v) FROM tst;");
like($explain, qr/Partial Aggregate/);
}
test_aggregate('avg');
test_aggregate('sum');
done_testing();

View File

@@ -1,34 +0,0 @@
use strict;
use warnings;
use PostgresNode;
use TestLib;
use Test::More;
my $dim = 1024;
# Initialize node
my $node = get_new_node('node');
$node->init;
$node->start;
# Create table
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
$node->safe_psql("postgres", "CREATE TABLE tst (v1 vector(1024), v2 vector(1024), v3 vector(1024));");
# Test insert succeeds
$node->safe_psql("postgres",
"INSERT INTO tst SELECT array_agg(n), array_agg(n), array_agg(n) FROM generate_series(1, $dim) n"
);
# Change storage to PLAIN
$node->safe_psql("postgres", "ALTER TABLE tst ALTER COLUMN v1 SET STORAGE PLAIN");
$node->safe_psql("postgres", "ALTER TABLE tst ALTER COLUMN v2 SET STORAGE PLAIN");
$node->safe_psql("postgres", "ALTER TABLE tst ALTER COLUMN v3 SET STORAGE PLAIN");
# Test insert fails
my ($ret, $stdout, $stderr) = $node->psql("postgres",
"INSERT INTO tst SELECT array_agg(n), array_agg(n), array_agg(n) FROM generate_series(1, $dim) n"
);
like($stderr, qr/row is too big/);
done_testing();

View File

@@ -1,99 +0,0 @@
# Based on postgres/contrib/bloom/t/001_wal.pl
# Test generic xlog record work for hnsw index replication.
use strict;
use warnings;
use PostgresNode;
use TestLib;
use Test::More;
my $dim = 32;
my $node_primary;
my $node_replica;
# Run few queries on both primary and replica and check their results match.
sub test_index_replay
{
my ($test_name) = @_;
# Wait for replica to catch up
my $applname = $node_replica->name;
my $caughtup_query = "SELECT pg_current_wal_lsn() <= replay_lsn FROM pg_stat_replication WHERE application_name = '$applname';";
$node_primary->poll_query_until('postgres', $caughtup_query)
or die "Timed out while waiting for replica 1 to catch up";
my @r = ();
for (1 .. $dim)
{
push(@r, rand());
}
my $sql = join(",", @r);
my $queries = qq(
SET enable_seqscan = off;
SELECT * FROM tst ORDER BY v <-> '[$sql]' LIMIT 10;
);
# Run test queries and compare their result
my $primary_result = $node_primary->safe_psql("postgres", $queries);
my $replica_result = $node_replica->safe_psql("postgres", $queries);
is($primary_result, $replica_result, "$test_name: query result matches");
return;
}
# Use ARRAY[random(), random(), random(), ...] over
# SELECT array_agg(random()) FROM generate_series(1, $dim)
# to generate different values for each row
my $array_sql = join(",", ('random()') x $dim);
# Initialize primary node
$node_primary = get_new_node('primary');
$node_primary->init(allows_streaming => 1);
if ($dim > 32)
{
# TODO use wal_keep_segments for Postgres < 13
$node_primary->append_conf('postgresql.conf', qq(wal_keep_size = 1GB));
}
if ($dim > 1500)
{
$node_primary->append_conf('postgresql.conf', qq(maintenance_work_mem = 128MB));
}
$node_primary->start;
my $backup_name = 'my_backup';
# Take backup
$node_primary->backup($backup_name);
# Create streaming replica linking to primary
$node_replica = get_new_node('replica');
$node_replica->init_from_backup($node_primary, $backup_name, has_streaming => 1);
$node_replica->start;
# Create hnsw index on primary
$node_primary->safe_psql("postgres", "CREATE EXTENSION vector;");
$node_primary->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector($dim));");
$node_primary->safe_psql("postgres",
"INSERT INTO tst SELECT i % 10, ARRAY[$array_sql] FROM generate_series(1, 1000) i;"
);
$node_primary->safe_psql("postgres", "CREATE INDEX ON tst USING hnsw (v vector_l2_ops);");
# Test that queries give same result
test_index_replay('initial');
# Run 10 cycles of table modification. Run test queries after each modification.
for my $i (1 .. 10)
{
$node_primary->safe_psql("postgres", "DELETE FROM tst WHERE i = $i;");
test_index_replay("delete $i");
$node_primary->safe_psql("postgres", "VACUUM tst;");
test_index_replay("vacuum $i");
my ($start, $end) = (1001 + ($i - 1) * 100, 1000 + $i * 100);
$node_primary->safe_psql("postgres",
"INSERT INTO tst SELECT i % 10, ARRAY[$array_sql] FROM generate_series($start, $end) i;"
);
test_index_replay("insert $i");
}
done_testing();

View File

@@ -1,54 +0,0 @@
use strict;
use warnings;
use PostgresNode;
use TestLib;
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);
# Initialize node
my $node = get_new_node('node');
$node->init;
$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));");
$node->safe_psql("postgres",
"INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(1, 10000) i;"
);
$node->safe_psql("postgres", "CREATE INDEX ON tst USING hnsw (v vector_l2_ops);");
# Get size
my $size = $node->safe_psql("postgres", "SELECT pg_total_relation_size('tst_v_idx');");
# 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, ARRAY[$array_sql] FROM generate_series(1, 10000) i;"
);
# Check size
# May increase some due to different levels
my $new_size = $node->safe_psql("postgres", "SELECT pg_total_relation_size('tst_v_idx');");
cmp_ok($new_size, "<=", $size * 1.02, "size does not increase too much");
# Delete all but one
$node->safe_psql("postgres", "DELETE FROM tst WHERE i != 123;");
$node->safe_psql("postgres", "VACUUM tst;");
my $res = $node->safe_psql("postgres", qq(
SET enable_seqscan = off;
SELECT i FROM tst ORDER BY v <-> '[0,0,0]' LIMIT 10;
));
is($res, 123);
done_testing();

View File

@@ -1,108 +0,0 @@
use strict;
use warnings;
use PostgresNode;
use TestLib;
use Test::More;
my $node;
my @queries = ();
my @expected;
my $limit = 20;
sub test_recall
{
my ($min, $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 = get_new_node('node');
$node->init;
$node->start;
# Create table
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
$node->safe_psql("postgres", "CREATE TABLE tst (i serial, v vector(3));");
# 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_ip_ops", "vector_cosine_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",
{
"013_hnsw_insert_recall_$opclass" => "INSERT INTO tst (v) VALUES (ARRAY[random(), random(), random()]);"
}
);
# 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);
}
my $min = $operator eq "<#>" ? 0.80 : 0.99;
test_recall($min, $operator);
$node->safe_psql("postgres", "DROP INDEX idx;");
$node->safe_psql("postgres", "TRUNCATE tst;");
}
done_testing();

View File

@@ -1,74 +0,0 @@
use strict;
use warnings;
use PostgresNode;
use TestLib;
use Test::More;
# Ensures elements and neighbors on both same and different pages
my $dim = 1900;
my $array_sql = join(",", ('random()') x $dim);
# Initialize node
my $node = get_new_node('node');
$node->init;
$node->start;
# Create table and index
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
$node->safe_psql("postgres", "CREATE TABLE tst (v vector($dim));");
$node->safe_psql("postgres", "CREATE INDEX ON tst USING hnsw (v vector_l2_ops);");
sub idx_scan
{
# Stats do not update instantaneously
# https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-STATS-VIEWS
sleep(1);
$node->safe_psql("postgres", "SELECT idx_scan FROM pg_stat_user_indexes WHERE indexrelid = 'tst_v_idx'::regclass;");
}
for my $i (1 .. 20)
{
$node->pgbench(
"--no-vacuum --client=10 --transactions=1",
0,
[qr{actually processed}],
[qr{^$}],
"concurrent INSERTs",
{
"014_hnsw_inserts_$i" => "INSERT INTO tst VALUES (ARRAY[$array_sql]);"
}
);
my $count = $node->safe_psql("postgres", qq(
SET enable_seqscan = off;
SELECT COUNT(*) FROM (SELECT v FROM tst ORDER BY v <-> (SELECT v FROM tst LIMIT 1)) t;
));
is($count, 10);
$node->safe_psql("postgres", "TRUNCATE tst;");
}
$node->pgbench(
"--no-vacuum --client=20 --transactions=5",
0,
[qr{actually processed}],
[qr{^$}],
"concurrent INSERTs",
{
"014_hnsw_inserts" => "INSERT INTO tst SELECT ARRAY[$array_sql] FROM generate_series(1, 10) i;"
}
);
my $count = $node->safe_psql("postgres", qq(
SET enable_seqscan = off;
SET hnsw.ef_search = 1000;
SELECT COUNT(*) FROM (SELECT v FROM tst ORDER BY v <-> (SELECT v FROM tst LIMIT 1)) t;
));
# Elements may lose all incoming connections with the HNSW algorithm
# Vacuuming can fix this if one of the elements neighbors is deleted
cmp_ok($count, ">=", 997);
is(idx_scan(), 21);
done_testing();

View File

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

View File

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

View File

@@ -1,117 +0,0 @@
use strict;
use warnings;
use PostgresNode;
use TestLib;
use Test::More;
my $node;
my @queries = ();
my @expected;
my $limit = 20;
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 %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 = get_new_node('node');
$node->init;
$node->start;
# Create table
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
$node->safe_psql("postgres", "CREATE TABLE tst (i serial, v vector(3));");
# 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_ip_ops", "vector_cosine_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 ivfflat (v $opclass);");
# Use concurrent inserts
$node->pgbench(
"--no-vacuum --client=10 --transactions=1000",
0,
[qr{actually processed}],
[qr{^$}],
"concurrent INSERTs",
{
"017_ivfflat_insert_recall_$opclass" => "INSERT INTO tst (v) SELECT ARRAY[random(), random(), random()] FROM generate_series(1, 10) i;"
}
);
# 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
if ($operator ne "<#>")
{
# TODO Fix test (uniform random vectors all have similar inner product)
test_recall(1, 0.71, $operator);
test_recall(10, 0.95, $operator);
}
# Account for equal distances
test_recall(100, 0.9925, $operator);
$node->safe_psql("postgres", "DROP INDEX idx;");
$node->safe_psql("postgres", "TRUNCATE tst;");
}
done_testing();

View File

@@ -1,43 +0,0 @@
use strict;
use warnings;
use PostgresNode;
use TestLib;
use Test::More;
my $dim = 3;
my $array_sql = join(",", ('random()') x $dim);
# Initialize node
my $node = get_new_node('node');
$node->init;
$node->start;
# Create table and index
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
$node->safe_psql("postgres", "CREATE TABLE tst (i serial, v vector($dim));");
$node->safe_psql("postgres",
"INSERT INTO tst (v) SELECT ARRAY[$array_sql] FROM generate_series(1, 10000) i;"
);
$node->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v vector_l2_ops);");
# Delete data
$node->safe_psql("postgres", "DELETE FROM tst WHERE i % 100 != 0;");
my $exp = $node->safe_psql("postgres", qq(
SET enable_indexscan = off;
SELECT i FROM tst ORDER BY v <-> '[0,0,0]';
));
# Run twice to make sure correct tuples marked as dead
for (1 .. 2)
{
my $res = $node->safe_psql("postgres", qq(
SET enable_seqscan = off;
SET ivfflat.probes = 100;
SELECT i FROM tst ORDER BY v <-> '[0,0,0]';
));
is($res, $exp);
}
done_testing();

View File

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