Compare commits

...

64 Commits

Author SHA1 Message Date
Andrew Kane
56cb5f3503 Version bump to 0.2.6 [skip ci] 2022-05-22 18:21:15 -07:00
Andrew Kane
ecbf46938f Moved metrics to separate function [skip ci] 2022-02-15 19:13:23 -08:00
Andrew Kane
4ca264ba02 Added Davies-Bouldin index [skip ci] 2022-02-15 19:08:47 -08:00
Andrew Kane
f5458414b8 Made recall test less flaky 2022-02-15 18:17:05 -08:00
Andrew Kane
e64ed39acb Disabled scan progress for sampling 2022-02-15 18:14:59 -08:00
Andrew Kane
0d025be9d3 Improved performance of index creation for Postgres < 12 2022-02-15 18:05:33 -08:00
Andrew Kane
fed60dce78 Added tests for lists 2022-02-15 17:04:20 -08:00
Andrew Kane
a37f5eea4a Improved compare method [skip ci] 2022-02-15 11:50:35 -08:00
Andrew Kane
4bdb27e85a Fixed style [skip ci] 2022-02-14 23:15:27 -08:00
Andrew Kane
38f869e0bd Added test for 100% recall 2022-02-14 23:13:36 -08:00
Andrew Kane
ad8acc00d4 Fixed spacing [skip ci] 2022-02-14 21:58:20 -08:00
Andrew Kane
01926a418e Print operator name for failed recall test [skip ci] 2022-02-14 21:52:31 -08:00
Andrew Kane
9658d3c1ad Made recall test less flaky 2022-02-14 21:49:08 -08:00
Andrew Kane
6b9c6516f4 Improved code 2022-02-14 21:41:27 -08:00
Andrew Kane
88be03a3fa Removed unused code 2022-02-14 21:37:53 -08:00
Andrew Kane
bf5b2c8d7e Use tuple id directly 2022-02-14 21:31:08 -08:00
Andrew Kane
ac65ec2856 Improved code [skip ci] 2022-02-14 19:38:59 -08:00
Andrew Kane
7bba0e2a01 Test recall for all operators 2022-02-14 15:22:53 -08:00
Andrew Kane
c35e9f3b84 Removed comment [skip ci] 2022-02-13 03:53:30 -08:00
Andrew Kane
9549d93260 Improved recall test 2022-02-13 03:08:39 -08:00
Andrew Kane
310809d0e5 Fixed warnings 2022-02-13 03:03:17 -08:00
Andrew Kane
ce72ca8620 Switched to heap for nearest lists for performance 2022-02-13 02:59:27 -08:00
Andrew Kane
4f2c937a1f Improved recall test 2022-02-13 02:35:28 -08:00
Andrew Kane
ff400ce5f1 Use macro for UpdateProgress 2022-02-12 23:56:28 -08:00
Andrew Kane
41d11c62d6 Fixed style [skip ci] 2022-02-12 22:14:57 -08:00
Andrew Kane
fa5e90585d Added inertia 2022-02-12 21:23:53 -08:00
Andrew Kane
71d2908be1 Fixed style [skip ci] 2022-02-12 20:47:25 -08:00
Andrew Kane
d0a1c1d0ed Added test for centers 2022-02-12 20:44:05 -08:00
Andrew Kane
8063201cdf Fixed indentation [skip ci] 2022-02-12 20:43:48 -08:00
Andrew Kane
2f52127bee Version bump to 0.2.5 [skip ci] 2022-02-11 18:50:04 -08:00
Andrew Kane
4778039ffa Added benchmarking to readme [skip ci] 2022-02-11 16:09:45 -08:00
Andrew Kane
e3c9d70acc Added to existing PG_CFLAGS 2022-02-11 16:06:25 -08:00
Andrew Kane
5971e28f8c Added more benchmarking [skip ci] 2022-02-11 16:05:45 -08:00
Andrew Kane
2da27e2f01 Updated name [skip ci] 2022-02-11 15:52:59 -08:00
Andrew Kane
7dd934913d Improved output [skip ci] 2022-02-11 15:51:14 -08:00
Andrew Kane
9b46987e73 Added support for benchmarking 2022-02-11 15:48:13 -08:00
Andrew Kane
868e1f99a4 Added todo [skip ci] 2022-02-11 15:14:09 -08:00
Andrew Kane
354271178f Added todo [skip ci] 2022-02-11 14:31:42 -08:00
Andrew Kane
fe9b218903 Reduced memory for unlogged tables [skip ci] 2022-02-11 13:57:53 -08:00
Andrew Kane
3cdd1c16fb Improved code [skip ci] 2022-02-11 13:55:39 -08:00
Andrew Kane
4f7a5c92da Use maxlen for check [skip ci] 2022-02-11 13:54:03 -08:00
Andrew Kane
9399575e02 Include samples size and centers size in check 2022-02-11 12:43:01 -08:00
Andrew Kane
e7539fe548 Added new centers size 2022-02-11 11:58:58 -08:00
Andrew Kane
61860ee6d9 Updated changelog [skip ci] 2022-02-11 11:49:25 -08:00
Andrew Kane
eb99730d87 Removed unnecessary cast [skip ci] 2022-02-11 03:43:31 -08:00
Andrew Kane
e5da5cc532 Add one to error message to ceil [skip ci] 2022-02-11 03:41:54 -08:00
Andrew Kane
f59b1184fc Calculate all allocation sizes 2022-02-11 03:33:43 -08:00
Andrew Kane
cab103a2e7 Fixed index creation exceeding maintenance_work_mem and fixed error when lists > 1600 2022-02-11 03:10:19 -08:00
Andrew Kane
0094b70a36 Updated changelog [skip ci] 2022-02-11 03:02:59 -08:00
Andrew Kane
46ed56d80f Use float instead of double for k-means to save memory 2022-02-11 01:56:28 -08:00
Andrew Kane
d390cc9fa6 Increased recall queries in test 2022-02-11 00:45:39 -08:00
Andrew Kane
02ae98791c Improved recall test 2022-02-11 00:43:01 -08:00
Andrew Kane
b64c7d6228 Improved test [skip ci] 2022-02-11 00:15:37 -08:00
Andrew Kane
76f117c81c Added test for array of vectors [skip ci] 2022-02-07 15:25:37 -08:00
Andrew Kane
5414b2bc9e Added link to pgvector-ruby [skip ci] 2022-02-07 00:52:46 -08:00
Andrew Kane
6ba583b587 Improved test style [skip ci] 2022-02-06 20:24:50 -08:00
Andrew Kane
da88217fd8 Fixed test style 2022-02-06 20:19:20 -08:00
Andrew Kane
d5da22f24b Fixed tests 2022-02-06 20:14:21 -08:00
Andrew Kane
062e220936 Test probes 2022-02-06 20:08:53 -08:00
Andrew Kane
3d2b867a58 Added recall test 2022-02-06 19:59:46 -08:00
Andrew Kane
5aeabc3648 Added comments on index handler [skip ci] 2022-02-06 15:26:18 -08:00
Andrew Kane
687ec6d8d7 Added amadjustmembers 2022-02-06 15:21:59 -08:00
Andrew Kane
51dc18d5b7 Added compiler error for unsupported versions of Postgres 2022-02-06 15:15:46 -08:00
Andrew Kane
cb108ebfd2 Removed reindexing section [skip ci] 2022-02-06 12:45:49 -08:00
21 changed files with 492 additions and 121 deletions

