Compare commits

..

2 Commits

Author SHA1 Message Date
Andrew Kane
f7f989d1b0 Fixed alloc [skip ci] 2022-03-12 12:58:35 -08:00
Andrew Kane
aabe549ec6 Significantly improved index query performance [skip ci] 2022-02-15 20:21:29 -08:00
25 changed files with 408 additions and 346 deletions

View File

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

View File

@@ -7,18 +7,17 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest]
postgres: [15, 14, 13, 12, 11, 10]
postgres: [14, 13, 12, 11, 10, 9.6]
include:
- os: macos-latest
postgres: 14
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- uses: ankane/setup-postgres@v1
with:
postgres-version: ${{ matrix.postgres }}
dev-files: true
- if: ${{ startsWith(matrix.os, 'ubuntu') }}
run: sudo apt-get update && sudo apt-get install libipc-run-perl
run: sudo apt-get update && sudo apt-get install postgresql-server-dev-${{ matrix.postgres }} libipc-run-perl
- run: make
- if: ${{ startsWith(matrix.os, 'ubuntu') }}
run: |
@@ -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_14_5.tar.gz
tar xf REL_14_5.tar.gz
make prove_installcheck PROVE=prove PROVE_FLAGS="-I ./postgres-REL_14_5/src/test/perl" PERL5LIB="/Users/runner/perl5/lib/perl5"
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

1
.gitignore vendored
View File

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

View File

