mirror of
https://github.com/pgvector/pgvector.git
synced 2026-07-22 12:07:34 +08:00
Compare commits
12 Commits
guc-explai
...
ivfflat-st
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4ac05f044 | ||
|
|
25f97fd91d | ||
|
|
cd3f9a38ae | ||
|
|
ff6267917e | ||
|
|
c950c5ffaa | ||
|
|
88f56dc234 | ||
|
|
e2ba6cf38f | ||
|
|
689f9c4659 | ||
|
|
88889f5a4c | ||
|
|
79851729f1 | ||
|
|
3fd6a29c49 | ||
|
|
2c58804756 |
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -8,8 +8,8 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- postgres: 18
|
||||
os: ubuntu-24.04
|
||||
# - postgres: 18
|
||||
# os: ubuntu-24.04
|
||||
- postgres: 17
|
||||
os: ubuntu-24.04
|
||||
- postgres: 16
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
- postgres: 16
|
||||
os: macos-14
|
||||
- postgres: 14
|
||||
os: macos-13
|
||||
os: macos-12
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ankane/setup-postgres@v1
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
## 0.8.0 (unreleased)
|
||||
|
||||
- Added support for iterative index scans
|
||||
- Added casts for arrays to `sparsevec`
|
||||
- Improved cost estimation
|
||||
- Improved performance of HNSW inserts and on-disk index builds
|
||||
- Reduced memory usage for HNSW index scans
|
||||
- Dropped support for Postgres 12
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
ARG PG_MAJOR=17
|
||||
ARG PG_MAJOR=16
|
||||
FROM postgres:$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
|
||||
|
||||
# for Docker
|
||||
PG_MAJOR ?= 17
|
||||
PG_MAJOR ?= 16
|
||||
|
||||
.PHONY: docker
|
||||
|
||||
|
||||
121
README.md
121
README.md
@@ -52,8 +52,6 @@ nmake /F Makefile.win
|
||||
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
|
||||
|
||||
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);
|
||||
```
|
||||
|
||||
Also supports [half-precision](#half-precision-vectors), [binary](#binary-vectors), and [sparse](#sparse-vectors) vectors
|
||||
|
||||
Insert vectors
|
||||
|
||||
```sql
|
||||
@@ -149,8 +145,6 @@ Supported distance functions are:
|
||||
- `<#>` - (negative) inner product
|
||||
- `<=>` - cosine distance
|
||||
- `<+>` - 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
|
||||
|
||||
@@ -451,89 +445,6 @@ Use [partitioning](https://www.postgresql.org/docs/current/ddl-partitioning.html
|
||||
CREATE TABLE items (embedding vector(3), category_id int) PARTITION BY LIST(category_id);
|
||||
```
|
||||
|
||||
## Iterative Index Scans
|
||||
|
||||
*Unreleased*
|
||||
|
||||
With approximate indexes, queries with filtering can return less results (due to post-filtering). Starting with 0.8.0, you can enable iterative index scans. If too few results from the initial scan match the filters, the scan will resume until enough results are found (or it reaches `hnsw.max_scan_tuples` or `ivfflat.max_probes`). This can significantly improve recall.
|
||||
|
||||
There are two modes for iterative scans: strict and relaxed.
|
||||
|
||||
Strict ensures results are in the exact order by distance
|
||||
|
||||
```sql
|
||||
SET hnsw.iterative_scan = strict_order;
|
||||
```
|
||||
|
||||
Relaxed allows results to be slightly out of order by distance, but provides better recall
|
||||
|
||||
```sql
|
||||
SET hnsw.iterative_scan = relaxed_order;
|
||||
# or
|
||||
SET ivfflat.iterative_scan = relaxed_order;
|
||||
```
|
||||
|
||||
With relaxed ordering, you can use a [materialized CTE](https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-CTE-MATERIALIZATION) to get strict ordering
|
||||
|
||||
```sql
|
||||
WITH relaxed_results AS MATERIALIZED (
|
||||
SELECT id, embedding <-> '[1,2,3]' AS distance FROM items WHERE category_id = 123 ORDER BY distance LIMIT 5
|
||||
) SELECT * FROM relaxed_results ORDER BY distance;
|
||||
```
|
||||
|
||||
For queries that filter by distance, use a materialized CTE and place the distance filter outside of it for best performance (due to the [current behavior](https://www.postgresql.org/message-id/flat/CAOdR5yGUoMQ6j7M5hNUXrySzaqZVGf_Ne%2B8fwZMRKTFxU1nbJg%40mail.gmail.com) of the Postgres executor)
|
||||
|
||||
```sql
|
||||
WITH nearest_results AS MATERIALIZED (
|
||||
SELECT id, embedding <-> '[1,2,3]' AS distance FROM items ORDER BY distance LIMIT 5
|
||||
) SELECT * FROM nearest_results WHERE distance < 5 ORDER BY distance;
|
||||
```
|
||||
|
||||
Note: Place any other filters inside the CTE
|
||||
|
||||
### Iterative Scan Options
|
||||
|
||||
Since scanning a large portion of an approximate index is expensive, there are options to control when a scan ends
|
||||
|
||||
#### HNSW
|
||||
|
||||
Specify the max number of tuples to visit (20,000 by default)
|
||||
|
||||
```sql
|
||||
SET hnsw.max_scan_tuples = 20000;
|
||||
```
|
||||
|
||||
Note: This is approximate and does not affect the initial scan
|
||||
|
||||
Specify the max amount of memory to use, as a multiple of `work_mem` (1 by default)
|
||||
|
||||
```sql
|
||||
SET hnsw.scan_mem_multiplier = 2;
|
||||
```
|
||||
|
||||
You can see when increasing this is needed by enabling debug messages
|
||||
|
||||
```sql
|
||||
SET client_min_messages = debug1;
|
||||
```
|
||||
|
||||
which will show when a scan reaches the memory limit
|
||||
|
||||
```text
|
||||
DEBUG: hnsw index scan reached memory limit after 20000 tuples
|
||||
HINT: Increase hnsw.scan_mem_multiplier to scan more tuples.
|
||||
```
|
||||
|
||||
#### IVFFlat
|
||||
|
||||
Specify the max number of probes
|
||||
|
||||
```sql
|
||||
SET ivfflat.max_probes = 100;
|
||||
```
|
||||
|
||||
Note: If this is lower than `ivfflat.probes`, `ivfflat.probes` will be used
|
||||
|
||||
## Half-Precision Vectors
|
||||
|
||||
*Added in 0.7.0*
|
||||
@@ -1072,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:
|
||||
|
||||
```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:
|
||||
@@ -1083,11 +994,11 @@ sudo --preserve-env=PG_CONFIG make install
|
||||
|
||||
A few common paths on Mac are:
|
||||
|
||||
- EDB installer - `/Library/PostgreSQL/17/bin/pg_config`
|
||||
- Homebrew (arm64) - `/opt/homebrew/opt/postgresql@17/bin/pg_config`
|
||||
- Homebrew (x86-64) - `/usr/local/opt/postgresql@17/bin/pg_config`
|
||||
- EDB installer - `/Library/PostgreSQL/16/bin/pg_config`
|
||||
- Homebrew (arm64) - `/opt/homebrew/opt/postgresql@16/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
|
||||
|
||||
@@ -1096,10 +1007,10 @@ If compilation fails with `fatal error: postgres.h: No such file or directory`,
|
||||
For Ubuntu and Debian, use:
|
||||
|
||||
```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
|
||||
|
||||
@@ -1132,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:
|
||||
|
||||
```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:
|
||||
|
||||
```sh
|
||||
git clone --branch v0.7.4 https://github.com/pgvector/pgvector.git
|
||||
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
|
||||
@@ -1153,7 +1064,7 @@ With Homebrew Postgres, you can use:
|
||||
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
|
||||
|
||||
@@ -1168,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:
|
||||
|
||||
```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
|
||||
|
||||
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
|
||||
sudo yum install pgvector_17
|
||||
sudo yum install pgvector_16
|
||||
# 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
|
||||
|
||||
|
||||
@@ -159,6 +159,24 @@ CheckStateArray(ArrayType *statearray, const char *caller)
|
||||
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
|
||||
*/
|
||||
|
||||
101
src/hnsw.c
101
src/hnsw.c
@@ -12,23 +12,12 @@
|
||||
#include "utils/float.h"
|
||||
#include "utils/guc.h"
|
||||
#include "utils/selfuncs.h"
|
||||
#include "utils/spccache.h"
|
||||
|
||||
#if PG_VERSION_NUM < 150000
|
||||
#define MarkGUCPrefixReserved(x) EmitWarningsOnPlaceholders(x)
|
||||
#endif
|
||||
|
||||
static const struct config_enum_entry hnsw_iterative_scan_options[] = {
|
||||
{"off", HNSW_ITERATIVE_SCAN_OFF, false},
|
||||
{"relaxed_order", HNSW_ITERATIVE_SCAN_RELAXED, false},
|
||||
{"strict_order", HNSW_ITERATIVE_SCAN_STRICT, false},
|
||||
{NULL, 0, false}
|
||||
};
|
||||
|
||||
int hnsw_ef_search;
|
||||
int hnsw_iterative_scan;
|
||||
int hnsw_max_scan_tuples;
|
||||
double hnsw_scan_mem_multiplier;
|
||||
int hnsw_lock_tranche_id;
|
||||
static relopt_kind hnsw_relopt_kind;
|
||||
|
||||
@@ -77,21 +66,7 @@ HnswInit(void)
|
||||
|
||||
DefineCustomIntVariable("hnsw.ef_search", "Sets the size of the dynamic candidate list for search",
|
||||
"Valid range is 1..1000.", &hnsw_ef_search,
|
||||
HNSW_DEFAULT_EF_SEARCH, HNSW_MIN_EF_SEARCH, HNSW_MAX_EF_SEARCH, PGC_USERSET, GUC_EXPLAIN, NULL, NULL, NULL);
|
||||
|
||||
DefineCustomEnumVariable("hnsw.iterative_scan", "Sets the mode for iterative scans",
|
||||
NULL, &hnsw_iterative_scan,
|
||||
HNSW_ITERATIVE_SCAN_OFF, hnsw_iterative_scan_options, PGC_USERSET, GUC_EXPLAIN, NULL, NULL, NULL);
|
||||
|
||||
/* This is approximate and does not affect the initial scan */
|
||||
DefineCustomIntVariable("hnsw.max_scan_tuples", "Sets the max number of tuples to visit for iterative scans",
|
||||
NULL, &hnsw_max_scan_tuples,
|
||||
20000, 1, INT_MAX, PGC_USERSET, GUC_EXPLAIN, NULL, NULL, NULL);
|
||||
|
||||
/* Same range as hash_mem_multiplier */
|
||||
DefineCustomRealVariable("hnsw.scan_mem_multiplier", "Sets the multiple of work_mem to use for iterative scans",
|
||||
NULL, &hnsw_scan_mem_multiplier,
|
||||
1, 1, 1000, PGC_USERSET, GUC_EXPLAIN, NULL, NULL, NULL);
|
||||
HNSW_DEFAULT_EF_SEARCH, HNSW_MIN_EF_SEARCH, HNSW_MAX_EF_SEARCH, PGC_USERSET, 0, NULL, NULL, NULL);
|
||||
|
||||
MarkGUCPrefixReserved("hnsw");
|
||||
}
|
||||
@@ -124,9 +99,7 @@ hnswcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
|
||||
{
|
||||
GenericCosts costs;
|
||||
int m;
|
||||
double ratio;
|
||||
double startupPages;
|
||||
double spc_seq_page_cost;
|
||||
int entryLevel;
|
||||
Relation index;
|
||||
|
||||
/* Never use index without order */
|
||||
@@ -137,80 +110,26 @@ hnswcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
|
||||
*indexSelectivity = 0;
|
||||
*indexCorrelation = 0;
|
||||
*indexPages = 0;
|
||||
#if PG_VERSION_NUM >= 180000
|
||||
/* See "On disable_cost" thread on pgsql-hackers */
|
||||
path->path.disabled_nodes = 2;
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
MemSet(&costs, 0, sizeof(costs));
|
||||
|
||||
genericcostestimate(root, path, loop_count, &costs);
|
||||
|
||||
index = index_open(path->indexinfo->indexoid, NoLock);
|
||||
HnswGetMetaPageInfo(index, &m, NULL);
|
||||
index_close(index, NoLock);
|
||||
|
||||
/*
|
||||
* HNSW cost estimation follows a formula that accounts for the total
|
||||
* 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).
|
||||
*/
|
||||
if (path->indexinfo->tuples > 0)
|
||||
{
|
||||
double scalingFactor = 0.55;
|
||||
int entryLevel = (int) (log(path->indexinfo->tuples) * HnswGetMl(m));
|
||||
int layer0TuplesMax = HnswGetLayerM(m, 0) * hnsw_ef_search;
|
||||
double layer0Selectivity = scalingFactor * log(path->indexinfo->tuples) / (log(m) * (1 + log(hnsw_ef_search)));
|
||||
/* Approximate entry level */
|
||||
entryLevel = (int) -log(1.0 / path->indexinfo->tuples) * HnswGetMl(m);
|
||||
|
||||
ratio = (entryLevel * m + layer0TuplesMax * layer0Selectivity) / path->indexinfo->tuples;
|
||||
/* TODO Improve estimate of visited tuples (currently underestimates) */
|
||||
/* Account for number of tuples (or entry level), m, and ef_search */
|
||||
costs.numIndexTuples = (entryLevel + 2) * m;
|
||||
|
||||
if (ratio > 1)
|
||||
ratio = 1;
|
||||
}
|
||||
else
|
||||
ratio = 1;
|
||||
genericcostestimate(root, path, loop_count, &costs);
|
||||
|
||||
get_tablespace_page_costs(path->indexinfo->reltablespace, NULL, &spc_seq_page_cost);
|
||||
|
||||
/* Startup cost is cost before returning the first row */
|
||||
costs.indexStartupCost = costs.indexTotalCost * ratio;
|
||||
|
||||
/* Adjust cost if needed since TOAST not included in seq scan cost */
|
||||
startupPages = costs.numIndexPages * ratio;
|
||||
if (startupPages > path->indexinfo->rel->pages && ratio < 0.5)
|
||||
{
|
||||
/* Change all page cost from random to sequential */
|
||||
costs.indexStartupCost -= startupPages * (costs.spc_random_page_cost - spc_seq_page_cost);
|
||||
|
||||
/* Remove cost of extra pages */
|
||||
costs.indexStartupCost -= (startupPages - path->indexinfo->rel->pages) * spc_seq_page_cost;
|
||||
}
|
||||
|
||||
*indexStartupCost = costs.indexStartupCost;
|
||||
/* Use total cost since most work happens before first tuple is returned */
|
||||
*indexStartupCost = costs.indexTotalCost;
|
||||
*indexTotalCost = costs.indexTotalCost;
|
||||
*indexSelectivity = costs.indexSelectivity;
|
||||
*indexCorrelation = costs.indexCorrelation;
|
||||
|
||||
90
src/hnsw.h
90
src/hnsw.h
@@ -88,9 +88,6 @@
|
||||
/* Ensure fits on page and in uint8 */
|
||||
#define HnswGetMaxLevel(m) Min(((BLCKSZ - MAXALIGN(SizeOfPageHeaderData) - MAXALIGN(sizeof(HnswPageOpaqueData)) - offsetof(HnswNeighborTupleData, indextids) - sizeof(ItemIdData)) / (sizeof(ItemPointerData)) / (m)) - 2, 255)
|
||||
|
||||
#define HnswGetSearchCandidate(membername, ptr) pairingheap_container(HnswSearchCandidate, membername, ptr)
|
||||
#define HnswGetSearchCandidateConst(membername, ptr) pairingheap_const_container(HnswSearchCandidate, membername, ptr)
|
||||
|
||||
#define HnswGetValue(base, element) PointerGetDatum(HnswPtrAccess(base, (element)->value))
|
||||
|
||||
#if PG_VERSION_NUM < 140005
|
||||
@@ -109,18 +106,8 @@
|
||||
|
||||
/* Variables */
|
||||
extern int hnsw_ef_search;
|
||||
extern int hnsw_iterative_scan;
|
||||
extern int hnsw_max_scan_tuples;
|
||||
extern double hnsw_scan_mem_multiplier;
|
||||
extern int hnsw_lock_tranche_id;
|
||||
|
||||
typedef enum HnswIterativeScanMode
|
||||
{
|
||||
HNSW_ITERATIVE_SCAN_OFF,
|
||||
HNSW_ITERATIVE_SCAN_RELAXED,
|
||||
HNSW_ITERATIVE_SCAN_STRICT
|
||||
} HnswIterativeScanMode;
|
||||
|
||||
typedef struct HnswElementData HnswElementData;
|
||||
typedef struct HnswNeighborArray HnswNeighborArray;
|
||||
|
||||
@@ -142,7 +129,6 @@ struct HnswElementData
|
||||
uint8 heaptidsLength;
|
||||
uint8 level;
|
||||
uint8 deleted;
|
||||
uint8 version;
|
||||
uint32 hash;
|
||||
HnswNeighborsPtr neighbors;
|
||||
BlockNumber blkno;
|
||||
@@ -174,7 +160,7 @@ typedef struct HnswSearchCandidate
|
||||
pairingheap_node c_node;
|
||||
pairingheap_node w_node;
|
||||
HnswElementPtr element;
|
||||
double distance;
|
||||
float distance;
|
||||
} HnswSearchCandidate;
|
||||
|
||||
/* HNSW index options */
|
||||
@@ -199,8 +185,8 @@ typedef struct HnswGraph
|
||||
|
||||
/* Allocations state */
|
||||
LWLock allocatorLock;
|
||||
Size memoryUsed;
|
||||
Size memoryTotal;
|
||||
long memoryUsed;
|
||||
long memoryTotal;
|
||||
|
||||
/* Flushed state */
|
||||
LWLock flushLock;
|
||||
@@ -251,18 +237,6 @@ typedef struct HnswTypeInfo
|
||||
void (*checkValue) (Pointer v);
|
||||
} HnswTypeInfo;
|
||||
|
||||
typedef struct HnswSupport
|
||||
{
|
||||
FmgrInfo *procinfo;
|
||||
FmgrInfo *normprocinfo;
|
||||
Oid collation;
|
||||
} HnswSupport;
|
||||
|
||||
typedef struct HnswQuery
|
||||
{
|
||||
Datum value;
|
||||
} HnswQuery;
|
||||
|
||||
typedef struct HnswBuildState
|
||||
{
|
||||
/* Info */
|
||||
@@ -282,7 +256,9 @@ typedef struct HnswBuildState
|
||||
double reltuples;
|
||||
|
||||
/* Support functions */
|
||||
HnswSupport support;
|
||||
FmgrInfo *procinfo;
|
||||
FmgrInfo *normprocinfo;
|
||||
Oid collation;
|
||||
|
||||
/* Variables */
|
||||
HnswGraph graphData;
|
||||
@@ -330,10 +306,10 @@ typedef struct HnswElementTupleData
|
||||
uint8 type;
|
||||
uint8 level;
|
||||
uint8 deleted;
|
||||
uint8 version;
|
||||
uint8 unused;
|
||||
ItemPointerData heaptids[HNSW_HEAPTIDS];
|
||||
ItemPointerData neighbortid;
|
||||
uint16 unused;
|
||||
uint16 unused2;
|
||||
Vector data;
|
||||
} HnswElementTupleData;
|
||||
|
||||
@@ -342,42 +318,24 @@ typedef HnswElementTupleData * HnswElementTuple;
|
||||
typedef struct HnswNeighborTupleData
|
||||
{
|
||||
uint8 type;
|
||||
uint8 version;
|
||||
uint8 unused;
|
||||
uint16 count;
|
||||
ItemPointerData indextids[FLEXIBLE_ARRAY_MEMBER];
|
||||
} HnswNeighborTupleData;
|
||||
|
||||
typedef HnswNeighborTupleData * HnswNeighborTuple;
|
||||
|
||||
typedef union
|
||||
{
|
||||
struct pointerhash_hash *pointers;
|
||||
struct offsethash_hash *offsets;
|
||||
struct tidhash_hash *tids;
|
||||
} visited_hash;
|
||||
|
||||
typedef union
|
||||
{
|
||||
HnswElement element;
|
||||
ItemPointerData indextid;
|
||||
} HnswUnvisited;
|
||||
|
||||
typedef struct HnswScanOpaqueData
|
||||
{
|
||||
const HnswTypeInfo *typeInfo;
|
||||
bool first;
|
||||
List *w;
|
||||
visited_hash v;
|
||||
pairingheap *discarded;
|
||||
HnswQuery q;
|
||||
int m;
|
||||
int64 tuples;
|
||||
double previousDistance;
|
||||
Size maxMemory;
|
||||
MemoryContext tmpCtx;
|
||||
|
||||
/* Support functions */
|
||||
HnswSupport support;
|
||||
FmgrInfo *procinfo;
|
||||
FmgrInfo *normprocinfo;
|
||||
Oid collation;
|
||||
} HnswScanOpaqueData;
|
||||
|
||||
typedef HnswScanOpaqueData * HnswScanOpaque;
|
||||
@@ -395,7 +353,8 @@ typedef struct HnswVacuumState
|
||||
int efConstruction;
|
||||
|
||||
/* Support functions */
|
||||
HnswSupport support;
|
||||
FmgrInfo *procinfo;
|
||||
Oid collation;
|
||||
|
||||
/* Variables */
|
||||
struct tidhash_hash *deleted;
|
||||
@@ -411,33 +370,30 @@ typedef struct HnswVacuumState
|
||||
int HnswGetM(Relation index);
|
||||
int HnswGetEfConstruction(Relation index);
|
||||
FmgrInfo *HnswOptionalProcInfo(Relation index, uint16 procnum);
|
||||
void HnswInitSupport(HnswSupport * support, Relation index);
|
||||
Datum HnswNormValue(const HnswTypeInfo * typeInfo, Oid collation, Datum value);
|
||||
bool HnswCheckNorm(HnswSupport * support, Datum value);
|
||||
bool HnswCheckNorm(FmgrInfo *procinfo, Oid collation, Datum value);
|
||||
Buffer HnswNewBuffer(Relation index, ForkNumber forkNum);
|
||||
void HnswInitPage(Buffer buf, Page page);
|
||||
void HnswInit(void);
|
||||
List *HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation index, HnswSupport * support, int m, bool inserting, HnswElement skipElement, visited_hash * v, pairingheap **discarded, bool initVisited, int64 *tuples);
|
||||
List *HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, FmgrInfo *procinfo, Oid collation, int m, bool inserting, HnswElement skipElement);
|
||||
HnswElement HnswGetEntryPoint(Relation index);
|
||||
void HnswGetMetaPageInfo(Relation index, int *m, HnswElement * entryPoint);
|
||||
void *HnswAlloc(HnswAllocator * allocator, Size size);
|
||||
HnswElement HnswInitElement(char *base, ItemPointer tid, int m, double ml, int maxLevel, HnswAllocator * alloc);
|
||||
HnswElement HnswInitElementFromBlock(BlockNumber blkno, OffsetNumber offno);
|
||||
void HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint, Relation index, HnswSupport * support, int m, int efConstruction, bool existing);
|
||||
HnswSearchCandidate *HnswEntryCandidate(char *base, HnswElement em, HnswQuery * q, Relation rel, HnswSupport * support, bool loadVec);
|
||||
void HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint, Relation index, FmgrInfo *procinfo, Oid collation, int m, int efConstruction, bool existing);
|
||||
HnswSearchCandidate *HnswEntryCandidate(char *base, HnswElement em, Datum q, Relation rel, FmgrInfo *procinfo, Oid collation, bool loadVec);
|
||||
void HnswUpdateMetaPage(Relation index, int updateEntry, HnswElement entryPoint, BlockNumber insertPage, ForkNumber forkNum, bool building);
|
||||
void HnswSetNeighborTuple(char *base, HnswNeighborTuple ntup, HnswElement e, int m);
|
||||
void HnswAddHeapTid(HnswElement element, ItemPointer heaptid);
|
||||
HnswNeighborArray *HnswInitNeighborArray(int lm, HnswAllocator * allocator);
|
||||
void HnswInitNeighbors(char *base, HnswElement element, int m, HnswAllocator * alloc);
|
||||
bool HnswInsertTupleOnDisk(Relation index, HnswSupport * support, Datum value, ItemPointer heaptid, bool building);
|
||||
void HnswUpdateNeighborsOnDisk(Relation index, HnswSupport * support, HnswElement e, int m, bool checkExisting, bool building);
|
||||
bool HnswInsertTupleOnDisk(Relation index, Datum value, Datum *values, bool *isnull, ItemPointer heap_tid, bool building);
|
||||
void HnswUpdateNeighborsOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, HnswElement e, int m, bool checkExisting, bool building);
|
||||
void HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool loadHeaptids, bool loadVec);
|
||||
void HnswLoadElement(HnswElement element, double *distance, HnswQuery * q, Relation index, HnswSupport * support, bool loadVec, double *maxDistance);
|
||||
bool HnswFormIndexValue(Datum *out, Datum *values, bool *isnull, const HnswTypeInfo * typeInfo, HnswSupport * support);
|
||||
void HnswLoadElement(HnswElement element, float *distance, Datum *q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec, float *maxDistance);
|
||||
void HnswSetElementTuple(char *base, HnswElementTuple etup, HnswElement element);
|
||||
void HnswUpdateConnection(char *base, HnswNeighborArray * neighbors, HnswElement newElement, float distance, int lm, int *updateIdx, Relation index, HnswSupport * support);
|
||||
bool HnswLoadNeighborTids(HnswElement element, ItemPointerData *indextids, Relation index, int m, int lm, int lc);
|
||||
void HnswUpdateConnection(char *base, HnswElement element, HnswCandidate * hc, int lm, int lc, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation);
|
||||
void HnswLoadNeighbors(HnswElement element, Relation index, int m);
|
||||
void HnswInitLockTranche(void);
|
||||
const HnswTypeInfo *HnswGetTypeInfo(Relation index);
|
||||
PGDLLEXPORT void HnswParallelBuildMain(dsm_segment *seg, shm_toc *toc);
|
||||
|
||||
@@ -366,7 +366,7 @@ AddElementInMemory(char *base, HnswGraph * graph, HnswElement element)
|
||||
* Update neighbors
|
||||
*/
|
||||
static void
|
||||
UpdateNeighborsInMemory(char *base, HnswSupport * support, HnswElement e, int m)
|
||||
UpdateNeighborsInMemory(char *base, FmgrInfo *procinfo, Oid collation, HnswElement e, int m)
|
||||
{
|
||||
for (int lc = e->level; lc >= 0; lc--)
|
||||
{
|
||||
@@ -388,7 +388,7 @@ UpdateNeighborsInMemory(char *base, HnswSupport * support, HnswElement e, int m)
|
||||
Assert(neighborElement);
|
||||
|
||||
LWLockAcquire(&neighborElement->lock, LW_EXCLUSIVE);
|
||||
HnswUpdateConnection(base, HnswGetNeighbors(base, neighborElement, lc), e, hc->distance, lm, NULL, NULL, support);
|
||||
HnswUpdateConnection(base, e, hc, lm, lc, NULL, NULL, procinfo, collation);
|
||||
LWLockRelease(&neighborElement->lock);
|
||||
}
|
||||
}
|
||||
@@ -398,7 +398,7 @@ UpdateNeighborsInMemory(char *base, HnswSupport * support, HnswElement e, int m)
|
||||
* Update graph in memory
|
||||
*/
|
||||
static void
|
||||
UpdateGraphInMemory(HnswSupport * support, HnswElement element, int m, int efConstruction, HnswElement entryPoint, HnswBuildState * buildstate)
|
||||
UpdateGraphInMemory(FmgrInfo *procinfo, Oid collation, HnswElement element, int m, int efConstruction, HnswElement entryPoint, HnswBuildState * buildstate)
|
||||
{
|
||||
HnswGraph *graph = buildstate->graph;
|
||||
char *base = buildstate->hnswarea;
|
||||
@@ -411,7 +411,7 @@ UpdateGraphInMemory(HnswSupport * support, HnswElement element, int m, int efCon
|
||||
AddElementInMemory(base, graph, element);
|
||||
|
||||
/* Update neighbors */
|
||||
UpdateNeighborsInMemory(base, support, element, m);
|
||||
UpdateNeighborsInMemory(base, procinfo, collation, element, m);
|
||||
|
||||
/* Update entry point if needed (already have lock) */
|
||||
if (entryPoint == NULL || element->level > entryPoint->level)
|
||||
@@ -424,8 +424,9 @@ UpdateGraphInMemory(HnswSupport * support, HnswElement element, int m, int efCon
|
||||
static void
|
||||
InsertTupleInMemory(HnswBuildState * buildstate, HnswElement element)
|
||||
{
|
||||
FmgrInfo *procinfo = buildstate->procinfo;
|
||||
Oid collation = buildstate->collation;
|
||||
HnswGraph *graph = buildstate->graph;
|
||||
HnswSupport *support = &buildstate->support;
|
||||
HnswElement entryPoint;
|
||||
LWLock *entryLock = &graph->entryLock;
|
||||
LWLock *entryWaitLock = &graph->entryWaitLock;
|
||||
@@ -457,10 +458,10 @@ InsertTupleInMemory(HnswBuildState * buildstate, HnswElement element)
|
||||
}
|
||||
|
||||
/* Find neighbors for element */
|
||||
HnswFindElementNeighbors(base, element, entryPoint, NULL, support, m, efConstruction, false);
|
||||
HnswFindElementNeighbors(base, element, entryPoint, NULL, procinfo, collation, m, efConstruction, false);
|
||||
|
||||
/* Update graph in memory */
|
||||
UpdateGraphInMemory(support, element, m, efConstruction, entryPoint, buildstate);
|
||||
UpdateGraphInMemory(procinfo, collation, element, m, efConstruction, entryPoint, buildstate);
|
||||
|
||||
/* Release entry lock */
|
||||
LWLockRelease(entryLock);
|
||||
@@ -472,20 +473,31 @@ InsertTupleInMemory(HnswBuildState * buildstate, HnswElement element)
|
||||
static bool
|
||||
InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid, HnswBuildState * buildstate)
|
||||
{
|
||||
const HnswTypeInfo *typeInfo = buildstate->typeInfo;
|
||||
HnswGraph *graph = buildstate->graph;
|
||||
HnswElement element;
|
||||
HnswAllocator *allocator = &buildstate->allocator;
|
||||
HnswSupport *support = &buildstate->support;
|
||||
Size valueSize;
|
||||
Pointer valuePtr;
|
||||
LWLock *flushLock = &graph->flushLock;
|
||||
char *base = buildstate->hnswarea;
|
||||
Datum value;
|
||||
|
||||
/* Form index value */
|
||||
if (!HnswFormIndexValue(&value, values, isnull, buildstate->typeInfo, support))
|
||||
/* Detoast once for all calls */
|
||||
Datum value = PointerGetDatum(PG_DETOAST_DATUM(values[0]));
|
||||
|
||||
/* Check value */
|
||||
if (typeInfo->checkValue != NULL)
|
||||
typeInfo->checkValue(DatumGetPointer(value));
|
||||
|
||||
/* Normalize if needed */
|
||||
if (buildstate->normprocinfo != NULL)
|
||||
{
|
||||
if (!HnswCheckNorm(buildstate->normprocinfo, buildstate->collation, value))
|
||||
return false;
|
||||
|
||||
value = HnswNormValue(typeInfo, buildstate->collation, value);
|
||||
}
|
||||
|
||||
/* Get datum size */
|
||||
valueSize = VARSIZE_ANY(DatumGetPointer(value));
|
||||
|
||||
@@ -497,7 +509,7 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid, Hn
|
||||
{
|
||||
LWLockRelease(flushLock);
|
||||
|
||||
return HnswInsertTupleOnDisk(index, support, value, heaptid, true);
|
||||
return HnswInsertTupleOnDisk(index, value, values, isnull, heaptid, true);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -529,7 +541,7 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid, Hn
|
||||
|
||||
LWLockRelease(flushLock);
|
||||
|
||||
return HnswInsertTupleOnDisk(index, support, value, heaptid, true);
|
||||
return HnswInsertTupleOnDisk(index, value, values, isnull, heaptid, true);
|
||||
}
|
||||
|
||||
/* Ok, we can proceed to allocate the element */
|
||||
@@ -595,7 +607,7 @@ BuildCallback(Relation index, ItemPointer tid, Datum *values,
|
||||
* Initialize the graph
|
||||
*/
|
||||
static void
|
||||
InitGraph(HnswGraph * graph, char *base, Size memoryTotal)
|
||||
InitGraph(HnswGraph * graph, char *base, long memoryTotal)
|
||||
{
|
||||
/* Initialize the lock tranche if needed */
|
||||
HnswInitLockTranche();
|
||||
@@ -692,9 +704,11 @@ InitBuildState(HnswBuildState * buildstate, Relation heap, Relation index, Index
|
||||
buildstate->indtuples = 0;
|
||||
|
||||
/* Get support functions */
|
||||
HnswInitSupport(&buildstate->support, index);
|
||||
buildstate->procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC);
|
||||
buildstate->normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC);
|
||||
buildstate->collation = index->rd_indcollation[0];
|
||||
|
||||
InitGraph(&buildstate->graphData, NULL, (Size) maintenance_work_mem * 1024L);
|
||||
InitGraph(&buildstate->graphData, NULL, maintenance_work_mem * 1024L);
|
||||
buildstate->graph = &buildstate->graphData;
|
||||
buildstate->ml = HnswGetMl(buildstate->m);
|
||||
buildstate->maxLevel = HnswGetMaxLevel(buildstate->m);
|
||||
|
||||
237
src/hnswinsert.c
237
src/hnswinsert.c
@@ -36,7 +36,7 @@ GetInsertPage(Relation index)
|
||||
* Check for a free offset
|
||||
*/
|
||||
static bool
|
||||
HnswFreeOffset(Relation index, Buffer buf, Page page, HnswElement element, Size etupSize, Size ntupSize, Buffer *nbuf, Page *npage, OffsetNumber *freeOffno, OffsetNumber *freeNeighborOffno, BlockNumber *newInsertPage, uint8 *tupleVersion)
|
||||
HnswFreeOffset(Relation index, Buffer buf, Page page, HnswElement element, Size etupSize, Size ntupSize, Buffer *nbuf, Page *npage, OffsetNumber *freeOffno, OffsetNumber *freeNeighborOffno, BlockNumber *newInsertPage)
|
||||
{
|
||||
OffsetNumber offno;
|
||||
OffsetNumber maxoffno = PageGetMaxOffsetNumber(page);
|
||||
@@ -98,7 +98,6 @@ HnswFreeOffset(Relation index, Buffer buf, Page page, HnswElement element, Size
|
||||
{
|
||||
*freeOffno = offno;
|
||||
*freeNeighborOffno = neighborOffno;
|
||||
*tupleVersion = etup->version;
|
||||
return true;
|
||||
}
|
||||
else if (*nbuf != buf)
|
||||
@@ -154,7 +153,6 @@ AddElementOnDisk(Relation index, HnswElement e, int m, BlockNumber insertPage, B
|
||||
OffsetNumber freeOffno = InvalidOffsetNumber;
|
||||
OffsetNumber freeNeighborOffno = InvalidOffsetNumber;
|
||||
BlockNumber newInsertPage = InvalidBlockNumber;
|
||||
uint8 tupleVersion;
|
||||
char *base = NULL;
|
||||
|
||||
/* Calculate sizes */
|
||||
@@ -204,7 +202,7 @@ AddElementOnDisk(Relation index, HnswElement e, int m, BlockNumber insertPage, B
|
||||
}
|
||||
|
||||
/* Next, try space from a deleted element */
|
||||
if (HnswFreeOffset(index, buf, page, e, etupSize, ntupSize, &nbuf, &npage, &freeOffno, &freeNeighborOffno, &newInsertPage, &tupleVersion))
|
||||
if (HnswFreeOffset(index, buf, page, e, etupSize, ntupSize, &nbuf, &npage, &freeOffno, &freeNeighborOffno, &newInsertPage))
|
||||
{
|
||||
if (nbuf != buf)
|
||||
{
|
||||
@@ -214,10 +212,6 @@ AddElementOnDisk(Relation index, HnswElement e, int m, BlockNumber insertPage, B
|
||||
npage = GenericXLogRegisterBuffer(state, nbuf, 0);
|
||||
}
|
||||
|
||||
/* Set tuple version */
|
||||
etup->version = tupleVersion;
|
||||
ntup->version = tupleVersion;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -340,107 +334,6 @@ AddElementOnDisk(Relation index, HnswElement e, int m, BlockNumber insertPage, B
|
||||
*updatedInsertPage = newInsertPage;
|
||||
}
|
||||
|
||||
/*
|
||||
* Load neighbors
|
||||
*/
|
||||
static HnswNeighborArray *
|
||||
HnswLoadNeighbors(HnswElement element, Relation index, int m, int lm, int lc)
|
||||
{
|
||||
char *base = NULL;
|
||||
HnswNeighborArray *neighbors = HnswInitNeighborArray(lm, NULL);
|
||||
ItemPointerData indextids[HNSW_MAX_M * 2];
|
||||
|
||||
if (!HnswLoadNeighborTids(element, indextids, index, m, lm, lc))
|
||||
return neighbors;
|
||||
|
||||
for (int i = 0; i < lm; i++)
|
||||
{
|
||||
ItemPointer indextid = &indextids[i];
|
||||
HnswElement e;
|
||||
HnswCandidate *hc;
|
||||
|
||||
if (!ItemPointerIsValid(indextid))
|
||||
break;
|
||||
|
||||
e = HnswInitElementFromBlock(ItemPointerGetBlockNumber(indextid), ItemPointerGetOffsetNumber(indextid));
|
||||
hc = &neighbors->items[neighbors->length++];
|
||||
HnswPtrStore(base, hc->element, e);
|
||||
}
|
||||
|
||||
return neighbors;
|
||||
}
|
||||
|
||||
/*
|
||||
* Load elements for insert
|
||||
*/
|
||||
static void
|
||||
LoadElementsForInsert(HnswNeighborArray * neighbors, HnswQuery * q, int *idx, Relation index, HnswSupport * support)
|
||||
{
|
||||
char *base = NULL;
|
||||
|
||||
for (int i = 0; i < neighbors->length; i++)
|
||||
{
|
||||
HnswCandidate *hc = &neighbors->items[i];
|
||||
HnswElement element = HnswPtrAccess(base, hc->element);
|
||||
double distance;
|
||||
|
||||
HnswLoadElement(element, &distance, q, index, support, true, NULL);
|
||||
hc->distance = distance;
|
||||
|
||||
/* Prune element if being deleted */
|
||||
if (element->heaptidsLength == 0)
|
||||
{
|
||||
*idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Get update index
|
||||
*/
|
||||
static int
|
||||
GetUpdateIndex(HnswElement element, HnswElement newElement, float distance, int m, int lm, int lc, Relation index, HnswSupport * support, MemoryContext updateCtx)
|
||||
{
|
||||
char *base = NULL;
|
||||
int idx = -1;
|
||||
HnswNeighborArray *neighbors;
|
||||
MemoryContext oldCtx = MemoryContextSwitchTo(updateCtx);
|
||||
|
||||
/*
|
||||
* Get latest neighbors since they may have changed. Do not lock yet since
|
||||
* selecting neighbors can take time. Could use optimistic locking to
|
||||
* retry if another update occurs before getting exclusive lock.
|
||||
*/
|
||||
neighbors = HnswLoadNeighbors(element, index, m, lm, lc);
|
||||
|
||||
/*
|
||||
* Could improve performance for vacuuming by checking neighbors against
|
||||
* list of elements being deleted to find index. It's important to exclude
|
||||
* already deleted elements for this since they can be replaced at any
|
||||
* time.
|
||||
*/
|
||||
|
||||
if (neighbors->length < lm)
|
||||
idx = -2;
|
||||
else
|
||||
{
|
||||
HnswQuery q;
|
||||
|
||||
q.value = HnswGetValue(base, element);
|
||||
|
||||
LoadElementsForInsert(neighbors, &q, &idx, index, support);
|
||||
|
||||
if (idx == -1)
|
||||
HnswUpdateConnection(base, neighbors, newElement, distance, lm, &idx, index, support);
|
||||
}
|
||||
|
||||
MemoryContextSwitchTo(oldCtx);
|
||||
MemoryContextReset(updateCtx);
|
||||
|
||||
return idx;
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if connection already exists
|
||||
*/
|
||||
@@ -462,20 +355,54 @@ ConnectionExists(HnswElement e, HnswNeighborTuple ntup, int startIdx, int lm)
|
||||
}
|
||||
|
||||
/*
|
||||
* Update neighbor
|
||||
* Update neighbors
|
||||
*/
|
||||
static void
|
||||
UpdateNeighborOnDisk(HnswElement element, HnswElement newElement, int idx, int m, int lm, int lc, Relation index, bool checkExisting, bool building)
|
||||
void
|
||||
HnswUpdateNeighborsOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, HnswElement e, int m, bool checkExisting, bool building)
|
||||
{
|
||||
char *base = NULL;
|
||||
|
||||
for (int lc = e->level; lc >= 0; lc--)
|
||||
{
|
||||
int lm = HnswGetLayerM(m, lc);
|
||||
HnswNeighborArray *neighbors = HnswGetNeighbors(base, e, lc);
|
||||
|
||||
for (int i = 0; i < neighbors->length; i++)
|
||||
{
|
||||
HnswCandidate *hc = &neighbors->items[i];
|
||||
Buffer buf;
|
||||
Page page;
|
||||
GenericXLogState *state;
|
||||
HnswNeighborTuple ntup;
|
||||
int idx = -1;
|
||||
int startIdx;
|
||||
OffsetNumber offno = element->neighborOffno;
|
||||
HnswElement neighborElement = HnswPtrAccess(base, hc->element);
|
||||
OffsetNumber offno = neighborElement->neighborOffno;
|
||||
|
||||
/*
|
||||
* Get latest neighbors since they may have changed. Do not lock
|
||||
* yet since selecting neighbors can take time. Could use
|
||||
* optimistic locking to retry if another update occurs before
|
||||
* getting exclusive lock.
|
||||
*/
|
||||
HnswLoadNeighbors(neighborElement, index, m);
|
||||
|
||||
/*
|
||||
* Could improve performance for vacuuming by checking neighbors
|
||||
* against list of elements being deleted to find index. It's
|
||||
* important to exclude already deleted elements for this since
|
||||
* they can be replaced at any time.
|
||||
*/
|
||||
|
||||
/* Select neighbors */
|
||||
HnswUpdateConnection(NULL, e, hc, lm, lc, &idx, index, procinfo, collation);
|
||||
|
||||
/* New element was not selected as a neighbor */
|
||||
if (idx == -1)
|
||||
continue;
|
||||
|
||||
/* Register page */
|
||||
buf = ReadBuffer(index, element->neighborPage);
|
||||
buf = ReadBuffer(index, neighborElement->neighborPage);
|
||||
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
|
||||
if (building)
|
||||
{
|
||||
@@ -492,10 +419,10 @@ UpdateNeighborOnDisk(HnswElement element, HnswElement newElement, int idx, int m
|
||||
ntup = (HnswNeighborTuple) PageGetItem(page, PageGetItemId(page, offno));
|
||||
|
||||
/* Calculate index for update */
|
||||
startIdx = (element->level - lc) * m;
|
||||
startIdx = (neighborElement->level - lc) * m;
|
||||
|
||||
/* Check for existing connection */
|
||||
if (checkExisting && ConnectionExists(newElement, ntup, startIdx, lm))
|
||||
if (checkExisting && ConnectionExists(e, ntup, startIdx, lm))
|
||||
idx = -1;
|
||||
else if (idx == -2)
|
||||
{
|
||||
@@ -519,7 +446,7 @@ UpdateNeighborOnDisk(HnswElement element, HnswElement newElement, int idx, int m
|
||||
ItemPointer indextid = &ntup->indextids[idx];
|
||||
|
||||
/* Update neighbor on the buffer */
|
||||
ItemPointerSet(indextid, newElement->blkno, newElement->offno);
|
||||
ItemPointerSet(indextid, e->blkno, e->offno);
|
||||
|
||||
/* Commit */
|
||||
if (building)
|
||||
@@ -532,47 +459,9 @@ UpdateNeighborOnDisk(HnswElement element, HnswElement newElement, int idx, int m
|
||||
|
||||
UnlockReleaseBuffer(buf);
|
||||
}
|
||||
|
||||
/*
|
||||
* Update neighbors
|
||||
*/
|
||||
void
|
||||
HnswUpdateNeighborsOnDisk(Relation index, HnswSupport * support, HnswElement e, int m, bool checkExisting, bool building)
|
||||
{
|
||||
char *base = NULL;
|
||||
|
||||
/* Use separate memory context to improve performance for larger vectors */
|
||||
MemoryContext updateCtx = GenerationContextCreate(CurrentMemoryContext,
|
||||
"Hnsw insert update context",
|
||||
#if PG_VERSION_NUM >= 150000
|
||||
128 * 1024, 128 * 1024,
|
||||
#endif
|
||||
128 * 1024);
|
||||
|
||||
for (int lc = e->level; lc >= 0; lc--)
|
||||
{
|
||||
int lm = HnswGetLayerM(m, lc);
|
||||
HnswNeighborArray *neighbors = HnswGetNeighbors(base, e, lc);
|
||||
|
||||
for (int i = 0; i < neighbors->length; i++)
|
||||
{
|
||||
HnswCandidate *hc = &neighbors->items[i];
|
||||
HnswElement neighborElement = HnswPtrAccess(base, hc->element);
|
||||
int idx;
|
||||
|
||||
idx = GetUpdateIndex(neighborElement, e, hc->distance, m, lm, lc, index, support, updateCtx);
|
||||
|
||||
/* New element was not selected as a neighbor */
|
||||
if (idx == -1)
|
||||
continue;
|
||||
|
||||
UpdateNeighborOnDisk(neighborElement, e, idx, m, lm, lc, index, checkExisting, building);
|
||||
}
|
||||
}
|
||||
|
||||
MemoryContextDelete(updateCtx);
|
||||
}
|
||||
|
||||
/*
|
||||
* Add a heap TID to an existing element
|
||||
*/
|
||||
@@ -660,7 +549,7 @@ FindDuplicateOnDisk(Relation index, HnswElement element, bool building)
|
||||
* Update graph on disk
|
||||
*/
|
||||
static void
|
||||
UpdateGraphOnDisk(Relation index, HnswSupport * support, HnswElement element, int m, int efConstruction, HnswElement entryPoint, bool building)
|
||||
UpdateGraphOnDisk(Relation index, FmgrInfo *procinfo, Oid collation, HnswElement element, int m, int efConstruction, HnswElement entryPoint, bool building)
|
||||
{
|
||||
BlockNumber newInsertPage = InvalidBlockNumber;
|
||||
|
||||
@@ -676,7 +565,7 @@ UpdateGraphOnDisk(Relation index, HnswSupport * support, HnswElement element, in
|
||||
HnswUpdateMetaPage(index, 0, NULL, newInsertPage, MAIN_FORKNUM, building);
|
||||
|
||||
/* Update neighbors */
|
||||
HnswUpdateNeighborsOnDisk(index, support, element, m, false, building);
|
||||
HnswUpdateNeighborsOnDisk(index, procinfo, collation, element, m, false, building);
|
||||
|
||||
/* Update entry point if needed */
|
||||
if (entryPoint == NULL || element->level > entryPoint->level)
|
||||
@@ -687,12 +576,14 @@ UpdateGraphOnDisk(Relation index, HnswSupport * support, HnswElement element, in
|
||||
* Insert a tuple into the index
|
||||
*/
|
||||
bool
|
||||
HnswInsertTupleOnDisk(Relation index, HnswSupport * support, Datum value, ItemPointer heaptid, bool building)
|
||||
HnswInsertTupleOnDisk(Relation index, Datum value, Datum *values, bool *isnull, ItemPointer heap_tid, bool building)
|
||||
{
|
||||
HnswElement entryPoint;
|
||||
HnswElement element;
|
||||
int m;
|
||||
int efConstruction = HnswGetEfConstruction(index);
|
||||
FmgrInfo *procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC);
|
||||
Oid collation = index->rd_indcollation[0];
|
||||
LOCKMODE lockmode = ShareLock;
|
||||
char *base = NULL;
|
||||
|
||||
@@ -707,7 +598,7 @@ HnswInsertTupleOnDisk(Relation index, HnswSupport * support, Datum value, ItemPo
|
||||
HnswGetMetaPageInfo(index, &m, &entryPoint);
|
||||
|
||||
/* Create an element */
|
||||
element = HnswInitElement(base, heaptid, m, HnswGetMl(m), HnswGetMaxLevel(m), NULL);
|
||||
element = HnswInitElement(base, heap_tid, m, HnswGetMl(m), HnswGetMaxLevel(m), NULL);
|
||||
HnswPtrStore(base, element->value, DatumGetPointer(value));
|
||||
|
||||
/* Prevent concurrent inserts when likely updating entry point */
|
||||
@@ -725,10 +616,10 @@ HnswInsertTupleOnDisk(Relation index, HnswSupport * support, Datum value, ItemPo
|
||||
}
|
||||
|
||||
/* Find neighbors for element */
|
||||
HnswFindElementNeighbors(base, element, entryPoint, index, support, m, efConstruction, false);
|
||||
HnswFindElementNeighbors(base, element, entryPoint, index, procinfo, collation, m, efConstruction, false);
|
||||
|
||||
/* Update graph on disk */
|
||||
UpdateGraphOnDisk(index, support, element, m, efConstruction, entryPoint, building);
|
||||
UpdateGraphOnDisk(index, procinfo, collation, element, m, efConstruction, entryPoint, building);
|
||||
|
||||
/* Release lock */
|
||||
UnlockPage(index, HNSW_UPDATE_LOCK, lockmode);
|
||||
@@ -740,19 +631,31 @@ HnswInsertTupleOnDisk(Relation index, HnswSupport * support, Datum value, ItemPo
|
||||
* Insert a tuple into the index
|
||||
*/
|
||||
static void
|
||||
HnswInsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heaptid)
|
||||
HnswInsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heap_tid)
|
||||
{
|
||||
Datum value;
|
||||
const HnswTypeInfo *typeInfo = HnswGetTypeInfo(index);
|
||||
HnswSupport support;
|
||||
FmgrInfo *normprocinfo;
|
||||
Oid collation = index->rd_indcollation[0];
|
||||
|
||||
HnswInitSupport(&support, index);
|
||||
/* Detoast once for all calls */
|
||||
value = PointerGetDatum(PG_DETOAST_DATUM(values[0]));
|
||||
|
||||
/* Form index value */
|
||||
if (!HnswFormIndexValue(&value, values, isnull, typeInfo, &support))
|
||||
/* Check value */
|
||||
if (typeInfo->checkValue != NULL)
|
||||
typeInfo->checkValue(DatumGetPointer(value));
|
||||
|
||||
/* Normalize if needed */
|
||||
normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC);
|
||||
if (normprocinfo != NULL)
|
||||
{
|
||||
if (!HnswCheckNorm(normprocinfo, collation, value))
|
||||
return;
|
||||
|
||||
HnswInsertTupleOnDisk(index, &support, value, heaptid, false);
|
||||
value = HnswNormValue(typeInfo, collation, value);
|
||||
}
|
||||
|
||||
HnswInsertTupleOnDisk(index, value, values, isnull, heap_tid, false);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
173
src/hnswscan.c
173
src/hnswscan.c
@@ -5,74 +5,39 @@
|
||||
#include "pgstat.h"
|
||||
#include "storage/bufmgr.h"
|
||||
#include "storage/lmgr.h"
|
||||
#include "utils/float.h"
|
||||
#include "utils/memutils.h"
|
||||
|
||||
/*
|
||||
* Algorithm 5 from paper
|
||||
*/
|
||||
static List *
|
||||
GetScanItems(IndexScanDesc scan, Datum value)
|
||||
GetScanItems(IndexScanDesc scan, Datum q)
|
||||
{
|
||||
HnswScanOpaque so = (HnswScanOpaque) scan->opaque;
|
||||
Relation index = scan->indexRelation;
|
||||
HnswSupport *support = &so->support;
|
||||
FmgrInfo *procinfo = so->procinfo;
|
||||
Oid collation = so->collation;
|
||||
List *ep;
|
||||
List *w;
|
||||
int m;
|
||||
HnswElement entryPoint;
|
||||
char *base = NULL;
|
||||
HnswQuery *q = &so->q;
|
||||
|
||||
/* Get m and entry point */
|
||||
HnswGetMetaPageInfo(index, &m, &entryPoint);
|
||||
|
||||
q->value = value;
|
||||
so->m = m;
|
||||
|
||||
if (entryPoint == NULL)
|
||||
return NIL;
|
||||
|
||||
ep = list_make1(HnswEntryCandidate(base, entryPoint, q, index, support, false));
|
||||
ep = list_make1(HnswEntryCandidate(base, entryPoint, q, index, procinfo, collation, false));
|
||||
|
||||
for (int lc = entryPoint->level; lc >= 1; lc--)
|
||||
{
|
||||
w = HnswSearchLayer(base, q, ep, 1, lc, index, support, m, false, NULL, NULL, NULL, true, NULL);
|
||||
w = HnswSearchLayer(base, q, ep, 1, lc, index, procinfo, collation, m, false, NULL);
|
||||
ep = w;
|
||||
}
|
||||
|
||||
return HnswSearchLayer(base, q, ep, hnsw_ef_search, 0, index, support, m, false, NULL, &so->v, hnsw_iterative_scan != HNSW_ITERATIVE_SCAN_OFF ? &so->discarded : NULL, true, &so->tuples);
|
||||
}
|
||||
|
||||
/*
|
||||
* Resume scan at ground level with discarded candidates
|
||||
*/
|
||||
static List *
|
||||
ResumeScanItems(IndexScanDesc scan)
|
||||
{
|
||||
HnswScanOpaque so = (HnswScanOpaque) scan->opaque;
|
||||
Relation index = scan->indexRelation;
|
||||
List *ep = NIL;
|
||||
char *base = NULL;
|
||||
int batch_size = hnsw_ef_search;
|
||||
|
||||
if (pairingheap_is_empty(so->discarded))
|
||||
return NIL;
|
||||
|
||||
/* Get next batch of candidates */
|
||||
for (int i = 0; i < batch_size; i++)
|
||||
{
|
||||
HnswSearchCandidate *sc;
|
||||
|
||||
if (pairingheap_is_empty(so->discarded))
|
||||
break;
|
||||
|
||||
sc = HnswGetSearchCandidate(w_node, pairingheap_remove_first(so->discarded));
|
||||
|
||||
ep = lappend(ep, sc);
|
||||
}
|
||||
|
||||
return HnswSearchLayer(base, &so->q, ep, batch_size, 0, index, &so->support, so->m, false, NULL, &so->v, &so->discarded, false, &so->tuples);
|
||||
return HnswSearchLayer(base, q, ep, hnsw_ef_search, 0, index, procinfo, collation, m, false, NULL);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -95,24 +60,13 @@ GetScanValue(IndexScanDesc scan)
|
||||
Assert(!VARATT_IS_EXTENDED(DatumGetPointer(value)));
|
||||
|
||||
/* Normalize if needed */
|
||||
if (so->support.normprocinfo != NULL)
|
||||
value = HnswNormValue(so->typeInfo, so->support.collation, value);
|
||||
if (so->normprocinfo != NULL)
|
||||
value = HnswNormValue(so->typeInfo, so->collation, value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
#if defined(HNSW_MEMORY)
|
||||
/*
|
||||
* Show memory usage
|
||||
*/
|
||||
static void
|
||||
ShowMemoryUsage(HnswScanOpaque so)
|
||||
{
|
||||
elog(INFO, "memory: %zu KB, tuples: " INT64_FORMAT, MemoryContextMemAllocated(so->tmpCtx, false) / 1024, so->tuples);
|
||||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Prepare for an index scan
|
||||
*/
|
||||
@@ -121,28 +75,20 @@ hnswbeginscan(Relation index, int nkeys, int norderbys)
|
||||
{
|
||||
IndexScanDesc scan;
|
||||
HnswScanOpaque so;
|
||||
double maxMemory;
|
||||
|
||||
scan = RelationGetIndexScan(index, nkeys, norderbys);
|
||||
|
||||
so = (HnswScanOpaque) palloc(sizeof(HnswScanOpaqueData));
|
||||
so->typeInfo = HnswGetTypeInfo(index);
|
||||
|
||||
/* Set support functions */
|
||||
HnswInitSupport(&so->support, index);
|
||||
|
||||
/*
|
||||
* Use a lower max allocation size than default to allow scanning more
|
||||
* tuples for iterative search before exceeding work_mem
|
||||
*/
|
||||
so->first = true;
|
||||
so->tmpCtx = AllocSetContextCreate(CurrentMemoryContext,
|
||||
"Hnsw scan temporary context",
|
||||
0, 8 * 1024, 256 * 1024);
|
||||
ALLOCSET_DEFAULT_SIZES);
|
||||
|
||||
/* Calculate max memory */
|
||||
/* Add 256 extra bytes to fill last block when close */
|
||||
maxMemory = (double) work_mem * hnsw_scan_mem_multiplier * 1024.0 + 256;
|
||||
so->maxMemory = Min(maxMemory, (double) SIZE_MAX);
|
||||
/* Set support functions */
|
||||
so->procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC);
|
||||
so->normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC);
|
||||
so->collation = index->rd_indcollation[0];
|
||||
|
||||
scan->opaque = so;
|
||||
|
||||
@@ -158,11 +104,6 @@ hnswrescan(IndexScanDesc scan, ScanKey keys, int nkeys, ScanKey orderbys, int no
|
||||
HnswScanOpaque so = (HnswScanOpaque) scan->opaque;
|
||||
|
||||
so->first = true;
|
||||
/* v and discarded are allocated in tmpCtx */
|
||||
so->v.tids = NULL;
|
||||
so->discarded = NULL;
|
||||
so->tuples = 0;
|
||||
so->previousDistance = -get_float8_infinity();
|
||||
MemoryContextReset(so->tmpCtx);
|
||||
|
||||
if (keys && scan->numberOfKeys > 0)
|
||||
@@ -220,104 +161,26 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir)
|
||||
so->first = false;
|
||||
|
||||
#if defined(HNSW_MEMORY)
|
||||
ShowMemoryUsage(so);
|
||||
elog(INFO, "memory: %zu KB", MemoryContextMemAllocated(so->tmpCtx, false) / 1024);
|
||||
#endif
|
||||
}
|
||||
|
||||
for (;;)
|
||||
while (list_length(so->w) > 0)
|
||||
{
|
||||
char *base = NULL;
|
||||
HnswSearchCandidate *sc;
|
||||
HnswElement element;
|
||||
HnswSearchCandidate *hc = llast(so->w);
|
||||
HnswElement element = HnswPtrAccess(base, hc->element);
|
||||
ItemPointer heaptid;
|
||||
|
||||
if (list_length(so->w) == 0)
|
||||
{
|
||||
if (hnsw_iterative_scan == HNSW_ITERATIVE_SCAN_OFF)
|
||||
break;
|
||||
|
||||
/* Empty index */
|
||||
if (so->discarded == NULL)
|
||||
break;
|
||||
|
||||
/* Reached max number of tuples */
|
||||
if (so->tuples >= hnsw_max_scan_tuples)
|
||||
{
|
||||
if (pairingheap_is_empty(so->discarded))
|
||||
break;
|
||||
|
||||
/* Return remaining tuples */
|
||||
so->w = lappend(so->w, HnswGetSearchCandidate(w_node, pairingheap_remove_first(so->discarded)));
|
||||
}
|
||||
/* Prevent scans from consuming too much memory */
|
||||
else if (MemoryContextMemAllocated(so->tmpCtx, false) > so->maxMemory)
|
||||
{
|
||||
if (pairingheap_is_empty(so->discarded))
|
||||
{
|
||||
ereport(DEBUG1,
|
||||
(errmsg("hnsw index scan reached memory limit after " INT64_FORMAT " tuples", so->tuples),
|
||||
errhint("Increase hnsw.scan_mem_multiplier to scan more tuples.")));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
/* Return remaining tuples */
|
||||
so->w = lappend(so->w, HnswGetSearchCandidate(w_node, pairingheap_remove_first(so->discarded)));
|
||||
}
|
||||
else
|
||||
{
|
||||
/*
|
||||
* Locking ensures when neighbors are read, the elements they
|
||||
* reference will not be deleted (and replaced) during the
|
||||
* iteration.
|
||||
*
|
||||
* Elements loaded into memory on previous iterations may have
|
||||
* been deleted (and replaced), so when reading neighbors, the
|
||||
* element version must be checked.
|
||||
*/
|
||||
LockPage(scan->indexRelation, HNSW_SCAN_LOCK, ShareLock);
|
||||
|
||||
so->w = ResumeScanItems(scan);
|
||||
|
||||
UnlockPage(scan->indexRelation, HNSW_SCAN_LOCK, ShareLock);
|
||||
|
||||
#if defined(HNSW_MEMORY)
|
||||
ShowMemoryUsage(so);
|
||||
#endif
|
||||
}
|
||||
|
||||
if (list_length(so->w) == 0)
|
||||
break;
|
||||
}
|
||||
|
||||
sc = llast(so->w);
|
||||
element = HnswPtrAccess(base, sc->element);
|
||||
|
||||
/* Move to next element if no valid heap TIDs */
|
||||
if (element->heaptidsLength == 0)
|
||||
{
|
||||
so->w = list_delete_last(so->w);
|
||||
|
||||
/* Mark memory as free for next iteration */
|
||||
if (hnsw_iterative_scan != HNSW_ITERATIVE_SCAN_OFF)
|
||||
{
|
||||
pfree(element);
|
||||
pfree(sc);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
heaptid = &element->heaptids[--element->heaptidsLength];
|
||||
|
||||
if (hnsw_iterative_scan == HNSW_ITERATIVE_SCAN_STRICT)
|
||||
{
|
||||
if (sc->distance < so->previousDistance)
|
||||
continue;
|
||||
|
||||
so->previousDistance = sc->distance;
|
||||
}
|
||||
|
||||
MemoryContextSwitchTo(oldCtx);
|
||||
|
||||
scan->xs_heaptid = *heaptid;
|
||||
|
||||
456
src/hnswutils.c
456
src/hnswutils.c
@@ -100,6 +100,19 @@ hash_offset(Size offset)
|
||||
#define SH_DEFINE
|
||||
#include "lib/simplehash.h"
|
||||
|
||||
typedef union
|
||||
{
|
||||
pointerhash_hash *pointers;
|
||||
offsethash_hash *offsets;
|
||||
tidhash_hash *tids;
|
||||
} visited_hash;
|
||||
|
||||
typedef union
|
||||
{
|
||||
HnswElement element;
|
||||
ItemPointerData indextid;
|
||||
} HnswUnvisited;
|
||||
|
||||
/*
|
||||
* Get the max number of connections in an upper layer for each element in the index
|
||||
*/
|
||||
@@ -140,17 +153,6 @@ HnswOptionalProcInfo(Relation index, uint16 procnum)
|
||||
return index_getprocinfo(index, 1, procnum);
|
||||
}
|
||||
|
||||
/*
|
||||
* Init support functions
|
||||
*/
|
||||
void
|
||||
HnswInitSupport(HnswSupport * support, Relation index)
|
||||
{
|
||||
support->procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC);
|
||||
support->collation = index->rd_indcollation[0];
|
||||
support->normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC);
|
||||
}
|
||||
|
||||
/*
|
||||
* Normalize value
|
||||
*/
|
||||
@@ -164,9 +166,9 @@ HnswNormValue(const HnswTypeInfo * typeInfo, Oid collation, Datum value)
|
||||
* Check if non-zero norm
|
||||
*/
|
||||
bool
|
||||
HnswCheckNorm(HnswSupport * support, Datum value)
|
||||
HnswCheckNorm(FmgrInfo *procinfo, Oid collation, Datum value)
|
||||
{
|
||||
return DatumGetFloat8(FunctionCall1Coll(support->normprocinfo, support->collation, value)) > 0;
|
||||
return DatumGetFloat8(FunctionCall1Coll(procinfo, collation, value)) > 0;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -195,7 +197,7 @@ HnswInitPage(Buffer buf, Page page)
|
||||
/*
|
||||
* Allocate a neighbor array
|
||||
*/
|
||||
HnswNeighborArray *
|
||||
static HnswNeighborArray *
|
||||
HnswInitNeighborArray(int lm, HnswAllocator * allocator)
|
||||
{
|
||||
HnswNeighborArray *a = HnswAlloc(allocator, HNSW_NEIGHBOR_ARRAY_SIZE(lm));
|
||||
@@ -251,8 +253,6 @@ HnswInitElement(char *base, ItemPointer heaptid, int m, double ml, int maxLevel,
|
||||
|
||||
element->level = level;
|
||||
element->deleted = 0;
|
||||
/* Start at one to make it easier to find issues */
|
||||
element->version = 1;
|
||||
|
||||
HnswInitNeighbors(base, element, m, allocator);
|
||||
|
||||
@@ -394,33 +394,6 @@ HnswUpdateMetaPage(Relation index, int updateEntry, HnswElement entryPoint, Bloc
|
||||
UnlockReleaseBuffer(buf);
|
||||
}
|
||||
|
||||
/*
|
||||
* Form index value
|
||||
*/
|
||||
bool
|
||||
HnswFormIndexValue(Datum *out, Datum *values, bool *isnull, const HnswTypeInfo * typeInfo, HnswSupport * support)
|
||||
{
|
||||
/* Detoast once for all calls */
|
||||
Datum value = PointerGetDatum(PG_DETOAST_DATUM(values[0]));
|
||||
|
||||
/* Check value */
|
||||
if (typeInfo->checkValue != NULL)
|
||||
typeInfo->checkValue(DatumGetPointer(value));
|
||||
|
||||
/* Normalize if needed */
|
||||
if (support->normprocinfo != NULL)
|
||||
{
|
||||
if (!HnswCheckNorm(support, value))
|
||||
return false;
|
||||
|
||||
value = HnswNormValue(typeInfo, support->collation, value);
|
||||
}
|
||||
|
||||
*out = value;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Set element tuple, except for neighbor info
|
||||
*/
|
||||
@@ -432,7 +405,6 @@ HnswSetElementTuple(char *base, HnswElementTuple etup, HnswElement element)
|
||||
etup->type = HNSW_ELEMENT_TUPLE_TYPE;
|
||||
etup->level = element->level;
|
||||
etup->deleted = 0;
|
||||
etup->version = element->version;
|
||||
for (int i = 0; i < HNSW_HEAPTIDS; i++)
|
||||
{
|
||||
if (i < element->heaptidsLength)
|
||||
@@ -475,7 +447,69 @@ HnswSetNeighborTuple(char *base, HnswNeighborTuple ntup, HnswElement e, int m)
|
||||
}
|
||||
|
||||
ntup->count = idx;
|
||||
ntup->version = e->version;
|
||||
}
|
||||
|
||||
/*
|
||||
* Load neighbors from page
|
||||
*/
|
||||
static void
|
||||
LoadNeighborsFromPage(HnswElement element, Relation index, Page page, int m)
|
||||
{
|
||||
char *base = NULL;
|
||||
|
||||
HnswNeighborTuple ntup = (HnswNeighborTuple) PageGetItem(page, PageGetItemId(page, element->neighborOffno));
|
||||
int neighborCount = (element->level + 2) * m;
|
||||
|
||||
Assert(HnswIsNeighborTuple(ntup));
|
||||
|
||||
HnswInitNeighbors(base, element, m, NULL);
|
||||
|
||||
/* Ensure expected neighbors */
|
||||
if (ntup->count != neighborCount)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < neighborCount; i++)
|
||||
{
|
||||
HnswElement e;
|
||||
int level;
|
||||
HnswCandidate *hc;
|
||||
ItemPointer indextid;
|
||||
HnswNeighborArray *neighbors;
|
||||
|
||||
indextid = &ntup->indextids[i];
|
||||
|
||||
if (!ItemPointerIsValid(indextid))
|
||||
continue;
|
||||
|
||||
e = HnswInitElementFromBlock(ItemPointerGetBlockNumber(indextid), ItemPointerGetOffsetNumber(indextid));
|
||||
|
||||
/* Calculate level based on offset */
|
||||
level = element->level - i / m;
|
||||
if (level < 0)
|
||||
level = 0;
|
||||
|
||||
neighbors = HnswGetNeighbors(base, element, level);
|
||||
hc = &neighbors->items[neighbors->length++];
|
||||
HnswPtrStore(base, hc->element, e);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Load neighbors
|
||||
*/
|
||||
void
|
||||
HnswLoadNeighbors(HnswElement element, Relation index, int m)
|
||||
{
|
||||
Buffer buf;
|
||||
Page page;
|
||||
|
||||
buf = ReadBuffer(index, element->neighborPage);
|
||||
LockBuffer(buf, BUFFER_LOCK_SHARE);
|
||||
page = BufferGetPage(buf);
|
||||
|
||||
LoadNeighborsFromPage(element, index, page, m);
|
||||
|
||||
UnlockReleaseBuffer(buf);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -486,7 +520,6 @@ HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool loadHe
|
||||
{
|
||||
element->level = etup->level;
|
||||
element->deleted = etup->deleted;
|
||||
element->version = etup->version;
|
||||
element->neighborPage = ItemPointerGetBlockNumber(&etup->neighbortid);
|
||||
element->neighborOffno = ItemPointerGetOffsetNumber(&etup->neighbortid);
|
||||
element->heaptidsLength = 0;
|
||||
@@ -512,20 +545,11 @@ HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool loadHe
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Calculate the distance between values
|
||||
*/
|
||||
static inline double
|
||||
HnswGetDistance(Datum a, Datum b, HnswSupport * support)
|
||||
{
|
||||
return DatumGetFloat8(FunctionCall2Coll(support->procinfo, support->collation, a, b));
|
||||
}
|
||||
|
||||
/*
|
||||
* Load an element and optionally get its distance from q
|
||||
*/
|
||||
static void
|
||||
HnswLoadElementImpl(BlockNumber blkno, OffsetNumber offno, double *distance, HnswQuery * q, Relation index, HnswSupport * support, bool loadVec, double *maxDistance, HnswElement * element)
|
||||
HnswLoadElementImpl(BlockNumber blkno, OffsetNumber offno, float *distance, Datum *q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec, float *maxDistance, HnswElement * element)
|
||||
{
|
||||
Buffer buf;
|
||||
Page page;
|
||||
@@ -543,10 +567,10 @@ HnswLoadElementImpl(BlockNumber blkno, OffsetNumber offno, double *distance, Hns
|
||||
/* Calculate distance */
|
||||
if (distance != NULL)
|
||||
{
|
||||
if (DatumGetPointer(q->value) == NULL)
|
||||
if (DatumGetPointer(*q) == NULL)
|
||||
*distance = 0;
|
||||
else
|
||||
*distance = HnswGetDistance(q->value, PointerGetDatum(&etup->data), support);
|
||||
*distance = (float) DatumGetFloat8(FunctionCall2Coll(procinfo, collation, *q, PointerGetDatum(&etup->data)));
|
||||
}
|
||||
|
||||
/* Load element */
|
||||
@@ -565,52 +589,41 @@ HnswLoadElementImpl(BlockNumber blkno, OffsetNumber offno, double *distance, Hns
|
||||
* Load an element and optionally get its distance from q
|
||||
*/
|
||||
void
|
||||
HnswLoadElement(HnswElement element, double *distance, HnswQuery * q, Relation index, HnswSupport * support, bool loadVec, double *maxDistance)
|
||||
HnswLoadElement(HnswElement element, float *distance, Datum *q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec, float *maxDistance)
|
||||
{
|
||||
HnswLoadElementImpl(element->blkno, element->offno, distance, q, index, support, loadVec, maxDistance, &element);
|
||||
HnswLoadElementImpl(element->blkno, element->offno, distance, q, index, procinfo, collation, loadVec, maxDistance, &element);
|
||||
}
|
||||
|
||||
/*
|
||||
* Get the distance for an element
|
||||
*/
|
||||
static double
|
||||
GetElementDistance(char *base, HnswElement element, HnswQuery * q, HnswSupport * support)
|
||||
static float
|
||||
GetElementDistance(char *base, HnswElement element, Datum q, FmgrInfo *procinfo, Oid collation)
|
||||
{
|
||||
Datum value = HnswGetValue(base, element);
|
||||
|
||||
return HnswGetDistance(q->value, value, support);
|
||||
}
|
||||
|
||||
/*
|
||||
* Allocate a search candidate
|
||||
*/
|
||||
static HnswSearchCandidate *
|
||||
HnswInitSearchCandidate(char *base, HnswElement element, double distance)
|
||||
{
|
||||
HnswSearchCandidate *sc = palloc(sizeof(HnswSearchCandidate));
|
||||
|
||||
HnswPtrStore(base, sc->element, element);
|
||||
sc->distance = distance;
|
||||
return sc;
|
||||
return DatumGetFloat8(FunctionCall2Coll(procinfo, collation, q, value));
|
||||
}
|
||||
|
||||
/*
|
||||
* Create a candidate for the entry point
|
||||
*/
|
||||
HnswSearchCandidate *
|
||||
HnswEntryCandidate(char *base, HnswElement entryPoint, HnswQuery * q, Relation index, HnswSupport * support, bool loadVec)
|
||||
HnswEntryCandidate(char *base, HnswElement entryPoint, Datum q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec)
|
||||
{
|
||||
bool inMemory = index == NULL;
|
||||
double distance;
|
||||
HnswSearchCandidate *hc = palloc(sizeof(HnswSearchCandidate));
|
||||
|
||||
if (inMemory)
|
||||
distance = GetElementDistance(base, entryPoint, q, support);
|
||||
HnswPtrStore(base, hc->element, entryPoint);
|
||||
if (index == NULL)
|
||||
hc->distance = GetElementDistance(base, entryPoint, q, procinfo, collation);
|
||||
else
|
||||
HnswLoadElement(entryPoint, &distance, q, index, support, loadVec, NULL);
|
||||
|
||||
return HnswInitSearchCandidate(base, entryPoint, distance);
|
||||
HnswLoadElement(entryPoint, &hc->distance, &q, index, procinfo, collation, loadVec, NULL);
|
||||
return hc;
|
||||
}
|
||||
|
||||
#define HnswGetSearchCandidate(membername, ptr) pairingheap_container(HnswSearchCandidate, membername, ptr)
|
||||
#define HnswGetSearchCandidateConst(membername, ptr) pairingheap_const_container(HnswSearchCandidate, membername, ptr)
|
||||
|
||||
/*
|
||||
* Compare candidate distances
|
||||
*/
|
||||
@@ -626,21 +639,6 @@ CompareNearestCandidates(const pairingheap_node *a, const pairingheap_node *b, v
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Compare discarded candidate distances
|
||||
*/
|
||||
static int
|
||||
CompareNearestDiscardedCandidates(const pairingheap_node *a, const pairingheap_node *b, void *arg)
|
||||
{
|
||||
if (HnswGetSearchCandidateConst(w_node, a)->distance < HnswGetSearchCandidateConst(w_node, b)->distance)
|
||||
return 1;
|
||||
|
||||
if (HnswGetSearchCandidateConst(w_node, a)->distance > HnswGetSearchCandidateConst(w_node, b)->distance)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Compare candidate distances
|
||||
*/
|
||||
@@ -660,9 +658,9 @@ CompareFurthestCandidates(const pairingheap_node *a, const pairingheap_node *b,
|
||||
* Init visited
|
||||
*/
|
||||
static inline void
|
||||
InitVisited(char *base, visited_hash * v, bool inMemory, int ef, int m)
|
||||
InitVisited(char *base, visited_hash * v, Relation index, int ef, int m)
|
||||
{
|
||||
if (!inMemory)
|
||||
if (index != NULL)
|
||||
v->tids = tidhash_create(CurrentMemoryContext, ef * m * 2, NULL);
|
||||
else if (base != NULL)
|
||||
v->offsets = offsethash_create(CurrentMemoryContext, ef * m * 2, NULL);
|
||||
@@ -674,9 +672,9 @@ InitVisited(char *base, visited_hash * v, bool inMemory, int ef, int m)
|
||||
* Add to visited
|
||||
*/
|
||||
static inline void
|
||||
AddToVisited(char *base, visited_hash * v, HnswElementPtr elementPtr, bool inMemory, bool *found)
|
||||
AddToVisited(char *base, visited_hash * v, HnswElementPtr elementPtr, Relation index, bool *found)
|
||||
{
|
||||
if (!inMemory)
|
||||
if (index != NULL)
|
||||
{
|
||||
HnswElement element = HnswPtrAccess(base, elementPtr);
|
||||
ItemPointerData indextid;
|
||||
@@ -737,60 +735,38 @@ HnswLoadUnvisitedFromMemory(char *base, HnswElement element, HnswUnvisited * unv
|
||||
HnswCandidate *hc = &localNeighborhood->items[i];
|
||||
bool found;
|
||||
|
||||
AddToVisited(base, v, hc->element, true, &found);
|
||||
AddToVisited(base, v, hc->element, NULL, &found);
|
||||
|
||||
if (!found)
|
||||
unvisited[(*unvisitedLength)++].element = HnswPtrAccess(base, hc->element);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Load neighbor index TIDs
|
||||
*/
|
||||
bool
|
||||
HnswLoadNeighborTids(HnswElement element, ItemPointerData *indextids, Relation index, int m, int lm, int lc)
|
||||
{
|
||||
Buffer buf;
|
||||
Page page;
|
||||
HnswNeighborTuple ntup;
|
||||
int start;
|
||||
|
||||
buf = ReadBuffer(index, element->neighborPage);
|
||||
LockBuffer(buf, BUFFER_LOCK_SHARE);
|
||||
page = BufferGetPage(buf);
|
||||
|
||||
ntup = (HnswNeighborTuple) PageGetItem(page, PageGetItemId(page, element->neighborOffno));
|
||||
|
||||
/*
|
||||
* Ensure the neighbor tuple has not been deleted or replaced between
|
||||
* index scan iterations
|
||||
*/
|
||||
if (ntup->version != element->version || ntup->count != (element->level + 2) * m)
|
||||
{
|
||||
UnlockReleaseBuffer(buf);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Copy to minimize lock time */
|
||||
start = (element->level - lc) * m;
|
||||
memcpy(indextids, ntup->indextids + start, lm * sizeof(ItemPointerData));
|
||||
|
||||
UnlockReleaseBuffer(buf);
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Load unvisited neighbors from disk
|
||||
*/
|
||||
static void
|
||||
HnswLoadUnvisitedFromDisk(HnswElement element, HnswUnvisited * unvisited, int *unvisitedLength, visited_hash * v, Relation index, int m, int lm, int lc)
|
||||
{
|
||||
Buffer buf;
|
||||
Page page;
|
||||
HnswNeighborTuple ntup;
|
||||
int start;
|
||||
ItemPointerData indextids[HNSW_MAX_M * 2];
|
||||
|
||||
*unvisitedLength = 0;
|
||||
buf = ReadBuffer(index, element->neighborPage);
|
||||
LockBuffer(buf, BUFFER_LOCK_SHARE);
|
||||
page = BufferGetPage(buf);
|
||||
|
||||
if (!HnswLoadNeighborTids(element, indextids, index, m, lm, lc))
|
||||
return;
|
||||
ntup = (HnswNeighborTuple) PageGetItem(page, PageGetItemId(page, element->neighborOffno));
|
||||
start = (element->level - lc) * m;
|
||||
|
||||
/* Copy to minimize lock time */
|
||||
memcpy(&indextids, ntup->indextids + start, lm * sizeof(ItemPointerData));
|
||||
|
||||
UnlockReleaseBuffer(buf);
|
||||
|
||||
*unvisitedLength = 0;
|
||||
|
||||
for (int i = 0; i < lm; i++)
|
||||
{
|
||||
@@ -811,37 +787,24 @@ HnswLoadUnvisitedFromDisk(HnswElement element, HnswUnvisited * unvisited, int *u
|
||||
* Algorithm 2 from paper
|
||||
*/
|
||||
List *
|
||||
HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation index, HnswSupport * support, int m, bool inserting, HnswElement skipElement, visited_hash * v, pairingheap **discarded, bool initVisited, int64 *tuples)
|
||||
HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, FmgrInfo *procinfo, Oid collation, int m, bool inserting, HnswElement skipElement)
|
||||
{
|
||||
List *w = NIL;
|
||||
pairingheap *C = pairingheap_allocate(CompareNearestCandidates, NULL);
|
||||
pairingheap *W = pairingheap_allocate(CompareFurthestCandidates, NULL);
|
||||
int wlen = 0;
|
||||
visited_hash vh;
|
||||
visited_hash v;
|
||||
ListCell *lc2;
|
||||
HnswNeighborArray *localNeighborhood = NULL;
|
||||
Size neighborhoodSize = 0;
|
||||
int lm = HnswGetLayerM(m, lc);
|
||||
HnswUnvisited *unvisited = palloc(lm * sizeof(HnswUnvisited));
|
||||
int unvisitedLength;
|
||||
bool inMemory = index == NULL;
|
||||
|
||||
if (v == NULL)
|
||||
{
|
||||
v = &vh;
|
||||
initVisited = true;
|
||||
}
|
||||
|
||||
if (initVisited)
|
||||
{
|
||||
InitVisited(base, v, inMemory, ef, m);
|
||||
|
||||
if (discarded != NULL)
|
||||
*discarded = pairingheap_allocate(CompareNearestDiscardedCandidates, NULL);
|
||||
}
|
||||
InitVisited(base, &v, index, ef, m);
|
||||
|
||||
/* Create local memory for neighborhood if needed */
|
||||
if (inMemory)
|
||||
if (index == NULL)
|
||||
{
|
||||
neighborhoodSize = HNSW_NEIGHBOR_ARRAY_SIZE(lm);
|
||||
localNeighborhood = palloc(neighborhoodSize);
|
||||
@@ -850,27 +813,20 @@ HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation in
|
||||
/* Add entry points to v, C, and W */
|
||||
foreach(lc2, ep)
|
||||
{
|
||||
HnswSearchCandidate *sc = (HnswSearchCandidate *) lfirst(lc2);
|
||||
HnswSearchCandidate *hc = (HnswSearchCandidate *) lfirst(lc2);
|
||||
bool found;
|
||||
|
||||
if (initVisited)
|
||||
{
|
||||
AddToVisited(base, v, sc->element, inMemory, &found);
|
||||
AddToVisited(base, &v, hc->element, index, &found);
|
||||
|
||||
/* OK to count elements instead of tuples */
|
||||
if (tuples != NULL)
|
||||
(*tuples)++;
|
||||
}
|
||||
|
||||
pairingheap_add(C, &sc->c_node);
|
||||
pairingheap_add(W, &sc->w_node);
|
||||
pairingheap_add(C, &hc->c_node);
|
||||
pairingheap_add(W, &hc->w_node);
|
||||
|
||||
/*
|
||||
* Do not count elements being deleted towards ef when vacuuming. It
|
||||
* would be ideal to do this for inserts as well, but this could
|
||||
* affect insert performance.
|
||||
*/
|
||||
if (CountElement(skipElement, HnswPtrAccess(base, sc->element)))
|
||||
if (CountElement(skipElement, HnswPtrAccess(base, hc->element)))
|
||||
wlen++;
|
||||
}
|
||||
|
||||
@@ -885,28 +841,24 @@ HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation in
|
||||
|
||||
cElement = HnswPtrAccess(base, c->element);
|
||||
|
||||
if (inMemory)
|
||||
HnswLoadUnvisitedFromMemory(base, cElement, unvisited, &unvisitedLength, v, lc, localNeighborhood, neighborhoodSize);
|
||||
if (index == NULL)
|
||||
HnswLoadUnvisitedFromMemory(base, cElement, unvisited, &unvisitedLength, &v, lc, localNeighborhood, neighborhoodSize);
|
||||
else
|
||||
HnswLoadUnvisitedFromDisk(cElement, unvisited, &unvisitedLength, v, index, m, lm, lc);
|
||||
|
||||
/* OK to count elements instead of tuples */
|
||||
if (tuples != NULL)
|
||||
(*tuples) += unvisitedLength;
|
||||
HnswLoadUnvisitedFromDisk(cElement, unvisited, &unvisitedLength, &v, index, m, lm, lc);
|
||||
|
||||
for (int i = 0; i < unvisitedLength; i++)
|
||||
{
|
||||
HnswElement eElement;
|
||||
HnswSearchCandidate *e;
|
||||
double eDistance;
|
||||
float eDistance;
|
||||
bool alwaysAdd = wlen < ef;
|
||||
|
||||
f = HnswGetSearchCandidate(w_node, pairingheap_first(W));
|
||||
|
||||
if (inMemory)
|
||||
if (index == NULL)
|
||||
{
|
||||
eElement = unvisited[i].element;
|
||||
eDistance = GetElementDistance(base, eElement, q, support);
|
||||
eDistance = GetElementDistance(base, eElement, q, procinfo, collation);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -916,30 +868,25 @@ HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation in
|
||||
|
||||
/* Avoid any allocations if not adding */
|
||||
eElement = NULL;
|
||||
HnswLoadElementImpl(blkno, offno, &eDistance, q, index, support, inserting, alwaysAdd || discarded != NULL ? NULL : &f->distance, &eElement);
|
||||
HnswLoadElementImpl(blkno, offno, &eDistance, &q, index, procinfo, collation, inserting, alwaysAdd ? NULL : &f->distance, &eElement);
|
||||
|
||||
if (eElement == NULL)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (eElement == NULL || !(eDistance < f->distance || alwaysAdd))
|
||||
{
|
||||
if (discarded != NULL)
|
||||
{
|
||||
/* Create a new candidate */
|
||||
e = HnswInitSearchCandidate(base, eElement, eDistance);
|
||||
pairingheap_add(*discarded, &e->w_node);
|
||||
}
|
||||
|
||||
if (!(eDistance < f->distance || alwaysAdd))
|
||||
continue;
|
||||
}
|
||||
|
||||
Assert(!eElement->deleted);
|
||||
|
||||
/* Make robust to issues */
|
||||
if (eElement->level < lc)
|
||||
continue;
|
||||
|
||||
/* Create a new candidate */
|
||||
e = HnswInitSearchCandidate(base, eElement, eDistance);
|
||||
e = palloc(sizeof(HnswSearchCandidate));
|
||||
HnswPtrStore(base, e->element, eElement);
|
||||
e->distance = eDistance;
|
||||
pairingheap_add(C, &e->c_node);
|
||||
pairingheap_add(W, &e->w_node);
|
||||
|
||||
@@ -954,12 +901,7 @@ HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation in
|
||||
|
||||
/* No need to decrement wlen */
|
||||
if (wlen > ef)
|
||||
{
|
||||
HnswSearchCandidate *d = HnswGetSearchCandidate(w_node, pairingheap_remove_first(W));
|
||||
|
||||
if (discarded != NULL)
|
||||
pairingheap_add(*discarded, &d->w_node);
|
||||
}
|
||||
pairingheap_remove_first(W);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -967,9 +909,9 @@ HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation in
|
||||
/* Add each element of W to w */
|
||||
while (!pairingheap_is_empty(W))
|
||||
{
|
||||
HnswSearchCandidate *sc = HnswGetSearchCandidate(w_node, pairingheap_remove_first(W));
|
||||
HnswSearchCandidate *hc = HnswGetSearchCandidate(w_node, pairingheap_remove_first(W));
|
||||
|
||||
w = lappend(w, sc);
|
||||
w = lappend(w, hc);
|
||||
}
|
||||
|
||||
return w;
|
||||
@@ -1023,22 +965,32 @@ CompareCandidateDistancesOffset(const ListCell *a, const ListCell *b)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Calculate the distance between elements
|
||||
*/
|
||||
static float
|
||||
HnswGetDistance(char *base, HnswElement a, HnswElement b, FmgrInfo *procinfo, Oid collation)
|
||||
{
|
||||
Datum aValue = HnswGetValue(base, a);
|
||||
Datum bValue = HnswGetValue(base, b);
|
||||
|
||||
return DatumGetFloat8(FunctionCall2Coll(procinfo, collation, aValue, bValue));
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if an element is closer to q than any element from R
|
||||
*/
|
||||
static bool
|
||||
CheckElementCloser(char *base, HnswCandidate * e, List *r, HnswSupport * support)
|
||||
CheckElementCloser(char *base, HnswCandidate * e, List *r, FmgrInfo *procinfo, Oid collation)
|
||||
{
|
||||
HnswElement eElement = HnswPtrAccess(base, e->element);
|
||||
Datum eValue = HnswGetValue(base, eElement);
|
||||
ListCell *lc2;
|
||||
|
||||
foreach(lc2, r)
|
||||
{
|
||||
HnswCandidate *ri = lfirst(lc2);
|
||||
HnswElement riElement = HnswPtrAccess(base, ri->element);
|
||||
Datum riValue = HnswGetValue(base, riElement);
|
||||
float distance = HnswGetDistance(eValue, riValue, support);
|
||||
float distance = HnswGetDistance(base, eElement, riElement, procinfo, collation);
|
||||
|
||||
if (distance <= e->distance)
|
||||
return false;
|
||||
@@ -1051,14 +1003,15 @@ CheckElementCloser(char *base, HnswCandidate * e, List *r, HnswSupport * support
|
||||
* Algorithm 4 from paper
|
||||
*/
|
||||
static List *
|
||||
SelectNeighbors(char *base, List *c, int lm, HnswSupport * support, bool *closerSet, HnswCandidate * newCandidate, HnswCandidate * *pruned, bool sortCandidates)
|
||||
SelectNeighbors(char *base, List *c, int lm, int lc, FmgrInfo *procinfo, Oid collation, HnswElement e2, HnswCandidate * newCandidate, HnswCandidate * *pruned, bool sortCandidates)
|
||||
{
|
||||
List *r = NIL;
|
||||
List *w = list_copy(c);
|
||||
HnswCandidate **wd;
|
||||
int wdlen = 0;
|
||||
int wdoff = 0;
|
||||
bool mustCalculate = !(*closerSet);
|
||||
HnswNeighborArray *neighbors = HnswGetNeighbors(base, e2, lc);
|
||||
bool mustCalculate = !neighbors->closerSet;
|
||||
List *added = NIL;
|
||||
bool removedAny = false;
|
||||
|
||||
@@ -1085,7 +1038,7 @@ SelectNeighbors(char *base, List *c, int lm, HnswSupport * support, bool *closer
|
||||
|
||||
/* Use previous state of r and wd to skip work when possible */
|
||||
if (mustCalculate)
|
||||
e->closer = CheckElementCloser(base, e, r, support);
|
||||
e->closer = CheckElementCloser(base, e, r, procinfo, collation);
|
||||
else if (list_length(added) > 0)
|
||||
{
|
||||
/* Keep Valgrind happy for in-memory, parallel builds */
|
||||
@@ -1098,7 +1051,7 @@ SelectNeighbors(char *base, List *c, int lm, HnswSupport * support, bool *closer
|
||||
*/
|
||||
if (e->closer)
|
||||
{
|
||||
e->closer = CheckElementCloser(base, e, added, support);
|
||||
e->closer = CheckElementCloser(base, e, added, procinfo, collation);
|
||||
|
||||
if (!e->closer)
|
||||
removedAny = true;
|
||||
@@ -1111,7 +1064,7 @@ SelectNeighbors(char *base, List *c, int lm, HnswSupport * support, bool *closer
|
||||
*/
|
||||
if (removedAny)
|
||||
{
|
||||
e->closer = CheckElementCloser(base, e, r, support);
|
||||
e->closer = CheckElementCloser(base, e, r, procinfo, collation);
|
||||
if (e->closer)
|
||||
added = lappend(added, e);
|
||||
}
|
||||
@@ -1119,7 +1072,7 @@ SelectNeighbors(char *base, List *c, int lm, HnswSupport * support, bool *closer
|
||||
}
|
||||
else if (e == newCandidate)
|
||||
{
|
||||
e->closer = CheckElementCloser(base, e, r, support);
|
||||
e->closer = CheckElementCloser(base, e, r, procinfo, collation);
|
||||
if (e->closer)
|
||||
added = lappend(added, e);
|
||||
}
|
||||
@@ -1135,7 +1088,7 @@ SelectNeighbors(char *base, List *c, int lm, HnswSupport * support, bool *closer
|
||||
}
|
||||
|
||||
/* Cached value can only be used in future if sorted deterministically */
|
||||
*closerSet = sortCandidates;
|
||||
neighbors->closerSet = sortCandidates;
|
||||
|
||||
/* Keep pruned connections */
|
||||
while (wdoff < wdlen && list_length(r) < lm)
|
||||
@@ -1170,16 +1123,18 @@ AddConnections(char *base, HnswElement element, List *neighbors, int lc)
|
||||
* Update connections
|
||||
*/
|
||||
void
|
||||
HnswUpdateConnection(char *base, HnswNeighborArray * neighbors, HnswElement newElement, float distance, int lm, int *updateIdx, Relation index, HnswSupport * support)
|
||||
HnswUpdateConnection(char *base, HnswElement element, HnswCandidate * hc, int lm, int lc, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation)
|
||||
{
|
||||
HnswCandidate newHc;
|
||||
HnswElement hce = HnswPtrAccess(base, hc->element);
|
||||
HnswNeighborArray *currentNeighbors = HnswGetNeighbors(base, hce, lc);
|
||||
HnswCandidate hc2;
|
||||
|
||||
HnswPtrStore(base, newHc.element, newElement);
|
||||
newHc.distance = distance;
|
||||
HnswPtrStore(base, hc2.element, element);
|
||||
hc2.distance = hc->distance;
|
||||
|
||||
if (neighbors->length < lm)
|
||||
if (currentNeighbors->length < lm)
|
||||
{
|
||||
neighbors->items[neighbors->length++] = newHc;
|
||||
currentNeighbors->items[currentNeighbors->length++] = hc2;
|
||||
|
||||
/* Track update */
|
||||
if (updateIdx != NULL)
|
||||
@@ -1188,26 +1143,54 @@ HnswUpdateConnection(char *base, HnswNeighborArray * neighbors, HnswElement newE
|
||||
else
|
||||
{
|
||||
/* Shrink connections */
|
||||
List *c = NIL;
|
||||
HnswCandidate *pruned = NULL;
|
||||
|
||||
/* Add candidates */
|
||||
for (int i = 0; i < neighbors->length; i++)
|
||||
c = lappend(c, &neighbors->items[i]);
|
||||
c = lappend(c, &newHc);
|
||||
/* Load elements on insert */
|
||||
if (index != NULL)
|
||||
{
|
||||
Datum q = HnswGetValue(base, hce);
|
||||
|
||||
SelectNeighbors(base, c, lm, support, &neighbors->closerSet, &newHc, &pruned, true);
|
||||
for (int i = 0; i < currentNeighbors->length; i++)
|
||||
{
|
||||
HnswCandidate *hc3 = ¤tNeighbors->items[i];
|
||||
HnswElement hc3Element = HnswPtrAccess(base, hc3->element);
|
||||
|
||||
if (HnswPtrIsNull(base, hc3Element->value))
|
||||
HnswLoadElement(hc3Element, &hc3->distance, &q, index, procinfo, collation, true, NULL);
|
||||
else
|
||||
hc3->distance = GetElementDistance(base, hc3Element, q, procinfo, collation);
|
||||
|
||||
/* Prune element if being deleted */
|
||||
if (hc3Element->heaptidsLength == 0)
|
||||
{
|
||||
pruned = ¤tNeighbors->items[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pruned == NULL)
|
||||
{
|
||||
List *c = NIL;
|
||||
|
||||
/* Add candidates */
|
||||
for (int i = 0; i < currentNeighbors->length; i++)
|
||||
c = lappend(c, ¤tNeighbors->items[i]);
|
||||
c = lappend(c, &hc2);
|
||||
|
||||
SelectNeighbors(base, c, lm, lc, procinfo, collation, hce, &hc2, &pruned, true);
|
||||
|
||||
/* Should not happen */
|
||||
if (pruned == NULL)
|
||||
return;
|
||||
}
|
||||
|
||||
/* Find and replace the pruned element */
|
||||
for (int i = 0; i < neighbors->length; i++)
|
||||
for (int i = 0; i < currentNeighbors->length; i++)
|
||||
{
|
||||
if (HnswPtrEqual(base, neighbors->items[i].element, pruned->element))
|
||||
if (HnswPtrEqual(base, currentNeighbors->items[i].element, pruned->element))
|
||||
{
|
||||
neighbors->items[i] = newHc;
|
||||
currentNeighbors->items[i] = hc2;
|
||||
|
||||
/* Track update */
|
||||
if (updateIdx != NULL)
|
||||
@@ -1267,20 +1250,17 @@ PrecomputeHash(char *base, HnswElement element)
|
||||
* Algorithm 1 from paper
|
||||
*/
|
||||
void
|
||||
HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint, Relation index, HnswSupport * support, int m, int efConstruction, bool existing)
|
||||
HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint, Relation index, FmgrInfo *procinfo, Oid collation, int m, int efConstruction, bool existing)
|
||||
{
|
||||
List *ep;
|
||||
List *w;
|
||||
int level = element->level;
|
||||
int entryLevel;
|
||||
HnswQuery q;
|
||||
Datum q = HnswGetValue(base, element);
|
||||
HnswElement skipElement = existing ? element : NULL;
|
||||
bool inMemory = index == NULL;
|
||||
|
||||
q.value = HnswGetValue(base, element);
|
||||
|
||||
/* Precompute hash */
|
||||
if (inMemory)
|
||||
if (index == NULL)
|
||||
PrecomputeHash(base, element);
|
||||
|
||||
/* No neighbors if no entry point */
|
||||
@@ -1288,13 +1268,13 @@ HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint
|
||||
return;
|
||||
|
||||
/* Get entry point and level */
|
||||
ep = list_make1(HnswEntryCandidate(base, entryPoint, &q, index, support, true));
|
||||
ep = list_make1(HnswEntryCandidate(base, entryPoint, q, index, procinfo, collation, true));
|
||||
entryLevel = entryPoint->level;
|
||||
|
||||
/* 1st phase: greedy search to insert level */
|
||||
for (int lc = entryLevel; lc >= level + 1; lc--)
|
||||
{
|
||||
w = HnswSearchLayer(base, &q, ep, 1, lc, index, support, m, true, skipElement, NULL, NULL, true, NULL);
|
||||
w = HnswSearchLayer(base, q, ep, 1, lc, index, procinfo, collation, m, true, skipElement);
|
||||
ep = w;
|
||||
}
|
||||
|
||||
@@ -1313,7 +1293,7 @@ HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint
|
||||
List *lw = NIL;
|
||||
ListCell *lc2;
|
||||
|
||||
w = HnswSearchLayer(base, &q, ep, efConstruction, lc, index, support, m, true, skipElement, NULL, NULL, true, NULL);
|
||||
w = HnswSearchLayer(base, q, ep, efConstruction, lc, index, procinfo, collation, m, true, skipElement);
|
||||
|
||||
/* Convert search candidates to candidates */
|
||||
foreach(lc2, w)
|
||||
@@ -1329,7 +1309,7 @@ HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint
|
||||
|
||||
/* Elements being deleted or skipped can help with search */
|
||||
/* but should be removed before selecting neighbors */
|
||||
if (!inMemory)
|
||||
if (index != NULL)
|
||||
lw = RemoveElements(base, lw, skipElement);
|
||||
|
||||
/*
|
||||
@@ -1337,7 +1317,7 @@ HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint
|
||||
* sortCandidates to true for in-memory builds to enable closer
|
||||
* caching, but there does not seem to be a difference in performance.
|
||||
*/
|
||||
neighbors = SelectNeighbors(base, lw, lm, support, &HnswGetNeighbors(base, element, lc)->closerSet, NULL, NULL, false);
|
||||
neighbors = SelectNeighbors(base, lw, lm, lc, procinfo, collation, element, NULL, NULL, false);
|
||||
|
||||
AddConnections(base, element, neighbors, lc);
|
||||
|
||||
|
||||
@@ -184,12 +184,13 @@ static void
|
||||
RepairGraphElement(HnswVacuumState * vacuumstate, HnswElement element, HnswElement entryPoint)
|
||||
{
|
||||
Relation index = vacuumstate->index;
|
||||
HnswSupport *support = &vacuumstate->support;
|
||||
Buffer buf;
|
||||
Page page;
|
||||
GenericXLogState *state;
|
||||
int m = vacuumstate->m;
|
||||
int efConstruction = vacuumstate->efConstruction;
|
||||
FmgrInfo *procinfo = vacuumstate->procinfo;
|
||||
Oid collation = vacuumstate->collation;
|
||||
BufferAccessStrategy bas = vacuumstate->bas;
|
||||
HnswNeighborTuple ntup = vacuumstate->ntup;
|
||||
Size ntupSize = HNSW_NEIGHBOR_TUPLE_SIZE(element->level, m);
|
||||
@@ -204,7 +205,7 @@ RepairGraphElement(HnswVacuumState * vacuumstate, HnswElement element, HnswEleme
|
||||
element->heaptidsLength = 0;
|
||||
|
||||
/* Find neighbors for element, skipping itself */
|
||||
HnswFindElementNeighbors(base, element, entryPoint, index, support, m, efConstruction, true);
|
||||
HnswFindElementNeighbors(base, element, entryPoint, index, procinfo, collation, m, efConstruction, true);
|
||||
|
||||
/* Zero memory for each element */
|
||||
MemSet(ntup, 0, HNSW_TUPLE_ALLOC_SIZE);
|
||||
@@ -228,7 +229,7 @@ RepairGraphElement(HnswVacuumState * vacuumstate, HnswElement element, HnswEleme
|
||||
UnlockReleaseBuffer(buf);
|
||||
|
||||
/* Update neighbors */
|
||||
HnswUpdateNeighborsOnDisk(index, support, element, m, true, false);
|
||||
HnswUpdateNeighborsOnDisk(index, procinfo, collation, element, m, true, false);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -238,7 +239,6 @@ static void
|
||||
RepairGraphEntryPoint(HnswVacuumState * vacuumstate)
|
||||
{
|
||||
Relation index = vacuumstate->index;
|
||||
HnswSupport *support = &vacuumstate->support;
|
||||
HnswElement highestPoint = &vacuumstate->highestPoint;
|
||||
HnswElement entryPoint;
|
||||
MemoryContext oldCtx = MemoryContextSwitchTo(vacuumstate->tmpCtx);
|
||||
@@ -256,7 +256,7 @@ RepairGraphEntryPoint(HnswVacuumState * vacuumstate)
|
||||
LockPage(index, HNSW_UPDATE_LOCK, ShareLock);
|
||||
|
||||
/* Load element */
|
||||
HnswLoadElement(highestPoint, NULL, NULL, index, support, true, NULL);
|
||||
HnswLoadElement(highestPoint, NULL, NULL, index, vacuumstate->procinfo, vacuumstate->collation, true, NULL);
|
||||
|
||||
/* Repair if needed */
|
||||
if (NeedsUpdated(vacuumstate, highestPoint))
|
||||
@@ -294,7 +294,7 @@ RepairGraphEntryPoint(HnswVacuumState * vacuumstate)
|
||||
* is outdated, this can remove connections at higher levels in
|
||||
* the graph until they are repaired, but this should be fine.
|
||||
*/
|
||||
HnswLoadElement(entryPoint, NULL, NULL, index, support, true, NULL);
|
||||
HnswLoadElement(entryPoint, NULL, NULL, index, vacuumstate->procinfo, vacuumstate->collation, true, NULL);
|
||||
|
||||
if (NeedsUpdated(vacuumstate, entryPoint))
|
||||
{
|
||||
@@ -527,14 +527,6 @@ MarkDeleted(HnswVacuumState * vacuumstate)
|
||||
for (int i = 0; i < ntup->count; i++)
|
||||
ItemPointerSetInvalid(&ntup->indextids[i]);
|
||||
|
||||
/* Increment version */
|
||||
/* This is used to avoid incorrect reads for iterative scans */
|
||||
/* Reserve some bits for future use */
|
||||
etup->version++;
|
||||
if (etup->version > 15)
|
||||
etup->version = 1;
|
||||
ntup->version = etup->version;
|
||||
|
||||
/*
|
||||
* We modified the tuples in place, no need to call
|
||||
* PageIndexTupleOverwrite
|
||||
@@ -581,13 +573,13 @@ InitVacuumState(HnswVacuumState * vacuumstate, IndexVacuumInfo *info, IndexBulkD
|
||||
vacuumstate->callback_state = callback_state;
|
||||
vacuumstate->efConstruction = HnswGetEfConstruction(index);
|
||||
vacuumstate->bas = GetAccessStrategy(BAS_BULKREAD);
|
||||
vacuumstate->procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC);
|
||||
vacuumstate->collation = index->rd_indcollation[0];
|
||||
vacuumstate->ntup = palloc0(HNSW_TUPLE_ALLOC_SIZE);
|
||||
vacuumstate->tmpCtx = AllocSetContextCreate(CurrentMemoryContext,
|
||||
"Hnsw vacuum temporary context",
|
||||
ALLOCSET_DEFAULT_SIZES);
|
||||
|
||||
HnswInitSupport(&vacuumstate->support, index);
|
||||
|
||||
/* Get m from metapage */
|
||||
HnswGetMetaPageInfo(index, &vacuumstate->m, NULL);
|
||||
|
||||
|
||||
@@ -227,12 +227,12 @@ BuildCallback(Relation index, ItemPointer tid, Datum *values,
|
||||
*/
|
||||
static inline void
|
||||
GetNextTuple(Tuplesortstate *sortstate, TupleDesc tupdesc, TupleTableSlot *slot, IndexTuple *itup, int *list)
|
||||
{
|
||||
if (tuplesort_gettupleslot(sortstate, true, false, slot, NULL))
|
||||
{
|
||||
Datum value;
|
||||
bool isnull;
|
||||
|
||||
if (tuplesort_gettupleslot(sortstate, true, false, slot, NULL))
|
||||
{
|
||||
*list = DatumGetInt32(slot_getattr(slot, 1, &isnull));
|
||||
value = slot_getattr(slot, 3, &isnull);
|
||||
|
||||
@@ -254,8 +254,8 @@ InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum)
|
||||
IndexTuple itup = NULL; /* silence compiler warning */
|
||||
int64 inserted = 0;
|
||||
|
||||
TupleTableSlot *slot = MakeSingleTupleTableSlot(buildstate->sortdesc, &TTSOpsMinimalTuple);
|
||||
TupleDesc tupdesc = buildstate->tupdesc;
|
||||
TupleTableSlot *slot = MakeSingleTupleTableSlot(buildstate->tupdesc, &TTSOpsMinimalTuple);
|
||||
TupleDesc tupdesc = RelationGetDescr(index);
|
||||
|
||||
pgstat_progress_update_param(PROGRESS_CREATEIDX_SUBPHASE, PROGRESS_IVFFLAT_PHASE_LOAD);
|
||||
|
||||
@@ -319,7 +319,6 @@ InitBuildState(IvfflatBuildState * buildstate, Relation heap, Relation index, In
|
||||
buildstate->index = index;
|
||||
buildstate->indexInfo = indexInfo;
|
||||
buildstate->typeInfo = IvfflatGetTypeInfo(index);
|
||||
buildstate->tupdesc = RelationGetDescr(index);
|
||||
|
||||
buildstate->lists = IvfflatGetLists(index);
|
||||
buildstate->dimensions = TupleDescAttr(index->rd_att, 0)->atttypmod;
|
||||
@@ -357,12 +356,12 @@ InitBuildState(IvfflatBuildState * buildstate, Relation heap, Relation index, In
|
||||
errmsg("dimensions must be greater than one for this opclass")));
|
||||
|
||||
/* Create tuple description for sorting */
|
||||
buildstate->sortdesc = CreateTemplateTupleDesc(3);
|
||||
TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 1, "list", INT4OID, -1, 0);
|
||||
TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 2, "tid", TIDOID, -1, 0);
|
||||
TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 3, "vector", buildstate->tupdesc->attrs[0].atttypid, -1, 0);
|
||||
buildstate->tupdesc = CreateTemplateTupleDesc(3);
|
||||
TupleDescInitEntry(buildstate->tupdesc, (AttrNumber) 1, "list", INT4OID, -1, 0);
|
||||
TupleDescInitEntry(buildstate->tupdesc, (AttrNumber) 2, "tid", TIDOID, -1, 0);
|
||||
TupleDescInitEntry(buildstate->tupdesc, (AttrNumber) 3, "vector", RelationGetDescr(index)->attrs[0].atttypid, -1, 0);
|
||||
|
||||
buildstate->slot = MakeSingleTupleTableSlot(buildstate->sortdesc, &TTSOpsVirtual);
|
||||
buildstate->slot = MakeSingleTupleTableSlot(buildstate->tupdesc, &TTSOpsVirtual);
|
||||
|
||||
buildstate->centers = VectorArrayInit(buildstate->lists, buildstate->dimensions, buildstate->typeInfo->itemSize(buildstate->dimensions));
|
||||
buildstate->listInfo = palloc(sizeof(ListInfo) * buildstate->lists);
|
||||
@@ -634,7 +633,7 @@ IvfflatParallelScanAndSort(IvfflatSpool * ivfspool, IvfflatShared * ivfshared, S
|
||||
InitBuildState(&buildstate, ivfspool->heap, ivfspool->index, indexInfo);
|
||||
memcpy(buildstate.centers->items, ivfcenters, buildstate.centers->itemsize * buildstate.centers->maxlen);
|
||||
buildstate.centers->length = buildstate.centers->maxlen;
|
||||
ivfspool->sortstate = InitBuildSortState(buildstate.sortdesc, sortmem, coordinate);
|
||||
ivfspool->sortstate = InitBuildSortState(buildstate.tupdesc, sortmem, coordinate);
|
||||
buildstate.sortstate = ivfspool->sortstate;
|
||||
scan = table_beginscan_parallel(ivfspool->heap,
|
||||
ParallelTableScanFromIvfflatShared(ivfshared));
|
||||
@@ -951,7 +950,7 @@ AssignTuples(IvfflatBuildState * buildstate)
|
||||
}
|
||||
|
||||
/* Begin serial/leader tuplesort */
|
||||
buildstate->sortstate = InitBuildSortState(buildstate->sortdesc, maintenance_work_mem, coordinate);
|
||||
buildstate->sortstate = InitBuildSortState(buildstate->tupdesc, maintenance_work_mem, coordinate);
|
||||
|
||||
/* Add tuples to sort */
|
||||
if (buildstate->heap != NULL)
|
||||
|
||||
109
src/ivfflat.c
109
src/ivfflat.c
@@ -17,16 +17,10 @@
|
||||
#endif
|
||||
|
||||
int ivfflat_probes;
|
||||
int ivfflat_iterative_scan;
|
||||
int ivfflat_max_probes;
|
||||
bool ivfflat_streaming;
|
||||
static relopt_kind ivfflat_relopt_kind;
|
||||
|
||||
static const struct config_enum_entry ivfflat_iterative_scan_options[] = {
|
||||
{"off", IVFFLAT_ITERATIVE_SCAN_OFF, false},
|
||||
{"relaxed_order", IVFFLAT_ITERATIVE_SCAN_RELAXED, false},
|
||||
{NULL, 0, false}
|
||||
};
|
||||
|
||||
/*
|
||||
* Initialize index options and variables
|
||||
*/
|
||||
@@ -39,16 +33,15 @@ IvfflatInit(void)
|
||||
|
||||
DefineCustomIntVariable("ivfflat.probes", "Sets the number of probes",
|
||||
"Valid range is 1..lists.", &ivfflat_probes,
|
||||
IVFFLAT_DEFAULT_PROBES, IVFFLAT_MIN_LISTS, IVFFLAT_MAX_LISTS, PGC_USERSET, GUC_EXPLAIN, NULL, NULL, NULL);
|
||||
IVFFLAT_DEFAULT_PROBES, IVFFLAT_MIN_LISTS, IVFFLAT_MAX_LISTS, PGC_USERSET, 0, NULL, NULL, NULL);
|
||||
|
||||
DefineCustomEnumVariable("ivfflat.iterative_scan", "Sets the mode for iterative scans",
|
||||
NULL, &ivfflat_iterative_scan,
|
||||
IVFFLAT_ITERATIVE_SCAN_OFF, ivfflat_iterative_scan_options, PGC_USERSET, GUC_EXPLAIN, NULL, NULL, NULL);
|
||||
|
||||
/* If this is less than probes, probes is used */
|
||||
DefineCustomIntVariable("ivfflat.max_probes", "Sets the max number of probes for iterative scans",
|
||||
NULL, &ivfflat_max_probes,
|
||||
IVFFLAT_MAX_LISTS, IVFFLAT_MIN_LISTS, IVFFLAT_MAX_LISTS, PGC_USERSET, GUC_EXPLAIN, NULL, NULL, NULL);
|
||||
-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");
|
||||
}
|
||||
@@ -74,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
|
||||
*/
|
||||
@@ -85,9 +107,8 @@ ivfflatcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
|
||||
{
|
||||
GenericCosts costs;
|
||||
int lists;
|
||||
int probes;
|
||||
double ratio;
|
||||
double sequentialRatio = 0.5;
|
||||
double startupPages;
|
||||
double spc_seq_page_cost;
|
||||
Relation index;
|
||||
|
||||
@@ -99,46 +120,64 @@ ivfflatcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
|
||||
*indexSelectivity = 0;
|
||||
*indexCorrelation = 0;
|
||||
*indexPages = 0;
|
||||
#if PG_VERSION_NUM >= 180000
|
||||
/* See "On disable_cost" thread on pgsql-hackers */
|
||||
path->path.disabled_nodes = 2;
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
MemSet(&costs, 0, sizeof(costs));
|
||||
|
||||
genericcostestimate(root, path, loop_count, &costs);
|
||||
|
||||
index = index_open(path->indexinfo->indexoid, NoLock);
|
||||
IvfflatGetMetaPageInfo(index, &lists, NULL);
|
||||
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 */
|
||||
ratio = ((double) ivfflat_probes) / lists;
|
||||
ratio = ((double) probes) / lists;
|
||||
if (ratio > 1.0)
|
||||
ratio = 1.0;
|
||||
|
||||
/*
|
||||
* This gives us the subset of tuples to visit. This value is passed into
|
||||
* 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);
|
||||
|
||||
/* Change some page cost from random to sequential */
|
||||
costs.indexTotalCost -= sequentialRatio * costs.numIndexPages * (costs.spc_random_page_cost - spc_seq_page_cost);
|
||||
|
||||
/* Startup cost is cost before returning the first row */
|
||||
costs.indexStartupCost = costs.indexTotalCost * ratio;
|
||||
|
||||
/* Adjust cost if needed since TOAST not included in seq scan cost */
|
||||
startupPages = costs.numIndexPages * ratio;
|
||||
if (startupPages > path->indexinfo->rel->pages && ratio < 0.5)
|
||||
if (costs.numIndexPages > path->indexinfo->rel->pages && ratio < 0.5)
|
||||
{
|
||||
/* Change rest of page cost from random to sequential */
|
||||
costs.indexStartupCost -= (1 - sequentialRatio) * startupPages * (costs.spc_random_page_cost - spc_seq_page_cost);
|
||||
/* 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.indexStartupCost -= (startupPages - path->indexinfo->rel->pages) * spc_seq_page_cost;
|
||||
costs.indexTotalCost -= (costs.numIndexPages - path->indexinfo->rel->pages) * spc_seq_page_cost;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Change some page cost from random to sequential */
|
||||
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;
|
||||
*indexSelectivity = costs.indexSelectivity;
|
||||
*indexCorrelation = costs.indexCorrelation;
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
#define IVFFLAT_MIN_LISTS 1
|
||||
#define IVFFLAT_MAX_LISTS 32768
|
||||
#define IVFFLAT_DEFAULT_PROBES 1
|
||||
#define IVFFLAT_DEFAULT_STREAMING false
|
||||
|
||||
/* Build phases */
|
||||
/* PROGRESS_CREATEIDX_SUBPHASE_INITIALIZE is 1 */
|
||||
@@ -80,14 +81,8 @@
|
||||
|
||||
/* Variables */
|
||||
extern int ivfflat_probes;
|
||||
extern int ivfflat_iterative_scan;
|
||||
extern int ivfflat_max_probes;
|
||||
|
||||
typedef enum IvfflatIterativeScanMode
|
||||
{
|
||||
IVFFLAT_ITERATIVE_SCAN_OFF,
|
||||
IVFFLAT_ITERATIVE_SCAN_RELAXED
|
||||
} IvfflatIterativeScanMode;
|
||||
extern bool ivfflat_streaming;
|
||||
|
||||
typedef struct VectorArrayData
|
||||
{
|
||||
@@ -173,7 +168,6 @@ typedef struct IvfflatBuildState
|
||||
Relation index;
|
||||
IndexInfo *indexInfo;
|
||||
const IvfflatTypeInfo *typeInfo;
|
||||
TupleDesc tupdesc;
|
||||
|
||||
/* Settings */
|
||||
int dimensions;
|
||||
@@ -207,7 +201,7 @@ typedef struct IvfflatBuildState
|
||||
|
||||
/* Sorting */
|
||||
Tuplesortstate *sortstate;
|
||||
TupleDesc sortdesc;
|
||||
TupleDesc tupdesc;
|
||||
TupleTableSlot *slot;
|
||||
|
||||
/* Memory */
|
||||
@@ -260,7 +254,6 @@ typedef struct IvfflatScanOpaqueData
|
||||
int dimensions;
|
||||
bool first;
|
||||
Datum value;
|
||||
MemoryContext tmpCtx;
|
||||
|
||||
/* Sorting */
|
||||
Tuplesortstate *sortstate;
|
||||
@@ -277,9 +270,9 @@ typedef struct IvfflatScanOpaqueData
|
||||
|
||||
/* Lists */
|
||||
pairingheap *listQueue;
|
||||
BlockNumber *listPages;
|
||||
int listIndex;
|
||||
IvfflatScanList *lists;
|
||||
BlockNumber *startPages;
|
||||
int currentIndex;
|
||||
IvfflatScanList lists[FLEXIBLE_ARRAY_MEMBER]; /* must come last */
|
||||
} IvfflatScanOpaqueData;
|
||||
|
||||
typedef IvfflatScanOpaqueData * IvfflatScanOpaque;
|
||||
|
||||
@@ -98,7 +98,7 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heap_tid, R
|
||||
IvfflatGetMetaPageInfo(index, NULL, NULL);
|
||||
|
||||
/* Find the insert page - sets the page and list info */
|
||||
FindInsertPage(index, &value, &insertPage, &listInfo);
|
||||
FindInsertPage(index, values, &insertPage, &listInfo);
|
||||
Assert(BlockNumberIsValid(insertPage));
|
||||
originalInsertPage = insertPage;
|
||||
|
||||
|
||||
@@ -10,7 +10,10 @@
|
||||
#include "miscadmin.h"
|
||||
#include "pgstat.h"
|
||||
#include "storage/bufmgr.h"
|
||||
|
||||
#ifdef IVFFLAT_MEMORY
|
||||
#include "utils/memutils.h"
|
||||
#endif
|
||||
|
||||
#define GetScanList(ptr) pairingheap_container(IvfflatScanList, ph_node, ptr)
|
||||
#define GetScanListConst(ptr) pairingheap_const_container(IvfflatScanList, ph_node, ptr)
|
||||
@@ -75,7 +78,7 @@ GetScanLists(IndexScanDesc scan, Datum value)
|
||||
pairingheap_add(so->listQueue, &scanlist->ph_node);
|
||||
|
||||
/* Calculate max distance */
|
||||
if (listCount == so->maxProbes)
|
||||
if (listCount == so->probes)
|
||||
maxDistance = GetScanList(pairingheap_first(so->listQueue))->distance;
|
||||
}
|
||||
else if (distance < maxDistance)
|
||||
@@ -101,7 +104,7 @@ GetScanLists(IndexScanDesc scan, Datum value)
|
||||
}
|
||||
|
||||
for (int i = listCount - 1; i >= 0; i--)
|
||||
so->listPages[i] = GetScanList(pairingheap_remove_first(so->listQueue))->startPage;
|
||||
so->startPages[i] = GetScanList(pairingheap_remove_first(so->listQueue))->startPage;
|
||||
|
||||
Assert(pairingheap_is_empty(so->listQueue));
|
||||
}
|
||||
@@ -121,9 +124,9 @@ GetScanItems(IndexScanDesc scan, Datum value)
|
||||
tuplesort_reset(so->sortstate);
|
||||
|
||||
/* Search closest probes lists */
|
||||
while (so->listIndex < so->maxProbes && (++batchProbes) <= so->probes)
|
||||
while (so->currentIndex < so->maxProbes && (++batchProbes) <= so->probes)
|
||||
{
|
||||
BlockNumber searchPage = so->listPages[so->listIndex++];
|
||||
BlockNumber searchPage = so->startPages[so->currentIndex++];
|
||||
|
||||
/* Search all entry pages for list */
|
||||
while (BlockNumberIsValid(searchPage))
|
||||
@@ -171,7 +174,7 @@ GetScanItems(IndexScanDesc scan, Datum value)
|
||||
}
|
||||
}
|
||||
|
||||
if (tuples < 100 && ivfflat_iterative_scan == IVFFLAT_ITERATIVE_SCAN_OFF)
|
||||
if (tuples < 100 && !ivfflat_streaming)
|
||||
ereport(DEBUG1,
|
||||
(errmsg("index scan found few tuples"),
|
||||
errdetail("Index may have been created with little data."),
|
||||
@@ -218,13 +221,7 @@ GetScanValue(IndexScanDesc scan)
|
||||
|
||||
/* Normalize if needed */
|
||||
if (so->normprocinfo != NULL)
|
||||
{
|
||||
MemoryContext oldCtx = MemoryContextSwitchTo(so->tmpCtx);
|
||||
|
||||
value = IvfflatNormValue(so->typeInfo, so->collation, value);
|
||||
|
||||
MemoryContextSwitchTo(oldCtx);
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
@@ -256,25 +253,26 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys)
|
||||
int dimensions;
|
||||
int probes = ivfflat_probes;
|
||||
int maxProbes;
|
||||
MemoryContext oldCtx;
|
||||
|
||||
scan = RelationGetIndexScan(index, nkeys, norderbys);
|
||||
|
||||
/* Get lists and dimensions from metapage */
|
||||
IvfflatGetMetaPageInfo(index, &lists, &dimensions);
|
||||
|
||||
if (ivfflat_iterative_scan != IVFFLAT_ITERATIVE_SCAN_OFF)
|
||||
maxProbes = Max(ivfflat_max_probes, probes);
|
||||
else
|
||||
maxProbes = probes;
|
||||
|
||||
if (probes > lists)
|
||||
probes = lists;
|
||||
|
||||
if (maxProbes > lists)
|
||||
if (ivfflat_streaming)
|
||||
{
|
||||
if (ivfflat_max_probes == -1)
|
||||
maxProbes = lists;
|
||||
else
|
||||
maxProbes = Min(ivfflat_max_probes, lists);
|
||||
}
|
||||
else
|
||||
maxProbes = probes;
|
||||
|
||||
so = (IvfflatScanOpaque) palloc(sizeof(IvfflatScanOpaqueData));
|
||||
so = (IvfflatScanOpaque) palloc(offsetof(IvfflatScanOpaqueData, lists) + maxProbes * sizeof(IvfflatScanList));
|
||||
so->typeInfo = IvfflatGetTypeInfo(index);
|
||||
so->first = true;
|
||||
so->probes = probes;
|
||||
@@ -286,12 +284,6 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys)
|
||||
so->normprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_NORM_PROC);
|
||||
so->collation = index->rd_indcollation[0];
|
||||
|
||||
so->tmpCtx = AllocSetContextCreate(CurrentMemoryContext,
|
||||
"Ivfflat scan temporary context",
|
||||
ALLOCSET_DEFAULT_SIZES);
|
||||
|
||||
oldCtx = MemoryContextSwitchTo(so->tmpCtx);
|
||||
|
||||
/* Create tuple description for sorting */
|
||||
so->tupdesc = CreateTemplateTupleDesc(2);
|
||||
TupleDescInitEntry(so->tupdesc, (AttrNumber) 1, "distance", FLOAT8OID, -1, 0);
|
||||
@@ -312,11 +304,8 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys)
|
||||
so->bas = GetAccessStrategy(BAS_BULKREAD);
|
||||
|
||||
so->listQueue = pairingheap_allocate(CompareLists, scan);
|
||||
so->listPages = palloc(maxProbes * sizeof(BlockNumber));
|
||||
so->listIndex = 0;
|
||||
so->lists = palloc(maxProbes * sizeof(IvfflatScanList));
|
||||
|
||||
MemoryContextSwitchTo(oldCtx);
|
||||
so->startPages = palloc(maxProbes * sizeof(BlockNumber));
|
||||
so->currentIndex = 0;
|
||||
|
||||
scan->opaque = so;
|
||||
|
||||
@@ -333,7 +322,7 @@ ivfflatrescan(IndexScanDesc scan, ScanKey keys, int nkeys, ScanKey orderbys, int
|
||||
|
||||
so->first = true;
|
||||
pairingheap_reset(so->listQueue);
|
||||
so->listIndex = 0;
|
||||
so->currentIndex = 0;
|
||||
|
||||
if (keys && scan->numberOfKeys > 0)
|
||||
memmove(scan->keyData, keys, scan->numberOfKeys * sizeof(ScanKeyData));
|
||||
@@ -379,11 +368,13 @@ ivfflatgettuple(IndexScanDesc scan, ScanDirection dir)
|
||||
IvfflatBench("GetScanItems", GetScanItems(scan, value));
|
||||
so->first = false;
|
||||
so->value = value;
|
||||
|
||||
/* TODO clean up if we allocated a new value */
|
||||
}
|
||||
|
||||
while (!tuplesort_gettupleslot(so->sortstate, true, false, so->mslot, NULL))
|
||||
{
|
||||
if (so->listIndex == so->maxProbes)
|
||||
if (so->currentIndex == so->maxProbes)
|
||||
return false;
|
||||
|
||||
IvfflatBench("GetScanItems", GetScanItems(scan, so->value));
|
||||
@@ -405,10 +396,13 @@ ivfflatendscan(IndexScanDesc scan)
|
||||
{
|
||||
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
|
||||
|
||||
/* Free any temporary files */
|
||||
pairingheap_free(so->listQueue);
|
||||
pfree(so->startPages);
|
||||
tuplesort_end(so->sortstate);
|
||||
FreeAccessStrategy(so->bas);
|
||||
FreeTupleDesc(so->tupdesc);
|
||||
|
||||
MemoryContextDelete(so->tmpCtx);
|
||||
/* TODO Free vslot and mslot without freeing TupleDesc */
|
||||
|
||||
pfree(so);
|
||||
scan->opaque = NULL;
|
||||
|
||||
@@ -26,7 +26,7 @@ ivfflatbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
|
||||
Page cpage;
|
||||
OffsetNumber coffno;
|
||||
OffsetNumber cmaxoffno;
|
||||
BlockNumber listPages[MaxOffsetNumber];
|
||||
BlockNumber startPages[MaxOffsetNumber];
|
||||
ListInfo listInfo;
|
||||
|
||||
cbuf = ReadBuffer(index, blkno);
|
||||
@@ -40,7 +40,7 @@ ivfflatbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
|
||||
{
|
||||
IvfflatList list = (IvfflatList) PageGetItem(cpage, PageGetItemId(cpage, coffno));
|
||||
|
||||
listPages[coffno - FirstOffsetNumber] = list->startPage;
|
||||
startPages[coffno - FirstOffsetNumber] = list->startPage;
|
||||
}
|
||||
|
||||
listInfo.blkno = blkno;
|
||||
@@ -50,7 +50,7 @@ ivfflatbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
|
||||
|
||||
for (coffno = FirstOffsetNumber; coffno <= cmaxoffno; coffno = OffsetNumberNext(coffno))
|
||||
{
|
||||
BlockNumber searchPage = listPages[coffno - FirstOffsetNumber];
|
||||
BlockNumber searchPage = startPages[coffno - FirstOffsetNumber];
|
||||
BlockNumber insertPage = InvalidBlockNumber;
|
||||
|
||||
/* Iterate over entry pages */
|
||||
|
||||
18
src/vector.c
18
src/vector.c
@@ -155,6 +155,24 @@ CheckStateArray(ArrayType *statearray, const char *caller)
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -99,32 +99,6 @@ SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <+> (SELECT NULL::vector)) t2
|
||||
4
|
||||
(1 row)
|
||||
|
||||
DROP TABLE t;
|
||||
-- iterative
|
||||
CREATE TABLE t (val vector(3));
|
||||
INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL);
|
||||
CREATE INDEX ON t USING hnsw (val vector_l2_ops);
|
||||
SET hnsw.iterative_scan = strict_order;
|
||||
SET hnsw.ef_search = 1;
|
||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
||||
val
|
||||
---------
|
||||
[1,2,3]
|
||||
[1,1,1]
|
||||
[0,0,0]
|
||||
(3 rows)
|
||||
|
||||
SET hnsw.iterative_scan = relaxed_order;
|
||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
||||
val
|
||||
---------
|
||||
[1,2,3]
|
||||
[1,1,1]
|
||||
[0,0,0]
|
||||
(3 rows)
|
||||
|
||||
RESET hnsw.iterative_scan;
|
||||
RESET hnsw.ef_search;
|
||||
DROP TABLE t;
|
||||
-- unlogged
|
||||
CREATE UNLOGGED TABLE t (val vector(3));
|
||||
@@ -165,29 +139,4 @@ SET hnsw.ef_search = 0;
|
||||
ERROR: 0 is outside the valid range for parameter "hnsw.ef_search" (1 .. 1000)
|
||||
SET hnsw.ef_search = 1001;
|
||||
ERROR: 1001 is outside the valid range for parameter "hnsw.ef_search" (1 .. 1000)
|
||||
SHOW hnsw.iterative_scan;
|
||||
hnsw.iterative_scan
|
||||
---------------------
|
||||
off
|
||||
(1 row)
|
||||
|
||||
SET hnsw.iterative_scan = on;
|
||||
ERROR: invalid value for parameter "hnsw.iterative_scan": "on"
|
||||
HINT: Available values: off, relaxed_order, strict_order.
|
||||
SHOW hnsw.max_scan_tuples;
|
||||
hnsw.max_scan_tuples
|
||||
----------------------
|
||||
20000
|
||||
(1 row)
|
||||
|
||||
SET hnsw.max_scan_tuples = 0;
|
||||
ERROR: 0 is outside the valid range for parameter "hnsw.max_scan_tuples" (1 .. 2147483647)
|
||||
SHOW hnsw.scan_mem_multiplier;
|
||||
hnsw.scan_mem_multiplier
|
||||
--------------------------
|
||||
1
|
||||
(1 row)
|
||||
|
||||
SET hnsw.scan_mem_multiplier = 0;
|
||||
ERROR: 0 is outside the valid range for parameter "hnsw.scan_mem_multiplier" (1 .. 1000)
|
||||
DROP TABLE t;
|
||||
|
||||
@@ -81,37 +81,6 @@ SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <=> (SELECT NULL::vector)) t2
|
||||
3
|
||||
(1 row)
|
||||
|
||||
DROP TABLE t;
|
||||
-- iterative
|
||||
CREATE TABLE t (val vector(3));
|
||||
INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL);
|
||||
CREATE INDEX ON t USING ivfflat (val vector_l2_ops) WITH (lists = 3);
|
||||
SET ivfflat.iterative_scan = relaxed_order;
|
||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
||||
val
|
||||
---------
|
||||
[1,2,3]
|
||||
[1,1,1]
|
||||
[0,0,0]
|
||||
(3 rows)
|
||||
|
||||
SET ivfflat.max_probes = 1;
|
||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
||||
val
|
||||
---------
|
||||
[1,2,3]
|
||||
(1 row)
|
||||
|
||||
SET ivfflat.max_probes = 2;
|
||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
||||
val
|
||||
---------
|
||||
[1,2,3]
|
||||
[1,1,1]
|
||||
(2 rows)
|
||||
|
||||
RESET ivfflat.iterative_scan;
|
||||
RESET ivfflat.max_probes;
|
||||
DROP TABLE t;
|
||||
-- unlogged
|
||||
CREATE UNLOGGED TABLE t (val vector(3));
|
||||
@@ -140,27 +109,4 @@ SHOW ivfflat.probes;
|
||||
1
|
||||
(1 row)
|
||||
|
||||
SET ivfflat.probes = 0;
|
||||
ERROR: 0 is outside the valid range for parameter "ivfflat.probes" (1 .. 32768)
|
||||
SET ivfflat.probes = 32769;
|
||||
ERROR: 32769 is outside the valid range for parameter "ivfflat.probes" (1 .. 32768)
|
||||
SHOW ivfflat.iterative_scan;
|
||||
ivfflat.iterative_scan
|
||||
------------------------
|
||||
off
|
||||
(1 row)
|
||||
|
||||
SET ivfflat.iterative_scan = on;
|
||||
ERROR: invalid value for parameter "ivfflat.iterative_scan": "on"
|
||||
HINT: Available values: off, relaxed_order.
|
||||
SHOW ivfflat.max_probes;
|
||||
ivfflat.max_probes
|
||||
--------------------
|
||||
32768
|
||||
(1 row)
|
||||
|
||||
SET ivfflat.max_probes = 0;
|
||||
ERROR: 0 is outside the valid range for parameter "ivfflat.max_probes" (1 .. 32768)
|
||||
SET ivfflat.max_probes = 32769;
|
||||
ERROR: 32769 is outside the valid range for parameter "ivfflat.max_probes" (1 .. 32768)
|
||||
DROP TABLE t;
|
||||
|
||||
@@ -57,23 +57,6 @@ SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <+> (SELECT NULL::vector)) t2
|
||||
|
||||
DROP TABLE t;
|
||||
|
||||
-- iterative
|
||||
|
||||
CREATE TABLE t (val vector(3));
|
||||
INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL);
|
||||
CREATE INDEX ON t USING hnsw (val vector_l2_ops);
|
||||
|
||||
SET hnsw.iterative_scan = strict_order;
|
||||
SET hnsw.ef_search = 1;
|
||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
||||
|
||||
SET hnsw.iterative_scan = relaxed_order;
|
||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
||||
|
||||
RESET hnsw.iterative_scan;
|
||||
RESET hnsw.ef_search;
|
||||
DROP TABLE t;
|
||||
|
||||
-- unlogged
|
||||
|
||||
CREATE UNLOGGED TABLE t (val vector(3));
|
||||
@@ -98,16 +81,4 @@ SHOW hnsw.ef_search;
|
||||
SET hnsw.ef_search = 0;
|
||||
SET hnsw.ef_search = 1001;
|
||||
|
||||
SHOW hnsw.iterative_scan;
|
||||
|
||||
SET hnsw.iterative_scan = on;
|
||||
|
||||
SHOW hnsw.max_scan_tuples;
|
||||
|
||||
SET hnsw.max_scan_tuples = 0;
|
||||
|
||||
SHOW hnsw.scan_mem_multiplier;
|
||||
|
||||
SET hnsw.scan_mem_multiplier = 0;
|
||||
|
||||
DROP TABLE t;
|
||||
|
||||
@@ -44,25 +44,6 @@ SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <=> (SELECT NULL::vector)) t2
|
||||
|
||||
DROP TABLE t;
|
||||
|
||||
-- iterative
|
||||
|
||||
CREATE TABLE t (val vector(3));
|
||||
INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL);
|
||||
CREATE INDEX ON t USING ivfflat (val vector_l2_ops) WITH (lists = 3);
|
||||
|
||||
SET ivfflat.iterative_scan = relaxed_order;
|
||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
||||
|
||||
SET ivfflat.max_probes = 1;
|
||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
||||
|
||||
SET ivfflat.max_probes = 2;
|
||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
||||
|
||||
RESET ivfflat.iterative_scan;
|
||||
RESET ivfflat.max_probes;
|
||||
DROP TABLE t;
|
||||
|
||||
-- unlogged
|
||||
|
||||
CREATE UNLOGGED TABLE t (val vector(3));
|
||||
@@ -81,16 +62,4 @@ CREATE INDEX ON t USING ivfflat (val vector_l2_ops) WITH (lists = 32769);
|
||||
|
||||
SHOW ivfflat.probes;
|
||||
|
||||
SET ivfflat.probes = 0;
|
||||
SET ivfflat.probes = 32769;
|
||||
|
||||
SHOW ivfflat.iterative_scan;
|
||||
|
||||
SET ivfflat.iterative_scan = on;
|
||||
|
||||
SHOW ivfflat.max_probes;
|
||||
|
||||
SET ivfflat.max_probes = 0;
|
||||
SET ivfflat.max_probes = 32769;
|
||||
|
||||
DROP TABLE t;
|
||||
|
||||
@@ -6,7 +6,13 @@ use Test::More;
|
||||
|
||||
my $dim = 3;
|
||||
|
||||
my $array_sql = join(",", ('random()') x $dim);
|
||||
my @r = ();
|
||||
for (1 .. $dim)
|
||||
{
|
||||
my $v = int(rand(1000)) + 1;
|
||||
push(@r, "i % $v");
|
||||
}
|
||||
my $array_sql = join(", ", @r);
|
||||
|
||||
# Initialize node
|
||||
my $node = PostgreSQL::Test::Cluster->new('node');
|
||||
@@ -17,20 +23,19 @@ $node->start;
|
||||
$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;"
|
||||
"INSERT INTO tst SELECT i % 10, ARRAY[$array_sql] FROM generate_series(1, 100000) i;"
|
||||
);
|
||||
$node->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v vector_l2_ops);");
|
||||
|
||||
# Get size
|
||||
my $size = $node->safe_psql("postgres", "SELECT pg_total_relation_size('tst_v_idx');");
|
||||
|
||||
# Store values
|
||||
$node->safe_psql("postgres", "CREATE TABLE tmp AS SELECT * FROM tst;");
|
||||
|
||||
# Delete all, vacuum, and insert same data
|
||||
$node->safe_psql("postgres", "DELETE FROM tst;");
|
||||
$node->safe_psql("postgres", "VACUUM tst;");
|
||||
$node->safe_psql("postgres", "INSERT INTO tst SELECT * FROM tmp;");
|
||||
$node->safe_psql("postgres",
|
||||
"INSERT INTO tst SELECT i % 10, ARRAY[$array_sql] FROM generate_series(1, 100000) i;"
|
||||
);
|
||||
|
||||
# Check size
|
||||
my $new_size = $node->safe_psql("postgres", "SELECT pg_total_relation_size('tst_v_idx');");
|
||||
|
||||
@@ -94,7 +94,8 @@ like($explain, qr/Seq Scan/);
|
||||
$explain = $node->safe_psql("postgres", qq(
|
||||
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
|
||||
$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 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();
|
||||
|
||||
@@ -18,13 +18,9 @@ $node->start;
|
||||
# Create table and index
|
||||
$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 cat (i int4 PRIMARY KEY, t text, b boolean);");
|
||||
$node->safe_psql("postgres",
|
||||
"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", "ANALYZE tst;");
|
||||
|
||||
@@ -41,7 +37,8 @@ my $c = int(rand() * $nc);
|
||||
my $explain = $node->safe_psql("postgres", qq(
|
||||
EXPLAIN ANALYZE SELECT i FROM tst WHERE c = $c ORDER BY v <-> '$query' LIMIT $limit;
|
||||
));
|
||||
like($explain, qr/Seq Scan/);
|
||||
# TODO Do not use index
|
||||
like($explain, qr/Index Scan using idx/);
|
||||
|
||||
# Test attribute filtering with few rows removed
|
||||
$explain = $node->safe_psql("postgres", qq(
|
||||
@@ -59,7 +56,8 @@ like($explain, qr/Index Scan using idx/);
|
||||
$explain = $node->safe_psql("postgres", qq(
|
||||
EXPLAIN ANALYZE SELECT i FROM tst WHERE c < 1 ORDER BY v <-> '$query' LIMIT $limit;
|
||||
));
|
||||
like($explain, qr/Seq Scan/);
|
||||
# TODO Do not use index
|
||||
like($explain, qr/Index Scan using idx/);
|
||||
|
||||
# Test attribute filtering with few rows removed like
|
||||
$explain = $node->safe_psql("postgres", qq(
|
||||
@@ -98,25 +96,13 @@ $explain = $node->safe_psql("postgres", qq(
|
||||
));
|
||||
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
|
||||
$node->safe_psql("postgres", "CREATE INDEX attribute_idx ON tst (c);");
|
||||
$explain = $node->safe_psql("postgres", qq(
|
||||
EXPLAIN ANALYZE SELECT i FROM tst WHERE c = $c ORDER BY v <-> '$query' LIMIT $limit;
|
||||
));
|
||||
# Use attribute index
|
||||
like($explain, qr/Bitmap Index Scan on attribute_idx/);
|
||||
# TODO Use attribute index
|
||||
like($explain, qr/Index Scan using idx/);
|
||||
|
||||
# Test partial index
|
||||
$node->safe_psql("postgres", "CREATE INDEX partial_idx ON tst USING hnsw (v vector_l2_ops) WHERE (c = $c);");
|
||||
|
||||
@@ -1,60 +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, 2000) 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/);
|
||||
|
||||
# 3x the rows are needed for distance filters
|
||||
# since the planner uses DEFAULT_INEQ_SEL for the selectivity (should be 1)
|
||||
# Recreate index for performance
|
||||
$node->safe_psql("postgres", "DROP INDEX idx;");
|
||||
$node->safe_psql("postgres",
|
||||
"INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(2001, 6000) i;"
|
||||
);
|
||||
$node->safe_psql("postgres", "CREATE INDEX idx ON tst USING hnsw (v vector_l2_ops);");
|
||||
$node->safe_psql("postgres", "ANALYZE tst;");
|
||||
|
||||
$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();
|
||||
@@ -14,7 +14,7 @@ $node->start;
|
||||
|
||||
# Create table
|
||||
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
|
||||
$node->safe_psql("postgres", "CREATE TABLE tst (i int4 PRIMARY KEY, v vector($dim));");
|
||||
$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;"
|
||||
);
|
||||
@@ -23,32 +23,9 @@ $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.iterative_scan = relaxed_order;
|
||||
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);
|
||||
|
||||
foreach ((30, 50, 70))
|
||||
{
|
||||
my $max_probes = $_;
|
||||
my $expected = $max_probes / 10;
|
||||
my $sum = 0;
|
||||
|
||||
for my $i (1 .. 20)
|
||||
{
|
||||
$count = $node->safe_psql("postgres", qq(
|
||||
SET enable_seqscan = off;
|
||||
SET ivfflat.probes = 10;
|
||||
SET ivfflat.iterative_scan = relaxed_order;
|
||||
SET ivfflat.max_probes = $max_probes;
|
||||
SELECT COUNT(*) FROM (SELECT v FROM tst WHERE i % 10000 = 0 ORDER BY v <-> (SELECT v FROM tst WHERE i = $i) LIMIT 11) t;
|
||||
));
|
||||
$sum += $count;
|
||||
}
|
||||
|
||||
my $avg = $sum / 20;
|
||||
cmp_ok($avg, '>', $expected - 2);
|
||||
cmp_ok($avg, '<', $expected + 2);
|
||||
}
|
||||
|
||||
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, 5000) 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();
|
||||
@@ -19,7 +19,7 @@ sub test_recall
|
||||
my $explain = $node->safe_psql("postgres", qq(
|
||||
SET enable_seqscan = off;
|
||||
SET ivfflat.probes = $probes;
|
||||
SET ivfflat.iterative_scan = relaxed_order;
|
||||
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/);
|
||||
@@ -29,7 +29,7 @@ sub test_recall
|
||||
my $actual = $node->safe_psql("postgres", qq(
|
||||
SET enable_seqscan = off;
|
||||
SET ivfflat.probes = $probes;
|
||||
SET ivfflat.iterative_scan = relaxed_order;
|
||||
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);
|
||||
@@ -48,7 +48,7 @@ sub test_recall
|
||||
$total += $limit;
|
||||
}
|
||||
|
||||
cmp_ok($correct / $total, ">=", $min, "$operator $c");
|
||||
cmp_ok($correct / $total, ">=", $min, $operator);
|
||||
}
|
||||
|
||||
# Initialize node
|
||||
@@ -103,7 +103,7 @@ for my $i (0 .. $#operators)
|
||||
|
||||
if ($c == 100)
|
||||
{
|
||||
test_recall($c, 1, 0.57, $operator);
|
||||
test_recall($c, 1, 0.58, $operator);
|
||||
test_recall($c, 10, 0.98, $operator);
|
||||
}
|
||||
else
|
||||
@@ -1,68 +0,0 @@
|
||||
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 PRIMARY KEY, 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", qq(
|
||||
SET maintenance_work_mem = '128MB';
|
||||
SET max_parallel_maintenance_workers = 2;
|
||||
CREATE INDEX ON tst USING hnsw (v vector_l2_ops)
|
||||
));
|
||||
|
||||
my $count = $node->safe_psql("postgres", qq(
|
||||
SET enable_seqscan = off;
|
||||
SET hnsw.iterative_scan = relaxed_order;
|
||||
SET hnsw.max_scan_tuples = 100000;
|
||||
SET hnsw.scan_mem_multiplier = 2;
|
||||
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);
|
||||
|
||||
foreach ((30000, 50000, 70000))
|
||||
{
|
||||
my $max_tuples = $_;
|
||||
my $expected = $max_tuples / 10000;
|
||||
my $sum = 0;
|
||||
|
||||
for my $i (1 .. 20)
|
||||
{
|
||||
$count = $node->safe_psql("postgres", qq(
|
||||
SET enable_seqscan = off;
|
||||
SET hnsw.iterative_scan = relaxed_order;
|
||||
SET hnsw.max_scan_tuples = $max_tuples;
|
||||
SET hnsw.scan_mem_multiplier = 2;
|
||||
SELECT COUNT(*) FROM (SELECT v FROM tst WHERE i % 10000 = 0 ORDER BY v <-> (SELECT v FROM tst WHERE i = $i) LIMIT 11) t;
|
||||
));
|
||||
$sum += $count;
|
||||
}
|
||||
|
||||
my $avg = $sum / 20;
|
||||
cmp_ok($avg, '>', $expected - 2);
|
||||
cmp_ok($avg, '<', $expected + 2);
|
||||
}
|
||||
|
||||
my ($ret, $stdout, $stderr) = $node->psql("postgres", qq(
|
||||
SET enable_seqscan = off;
|
||||
SET hnsw.iterative_scan = relaxed_order;
|
||||
SET client_min_messages = debug1;
|
||||
SET work_mem = '1MB';
|
||||
SELECT COUNT(*) FROM (SELECT v FROM tst WHERE i % 10000 = 0 ORDER BY v <-> (SELECT v FROM tst LIMIT 1) LIMIT 11) t;
|
||||
));
|
||||
like($stderr, qr/hnsw index scan reached memory limit after \d+ tuples/);
|
||||
|
||||
done_testing();
|
||||
@@ -1,118 +0,0 @@
|
||||
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 $dim = 3;
|
||||
my $array_sql = join(",", ('random()') x $dim);
|
||||
my @cs = (50, 500);
|
||||
|
||||
sub test_recall
|
||||
{
|
||||
my ($c, $ef_search, $min, $operator, $mode) = @_;
|
||||
my $correct = 0;
|
||||
my $total = 0;
|
||||
|
||||
my $explain = $node->safe_psql("postgres", qq(
|
||||
SET enable_seqscan = off;
|
||||
SET hnsw.ef_search = $ef_search;
|
||||
SET hnsw.iterative_scan = $mode;
|
||||
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 hnsw.ef_search = $ef_search;
|
||||
SET hnsw.iterative_scan = $mode;
|
||||
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 $mode $c");
|
||||
}
|
||||
|
||||
# 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($dim));");
|
||||
$node->safe_psql("postgres",
|
||||
"INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(1, 50000) i;"
|
||||
);
|
||||
|
||||
# Generate queries
|
||||
for (1 .. 20)
|
||||
{
|
||||
my @r = ();
|
||||
for (1 .. $dim)
|
||||
{
|
||||
push(@r, rand());
|
||||
}
|
||||
push(@queries, "[" . join(",", @r) . "]");
|
||||
}
|
||||
|
||||
# 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", qq(
|
||||
SET maintenance_work_mem = '128MB';
|
||||
CREATE INDEX idx ON tst USING hnsw (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);
|
||||
}
|
||||
|
||||
test_recall($c, 40, 0.99, $operator, "strict_order");
|
||||
test_recall($c, 40, 0.99, $operator, "relaxed_order");
|
||||
}
|
||||
|
||||
$node->safe_psql("postgres", "DROP INDEX idx;");
|
||||
}
|
||||
|
||||
done_testing();
|
||||
Reference in New Issue
Block a user