View File

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

View File

@@ -1,3 +1,13 @@
## 0.2.6 (2022-05-22)
- 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

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.2.4",
"version": "0.2.6",
"maintainer": [
"Andrew Kane <andrew@ankane.org>"
],
@@ -20,7 +20,7 @@
"vector": {
"file": "sql/vector.sql",
"docfile": "README.md",
"version": "0.2.4",
"version": "0.2.6",
"abstract": "Open-source vector similarity search for Postgres"
}
},

View File

@@ -1,5 +1,5 @@
EXTENSION = vector
EXTVERSION = 0.2.4
EXTVERSION = 0.2.6
MODULE_big = vector
DATA = $(wildcard sql/*--*.sql)
@@ -21,7 +21,7 @@ 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

View File

@@ -17,7 +17,7 @@ Supports L2 distance, inner product, and cosine distance
Compile and install the extension (supports Postgres 9.6+)
```sh
git clone --branch v0.2.4 https://github.com/pgvector/pgvector.git
git clone --branch v0.2.6 https://github.com/pgvector/pgvector.git
cd pgvector
make
make install # may need sudo
@@ -77,19 +77,7 @@ Cosine distance
CREATE INDEX ON table USING ivfflat (column vector_cosine_ops);
```
Indexes should be created after the table has data for optimal clustering. If the distribution of data changes significantly, you can reindex without downtime:
```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;
```
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
@@ -194,6 +182,7 @@ Libraries that use pgvector:
- [pgvector-python](https://github.com/pgvector/pgvector-python) (Python)
- [Neighbor](https://github.com/ankane/neighbor) (Ruby)
- [pgvector-ruby](https://github.com/pgvector/pgvector-ruby) (Ruby)
- [pgvector-node](https://github.com/pgvector/pgvector-node) (Node.js)
- [pgvector-go](https://github.com/pgvector/pgvector-go) (Go)
- [pgvector-rust](https://github.com/pgvector/pgvector-rust) (Rust)
@@ -231,7 +220,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.2.4 https://github.com/pgvector/pgvector.git
git clone --branch v0.2.6 https://github.com/pgvector/pgvector.git
cd pgvector
docker build -t pgvector .
```
@@ -315,6 +304,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)

View 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

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

View File

@@ -36,16 +36,11 @@
#define CALLBACK_ITEM_POINTER HeapTuple hup
#endif
/*
* Update build phase progress
*/
static inline void
UpdateProgress(int index, int64 val)
{
#if PG_VERSION_NUM >= 120000
pgstat_progress_update_param(index, val);
#define UpdateProgress(index, val) pgstat_progress_update_param(index, val)
#else
#define UpdateProgress(index, val) ((void)val)
#endif
}
/*
* Callback for sampling
@@ -117,13 +112,13 @@ SampleRows(IvfflatBuildState * buildstate)
#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);
false, true, false, 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);
false, true, targblock, 1, SampleCallback, (void *) buildstate, NULL);
#else
IndexBuildHeapRangeScan(buildstate->heap, buildstate->index, buildstate->indexInfo,
true, true, targblock, 1, SampleCallback, (void *) buildstate);
false, true, targblock, 1, SampleCallback, (void *) buildstate);
#endif
}
}
@@ -170,16 +165,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);
/*
@@ -201,8 +200,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))
@@ -211,13 +208,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;
@@ -326,17 +321,16 @@ InitBuildState(IvfflatBuildState * buildstate, Relation heap, Relation index, In
/* 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
@@ -350,6 +344,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
}
/*
@@ -361,6 +361,11 @@ FreeBuildState(IvfflatBuildState * buildstate)
pfree(buildstate->centers);
pfree(buildstate->listInfo);
pfree(buildstate->normvec);
#ifdef IVFFLAT_KMEANS_DEBUG
pfree(buildstate->listSums);
pfree(buildstate->listCounts);
#endif
}
/*
@@ -377,14 +382,19 @@ ComputeCenters(IvfflatBuildState * buildstate)
if (numSamples < 10000)
numSamples = 10000;
/* Sample samples */
/* 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);
IvfflatKmeans(buildstate->index, buildstate->samples, buildstate->centers);
IvfflatBench("k-means", IvfflatKmeans(buildstate->index, buildstate->samples, buildstate->centers));
/* Free samples before we allocate more memory */
pfree(buildstate->samples);
@@ -463,6 +473,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
*/
@@ -497,8 +552,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);
}
@@ -517,7 +578,7 @@ BuildIndex(Relation heap, Relation index, IndexInfo *indexInfo,
/* 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);
}

View File

@@ -159,6 +159,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)
@@ -193,6 +198,7 @@ ivfflathandler(PG_FUNCTION_ARGS)
#endif
amroutine->amkeytype = InvalidOid;
/* Interface functions */
amroutine->ambuild = ivfflatbuild;
amroutine->ambuildempty = ivfflatbuildempty;
amroutine->aminsert = ivfflatinsert;
@@ -206,6 +212,9 @@ ivfflathandler(PG_FUNCTION_ARGS)
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;
@@ -213,6 +222,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;

View File

@@ -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
@@ -39,9 +47,19 @@
#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 */
@@ -97,6 +115,12 @@ typedef struct IvfflatBuildState
ListInfo *listInfo;
Vector *normvec;
#ifdef IVFFLAT_KMEANS_DEBUG
double inertia;
double *listSums;
int *listCounts;
#endif
/* Sampling */
BlockSamplerData bs;
ReservoirStateData rstate;
@@ -138,6 +162,7 @@ typedef IvfflatListData * IvfflatList;
typedef struct IvfflatScanList
{
pairingheap_node ph_node;
BlockNumber startPage;
double distance;
} IvfflatScanList;
@@ -159,6 +184,8 @@ typedef struct IvfflatScanOpaqueData
FmgrInfo *normprocinfo;
Oid collation;
/* Lists */
pairingheap *listQueue;
IvfflatScanList lists[FLEXIBLE_ARRAY_MEMBER]; /* must come last */
} IvfflatScanOpaqueData;

