Compare commits

..

10 Commits

Author SHA1 Message Date
Andrew Kane
9852351746 Merge branch 'master' into minibatch 2022-02-15 19:14:41 -08:00
Andrew Kane
50349ed4f5 Improved code [skip ci] 2022-02-15 18:32:13 -08:00
Andrew Kane
2ee510aa67 Disabled scan progress [skip ci] 2022-02-15 18:14:03 -08:00
Andrew Kane
cad655b77f Improved performance of index creation for Postgres < 12 2022-02-15 18:06:41 -08:00
Andrew Kane
21ca5d3845 Improved code [skip ci] 2022-02-13 00:07:54 -08:00
Andrew Kane
8374498e6c Use double [skip ci] 2022-02-12 23:28:35 -08:00
Andrew Kane
c1d6b9b41b Added comment [skip ci] 2022-02-12 23:27:02 -08:00
Andrew Kane
a77340d40b Fixed CI 2022-02-12 23:18:34 -08:00
Andrew Kane
81b68fbf5b Check for interrupts [skip ci] 2022-02-12 23:15:27 -08:00
Andrew Kane
8ee6d0e596 Switched to mini-batch k-means 2022-02-12 22:56:00 -08:00
23 changed files with 292 additions and 649 deletions

View File

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

View File

@@ -7,12 +7,12 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest] os: [ubuntu-latest]
postgres: [15, 14, 13, 12, 11, 10] postgres: [14, 13, 12, 11, 10, 9.6]
include: include:
- os: macos-latest - os: macos-latest
postgres: 14 postgres: 14
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- uses: ankane/setup-postgres@v1 - uses: ankane/setup-postgres@v1
with: with:
postgres-version: ${{ matrix.postgres }} postgres-version: ${{ matrix.postgres }}
@@ -33,6 +33,6 @@ jobs:
- if: ${{ startsWith(matrix.os, 'macos') }} - if: ${{ startsWith(matrix.os, 'macos') }}
run: | run: |
brew install cpanm && cpanm IPC::Run brew install cpanm && 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_1.tar.gz
tar xf REL_14_5.tar.gz tar xf REL_14_1.tar.gz
make prove_installcheck PROVE=prove PROVE_FLAGS="-I ./postgres-REL_14_5/src/test/perl" PERL5LIB="/Users/runner/perl5/lib/perl5" make prove_installcheck PROVE=prove PERL5LIB=postgres-REL_14_1/src/test/perl

1
.gitignore vendored
View File

@@ -5,4 +5,3 @@
regression.* regression.*
*.o *.o
*.so *.so
*.bc

View File

@@ -1,21 +1,6 @@
## 0.3.1 (2022-11-02) ## 0.2.6 (unreleased)
If upgrading from 0.2.7 or 0.3.0, [recreate](https://github.com/pgvector/pgvector#031) all `ivfflat` indexes after upgrading to ensure all data is indexed.
- Fixed issue with inserts silently corrupting `ivfflat` indexes (introduced in 0.2.7)
- Fixed segmentation fault with index creation when lists > 6500
## 0.3.0 (2022-10-15)
- Added support for Postgres 15
- Dropped support for Postgres 9.6
## 0.2.7 (2022-07-31)
- Fixed `unexpected data beyond EOF` error
## 0.2.6 (2022-05-22)
- Switched to mini-batch k-means
- Improved performance of index creation for Postgres < 12 - Improved performance of index creation for Postgres < 12
## 0.2.5 (2022-02-11) ## 0.2.5 (2022-02-11)

View File

@@ -1,9 +1,9 @@
FROM postgres:15 FROM postgres:14
COPY . /tmp/pgvector COPY . /tmp/pgvector
RUN apt-get update && \ RUN apt-get update && \
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-14 && \
cd /tmp/pgvector && \ cd /tmp/pgvector && \
make clean && \ make clean && \
make OPTFLAGS="" && \ make OPTFLAGS="" && \
@@ -11,6 +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-15 && \ apt-get remove -y build-essential postgresql-server-dev-14 && \
apt-get autoremove -y && \ apt-get autoremove -y && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*

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.3.1", "version": "0.2.5",
"maintainer": [ "maintainer": [
"Andrew Kane <andrew@ankane.org>" "Andrew Kane <andrew@ankane.org>"
], ],
@@ -12,7 +12,7 @@
"prereqs": { "prereqs": {
"runtime": { "runtime": {
"requires": { "requires": {
"PostgreSQL": "10.0.0" "PostgreSQL": "9.6.0"
} }
} }
}, },
@@ -20,7 +20,7 @@
"vector": { "vector": {
"file": "sql/vector.sql", "file": "sql/vector.sql",
"docfile": "README.md", "docfile": "README.md",
"version": "0.3.1", "version": "0.2.5",
"abstract": "Open-source vector similarity search for Postgres" "abstract": "Open-source vector similarity search for Postgres"
} }
}, },

View File