@@ -1,25 +1,6 @@
## 0.3.2 (2022-11-22)
- Fixed `invalid memory alloc request size` error
## 0.3.1 (2022-11-02)
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)
## 0.2.6 (unreleased)
- Significantly improved index query performance
- Improved performance of index creation for Postgres < 12
## 0.2.5 (2022-02-11)

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
EXTENSION = vector
EXTVERSION = 0.3.2
EXTVERSION = 0.2.5
MODULE_big = vector
DATA = $(wildcard sql/*--*.sql)
@@ -40,9 +40,6 @@ PG_CONFIG ?= pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)
# for Postgres 15
PROVE_FLAGS += -I ./test/perl
prove_installcheck:
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)

View File

@@ -3,9 +3,9 @@
Open-source vector similarity search for Postgres
```sql
CREATE TABLE items (embedding vector(3));
CREATE INDEX ON items USING ivfflat (embedding vector_l2_ops);
SELECT * FROM items ORDER BY embedding <-> '[1,2,3]' LIMIT 5;
CREATE TABLE table (column vector(3));
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
@@ -14,10 +14,10 @@ Supports L2 distance, inner product, and cosine distance
## Installation
Compile and install the extension (supports Postgres 10+)
Compile and install the extension (supports Postgres 9.6+)
```sh
git clone --branch v0.3.2 https://github.com/pgvector/pgvector.git
git clone --branch v0.2.5 https://github.com/pgvector/pgvector.git
cd pgvector
make
make install # may need sudo
@@ -33,22 +33,22 @@ You can also install it with [Docker](#docker), [Homebrew](#homebrew), or [PGXN]
## 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
CREATE TABLE items (embedding vector(3));
CREATE TABLE table (column vector(3));
```
Insert values
```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
```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 (`<=>`)
@@ -62,19 +62,19 @@ Speed up queries with an approximate index. Add an index for each distance funct
L2 distance
```sql
CREATE INDEX ON items USING ivfflat (embedding vector_l2_ops);
CREATE INDEX ON table USING ivfflat (column vector_l2_ops);
```
Inner product
```sql
CREATE INDEX ON items USING ivfflat (embedding vector_ip_ops);
CREATE INDEX ON table USING ivfflat (column vector_ip_ops);
```
Cosine distance
```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.
@@ -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)
```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)`
@@ -131,20 +131,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
```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:
```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);
```
To index many different values of `other_column`, consider [partitioning](https://www.postgresql.org/docs/current/ddl-partitioning.html) on `other_column`.
## Performance
@@ -157,7 +147,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).
```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
@@ -195,10 +185,8 @@ Libraries that use pgvector:
- [pgvector-ruby](https://github.com/pgvector/pgvector-ruby) (Ruby)
- [pgvector-node](https://github.com/pgvector/pgvector-node) (Node.js)
- [pgvector-go](https://github.com/pgvector/pgvector-go) (Go)
- [pgvector-php](https://github.com/pgvector/pgvector-php) (PHP)
- [pgvector-rust](https://github.com/pgvector/pgvector-rust) (Rust)
- [pgvector-cpp](https://github.com/pgvector/pgvector-cpp) (C++)
- [pgvector-elixir](https://github.com/pgvector/pgvector-elixir) (Elixir)
## Frequently Asked Questions
@@ -232,14 +220,14 @@ This adds pgvector to the [Postgres image](https://hub.docker.com/_/postgres).
You can also build the image manually
```sh
git clone --branch v0.3.2 https://github.com/pgvector/pgvector.git
git clone --branch v0.2.5 https://github.com/pgvector/pgvector.git
cd pgvector
docker build -t pgvector .
```
### Homebrew
With Homebrew Postgres, you can use:
On Mac with Homebrew Postgres, you can use:
```sh
brew install pgvector/brew/pgvector
@@ -270,22 +258,6 @@ Install the latest version and run:
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 to:

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

@@ -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.2'" to load this file. \quit

View File

@@ -80,11 +80,7 @@ SampleCallback(Relation index, CALLBACK_ITEM_POINTER, Datum *values,
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));
@@ -233,8 +229,8 @@ InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum)
GenericXLogState *state;
int list;
IndexTuple itup = NULL; /* silence compiler warning */
BlockNumber startPage;
BlockNumber insertPage;
BlockNumber startPage = InvalidBlockNumber;
BlockNumber insertPage = InvalidBlockNumber;
Size itemsz;
int i;
int64 inserted = 0;
@@ -259,7 +255,7 @@ InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum)
CHECK_FOR_INTERRUPTS();
buf = IvfflatNewBuffer(index, forkNum);
IvfflatInitRegisterPage(index, &buf, &page, &state);
IvfflatInitPage(index, &buf, &page, &state);
startPage = BufferGetBlockNumber(buf);
@@ -416,7 +412,7 @@ CreateMetaPage(Relation index, int dimensions, int lists, ForkNumber forkNum)
IvfflatMetaPage metap;
buf = IvfflatNewBuffer(index, forkNum);
IvfflatInitRegisterPage(index, &buf, &page, &state);
IvfflatInitPage(index, &buf, &page, &state);
/* Set metapage data */
metap = IvfflatPageGetMeta(page);
@@ -449,7 +445,7 @@ CreateListPages(Relation index, VectorArray centers, int dimensions,
list = palloc(itemsz);
buf = IvfflatNewBuffer(index, forkNum);
IvfflatInitRegisterPage(index, &buf, &page, &state);
IvfflatInitPage(index, &buf, &page, &state);
for (i = 0; i < lists; i++)
{

View File

@@ -14,8 +14,8 @@
#include "portability/instr_time.h"
#endif
#if PG_VERSION_NUM < 100000
#error "Requires PostgreSQL 10+"
#if PG_VERSION_NUM < 90600
#error "Requires PostgreSQL 9.6+"
#endif
/* Support functions */
@@ -167,12 +167,28 @@ typedef struct IvfflatScanList
double distance;
} IvfflatScanList;
typedef struct IvfflatScanItem
{
pairingheap_node ph_node;
BlockNumber searchPage;
double distance;
ItemPointerData tid;
} IvfflatScanItem;
typedef struct IvfflatScanOpaqueData
{
int probes;
bool first;
int stage;
Buffer buf;
/* Items */
int maxItems;
int itemCount;
pairingheap *itemQueue;
IvfflatScanItem *items;
IvfflatScanItem **sortedItems;
bool heapFull;
/* Sorting */
Tuplesortstate *sortstate;
TupleDesc tupdesc;
@@ -186,6 +202,7 @@ typedef struct IvfflatScanOpaqueData
/* Lists */
pairingheap *listQueue;
IvfflatScanList **sortedLists;
IvfflatScanList lists[FLEXIBLE_ARRAY_MEMBER]; /* must come last */
} IvfflatScanOpaqueData;
@@ -208,8 +225,7 @@ void IvfflatUpdateList(Relation index, GenericXLogState *state, ListInfo listIn
void IvfflatCommitBuffer(Buffer buf, GenericXLogState *state);
void IvfflatAppendPage(Relation index, Buffer *buf, Page *page, GenericXLogState **state, ForkNumber forkNum);
Buffer IvfflatNewBuffer(Relation index, ForkNumber forkNum);
void IvfflatInitPage(Buffer buf, Page page);
void IvfflatInitRegisterPage(Relation index, Buffer *buf, Page *page, GenericXLogState **state);
void IvfflatInitPage(Relation index, Buffer *buf, Page *page, GenericXLogState **state);
/* Index access methods */
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
*/
@@ -75,18 +87,11 @@ InsertTuple(Relation rel, IndexTuple itup, Relation heapRel, Datum *values)
itemsz = MAXALIGN(IndexTupleSize(itup));
Assert(itemsz <= BLCKSZ - MAXALIGN(SizeOfPageHeaderData) - MAXALIGN(sizeof(IvfflatPageOpaqueData)));
LoadInsertPage(rel, &buf, &page, &state, insertPage);
/* 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;
if (BlockNumberIsValid(insertPage))
@@ -94,45 +99,15 @@ InsertTuple(Relation rel, IndexTuple itup, Relation heapRel, Datum *values)
/* Move to next page */
GenericXLogAbort(state);
UnlockReleaseBuffer(buf);
LoadInsertPage(rel, &buf, &page, &state, insertPage);
}
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 */
newbuf = IvfflatNewBuffer(rel, MAIN_FORKNUM);
newpage = GenericXLogRegisterBuffer(state, newbuf, GENERIC_XLOG_FULL_IMAGE);
IvfflatAppendPage(rel, &buf, &page, &state, MAIN_FORKNUM);
/* Init new page */
IvfflatInitPage(newbuf, newpage);
/* Update insert page */
insertPage = BufferGetBlockNumber(newbuf);
/* Update previous buffer */
IvfflatPageGetOpaque(page)->nextblkno = insertPage;
/* Commit */
MarkBufferDirty(newbuf);
MarkBufferDirty(buf);
GenericXLogFinish(state);
/* Unlock extend relation lock as early as possible */
UnlockReleaseBuffer(metabuf);
/* Unlock rest */
UnlockReleaseBuffer(newbuf);
UnlockReleaseBuffer(buf);
insertPage = BufferGetBlockNumber(buf);
}
}

