mirror of
https://github.com/pgvector/pgvector.git
synced 2026-07-22 03:57:34 +08:00
Compare commits
113 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9852351746 | ||
|
|
ecbf46938f | ||
|
|
4ca264ba02 | ||
|
|
50349ed4f5 | ||
|
|
f5458414b8 | ||
|
|
e64ed39acb | ||
|
|
2ee510aa67 | ||
|
|
cad655b77f | ||
|
|
0d025be9d3 | ||
|
|
fed60dce78 | ||
|
|
a37f5eea4a | ||
|
|
4bdb27e85a | ||
|
|
38f869e0bd | ||
|
|
ad8acc00d4 | ||
|
|
01926a418e | ||
|
|
9658d3c1ad | ||
|
|
6b9c6516f4 | ||
|
|
88be03a3fa | ||
|
|
bf5b2c8d7e | ||
|
|
ac65ec2856 | ||
|
|
7bba0e2a01 | ||
|
|
c35e9f3b84 | ||
|
|
9549d93260 | ||
|
|
310809d0e5 | ||
|
|
ce72ca8620 | ||
|
|
4f2c937a1f | ||
|
|
21ca5d3845 | ||
|
|
ff400ce5f1 | ||
|
|
8374498e6c | ||
|
|
c1d6b9b41b | ||
|
|
a77340d40b | ||
|
|
81b68fbf5b | ||
|
|
8ee6d0e596 | ||
|
|
41d11c62d6 | ||
|
|
fa5e90585d | ||
|
|
71d2908be1 | ||
|
|
d0a1c1d0ed | ||
|
|
8063201cdf | ||
|
|
2f52127bee | ||
|
|
4778039ffa | ||
|
|
e3c9d70acc | ||
|
|
5971e28f8c | ||
|
|
2da27e2f01 | ||
|
|
7dd934913d | ||
|
|
9b46987e73 | ||
|
|
868e1f99a4 | ||
|
|
354271178f | ||
|
|
fe9b218903 | ||
|
|
3cdd1c16fb | ||
|
|
4f7a5c92da | ||
|
|
9399575e02 | ||
|
|
e7539fe548 | ||
|
|
61860ee6d9 | ||
|
|
eb99730d87 | ||
|
|
e5da5cc532 | ||
|
|
f59b1184fc | ||
|
|
cab103a2e7 | ||
|
|
0094b70a36 | ||
|
|
46ed56d80f | ||
|
|
d390cc9fa6 | ||
|
|
02ae98791c | ||
|
|
b64c7d6228 | ||
|
|
76f117c81c | ||
|
|
5414b2bc9e | ||
|
|
6ba583b587 | ||
|
|
da88217fd8 | ||
|
|
d5da22f24b | ||
|
|
062e220936 | ||
|
|
3d2b867a58 | ||
|
|
5aeabc3648 | ||
|
|
687ec6d8d7 | ||
|
|
51dc18d5b7 | ||
|
|
cb108ebfd2 | ||
|
|
12bc4eb036 | ||
|
|
fd0e472316 | ||
|
|
9b11b05cd7 | ||
|
|
ab079c1911 | ||
|
|
f4eaa3de1f | ||
|
|
e47675d6ae | ||
|
|
20167ddc33 | ||
|
|
e14fa82626 | ||
|
|
2ae4de83fe | ||
|
|
5d2c4883dc | ||
|
|
fc1de9806a | ||
|
|
e375dd33f3 | ||
|
|
b643ef637a | ||
|
|
0ee5bfda65 | ||
|
|
c5bec2a534 | ||
|
|
40f65dc8ba | ||
|
|
9ec174137f | ||
|
|
423e478a85 | ||
|
|
8b301197fe | ||
|
|
c02feaa9ad | ||
|
|
729374a9b8 | ||
|
|
a8245d6cda | ||
|
|
5220a6ec7e | ||
|
|
ddd873f369 | ||
|
|
8735c4782a | ||
|
|
17ec3f3852 | ||
|
|
8df12a62e7 | ||
|
|
647c9002be | ||
|
|
3e6345f02a | ||
|
|
2b3484ddcd | ||
|
|
1a6debf281 | ||
|
|
bff4d7ea68 | ||
|
|
d5b979e4fa | ||
|
|
d52426b3fa | ||
|
|
b125b2debb | ||
|
|
3d7186a17c | ||
|
|
58ba2139ce | ||
|
|
fd93c9db86 | ||
|
|
8dde26920c | ||
|
|
9153a545a7 |
@@ -1,6 +1,6 @@
|
||||
root = true
|
||||
|
||||
[*.{c,h}]
|
||||
[*.{c,h,pl}]
|
||||
indent_style = tab
|
||||
indent_size = tab
|
||||
tab_width = 4
|
||||
|
||||
11
.github/workflows/build.yml
vendored
11
.github/workflows/build.yml
vendored
@@ -2,16 +2,15 @@ name: build
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
build:
|
||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
postgres: [13, 12, 11, 10, 9.6]
|
||||
postgres: [14, 13, 12, 11, 10, 9.6]
|
||||
include:
|
||||
- os: macos-latest
|
||||
postgres: 13
|
||||
postgres: 14
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: ankane/setup-postgres@v1
|
||||
@@ -34,6 +33,6 @@ jobs:
|
||||
- if: ${{ startsWith(matrix.os, 'macos') }}
|
||||
run: |
|
||||
brew install cpanm && cpanm IPC::Run
|
||||
wget -q https://github.com/postgres/postgres/archive/refs/tags/REL_13_2.tar.gz
|
||||
tar xf REL_13_2.tar.gz
|
||||
make prove_installcheck PROVE=prove PERL5LIB=postgres-REL_13_2/src/test/perl
|
||||
wget -q https://github.com/postgres/postgres/archive/refs/tags/REL_14_1.tar.gz
|
||||
tar xf REL_14_1.tar.gz
|
||||
make prove_installcheck PROVE=prove PERL5LIB=postgres-REL_14_1/src/test/perl
|
||||
|
||||
33
CHANGELOG.md
33
CHANGELOG.md
@@ -1,3 +1,36 @@
|
||||
## 0.2.6 (unreleased)
|
||||
|
||||
- Switched to mini-batch k-means
|
||||
- Improved performance of index creation for Postgres < 12
|
||||
|
||||
## 0.2.5 (2022-02-11)
|
||||
|
||||
- Reduced memory usage during index creation
|
||||
- Fixed index creation exceeding `maintenance_work_mem`
|
||||
- Fixed error with index creation when lists > 1600
|
||||
|
||||
## 0.2.4 (2022-02-06)
|
||||
|
||||
- Added support for parallel vacuum
|
||||
- Fixed issue with index not reusing space
|
||||
|
||||
## 0.2.3 (2022-01-30)
|
||||
|
||||
- Added indexing progress for Postgres 12+
|
||||
- Improved interrupt handling during index creation
|
||||
|
||||
## 0.2.2 (2022-01-15)
|
||||
|
||||
- Fixed compilation error on Mac ARM
|
||||
|
||||
## 0.2.1 (2022-01-02)
|
||||
|
||||
- Fixed `operator is not unique` error
|
||||
|
||||
## 0.2.0 (2021-10-03)
|
||||
|
||||
- Added support for Postgres 14
|
||||
|
||||
## 0.1.8 (2021-09-07)
|
||||
|
||||
- Added cast for `vector` to `real[]`
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
FROM postgres
|
||||
FROM postgres:14
|
||||
|
||||
COPY . /tmp/pgvector
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends build-essential postgresql-server-dev-13 && \
|
||||
apt-get install -y --no-install-recommends build-essential postgresql-server-dev-14 && \
|
||||
cd /tmp/pgvector && \
|
||||
make clean && \
|
||||
make OPTFLAGS="" && \
|
||||
@@ -11,6 +11,6 @@ RUN apt-get update && \
|
||||
mkdir /usr/share/doc/pgvector && \
|
||||
cp LICENSE README.md /usr/share/doc/pgvector && \
|
||||
rm -r /tmp/pgvector && \
|
||||
apt-get remove -y build-essential postgresql-server-dev-13 && \
|
||||
apt-get remove -y build-essential postgresql-server-dev-14 && \
|
||||
apt-get autoremove -y && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
12
META.json
12
META.json
@@ -2,7 +2,7 @@
|
||||
"name": "vector",
|
||||
"abstract": "Open-source vector similarity search for Postgres",
|
||||
"description": "Supports L2 distance, inner product, and cosine distance",
|
||||
"version": "0.1.8",
|
||||
"version": "0.2.5",
|
||||
"maintainer": [
|
||||
"Andrew Kane <andrew@ankane.org>"
|
||||
],
|
||||
@@ -20,18 +20,18 @@
|
||||
"vector": {
|
||||
"file": "sql/vector.sql",
|
||||
"docfile": "README.md",
|
||||
"version": "0.1.8",
|
||||
"version": "0.2.5",
|
||||
"abstract": "Open-source vector similarity search for Postgres"
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"homepage": "https://github.com/ankane/pgvector",
|
||||
"homepage": "https://github.com/pgvector/pgvector",
|
||||
"bugtracker": {
|
||||
"web": "https://github.com/ankane/pgvector/issues"
|
||||
"web": "https://github.com/pgvector/pgvector/issues"
|
||||
},
|
||||
"repository": {
|
||||
"url": "https://github.com/ankane/pgvector.git",
|
||||
"web": "https://github.com/ankane/pgvector",
|
||||
"url": "https://github.com/pgvector/pgvector.git",
|
||||
"web": "https://github.com/pgvector/pgvector",
|
||||
"type": "git"
|
||||
}
|
||||
},
|
||||
|
||||
16
Makefile
16
Makefile
@@ -1,5 +1,5 @@
|
||||
EXTENSION = vector
|
||||
EXTVERSION = 0.1.8
|
||||
EXTVERSION = 0.2.5
|
||||
|
||||
MODULE_big = vector
|
||||
DATA = $(wildcard sql/*--*.sql)
|
||||
@@ -11,10 +11,17 @@ REGRESS_OPTS = --inputdir=test
|
||||
|
||||
OPTFLAGS = -march=native
|
||||
|
||||
# Mac ARM doesn't support -march=native
|
||||
ifeq ($(shell uname -s), Darwin)
|
||||
ifeq ($(shell uname -p), arm)
|
||||
OPTFLAGS =
|
||||
endif
|
||||
endif
|
||||
|
||||
# For auto-vectorization:
|
||||
# - 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
|
||||
PG_CFLAGS = $(OPTFLAGS) -ftree-vectorize -fassociative-math -fno-signed-zeros -fno-trapping-math
|
||||
PG_CFLAGS += $(OPTFLAGS) -ftree-vectorize -fassociative-math -fno-signed-zeros -fno-trapping-math
|
||||
|
||||
# Debug GCC auto-vectorization
|
||||
# PG_CFLAGS += -fopt-info-vec
|
||||
@@ -42,3 +49,8 @@ prove_installcheck:
|
||||
dist:
|
||||
mkdir -p dist
|
||||
git archive --format zip --prefix=$(EXTENSION)-$(EXTVERSION)/ --output dist/$(EXTENSION)-$(EXTVERSION).zip master
|
||||
|
||||
.PHONY: docker
|
||||
|
||||
docker:
|
||||
docker build --pull --no-cache -t ankane/pgvector:latest .
|
||||
|
||||
98
README.md
98
README.md
@@ -4,20 +4,20 @@ Open-source vector similarity search for Postgres
|
||||
|
||||
```sql
|
||||
CREATE TABLE table (column vector(3));
|
||||
CREATE INDEX ON table USING ivfflat (column);
|
||||
CREATE INDEX ON table USING ivfflat (column vector_l2_ops);
|
||||
SELECT * FROM table ORDER BY column <-> '[1,2,3]' LIMIT 5;
|
||||
```
|
||||
|
||||
Supports L2 distance, inner product, and cosine distance
|
||||
|
||||
[](https://github.com/ankane/pgvector/actions)
|
||||
[](https://github.com/pgvector/pgvector/actions)
|
||||
|
||||
## Installation
|
||||
|
||||
Compile and install the extension (supports Postgres 9.6+)
|
||||
|
||||
```sh
|
||||
git clone --branch v0.1.8 https://github.com/ankane/pgvector.git
|
||||
git clone --branch v0.2.5 https://github.com/pgvector/pgvector.git
|
||||
cd pgvector
|
||||
make
|
||||
make install # may need sudo
|
||||
@@ -62,7 +62,7 @@ Speed up queries with an approximate index. Add an index for each distance funct
|
||||
L2 distance
|
||||
|
||||
```sql
|
||||
CREATE INDEX ON table USING ivfflat (column);
|
||||
CREATE INDEX ON table USING ivfflat (column vector_l2_ops);
|
||||
```
|
||||
|
||||
Inner product
|
||||
@@ -77,16 +77,18 @@ Cosine distance
|
||||
CREATE INDEX ON table USING ivfflat (column vector_cosine_ops);
|
||||
```
|
||||
|
||||
Indexes should be created after the table has data for optimal clustering. Also, unlike typical indexes which only affect performance, you may see different results for queries after adding an approximate index.
|
||||
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 table USING ivfflat (column) WITH (lists = 100);
|
||||
CREATE INDEX ON table USING ivfflat (column opclass) WITH (lists = 100);
|
||||
```
|
||||
|
||||
A [good place to start](https://github.com/facebookresearch/faiss/issues/112) is `4 * sqrt(rows)`
|
||||
|
||||
### Query Options
|
||||
|
||||
Specify the number of probes (1 by default)
|
||||
@@ -106,16 +108,47 @@ SELECT ...
|
||||
COMMIT;
|
||||
```
|
||||
|
||||
### Indexing Progress
|
||||
|
||||
Check [indexing progress](https://www.postgresql.org/docs/current/progress-reporting.html#CREATE-INDEX-PROGRESS-REPORTING) with Postgres 12+
|
||||
|
||||
```sql
|
||||
SELECT phase, tuples_done, tuples_total FROM pg_stat_progress_create_index;
|
||||
```
|
||||
|
||||
The phases are:
|
||||
|
||||
1. `initializing`
|
||||
2. `performing k-means`
|
||||
3. `sorting tuples`
|
||||
4. `loading tuples`
|
||||
|
||||
Note: `tuples_done` and `tuples_total` are only populated during the `loading tuples` phase
|
||||
|
||||
### Partial Indexes
|
||||
|
||||
Consider [partial indexes](https://www.postgresql.org/docs/current/indexes-partial.html) for queries with a `WHERE` clause
|
||||
|
||||
```sql
|
||||
CREATE INDEX ON table USING ivfflat (column) WHERE (other_column = 123);
|
||||
CREATE INDEX ON table USING ivfflat (column opclass) WHERE (other_column = 123);
|
||||
```
|
||||
|
||||
To index many different values of `other_column`, consider [partitioning](https://www.postgresql.org/docs/current/ddl-partitioning.html) on `other_column`.
|
||||
|
||||
## Performance
|
||||
|
||||
To speed up queries without an index, increase `max_parallel_workers_per_gather`.
|
||||
|
||||
```sql
|
||||
SET max_parallel_workers_per_gather = 4;
|
||||
```
|
||||
|
||||
To speed up queries with an index, increase the number of inverted lists (at the expense of recall).
|
||||
|
||||
```sql
|
||||
CREATE INDEX ON table USING ivfflat (column opclass) WITH (lists = 1000);
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
### Vector Type
|
||||
@@ -146,17 +179,36 @@ vector_norm(vector) | Euclidean norm
|
||||
|
||||
Libraries that use pgvector:
|
||||
|
||||
- [pgvector-python](https://github.com/ankane/pgvector-python) (Python)
|
||||
- [pgvector-python](https://github.com/pgvector/pgvector-python) (Python)
|
||||
- [Neighbor](https://github.com/ankane/neighbor) (Ruby)
|
||||
- [pgvector-node](https://github.com/ankane/pgvector-node) (Node.js)
|
||||
- [pgvector-go](https://github.com/ankane/pgvector-go) (Go)
|
||||
- [pgvector-rust](https://github.com/ankane/pgvector-rust) (Rust)
|
||||
- [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-rust](https://github.com/pgvector/pgvector-rust) (Rust)
|
||||
- [pgvector-cpp](https://github.com/pgvector/pgvector-cpp) (C++)
|
||||
|
||||
## Frequently Asked Questions
|
||||
|
||||
#### How many vectors can be stored in a single table?
|
||||
|
||||
A non-partitioned table has a limit of 32 TB by default in Postgres. A partitioned table can have thousands of partitions of that size.
|
||||
|
||||
#### Is replication supported?
|
||||
|
||||
Yes, pgvector uses the write-ahead log (WAL), which allows for replication and point-in-time recovery.
|
||||
|
||||
#### What if my data has more than 1024 dimensions?
|
||||
|
||||
Two things you can try are:
|
||||
|
||||
1. use dimensionality reduction
|
||||
2. compile Postgres with a larger block size (`./configure --with-blocksize=32`) and edit the limit in `src/vector.h`
|
||||
|
||||
## Additional Installation Methods
|
||||
|
||||
### Docker
|
||||
|
||||
Get the [Docker image](https://hub.docker.com/repository/docker/ankane/pgvector) with:
|
||||
Get the [Docker image](https://hub.docker.com/r/ankane/pgvector) with:
|
||||
|
||||
```sh
|
||||
docker pull ankane/pgvector
|
||||
@@ -167,7 +219,7 @@ This adds pgvector to the [Postgres image](https://hub.docker.com/_/postgres).
|
||||
You can also build the image manually
|
||||
|
||||
```sh
|
||||
git clone --branch v0.1.8 https://github.com/ankane/pgvector.git
|
||||
git clone --branch v0.2.5 https://github.com/pgvector/pgvector.git
|
||||
cd pgvector
|
||||
docker build -t pgvector .
|
||||
```
|
||||
@@ -177,7 +229,7 @@ docker build -t pgvector .
|
||||
On Mac with Homebrew Postgres, you can use:
|
||||
|
||||
```sh
|
||||
brew install ankane/brew/pgvector
|
||||
brew install pgvector/brew/pgvector
|
||||
```
|
||||
|
||||
### PGXN
|
||||
@@ -194,7 +246,7 @@ Some Postgres providers only support specific extensions. To request a new exten
|
||||
|
||||
- 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 - follow the instructions on [this page](https://docs.digitalocean.com/products/databases/postgresql/resources/supported-extensions/#supported-extensions)
|
||||
- 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
|
||||
@@ -211,27 +263,27 @@ Thanks to:
|
||||
|
||||
- [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)
|
||||
- [Using the Triangle Inequality to Accelerate k-means](https://www.aaai.org/Papers/ICML/2003/ICML03-022.pdf)
|
||||
- [Web-Scale k-means Clustering](https://www.eecs.tufts.edu/~dsculley/papers/fastkmeans.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)
|
||||
|
||||
## History
|
||||
|
||||
View the [changelog](https://github.com/ankane/pgvector/blob/master/CHANGELOG.md)
|
||||
View the [changelog](https://github.com/pgvector/pgvector/blob/master/CHANGELOG.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
Everyone is encouraged to help improve this project. Here are a few ways you can help:
|
||||
|
||||
- [Report bugs](https://github.com/ankane/pgvector/issues)
|
||||
- Fix bugs and [submit pull requests](https://github.com/ankane/pgvector/pulls)
|
||||
- [Report bugs](https://github.com/pgvector/pgvector/issues)
|
||||
- Fix bugs and [submit pull requests](https://github.com/pgvector/pgvector/pulls)
|
||||
- Write, clarify, or fix documentation
|
||||
- Suggest or add new features
|
||||
|
||||
To get started with development:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/ankane/pgvector.git
|
||||
git clone https://github.com/pgvector/pgvector.git
|
||||
cd pgvector
|
||||
make
|
||||
make install
|
||||
@@ -251,6 +303,12 @@ make installcheck REGRESS=functions # regression test
|
||||
make prove_installcheck PROVE_TESTS=test/t/001_wal.pl # TAP test
|
||||
```
|
||||
|
||||
To enable benchmarking:
|
||||
|
||||
```sh
|
||||
make clean && PG_CFLAGS=-DIVFFLAT_BENCH make && make install
|
||||
```
|
||||
|
||||
Resources for contributors
|
||||
|
||||
- [Extension Building Infrastructure](https://www.postgresql.org/docs/current/extend-pgxs.html)
|
||||
|
||||
2
sql/vector--0.1.8--0.2.0.sql
Normal file
2
sql/vector--0.1.8--0.2.0.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
|
||||
\echo Use "ALTER EXTENSION vector UPDATE TO '0.2.0'" to load this file. \quit
|
||||
19
sql/vector--0.2.0--0.2.1.sql
Normal file
19
sql/vector--0.2.0--0.2.1.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
|
||||
\echo Use "ALTER EXTENSION vector UPDATE TO '0.2.1'" to load this file. \quit
|
||||
|
||||
DROP CAST (integer[] AS vector);
|
||||
DROP CAST (real[] AS vector);
|
||||
DROP CAST (double precision[] AS vector);
|
||||
DROP CAST (numeric[] AS vector);
|
||||
|
||||
CREATE CAST (integer[] AS vector)
|
||||
WITH FUNCTION array_to_vector(integer[], integer, boolean) AS ASSIGNMENT;
|
||||
|
||||
CREATE CAST (real[] AS vector)
|
||||
WITH FUNCTION array_to_vector(real[], integer, boolean) AS ASSIGNMENT;
|
||||
|
||||
CREATE CAST (double precision[] AS vector)
|
||||
WITH FUNCTION array_to_vector(double precision[], integer, boolean) AS ASSIGNMENT;
|
||||
|
||||
CREATE CAST (numeric[] AS vector)
|
||||
WITH FUNCTION array_to_vector(numeric[], integer, boolean) AS ASSIGNMENT;
|
||||
2
sql/vector--0.2.1--0.2.2.sql
Normal file
2
sql/vector--0.2.1--0.2.2.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
|
||||
\echo Use "ALTER EXTENSION vector UPDATE TO '0.2.2'" to load this file. \quit
|
||||
2
sql/vector--0.2.2--0.2.3.sql
Normal file
2
sql/vector--0.2.2--0.2.3.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
|
||||
\echo Use "ALTER EXTENSION vector UPDATE TO '0.2.3'" to load this file. \quit
|
||||
2
sql/vector--0.2.3--0.2.4.sql
Normal file
2
sql/vector--0.2.3--0.2.4.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
|
||||
\echo Use "ALTER EXTENSION vector UPDATE TO '0.2.4'" to load this file. \quit
|
||||
2
sql/vector--0.2.4--0.2.5.sql
Normal file
2
sql/vector--0.2.4--0.2.5.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
|
||||
\echo Use "ALTER EXTENSION vector UPDATE TO '0.2.5'" to load this file. \quit
|
||||
@@ -108,21 +108,21 @@ CREATE FUNCTION vector_to_float4(vector, integer, boolean) RETURNS real[]
|
||||
CREATE CAST (vector AS vector)
|
||||
WITH FUNCTION vector(vector, integer, boolean) AS IMPLICIT;
|
||||
|
||||
CREATE CAST (integer[] AS vector)
|
||||
WITH FUNCTION array_to_vector(integer[], integer, boolean) AS IMPLICIT;
|
||||
|
||||
CREATE CAST (real[] AS vector)
|
||||
WITH FUNCTION array_to_vector(real[], integer, boolean) AS IMPLICIT;
|
||||
|
||||
CREATE CAST (double precision[] AS vector)
|
||||
WITH FUNCTION array_to_vector(double precision[], integer, boolean) AS IMPLICIT;
|
||||
|
||||
CREATE CAST (numeric[] AS vector)
|
||||
WITH FUNCTION array_to_vector(numeric[], integer, boolean) AS IMPLICIT;
|
||||
|
||||
CREATE CAST (vector AS real[])
|
||||
WITH FUNCTION vector_to_float4(vector, integer, boolean) AS IMPLICIT;
|
||||
|
||||
CREATE CAST (integer[] AS vector)
|
||||
WITH FUNCTION array_to_vector(integer[], integer, boolean) AS ASSIGNMENT;
|
||||
|
||||
CREATE CAST (real[] AS vector)
|
||||
WITH FUNCTION array_to_vector(real[], integer, boolean) AS ASSIGNMENT;
|
||||
|
||||
CREATE CAST (double precision[] AS vector)
|
||||
WITH FUNCTION array_to_vector(double precision[], integer, boolean) AS ASSIGNMENT;
|
||||
|
||||
CREATE CAST (numeric[] AS vector)
|
||||
WITH FUNCTION array_to_vector(numeric[], integer, boolean) AS ASSIGNMENT;
|
||||
|
||||
-- operators
|
||||
|
||||
CREATE OPERATOR <-> (
|
||||
|
||||
236
src/ivfbuild.c
236
src/ivfbuild.c
@@ -7,8 +7,19 @@
|
||||
#include "miscadmin.h"
|
||||
#include "storage/bufmgr.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_SUBPHASE 0
|
||||
#define PROGRESS_CREATEIDX_TUPLES_TOTAL 0
|
||||
#define PROGRESS_CREATEIDX_TUPLES_DONE 0
|
||||
#endif
|
||||
|
||||
#if PG_VERSION_NUM >= 110000
|
||||
@@ -25,84 +36,11 @@
|
||||
#define CALLBACK_ITEM_POINTER HeapTuple hup
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Callback for sampling
|
||||
*/
|
||||
static void
|
||||
SampleCallback(Relation index, CALLBACK_ITEM_POINTER, Datum *values,
|
||||
bool *isnull, bool tupleIsAlive, void *state)
|
||||
{
|
||||
IvfflatBuildState *buildstate = (IvfflatBuildState *) state;
|
||||
VectorArray samples = buildstate->samples;
|
||||
int targsamples = samples->maxlen;
|
||||
Datum value = values[0];
|
||||
|
||||
/* Skip nulls */
|
||||
if (isnull[0])
|
||||
return;
|
||||
|
||||
/*
|
||||
* Normalize with KMEANS_NORM_PROC since spherical distance function
|
||||
* expects unit vectors
|
||||
*/
|
||||
if (buildstate->kmeansnormprocinfo != NULL)
|
||||
{
|
||||
if (!IvfflatNormValue(buildstate->kmeansnormprocinfo, buildstate->collation, &value, buildstate->normvec))
|
||||
return;
|
||||
}
|
||||
|
||||
if (samples->length < targsamples)
|
||||
{
|
||||
VectorArraySet(samples, samples->length, DatumGetVector(value));
|
||||
samples->length++;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (buildstate->rowstoskip < 0)
|
||||
buildstate->rowstoskip = reservoir_get_next_S(&buildstate->rstate, samples->length, targsamples);
|
||||
|
||||
if (buildstate->rowstoskip <= 0)
|
||||
{
|
||||
int k = (int) (targsamples * sampler_random_fract(buildstate->rstate.randstate));
|
||||
|
||||
Assert(k >= 0 && k < targsamples);
|
||||
VectorArraySet(samples, k, DatumGetVector(value));
|
||||
}
|
||||
|
||||
buildstate->rowstoskip -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Sample rows with same logic as ANALYZE
|
||||
*/
|
||||
static void
|
||||
SampleRows(IvfflatBuildState * buildstate)
|
||||
{
|
||||
int targsamples = buildstate->samples->maxlen;
|
||||
BlockNumber totalblocks = RelationGetNumberOfBlocks(buildstate->heap);
|
||||
|
||||
buildstate->rowstoskip = -1;
|
||||
|
||||
BlockSampler_Init(&buildstate->bs, totalblocks, targsamples, random());
|
||||
|
||||
reservoir_init_selection_state(&buildstate->rstate, targsamples);
|
||||
while (BlockSampler_HasMore(&buildstate->bs))
|
||||
{
|
||||
BlockNumber targblock = BlockSampler_Next(&buildstate->bs);
|
||||
|
||||
#if PG_VERSION_NUM >= 120000
|
||||
table_index_build_range_scan(buildstate->heap, buildstate->index, buildstate->indexInfo,
|
||||
false, true, true, targblock, 1, SampleCallback, (void *) buildstate, NULL);
|
||||
#elif PG_VERSION_NUM >= 110000
|
||||
IndexBuildHeapRangeScan(buildstate->heap, buildstate->index, buildstate->indexInfo,
|
||||
true, true, targblock, 1, SampleCallback, (void *) buildstate, NULL);
|
||||
#define UpdateProgress(index, val) pgstat_progress_update_param(index, val)
|
||||
#else
|
||||
IndexBuildHeapRangeScan(buildstate->heap, buildstate->index, buildstate->indexInfo,
|
||||
true, true, targblock, 1, SampleCallback, (void *) buildstate);
|
||||
#define UpdateProgress(index, val) ((void)val)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Callback for table_index_build_scan
|
||||
@@ -146,16 +84,20 @@ BuildCallback(Relation index, CALLBACK_ITEM_POINTER, Datum *values,
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef IVFFLAT_KMEANS_DEBUG
|
||||
buildstate->inertia += minDistance;
|
||||
buildstate->listSums[closestCenter] += minDistance;
|
||||
buildstate->listCounts[closestCenter]++;
|
||||
#endif
|
||||
|
||||
/* Create a virtual tuple */
|
||||
ExecClearTuple(slot);
|
||||
slot->tts_values[0] = Int32GetDatum(closestCenter);
|
||||
slot->tts_isnull[0] = false;
|
||||
slot->tts_values[1] = Int32GetDatum(ItemPointerGetBlockNumberNoCheck(tid));
|
||||
slot->tts_values[1] = PointerGetDatum(tid);
|
||||
slot->tts_isnull[1] = false;
|
||||
slot->tts_values[2] = Int32GetDatum(ItemPointerGetOffsetNumberNoCheck(tid));
|
||||
slot->tts_values[2] = value;
|
||||
slot->tts_isnull[2] = false;
|
||||
slot->tts_values[3] = value;
|
||||
slot->tts_isnull[3] = false;
|
||||
ExecStoreVirtualTuple(slot);
|
||||
|
||||
/*
|
||||
@@ -165,6 +107,8 @@ BuildCallback(Relation index, CALLBACK_ITEM_POINTER, Datum *values,
|
||||
* need not save it.
|
||||
*/
|
||||
tuplesort_puttupleslot(buildstate->sortstate, slot);
|
||||
|
||||
buildstate->indtuples++;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -175,8 +119,6 @@ GetNextTuple(Tuplesortstate *sortstate, TupleDesc tupdesc, TupleTableSlot *slot,
|
||||
{
|
||||
Datum value;
|
||||
bool isnull;
|
||||
int tupblk;
|
||||
int tupoff;
|
||||
|
||||
#if PG_VERSION_NUM >= 100000
|
||||
if (tuplesort_gettupleslot(sortstate, true, false, slot, NULL))
|
||||
@@ -185,13 +127,11 @@ GetNextTuple(Tuplesortstate *sortstate, TupleDesc tupdesc, TupleTableSlot *slot,
|
||||
#endif
|
||||
{
|
||||
*list = DatumGetInt32(slot_getattr(slot, 1, &isnull));
|
||||
tupblk = DatumGetInt32(slot_getattr(slot, 2, &isnull));
|
||||
tupoff = DatumGetInt32(slot_getattr(slot, 3, &isnull));
|
||||
value = slot_getattr(slot, 4, &isnull);
|
||||
value = slot_getattr(slot, 3, &isnull);
|
||||
|
||||
/* Form the index tuple */
|
||||
*itup = index_form_tuple(tupdesc, &value, &isnull);
|
||||
ItemPointerSet(&(*itup)->t_tid, tupblk, tupoff);
|
||||
(*itup)->t_tid = *((ItemPointer) DatumGetPointer(slot_getattr(slot, 2, &isnull)));
|
||||
}
|
||||
else
|
||||
*list = -1;
|
||||
@@ -212,6 +152,7 @@ InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum)
|
||||
BlockNumber insertPage = InvalidBlockNumber;
|
||||
Size itemsz;
|
||||
int i;
|
||||
int64 inserted = 0;
|
||||
|
||||
#if PG_VERSION_NUM >= 120000
|
||||
TupleTableSlot *slot = MakeSingleTupleTableSlot(buildstate->tupdesc, &TTSOpsMinimalTuple);
|
||||
@@ -220,10 +161,18 @@ InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum)
|
||||
#endif
|
||||
TupleDesc tupdesc = RelationGetDescr(index);
|
||||
|
||||
UpdateProgress(PROGRESS_CREATEIDX_SUBPHASE, PROGRESS_IVFFLAT_PHASE_LOAD);
|
||||
|
||||
UpdateProgress(PROGRESS_CREATEIDX_TUPLES_TOTAL, buildstate->indtuples);
|
||||
|
||||
GetNextTuple(buildstate->sortstate, tupdesc, slot, &itup, &list);
|
||||
|
||||
for (i = 0; i < buildstate->centers->length; i++)
|
||||
{
|
||||
/* Can take a while, so ensure we can interrupt */
|
||||
/* Needs to be called when no buffer locks are held */
|
||||
CHECK_FOR_INTERRUPTS();
|
||||
|
||||
buf = IvfflatNewBuffer(index, forkNum);
|
||||
IvfflatInitPage(index, &buf, &page, &state);
|
||||
|
||||
@@ -243,7 +192,7 @@ InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum)
|
||||
|
||||
pfree(itup);
|
||||
|
||||
buildstate->indtuples += 1;
|
||||
UpdateProgress(PROGRESS_CREATEIDX_TUPLES_DONE, ++inserted);
|
||||
|
||||
GetNextTuple(buildstate->sortstate, tupdesc, slot, &itup, &list);
|
||||
}
|
||||
@@ -253,7 +202,7 @@ InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum)
|
||||
IvfflatCommitBuffer(buf, state);
|
||||
|
||||
/* Set the start and insert pages */
|
||||
IvfflatUpdateList(index, state, buildstate->listInfo[i], insertPage, startPage, forkNum);
|
||||
IvfflatUpdateList(index, state, buildstate->listInfo[i], insertPage, InvalidBlockNumber, startPage, forkNum);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,19 +232,24 @@ InitBuildState(IvfflatBuildState * buildstate, Relation heap, Relation index, In
|
||||
buildstate->kmeansnormprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_KMEANS_NORM_PROC);
|
||||
buildstate->collation = index->rd_indcollation[0];
|
||||
|
||||
/* Require more than one dimension for spherical k-means */
|
||||
/* 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");
|
||||
|
||||
/* Create tuple description for sorting */
|
||||
#if PG_VERSION_NUM >= 120000
|
||||
buildstate->tupdesc = CreateTemplateTupleDesc(4);
|
||||
buildstate->tupdesc = CreateTemplateTupleDesc(3);
|
||||
#else
|
||||
buildstate->tupdesc = CreateTemplateTupleDesc(4, false);
|
||||
buildstate->tupdesc = CreateTemplateTupleDesc(3, false);
|
||||
#endif
|
||||
TupleDescInitEntry(buildstate->tupdesc, (AttrNumber) 1, "list", INT4OID, -1, 0);
|
||||
TupleDescInitEntry(buildstate->tupdesc, (AttrNumber) 2, "blkno", INT4OID, -1, 0);
|
||||
TupleDescInitEntry(buildstate->tupdesc, (AttrNumber) 3, "offset", INT4OID, -1, 0);
|
||||
TupleDescInitEntry(buildstate->tupdesc, (AttrNumber) 2, "tid", TIDOID, -1, 0);
|
||||
#if PG_VERSION_NUM >= 110000
|
||||
TupleDescInitEntry(buildstate->tupdesc, (AttrNumber) 4, "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) 4, "vector", RelationGetDescr(index)->attrs[0]->atttypid, -1, 0);
|
||||
TupleDescInitEntry(buildstate->tupdesc, (AttrNumber) 3, "vector", RelationGetDescr(index)->attrs[0]->atttypid, -1, 0);
|
||||
#endif
|
||||
|
||||
#if PG_VERSION_NUM >= 120000
|
||||
@@ -309,6 +263,12 @@ InitBuildState(IvfflatBuildState * buildstate, Relation heap, Relation index, In
|
||||
|
||||
/* Reuse for each tuple */
|
||||
buildstate->normvec = InitVector(buildstate->dimensions);
|
||||
|
||||
#ifdef IVFFLAT_KMEANS_DEBUG
|
||||
buildstate->inertia = 0;
|
||||
buildstate->listSums = palloc0(sizeof(double) * buildstate->lists);
|
||||
buildstate->listCounts = palloc0(sizeof(int) * buildstate->lists);
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -320,32 +280,11 @@ FreeBuildState(IvfflatBuildState * buildstate)
|
||||
pfree(buildstate->centers);
|
||||
pfree(buildstate->listInfo);
|
||||
pfree(buildstate->normvec);
|
||||
}
|
||||
|
||||
/*
|
||||
* Compute centers
|
||||
*/
|
||||
static void
|
||||
ComputeCenters(IvfflatBuildState * buildstate)
|
||||
{
|
||||
int numSamples;
|
||||
|
||||
/* Target 50 samples per list, with at least 10000 samples */
|
||||
/* The number of samples has a large effect on index build time */
|
||||
numSamples = buildstate->lists * 50;
|
||||
if (numSamples < 10000)
|
||||
numSamples = 10000;
|
||||
|
||||
/* Sample samples */
|
||||
buildstate->samples = VectorArrayInit(numSamples, buildstate->dimensions);
|
||||
if (buildstate->heap != NULL)
|
||||
SampleRows(buildstate);
|
||||
|
||||
/* Calculate centers */
|
||||
IvfflatKmeans(buildstate->index, buildstate->samples, buildstate->centers);
|
||||
|
||||
/* Free samples before we allocate more memory */
|
||||
pfree(buildstate->samples);
|
||||
#ifdef IVFFLAT_KMEANS_DEBUG
|
||||
pfree(buildstate->listSums);
|
||||
pfree(buildstate->listCounts);
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -421,6 +360,51 @@ CreateListPages(Relation index, VectorArray centers, int dimensions,
|
||||
pfree(list);
|
||||
}
|
||||
|
||||
/*
|
||||
* Print k-means metrics
|
||||
*/
|
||||
#ifdef IVFFLAT_KMEANS_DEBUG
|
||||
static void
|
||||
PrintKmeansMetrics(IvfflatBuildState * buildstate)
|
||||
{
|
||||
elog(INFO, "inertia: %.3e", buildstate->inertia);
|
||||
|
||||
/* Calculate Davies-Bouldin index */
|
||||
if (buildstate->lists > 1)
|
||||
{
|
||||
double db = 0.0;
|
||||
|
||||
/* Calculate average distance */
|
||||
for (int i = 0; i < buildstate->lists; i++)
|
||||
{
|
||||
if (buildstate->listCounts[i] > 0)
|
||||
buildstate->listSums[i] /= buildstate->listCounts[i];
|
||||
}
|
||||
|
||||
for (int i = 0; i < buildstate->lists; i++)
|
||||
{
|
||||
double max = 0.0;
|
||||
double distance;
|
||||
|
||||
for (int j = 0; j < buildstate->lists; j++)
|
||||
{
|
||||
if (j == i)
|
||||
continue;
|
||||
|
||||
distance = DatumGetFloat8(FunctionCall2Coll(buildstate->procinfo, buildstate->collation, PointerGetDatum(VectorArrayGet(buildstate->centers, i)), PointerGetDatum(VectorArrayGet(buildstate->centers, j))));
|
||||
distance = (buildstate->listSums[i] + buildstate->listSums[j]) / distance;
|
||||
|
||||
if (distance > max)
|
||||
max = distance;
|
||||
}
|
||||
db += max;
|
||||
}
|
||||
db /= buildstate->lists;
|
||||
elog(INFO, "davies-bouldin: %.3f", db);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Create entry pages
|
||||
*/
|
||||
@@ -432,6 +416,8 @@ CreateEntryPages(IvfflatBuildState * buildstate, ForkNumber forkNum)
|
||||
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
|
||||
@@ -453,8 +439,14 @@ CreateEntryPages(IvfflatBuildState * buildstate, ForkNumber forkNum)
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Sort and insert */
|
||||
/* Sort */
|
||||
tuplesort_performsort(buildstate->sortstate);
|
||||
|
||||
#ifdef IVFFLAT_KMEANS_DEBUG
|
||||
PrintKmeansMetrics(buildstate);
|
||||
#endif
|
||||
|
||||
/* Insert */
|
||||
InsertTuples(buildstate->index, buildstate, forkNum);
|
||||
tuplesort_end(buildstate->sortstate);
|
||||
}
|
||||
@@ -468,12 +460,14 @@ BuildIndex(Relation heap, Relation index, IndexInfo *indexInfo,
|
||||
{
|
||||
InitBuildState(buildstate, heap, index, indexInfo);
|
||||
|
||||
ComputeCenters(buildstate);
|
||||
/* Perform k-means clustering */
|
||||
UpdateProgress(PROGRESS_CREATEIDX_SUBPHASE, PROGRESS_IVFFLAT_PHASE_KMEANS);
|
||||
IvfflatBench("k-means", IvfflatKmeans(buildstate));
|
||||
|
||||
/* Create pages */
|
||||
CreateMetaPage(index, buildstate->dimensions, buildstate->lists, forkNum);
|
||||
CreateListPages(index, buildstate->centers, buildstate->dimensions, buildstate->lists, forkNum, &buildstate->listInfo);
|
||||
CreateEntryPages(buildstate, forkNum);
|
||||
IvfflatBench("CreateEntryPages", CreateEntryPages(buildstate, forkNum));
|
||||
|
||||
FreeBuildState(buildstate);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
#include "utils/guc.h"
|
||||
#include "utils/selfuncs.h"
|
||||
|
||||
#if PG_VERSION_NUM >= 120000
|
||||
#include "commands/progress.h"
|
||||
#endif
|
||||
|
||||
int ivfflat_probes;
|
||||
static relopt_kind ivfflat_relopt_kind;
|
||||
|
||||
@@ -30,6 +34,29 @@ _PG_init(void)
|
||||
1, 1, IVFFLAT_MAX_LISTS, PGC_USERSET, 0, NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
/*
|
||||
* Get the name of index build phase
|
||||
*/
|
||||
#if PG_VERSION_NUM >= 120000
|
||||
static char *
|
||||
ivfflatbuildphasename(int64 phasenum)
|
||||
{
|
||||
switch (phasenum)
|
||||
{
|
||||
case PROGRESS_CREATEIDX_SUBPHASE_INITIALIZE:
|
||||
return "initializing";
|
||||
case PROGRESS_IVFFLAT_PHASE_KMEANS:
|
||||
return "performing k-means";
|
||||
case PROGRESS_IVFFLAT_PHASE_SORT:
|
||||
return "sorting tuples";
|
||||
case PROGRESS_IVFFLAT_PHASE_LOAD:
|
||||
return "loading tuples";
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Estimate the cost of an index scan
|
||||
*/
|
||||
@@ -130,6 +157,11 @@ ivfflatvalidate(Oid opclassoid)
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Define index handler
|
||||
*
|
||||
* See https://www.postgresql.org/docs/current/index-api.html
|
||||
*/
|
||||
PG_FUNCTION_INFO_V1(ivfflathandler);
|
||||
Datum
|
||||
ivfflathandler(PG_FUNCTION_ARGS)
|
||||
@@ -160,23 +192,27 @@ ivfflathandler(PG_FUNCTION_ARGS)
|
||||
#endif
|
||||
#if PG_VERSION_NUM >= 130000
|
||||
amroutine->amusemaintenanceworkmem = false; /* not used during VACUUM */
|
||||
amroutine->amparallelvacuumoptions = VACUUM_OPTION_NO_PARALLEL; /* TODO support parallel */
|
||||
amroutine->amparallelvacuumoptions = VACUUM_OPTION_PARALLEL_BULKDEL;
|
||||
#endif
|
||||
amroutine->amkeytype = InvalidOid;
|
||||
|
||||
/* Interface functions */
|
||||
amroutine->ambuild = ivfflatbuild;
|
||||
amroutine->ambuildempty = ivfflatbuildempty;
|
||||
amroutine->aminsert = ivfflatinsert;
|
||||
amroutine->ambulkdelete = ivfflatbulkdelete;
|
||||
amroutine->amvacuumcleanup = ivfflatvacuumcleanup;
|
||||
amroutine->amcanreturn = NULL;
|
||||
amroutine->amcanreturn = NULL; /* tuple not included in heapsort */
|
||||
amroutine->amcostestimate = ivfflatcostestimate;
|
||||
amroutine->amoptions = ivfflatoptions;
|
||||
amroutine->amproperty = NULL; /* TODO AMPROP_DISTANCE_ORDERABLE */
|
||||
#if PG_VERSION_NUM >= 120000
|
||||
amroutine->ambuildphasename = NULL;
|
||||
amroutine->ambuildphasename = ivfflatbuildphasename;
|
||||
#endif
|
||||
amroutine->amvalidate = ivfflatvalidate;
|
||||
#if PG_VERSION_NUM >= 140000
|
||||
amroutine->amadjustmembers = NULL;
|
||||
#endif
|
||||
amroutine->ambeginscan = ivfflatbeginscan;
|
||||
amroutine->amrescan = ivfflatrescan;
|
||||
amroutine->amgettuple = ivfflatgettuple;
|
||||
@@ -184,6 +220,8 @@ ivfflathandler(PG_FUNCTION_ARGS)
|
||||
amroutine->amendscan = ivfflatendscan;
|
||||
amroutine->ammarkpos = NULL;
|
||||
amroutine->amrestrpos = NULL;
|
||||
|
||||
/* Interface functions to support parallel index scans */
|
||||
#if PG_VERSION_NUM >= 100000
|
||||
amroutine->amestimateparallelscan = NULL;
|
||||
amroutine->aminitparallelscan = NULL;
|
||||
|
||||
@@ -10,6 +10,14 @@
|
||||
#include "utils/tuplesort.h"
|
||||
#include "vector.h"
|
||||
|
||||
#ifdef IVFFLAT_BENCH
|
||||
#include "portability/instr_time.h"
|
||||
#endif
|
||||
|
||||
#if PG_VERSION_NUM < 90600
|
||||
#error "Requires PostgreSQL 9.6+"
|
||||
#endif
|
||||
|
||||
/* Support functions */
|
||||
#define IVFFLAT_DISTANCE_PROC 1
|
||||
#define IVFFLAT_NORM_PROC 2
|
||||
@@ -27,14 +35,30 @@
|
||||
#define IVFFLAT_DEFAULT_LISTS 100
|
||||
#define IVFFLAT_MAX_LISTS 32768
|
||||
|
||||
/* Build phases */
|
||||
/* PROGRESS_CREATEIDX_SUBPHASE_INITIALIZE is 1 */
|
||||
#define PROGRESS_IVFFLAT_PHASE_KMEANS 2
|
||||
#define PROGRESS_IVFFLAT_PHASE_SORT 3
|
||||
#define PROGRESS_IVFFLAT_PHASE_LOAD 4
|
||||
|
||||
#define IVFFLAT_LIST_SIZE(_dim) (offsetof(IvfflatListData, center) + VECTOR_SIZE(_dim))
|
||||
|
||||
#define IvfflatPageGetOpaque(page) ((IvfflatPageOpaque) PageGetSpecialPointer(page))
|
||||
#define IvfflatPageGetMeta(page) ((IvfflatMetaPageData *) PageGetContents(page))
|
||||
|
||||
#if PG_VERSION_NUM < 100000
|
||||
#define ItemPointerGetBlockNumberNoCheck ItemPointerGetBlockNumber
|
||||
#define ItemPointerGetOffsetNumberNoCheck ItemPointerGetOffsetNumber
|
||||
#ifdef IVFFLAT_BENCH
|
||||
#define IvfflatBench(name, code) \
|
||||
do { \
|
||||
instr_time start; \
|
||||
instr_time duration; \
|
||||
INSTR_TIME_SET_CURRENT(start); \
|
||||
(code); \
|
||||
INSTR_TIME_SET_CURRENT(duration); \
|
||||
INSTR_TIME_SUBTRACT(duration, start); \
|
||||
elog(INFO, "%s: %.3f ms", name, INSTR_TIME_GET_MILLISEC(duration)); \
|
||||
} while (0)
|
||||
#else
|
||||
#define IvfflatBench(name, code) (code)
|
||||
#endif
|
||||
|
||||
/* Variables */
|
||||
@@ -90,6 +114,12 @@ typedef struct IvfflatBuildState
|
||||
ListInfo *listInfo;
|
||||
Vector *normvec;
|
||||
|
||||
#ifdef IVFFLAT_KMEANS_DEBUG
|
||||
double inertia;
|
||||
double *listSums;
|
||||
int *listCounts;
|
||||
#endif
|
||||
|
||||
/* Sampling */
|
||||
BlockSamplerData bs;
|
||||
ReservoirStateData rstate;
|
||||
@@ -131,6 +161,7 @@ typedef IvfflatListData * IvfflatList;
|
||||
|
||||
typedef struct IvfflatScanList
|
||||
{
|
||||
pairingheap_node ph_node;
|
||||
BlockNumber startPage;
|
||||
double distance;
|
||||
} IvfflatScanList;
|
||||
@@ -152,6 +183,8 @@ typedef struct IvfflatScanOpaqueData
|
||||
FmgrInfo *normprocinfo;
|
||||
Oid collation;
|
||||
|
||||
/* Lists */
|
||||
pairingheap *listQueue;
|
||||
IvfflatScanList lists[FLEXIBLE_ARRAY_MEMBER]; /* must come last */
|
||||
} IvfflatScanOpaqueData;
|
||||
|
||||
@@ -166,11 +199,11 @@ typedef IvfflatScanOpaqueData * IvfflatScanOpaque;
|
||||
void _PG_init(void);
|
||||
VectorArray VectorArrayInit(int maxlen, int dimensions);
|
||||
void PrintVectorArray(char *msg, VectorArray arr);
|
||||
void IvfflatKmeans(Relation index, VectorArray samples, VectorArray centers);
|
||||
void IvfflatKmeans(IvfflatBuildState * buildstate);
|
||||
FmgrInfo *IvfflatOptionalProcInfo(Relation rel, uint16 procnum);
|
||||
bool IvfflatNormValue(FmgrInfo *procinfo, Oid collation, Datum *value, Vector * result);
|
||||
int IvfflatGetLists(Relation index);
|
||||
void IvfflatUpdateList(Relation index, GenericXLogState *state, ListInfo listInfo, BlockNumber insertPage, BlockNumber startPage, ForkNumber forkNum);
|
||||
void IvfflatUpdateList(Relation index, GenericXLogState *state, ListInfo listInfo, BlockNumber insertPage, BlockNumber originalInsertPage, BlockNumber startPage, ForkNumber forkNum);
|
||||
void IvfflatCommitBuffer(Buffer buf, GenericXLogState *state);
|
||||
void IvfflatAppendPage(Relation index, Buffer *buf, Page *page, GenericXLogState **state, ForkNumber forkNum);
|
||||
Buffer IvfflatNewBuffer(Relation index, ForkNumber forkNum);
|
||||
@@ -180,6 +213,9 @@ void IvfflatInitPage(Relation index, Buffer *buf, Page *page, GenericXLogState
|
||||
IndexBuildResult *ivfflatbuild(Relation heap, Relation index, IndexInfo *indexInfo);
|
||||
void ivfflatbuildempty(Relation index);
|
||||
bool ivfflatinsert(Relation index, Datum *values, bool *isnull, ItemPointer heap_tid, Relation heap, IndexUniqueCheck checkUnique
|
||||
#if PG_VERSION_NUM >= 140000
|
||||
,bool indexUnchanged
|
||||
#endif
|
||||
#if PG_VERSION_NUM >= 100000
|
||||
,IndexInfo *indexInfo
|
||||
#endif
|
||||
|
||||
@@ -77,11 +77,12 @@ InsertTuple(Relation rel, IndexTuple itup, Relation heapRel, Datum *values)
|
||||
Size itemsz;
|
||||
BlockNumber insertPage = InvalidBlockNumber;
|
||||
ListInfo listInfo;
|
||||
bool newPage = false;
|
||||
BlockNumber originalInsertPage;
|
||||
|
||||
/* Find the insert page - sets the page and list info */
|
||||
FindInsertPage(rel, values, &insertPage, &listInfo);
|
||||
Assert(BlockNumberIsValid(insertPage));
|
||||
originalInsertPage = insertPage;
|
||||
|
||||
itemsz = MAXALIGN(IndexTupleSize(itup));
|
||||
Assert(itemsz <= BLCKSZ - MAXALIGN(SizeOfPageHeaderData) - MAXALIGN(sizeof(IvfflatPageOpaqueData)));
|
||||
@@ -107,7 +108,6 @@ InsertTuple(Relation rel, IndexTuple itup, Relation heapRel, Datum *values)
|
||||
IvfflatAppendPage(rel, &buf, &page, &state, MAIN_FORKNUM);
|
||||
|
||||
insertPage = BufferGetBlockNumber(buf);
|
||||
newPage = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,8 +118,8 @@ InsertTuple(Relation rel, IndexTuple itup, Relation heapRel, Datum *values)
|
||||
IvfflatCommitBuffer(buf, state);
|
||||
|
||||
/* Update the insert page */
|
||||
if (newPage)
|
||||
IvfflatUpdateList(rel, state, listInfo, insertPage, InvalidBlockNumber, MAIN_FORKNUM);
|
||||
if (insertPage != originalInsertPage)
|
||||
IvfflatUpdateList(rel, state, listInfo, insertPage, originalInsertPage, InvalidBlockNumber, MAIN_FORKNUM);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -128,6 +128,9 @@ InsertTuple(Relation rel, IndexTuple itup, Relation heapRel, Datum *values)
|
||||
bool
|
||||
ivfflatinsert(Relation index, Datum *values, bool *isnull, ItemPointer heap_tid,
|
||||
Relation heap, IndexUniqueCheck checkUnique
|
||||
#if PG_VERSION_NUM >= 140000
|
||||
,bool indexUnchanged
|
||||
#endif
|
||||
#if PG_VERSION_NUM >= 100000
|
||||
,IndexInfo *indexInfo
|
||||
#endif
|
||||
|
||||
461
src/ivfkmeans.c
461
src/ivfkmeans.c
@@ -2,8 +2,20 @@
|
||||
|
||||
#include <float.h>
|
||||
|
||||
#include "catalog/index.h"
|
||||
#include "ivfflat.h"
|
||||
#include "miscadmin.h"
|
||||
#include "storage/bufmgr.h"
|
||||
|
||||
#if PG_VERSION_NUM >= 120000
|
||||
#include "access/tableam.h"
|
||||
#endif
|
||||
|
||||
#if PG_VERSION_NUM >= 130000
|
||||
#define CALLBACK_ITEM_POINTER ItemPointer tid
|
||||
#else
|
||||
#define CALLBACK_ITEM_POINTER HeapTuple hup
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Initialize with kmeans++
|
||||
@@ -11,7 +23,7 @@
|
||||
* https://theory.stanford.edu/~sergei/papers/kMeansPP-soda.pdf
|
||||
*/
|
||||
static void
|
||||
InitCenters(Relation index, VectorArray samples, VectorArray centers, double *lowerBound)
|
||||
InitCenters(Relation index, VectorArray samples, VectorArray centers)
|
||||
{
|
||||
FmgrInfo *procinfo;
|
||||
Oid collation;
|
||||
@@ -21,7 +33,7 @@ InitCenters(Relation index, VectorArray samples, VectorArray centers, double *lo
|
||||
double sum;
|
||||
double choice;
|
||||
Vector *vec;
|
||||
double *weight = palloc(samples->length * sizeof(double));
|
||||
float *weight = palloc(samples->length * sizeof(float));
|
||||
int numCenters = centers->maxlen;
|
||||
int numSamples = samples->length;
|
||||
|
||||
@@ -35,7 +47,7 @@ InitCenters(Relation index, VectorArray samples, VectorArray centers, double *lo
|
||||
for (j = 0; j < numSamples; j++)
|
||||
weight[j] = DBL_MAX;
|
||||
|
||||
for (i = 0; i < numCenters; i++)
|
||||
for (i = 0; i < numCenters - 1; i++)
|
||||
{
|
||||
CHECK_FOR_INTERRUPTS();
|
||||
|
||||
@@ -49,9 +61,6 @@ InitCenters(Relation index, VectorArray samples, VectorArray centers, double *lo
|
||||
/* TODO Use triangle inequality to reduce distance calculations */
|
||||
distance = DatumGetFloat8(FunctionCall2Coll(procinfo, collation, PointerGetDatum(vec), PointerGetDatum(VectorArrayGet(centers, i))));
|
||||
|
||||
/* Set lower bound */
|
||||
lowerBound[j * numCenters + i] = distance;
|
||||
|
||||
/* Use distance squared for weighted probability distribution */
|
||||
distance *= distance;
|
||||
|
||||
@@ -61,10 +70,6 @@ InitCenters(Relation index, VectorArray samples, VectorArray centers, double *lo
|
||||
sum += weight[j];
|
||||
}
|
||||
|
||||
/* Only compute lower bound on last iteration */
|
||||
if (i + 1 == numCenters)
|
||||
break;
|
||||
|
||||
/* Choose new center using weighted probability distribution. */
|
||||
choice = sum * (((double) random()) / MAX_RANDOM_VALUE);
|
||||
for (j = 0; j < numSamples - 1; j++)
|
||||
@@ -121,15 +126,18 @@ QuickCenters(Relation index, VectorArray samples, VectorArray centers)
|
||||
FmgrInfo *normprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_KMEANS_NORM_PROC);
|
||||
|
||||
/* Copy existing vectors while avoiding duplicates */
|
||||
qsort(samples->items, samples->length, VECTOR_SIZE(samples->dim), CompareVectors);
|
||||
for (i = 0; i < samples->length; i++)
|
||||
if (samples->length > 0)
|
||||
{
|
||||
vec = VectorArrayGet(samples, i);
|
||||
|
||||
if (i == 0 || CompareVectors(vec, VectorArrayGet(samples, i - 1)) != 0)
|
||||
qsort(samples->items, samples->length, VECTOR_SIZE(samples->dim), CompareVectors);
|
||||
for (i = 0; i < samples->length; i++)
|
||||
{
|
||||
VectorArraySet(centers, centers->length, vec);
|
||||
centers->length++;
|
||||
vec = VectorArrayGet(samples, i);
|
||||
|
||||
if (i == 0 || CompareVectors(vec, VectorArrayGet(samples, i - 1)) != 0)
|
||||
{
|
||||
VectorArraySet(centers, centers->length, vec);
|
||||
centers->length++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,275 +161,202 @@ QuickCenters(Relation index, VectorArray samples, VectorArray centers)
|
||||
}
|
||||
|
||||
/*
|
||||
* Use Elkan for performance. This requires distance function to satisfy triangle inequality.
|
||||
* Callback for sampling
|
||||
*/
|
||||
static void
|
||||
SampleCallback(Relation index, CALLBACK_ITEM_POINTER, Datum *values,
|
||||
bool *isnull, bool tupleIsAlive, void *state)
|
||||
{
|
||||
IvfflatBuildState *buildstate = (IvfflatBuildState *) state;
|
||||
VectorArray samples = buildstate->samples;
|
||||
int targsamples = samples->maxlen;
|
||||
Datum value = values[0];
|
||||
|
||||
/* Skip nulls */
|
||||
if (isnull[0])
|
||||
return;
|
||||
|
||||
/*
|
||||
* Normalize with KMEANS_NORM_PROC since spherical distance function
|
||||
* expects unit vectors
|
||||
*/
|
||||
if (buildstate->kmeansnormprocinfo != NULL)
|
||||
{
|
||||
if (!IvfflatNormValue(buildstate->kmeansnormprocinfo, buildstate->collation, &value, buildstate->normvec))
|
||||
return;
|
||||
}
|
||||
|
||||
if (samples->length < targsamples)
|
||||
{
|
||||
VectorArraySet(samples, samples->length, DatumGetVector(value));
|
||||
samples->length++;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (buildstate->rowstoskip < 0)
|
||||
buildstate->rowstoskip = reservoir_get_next_S(&buildstate->rstate, samples->length, targsamples);
|
||||
|
||||
if (buildstate->rowstoskip <= 0)
|
||||
{
|
||||
int k = (int) (targsamples * sampler_random_fract(buildstate->rstate.randstate));
|
||||
|
||||
Assert(k >= 0 && k < targsamples);
|
||||
VectorArraySet(samples, k, DatumGetVector(value));
|
||||
}
|
||||
|
||||
buildstate->rowstoskip -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Sample rows with same logic as ANALYZE
|
||||
*/
|
||||
static void
|
||||
SampleRows(IvfflatBuildState * buildstate)
|
||||
{
|
||||
int targsamples = buildstate->samples->maxlen;
|
||||
BlockNumber totalblocks = RelationGetNumberOfBlocks(buildstate->heap);
|
||||
|
||||
buildstate->rowstoskip = -1;
|
||||
buildstate->samples->length = 0;
|
||||
|
||||
BlockSampler_Init(&buildstate->bs, totalblocks, targsamples, random());
|
||||
|
||||
reservoir_init_selection_state(&buildstate->rstate, targsamples);
|
||||
while (BlockSampler_HasMore(&buildstate->bs))
|
||||
{
|
||||
BlockNumber targblock = BlockSampler_Next(&buildstate->bs);
|
||||
|
||||
#if PG_VERSION_NUM >= 120000
|
||||
table_index_build_range_scan(buildstate->heap, buildstate->index, buildstate->indexInfo,
|
||||
false, true, false, targblock, 1, SampleCallback, (void *) buildstate, NULL);
|
||||
#elif PG_VERSION_NUM >= 110000
|
||||
IndexBuildHeapRangeScan(buildstate->heap, buildstate->index, buildstate->indexInfo,
|
||||
false, true, targblock, 1, SampleCallback, (void *) buildstate, NULL);
|
||||
#else
|
||||
IndexBuildHeapRangeScan(buildstate->heap, buildstate->index, buildstate->indexInfo,
|
||||
false, true, targblock, 1, SampleCallback, (void *) buildstate);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Use mini-batch k-means
|
||||
*
|
||||
* We use L2 distance for L2 (not L2 squared like index scan)
|
||||
* and angular distance for inner product and cosine distance
|
||||
*
|
||||
* https://www.aaai.org/Papers/ICML/2003/ICML03-022.pdf
|
||||
* https://www.eecs.tufts.edu/~dsculley/papers/fastkmeans.pdf
|
||||
*/
|
||||
static void
|
||||
ElkanKmeans(Relation index, VectorArray samples, VectorArray centers)
|
||||
MiniBatchKmeans(IvfflatBuildState * buildstate)
|
||||
{
|
||||
FmgrInfo *procinfo;
|
||||
FmgrInfo *normprocinfo;
|
||||
Oid collation;
|
||||
Vector *vec;
|
||||
Vector *newCenter;
|
||||
int iteration;
|
||||
int j;
|
||||
int k;
|
||||
int dimensions = centers->dim;
|
||||
int numCenters = centers->maxlen;
|
||||
int numSamples = samples->length;
|
||||
VectorArray newCenters;
|
||||
int *centerCounts;
|
||||
int *closestCenters;
|
||||
double *lowerBound;
|
||||
double *upperBound;
|
||||
double *s;
|
||||
double *halfcdist;
|
||||
double *newcdist;
|
||||
int changes;
|
||||
VectorArray centers = buildstate->centers;
|
||||
VectorArray m = buildstate->samples;
|
||||
int b = m->maxlen;
|
||||
int t = 20;
|
||||
double distance;
|
||||
double minDistance;
|
||||
int closestCenter;
|
||||
double distance;
|
||||
bool rj;
|
||||
bool rjreset;
|
||||
double dxcx;
|
||||
double dxc;
|
||||
int i;
|
||||
int j;
|
||||
int k;
|
||||
Vector *c;
|
||||
Vector *x;
|
||||
int *v;
|
||||
int *d;
|
||||
double eta;
|
||||
|
||||
/* Set support functions */
|
||||
procinfo = index_getprocinfo(index, 1, IVFFLAT_KMEANS_DISTANCE_PROC);
|
||||
normprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_KMEANS_NORM_PROC);
|
||||
collation = index->rd_indcollation[0];
|
||||
|
||||
/* Allocate space */
|
||||
centerCounts = palloc(sizeof(int) * numCenters);
|
||||
closestCenters = palloc(sizeof(int) * numSamples);
|
||||
lowerBound = palloc(sizeof(double) * numSamples * numCenters);
|
||||
upperBound = palloc(sizeof(double) * numSamples);
|
||||
s = palloc(sizeof(double) * numCenters);
|
||||
halfcdist = palloc(sizeof(double) * numCenters * numCenters);
|
||||
newcdist = palloc(sizeof(double) * numCenters);
|
||||
|
||||
newCenters = VectorArrayInit(numCenters, dimensions);
|
||||
for (j = 0; j < numCenters; j++)
|
||||
{
|
||||
vec = VectorArrayGet(newCenters, j);
|
||||
SET_VARSIZE(vec, VECTOR_SIZE(dimensions));
|
||||
vec->dim = dimensions;
|
||||
}
|
||||
FmgrInfo *procinfo = index_getprocinfo(buildstate->index, 1, IVFFLAT_KMEANS_DISTANCE_PROC);
|
||||
FmgrInfo *normprocinfo = buildstate->kmeansnormprocinfo;
|
||||
Oid collation = buildstate->index->rd_indcollation[0];
|
||||
|
||||
/* Pick initial centers */
|
||||
InitCenters(index, samples, centers, lowerBound);
|
||||
InitCenters(buildstate->index, buildstate->samples, buildstate->centers);
|
||||
|
||||
/* Assign each x to its closest initial center c(x) = argmin d(x,c) */
|
||||
for (j = 0; j < numSamples; j++)
|
||||
{
|
||||
minDistance = DBL_MAX;
|
||||
closestCenter = -1;
|
||||
v = palloc(sizeof(int) * centers->maxlen);
|
||||
d = palloc(sizeof(int) * b);
|
||||
|
||||
vec = VectorArrayGet(samples, j);
|
||||
for (int i = 0; i < centers->length; i++)
|
||||
v[i] = 0;
|
||||
|
||||
/* Find closest center */
|
||||
for (k = 0; k < numCenters; k++)
|
||||
{
|
||||
/* TODO Use Lemma 1 in k-means++ initialization */
|
||||
distance = lowerBound[j * numCenters + k];
|
||||
|
||||
if (distance < minDistance)
|
||||
{
|
||||
minDistance = distance;
|
||||
closestCenter = k;
|
||||
}
|
||||
}
|
||||
|
||||
upperBound[j] = minDistance;
|
||||
closestCenters[j] = closestCenter;
|
||||
}
|
||||
|
||||
/* Give 500 iterations to converge */
|
||||
for (iteration = 0; iteration < 500; iteration++)
|
||||
for (i = 0; i < t; i++)
|
||||
{
|
||||
/* Can take a while, so ensure we can interrupt */
|
||||
CHECK_FOR_INTERRUPTS();
|
||||
|
||||
changes = 0;
|
||||
/* Get b examples picked randomly from X */
|
||||
SampleRows(buildstate);
|
||||
|
||||
/* Step 1: For all centers, compute distance */
|
||||
for (j = 0; j < numCenters; j++)
|
||||
{
|
||||
vec = VectorArrayGet(centers, j);
|
||||
|
||||
for (k = j + 1; k < numCenters; k++)
|
||||
{
|
||||
distance = 0.5 * DatumGetFloat8(FunctionCall2Coll(procinfo, collation, PointerGetDatum(vec), PointerGetDatum(VectorArrayGet(centers, k))));
|
||||
halfcdist[j * numCenters + k] = distance;
|
||||
halfcdist[k * numCenters + j] = distance;
|
||||
}
|
||||
}
|
||||
|
||||
/* For all centers c, compute s(c) */
|
||||
for (j = 0; j < numCenters; j++)
|
||||
/* Cache nearest center to x */
|
||||
for (j = 0; j < m->length; j++)
|
||||
{
|
||||
/* compute closest */
|
||||
minDistance = DBL_MAX;
|
||||
closestCenter = -1;
|
||||
|
||||
for (k = 0; k < numCenters; k++)
|
||||
x = VectorArrayGet(m, j);
|
||||
|
||||
/* Find closest center */
|
||||
for (k = 0; k < centers->length; k++)
|
||||
{
|
||||
if (j == k)
|
||||
continue;
|
||||
distance = DatumGetFloat8(FunctionCall2Coll(procinfo, collation, PointerGetDatum(x), PointerGetDatum(VectorArrayGet(centers, k))));
|
||||
|
||||
distance = halfcdist[j * numCenters + k];
|
||||
if (distance < minDistance)
|
||||
{
|
||||
minDistance = distance;
|
||||
closestCenter = k;
|
||||
}
|
||||
}
|
||||
|
||||
s[j] = minDistance;
|
||||
d[j] = closestCenter;
|
||||
}
|
||||
|
||||
rjreset = iteration != 0;
|
||||
|
||||
for (j = 0; j < numSamples; j++)
|
||||
for (j = 0; j < m->length; j++)
|
||||
{
|
||||
/* Step 2: Identify all points x such that u(x) <= s(c(x)) */
|
||||
if (upperBound[j] <= s[closestCenters[j]])
|
||||
continue;
|
||||
x = VectorArrayGet(m, j);
|
||||
|
||||
rj = rjreset;
|
||||
/* Get cached center for this x */
|
||||
c = VectorArrayGet(centers, d[j]);
|
||||
|
||||
for (k = 0; k < numCenters; k++)
|
||||
/* Update per-center counts */
|
||||
v[d[j]]++;
|
||||
|
||||
/* Get per-center learning rate */
|
||||
eta = 1.0 / v[d[j]];
|
||||
|
||||
/* Take gradient step */
|
||||
for (k = 0; k < c->dim; k++)
|
||||
c->x[k] = (1 - eta) * c->x[k] + eta * x->x[k];
|
||||
}
|
||||
|
||||
/* Check for empty centers (likely duplicates) */
|
||||
if (i == 0)
|
||||
{
|
||||
for (j = 0; j < centers->length; j++)
|
||||
{
|
||||
/* Step 3: For all remaining points x and centers c */
|
||||
if (k == closestCenters[j])
|
||||
continue;
|
||||
|
||||
if (upperBound[j] <= lowerBound[j * numCenters + k])
|
||||
continue;
|
||||
|
||||
if (upperBound[j] <= halfcdist[closestCenters[j] * numCenters + k])
|
||||
continue;
|
||||
|
||||
vec = VectorArrayGet(samples, j);
|
||||
|
||||
/* Step 3a */
|
||||
if (rj)
|
||||
if (v[j] == 0)
|
||||
{
|
||||
dxcx = DatumGetFloat8(FunctionCall2Coll(procinfo, collation, PointerGetDatum(vec), PointerGetDatum(VectorArrayGet(centers, closestCenters[j]))));
|
||||
|
||||
/* d(x,c(x)) computed, which is a form of d(x,c) */
|
||||
lowerBound[j * numCenters + closestCenters[j]] = dxcx;
|
||||
upperBound[j] = dxcx;
|
||||
|
||||
rj = false;
|
||||
}
|
||||
else
|
||||
dxcx = upperBound[j];
|
||||
|
||||
/* Step 3b */
|
||||
if (dxcx > lowerBound[j * numCenters + k] || dxcx > halfcdist[closestCenters[j] * numCenters + k])
|
||||
{
|
||||
dxc = DatumGetFloat8(FunctionCall2Coll(procinfo, collation, PointerGetDatum(vec), PointerGetDatum(VectorArrayGet(centers, k))));
|
||||
|
||||
/* d(x,c) calculated */
|
||||
lowerBound[j * numCenters + k] = dxc;
|
||||
|
||||
if (dxc < dxcx)
|
||||
{
|
||||
closestCenters[j] = k;
|
||||
|
||||
/* c(x) changed */
|
||||
upperBound[j] = dxc;
|
||||
|
||||
changes++;
|
||||
}
|
||||
c = VectorArrayGet(centers, j);
|
||||
|
||||
/* TODO Handle empty centers properly */
|
||||
for (k = 0; k < c->dim; k++)
|
||||
c->x[k] = ((double) random()) / MAX_RANDOM_VALUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Step 4: For each center c, let m(c) be mean of all points assigned */
|
||||
for (j = 0; j < numCenters; j++)
|
||||
/* Normalize if needed */
|
||||
if (normprocinfo != NULL)
|
||||
{
|
||||
vec = VectorArrayGet(newCenters, j);
|
||||
for (k = 0; k < dimensions; k++)
|
||||
vec->x[k] = 0.0;
|
||||
|
||||
centerCounts[j] = 0;
|
||||
for (j = 0; j < centers->length; j++)
|
||||
ApplyNorm(normprocinfo, collation, VectorArrayGet(centers, j));
|
||||
}
|
||||
|
||||
for (j = 0; j < numSamples; j++)
|
||||
{
|
||||
vec = VectorArrayGet(samples, j);
|
||||
closestCenter = closestCenters[j];
|
||||
|
||||
/* Increment sum and count of closest center */
|
||||
newCenter = VectorArrayGet(newCenters, closestCenter);
|
||||
for (k = 0; k < dimensions; k++)
|
||||
newCenter->x[k] += vec->x[k];
|
||||
|
||||
centerCounts[closestCenter] += 1;
|
||||
}
|
||||
|
||||
for (j = 0; j < numCenters; j++)
|
||||
{
|
||||
vec = VectorArrayGet(newCenters, j);
|
||||
|
||||
if (centerCounts[j] > 0)
|
||||
{
|
||||
for (k = 0; k < dimensions; k++)
|
||||
vec->x[k] /= centerCounts[j];
|
||||
}
|
||||
else
|
||||
{
|
||||
/* TODO Handle empty centers properly */
|
||||
for (k = 0; k < dimensions; k++)
|
||||
vec->x[k] = ((double) random()) / MAX_RANDOM_VALUE;
|
||||
}
|
||||
|
||||
/* Normalize if needed */
|
||||
if (normprocinfo != NULL)
|
||||
ApplyNorm(normprocinfo, collation, vec);
|
||||
}
|
||||
|
||||
/* Step 5 */
|
||||
for (j = 0; j < numCenters; j++)
|
||||
newcdist[j] = DatumGetFloat8(FunctionCall2Coll(procinfo, collation, PointerGetDatum(VectorArrayGet(centers, j)), PointerGetDatum(VectorArrayGet(newCenters, j))));
|
||||
|
||||
for (j = 0; j < numSamples; j++)
|
||||
{
|
||||
for (k = 0; k < numCenters; k++)
|
||||
{
|
||||
distance = lowerBound[j * numCenters + k] - newcdist[k];
|
||||
|
||||
if (distance < 0)
|
||||
distance = 0;
|
||||
|
||||
lowerBound[j * numCenters + k] = distance;
|
||||
}
|
||||
}
|
||||
|
||||
/* Step 6 */
|
||||
/* We reset r(x) before Step 3 in the next iteration */
|
||||
for (j = 0; j < numSamples; j++)
|
||||
upperBound[j] += newcdist[closestCenters[j]];
|
||||
|
||||
/* Step 7 */
|
||||
for (j = 0; j < numCenters; j++)
|
||||
memcpy(VectorArrayGet(centers, j), VectorArrayGet(newCenters, j), VECTOR_SIZE(dimensions));
|
||||
|
||||
if (changes == 0 && iteration != 0)
|
||||
break;
|
||||
}
|
||||
|
||||
pfree(newCenters);
|
||||
pfree(centerCounts);
|
||||
pfree(closestCenters);
|
||||
pfree(lowerBound);
|
||||
pfree(upperBound);
|
||||
pfree(s);
|
||||
pfree(halfcdist);
|
||||
pfree(newcdist);
|
||||
pfree(v);
|
||||
pfree(d);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -464,16 +399,48 @@ CheckCenters(Relation index, VectorArray centers)
|
||||
}
|
||||
|
||||
/*
|
||||
* Perform naive k-means centering
|
||||
* Perform k-means clustering
|
||||
* We use spherical k-means for inner product and cosine
|
||||
*/
|
||||
void
|
||||
IvfflatKmeans(Relation index, VectorArray samples, VectorArray centers)
|
||||
IvfflatKmeans(IvfflatBuildState * buildstate)
|
||||
{
|
||||
if (samples->length <= centers->maxlen)
|
||||
QuickCenters(index, samples, centers);
|
||||
else
|
||||
ElkanKmeans(index, samples, centers);
|
||||
int numSamples;
|
||||
Size totalSize;
|
||||
|
||||
CheckCenters(index, centers);
|
||||
/* Target 10 samples per list, with at least 10000 samples */
|
||||
/* The number of samples has a large effect on index build time */
|
||||
numSamples = buildstate->lists * 10;
|
||||
if (numSamples < 10000)
|
||||
numSamples = 10000;
|
||||
|
||||
/* Skip samples for unlogged table */
|
||||
if (buildstate->heap == NULL)
|
||||
numSamples = 1;
|
||||
|
||||
/* Calculate total size */
|
||||
totalSize = VECTOR_ARRAY_SIZE(numSamples, buildstate->dimensions);
|
||||
|
||||
/* Check memory requirements */
|
||||
/* Add one to error message to ceil */
|
||||
if (totalSize / 1024 > maintenance_work_mem)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
|
||||
errmsg("memory required is %zu MB, maintenance_work_mem is %d MB",
|
||||
totalSize / (1024 * 1024) + 1, maintenance_work_mem / 1024)));
|
||||
|
||||
/* Sample rows */
|
||||
buildstate->samples = VectorArrayInit(numSamples, buildstate->dimensions);
|
||||
if (buildstate->heap != NULL)
|
||||
SampleRows(buildstate);
|
||||
|
||||
if (buildstate->samples->length <= buildstate->centers->maxlen)
|
||||
QuickCenters(buildstate->index, buildstate->samples, buildstate->centers);
|
||||
else
|
||||
MiniBatchKmeans(buildstate);
|
||||
|
||||
CheckCenters(buildstate->index, buildstate->centers);
|
||||
|
||||
/* Free samples before we allocate more memory */
|
||||
pfree(buildstate->samples);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "postgres.h"
|
||||
|
||||
#include <float.h>
|
||||
|
||||
#include "access/relscan.h"
|
||||
#include "ivfflat.h"
|
||||
#include "miscadmin.h"
|
||||
@@ -17,14 +19,12 @@
|
||||
* Compare list distances
|
||||
*/
|
||||
static int
|
||||
CompareLists(const void *a, const void *b)
|
||||
CompareLists(const pairingheap_node *a, const pairingheap_node *b, void *arg)
|
||||
{
|
||||
double diff = (((IvfflatScanList *) a)->distance - ((IvfflatScanList *) b)->distance);
|
||||
|
||||
if (diff > 0)
|
||||
if (((const IvfflatScanList *) a)->distance > ((const IvfflatScanList *) b)->distance)
|
||||
return 1;
|
||||
|
||||
if (diff < 0)
|
||||
if (((const IvfflatScanList *) a)->distance < ((const IvfflatScanList *) b)->distance)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
@@ -45,6 +45,8 @@ GetScanLists(IndexScanDesc scan, Datum value)
|
||||
int listCount = 0;
|
||||
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
|
||||
double distance;
|
||||
IvfflatScanList *scanlist;
|
||||
double maxDistance = DBL_MAX;
|
||||
|
||||
/* Search all list pages */
|
||||
while (BlockNumberIsValid(nextblkno))
|
||||
@@ -62,21 +64,39 @@ GetScanLists(IndexScanDesc scan, Datum value)
|
||||
/* Use procinfo from the index instead of scan key for performance */
|
||||
distance = DatumGetFloat8(FunctionCall2Coll(so->procinfo, so->collation, PointerGetDatum(&list->center), value));
|
||||
|
||||
so->lists[listCount].startPage = list->startPage;
|
||||
so->lists[listCount].distance = distance;
|
||||
listCount++;
|
||||
if (listCount < so->probes)
|
||||
{
|
||||
scanlist = &so->lists[listCount];
|
||||
scanlist->startPage = list->startPage;
|
||||
scanlist->distance = distance;
|
||||
listCount++;
|
||||
|
||||
/* Add to heap */
|
||||
pairingheap_add(so->listQueue, &scanlist->ph_node);
|
||||
|
||||
/* Calculate max distance */
|
||||
if (listCount == so->probes)
|
||||
maxDistance = ((IvfflatScanList *) pairingheap_first(so->listQueue))->distance;
|
||||
}
|
||||
else if (distance < maxDistance)
|
||||
{
|
||||
/* Remove */
|
||||
scanlist = (IvfflatScanList *) pairingheap_remove_first(so->listQueue);
|
||||
|
||||
/* Reuse */
|
||||
scanlist->startPage = list->startPage;
|
||||
scanlist->distance = distance;
|
||||
pairingheap_add(so->listQueue, &scanlist->ph_node);
|
||||
|
||||
/* Update max distance */
|
||||
maxDistance = ((IvfflatScanList *) pairingheap_first(so->listQueue))->distance;
|
||||
}
|
||||
}
|
||||
|
||||
nextblkno = IvfflatPageGetOpaque(cpage)->nextblkno;
|
||||
|
||||
UnlockReleaseBuffer(cbuf);
|
||||
}
|
||||
|
||||
/* Sort by distance */
|
||||
qsort(so->lists, listCount, sizeof(IvfflatScanList), CompareLists);
|
||||
|
||||
if (so->probes > listCount)
|
||||
so->probes = listCount;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -94,7 +114,6 @@ GetScanItems(IndexScanDesc scan, Datum value)
|
||||
OffsetNumber maxoffno;
|
||||
Datum datum;
|
||||
bool isnull;
|
||||
int i;
|
||||
TupleDesc tupdesc = RelationGetDescr(scan->indexRelation);
|
||||
|
||||
#if PG_VERSION_NUM >= 120000
|
||||
@@ -111,9 +130,9 @@ GetScanItems(IndexScanDesc scan, Datum value)
|
||||
BufferAccessStrategy bas = GetAccessStrategy(BAS_BULKREAD);
|
||||
|
||||
/* Search closest probes lists */
|
||||
for (i = 0; i < so->probes; i++)
|
||||
while (!pairingheap_is_empty(so->listQueue))
|
||||
{
|
||||
searchPage = so->lists[i].startPage;
|
||||
searchPage = ((IvfflatScanList *) pairingheap_remove_first(so->listQueue))->startPage;
|
||||
|
||||
/* Search all entry pages for list */
|
||||
while (BlockNumberIsValid(searchPage))
|
||||
@@ -137,12 +156,10 @@ GetScanItems(IndexScanDesc scan, Datum value)
|
||||
ExecClearTuple(slot);
|
||||
slot->tts_values[0] = FunctionCall2Coll(so->procinfo, so->collation, datum, value);
|
||||
slot->tts_isnull[0] = false;
|
||||
slot->tts_values[1] = Int32GetDatum((int) ItemPointerGetBlockNumberNoCheck(&itup->t_tid));
|
||||
slot->tts_values[1] = PointerGetDatum(&itup->t_tid);
|
||||
slot->tts_isnull[1] = false;
|
||||
slot->tts_values[2] = Int32GetDatum((int) ItemPointerGetOffsetNumberNoCheck(&itup->t_tid));
|
||||
slot->tts_values[2] = Int32GetDatum((int) searchPage);
|
||||
slot->tts_isnull[2] = false;
|
||||
slot->tts_values[3] = Int32GetDatum((int) searchPage);
|
||||
slot->tts_isnull[3] = false;
|
||||
ExecStoreVirtualTuple(slot);
|
||||
|
||||
tuplesort_puttupleslot(so->sortstate, slot);
|
||||
@@ -153,6 +170,8 @@ GetScanItems(IndexScanDesc scan, Datum value)
|
||||
UnlockReleaseBuffer(buf);
|
||||
}
|
||||
}
|
||||
|
||||
tuplesort_performsort(so->sortstate);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -168,13 +187,18 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys)
|
||||
Oid sortOperators[] = {Float8LessOperator};
|
||||
Oid sortCollations[] = {InvalidOid};
|
||||
bool nullsFirstFlags[] = {false};
|
||||
int probes = ivfflat_probes;
|
||||
|
||||
scan = RelationGetIndexScan(index, nkeys, norderbys);
|
||||
lists = IvfflatGetLists(scan->indexRelation);
|
||||
|
||||
so = (IvfflatScanOpaque) palloc(offsetof(IvfflatScanOpaqueData, lists) + lists * sizeof(IvfflatScanList));
|
||||
if (probes > lists)
|
||||
probes = lists;
|
||||
|
||||
so = (IvfflatScanOpaque) palloc(offsetof(IvfflatScanOpaqueData, lists) + probes * sizeof(IvfflatScanList));
|
||||
so->buf = InvalidBuffer;
|
||||
so->first = true;
|
||||
so->probes = probes;
|
||||
|
||||
/* Set support functions */
|
||||
so->procinfo = index_getprocinfo(index, 1, IVFFLAT_DISTANCE_PROC);
|
||||
@@ -183,14 +207,13 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys)
|
||||
|
||||
/* Create tuple description for sorting */
|
||||
#if PG_VERSION_NUM >= 120000
|
||||
so->tupdesc = CreateTemplateTupleDesc(4);
|
||||
so->tupdesc = CreateTemplateTupleDesc(3);
|
||||
#else
|
||||
so->tupdesc = CreateTemplateTupleDesc(4, false);
|
||||
so->tupdesc = CreateTemplateTupleDesc(3, false);
|
||||
#endif
|
||||
TupleDescInitEntry(so->tupdesc, (AttrNumber) 1, "distance", FLOAT8OID, -1, 0);
|
||||
TupleDescInitEntry(so->tupdesc, (AttrNumber) 2, "blkno", INT4OID, -1, 0);
|
||||
TupleDescInitEntry(so->tupdesc, (AttrNumber) 3, "offset", INT4OID, -1, 0);
|
||||
TupleDescInitEntry(so->tupdesc, (AttrNumber) 4, "indexblkno", INT4OID, -1, 0);
|
||||
TupleDescInitEntry(so->tupdesc, (AttrNumber) 2, "tid", TIDOID, -1, 0);
|
||||
TupleDescInitEntry(so->tupdesc, (AttrNumber) 3, "indexblkno", INT4OID, -1, 0);
|
||||
|
||||
/* Prep sort */
|
||||
#if PG_VERSION_NUM >= 110000
|
||||
@@ -205,6 +228,8 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys)
|
||||
so->slot = MakeSingleTupleTableSlot(so->tupdesc);
|
||||
#endif
|
||||
|
||||
so->listQueue = pairingheap_allocate(CompareLists, scan);
|
||||
|
||||
scan->opaque = so;
|
||||
|
||||
return scan;
|
||||
@@ -224,7 +249,7 @@ ivfflatrescan(IndexScanDesc scan, ScanKey keys, int nkeys, ScanKey orderbys, int
|
||||
#endif
|
||||
|
||||
so->first = true;
|
||||
so->probes = ivfflat_probes;
|
||||
pairingheap_reset(so->listQueue);
|
||||
|
||||
if (keys && scan->numberOfKeys > 0)
|
||||
memmove(scan->keyData, keys, scan->numberOfKeys * sizeof(ScanKeyData));
|
||||
@@ -268,9 +293,8 @@ ivfflatgettuple(IndexScanDesc scan, ScanDirection dir)
|
||||
return false;
|
||||
}
|
||||
|
||||
GetScanLists(scan, value);
|
||||
GetScanItems(scan, value);
|
||||
tuplesort_performsort(so->sortstate);
|
||||
IvfflatBench("GetScanLists", GetScanLists(scan, value));
|
||||
IvfflatBench("GetScanItems", GetScanItems(scan, value));
|
||||
so->first = false;
|
||||
|
||||
/* Clean up if we allocated a new value */
|
||||
@@ -284,14 +308,13 @@ ivfflatgettuple(IndexScanDesc scan, ScanDirection dir)
|
||||
if (tuplesort_gettupleslot(so->sortstate, true, so->slot, NULL))
|
||||
#endif
|
||||
{
|
||||
BlockNumber blkno = DatumGetInt32(slot_getattr(so->slot, 2, &so->isnull));
|
||||
OffsetNumber offset = DatumGetInt32(slot_getattr(so->slot, 3, &so->isnull));
|
||||
BlockNumber indexblkno = DatumGetInt32(slot_getattr(so->slot, 4, &so->isnull));
|
||||
ItemPointer tid = (ItemPointer) DatumGetPointer(slot_getattr(so->slot, 2, &so->isnull));
|
||||
BlockNumber indexblkno = DatumGetInt32(slot_getattr(so->slot, 3, &so->isnull));
|
||||
|
||||
#if PG_VERSION_NUM >= 120000
|
||||
ItemPointerSet(&scan->xs_heaptid, blkno, offset);
|
||||
scan->xs_heaptid = *tid;
|
||||
#else
|
||||
ItemPointerSet(&scan->xs_ctup.t_self, blkno, offset);
|
||||
scan->xs_ctup.t_self = *tid;
|
||||
#endif
|
||||
|
||||
if (BufferIsValid(so->buf))
|
||||
@@ -324,6 +347,7 @@ ivfflatendscan(IndexScanDesc scan)
|
||||
if (BufferIsValid(so->buf))
|
||||
ReleaseBuffer(so->buf);
|
||||
|
||||
pairingheap_free(so->listQueue);
|
||||
tuplesort_end(so->sortstate);
|
||||
|
||||
pfree(so);
|
||||
|
||||
@@ -153,11 +153,13 @@ IvfflatAppendPage(Relation index, Buffer *buf, Page *page, GenericXLogState **st
|
||||
*/
|
||||
void
|
||||
IvfflatUpdateList(Relation index, GenericXLogState *state, ListInfo listInfo,
|
||||
BlockNumber insertPage, BlockNumber startPage, ForkNumber forkNum)
|
||||
BlockNumber insertPage, BlockNumber originalInsertPage,
|
||||
BlockNumber startPage, ForkNumber forkNum)
|
||||
{
|
||||
Buffer buf;
|
||||
Page page;
|
||||
IvfflatList list;
|
||||
bool changed = false;
|
||||
|
||||
buf = ReadBufferExtended(index, forkNum, listInfo.blkno, RBM_NORMAL, NULL);
|
||||
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
|
||||
@@ -165,12 +167,29 @@ IvfflatUpdateList(Relation index, GenericXLogState *state, ListInfo listInfo,
|
||||
page = GenericXLogRegisterBuffer(state, buf, 0);
|
||||
list = (IvfflatList) PageGetItem(page, PageGetItemId(page, listInfo.offno));
|
||||
|
||||
if (BlockNumberIsValid(insertPage))
|
||||
list->insertPage = insertPage;
|
||||
if (BlockNumberIsValid(insertPage) && insertPage != list->insertPage)
|
||||
{
|
||||
/* Skip update if insert page is lower than original insert page */
|
||||
/* This is needed to prevent insert from overwriting vacuum */
|
||||
if (!BlockNumberIsValid(originalInsertPage) || insertPage >= originalInsertPage)
|
||||
{
|
||||
list->insertPage = insertPage;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (BlockNumberIsValid(startPage))
|
||||
if (BlockNumberIsValid(startPage) && startPage != list->startPage)
|
||||
{
|
||||
list->startPage = startPage;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
/* Could only commit if changed, but extra complexity isn't needed */
|
||||
IvfflatCommitBuffer(buf, state);
|
||||
/* Only commit if changed */
|
||||
if (changed)
|
||||
IvfflatCommitBuffer(buf, state);
|
||||
else
|
||||
{
|
||||
GenericXLogAbort(state);
|
||||
UnlockReleaseBuffer(buf);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ ivfflatbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
|
||||
ItemPointer htup;
|
||||
OffsetNumber deletable[MaxOffsetNumber];
|
||||
int ndeletable;
|
||||
OffsetNumber startPages[MaxOffsetNumber];
|
||||
BlockNumber startPages[MaxOffsetNumber];
|
||||
BlockNumber nextblkno = IVFFLAT_HEAD_BLKNO;
|
||||
BlockNumber searchPage;
|
||||
BlockNumber insertPage;
|
||||
@@ -98,6 +98,11 @@ ivfflatbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
|
||||
stats->num_index_tuples++;
|
||||
}
|
||||
|
||||
/* Set to first free page */
|
||||
/* Must be set before searchPage is updated */
|
||||
if (!BlockNumberIsValid(insertPage) && ndeletable > 0)
|
||||
insertPage = searchPage;
|
||||
|
||||
searchPage = IvfflatPageGetOpaque(page)->nextblkno;
|
||||
|
||||
if (ndeletable > 0)
|
||||
@@ -106,10 +111,6 @@ ivfflatbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
|
||||
PageIndexMultiDelete(page, deletable, ndeletable);
|
||||
MarkBufferDirty(buf);
|
||||
GenericXLogFinish(state);
|
||||
|
||||
/* Set to first free page */
|
||||
if (!BlockNumberIsValid(insertPage))
|
||||
insertPage = searchPage;
|
||||
}
|
||||
else
|
||||
GenericXLogAbort(state);
|
||||
@@ -123,10 +124,10 @@ ivfflatbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
|
||||
* We don't add or delete items from lists pages, so offset won't
|
||||
* change.
|
||||
*/
|
||||
if (!BlockNumberIsValid(insertPage))
|
||||
if (BlockNumberIsValid(insertPage))
|
||||
{
|
||||
listInfo.offno = coffno;
|
||||
IvfflatUpdateList(index, state, listInfo, insertPage, InvalidBlockNumber, MAIN_FORKNUM);
|
||||
IvfflatUpdateList(index, state, listInfo, insertPage, InvalidBlockNumber, InvalidBlockNumber, MAIN_FORKNUM);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,3 +42,10 @@ SELECT '[1,2,3]'::vector::real[];
|
||||
|
||||
SELECT array_agg(n)::vector FROM generate_series(1, 1025) n;
|
||||
ERROR: vector cannot have more than 1024 dimensions
|
||||
-- ensure no error
|
||||
SELECT ARRAY[1,2,3] = ARRAY[1,2,3];
|
||||
?column?
|
||||
----------
|
||||
t
|
||||
(1 row)
|
||||
|
||||
|
||||
@@ -53,3 +53,12 @@ LINE 1: SELECT '[1,]'::vector;
|
||||
^
|
||||
SELECT '[1,2,3]'::vector(2);
|
||||
ERROR: expected 2 dimensions, not 3
|
||||
SELECT unnest('{"[1,2,3]", "[4,5,6]"}'::vector[]);
|
||||
unnest
|
||||
---------
|
||||
[1,2,3]
|
||||
[4,5,6]
|
||||
(2 rows)
|
||||
|
||||
SELECT '{"[1,2,3]"}'::vector(2)[];
|
||||
ERROR: expected 2 dimensions, not 3
|
||||
|
||||
@@ -12,3 +12,6 @@ SELECT '{-Infinity}'::real[]::vector;
|
||||
SELECT '{}'::real[]::vector;
|
||||
SELECT '[1,2,3]'::vector::real[];
|
||||
SELECT array_agg(n)::vector FROM generate_series(1, 1025) n;
|
||||
|
||||
-- ensure no error
|
||||
SELECT ARRAY[1,2,3] = ARRAY[1,2,3];
|
||||
|
||||
@@ -13,3 +13,6 @@ SELECT '1,2,3'::vector;
|
||||
SELECT '[]'::vector;
|
||||
SELECT '[1,]'::vector;
|
||||
SELECT '[1,2,3]'::vector(2);
|
||||
|
||||
SELECT unnest('{"[1,2,3]", "[4,5,6]"}'::vector[]);
|
||||
SELECT '{"[1,2,3]"}'::vector(2)[];
|
||||
|
||||
@@ -34,9 +34,10 @@ sub test_index_replay
|
||||
my $r2 = rand();
|
||||
my $r3 = rand();
|
||||
|
||||
my $queries = qq(SET enable_seqscan=off;
|
||||
SELECT * FROM tst ORDER BY v <-> '[$r1,$r2,$r3]' LIMIT 10;
|
||||
);
|
||||
my $queries = qq(
|
||||
SET enable_seqscan = off;
|
||||
SELECT * FROM tst ORDER BY v <-> '[$r1,$r2,$r3]' LIMIT 10;
|
||||
);
|
||||
|
||||
# Run test queries and compare their result
|
||||
my $primary_result = $node_primary->safe_psql("postgres", $queries);
|
||||
@@ -65,10 +66,9 @@ $node_replica->start;
|
||||
$node_primary->safe_psql("postgres", "CREATE EXTENSION vector;");
|
||||
$node_primary->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector(3));");
|
||||
$node_primary->safe_psql("postgres",
|
||||
"INSERT INTO tst SELECT i%10, ARRAY[random(), random(), random()] FROM generate_series(1,100000) i;"
|
||||
"INSERT INTO tst SELECT i % 10, ARRAY[random(), random(), random()] FROM generate_series(1, 100000) i;"
|
||||
);
|
||||
$node_primary->safe_psql("postgres",
|
||||
"CREATE INDEX ON tst USING ivfflat (v);");
|
||||
$node_primary->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v);");
|
||||
|
||||
# Test that queries give same result
|
||||
test_index_replay('initial');
|
||||
@@ -82,7 +82,7 @@ for my $i (1 .. 10)
|
||||
test_index_replay("vacuum $i");
|
||||
my ($start, $end) = (100001 + ($i - 1) * 10000, 100000 + $i * 10000);
|
||||
$node_primary->safe_psql("postgres",
|
||||
"INSERT INTO tst SELECT i%10, ARRAY[random(), random(), random()] FROM generate_series($start,$end) i;"
|
||||
"INSERT INTO tst SELECT i % 10, ARRAY[random(), random(), random()] FROM generate_series($start, $end) i;"
|
||||
);
|
||||
test_index_replay("insert $i");
|
||||
}
|
||||
|
||||
32
test/t/002_vacuum.pl
Normal file
32
test/t/002_vacuum.pl
Normal file
@@ -0,0 +1,32 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use PostgresNode;
|
||||
use TestLib;
|
||||
use Test::More tests => 1;
|
||||
|
||||
# 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(3));");
|
||||
$node->safe_psql("postgres",
|
||||
"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);");
|
||||
|
||||
# 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 % 10, ARRAY[i % 1000, i % 333, i % 55] FROM generate_series(1, 100000) i;"
|
||||
);
|
||||
|
||||
# Check size
|
||||
my $new_size = $node->safe_psql("postgres", "SELECT pg_total_relation_size('tst_v_idx');");
|
||||
is($size, $new_size, "size does not change");
|
||||
88
test/t/003_recall.pl
Normal file
88
test/t/003_recall.pl
Normal file
@@ -0,0 +1,88 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use PostgresNode;
|
||||
use TestLib;
|
||||
use Test::More tests => 9;
|
||||
|
||||
my $node;
|
||||
my @queries = ();
|
||||
my @expected;
|
||||
my $limit = 20;
|
||||
|
||||
sub test_recall
|
||||
{
|
||||
my ($probes, $min, $operator) = @_;
|
||||
my $correct = 0;
|
||||
my $total = 0;
|
||||
|
||||
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 = ("<->", "<#>", "<=>");
|
||||
|
||||
foreach (@operators) {
|
||||
my $operator = $_;
|
||||
|
||||
# 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);
|
||||
}
|
||||
|
||||
# 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);");
|
||||
|
||||
# Test approximate results
|
||||
test_recall(1, 0.75, $operator);
|
||||
test_recall(10, 0.95, $operator);
|
||||
test_recall(100, 1.0, $operator);
|
||||
}
|
||||
36
test/t/004_centers.pl
Normal file
36
test/t/004_centers.pl
Normal file
@@ -0,0 +1,36 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use PostgresNode;
|
||||
use TestLib;
|
||||
use Test::More tests => 3;
|
||||
|
||||
# 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 (i int4, v vector(3));");
|
||||
$node->safe_psql("postgres",
|
||||
"INSERT INTO tst SELECT i, '[1,2,3]' FROM generate_series(1, 10) i;"
|
||||
);
|
||||
|
||||
sub test_centers
|
||||
{
|
||||
my ($lists, $min) = @_;
|
||||
|
||||
my ($ret, $stdout, $stderr) = $node->psql("postgres", "CREATE INDEX ON tst USING ivfflat (v) WITH (lists = $lists);");
|
||||
is($ret, 0, $stderr);
|
||||
}
|
||||
|
||||
# Test no error for duplicate centers
|
||||
test_centers(5);
|
||||
test_centers(10);
|
||||
|
||||
$node->safe_psql("postgres",
|
||||
"INSERT INTO tst SELECT i, '[4,5,6]' FROM generate_series(1, 10) i;"
|
||||
);
|
||||
|
||||
# Test no error for duplicate centers
|
||||
test_centers(10);
|
||||
45
test/t/005_query_recall.pl
Normal file
45
test/t/005_query_recall.pl
Normal file
@@ -0,0 +1,45 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use PostgresNode;
|
||||
use TestLib;
|
||||
use Test::More tests => 60;
|
||||
|
||||
# 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 (i int4 primary key, v vector(3));");
|
||||
$node->safe_psql("postgres",
|
||||
"INSERT INTO tst SELECT i, ARRAY[random(), random(), random()] FROM generate_series(1, 100000) i;"
|
||||
);
|
||||
|
||||
# Check each index type
|
||||
my @operators = ("<->", "<#>", "<=>");
|
||||
foreach (@operators) {
|
||||
my $operator = $_;
|
||||
|
||||
# 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);");
|
||||
|
||||
# Test 100% recall
|
||||
for (1..20) {
|
||||
my $i = int(rand() * 100000);
|
||||
my $query = $node->safe_psql("postgres", "SELECT v FROM tst WHERE i = $i;");
|
||||
my $res = $node->safe_psql("postgres", qq(
|
||||
SET enable_seqscan = off;
|
||||
SELECT v FROM tst ORDER BY v <-> '$query' LIMIT 1;
|
||||
));
|
||||
is($res, $query);
|
||||
}
|
||||
}
|
||||
31
test/t/006_lists.pl
Normal file
31
test/t/006_lists.pl
Normal file
@@ -0,0 +1,31 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use PostgresNode;
|
||||
use TestLib;
|
||||
use Test::More tests => 3;
|
||||
|
||||
# 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));");
|
||||
$node->safe_psql("postgres",
|
||||
"INSERT INTO tst SELECT ARRAY[random(), random(), random()] FROM generate_series(1, 100000) i;"
|
||||
);
|
||||
|
||||
$node->safe_psql("postgres", "CREATE INDEX lists50 ON tst USING ivfflat (v) WITH (lists = 50);");
|
||||
$node->safe_psql("postgres", "CREATE INDEX lists100 ON tst USING ivfflat (v) WITH (lists = 100);");
|
||||
|
||||
# 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;");
|
||||
like($res, qr/lists100/);
|
||||
unlike($res, qr/lists50/);
|
||||
|
||||
# Test errors with too much memory
|
||||
my ($ret, $stdout, $stderr) = $node->psql("postgres",
|
||||
"CREATE INDEX lists10000 ON tst USING ivfflat (v) WITH (lists = 10000);"
|
||||
);
|
||||
like($stderr, qr/memory required is/);
|
||||
@@ -1,4 +1,4 @@
|
||||
comment = 'vector data type and ivfflat access method'
|
||||
default_version = '0.1.8'
|
||||
default_version = '0.2.5'
|
||||
module_pathname = '$libdir/vector'
|
||||
relocatable = true
|
||||
|
||||
Reference in New Issue
Block a user