View File

@@ -11,7 +11,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, float *lowerBound)
{
FmgrInfo *procinfo;
Oid collation;
@@ -21,7 +21,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;
@@ -121,15 +121,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++;
}
}
}
@@ -177,11 +180,11 @@ ElkanKmeans(Relation index, VectorArray samples, VectorArray centers)
VectorArray newCenters;
int *centerCounts;
int *closestCenters;
double *lowerBound;
double *upperBound;
double *s;
double *halfcdist;
double *newcdist;
float *lowerBound;
float *upperBound;
float *s;
float *halfcdist;
float *newcdist;
int changes;
double minDistance;
int closestCenter;
@@ -191,19 +194,43 @@ ElkanKmeans(Relation index, VectorArray samples, VectorArray centers)
double dxcx;
double dxc;
/* Calculate allocation sizes */
Size samplesSize = VECTOR_ARRAY_SIZE(samples->maxlen, samples->dim);
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)));
/* 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);
/* 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++)

View File

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

View File

@@ -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

View File

@@ -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)[];

View File

@@ -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");
}

View File

@@ -13,7 +13,7 @@ $node->start;
$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;"
"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);");
@@ -24,7 +24,7 @@ my $size = $node->safe_psql("postgres", "SELECT pg_total_relation_size('tst_v_id
$node->safe_psql("postgres", "DELETE FROM tst;");
$node->safe_psql("postgres", "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;"
"INSERT INTO tst SELECT i % 10, ARRAY[i % 1000, i % 333, i % 55] FROM generate_series(1, 100000) i;"
);
# Check size

88
test/t/003_recall.pl Normal file
View 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
View 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);

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

View File

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