View File

@@ -5,16 +5,6 @@
#include "ivfflat.h"
#include "miscadmin.h"
#if PG_VERSION_NUM >= 150000
#include "common/pg_prng.h"
#endif
#if PG_VERSION_NUM >= 150000
#define RandomDouble() pg_prng_double(&pg_global_prng_state)
#else
#define RandomDouble() (((double) random()) / MAX_RANDOM_VALUE)
#endif
/*
* Initialize with kmeans++
*
@@ -26,7 +16,7 @@ InitCenters(Relation index, VectorArray samples, VectorArray centers, float *low
FmgrInfo *procinfo;
Oid collation;
int i;
int64 j;
int j;
double distance;
double sum;
double choice;
@@ -76,7 +66,7 @@ InitCenters(Relation index, VectorArray samples, VectorArray centers, float *low
break;
/* Choose new center using weighted probability distribution. */
choice = sum * RandomDouble();
choice = sum * (((double) random()) / MAX_RANDOM_VALUE);
for (j = 0; j < numSamples - 1; j++)
{
choice -= weight[j];
@@ -155,7 +145,7 @@ QuickCenters(Relation index, VectorArray samples, VectorArray centers)
vec->dim = dimensions;
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) */
if (normprocinfo != NULL)
@@ -182,8 +172,8 @@ ElkanKmeans(Relation index, VectorArray samples, VectorArray centers)
Vector *vec;
Vector *newCenter;
int iteration;
int64 j;
int64 k;
int j;
int k;
int dimensions = centers->dim;
int numCenters = centers->maxlen;
int numSamples = samples->length;
@@ -227,10 +217,6 @@ ElkanKmeans(Relation index, VectorArray samples, VectorArray centers)
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 */
procinfo = index_getprocinfo(index, 1, IVFFLAT_KMEANS_DISTANCE_PROC);
normprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_KMEANS_NORM_PROC);
@@ -243,7 +229,7 @@ ElkanKmeans(Relation index, VectorArray samples, VectorArray centers)
lowerBound = palloc_extended(lowerBoundSize, MCXT_ALLOC_HUGE);
upperBound = palloc(upperBoundSize);
s = palloc(sSize);
halfcdist = palloc_extended(halfcdistSize, MCXT_ALLOC_HUGE);
halfcdist = palloc(halfcdistSize);
newcdist = palloc(newcdistSize);
newCenters = VectorArrayInit(numCenters, dimensions);
@@ -263,6 +249,8 @@ ElkanKmeans(Relation index, VectorArray samples, VectorArray centers)
minDistance = DBL_MAX;
closestCenter = -1;
vec = VectorArrayGet(samples, j);
/* Find closest center */
for (k = 0; k < numCenters; k++)
{
@@ -415,7 +403,7 @@ ElkanKmeans(Relation index, VectorArray samples, VectorArray centers)
{
/* TODO Handle empty centers properly */
for (k = 0; k < dimensions; k++)
vec->x[k] = RandomDouble();
vec->x[k] = ((double) random()) / MAX_RANDOM_VALUE;
}
/* Normalize if needed */

View File

@@ -30,6 +30,21 @@ CompareLists(const pairingheap_node *a, const pairingheap_node *b, void *arg)
return 0;
}
/*
* Compare item distances
*/
static int
CompareItems(const pairingheap_node *a, const pairingheap_node *b, void *arg)
{
if (((const IvfflatScanItem *) a)->distance > ((const IvfflatScanItem *) b)->distance)
return 1;
if (((const IvfflatScanItem *) a)->distance < ((const IvfflatScanItem *) b)->distance)
return -1;
return ItemPointerCompare(&((IvfflatScanItem *) a)->tid, &((IvfflatScanItem *) b)->tid);
}
/*
* Get lists and sort by distance
*/
@@ -44,6 +59,7 @@ GetScanLists(IndexScanDesc scan, Datum value)
BlockNumber nextblkno = IVFFLAT_HEAD_BLKNO;
int listCount = 0;
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
int i;
double distance;
IvfflatScanList *scanlist;
double maxDistance = DBL_MAX;
@@ -97,6 +113,140 @@ GetScanLists(IndexScanDesc scan, Datum value)
UnlockReleaseBuffer(cbuf);
}
for (i = 0; i < so->probes; i++)
so->sortedLists[i] = (IvfflatScanList *) pairingheap_remove_first(so->listQueue);
Assert(pairingheap_is_empty(so->listQueue));
}
/*
* Get items
*/
static void
GetScanItemsQuick(IndexScanDesc scan, Datum value)
{
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
Buffer buf;
Page page;
IndexTuple itup;
BlockNumber searchPage;
OffsetNumber offno;
OffsetNumber maxoffno;
Datum datum;
bool isnull;
TupleDesc tupdesc = RelationGetDescr(scan->indexRelation);
int i;
double distance;
IvfflatScanItem *scanitem;
double maxDistance = DBL_MAX;
/*
* Reuse same set of shared buffers for scan
*
* See postgres/src/backend/storage/buffer/README for description
*/
BufferAccessStrategy bas = GetAccessStrategy(BAS_BULKREAD);
/* Search closest probes lists */
for (i = 0; i < so->probes; i++)
{
/* Read closest lists first for performance */
searchPage = so->sortedLists[i]->startPage;
/* Search all entry pages for list */
while (BlockNumberIsValid(searchPage))
{
buf = ReadBufferExtended(scan->indexRelation, MAIN_FORKNUM, searchPage, RBM_NORMAL, bas);
LockBuffer(buf, BUFFER_LOCK_SHARE);
page = BufferGetPage(buf);
maxoffno = PageGetMaxOffsetNumber(page);
for (offno = FirstOffsetNumber; offno <= maxoffno; offno = OffsetNumberNext(offno))
{
itup = (IndexTuple) PageGetItem(page, PageGetItemId(page, offno));
datum = index_getattr(itup, 1, tupdesc, &isnull);
distance = DatumGetFloat8(FunctionCall2Coll(so->procinfo, so->collation, datum, value));
if (so->itemCount < so->maxItems)
{
scanitem = &so->items[so->itemCount];
scanitem->searchPage = searchPage;
scanitem->tid = itup->t_tid;
scanitem->distance = distance;
so->itemCount++;
/* Add to heap */
pairingheap_add(so->itemQueue, &scanitem->ph_node);
/* Calculate max distance */
if (so->itemCount == so->maxItems)
{
maxDistance = ((IvfflatScanItem *) pairingheap_first(so->itemQueue))->distance;
scanitem = &so->items[so->itemCount];
}
}
else if (distance <= maxDistance)
{
/* Reuse */
scanitem->searchPage = searchPage;
scanitem->tid = itup->t_tid;
scanitem->distance = distance;
pairingheap_add(so->itemQueue, &scanitem->ph_node);
/* Remove */
scanitem = (IvfflatScanItem *) pairingheap_remove_first(so->itemQueue);
/* Update max distance */
maxDistance = ((IvfflatScanItem *) pairingheap_first(so->itemQueue))->distance;
}
}
searchPage = IvfflatPageGetOpaque(page)->nextblkno;
UnlockReleaseBuffer(buf);
}
}
for (i = 0; i < so->itemCount; i++)
so->sortedItems[i] = (IvfflatScanItem *) pairingheap_remove_first(so->itemQueue);
Assert(pairingheap_is_empty(so->itemQueue));
}
/*
* Initialize sort
*/
static void
InitSort(IvfflatScanOpaque so)
{
AttrNumber attNums[] = {1, 2};
Oid sortOperators[] = {Float8LessOperator, TIDLessOperator};
Oid sortCollations[] = {InvalidOid, InvalidOid};
bool nullsFirstFlags[] = {false, false};
/* Create tuple description for sorting */
#if PG_VERSION_NUM >= 120000
so->tupdesc = CreateTemplateTupleDesc(3);
#else
so->tupdesc = CreateTemplateTupleDesc(3, false);
#endif
TupleDescInitEntry(so->tupdesc, (AttrNumber) 1, "distance", FLOAT8OID, -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
so->sortstate = tuplesort_begin_heap(so->tupdesc, sizeof(attNums) / sizeof(attNums[0]), attNums, sortOperators, sortCollations, nullsFirstFlags, work_mem, NULL, false);
#else
so->sortstate = tuplesort_begin_heap(so->tupdesc, sizeof(attNums) / sizeof(attNums[0]), attNums, sortOperators, sortCollations, nullsFirstFlags, work_mem, false);
#endif
#if PG_VERSION_NUM >= 120000
so->slot = MakeSingleTupleTableSlot(so->tupdesc, &TTSOpsMinimalTuple);
#else
so->slot = MakeSingleTupleTableSlot(so->tupdesc);
#endif
}
/*
@@ -115,6 +265,7 @@ GetScanItems(IndexScanDesc scan, Datum value)
Datum datum;
bool isnull;
TupleDesc tupdesc = RelationGetDescr(scan->indexRelation);
int i;
#if PG_VERSION_NUM >= 120000
TupleTableSlot *slot = MakeSingleTupleTableSlot(so->tupdesc, &TTSOpsVirtual);
@@ -130,9 +281,9 @@ GetScanItems(IndexScanDesc scan, Datum value)
BufferAccessStrategy bas = GetAccessStrategy(BAS_BULKREAD);
/* Search closest probes lists */
while (!pairingheap_is_empty(so->listQueue))
for (i = 0; i < so->probes; i++)
{
searchPage = ((IvfflatScanList *) pairingheap_remove_first(so->listQueue))->startPage;
searchPage = so->sortedLists[i]->startPage;
/* Search all entry pages for list */
while (BlockNumberIsValid(searchPage))
@@ -172,6 +323,7 @@ GetScanItems(IndexScanDesc scan, Datum value)
}
tuplesort_performsort(so->sortstate);
tuplesort_skiptuples(so->sortstate, so->maxItems, true);
}
/*
@@ -180,24 +332,17 @@ GetScanItems(IndexScanDesc scan, Datum value)
IndexScanDesc
ivfflatbeginscan(Relation index, int nkeys, int norderbys)
{
IndexScanDesc scan;
IvfflatScanOpaque so;
int lists;
AttrNumber attNums[] = {1};
Oid sortOperators[] = {Float8LessOperator};
Oid sortCollations[] = {InvalidOid};
bool nullsFirstFlags[] = {false};
IndexScanDesc scan = RelationGetIndexScan(index, nkeys, norderbys);
int lists = IvfflatGetLists(scan->indexRelation);
int probes = ivfflat_probes;
scan = RelationGetIndexScan(index, nkeys, norderbys);
lists = IvfflatGetLists(scan->indexRelation);
IvfflatScanOpaque so;
if (probes > lists)
probes = lists;
so = (IvfflatScanOpaque) palloc(offsetof(IvfflatScanOpaqueData, lists) + probes * sizeof(IvfflatScanList));
so->buf = InvalidBuffer;
so->first = true;
so->stage = 0;
so->probes = probes;
/* Set support functions */
@@ -205,30 +350,16 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys)
so->normprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_NORM_PROC);
so->collation = index->rd_indcollation[0];
/* Create tuple description for sorting */
#if PG_VERSION_NUM >= 120000
so->tupdesc = CreateTemplateTupleDesc(3);
#else
so->tupdesc = CreateTemplateTupleDesc(3, false);
#endif
TupleDescInitEntry(so->tupdesc, (AttrNumber) 1, "distance", FLOAT8OID, -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
so->sortstate = tuplesort_begin_heap(so->tupdesc, 1, attNums, sortOperators, sortCollations, nullsFirstFlags, work_mem, NULL, false);
#else
so->sortstate = tuplesort_begin_heap(so->tupdesc, 1, attNums, sortOperators, sortCollations, nullsFirstFlags, work_mem, false);
#endif
#if PG_VERSION_NUM >= 120000
so->slot = MakeSingleTupleTableSlot(so->tupdesc, &TTSOpsMinimalTuple);
#else
so->slot = MakeSingleTupleTableSlot(so->tupdesc);
#endif
so->listQueue = pairingheap_allocate(CompareLists, scan);
so->sortedLists = palloc(sizeof(IvfflatScanItem *) * probes);
so->maxItems = 1024;
so->itemCount = 0;
so->itemQueue = pairingheap_allocate(CompareItems, scan);
so->items = palloc(sizeof(IvfflatScanItem) * (so->maxItems + 1));
so->sortedItems = palloc(sizeof(IvfflatScanItem *) * so->maxItems);
so->sortstate = NULL;
scan->opaque = so;
@@ -244,12 +375,14 @@ ivfflatrescan(IndexScanDesc scan, ScanKey keys, int nkeys, ScanKey orderbys, int
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
#if PG_VERSION_NUM >= 130000
if (!so->first)
if (so->sortstate != NULL)
tuplesort_reset(so->sortstate);
#endif
so->first = true;
so->stage = 0;
pairingheap_reset(so->listQueue);
pairingheap_reset(so->itemQueue);
so->itemCount = 0;
if (keys && scan->numberOfKeys > 0)
memmove(scan->keyData, keys, scan->numberOfKeys * sizeof(ScanKeyData));
@@ -272,7 +405,7 @@ ivfflatgettuple(IndexScanDesc scan, ScanDirection dir)
*/
Assert(ScanDirectionIsForward(dir));
if (so->first)
if (so->stage == 0)
{
Datum value;
@@ -294,42 +427,101 @@ ivfflatgettuple(IndexScanDesc scan, ScanDirection dir)
}
IvfflatBench("GetScanLists", GetScanLists(scan, value));
IvfflatBench("GetScanItems", GetScanItems(scan, value));
so->first = false;
IvfflatBench("GetScanItemsQuick", GetScanItemsQuick(scan, value));
so->heapFull = so->itemCount == so->maxItems;
so->stage++;
/* Clean up if we allocated a new value */
if (value != scan->orderByData->sk_argument)
pfree(DatumGetPointer(value));
}
#if PG_VERSION_NUM >= 100000
if (tuplesort_gettupleslot(so->sortstate, true, false, so->slot, NULL))
#else
if (tuplesort_gettupleslot(so->sortstate, true, so->slot, NULL))
#endif
if (so->stage == 1)
{
ItemPointer tid = (ItemPointer) DatumGetPointer(slot_getattr(so->slot, 2, &so->isnull));
BlockNumber indexblkno = DatumGetInt32(slot_getattr(so->slot, 3, &so->isnull));
if (so->itemCount > 0)
{
IvfflatScanItem *scanitem;
so->itemCount--;
scanitem = so->sortedItems[so->itemCount];
#if PG_VERSION_NUM >= 120000
scan->xs_heaptid = *tid;
scan->xs_heaptid = scanitem->tid;
#else
scan->xs_ctup.t_self = *tid;
scan->xs_ctup.t_sef = scanitem->tid;
#endif
if (BufferIsValid(so->buf))
ReleaseBuffer(so->buf);
if (BufferIsValid(so->buf))
ReleaseBuffer(so->buf);
/*
* An index scan must maintain a pin on the index page holding the
* item last returned by amgettuple
*
* https://www.postgresql.org/docs/current/index-locking.html
*/
so->buf = ReadBuffer(scan->indexRelation, indexblkno);
/*
* An index scan must maintain a pin on the index page holding the
* item last returned by amgettuple
*
* https://www.postgresql.org/docs/current/index-locking.html
*/
so->buf = ReadBuffer(scan->indexRelation, scanitem->searchPage);
scan->xs_recheckorderby = false;
return true;
scan->xs_recheckorderby = false;
return true;
}
else if (so->heapFull)
{
Datum value = scan->orderByData->sk_argument;
if (so->normprocinfo != NULL)
{
/* No items will match if normalization fails */
if (!IvfflatNormValue(so->normprocinfo, so->collation, &value, NULL))
return false;
}
if (so->sortstate == NULL)
InitSort(so);
IvfflatBench("GetScanItems", GetScanItems(scan, value));
so->stage++;
/* Clean up if we allocated a new value */
if (value != scan->orderByData->sk_argument)
pfree(DatumGetPointer(value));
}
else
so->stage = 3;
}
if (so->stage == 2)
{
#if PG_VERSION_NUM >= 100000
if (tuplesort_gettupleslot(so->sortstate, true, false, so->slot, NULL))
#else
if (tuplesort_gettupleslot(so->sortstate, true, so->slot, NULL))
#endif
{
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
scan->xs_heaptid = *tid;
#else
scan->xs_ctup.t_self = *tid;
#endif
if (BufferIsValid(so->buf))
ReleaseBuffer(so->buf);
/*
* An index scan must maintain a pin on the index page holding the
* item last returned by amgettuple
*
* https://www.postgresql.org/docs/current/index-locking.html
*/
so->buf = ReadBuffer(scan->indexRelation, indexblkno);
scan->xs_recheckorderby = false;
return true;
}
}
return false;
@@ -348,7 +540,14 @@ ivfflatendscan(IndexScanDesc scan)
ReleaseBuffer(so->buf);
pairingheap_free(so->listQueue);
tuplesort_end(so->sortstate);
pfree(so->sortedLists);
if (so->sortstate != NULL)
tuplesort_end(so->sortstate);
pairingheap_free(so->itemQueue);
pfree(so->items);
pfree(so->sortedItems);
pfree(so);
scan->opaque = NULL;

View File

@@ -10,7 +10,7 @@
VectorArray
VectorArrayInit(int maxlen, int dimensions)
{
VectorArray res = palloc_extended(VECTOR_ARRAY_SIZE(maxlen, dimensions), MCXT_ALLOC_ZERO | MCXT_ALLOC_HUGE);
VectorArray res = palloc0(VECTOR_ARRAY_SIZE(maxlen, dimensions));
res->length = 0;
res->maxlen = maxlen;
@@ -107,22 +107,13 @@ IvfflatNewBuffer(Relation index, ForkNumber forkNum)
* Init page
*/
void
IvfflatInitPage(Buffer buf, Page page)
{
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)
IvfflatInitPage(Relation index, Buffer *buf, Page *page, GenericXLogState **state)
{
*state = GenericXLogStart(index);
*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
IvfflatAppendPage(Relation index, Buffer *buf, Page *page, GenericXLogState **state, ForkNumber forkNum)
{
/* Get new buffer */
Buffer newbuf = IvfflatNewBuffer(index, forkNum);
Page newpage = GenericXLogRegisterBuffer(*state, newbuf, GENERIC_XLOG_FULL_IMAGE);
Buffer prevbuf = *buf;
/* Update the previous buffer */
IvfflatPageGetOpaque(*page)->nextblkno = BufferGetBlockNumber(newbuf);
/* Get new buffer */
*buf = IvfflatNewBuffer(index, forkNum);
/* Update and commit previous buffer */
IvfflatPageGetOpaque(*page)->nextblkno = BufferGetBlockNumber(*buf);
IvfflatCommitBuffer(prevbuf, *state);
/* Init new page */
IvfflatInitPage(newbuf, newpage);
/* Commit */
MarkBufferDirty(*buf);
MarkBufferDirty(newbuf);
GenericXLogFinish(*state);
/* Unlock */
UnlockReleaseBuffer(*buf);
*state = GenericXLogStart(index);
*page = GenericXLogRegisterBuffer(*state, newbuf, GENERIC_XLOG_FULL_IMAGE);
*buf = newbuf;
IvfflatInitPage(index, buf, page, state);
}
/*

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 Test::More tests => 31;
my $dim = 32;
my $node_primary;
my $node_replica;
@@ -32,15 +30,13 @@ sub test_index_replay
$node_primary->poll_query_until('postgres', $caughtup_query)
or die "Timed out while waiting for replica 1 to catch up";
my @r = ();
for (1 .. $dim) {
push(@r, rand());
}
my $sql = join(",", @r);
my $r1 = rand();
my $r2 = rand();
my $r3 = rand();
my $queries = qq(
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
@@ -51,18 +47,9 @@ sub test_index_replay
return;
}
# Use ARRAY[random(), random(), random(), ...] over
# SELECT array_agg(random()) FROM generate_series(1, $dim)
# to generate different values for each row
my $array_sql = join(",", ('random()') x $dim);
# Initialize primary node
$node_primary = get_new_node('primary');
$node_primary->init(allows_streaming => 1);
if ($dim > 32) {
# TODO use wal_keep_segments for Postgres < 13
$node_primary->append_conf('postgresql.conf', qq(wal_keep_size = 1GB));
}
$node_primary->start;
my $backup_name = 'my_backup';
@@ -77,9 +64,9 @@ $node_replica->start;
# Create ivfflat index on primary
$node_primary->safe_psql("postgres", "CREATE EXTENSION vector;");
$node_primary->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector($dim));");
$node_primary->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector(3));");
$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);");
@@ -95,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[$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");
}

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);

38
test/t/007_stages.pl Normal file
View File

@@ -0,0 +1,38 @@
use strict;
use warnings;
use PostgresNode;
use TestLib;
use Test::More tests => 2;
# 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, ARRAY[i % 1000, i % 1000, i % 1000] FROM generate_series(1, 10000) i;"
);
my @limits = (128, 2048);
my @expected = ();
foreach (@limits) {
my $res = $node->safe_psql("postgres", "SELECT i, v FROM tst ORDER BY v <-> '[0,0,0]', i LIMIT $_;");
push(@expected, $res);
}
$node->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v) WITH (lists = 5);");
for my $i (0 .. $#limits) {
my $res = $node->safe_psql("postgres", qq(
SET enable_seqscan = off;
SET ivfflat.probes = 5;
WITH tmp AS (
SELECT *, v <-> '[0,0,0]' AS d FROM tst ORDER BY v <-> '[0,0,0]' LIMIT $limits[$i]
) SELECT i, v FROM tmp ORDER BY d, i;
));
is($res, $expected[$i]);
}

View File

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