@@ -1,5 +1,5 @@
EXTENSION = vector EXTENSION = vector
EXTVERSION = 0.3.1 EXTVERSION = 0.2.5
MODULE_big = vector MODULE_big = vector
DATA = $(wildcard sql/*--*.sql) DATA = $(wildcard sql/*--*.sql)
@@ -40,9 +40,6 @@ PG_CONFIG ?= pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs) PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS) include $(PGXS)
# for Postgres 15
PROVE_FLAGS += -I ./test/perl
prove_installcheck: prove_installcheck:
rm -rf $(CURDIR)/tmp_check rm -rf $(CURDIR)/tmp_check
cd $(srcdir) && TESTDIR='$(CURDIR)' PATH="$(bindir):$$PATH" PGPORT='6$(DEF_PGPORT)' PG_REGRESS='$(top_builddir)/src/test/regress/pg_regress' $(PROVE) $(PG_PROVE_FLAGS) $(PROVE_FLAGS) $(if $(PROVE_TESTS),$(PROVE_TESTS),test/t/*.pl) cd $(srcdir) && TESTDIR='$(CURDIR)' PATH="$(bindir):$$PATH" PGPORT='6$(DEF_PGPORT)' PG_REGRESS='$(top_builddir)/src/test/regress/pg_regress' $(PROVE) $(PG_PROVE_FLAGS) $(PROVE_FLAGS) $(if $(PROVE_TESTS),$(PROVE_TESTS),test/t/*.pl)

View File

@@ -3,9 +3,9 @@
Open-source vector similarity search for Postgres Open-source vector similarity search for Postgres
```sql ```sql
CREATE TABLE items (embedding vector(3)); CREATE TABLE table (column vector(3));
CREATE INDEX ON items USING ivfflat (embedding vector_l2_ops); CREATE INDEX ON table USING ivfflat (column vector_l2_ops);
SELECT * FROM items ORDER BY embedding <-> '[1,2,3]' LIMIT 5; SELECT * FROM table ORDER BY column <-> '[1,2,3]' LIMIT 5;
``` ```
Supports L2 distance, inner product, and cosine distance Supports L2 distance, inner product, and cosine distance
@@ -14,10 +14,10 @@ Supports L2 distance, inner product, and cosine distance
## Installation ## Installation
Compile and install the extension (supports Postgres 10+) Compile and install the extension (supports Postgres 9.6+)
```sh ```sh
git clone --branch v0.3.1 https://github.com/pgvector/pgvector.git git clone --branch v0.2.5 https://github.com/pgvector/pgvector.git
cd pgvector cd pgvector
make make
make install # may need sudo make install # may need sudo
@@ -33,22 +33,22 @@ You can also install it with [Docker](#docker), [Homebrew](#homebrew), or [PGXN]
## Getting Started ## Getting Started
Create a vector column with 3 dimensions Create a vector column with 3 dimensions (replace `table` and `column` with non-reserved names)
```sql ```sql
CREATE TABLE items (embedding vector(3)); CREATE TABLE table (column vector(3));
``` ```
Insert values Insert values
```sql ```sql
INSERT INTO items VALUES ('[1,2,3]'), ('[4,5,6]'); INSERT INTO table VALUES ('[1,2,3]'), ('[4,5,6]');
``` ```
Get the nearest neighbor by L2 distance Get the nearest neighbor by L2 distance
```sql ```sql
SELECT * FROM items ORDER BY embedding <-> '[3,1,2]' LIMIT 1; SELECT * FROM table ORDER BY column <-> '[3,1,2]' LIMIT 1;
``` ```
Also supports inner product (`<#>`) and cosine distance (`<=>`) Also supports inner product (`<#>`) and cosine distance (`<=>`)
@@ -62,19 +62,19 @@ Speed up queries with an approximate index. Add an index for each distance funct
L2 distance L2 distance
```sql ```sql
CREATE INDEX ON items USING ivfflat (embedding vector_l2_ops); CREATE INDEX ON table USING ivfflat (column vector_l2_ops);
``` ```
Inner product Inner product
```sql ```sql
CREATE INDEX ON items USING ivfflat (embedding vector_ip_ops); CREATE INDEX ON table USING ivfflat (column vector_ip_ops);
``` ```
Cosine distance Cosine distance
```sql ```sql
CREATE INDEX ON items USING ivfflat (embedding vector_cosine_ops); CREATE INDEX ON table USING ivfflat (column vector_cosine_ops);
``` ```
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. 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.
@@ -84,7 +84,7 @@ Indexes should be created after the table has some data for optimal clustering.
Specify the number of inverted lists (100 by default) Specify the number of inverted lists (100 by default)
```sql ```sql
CREATE INDEX ON items USING ivfflat (embedding vector_l2_ops) 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)` A [good place to start](https://github.com/facebookresearch/faiss/issues/112) is `4 * sqrt(rows)`
@@ -119,10 +119,9 @@ SELECT phase, tuples_done, tuples_total FROM pg_stat_progress_create_index;
The phases are: The phases are:
1. `initializing` 1. `initializing`
2. `sampling table` 2. `performing k-means`
3. `performing k-means` 3. `sorting tuples`
4. `sorting tuples` 4. `loading 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
@@ -131,20 +130,10 @@ Note: `tuples_done` and `tuples_total` are only populated during the `loading tu
Consider [partial indexes](https://www.postgresql.org/docs/current/indexes-partial.html) for 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; CREATE INDEX ON table USING ivfflat (column opclass) WHERE (other_column = 123);
``` ```
can be indexed with: To index many different values of `other_column`, consider [partitioning](https://www.postgresql.org/docs/current/ddl-partitioning.html) on `other_column`.
```sql
CREATE INDEX ON items USING ivfflat (embedding vector_l2_ops) WHERE (category_id = 123);
```
To index many different values of `category_id`, consider [partitioning](https://www.postgresql.org/docs/current/ddl-partitioning.html) on `category_id`.
```sql
CREATE TABLE items (embedding vector(3), category_id int) PARTITION BY LIST(category_id);
```
## Performance ## Performance
@@ -157,7 +146,7 @@ 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). To speed up queries with an 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 table USING ivfflat (column opclass) WITH (lists = 1000);
``` ```
## Reference ## Reference
@@ -195,10 +184,8 @@ Libraries that use pgvector:
- [pgvector-ruby](https://github.com/pgvector/pgvector-ruby) (Ruby) - [pgvector-ruby](https://github.com/pgvector/pgvector-ruby) (Ruby)
- [pgvector-node](https://github.com/pgvector/pgvector-node) (Node.js) - [pgvector-node](https://github.com/pgvector/pgvector-node) (Node.js)
- [pgvector-go](https://github.com/pgvector/pgvector-go) (Go) - [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-rust](https://github.com/pgvector/pgvector-rust) (Rust)
- [pgvector-cpp](https://github.com/pgvector/pgvector-cpp) (C++) - [pgvector-cpp](https://github.com/pgvector/pgvector-cpp) (C++)
- [pgvector-elixir](https://github.com/pgvector/pgvector-elixir) (Elixir)
## Frequently Asked Questions ## Frequently Asked Questions
@@ -232,14 +219,14 @@ 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.3.1 https://github.com/pgvector/pgvector.git git clone --branch v0.2.5 https://github.com/pgvector/pgvector.git
cd pgvector cd pgvector
docker build -t pgvector . docker build -t pgvector .
``` ```
### Homebrew ### Homebrew
With Homebrew Postgres, you can use: On Mac with Homebrew Postgres, you can use:
```sh ```sh
brew install pgvector/brew/pgvector brew install pgvector/brew/pgvector
@@ -270,29 +257,13 @@ Install the latest version and run:
ALTER EXTENSION vector UPDATE; ALTER EXTENSION vector UPDATE;
``` ```
## Upgrade Notes
### 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.
```sql
-- Postgres 12+
REINDEX INDEX CONCURRENTLY index_name;
-- Postgres < 12
CREATE INDEX CONCURRENTLY temp_name ON table USING ivfflat (column opclass);
DROP INDEX CONCURRENTLY index_name;
ALTER INDEX temp_name RENAME TO index_name;
```
## Thanks ## Thanks
Thanks to: 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://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) - [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)

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.2.6'" 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.2.7'" 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.3.0'" 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.3.1'" to load this file. \quit

View File

@@ -42,91 +42,6 @@
#define UpdateProgress(index, val) ((void)val) #define UpdateProgress(index, val) ((void)val)
#endif #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)
{
#if PG_VERSION_NUM >= 150000
int k = (int) (targsamples * sampler_random_fract(&buildstate->rstate.randstate));
#else
int k = (int) (targsamples * sampler_random_fract(buildstate->rstate.randstate));
#endif
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);
UpdateProgress(PROGRESS_CREATEIDX_SUBPHASE, PROGRESS_IVFFLAT_PHASE_SAMPLE);
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, 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
}
}
/* /*
* Callback for table_index_build_scan * Callback for table_index_build_scan
*/ */
@@ -233,8 +148,8 @@ InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum)
GenericXLogState *state; GenericXLogState *state;
int list; int list;
IndexTuple itup = NULL; /* silence compiler warning */ IndexTuple itup = NULL; /* silence compiler warning */
BlockNumber startPage; BlockNumber startPage = InvalidBlockNumber;
BlockNumber insertPage; BlockNumber insertPage = InvalidBlockNumber;
Size itemsz; Size itemsz;
int i; int i;
int64 inserted = 0; int64 inserted = 0;
@@ -259,7 +174,7 @@ InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum)
CHECK_FOR_INTERRUPTS(); CHECK_FOR_INTERRUPTS();
buf = IvfflatNewBuffer(index, forkNum); buf = IvfflatNewBuffer(index, forkNum);
IvfflatInitRegisterPage(index, &buf, &page, &state); IvfflatInitPage(index, &buf, &page, &state);
startPage = BufferGetBlockNumber(buf); startPage = BufferGetBlockNumber(buf);
@@ -372,38 +287,6 @@ FreeBuildState(IvfflatBuildState * buildstate)
#endif #endif
} }
/*
* 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;
/* Skip samples for unlogged table */
if (buildstate->heap == NULL)
numSamples = 1;
/* Sample rows */
/* TODO Ensure within maintenance_work_mem */
buildstate->samples = VectorArrayInit(numSamples, buildstate->dimensions);
if (buildstate->heap != NULL)
SampleRows(buildstate);
/* Calculate centers */
UpdateProgress(PROGRESS_CREATEIDX_SUBPHASE, PROGRESS_IVFFLAT_PHASE_KMEANS);
IvfflatBench("k-means", IvfflatKmeans(buildstate->index, buildstate->samples, buildstate->centers));
/* Free samples before we allocate more memory */
pfree(buildstate->samples);
}
/* /*
* Create the metapage * Create the metapage
*/ */
@@ -416,7 +299,7 @@ CreateMetaPage(Relation index, int dimensions, int lists, ForkNumber forkNum)
IvfflatMetaPage metap; IvfflatMetaPage metap;
buf = IvfflatNewBuffer(index, forkNum); buf = IvfflatNewBuffer(index, forkNum);
IvfflatInitRegisterPage(index, &buf, &page, &state); IvfflatInitPage(index, &buf, &page, &state);
/* Set metapage data */ /* Set metapage data */
metap = IvfflatPageGetMeta(page); metap = IvfflatPageGetMeta(page);
@@ -449,7 +332,7 @@ CreateListPages(Relation index, VectorArray centers, int dimensions,
list = palloc(itemsz); list = palloc(itemsz);
buf = IvfflatNewBuffer(index, forkNum); buf = IvfflatNewBuffer(index, forkNum);
IvfflatInitRegisterPage(index, &buf, &page, &state); IvfflatInitPage(index, &buf, &page, &state);
for (i = 0; i < lists; i++) for (i = 0; i < lists; i++)
{ {
@@ -577,7 +460,9 @@ BuildIndex(Relation heap, Relation index, IndexInfo *indexInfo,
{ {
InitBuildState(buildstate, heap, index, 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 */ /* Create pages */
CreateMetaPage(index, buildstate->dimensions, buildstate->lists, forkNum); CreateMetaPage(index, buildstate->dimensions, buildstate->lists, forkNum);

View File

@@ -45,8 +45,6 @@ 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_SORT: case PROGRESS_IVFFLAT_PHASE_SORT:

View File

@@ -14,8 +14,8 @@
#include "portability/instr_time.h" #include "portability/instr_time.h"
#endif #endif
#if PG_VERSION_NUM < 100000 #if PG_VERSION_NUM < 90600
#error "Requires PostgreSQL 10+" #error "Requires PostgreSQL 9.6+"
#endif #endif
/* Support functions */ /* Support functions */
@@ -37,10 +37,9 @@
/* Build phases */ /* Build phases */
/* PROGRESS_CREATEIDX_SUBPHASE_INITIALIZE is 1 */ /* PROGRESS_CREATEIDX_SUBPHASE_INITIALIZE is 1 */
#define PROGRESS_IVFFLAT_PHASE_SAMPLE 2 #define PROGRESS_IVFFLAT_PHASE_KMEANS 2
#define PROGRESS_IVFFLAT_PHASE_KMEANS 3 #define PROGRESS_IVFFLAT_PHASE_SORT 3
#define PROGRESS_IVFFLAT_PHASE_SORT 4 #define PROGRESS_IVFFLAT_PHASE_LOAD 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))
@@ -200,7 +199,7 @@ typedef IvfflatScanOpaqueData * IvfflatScanOpaque;
void _PG_init(void); void _PG_init(void);
VectorArray VectorArrayInit(int maxlen, int dimensions); VectorArray VectorArrayInit(int maxlen, int dimensions);
void PrintVectorArray(char *msg, VectorArray arr); void PrintVectorArray(char *msg, VectorArray arr);
void IvfflatKmeans(Relation index, VectorArray samples, VectorArray centers); void IvfflatKmeans(IvfflatBuildState * buildstate);
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);
@@ -208,8 +207,7 @@ void IvfflatUpdateList(Relation index, GenericXLogState *state, ListInfo listIn
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(Relation index, Buffer *buf, Page *page, GenericXLogState **state);
void IvfflatInitRegisterPage(Relation index, Buffer *buf, Page *page, GenericXLogState **state);
/* Index access methods */ /* Index access methods */
IndexBuildResult *ivfflatbuild(Relation heap, Relation index, IndexInfo *indexInfo); IndexBuildResult *ivfflatbuild(Relation heap, Relation index, IndexInfo *indexInfo);

View File

@@ -53,6 +53,18 @@ FindInsertPage(Relation rel, Datum *values, BlockNumber *insertPage, ListInfo *
} }
} }
/*
* Prepare to insert an index tuple
*/
static void
LoadInsertPage(Relation index, Buffer *buf, Page *page, GenericXLogState **state, BlockNumber insertPage)
{
*buf = ReadBuffer(index, insertPage);
LockBuffer(*buf, BUFFER_LOCK_EXCLUSIVE);
*state = GenericXLogStart(index);
*page = GenericXLogRegisterBuffer(*state, *buf, 0);
}
/* /*
* Insert a tuple into the index * Insert a tuple into the index
*/ */
@@ -75,18 +87,11 @@ InsertTuple(Relation rel, IndexTuple itup, Relation heapRel, Datum *values)
itemsz = MAXALIGN(IndexTupleSize(itup)); itemsz = MAXALIGN(IndexTupleSize(itup));
Assert(itemsz <= BLCKSZ - MAXALIGN(SizeOfPageHeaderData) - MAXALIGN(sizeof(IvfflatPageOpaqueData))); Assert(itemsz <= BLCKSZ - MAXALIGN(SizeOfPageHeaderData) - MAXALIGN(sizeof(IvfflatPageOpaqueData)));
LoadInsertPage(rel, &buf, &page, &state, insertPage);
/* Find a page to insert the item */ /* Find a page to insert the item */
for (;;) while (PageGetFreeSpace(page) < itemsz)
{ {
buf = ReadBuffer(rel, insertPage);
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
state = GenericXLogStart(rel);
page = GenericXLogRegisterBuffer(state, buf, 0);
if (PageGetFreeSpace(page) >= itemsz)
break;
insertPage = IvfflatPageGetOpaque(page)->nextblkno; insertPage = IvfflatPageGetOpaque(page)->nextblkno;
if (BlockNumberIsValid(insertPage)) if (BlockNumberIsValid(insertPage))
@@ -94,42 +99,15 @@ InsertTuple(Relation rel, IndexTuple itup, Relation heapRel, Datum *values)
/* Move to next page */ /* Move to next page */
GenericXLogAbort(state); GenericXLogAbort(state);
UnlockReleaseBuffer(buf); UnlockReleaseBuffer(buf);
LoadInsertPage(rel, &buf, &page, &state, insertPage);
} }
else else
{ {
Buffer metabuf;
Buffer newbuf;
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 */
newbuf = IvfflatNewBuffer(rel, MAIN_FORKNUM); IvfflatAppendPage(rel, &buf, &page, &state, MAIN_FORKNUM);
newpage = GenericXLogRegisterBuffer(state, newbuf, GENERIC_XLOG_FULL_IMAGE);
insertPage = BufferGetBlockNumber(newbuf); insertPage = BufferGetBlockNumber(buf);
/* Update previous buffer */
IvfflatPageGetOpaque(page)->nextblkno = insertPage;
/* Init page */
IvfflatInitPage(newbuf, newpage);
/* Commit */
MarkBufferDirty(buf);
MarkBufferDirty(newbuf);
GenericXLogFinish(state);
/* Unlock */
UnlockReleaseBuffer(buf);
UnlockReleaseBuffer(newbuf);
UnlockReleaseBuffer(metabuf);
} }
} }

View File

@@ -2,17 +2,19 @@
#include <float.h> #include <float.h>
#include "catalog/index.h"
#include "ivfflat.h" #include "ivfflat.h"
#include "miscadmin.h" #include "miscadmin.h"
#include "storage/bufmgr.h"
#if PG_VERSION_NUM >= 150000 #if PG_VERSION_NUM >= 120000
#include "common/pg_prng.h" #include "access/tableam.h"
#endif #endif
#if PG_VERSION_NUM >= 150000 #if PG_VERSION_NUM >= 130000
#define RandomDouble() pg_prng_double(&pg_global_prng_state) #define CALLBACK_ITEM_POINTER ItemPointer tid
#else #else
#define RandomDouble() (((double) random()) / MAX_RANDOM_VALUE) #define CALLBACK_ITEM_POINTER HeapTuple hup
#endif #endif
/* /*
@@ -21,12 +23,12 @@
* https://theory.stanford.edu/~sergei/papers/kMeansPP-soda.pdf * https://theory.stanford.edu/~sergei/papers/kMeansPP-soda.pdf
*/ */
static void static void
InitCenters(Relation index, VectorArray samples, VectorArray centers, float *lowerBound) InitCenters(Relation index, VectorArray samples, VectorArray centers)
{ {
FmgrInfo *procinfo; FmgrInfo *procinfo;
Oid collation; Oid collation;
int i; int i;
int64 j; int j;
double distance; double distance;
double sum; double sum;
double choice; double choice;
@@ -45,7 +47,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 (i = 0; i < numCenters; i++) for (i = 0; i < numCenters - 1; i++)
{ {
CHECK_FOR_INTERRUPTS(); CHECK_FOR_INTERRUPTS();
@@ -59,9 +61,6 @@ InitCenters(Relation index, VectorArray samples, VectorArray centers, float *low
/* TODO Use triangle inequality to reduce distance calculations */ /* TODO Use triangle inequality to reduce distance calculations */
distance = DatumGetFloat8(FunctionCall2Coll(procinfo, collation, PointerGetDatum(vec), PointerGetDatum(VectorArrayGet(centers, i)))); 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 */ /* Use distance squared for weighted probability distribution */
distance *= distance; distance *= distance;
@@ -71,12 +70,8 @@ InitCenters(Relation index, VectorArray samples, VectorArray centers, float *low
sum += weight[j]; sum += weight[j];
} }
/* Only compute lower bound on last iteration */
if (i + 1 == numCenters)
break;
/* Choose new center using weighted probability distribution. */ /* Choose new center using weighted probability distribution. */
choice = sum * RandomDouble(); choice = sum * (((double) random()) / MAX_RANDOM_VALUE);
for (j = 0; j < numSamples - 1; j++) for (j = 0; j < numSamples - 1; j++)
{ {
choice -= weight[j]; choice -= weight[j];
@@ -155,7 +150,7 @@ QuickCenters(Relation index, VectorArray samples, VectorArray centers)
vec->dim = dimensions; vec->dim = dimensions;
for (j = 0; j < dimensions; j++) for (j = 0; j < dimensions; j++)
vec->x[j] = RandomDouble(); vec->x[j] = ((double) random()) / MAX_RANDOM_VALUE;
/* Normalize if needed (only needed for random centers) */ /* Normalize if needed (only needed for random centers) */
if (normprocinfo != NULL) if (normprocinfo != NULL)
@@ -166,108 +161,147 @@ 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) * We use L2 distance for L2 (not L2 squared like index scan)
* and angular distance for inner product and cosine distance * 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 static void
ElkanKmeans(Relation index, VectorArray samples, VectorArray centers) MiniBatchKmeans(IvfflatBuildState * buildstate)
{ {
FmgrInfo *procinfo; VectorArray centers = buildstate->centers;
FmgrInfo *normprocinfo; VectorArray m = buildstate->samples;
Oid collation; int b = m->maxlen;
Vector *vec; int t = 20;
Vector *newCenter; double distance;
int iteration;
int64 j;
int64 k;
int dimensions = centers->dim;
int numCenters = centers->maxlen;
int numSamples = samples->length;
VectorArray newCenters;
int *centerCounts;
int *closestCenters;
float *lowerBound;
float *upperBound;
float *s;
float *halfcdist;
float *newcdist;
int changes;
double minDistance; double minDistance;
int closestCenter; int closestCenter;
double distance; int i;
bool rj; int j;
bool rjreset; int k;
double dxcx; Vector *c;
double dxc; Vector *x;
int *v;
/* Calculate allocation sizes */ int *d;
Size samplesSize = VECTOR_ARRAY_SIZE(samples->maxlen, samples->dim); double eta;
Size centersSize = VECTOR_ARRAY_SIZE(centers->maxlen, centers->dim);
Size newCentersSize = VECTOR_ARRAY_SIZE(numCenters, dimensions);
Size centerCountsSize = sizeof(int) * numCenters;
Size closestCentersSize = sizeof(int) * numSamples;
Size lowerBoundSize = sizeof(float) * numSamples * numCenters;
Size upperBoundSize = sizeof(float) * numSamples;
Size sSize = sizeof(float) * numCenters;
Size halfcdistSize = sizeof(float) * numCenters * numCenters;
Size newcdistSize = sizeof(float) * numCenters;
/* Calculate total size */
Size totalSize = samplesSize + centersSize + newCentersSize + centerCountsSize + closestCentersSize + lowerBoundSize + upperBoundSize + sSize + halfcdistSize + newcdistSize;
/* 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)));
/* Ensure indexing does not overflow */
if (numCenters * numCenters > INT_MAX)
elog(ERROR, "Indexing overflow detected. Please report a bug.");
/* Set support functions */ /* Set support functions */
procinfo = index_getprocinfo(index, 1, IVFFLAT_KMEANS_DISTANCE_PROC); FmgrInfo *procinfo = index_getprocinfo(buildstate->index, 1, IVFFLAT_KMEANS_DISTANCE_PROC);
normprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_KMEANS_NORM_PROC); FmgrInfo *normprocinfo = buildstate->kmeansnormprocinfo;
collation = index->rd_indcollation[0]; Oid collation = buildstate->index->rd_indcollation[0];
/* Allocate space */
/* Use float instead of double to save memory */
centerCounts = palloc(centerCountsSize);
closestCenters = palloc(closestCentersSize);
lowerBound = palloc_extended(lowerBoundSize, MCXT_ALLOC_HUGE);
upperBound = palloc(upperBoundSize);
s = palloc(sSize);
halfcdist = palloc(halfcdistSize);
newcdist = palloc(newcdistSize);
newCenters = VectorArrayInit(numCenters, dimensions);
for (j = 0; j < numCenters; j++)
{
vec = VectorArrayGet(newCenters, j);
SET_VARSIZE(vec, VECTOR_SIZE(dimensions));
vec->dim = dimensions;
}
/* Pick initial centers */ /* 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) */ v = palloc(sizeof(int) * centers->maxlen);
for (j = 0; j < numSamples; j++) d = palloc(sizeof(int) * b);
for (int i = 0; i < centers->length; i++)
v[i] = 0;
for (i = 0; i < t; i++)
{ {
/* Can take a while, so ensure we can interrupt */
CHECK_FOR_INTERRUPTS();
/* Get b examples picked randomly from X */
SampleRows(buildstate);
/* Cache nearest center to x */
for (j = 0; j < m->length; j++)
{
/* compute closest */
minDistance = DBL_MAX; minDistance = DBL_MAX;
closestCenter = -1; closestCenter = -1;
x = VectorArrayGet(m, j);
/* Find closest center */ /* Find closest center */
for (k = 0; k < numCenters; k++) for (k = 0; k < centers->length; k++)
{ {
/* TODO Use Lemma 1 in k-means++ initialization */ distance = DatumGetFloat8(FunctionCall2Coll(procinfo, collation, PointerGetDatum(x), PointerGetDatum(VectorArrayGet(centers, k))));
distance = lowerBound[j * numCenters + k];
if (distance < minDistance) if (distance < minDistance)
{ {
@@ -276,191 +310,53 @@ ElkanKmeans(Relation index, VectorArray samples, VectorArray centers)
} }
} }
upperBound[j] = minDistance; d[j] = closestCenter;
closestCenters[j] = closestCenter;
} }
/* Give 500 iterations to converge */ for (j = 0; j < m->length; j++)
for (iteration = 0; iteration < 500; iteration++)
{ {
/* Can take a while, so ensure we can interrupt */ x = VectorArrayGet(m, j);
CHECK_FOR_INTERRUPTS();
changes = 0; /* Get cached center for this x */
c = VectorArrayGet(centers, d[j]);
/* Step 1: For all centers, compute distance */ /* Update per-center counts */
for (j = 0; j < numCenters; j++) v[d[j]]++;
{
vec = VectorArrayGet(centers, j);
for (k = j + 1; k < numCenters; k++) /* Get per-center learning rate */
{ eta = 1.0 / v[d[j]];
distance = 0.5 * DatumGetFloat8(FunctionCall2Coll(procinfo, collation, PointerGetDatum(vec), PointerGetDatum(VectorArrayGet(centers, k))));
halfcdist[j * numCenters + k] = distance; /* Take gradient step */
halfcdist[k * numCenters + j] = distance; for (k = 0; k < c->dim; k++)
} c->x[k] = (1 - eta) * c->x[k] + eta * x->x[k];
} }
/* For all centers c, compute s(c) */ /* Check for empty centers (likely duplicates) */
for (j = 0; j < numCenters; j++) if (i == 0)
{ {
minDistance = DBL_MAX; for (j = 0; j < centers->length; j++)
for (k = 0; k < numCenters; k++)
{ {
if (j == k) if (v[j] == 0)
continue;
distance = halfcdist[j * numCenters + k];
if (distance < minDistance)
minDistance = distance;
}
s[j] = minDistance;
}
rjreset = iteration != 0;
for (j = 0; j < numSamples; j++)
{ {
/* Step 2: Identify all points x such that u(x) <= s(c(x)) */ c = VectorArrayGet(centers, j);
if (upperBound[j] <= s[closestCenters[j]])
continue;
rj = rjreset;
for (k = 0; k < numCenters; k++)
{
/* 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)
{
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++;
}
}
}
}
/* Step 4: For each center c, let m(c) be mean of all points assigned */
for (j = 0; j < numCenters; j++)
{
vec = VectorArrayGet(newCenters, j);
for (k = 0; k < dimensions; k++)
vec->x[k] = 0.0;
centerCounts[j] = 0;
}
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 */ /* TODO Handle empty centers properly */
for (k = 0; k < dimensions; k++) for (k = 0; k < c->dim; k++)
vec->x[k] = RandomDouble(); c->x[k] = ((double) random()) / MAX_RANDOM_VALUE;
}
}
} }
/* Normalize if needed */ /* Normalize if needed */
if (normprocinfo != NULL) 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++) for (j = 0; j < centers->length; j++)
{ ApplyNorm(normprocinfo, collation, VectorArrayGet(centers, j));
distance = lowerBound[j * numCenters + k] - newcdist[k];
if (distance < 0)
distance = 0;
lowerBound[j * numCenters + k] = distance;
} }
} }
/* Step 6 */ pfree(v);
/* We reset r(x) before Step 3 in the next iteration */ pfree(d);
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);
} }
/* /*
@@ -503,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 * We use spherical k-means for inner product and cosine
*/ */
void void
IvfflatKmeans(Relation index, VectorArray samples, VectorArray centers) IvfflatKmeans(IvfflatBuildState * buildstate)
{ {
if (samples->length <= centers->maxlen) int numSamples;
QuickCenters(index, samples, centers); Size totalSize;
else
ElkanKmeans(index, samples, centers);
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);
} }

View File

@@ -107,22 +107,13 @@ IvfflatNewBuffer(Relation index, ForkNumber forkNum)
* Init page * Init page
*/ */
void void
IvfflatInitPage(Buffer buf, Page page) IvfflatInitPage(Relation index, Buffer *buf, Page *page, GenericXLogState **state)
{
PageInit(page, BufferGetPageSize(buf), sizeof(IvfflatPageOpaqueData));
IvfflatPageGetOpaque(page)->nextblkno = InvalidBlockNumber;
IvfflatPageGetOpaque(page)->page_id = IVFFLAT_PAGE_ID;
}
/*
* Init and register page
*/
void
IvfflatInitRegisterPage(Relation index, Buffer *buf, Page *page, GenericXLogState **state)
{ {
*state = GenericXLogStart(index); *state = GenericXLogStart(index);
*page = GenericXLogRegisterBuffer(*state, *buf, GENERIC_XLOG_FULL_IMAGE); *page = GenericXLogRegisterBuffer(*state, *buf, GENERIC_XLOG_FULL_IMAGE);
IvfflatInitPage(*buf, *page); PageInit(*page, BufferGetPageSize(*buf), sizeof(IvfflatPageOpaqueData));
IvfflatPageGetOpaque(*page)->nextblkno = InvalidBlockNumber;
IvfflatPageGetOpaque(*page)->page_id = IVFFLAT_PAGE_ID;
} }
/* /*
@@ -144,27 +135,17 @@ IvfflatCommitBuffer(Buffer buf, GenericXLogState *state)
void void
IvfflatAppendPage(Relation index, Buffer *buf, Page *page, GenericXLogState **state, ForkNumber forkNum) IvfflatAppendPage(Relation index, Buffer *buf, Page *page, GenericXLogState **state, ForkNumber forkNum)
{ {
/* Get new buffer */ Buffer prevbuf = *buf;
Buffer newbuf = IvfflatNewBuffer(index, forkNum);
Page newpage = GenericXLogRegisterBuffer(*state, newbuf, GENERIC_XLOG_FULL_IMAGE);
/* Update the previous buffer */ /* Get new buffer */
IvfflatPageGetOpaque(*page)->nextblkno = BufferGetBlockNumber(newbuf); *buf = IvfflatNewBuffer(index, forkNum);
/* Update and commit previous buffer */
IvfflatPageGetOpaque(*page)->nextblkno = BufferGetBlockNumber(*buf);
IvfflatCommitBuffer(prevbuf, *state);
/* Init new page */ /* Init new page */
IvfflatInitPage(newbuf, newpage); IvfflatInitPage(index, buf, page, state);
/* Commit */
MarkBufferDirty(*buf);
MarkBufferDirty(newbuf);
GenericXLogFinish(*state);
/* Unlock */
UnlockReleaseBuffer(*buf);
*state = GenericXLogStart(index);
*page = GenericXLogRegisterBuffer(*state, newbuf, GENERIC_XLOG_FULL_IMAGE);
*buf = newbuf;
} }
/* /*

View File

@@ -1,8 +0,0 @@
use PostgreSQL::Test::Cluster;
sub get_new_node
{
return PostgreSQL::Test::Cluster->new(@_);
}
1;

View File

@@ -1,3 +0,0 @@
use PostgreSQL::Test::Utils;
1;

View File

@@ -7,8 +7,6 @@ use PostgresNode;
use TestLib; use TestLib;
use Test::More tests => 31; use Test::More tests => 31;
my $dim = 32;
my $node_primary; my $node_primary;
my $node_replica; my $node_replica;
@@ -32,15 +30,13 @@ sub test_index_replay
$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 $r1 = rand();
for (1 .. $dim) { my $r2 = rand();
push(@r, rand()); my $r3 = rand();
}
my $sql = join(",", @r);
my $queries = qq( my $queries = qq(
SET enable_seqscan = off; SET enable_seqscan = off;
SELECT * FROM tst ORDER BY v <-> '[$sql]' LIMIT 10; SELECT * FROM tst ORDER BY v <-> '[$r1,$r2,$r3]' LIMIT 10;
); );
# Run test queries and compare their result # Run test queries and compare their result
@@ -51,18 +47,9 @@ sub test_index_replay
return; 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 # 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) {
# TODO use wal_keep_segments for Postgres < 13
$node_primary->append_conf('postgresql.conf', qq(wal_keep_size = 1GB));
}
$node_primary->start; $node_primary->start;
my $backup_name = 'my_backup'; my $backup_name = 'my_backup';
@@ -77,9 +64,9 @@ $node_replica->start;
# Create ivfflat index on primary # Create ivfflat index on primary
$node_primary->safe_psql("postgres", "CREATE EXTENSION vector;"); $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", "CREATE TABLE tst (i int4, v vector(3));");
$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[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);");
@@ -95,7 +82,7 @@ for my $i (1 .. 10)
test_index_replay("vacuum $i"); test_index_replay("vacuum $i");
my ($start, $end) = (100001 + ($i - 1) * 10000, 100000 + $i * 10000); my ($start, $end) = (100001 + ($i - 1) * 10000, 100000 + $i * 10000);
$node_primary->safe_psql("postgres", $node_primary->safe_psql("postgres",
"INSERT INTO tst SELECT i % 10, ARRAY[$array_sql] 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"); test_index_replay("insert $i");
} }

View File

@@ -1,45 +0,0 @@
use strict;
use warnings;
use PostgresNode;
use TestLib;
use Test::More tests => 5;
my $dim = 768;
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",
"INSERT INTO tst SELECT ARRAY[$array_sql] FROM generate_series(1, 10000) i;"
);
$node->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v);");
$node->pgbench(
"--no-vacuum --client=5 --transactions=100",
0,
[qr{actually processed}],
[qr{^$}],
"concurrent INSERTs",
{
"007_inserts" => "INSERT INTO tst SELECT ARRAY[$array_sql] FROM generate_series(1, 10) i;"
}
);
my $expected = 10000 + 5 * 100 * 10;
my $count = $node->safe_psql("postgres", "SELECT COUNT(*) FROM tst;");
is($count, $expected);
$count = $node->safe_psql("postgres", qq(
SET enable_seqscan = off;
SET ivfflat.probes = 100;
SELECT COUNT(*) FROM (SELECT v FROM tst ORDER BY v <-> (SELECT v FROM tst LIMIT 1)) t;
));
is($count, $expected);

View File

@@ -1,4 +1,4 @@
comment = 'vector data type and ivfflat access method' comment = 'vector data type and ivfflat access method'
default_version = '0.3.1' default_version = '0.2.5'
module_pathname = '$libdir/vector' module_pathname = '$libdir/vector'
relocatable = true relocatable = true