mirror of
https://github.com/pgvector/pgvector.git
synced 2026-07-22 12:07:34 +08:00
Compare commits
12 Commits
ivfflat-co
...
ivfflat-st
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4ac05f044 | ||
|
|
25f97fd91d | ||
|
|
cd3f9a38ae | ||
|
|
ff6267917e | ||
|
|
c950c5ffaa | ||
|
|
88f56dc234 | ||
|
|
e2ba6cf38f | ||
|
|
689f9c4659 | ||
|
|
88889f5a4c | ||
|
|
79851729f1 | ||
|
|
3fd6a29c49 | ||
|
|
2c58804756 |
@@ -1,7 +1,6 @@
|
|||||||
## 0.8.0 (unreleased)
|
## 0.8.0 (unreleased)
|
||||||
|
|
||||||
- Added casts for arrays to `sparsevec`
|
- Added casts for arrays to `sparsevec`
|
||||||
- Improved cost estimation
|
|
||||||
- Reduced memory usage for HNSW index scans
|
- Reduced memory usage for HNSW index scans
|
||||||
- Dropped support for Postgres 12
|
- Dropped support for Postgres 12
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
ARG PG_MAJOR=17
|
ARG PG_MAJOR=16
|
||||||
FROM postgres:$PG_MAJOR
|
FROM postgres:$PG_MAJOR
|
||||||
ARG PG_MAJOR
|
ARG PG_MAJOR
|
||||||
|
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -66,7 +66,7 @@ dist:
|
|||||||
git archive --format zip --prefix=$(EXTENSION)-$(EXTVERSION)/ --output dist/$(EXTENSION)-$(EXTVERSION).zip master
|
git archive --format zip --prefix=$(EXTENSION)-$(EXTVERSION)/ --output dist/$(EXTENSION)-$(EXTVERSION).zip master
|
||||||
|
|
||||||
# for Docker
|
# for Docker
|
||||||
PG_MAJOR ?= 17
|
PG_MAJOR ?= 16
|
||||||
|
|
||||||
.PHONY: docker
|
.PHONY: docker
|
||||||
|
|
||||||
|
|||||||
38
README.md
38
README.md
@@ -52,8 +52,6 @@ nmake /F Makefile.win
|
|||||||
nmake /F Makefile.win install
|
nmake /F Makefile.win install
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: Postgres 17 is not supported yet due to an upstream issue
|
|
||||||
|
|
||||||
See the [installation notes](#installation-notes---windows) if you run into issues
|
See the [installation notes](#installation-notes---windows) if you run into issues
|
||||||
|
|
||||||
You can also install it with [Docker](#docker) or [conda-forge](#conda-forge).
|
You can also install it with [Docker](#docker) or [conda-forge](#conda-forge).
|
||||||
@@ -102,8 +100,6 @@ Or add a vector column to an existing table
|
|||||||
ALTER TABLE items ADD COLUMN embedding vector(3);
|
ALTER TABLE items ADD COLUMN embedding vector(3);
|
||||||
```
|
```
|
||||||
|
|
||||||
Also supports [half-precision](#half-precision-vectors), [binary](#binary-vectors), and [sparse](#sparse-vectors) vectors
|
|
||||||
|
|
||||||
Insert vectors
|
Insert vectors
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
@@ -149,8 +145,6 @@ Supported distance functions are:
|
|||||||
- `<#>` - (negative) inner product
|
- `<#>` - (negative) inner product
|
||||||
- `<=>` - cosine distance
|
- `<=>` - cosine distance
|
||||||
- `<+>` - L1 distance (added in 0.7.0)
|
- `<+>` - L1 distance (added in 0.7.0)
|
||||||
- `<~>` - Hamming distance (binary vectors, added in 0.7.0)
|
|
||||||
- `<%>` - Jaccard distance (binary vectors, added in 0.7.0)
|
|
||||||
|
|
||||||
Get the nearest neighbors to a row
|
Get the nearest neighbors to a row
|
||||||
|
|
||||||
@@ -989,7 +983,7 @@ l2_normalize(sparsevec) → sparsevec | Normalize with Euclidean norm | 0.7.0
|
|||||||
If your machine has multiple Postgres installations, specify the path to [pg_config](https://www.postgresql.org/docs/current/app-pgconfig.html) with:
|
If your machine has multiple Postgres installations, specify the path to [pg_config](https://www.postgresql.org/docs/current/app-pgconfig.html) with:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
export PG_CONFIG=/Library/PostgreSQL/17/bin/pg_config
|
export PG_CONFIG=/Library/PostgreSQL/16/bin/pg_config
|
||||||
```
|
```
|
||||||
|
|
||||||
Then re-run the installation instructions (run `make clean` before `make` if needed). If `sudo` is needed for `make install`, use:
|
Then re-run the installation instructions (run `make clean` before `make` if needed). If `sudo` is needed for `make install`, use:
|
||||||
@@ -1000,11 +994,11 @@ sudo --preserve-env=PG_CONFIG make install
|
|||||||
|
|
||||||
A few common paths on Mac are:
|
A few common paths on Mac are:
|
||||||
|
|
||||||
- EDB installer - `/Library/PostgreSQL/17/bin/pg_config`
|
- EDB installer - `/Library/PostgreSQL/16/bin/pg_config`
|
||||||
- Homebrew (arm64) - `/opt/homebrew/opt/postgresql@17/bin/pg_config`
|
- Homebrew (arm64) - `/opt/homebrew/opt/postgresql@16/bin/pg_config`
|
||||||
- Homebrew (x86-64) - `/usr/local/opt/postgresql@17/bin/pg_config`
|
- Homebrew (x86-64) - `/usr/local/opt/postgresql@16/bin/pg_config`
|
||||||
|
|
||||||
Note: Replace `17` with your Postgres server version
|
Note: Replace `16` with your Postgres server version
|
||||||
|
|
||||||
### Missing Header
|
### Missing Header
|
||||||
|
|
||||||
@@ -1013,10 +1007,10 @@ If compilation fails with `fatal error: postgres.h: No such file or directory`,
|
|||||||
For Ubuntu and Debian, use:
|
For Ubuntu and Debian, use:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo apt install postgresql-server-dev-17
|
sudo apt install postgresql-server-dev-16
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: Replace `17` with your Postgres server version
|
Note: Replace `16` with your Postgres server version
|
||||||
|
|
||||||
### Missing SDK
|
### Missing SDK
|
||||||
|
|
||||||
@@ -1049,17 +1043,17 @@ If installation fails with `Access is denied`, re-run the installation instructi
|
|||||||
Get the [Docker image](https://hub.docker.com/r/pgvector/pgvector) with:
|
Get the [Docker image](https://hub.docker.com/r/pgvector/pgvector) with:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker pull pgvector/pgvector:pg17
|
docker pull pgvector/pgvector:pg16
|
||||||
```
|
```
|
||||||
|
|
||||||
This adds pgvector to the [Postgres image](https://hub.docker.com/_/postgres) (replace `17` with your Postgres server version, and run it the same way).
|
This adds pgvector to the [Postgres image](https://hub.docker.com/_/postgres) (replace `16` with your Postgres server version, and run it the same way).
|
||||||
|
|
||||||
You can also build the image manually:
|
You can also build the image manually:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone --branch v0.7.4 https://github.com/pgvector/pgvector.git
|
git clone --branch v0.7.4 https://github.com/pgvector/pgvector.git
|
||||||
cd pgvector
|
cd pgvector
|
||||||
docker build --pull --build-arg PG_MAJOR=17 -t myuser/pgvector .
|
docker build --pull --build-arg PG_MAJOR=16 -t myuser/pgvector .
|
||||||
```
|
```
|
||||||
|
|
||||||
### Homebrew
|
### Homebrew
|
||||||
@@ -1070,7 +1064,7 @@ With Homebrew Postgres, you can use:
|
|||||||
brew install pgvector
|
brew install pgvector
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: This only adds it to the `postgresql@17` and `postgresql@14` formulas
|
Note: This only adds it to the `postgresql@14` formula
|
||||||
|
|
||||||
### PGXN
|
### PGXN
|
||||||
|
|
||||||
@@ -1085,22 +1079,22 @@ pgxn install vector
|
|||||||
Debian and Ubuntu packages are available from the [PostgreSQL APT Repository](https://wiki.postgresql.org/wiki/Apt). Follow the [setup instructions](https://wiki.postgresql.org/wiki/Apt#Quickstart) and run:
|
Debian and Ubuntu packages are available from the [PostgreSQL APT Repository](https://wiki.postgresql.org/wiki/Apt). Follow the [setup instructions](https://wiki.postgresql.org/wiki/Apt#Quickstart) and run:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo apt install postgresql-17-pgvector
|
sudo apt install postgresql-16-pgvector
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: Replace `17` with your Postgres server version
|
Note: Replace `16` with your Postgres server version
|
||||||
|
|
||||||
### Yum
|
### Yum
|
||||||
|
|
||||||
RPM packages are available from the [PostgreSQL Yum Repository](https://yum.postgresql.org/). Follow the [setup instructions](https://www.postgresql.org/download/linux/redhat/) for your distribution and run:
|
RPM packages are available from the [PostgreSQL Yum Repository](https://yum.postgresql.org/). Follow the [setup instructions](https://www.postgresql.org/download/linux/redhat/) for your distribution and run:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo yum install pgvector_17
|
sudo yum install pgvector_16
|
||||||
# or
|
# or
|
||||||
sudo dnf install pgvector_17
|
sudo dnf install pgvector_16
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: Replace `17` with your Postgres server version
|
Note: Replace `16` with your Postgres server version
|
||||||
|
|
||||||
### pkg
|
### pkg
|
||||||
|
|
||||||
|
|||||||
@@ -159,6 +159,24 @@ CheckStateArray(ArrayType *statearray, const char *caller)
|
|||||||
return (float8 *) ARR_DATA_PTR(statearray);
|
return (float8 *) ARR_DATA_PTR(statearray);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if PG_VERSION_NUM < 120003
|
||||||
|
static pg_noinline void
|
||||||
|
float_overflow_error(void)
|
||||||
|
{
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||||
|
errmsg("value out of range: overflow")));
|
||||||
|
}
|
||||||
|
|
||||||
|
static pg_noinline void
|
||||||
|
float_underflow_error(void)
|
||||||
|
{
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||||
|
errmsg("value out of range: underflow")));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Convert textual representation to internal representation
|
* Convert textual representation to internal representation
|
||||||
*/
|
*/
|
||||||
|
|||||||
55
src/hnsw.c
55
src/hnsw.c
@@ -12,7 +12,6 @@
|
|||||||
#include "utils/float.h"
|
#include "utils/float.h"
|
||||||
#include "utils/guc.h"
|
#include "utils/guc.h"
|
||||||
#include "utils/selfuncs.h"
|
#include "utils/selfuncs.h"
|
||||||
#include "utils/spccache.h"
|
|
||||||
|
|
||||||
#if PG_VERSION_NUM < 150000
|
#if PG_VERSION_NUM < 150000
|
||||||
#define MarkGUCPrefixReserved(x) EmitWarningsOnPlaceholders(x)
|
#define MarkGUCPrefixReserved(x) EmitWarningsOnPlaceholders(x)
|
||||||
@@ -101,10 +100,6 @@ hnswcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
|
|||||||
GenericCosts costs;
|
GenericCosts costs;
|
||||||
int m;
|
int m;
|
||||||
int entryLevel;
|
int entryLevel;
|
||||||
int layer0TuplesMax;
|
|
||||||
double layer0Selectivity;
|
|
||||||
double scalingFactor = 0.55;
|
|
||||||
double spc_seq_page_cost;
|
|
||||||
Relation index;
|
Relation index;
|
||||||
|
|
||||||
/* Never use index without order */
|
/* Never use index without order */
|
||||||
@@ -124,55 +119,15 @@ hnswcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
|
|||||||
HnswGetMetaPageInfo(index, &m, NULL);
|
HnswGetMetaPageInfo(index, &m, NULL);
|
||||||
index_close(index, NoLock);
|
index_close(index, NoLock);
|
||||||
|
|
||||||
/*
|
/* Approximate entry level */
|
||||||
* HNSW cost estimation follows a formula that accounts for the total
|
entryLevel = (int) -log(1.0 / path->indexinfo->tuples) * HnswGetMl(m);
|
||||||
* number of tuples indexed combined with the parameters that most
|
|
||||||
* influence the duration of the index scan, namely: m - the number of
|
|
||||||
* tuples that are scanned in each step of the HNSW graph traversal
|
|
||||||
* ef_search - which influences the total number of steps taken at layer 0
|
|
||||||
*
|
|
||||||
* The source of the vector data can impact how many steps it takes to
|
|
||||||
* converge on the set of vectors to return to the executor. Currently, we
|
|
||||||
* use a hardcoded scaling factor (HNSWScanScalingFactor) to help
|
|
||||||
* influence that, but this could later become a configurable parameter
|
|
||||||
* based on the cost estimations.
|
|
||||||
*
|
|
||||||
* The tuple estimator formula is below:
|
|
||||||
*
|
|
||||||
* numIndexTuples = entryLevel * m + layer0TuplesMax * layer0Selectivity
|
|
||||||
*
|
|
||||||
* "entryLevel * m" represents the floor of tuples we need to scan to get
|
|
||||||
* to layer 0 (L0).
|
|
||||||
*
|
|
||||||
* "layer0TuplesMax" is the estimated total number of tuples we'd scan at
|
|
||||||
* L0 if we weren't discarding already visited tuples as part of the scan.
|
|
||||||
*
|
|
||||||
* "layer0Selectivity" estimates the percentage of tuples that are scanned
|
|
||||||
* at L0, accounting for previously visited tuples, multiplied by the
|
|
||||||
* "scalingFactor" (currently hardcoded).
|
|
||||||
*/
|
|
||||||
entryLevel = (int) (log(path->indexinfo->tuples + 1) * HnswGetMl(m));
|
|
||||||
layer0TuplesMax = HnswGetLayerM(m, 0) * hnsw_ef_search;
|
|
||||||
layer0Selectivity = (scalingFactor * log(path->indexinfo->tuples + 1)) /
|
|
||||||
(log(m) * (1 + log(hnsw_ef_search)));
|
|
||||||
|
|
||||||
costs.numIndexTuples = (entryLevel * m) +
|
/* TODO Improve estimate of visited tuples (currently underestimates) */
|
||||||
(layer0TuplesMax * layer0Selectivity);
|
/* Account for number of tuples (or entry level), m, and ef_search */
|
||||||
|
costs.numIndexTuples = (entryLevel + 2) * m;
|
||||||
|
|
||||||
genericcostestimate(root, path, loop_count, &costs);
|
genericcostestimate(root, path, loop_count, &costs);
|
||||||
|
|
||||||
get_tablespace_page_costs(path->indexinfo->reltablespace, NULL, &spc_seq_page_cost);
|
|
||||||
|
|
||||||
/* Adjust cost if needed since TOAST not included in seq scan cost */
|
|
||||||
if (costs.numIndexPages > path->indexinfo->rel->pages)
|
|
||||||
{
|
|
||||||
/* Change all page cost from random to sequential */
|
|
||||||
costs.indexTotalCost -= costs.numIndexPages * (costs.spc_random_page_cost - spc_seq_page_cost);
|
|
||||||
|
|
||||||
/* Remove cost of extra pages */
|
|
||||||
costs.indexTotalCost -= (costs.numIndexPages - path->indexinfo->rel->pages) * spc_seq_page_cost;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Use total cost since most work happens before first tuple is returned */
|
/* Use total cost since most work happens before first tuple is returned */
|
||||||
*indexStartupCost = costs.indexTotalCost;
|
*indexStartupCost = costs.indexTotalCost;
|
||||||
*indexTotalCost = costs.indexTotalCost;
|
*indexTotalCost = costs.indexTotalCost;
|
||||||
|
|||||||
@@ -759,16 +759,9 @@ HnswLoadUnvisitedFromDisk(HnswElement element, HnswUnvisited * unvisited, int *u
|
|||||||
page = BufferGetPage(buf);
|
page = BufferGetPage(buf);
|
||||||
|
|
||||||
ntup = (HnswNeighborTuple) PageGetItem(page, PageGetItemId(page, element->neighborOffno));
|
ntup = (HnswNeighborTuple) PageGetItem(page, PageGetItemId(page, element->neighborOffno));
|
||||||
|
start = (element->level - lc) * m;
|
||||||
/* Ensure expected neighbors */
|
|
||||||
if (ntup->count != (element->level + 2) * m)
|
|
||||||
{
|
|
||||||
UnlockReleaseBuffer(buf);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Copy to minimize lock time */
|
/* Copy to minimize lock time */
|
||||||
start = (element->level - lc) * m;
|
|
||||||
memcpy(&indextids, ntup->indextids + start, lm * sizeof(ItemPointerData));
|
memcpy(&indextids, ntup->indextids + start, lm * sizeof(ItemPointerData));
|
||||||
|
|
||||||
UnlockReleaseBuffer(buf);
|
UnlockReleaseBuffer(buf);
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
int ivfflat_probes;
|
int ivfflat_probes;
|
||||||
|
int ivfflat_max_probes;
|
||||||
|
bool ivfflat_streaming;
|
||||||
static relopt_kind ivfflat_relopt_kind;
|
static relopt_kind ivfflat_relopt_kind;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -33,6 +35,14 @@ IvfflatInit(void)
|
|||||||
"Valid range is 1..lists.", &ivfflat_probes,
|
"Valid range is 1..lists.", &ivfflat_probes,
|
||||||
IVFFLAT_DEFAULT_PROBES, IVFFLAT_MIN_LISTS, IVFFLAT_MAX_LISTS, PGC_USERSET, 0, NULL, NULL, NULL);
|
IVFFLAT_DEFAULT_PROBES, IVFFLAT_MIN_LISTS, IVFFLAT_MAX_LISTS, PGC_USERSET, 0, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
DefineCustomIntVariable("ivfflat.max_probes", "Sets the max number of probes for iterative scans",
|
||||||
|
NULL, &ivfflat_max_probes,
|
||||||
|
-1, IVFFLAT_MIN_LISTS, IVFFLAT_MAX_LISTS, PGC_USERSET, 0, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
DefineCustomBoolVariable("ivfflat.streaming", "Use streaming mode",
|
||||||
|
NULL, &ivfflat_streaming,
|
||||||
|
IVFFLAT_DEFAULT_STREAMING, PGC_USERSET, 0, NULL, NULL, NULL);
|
||||||
|
|
||||||
MarkGUCPrefixReserved("ivfflat");
|
MarkGUCPrefixReserved("ivfflat");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +67,35 @@ ivfflatbuildphasename(int64 phasenum)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Estimate the number of probes for iterative scans
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
EstimateProbes(PlannerInfo *root, IndexPath *path, int lists)
|
||||||
|
{
|
||||||
|
double selectivity = 1;
|
||||||
|
ListCell *lc;
|
||||||
|
double tuplesPerList;
|
||||||
|
|
||||||
|
/* Cannot estimate without limit */
|
||||||
|
/* limit_tuples includes offset */
|
||||||
|
if (root->limit_tuples < 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
/* Get the selectivity of non-index conditions */
|
||||||
|
foreach(lc, path->indexinfo->indrestrictinfo)
|
||||||
|
{
|
||||||
|
RestrictInfo *rinfo = lfirst(lc);
|
||||||
|
|
||||||
|
/* Skip DEFAULT_INEQ_SEL since it may be a distance filter */
|
||||||
|
if (rinfo->norm_selec >= 0 && rinfo->norm_selec <= 1 && rinfo->norm_selec != (Selectivity) DEFAULT_INEQ_SEL)
|
||||||
|
selectivity *= rinfo->norm_selec;
|
||||||
|
}
|
||||||
|
|
||||||
|
tuplesPerList = path->indexinfo->tuples / (double) lists;
|
||||||
|
return root->limit_tuples / (tuplesPerList * selectivity);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Estimate the cost of an index scan
|
* Estimate the cost of an index scan
|
||||||
*/
|
*/
|
||||||
@@ -68,6 +107,7 @@ ivfflatcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
|
|||||||
{
|
{
|
||||||
GenericCosts costs;
|
GenericCosts costs;
|
||||||
int lists;
|
int lists;
|
||||||
|
int probes;
|
||||||
double ratio;
|
double ratio;
|
||||||
double spc_seq_page_cost;
|
double spc_seq_page_cost;
|
||||||
Relation index;
|
Relation index;
|
||||||
@@ -85,20 +125,32 @@ ivfflatcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
|
|||||||
|
|
||||||
MemSet(&costs, 0, sizeof(costs));
|
MemSet(&costs, 0, sizeof(costs));
|
||||||
|
|
||||||
genericcostestimate(root, path, loop_count, &costs);
|
|
||||||
|
|
||||||
index = index_open(path->indexinfo->indexoid, NoLock);
|
index = index_open(path->indexinfo->indexoid, NoLock);
|
||||||
IvfflatGetMetaPageInfo(index, &lists, NULL);
|
IvfflatGetMetaPageInfo(index, &lists, NULL);
|
||||||
index_close(index, NoLock);
|
index_close(index, NoLock);
|
||||||
|
|
||||||
|
probes = ivfflat_probes;
|
||||||
|
if (ivfflat_streaming)
|
||||||
|
{
|
||||||
|
probes = Max(probes, EstimateProbes(root, path, lists));
|
||||||
|
|
||||||
|
if (ivfflat_max_probes != -1)
|
||||||
|
probes = Min(probes, ivfflat_max_probes);
|
||||||
|
}
|
||||||
|
|
||||||
/* Get the ratio of lists that we need to visit */
|
/* Get the ratio of lists that we need to visit */
|
||||||
ratio = ((double) ivfflat_probes) / lists;
|
ratio = ((double) probes) / lists;
|
||||||
if (ratio > 1.0)
|
if (ratio > 1.0)
|
||||||
ratio = 1.0;
|
ratio = 1.0;
|
||||||
|
|
||||||
/* Set startup cost since most work happens before first tuple is returned */
|
/*
|
||||||
costs.indexStartupCost = costs.indexTotalCost * ratio;
|
* This gives us the subset of tuples to visit. This value is passed into
|
||||||
costs.numIndexPages *= ratio;
|
* the generic cost estimator to determine the number of pages to visit
|
||||||
|
* during the index scan.
|
||||||
|
*/
|
||||||
|
costs.numIndexTuples = path->indexinfo->tuples * ratio;
|
||||||
|
|
||||||
|
genericcostestimate(root, path, loop_count, &costs);
|
||||||
|
|
||||||
get_tablespace_page_costs(path->indexinfo->reltablespace, NULL, &spc_seq_page_cost);
|
get_tablespace_page_costs(path->indexinfo->reltablespace, NULL, &spc_seq_page_cost);
|
||||||
|
|
||||||
@@ -106,25 +158,30 @@ ivfflatcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
|
|||||||
if (costs.numIndexPages > path->indexinfo->rel->pages && ratio < 0.5)
|
if (costs.numIndexPages > path->indexinfo->rel->pages && ratio < 0.5)
|
||||||
{
|
{
|
||||||
/* Change all page cost from random to sequential */
|
/* Change all page cost from random to sequential */
|
||||||
costs.indexStartupCost -= costs.numIndexPages * (costs.spc_random_page_cost - spc_seq_page_cost);
|
costs.indexTotalCost -= costs.numIndexPages * (costs.spc_random_page_cost - spc_seq_page_cost);
|
||||||
|
|
||||||
/* Remove cost of extra pages */
|
/* Remove cost of extra pages */
|
||||||
costs.indexStartupCost -= (costs.numIndexPages - path->indexinfo->rel->pages) * spc_seq_page_cost;
|
costs.indexTotalCost -= (costs.numIndexPages - path->indexinfo->rel->pages) * spc_seq_page_cost;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
/* Change some page cost from random to sequential */
|
/* Change some page cost from random to sequential */
|
||||||
costs.indexStartupCost -= 0.5 * costs.numIndexPages * (costs.spc_random_page_cost - spc_seq_page_cost);
|
costs.indexTotalCost -= 0.5 * costs.numIndexPages * (costs.spc_random_page_cost - spc_seq_page_cost);
|
||||||
}
|
}
|
||||||
|
|
||||||
*indexStartupCost = costs.indexStartupCost;
|
/*
|
||||||
|
* If the list selectivity is lower than what is returned from the generic
|
||||||
|
* cost estimator, use that.
|
||||||
|
*/
|
||||||
|
if (ratio < costs.indexSelectivity)
|
||||||
|
costs.indexSelectivity = ratio;
|
||||||
|
|
||||||
|
/* Use total cost since most work happens before first tuple is returned */
|
||||||
|
*indexStartupCost = costs.indexTotalCost;
|
||||||
*indexTotalCost = costs.indexTotalCost;
|
*indexTotalCost = costs.indexTotalCost;
|
||||||
*indexSelectivity = costs.indexSelectivity;
|
*indexSelectivity = costs.indexSelectivity;
|
||||||
*indexCorrelation = costs.indexCorrelation;
|
*indexCorrelation = costs.indexCorrelation;
|
||||||
*indexPages = costs.numIndexPages;
|
*indexPages = costs.numIndexPages;
|
||||||
|
|
||||||
Assert(*indexStartupCost > 0);
|
|
||||||
Assert(*indexTotalCost > 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
#define IVFFLAT_MIN_LISTS 1
|
#define IVFFLAT_MIN_LISTS 1
|
||||||
#define IVFFLAT_MAX_LISTS 32768
|
#define IVFFLAT_MAX_LISTS 32768
|
||||||
#define IVFFLAT_DEFAULT_PROBES 1
|
#define IVFFLAT_DEFAULT_PROBES 1
|
||||||
|
#define IVFFLAT_DEFAULT_STREAMING false
|
||||||
|
|
||||||
/* Build phases */
|
/* Build phases */
|
||||||
/* PROGRESS_CREATEIDX_SUBPHASE_INITIALIZE is 1 */
|
/* PROGRESS_CREATEIDX_SUBPHASE_INITIALIZE is 1 */
|
||||||
@@ -80,6 +81,8 @@
|
|||||||
|
|
||||||
/* Variables */
|
/* Variables */
|
||||||
extern int ivfflat_probes;
|
extern int ivfflat_probes;
|
||||||
|
extern int ivfflat_max_probes;
|
||||||
|
extern bool ivfflat_streaming;
|
||||||
|
|
||||||
typedef struct VectorArrayData
|
typedef struct VectorArrayData
|
||||||
{
|
{
|
||||||
@@ -247,8 +250,10 @@ typedef struct IvfflatScanOpaqueData
|
|||||||
{
|
{
|
||||||
const IvfflatTypeInfo *typeInfo;
|
const IvfflatTypeInfo *typeInfo;
|
||||||
int probes;
|
int probes;
|
||||||
|
int maxProbes;
|
||||||
int dimensions;
|
int dimensions;
|
||||||
bool first;
|
bool first;
|
||||||
|
Datum value;
|
||||||
|
|
||||||
/* Sorting */
|
/* Sorting */
|
||||||
Tuplesortstate *sortstate;
|
Tuplesortstate *sortstate;
|
||||||
@@ -265,6 +270,8 @@ typedef struct IvfflatScanOpaqueData
|
|||||||
|
|
||||||
/* Lists */
|
/* Lists */
|
||||||
pairingheap *listQueue;
|
pairingheap *listQueue;
|
||||||
|
BlockNumber *startPages;
|
||||||
|
int currentIndex;
|
||||||
IvfflatScanList lists[FLEXIBLE_ARRAY_MEMBER]; /* must come last */
|
IvfflatScanList lists[FLEXIBLE_ARRAY_MEMBER]; /* must come last */
|
||||||
} IvfflatScanOpaqueData;
|
} IvfflatScanOpaqueData;
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ GetScanLists(IndexScanDesc scan, Datum value)
|
|||||||
/* Use procinfo from the index instead of scan key for performance */
|
/* Use procinfo from the index instead of scan key for performance */
|
||||||
distance = DatumGetFloat8(so->distfunc(so->procinfo, so->collation, PointerGetDatum(&list->center), value));
|
distance = DatumGetFloat8(so->distfunc(so->procinfo, so->collation, PointerGetDatum(&list->center), value));
|
||||||
|
|
||||||
if (listCount < so->probes)
|
if (listCount < so->maxProbes)
|
||||||
{
|
{
|
||||||
IvfflatScanList *scanlist;
|
IvfflatScanList *scanlist;
|
||||||
|
|
||||||
@@ -102,6 +102,11 @@ GetScanLists(IndexScanDesc scan, Datum value)
|
|||||||
|
|
||||||
UnlockReleaseBuffer(cbuf);
|
UnlockReleaseBuffer(cbuf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (int i = listCount - 1; i >= 0; i--)
|
||||||
|
so->startPages[i] = GetScanList(pairingheap_remove_first(so->listQueue))->startPage;
|
||||||
|
|
||||||
|
Assert(pairingheap_is_empty(so->listQueue));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -114,11 +119,14 @@ GetScanItems(IndexScanDesc scan, Datum value)
|
|||||||
TupleDesc tupdesc = RelationGetDescr(scan->indexRelation);
|
TupleDesc tupdesc = RelationGetDescr(scan->indexRelation);
|
||||||
double tuples = 0;
|
double tuples = 0;
|
||||||
TupleTableSlot *slot = so->vslot;
|
TupleTableSlot *slot = so->vslot;
|
||||||
|
int batchProbes = 0;
|
||||||
|
|
||||||
|
tuplesort_reset(so->sortstate);
|
||||||
|
|
||||||
/* Search closest probes lists */
|
/* Search closest probes lists */
|
||||||
while (!pairingheap_is_empty(so->listQueue))
|
while (so->currentIndex < so->maxProbes && (++batchProbes) <= so->probes)
|
||||||
{
|
{
|
||||||
BlockNumber searchPage = GetScanList(pairingheap_remove_first(so->listQueue))->startPage;
|
BlockNumber searchPage = so->startPages[so->currentIndex++];
|
||||||
|
|
||||||
/* Search all entry pages for list */
|
/* Search all entry pages for list */
|
||||||
while (BlockNumberIsValid(searchPage))
|
while (BlockNumberIsValid(searchPage))
|
||||||
@@ -166,13 +174,17 @@ GetScanItems(IndexScanDesc scan, Datum value)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tuples < 100)
|
if (tuples < 100 && !ivfflat_streaming)
|
||||||
ereport(DEBUG1,
|
ereport(DEBUG1,
|
||||||
(errmsg("index scan found few tuples"),
|
(errmsg("index scan found few tuples"),
|
||||||
errdetail("Index may have been created with little data."),
|
errdetail("Index may have been created with little data."),
|
||||||
errhint("Recreate the index and possibly decrease lists.")));
|
errhint("Recreate the index and possibly decrease lists.")));
|
||||||
|
|
||||||
tuplesort_performsort(so->sortstate);
|
tuplesort_performsort(so->sortstate);
|
||||||
|
|
||||||
|
#if defined(IVFFLAT_MEMORY)
|
||||||
|
elog(INFO, "memory: %zu MB", MemoryContextMemAllocated(CurrentMemoryContext, true) / (1024 * 1024));
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -240,6 +252,7 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys)
|
|||||||
int lists;
|
int lists;
|
||||||
int dimensions;
|
int dimensions;
|
||||||
int probes = ivfflat_probes;
|
int probes = ivfflat_probes;
|
||||||
|
int maxProbes;
|
||||||
|
|
||||||
scan = RelationGetIndexScan(index, nkeys, norderbys);
|
scan = RelationGetIndexScan(index, nkeys, norderbys);
|
||||||
|
|
||||||
@@ -249,10 +262,21 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys)
|
|||||||
if (probes > lists)
|
if (probes > lists)
|
||||||
probes = lists;
|
probes = lists;
|
||||||
|
|
||||||
so = (IvfflatScanOpaque) palloc(offsetof(IvfflatScanOpaqueData, lists) + probes * sizeof(IvfflatScanList));
|
if (ivfflat_streaming)
|
||||||
|
{
|
||||||
|
if (ivfflat_max_probes == -1)
|
||||||
|
maxProbes = lists;
|
||||||
|
else
|
||||||
|
maxProbes = Min(ivfflat_max_probes, lists);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
maxProbes = probes;
|
||||||
|
|
||||||
|
so = (IvfflatScanOpaque) palloc(offsetof(IvfflatScanOpaqueData, lists) + maxProbes * sizeof(IvfflatScanList));
|
||||||
so->typeInfo = IvfflatGetTypeInfo(index);
|
so->typeInfo = IvfflatGetTypeInfo(index);
|
||||||
so->first = true;
|
so->first = true;
|
||||||
so->probes = probes;
|
so->probes = probes;
|
||||||
|
so->maxProbes = maxProbes;
|
||||||
so->dimensions = dimensions;
|
so->dimensions = dimensions;
|
||||||
|
|
||||||
/* Set support functions */
|
/* Set support functions */
|
||||||
@@ -280,6 +304,8 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys)
|
|||||||
so->bas = GetAccessStrategy(BAS_BULKREAD);
|
so->bas = GetAccessStrategy(BAS_BULKREAD);
|
||||||
|
|
||||||
so->listQueue = pairingheap_allocate(CompareLists, scan);
|
so->listQueue = pairingheap_allocate(CompareLists, scan);
|
||||||
|
so->startPages = palloc(maxProbes * sizeof(BlockNumber));
|
||||||
|
so->currentIndex = 0;
|
||||||
|
|
||||||
scan->opaque = so;
|
scan->opaque = so;
|
||||||
|
|
||||||
@@ -294,11 +320,9 @@ ivfflatrescan(IndexScanDesc scan, ScanKey keys, int nkeys, ScanKey orderbys, int
|
|||||||
{
|
{
|
||||||
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
|
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
|
||||||
|
|
||||||
if (!so->first)
|
|
||||||
tuplesort_reset(so->sortstate);
|
|
||||||
|
|
||||||
so->first = true;
|
so->first = true;
|
||||||
pairingheap_reset(so->listQueue);
|
pairingheap_reset(so->listQueue);
|
||||||
|
so->currentIndex = 0;
|
||||||
|
|
||||||
if (keys && scan->numberOfKeys > 0)
|
if (keys && scan->numberOfKeys > 0)
|
||||||
memmove(scan->keyData, keys, scan->numberOfKeys * sizeof(ScanKeyData));
|
memmove(scan->keyData, keys, scan->numberOfKeys * sizeof(ScanKeyData));
|
||||||
@@ -314,6 +338,8 @@ bool
|
|||||||
ivfflatgettuple(IndexScanDesc scan, ScanDirection dir)
|
ivfflatgettuple(IndexScanDesc scan, ScanDirection dir)
|
||||||
{
|
{
|
||||||
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
|
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
|
||||||
|
ItemPointer heaptid;
|
||||||
|
bool isnull;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Index can be used to scan backward, but Postgres doesn't support
|
* Index can be used to scan backward, but Postgres doesn't support
|
||||||
@@ -341,28 +367,25 @@ ivfflatgettuple(IndexScanDesc scan, ScanDirection dir)
|
|||||||
IvfflatBench("GetScanLists", GetScanLists(scan, value));
|
IvfflatBench("GetScanLists", GetScanLists(scan, value));
|
||||||
IvfflatBench("GetScanItems", GetScanItems(scan, value));
|
IvfflatBench("GetScanItems", GetScanItems(scan, value));
|
||||||
so->first = false;
|
so->first = false;
|
||||||
|
so->value = value;
|
||||||
|
|
||||||
#if defined(IVFFLAT_MEMORY)
|
/* TODO clean up if we allocated a new value */
|
||||||
elog(INFO, "memory: %zu MB", MemoryContextMemAllocated(CurrentMemoryContext, true) / (1024 * 1024));
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* Clean up if we allocated a new value */
|
|
||||||
if (value != scan->orderByData->sk_argument)
|
|
||||||
pfree(DatumGetPointer(value));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tuplesort_gettupleslot(so->sortstate, true, false, so->mslot, NULL))
|
while (!tuplesort_gettupleslot(so->sortstate, true, false, so->mslot, NULL))
|
||||||
{
|
{
|
||||||
bool isnull;
|
if (so->currentIndex == so->maxProbes)
|
||||||
ItemPointer heaptid = (ItemPointer) DatumGetPointer(slot_getattr(so->mslot, 2, &isnull));
|
return false;
|
||||||
|
|
||||||
scan->xs_heaptid = *heaptid;
|
IvfflatBench("GetScanItems", GetScanItems(scan, so->value));
|
||||||
scan->xs_recheck = false;
|
|
||||||
scan->xs_recheckorderby = false;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
heaptid = (ItemPointer) DatumGetPointer(slot_getattr(so->mslot, 2, &isnull));
|
||||||
|
|
||||||
|
scan->xs_heaptid = *heaptid;
|
||||||
|
scan->xs_recheck = false;
|
||||||
|
scan->xs_recheckorderby = false;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -374,6 +397,7 @@ ivfflatendscan(IndexScanDesc scan)
|
|||||||
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
|
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
|
||||||
|
|
||||||
pairingheap_free(so->listQueue);
|
pairingheap_free(so->listQueue);
|
||||||
|
pfree(so->startPages);
|
||||||
tuplesort_end(so->sortstate);
|
tuplesort_end(so->sortstate);
|
||||||
FreeAccessStrategy(so->bas);
|
FreeAccessStrategy(so->bas);
|
||||||
FreeTupleDesc(so->tupdesc);
|
FreeTupleDesc(so->tupdesc);
|
||||||
|
|||||||
18
src/vector.c
18
src/vector.c
@@ -155,6 +155,24 @@ CheckStateArray(ArrayType *statearray, const char *caller)
|
|||||||
return (float8 *) ARR_DATA_PTR(statearray);
|
return (float8 *) ARR_DATA_PTR(statearray);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if PG_VERSION_NUM < 120003
|
||||||
|
static pg_noinline void
|
||||||
|
float_overflow_error(void)
|
||||||
|
{
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||||
|
errmsg("value out of range: overflow")));
|
||||||
|
}
|
||||||
|
|
||||||
|
static pg_noinline void
|
||||||
|
float_underflow_error(void)
|
||||||
|
{
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||||
|
errmsg("value out of range: underflow")));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Convert textual representation to internal representation
|
* Convert textual representation to internal representation
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -94,7 +94,8 @@ like($explain, qr/Seq Scan/);
|
|||||||
$explain = $node->safe_psql("postgres", qq(
|
$explain = $node->safe_psql("postgres", qq(
|
||||||
EXPLAIN ANALYZE SELECT i FROM tst WHERE v <-> '$query' < 1 ORDER BY v <-> '$query';
|
EXPLAIN ANALYZE SELECT i FROM tst WHERE v <-> '$query' < 1 ORDER BY v <-> '$query';
|
||||||
));
|
));
|
||||||
like($explain, qr/Seq Scan/);
|
# TODO Do not use index
|
||||||
|
like($explain, qr/Index Scan using idx/);
|
||||||
|
|
||||||
# Test attribute index
|
# Test attribute index
|
||||||
$node->safe_psql("postgres", "CREATE INDEX attribute_idx ON tst (c);");
|
$node->safe_psql("postgres", "CREATE INDEX attribute_idx ON tst (c);");
|
||||||
@@ -109,6 +110,7 @@ $node->safe_psql("postgres", "CREATE INDEX partial_idx ON tst USING ivfflat (v v
|
|||||||
$explain = $node->safe_psql("postgres", qq(
|
$explain = $node->safe_psql("postgres", qq(
|
||||||
EXPLAIN ANALYZE SELECT i FROM tst WHERE c = $c ORDER BY v <-> '$query' LIMIT $limit;
|
EXPLAIN ANALYZE SELECT i FROM tst WHERE c = $c ORDER BY v <-> '$query' LIMIT $limit;
|
||||||
));
|
));
|
||||||
like($explain, qr/Index Scan using partial_idx/);
|
# TODO Use partial index
|
||||||
|
like($explain, qr/Index Scan using idx/);
|
||||||
|
|
||||||
done_testing();
|
done_testing();
|
||||||
|
|||||||
@@ -18,13 +18,9 @@ $node->start;
|
|||||||
# Create table and index
|
# Create table and index
|
||||||
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
|
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
|
||||||
$node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector($dim), c int4, t text);");
|
$node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector($dim), c int4, t text);");
|
||||||
$node->safe_psql("postgres", "CREATE TABLE cat (i int4 PRIMARY KEY, t text, b boolean);");
|
|
||||||
$node->safe_psql("postgres",
|
$node->safe_psql("postgres",
|
||||||
"INSERT INTO tst SELECT i, ARRAY[$array_sql], i % $nc, 'test ' || i FROM generate_series(1, 10000) i;"
|
"INSERT INTO tst SELECT i, ARRAY[$array_sql], i % $nc, 'test ' || i FROM generate_series(1, 10000) i;"
|
||||||
);
|
);
|
||||||
$node->safe_psql("postgres",
|
|
||||||
"INSERT INTO cat SELECT i, 'cat ' || i, i % 5 = 0 FROM generate_series(1, $nc) i;"
|
|
||||||
);
|
|
||||||
$node->safe_psql("postgres", "CREATE INDEX idx ON tst USING hnsw (v vector_l2_ops);");
|
$node->safe_psql("postgres", "CREATE INDEX idx ON tst USING hnsw (v vector_l2_ops);");
|
||||||
$node->safe_psql("postgres", "ANALYZE tst;");
|
$node->safe_psql("postgres", "ANALYZE tst;");
|
||||||
|
|
||||||
@@ -100,25 +96,13 @@ $explain = $node->safe_psql("postgres", qq(
|
|||||||
));
|
));
|
||||||
like($explain, qr/Seq Scan/);
|
like($explain, qr/Seq Scan/);
|
||||||
|
|
||||||
# Test join
|
|
||||||
$explain = $node->safe_psql("postgres", qq(
|
|
||||||
EXPLAIN ANALYZE SELECT cat.t FROM cat INNER JOIN tst ON cat.i = tst.c ORDER BY v <-> '$query' LIMIT $limit;
|
|
||||||
));
|
|
||||||
like($explain, qr/Index Scan using idx/);
|
|
||||||
|
|
||||||
# Test join with attribute filtering
|
|
||||||
$explain = $node->safe_psql("postgres", qq(
|
|
||||||
EXPLAIN ANALYZE SELECT cat.t FROM cat INNER JOIN tst ON cat.i = tst.c WHERE cat.b = 't' ORDER BY v <-> '$query' LIMIT $limit;
|
|
||||||
));
|
|
||||||
like($explain, qr/Index Scan using idx/);
|
|
||||||
|
|
||||||
# Test attribute index
|
# Test attribute index
|
||||||
$node->safe_psql("postgres", "CREATE INDEX attribute_idx ON tst (c);");
|
$node->safe_psql("postgres", "CREATE INDEX attribute_idx ON tst (c);");
|
||||||
$explain = $node->safe_psql("postgres", qq(
|
$explain = $node->safe_psql("postgres", qq(
|
||||||
EXPLAIN ANALYZE SELECT i FROM tst WHERE c = $c ORDER BY v <-> '$query' LIMIT $limit;
|
EXPLAIN ANALYZE SELECT i FROM tst WHERE c = $c ORDER BY v <-> '$query' LIMIT $limit;
|
||||||
));
|
));
|
||||||
# Use attribute index
|
# TODO Use attribute index
|
||||||
like($explain, qr/Bitmap Index Scan on attribute_idx/);
|
like($explain, qr/Index Scan using idx/);
|
||||||
|
|
||||||
# Test partial index
|
# Test partial index
|
||||||
$node->safe_psql("postgres", "CREATE INDEX partial_idx ON tst USING hnsw (v vector_l2_ops) WHERE (c = $c);");
|
$node->safe_psql("postgres", "CREATE INDEX partial_idx ON tst USING hnsw (v vector_l2_ops) WHERE (c = $c);");
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
use strict;
|
|
||||||
use warnings FATAL => 'all';
|
|
||||||
use PostgreSQL::Test::Cluster;
|
|
||||||
use PostgreSQL::Test::Utils;
|
|
||||||
use Test::More;
|
|
||||||
|
|
||||||
my @dims = (384, 1536);
|
|
||||||
my $limit = 10;
|
|
||||||
|
|
||||||
# Initialize node
|
|
||||||
my $node = PostgreSQL::Test::Cluster->new('node');
|
|
||||||
$node->init;
|
|
||||||
$node->start;
|
|
||||||
|
|
||||||
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
|
|
||||||
|
|
||||||
for my $dim (@dims)
|
|
||||||
{
|
|
||||||
my $array_sql = join(",", ('random()') x $dim);
|
|
||||||
my $n = $dim == 384 ? 3000 : 1000;
|
|
||||||
|
|
||||||
# Create table and index
|
|
||||||
$node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector($dim));");
|
|
||||||
$node->safe_psql("postgres",
|
|
||||||
"INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(1, $n) i;"
|
|
||||||
);
|
|
||||||
$node->safe_psql("postgres", "CREATE INDEX idx ON tst USING hnsw (v vector_l2_ops);");
|
|
||||||
$node->safe_psql("postgres", "ANALYZE tst;");
|
|
||||||
|
|
||||||
# Generate query
|
|
||||||
my @r = ();
|
|
||||||
for (1 .. $dim)
|
|
||||||
{
|
|
||||||
push(@r, rand());
|
|
||||||
}
|
|
||||||
my $query = "[" . join(",", @r) . "]";
|
|
||||||
|
|
||||||
my $explain = $node->safe_psql("postgres", qq(
|
|
||||||
EXPLAIN ANALYZE SELECT i FROM tst ORDER BY v <-> '$query' LIMIT $limit;
|
|
||||||
));
|
|
||||||
like($explain, qr/Index Scan using idx/);
|
|
||||||
|
|
||||||
$explain = $node->safe_psql("postgres", qq(
|
|
||||||
EXPLAIN ANALYZE SELECT i FROM tst WHERE v <-> '$query' < 1 ORDER BY v <-> '$query' LIMIT $limit;
|
|
||||||
));
|
|
||||||
like($explain, qr/Index Scan using idx/);
|
|
||||||
|
|
||||||
$node->safe_psql("postgres", "DROP TABLE tst;");
|
|
||||||
}
|
|
||||||
|
|
||||||
done_testing();
|
|
||||||
31
test/t/039_ivfflat_streaming.pl
Normal file
31
test/t/039_ivfflat_streaming.pl
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
use strict;
|
||||||
|
use warnings FATAL => 'all';
|
||||||
|
use PostgreSQL::Test::Cluster;
|
||||||
|
use PostgreSQL::Test::Utils;
|
||||||
|
use Test::More;
|
||||||
|
|
||||||
|
my $dim = 3;
|
||||||
|
my $array_sql = join(",", ('random()') x $dim);
|
||||||
|
|
||||||
|
# Initialize node
|
||||||
|
my $node = PostgreSQL::Test::Cluster->new('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($dim));");
|
||||||
|
$node->safe_psql("postgres",
|
||||||
|
"INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(1, 100000) i;"
|
||||||
|
);
|
||||||
|
$node->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v vector_l2_ops);");
|
||||||
|
|
||||||
|
my $count = $node->safe_psql("postgres", qq(
|
||||||
|
SET enable_seqscan = off;
|
||||||
|
SET ivfflat.probes = 10;
|
||||||
|
SET ivfflat.streaming = on;
|
||||||
|
SELECT COUNT(*) FROM (SELECT v FROM tst WHERE i % 10000 = 0 ORDER BY v <-> (SELECT v FROM tst LIMIT 1) LIMIT 11) t;
|
||||||
|
));
|
||||||
|
is($count, 10);
|
||||||
|
|
||||||
|
done_testing();
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
use strict;
|
|
||||||
use warnings FATAL => 'all';
|
|
||||||
use PostgreSQL::Test::Cluster;
|
|
||||||
use PostgreSQL::Test::Utils;
|
|
||||||
use Test::More;
|
|
||||||
|
|
||||||
my @dims = (384, 1536);
|
|
||||||
my $limit = 10;
|
|
||||||
|
|
||||||
# Initialize node
|
|
||||||
my $node = PostgreSQL::Test::Cluster->new('node');
|
|
||||||
$node->init;
|
|
||||||
$node->start;
|
|
||||||
|
|
||||||
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
|
|
||||||
|
|
||||||
for my $dim (@dims)
|
|
||||||
{
|
|
||||||
my $array_sql = join(",", ('random()') x $dim);
|
|
||||||
|
|
||||||
# Create table and index
|
|
||||||
$node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector($dim));");
|
|
||||||
$node->safe_psql("postgres",
|
|
||||||
"INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(1, 6000) i;"
|
|
||||||
);
|
|
||||||
$node->safe_psql("postgres", "CREATE INDEX idx ON tst USING ivfflat (v vector_l2_ops) WITH (lists = 5);");
|
|
||||||
$node->safe_psql("postgres", "ANALYZE tst;");
|
|
||||||
|
|
||||||
# Generate query
|
|
||||||
my @r = ();
|
|
||||||
for (1 .. $dim)
|
|
||||||
{
|
|
||||||
push(@r, rand());
|
|
||||||
}
|
|
||||||
my $query = "[" . join(",", @r) . "]";
|
|
||||||
|
|
||||||
my $explain = $node->safe_psql("postgres", qq(
|
|
||||||
EXPLAIN ANALYZE SELECT i FROM tst ORDER BY v <-> '$query' LIMIT $limit;
|
|
||||||
));
|
|
||||||
like($explain, qr/Index Scan using idx/);
|
|
||||||
|
|
||||||
$explain = $node->safe_psql("postgres", qq(
|
|
||||||
EXPLAIN ANALYZE SELECT i FROM tst WHERE v <-> '$query' < 1 ORDER BY v <-> '$query' LIMIT $limit;
|
|
||||||
));
|
|
||||||
like($explain, qr/Index Scan using idx/);
|
|
||||||
|
|
||||||
$node->safe_psql("postgres", "DROP TABLE tst;");
|
|
||||||
}
|
|
||||||
|
|
||||||
done_testing();
|
|
||||||
125
test/t/040_ivfflat_streaming_recall.pl
Normal file
125
test/t/040_ivfflat_streaming_recall.pl
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
use strict;
|
||||||
|
use warnings FATAL => 'all';
|
||||||
|
use PostgreSQL::Test::Cluster;
|
||||||
|
use PostgreSQL::Test::Utils;
|
||||||
|
use Test::More;
|
||||||
|
|
||||||
|
my $node;
|
||||||
|
my @queries = ();
|
||||||
|
my @expected;
|
||||||
|
my $limit = 20;
|
||||||
|
my @cs = (100, 1000);
|
||||||
|
|
||||||
|
sub test_recall
|
||||||
|
{
|
||||||
|
my ($c, $probes, $min, $operator) = @_;
|
||||||
|
my $correct = 0;
|
||||||
|
my $total = 0;
|
||||||
|
|
||||||
|
my $explain = $node->safe_psql("postgres", qq(
|
||||||
|
SET enable_seqscan = off;
|
||||||
|
SET ivfflat.probes = $probes;
|
||||||
|
SET ivfflat.streaming = on;
|
||||||
|
EXPLAIN ANALYZE SELECT i FROM tst WHERE i % $c = 0 ORDER BY v $operator '$queries[0]' LIMIT $limit;
|
||||||
|
));
|
||||||
|
like($explain, qr/Index Scan using idx on tst/);
|
||||||
|
|
||||||
|
for my $i (0 .. $#queries)
|
||||||
|
{
|
||||||
|
my $actual = $node->safe_psql("postgres", qq(
|
||||||
|
SET enable_seqscan = off;
|
||||||
|
SET ivfflat.probes = $probes;
|
||||||
|
SET ivfflat.streaming = on;
|
||||||
|
SELECT i FROM tst WHERE i % $c = 0 ORDER BY v $operator '$queries[$i]' LIMIT $limit;
|
||||||
|
));
|
||||||
|
my @actual_ids = split("\n", $actual);
|
||||||
|
|
||||||
|
my @expected_ids = split("\n", $expected[$i]);
|
||||||
|
my %expected_set = map { $_ => 1 } @expected_ids;
|
||||||
|
|
||||||
|
foreach (@actual_ids)
|
||||||
|
{
|
||||||
|
if (exists($expected_set{$_}))
|
||||||
|
{
|
||||||
|
$correct++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$total += $limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
cmp_ok($correct / $total, ">=", $min, $operator);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Initialize node
|
||||||
|
$node = PostgreSQL::Test::Cluster->new('node');
|
||||||
|
$node->init;
|
||||||
|
$node->start;
|
||||||
|
|
||||||
|
# Create table
|
||||||
|
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
|
||||||
|
$node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector(3));");
|
||||||
|
$node->safe_psql("postgres",
|
||||||
|
"INSERT INTO tst SELECT i, ARRAY[random(), random(), random()] FROM generate_series(1, 100000) i;"
|
||||||
|
);
|
||||||
|
|
||||||
|
# Generate queries
|
||||||
|
for (1 .. 20)
|
||||||
|
{
|
||||||
|
my $r1 = rand();
|
||||||
|
my $r2 = rand();
|
||||||
|
my $r3 = rand();
|
||||||
|
push(@queries, "[$r1,$r2,$r3]");
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check each index type
|
||||||
|
my @operators = ("<->", "<=>");
|
||||||
|
my @opclasses = ("vector_l2_ops", "vector_cosine_ops");
|
||||||
|
|
||||||
|
for my $i (0 .. $#operators)
|
||||||
|
{
|
||||||
|
my $operator = $operators[$i];
|
||||||
|
my $opclass = $opclasses[$i];
|
||||||
|
|
||||||
|
$node->safe_psql("postgres", "CREATE INDEX idx ON tst USING ivfflat (v $opclass);");
|
||||||
|
|
||||||
|
foreach (@cs)
|
||||||
|
{
|
||||||
|
my $c = $_;
|
||||||
|
|
||||||
|
# Get exact results
|
||||||
|
@expected = ();
|
||||||
|
foreach (@queries)
|
||||||
|
{
|
||||||
|
my $res = $node->safe_psql("postgres", qq(
|
||||||
|
SET enable_indexscan = off;
|
||||||
|
WITH top AS (
|
||||||
|
SELECT v $operator '$_' AS distance FROM tst WHERE i % $c = 0 ORDER BY distance LIMIT $limit
|
||||||
|
)
|
||||||
|
SELECT i FROM tst WHERE (v $operator '$_') <= (SELECT MAX(distance) FROM top)
|
||||||
|
));
|
||||||
|
push(@expected, $res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($c == 100)
|
||||||
|
{
|
||||||
|
test_recall($c, 1, 0.58, $operator);
|
||||||
|
test_recall($c, 10, 0.98, $operator);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if ($operator eq "<->")
|
||||||
|
{
|
||||||
|
test_recall($c, 1, 0.80, $operator);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
test_recall($c, 1, 0.88, $operator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$node->safe_psql("postgres", "DROP INDEX idx;");
|
||||||
|
}
|
||||||
|
|
||||||
|
done_testing();
|
||||||
Reference in New Issue
Block a user