mirror of
https://github.com/pgvector/pgvector.git
synced 2026-07-26 22:02:41 +08:00
Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2627c5ff77 | ||
|
|
34b3cfdc43 | ||
|
|
cd218aae5a | ||
|
|
ba9367f86c | ||
|
|
9c20550a41 | ||
|
|
e3e74fe94e | ||
|
|
96a5a44632 | ||
|
|
67e1392a83 | ||
|
|
e530a1a026 | ||
|
|
6170e2645b | ||
|
|
e6bae175f1 | ||
|
|
52b777e04a | ||
|
|
307271214f | ||
|
|
6e9f74ddce | ||
|
|
258215ad97 | ||
|
|
fb87b6da91 | ||
|
|
a2a0b377f0 | ||
|
|
2f770307b8 | ||
|
|
c04e16ff5b | ||
|
|
bd4d272f26 | ||
|
|
8bb797cc2f | ||
|
|
fe6ec03dac | ||
|
|
c1161f8889 | ||
|
|
c530a3c490 | ||
|
|
d8b9e8ef73 | ||
|
|
00894efed5 | ||
|
|
0aa0f6619b | ||
|
|
38d053001e | ||
|
|
ccb95407e7 | ||
|
|
04d5e934a1 | ||
|
|
b163b5b196 | ||
|
|
6a30c1e824 | ||
|
|
2db1b19644 | ||
|
|
305d62146e | ||
|
|
f9d627c9a9 | ||
|
|
38f42820be | ||
|
|
15c8245b42 | ||
|
|
572a9ab404 | ||
|
|
00492d7e57 | ||
|
|
857d716d9e | ||
|
|
c5dd2af750 | ||
|
|
78b877bdaf | ||
|
|
7043cce893 | ||
|
|
62039d74f6 | ||
|
|
ac6576e53a | ||
|
|
67eff41c44 | ||
|
|
1291b12090 | ||
|
|
24522700b8 | ||
|
|
bfb3a45b31 | ||
|
|
e718eb8da4 | ||
|
|
049972a4a3 | ||
|
|
61027645e9 | ||
|
|
a41b327b33 | ||
|
|
7f735ebd9b | ||
|
|
02b01e1ca9 | ||
|
|
388e42f6e6 | ||
|
|
bf379eed86 | ||
|
|
e1bc929429 | ||
|
|
38285aacc7 | ||
|
|
a2408e60fa | ||
|
|
53a8734bac | ||
|
|
7484625227 | ||
|
|
d1ebb8db73 | ||
|
|
42af8aa1d1 | ||
|
|
9d15a76b60 | ||
|
|
a3a20f9816 | ||
|
|
b26a21b848 | ||
|
|
2dc392ed6c | ||
|
|
960d2848cb | ||
|
|
8e88b481a6 | ||
|
|
124018b8dd |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -8,8 +8,8 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
# - postgres: 18
|
- postgres: 18
|
||||||
# os: ubuntu-24.04
|
os: ubuntu-24.04
|
||||||
- postgres: 17
|
- postgres: 17
|
||||||
os: ubuntu-24.04
|
os: ubuntu-24.04
|
||||||
- postgres: 16
|
- postgres: 16
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
## 0.8.0 (unreleased)
|
## 0.8.0 (2024-10-30)
|
||||||
|
|
||||||
- Added support for iterative index scans
|
- Added support for iterative index scans
|
||||||
- Added casts for arrays to `sparsevec`
|
- Added casts for arrays to `sparsevec`
|
||||||
- Improved cost estimation
|
- Improved cost estimation for better index selection when filtering
|
||||||
|
- Improved performance of HNSW index scans
|
||||||
- Improved performance of HNSW inserts and on-disk index builds
|
- Improved performance of HNSW inserts and on-disk index builds
|
||||||
- Reduced memory usage for HNSW index scans
|
|
||||||
- Dropped support for Postgres 12
|
- Dropped support for Postgres 12
|
||||||
|
|
||||||
## 0.7.4 (2024-08-05)
|
## 0.7.4 (2024-08-05)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "vector",
|
"name": "vector",
|
||||||
"abstract": "Open-source vector similarity search for Postgres",
|
"abstract": "Open-source vector similarity search for Postgres",
|
||||||
"description": "Supports L2 distance, inner product, and cosine distance",
|
"description": "Supports L2 distance, inner product, and cosine distance",
|
||||||
"version": "0.7.4",
|
"version": "0.8.0",
|
||||||
"maintainer": [
|
"maintainer": [
|
||||||
"Andrew Kane <andrew@ankane.org>"
|
"Andrew Kane <andrew@ankane.org>"
|
||||||
],
|
],
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"prereqs": {
|
"prereqs": {
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"requires": {
|
"requires": {
|
||||||
"PostgreSQL": "12.0.0"
|
"PostgreSQL": "13.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
"vector": {
|
"vector": {
|
||||||
"file": "sql/vector.sql",
|
"file": "sql/vector.sql",
|
||||||
"docfile": "README.md",
|
"docfile": "README.md",
|
||||||
"version": "0.7.4",
|
"version": "0.8.0",
|
||||||
"abstract": "Open-source vector similarity search for Postgres"
|
"abstract": "Open-source vector similarity search for Postgres"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -1,5 +1,5 @@
|
|||||||
EXTENSION = vector
|
EXTENSION = vector
|
||||||
EXTVERSION = 0.7.4
|
EXTVERSION = 0.8.0
|
||||||
|
|
||||||
MODULE_big = vector
|
MODULE_big = vector
|
||||||
DATA = $(wildcard sql/*--*--*.sql)
|
DATA = $(wildcard sql/*--*--*.sql)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
EXTENSION = vector
|
EXTENSION = vector
|
||||||
EXTVERSION = 0.7.4
|
EXTVERSION = 0.8.0
|
||||||
|
|
||||||
DATA_built = sql\$(EXTENSION)--$(EXTVERSION).sql
|
DATA_built = sql\$(EXTENSION)--$(EXTVERSION).sql
|
||||||
OBJS = src\bitutils.obj src\bitvec.obj src\halfutils.obj src\halfvec.obj src\hnsw.obj src\hnswbuild.obj src\hnswinsert.obj src\hnswscan.obj src\hnswutils.obj src\hnswvacuum.obj src\ivfbuild.obj src\ivfflat.obj src\ivfinsert.obj src\ivfkmeans.obj src\ivfscan.obj src\ivfutils.obj src\ivfvacuum.obj src\sparsevec.obj src\vector.obj
|
OBJS = src\bitutils.obj src\bitvec.obj src\halfutils.obj src\halfvec.obj src\hnsw.obj src\hnswbuild.obj src\hnswinsert.obj src\hnswscan.obj src\hnswutils.obj src\hnswvacuum.obj src\ivfbuild.obj src\ivfflat.obj src\ivfinsert.obj src\ivfkmeans.obj src\ivfscan.obj src\ivfutils.obj src\ivfvacuum.obj src\sparsevec.obj src\vector.obj
|
||||||
|
|||||||
116
README.md
116
README.md
@@ -17,11 +17,11 @@ Plus [ACID](https://en.wikipedia.org/wiki/ACID) compliance, point-in-time recove
|
|||||||
|
|
||||||
### Linux and Mac
|
### Linux and Mac
|
||||||
|
|
||||||
Compile and install the extension (supports Postgres 12+)
|
Compile and install the extension (supports Postgres 13+)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd /tmp
|
cd /tmp
|
||||||
git clone --branch v0.7.4 https://github.com/pgvector/pgvector.git
|
git clone --branch v0.8.0 https://github.com/pgvector/pgvector.git
|
||||||
cd pgvector
|
cd pgvector
|
||||||
make
|
make
|
||||||
make install # may need sudo
|
make install # may need sudo
|
||||||
@@ -46,7 +46,7 @@ Then use `nmake` to build:
|
|||||||
```cmd
|
```cmd
|
||||||
set "PGROOT=C:\Program Files\PostgreSQL\16"
|
set "PGROOT=C:\Program Files\PostgreSQL\16"
|
||||||
cd %TEMP%
|
cd %TEMP%
|
||||||
git clone --branch v0.7.4 https://github.com/pgvector/pgvector.git
|
git clone --branch v0.8.0 https://github.com/pgvector/pgvector.git
|
||||||
cd pgvector
|
cd pgvector
|
||||||
nmake /F Makefile.win
|
nmake /F Makefile.win
|
||||||
nmake /F Makefile.win install
|
nmake /F Makefile.win install
|
||||||
@@ -324,7 +324,7 @@ For a large number of workers, you may also need to increase `max_parallel_worke
|
|||||||
|
|
||||||
### Indexing Progress
|
### Indexing Progress
|
||||||
|
|
||||||
Check [indexing progress](https://www.postgresql.org/docs/current/progress-reporting.html#CREATE-INDEX-PROGRESS-REPORTING) with Postgres 12+
|
Check [indexing progress](https://www.postgresql.org/docs/current/progress-reporting.html#CREATE-INDEX-PROGRESS-REPORTING)
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
SELECT phase, round(100.0 * blocks_done / nullif(blocks_total, 0), 1) AS "%" FROM pg_stat_progress_create_index;
|
SELECT phase, round(100.0 * blocks_done / nullif(blocks_total, 0), 1) AS "%" FROM pg_stat_progress_create_index;
|
||||||
@@ -410,7 +410,7 @@ For a large number of workers, you may also need to increase `max_parallel_worke
|
|||||||
|
|
||||||
### Indexing Progress
|
### Indexing Progress
|
||||||
|
|
||||||
Check [indexing progress](https://www.postgresql.org/docs/current/progress-reporting.html#CREATE-INDEX-PROGRESS-REPORTING) with Postgres 12+
|
Check [indexing progress](https://www.postgresql.org/docs/current/progress-reporting.html#CREATE-INDEX-PROGRESS-REPORTING)
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
SELECT phase, round(100.0 * tuples_done / nullif(tuples_total, 0), 1) AS "%" FROM pg_stat_progress_create_index;
|
SELECT phase, round(100.0 * tuples_done / nullif(tuples_total, 0), 1) AS "%" FROM pg_stat_progress_create_index;
|
||||||
@@ -427,30 +427,126 @@ Note: `%` is only populated during the `loading tuples` phase
|
|||||||
|
|
||||||
## Filtering
|
## Filtering
|
||||||
|
|
||||||
There are a few ways to index nearest neighbor queries with a `WHERE` clause
|
There are a few ways to index nearest neighbor queries with a `WHERE` clause.
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
SELECT * FROM items WHERE category_id = 123 ORDER BY embedding <-> '[3,1,2]' LIMIT 5;
|
SELECT * FROM items WHERE category_id = 123 ORDER BY embedding <-> '[3,1,2]' LIMIT 5;
|
||||||
```
|
```
|
||||||
|
|
||||||
Create an index on one [or more](https://www.postgresql.org/docs/current/indexes-multicolumn.html) of the `WHERE` columns for exact search
|
A good place to start is creating an index on the filter column. This can provide fast, exact nearest neighbor search in many cases. Postgres has a number of [index types](https://www.postgresql.org/docs/current/indexes-types.html) for this: B-tree (default), hash, GiST, SP-GiST, GIN, and BRIN.
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
CREATE INDEX ON items (category_id);
|
CREATE INDEX ON items (category_id);
|
||||||
```
|
```
|
||||||
|
|
||||||
Or a [partial index](https://www.postgresql.org/docs/current/indexes-partial.html) on the vector column for approximate search
|
For multiple columns, consider a [multicolumn index](https://www.postgresql.org/docs/current/indexes-multicolumn.html).
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX ON items (location_id, category_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
Exact indexes work well for conditions that match a low percentage of rows. Otherwise, [approximate indexes](#indexing) can work better.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX ON items USING hnsw (embedding vector_l2_ops);
|
||||||
|
```
|
||||||
|
|
||||||
|
With approximate indexes, filtering is applied *after* the index is scanned. If a condition matches 10% of rows, with HNSW and the default `hnsw.ef_search` of 40, only 4 rows will match on average. For more rows, increase `hnsw.ef_search`.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SET hnsw.ef_search = 200;
|
||||||
|
```
|
||||||
|
|
||||||
|
Starting with 0.8.0, you can enable [iterative index scans](#iterative-index-scans), which will automatically scan more of the index when needed.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SET hnsw.iterative_scan = strict_order;
|
||||||
|
```
|
||||||
|
|
||||||
|
If filtering by only a few distinct values, consider [partial indexing](https://www.postgresql.org/docs/current/indexes-partial.html).
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
CREATE INDEX ON items USING hnsw (embedding vector_l2_ops) WHERE (category_id = 123);
|
CREATE INDEX ON items USING hnsw (embedding vector_l2_ops) WHERE (category_id = 123);
|
||||||
```
|
```
|
||||||
|
|
||||||
Use [partitioning](https://www.postgresql.org/docs/current/ddl-partitioning.html) for approximate search on many different values of the `WHERE` columns
|
If filtering by many different values, consider [partitioning](https://www.postgresql.org/docs/current/ddl-partitioning.html).
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
CREATE TABLE items (embedding vector(3), category_id int) PARTITION BY LIST(category_id);
|
CREATE TABLE items (embedding vector(3), category_id int) PARTITION BY LIST(category_id);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Iterative Index Scans
|
||||||
|
|
||||||
|
*Added in 0.8.0*
|
||||||
|
|
||||||
|
With approximate indexes, queries with filtering can return less results since filtering is applied *after* the index is scanned. Starting with 0.8.0, you can enable iterative index scans, which will automatically scan more of the index until enough results are found (or it reaches `hnsw.max_scan_tuples` or `ivfflat.max_probes`).
|
||||||
|
|
||||||
|
Iterative scans can use strict or relaxed ordering.
|
||||||
|
|
||||||
|
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;
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: Try increasing this if increasing `hnsw.max_scan_tuples` does not improve recall
|
||||||
|
|
||||||
|
#### 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
|
## Half-Precision Vectors
|
||||||
|
|
||||||
*Added in 0.7.0*
|
*Added in 0.7.0*
|
||||||
@@ -1057,7 +1153,7 @@ This adds pgvector to the [Postgres image](https://hub.docker.com/_/postgres) (r
|
|||||||
You can also build the image manually:
|
You can also build the image manually:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone --branch v0.7.4 https://github.com/pgvector/pgvector.git
|
git clone --branch v0.8.0 https://github.com/pgvector/pgvector.git
|
||||||
cd pgvector
|
cd pgvector
|
||||||
docker build --pull --build-arg PG_MAJOR=17 -t myuser/pgvector .
|
docker build --pull --build-arg PG_MAJOR=17 -t myuser/pgvector .
|
||||||
```
|
```
|
||||||
|
|||||||
36
src/hnsw.c
36
src/hnsw.c
@@ -18,16 +18,17 @@
|
|||||||
#define MarkGUCPrefixReserved(x) EmitWarningsOnPlaceholders(x)
|
#define MarkGUCPrefixReserved(x) EmitWarningsOnPlaceholders(x)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
static const struct config_enum_entry hnsw_iterative_search_options[] = {
|
static const struct config_enum_entry hnsw_iterative_scan_options[] = {
|
||||||
{"off", HNSW_ITERATIVE_SEARCH_OFF, false},
|
{"off", HNSW_ITERATIVE_SCAN_OFF, false},
|
||||||
{"on", HNSW_ITERATIVE_SEARCH_RELAXED, false},
|
{"relaxed_order", HNSW_ITERATIVE_SCAN_RELAXED, false},
|
||||||
{"strict", HNSW_ITERATIVE_SEARCH_STRICT, false},
|
{"strict_order", HNSW_ITERATIVE_SCAN_STRICT, false},
|
||||||
{NULL, 0, false}
|
{NULL, 0, false}
|
||||||
};
|
};
|
||||||
|
|
||||||
int hnsw_ef_search;
|
int hnsw_ef_search;
|
||||||
int hnsw_iterative_search_max_tuples;
|
int hnsw_iterative_scan;
|
||||||
int hnsw_iterative_search;
|
int hnsw_max_scan_tuples;
|
||||||
|
double hnsw_scan_mem_multiplier;
|
||||||
int hnsw_lock_tranche_id;
|
int hnsw_lock_tranche_id;
|
||||||
static relopt_kind hnsw_relopt_kind;
|
static relopt_kind hnsw_relopt_kind;
|
||||||
|
|
||||||
@@ -78,14 +79,19 @@ HnswInit(void)
|
|||||||
"Valid range is 1..1000.", &hnsw_ef_search,
|
"Valid range is 1..1000.", &hnsw_ef_search,
|
||||||
HNSW_DEFAULT_EF_SEARCH, HNSW_MIN_EF_SEARCH, HNSW_MAX_EF_SEARCH, PGC_USERSET, 0, NULL, NULL, NULL);
|
HNSW_DEFAULT_EF_SEARCH, HNSW_MIN_EF_SEARCH, HNSW_MAX_EF_SEARCH, PGC_USERSET, 0, NULL, NULL, NULL);
|
||||||
|
|
||||||
DefineCustomEnumVariable("hnsw.iterative_search", "Sets iterative search",
|
DefineCustomEnumVariable("hnsw.iterative_scan", "Sets the mode for iterative scans",
|
||||||
NULL, &hnsw_iterative_search,
|
NULL, &hnsw_iterative_scan,
|
||||||
HNSW_ITERATIVE_SEARCH_OFF, hnsw_iterative_search_options, PGC_USERSET, 0, NULL, NULL, NULL);
|
HNSW_ITERATIVE_SCAN_OFF, hnsw_iterative_scan_options, PGC_USERSET, 0, NULL, NULL, NULL);
|
||||||
|
|
||||||
/* TODO Ensure ivfflat.max_probes uses same value for "all" */
|
/* This is approximate and does not affect the initial scan */
|
||||||
DefineCustomIntVariable("hnsw.iterative_search_max_tuples", "Sets the max number of candidates to visit for iterative search",
|
DefineCustomIntVariable("hnsw.max_scan_tuples", "Sets the max number of tuples to visit for iterative scans",
|
||||||
"-1 means all", &hnsw_iterative_search_max_tuples,
|
NULL, &hnsw_max_scan_tuples,
|
||||||
-1, -1, INT_MAX, PGC_USERSET, 0, NULL, NULL, NULL);
|
20000, 1, INT_MAX, PGC_USERSET, 0, 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, 0, NULL, NULL, NULL);
|
||||||
|
|
||||||
MarkGUCPrefixReserved("hnsw");
|
MarkGUCPrefixReserved("hnsw");
|
||||||
}
|
}
|
||||||
@@ -131,6 +137,10 @@ hnswcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
|
|||||||
*indexSelectivity = 0;
|
*indexSelectivity = 0;
|
||||||
*indexCorrelation = 0;
|
*indexCorrelation = 0;
|
||||||
*indexPages = 0;
|
*indexPages = 0;
|
||||||
|
#if PG_VERSION_NUM >= 180000
|
||||||
|
/* See "On disable_cost" thread on pgsql-hackers */
|
||||||
|
path->path.disabled_nodes = 2;
|
||||||
|
#endif
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
16
src/hnsw.h
16
src/hnsw.h
@@ -109,16 +109,17 @@
|
|||||||
|
|
||||||
/* Variables */
|
/* Variables */
|
||||||
extern int hnsw_ef_search;
|
extern int hnsw_ef_search;
|
||||||
extern int hnsw_iterative_search;
|
extern int hnsw_iterative_scan;
|
||||||
extern int hnsw_iterative_search_max_tuples;
|
extern int hnsw_max_scan_tuples;
|
||||||
|
extern double hnsw_scan_mem_multiplier;
|
||||||
extern int hnsw_lock_tranche_id;
|
extern int hnsw_lock_tranche_id;
|
||||||
|
|
||||||
typedef enum HnswIterativeSearchType
|
typedef enum HnswIterativeScanMode
|
||||||
{
|
{
|
||||||
HNSW_ITERATIVE_SEARCH_OFF,
|
HNSW_ITERATIVE_SCAN_OFF,
|
||||||
HNSW_ITERATIVE_SEARCH_RELAXED,
|
HNSW_ITERATIVE_SCAN_RELAXED,
|
||||||
HNSW_ITERATIVE_SEARCH_STRICT
|
HNSW_ITERATIVE_SCAN_STRICT
|
||||||
} HnswIterativeSearchType;
|
} HnswIterativeScanMode;
|
||||||
|
|
||||||
typedef struct HnswElementData HnswElementData;
|
typedef struct HnswElementData HnswElementData;
|
||||||
typedef struct HnswNeighborArray HnswNeighborArray;
|
typedef struct HnswNeighborArray HnswNeighborArray;
|
||||||
@@ -372,6 +373,7 @@ typedef struct HnswScanOpaqueData
|
|||||||
int m;
|
int m;
|
||||||
int64 tuples;
|
int64 tuples;
|
||||||
double previousDistance;
|
double previousDistance;
|
||||||
|
Size maxMemory;
|
||||||
MemoryContext tmpCtx;
|
MemoryContext tmpCtx;
|
||||||
|
|
||||||
/* Support functions */
|
/* Support functions */
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ GetScanItems(IndexScanDesc scan, Datum value)
|
|||||||
ep = w;
|
ep = w;
|
||||||
}
|
}
|
||||||
|
|
||||||
return HnswSearchLayer(base, q, ep, hnsw_ef_search, 0, index, support, m, false, NULL, &so->v, hnsw_iterative_search != HNSW_ITERATIVE_SEARCH_OFF ? &so->discarded : NULL, true, &so->tuples);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -102,6 +102,17 @@ GetScanValue(IndexScanDesc scan)
|
|||||||
return 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
|
* Prepare for an index scan
|
||||||
*/
|
*/
|
||||||
@@ -110,21 +121,29 @@ hnswbeginscan(Relation index, int nkeys, int norderbys)
|
|||||||
{
|
{
|
||||||
IndexScanDesc scan;
|
IndexScanDesc scan;
|
||||||
HnswScanOpaque so;
|
HnswScanOpaque so;
|
||||||
|
double maxMemory;
|
||||||
|
|
||||||
scan = RelationGetIndexScan(index, nkeys, norderbys);
|
scan = RelationGetIndexScan(index, nkeys, norderbys);
|
||||||
|
|
||||||
so = (HnswScanOpaque) palloc(sizeof(HnswScanOpaqueData));
|
so = (HnswScanOpaque) palloc(sizeof(HnswScanOpaqueData));
|
||||||
so->typeInfo = HnswGetTypeInfo(index);
|
so->typeInfo = HnswGetTypeInfo(index);
|
||||||
so->first = true;
|
|
||||||
so->v.tids = NULL;
|
|
||||||
so->discarded = NULL;
|
|
||||||
so->tmpCtx = AllocSetContextCreate(CurrentMemoryContext,
|
|
||||||
"Hnsw scan temporary context",
|
|
||||||
ALLOCSET_DEFAULT_SIZES);
|
|
||||||
|
|
||||||
/* Set support functions */
|
/* Set support functions */
|
||||||
HnswInitSupport(&so->support, index);
|
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->tmpCtx = AllocSetContextCreate(CurrentMemoryContext,
|
||||||
|
"Hnsw scan temporary context",
|
||||||
|
0, 8 * 1024, 256 * 1024);
|
||||||
|
|
||||||
|
/* 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);
|
||||||
|
|
||||||
scan->opaque = so;
|
scan->opaque = so;
|
||||||
|
|
||||||
return scan;
|
return scan;
|
||||||
@@ -138,13 +157,10 @@ hnswrescan(IndexScanDesc scan, ScanKey keys, int nkeys, ScanKey orderbys, int no
|
|||||||
{
|
{
|
||||||
HnswScanOpaque so = (HnswScanOpaque) scan->opaque;
|
HnswScanOpaque so = (HnswScanOpaque) scan->opaque;
|
||||||
|
|
||||||
if (so->v.tids != NULL)
|
|
||||||
tidhash_reset(so->v.tids);
|
|
||||||
|
|
||||||
if (so->discarded != NULL)
|
|
||||||
pairingheap_reset(so->discarded);
|
|
||||||
|
|
||||||
so->first = true;
|
so->first = true;
|
||||||
|
/* v and discarded are allocated in tmpCtx */
|
||||||
|
so->v.tids = NULL;
|
||||||
|
so->discarded = NULL;
|
||||||
so->tuples = 0;
|
so->tuples = 0;
|
||||||
so->previousDistance = -get_float8_infinity();
|
so->previousDistance = -get_float8_infinity();
|
||||||
MemoryContextReset(so->tmpCtx);
|
MemoryContextReset(so->tmpCtx);
|
||||||
@@ -204,7 +220,7 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir)
|
|||||||
so->first = false;
|
so->first = false;
|
||||||
|
|
||||||
#if defined(HNSW_MEMORY)
|
#if defined(HNSW_MEMORY)
|
||||||
elog(INFO, "memory: %zu KB", MemoryContextMemAllocated(so->tmpCtx, false) / 1024);
|
ShowMemoryUsage(so);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,15 +233,15 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir)
|
|||||||
|
|
||||||
if (list_length(so->w) == 0)
|
if (list_length(so->w) == 0)
|
||||||
{
|
{
|
||||||
if (hnsw_iterative_search == HNSW_ITERATIVE_SEARCH_OFF)
|
if (hnsw_iterative_scan == HNSW_ITERATIVE_SCAN_OFF)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
/* Empty index */
|
/* Empty index */
|
||||||
if (so->discarded == NULL)
|
if (so->discarded == NULL)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
/* Reached max number of additional tuples */
|
/* Reached max number of tuples or memory limit */
|
||||||
if (hnsw_iterative_search_max_tuples != -1 && so->tuples >= hnsw_iterative_search_max_tuples)
|
if (so->tuples >= hnsw_max_scan_tuples || MemoryContextMemAllocated(so->tmpCtx, false) > so->maxMemory)
|
||||||
{
|
{
|
||||||
if (pairingheap_is_empty(so->discarded))
|
if (pairingheap_is_empty(so->discarded))
|
||||||
break;
|
break;
|
||||||
@@ -233,21 +249,6 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir)
|
|||||||
/* Return remaining tuples */
|
/* Return remaining tuples */
|
||||||
so->w = lappend(so->w, HnswGetSearchCandidate(w_node, pairingheap_remove_first(so->discarded)));
|
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) > (Size) work_mem * 1024L)
|
|
||||||
{
|
|
||||||
if (pairingheap_is_empty(so->discarded))
|
|
||||||
{
|
|
||||||
ereport(DEBUG1,
|
|
||||||
(errmsg("hnsw index scan exceeded work_mem after " INT64_FORMAT " tuples", so->tuples),
|
|
||||||
errhint("Increase work_mem to scan more tuples.")));
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Return remaining tuples */
|
|
||||||
so->w = lappend(so->w, HnswGetSearchCandidate(w_node, pairingheap_remove_first(so->discarded)));
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
@@ -266,7 +267,7 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir)
|
|||||||
UnlockPage(scan->indexRelation, HNSW_SCAN_LOCK, ShareLock);
|
UnlockPage(scan->indexRelation, HNSW_SCAN_LOCK, ShareLock);
|
||||||
|
|
||||||
#if defined(HNSW_MEMORY)
|
#if defined(HNSW_MEMORY)
|
||||||
elog(INFO, "memory: %zu KB", MemoryContextMemAllocated(so->tmpCtx, false) / 1024);
|
ShowMemoryUsage(so);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,7 +284,7 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir)
|
|||||||
so->w = list_delete_last(so->w);
|
so->w = list_delete_last(so->w);
|
||||||
|
|
||||||
/* Mark memory as free for next iteration */
|
/* Mark memory as free for next iteration */
|
||||||
if (hnsw_iterative_search != HNSW_ITERATIVE_SEARCH_OFF)
|
if (hnsw_iterative_scan != HNSW_ITERATIVE_SCAN_OFF)
|
||||||
{
|
{
|
||||||
pfree(element);
|
pfree(element);
|
||||||
pfree(sc);
|
pfree(sc);
|
||||||
@@ -294,7 +295,7 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir)
|
|||||||
|
|
||||||
heaptid = &element->heaptids[--element->heaptidsLength];
|
heaptid = &element->heaptids[--element->heaptidsLength];
|
||||||
|
|
||||||
if (hnsw_iterative_search == HNSW_ITERATIVE_SEARCH_STRICT)
|
if (hnsw_iterative_scan == HNSW_ITERATIVE_SCAN_STRICT)
|
||||||
{
|
{
|
||||||
if (sc->distance < so->previousDistance)
|
if (sc->distance < so->previousDistance)
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -581,21 +581,34 @@ GetElementDistance(char *base, HnswElement element, HnswQuery * q, HnswSupport *
|
|||||||
return HnswGetDistance(q->value, value, support);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Create a candidate for the entry point
|
* Create a candidate for the entry point
|
||||||
*/
|
*/
|
||||||
HnswSearchCandidate *
|
HnswSearchCandidate *
|
||||||
HnswEntryCandidate(char *base, HnswElement entryPoint, HnswQuery * q, Relation index, HnswSupport * support, bool loadVec)
|
HnswEntryCandidate(char *base, HnswElement entryPoint, HnswQuery * q, Relation index, HnswSupport * support, bool loadVec)
|
||||||
{
|
{
|
||||||
HnswSearchCandidate *sc = palloc(sizeof(HnswSearchCandidate));
|
|
||||||
bool inMemory = index == NULL;
|
bool inMemory = index == NULL;
|
||||||
|
double distance;
|
||||||
|
|
||||||
HnswPtrStore(base, sc->element, entryPoint);
|
|
||||||
if (inMemory)
|
if (inMemory)
|
||||||
sc->distance = GetElementDistance(base, entryPoint, q, support);
|
distance = GetElementDistance(base, entryPoint, q, support);
|
||||||
else
|
else
|
||||||
HnswLoadElement(entryPoint, &sc->distance, q, index, support, loadVec, NULL);
|
HnswLoadElement(entryPoint, &distance, q, index, support, loadVec, NULL);
|
||||||
return sc;
|
|
||||||
|
return HnswInitSearchCandidate(base, entryPoint, distance);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -844,6 +857,7 @@ HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation in
|
|||||||
{
|
{
|
||||||
AddToVisited(base, v, sc->element, inMemory, &found);
|
AddToVisited(base, v, sc->element, inMemory, &found);
|
||||||
|
|
||||||
|
/* OK to count elements instead of tuples */
|
||||||
if (tuples != NULL)
|
if (tuples != NULL)
|
||||||
(*tuples)++;
|
(*tuples)++;
|
||||||
}
|
}
|
||||||
@@ -876,6 +890,7 @@ HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation in
|
|||||||
else
|
else
|
||||||
HnswLoadUnvisitedFromDisk(cElement, unvisited, &unvisitedLength, v, index, m, lm, lc);
|
HnswLoadUnvisitedFromDisk(cElement, unvisited, &unvisitedLength, v, index, m, lm, lc);
|
||||||
|
|
||||||
|
/* OK to count elements instead of tuples */
|
||||||
if (tuples != NULL)
|
if (tuples != NULL)
|
||||||
(*tuples) += unvisitedLength;
|
(*tuples) += unvisitedLength;
|
||||||
|
|
||||||
@@ -912,9 +927,7 @@ HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation in
|
|||||||
if (discarded != NULL)
|
if (discarded != NULL)
|
||||||
{
|
{
|
||||||
/* Create a new candidate */
|
/* Create a new candidate */
|
||||||
e = palloc(sizeof(HnswSearchCandidate));
|
e = HnswInitSearchCandidate(base, eElement, eDistance);
|
||||||
HnswPtrStore(base, e->element, eElement);
|
|
||||||
e->distance = eDistance;
|
|
||||||
pairingheap_add(*discarded, &e->w_node);
|
pairingheap_add(*discarded, &e->w_node);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -926,9 +939,7 @@ HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation in
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
/* Create a new candidate */
|
/* Create a new candidate */
|
||||||
e = palloc(sizeof(HnswSearchCandidate));
|
e = HnswInitSearchCandidate(base, eElement, eDistance);
|
||||||
HnswPtrStore(base, e->element, eElement);
|
|
||||||
e->distance = eDistance;
|
|
||||||
pairingheap_add(C, &e->c_node);
|
pairingheap_add(C, &e->c_node);
|
||||||
pairingheap_add(W, &e->w_node);
|
pairingheap_add(W, &e->w_node);
|
||||||
|
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ SampleRows(IvfflatBuildState * buildstate)
|
|||||||
* Add tuple to sort
|
* Add tuple to sort
|
||||||
*/
|
*/
|
||||||
static void
|
static void
|
||||||
AddTupleToSort(Relation index, ItemPointer tid, Datum *values, bool *isnull, IvfflatBuildState * buildstate)
|
AddTupleToSort(Relation index, ItemPointer tid, Datum *values, IvfflatBuildState * buildstate)
|
||||||
{
|
{
|
||||||
double distance;
|
double distance;
|
||||||
double minDistance = DBL_MAX;
|
double minDistance = DBL_MAX;
|
||||||
@@ -184,11 +184,6 @@ AddTupleToSort(Relation index, ItemPointer tid, Datum *values, bool *isnull, Ivf
|
|||||||
slot->tts_isnull[1] = false;
|
slot->tts_isnull[1] = false;
|
||||||
slot->tts_values[2] = value;
|
slot->tts_values[2] = value;
|
||||||
slot->tts_isnull[2] = false;
|
slot->tts_isnull[2] = false;
|
||||||
for (int i = 1; i < buildstate->tupdesc->natts; i++)
|
|
||||||
{
|
|
||||||
slot->tts_values[2 + i] = values[i];
|
|
||||||
slot->tts_isnull[2 + i] = isnull[i];
|
|
||||||
}
|
|
||||||
ExecStoreVirtualTuple(slot);
|
ExecStoreVirtualTuple(slot);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -220,7 +215,7 @@ BuildCallback(Relation index, ItemPointer tid, Datum *values,
|
|||||||
oldCtx = MemoryContextSwitchTo(buildstate->tmpCtx);
|
oldCtx = MemoryContextSwitchTo(buildstate->tmpCtx);
|
||||||
|
|
||||||
/* Add tuple to sort */
|
/* Add tuple to sort */
|
||||||
AddTupleToSort(index, tid, values, isnull, buildstate);
|
AddTupleToSort(index, tid, values, buildstate);
|
||||||
|
|
||||||
/* Reset memory context */
|
/* Reset memory context */
|
||||||
MemoryContextSwitchTo(oldCtx);
|
MemoryContextSwitchTo(oldCtx);
|
||||||
@@ -231,20 +226,19 @@ BuildCallback(Relation index, ItemPointer tid, Datum *values,
|
|||||||
* Get index tuple from sort state
|
* Get index tuple from sort state
|
||||||
*/
|
*/
|
||||||
static inline void
|
static inline void
|
||||||
GetNextTuple(Tuplesortstate *sortstate, TupleDesc tupdesc, TupleTableSlot *slot, Datum *values, bool *isnull, IndexTuple *itup, int *list)
|
GetNextTuple(Tuplesortstate *sortstate, TupleDesc tupdesc, TupleTableSlot *slot, IndexTuple *itup, int *list)
|
||||||
{
|
{
|
||||||
if (tuplesort_gettupleslot(sortstate, true, false, slot, NULL))
|
if (tuplesort_gettupleslot(sortstate, true, false, slot, NULL))
|
||||||
{
|
{
|
||||||
bool unused;
|
Datum value;
|
||||||
|
bool isnull;
|
||||||
|
|
||||||
*list = DatumGetInt32(slot_getattr(slot, 1, &unused));
|
*list = DatumGetInt32(slot_getattr(slot, 1, &isnull));
|
||||||
|
value = slot_getattr(slot, 3, &isnull);
|
||||||
for (int i = 0; i < tupdesc->natts; i++)
|
|
||||||
values[i] = slot_getattr(slot, 3 + i, &isnull[i]);
|
|
||||||
|
|
||||||
/* Form the index tuple */
|
/* Form the index tuple */
|
||||||
*itup = index_form_tuple(tupdesc, values, isnull);
|
*itup = index_form_tuple(tupdesc, &value, &isnull);
|
||||||
(*itup)->t_tid = *((ItemPointer) DatumGetPointer(slot_getattr(slot, 2, &unused)));
|
(*itup)->t_tid = *((ItemPointer) DatumGetPointer(slot_getattr(slot, 2, &isnull)));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
*list = -1;
|
*list = -1;
|
||||||
@@ -262,14 +256,12 @@ InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum)
|
|||||||
|
|
||||||
TupleTableSlot *slot = MakeSingleTupleTableSlot(buildstate->sortdesc, &TTSOpsMinimalTuple);
|
TupleTableSlot *slot = MakeSingleTupleTableSlot(buildstate->sortdesc, &TTSOpsMinimalTuple);
|
||||||
TupleDesc tupdesc = buildstate->tupdesc;
|
TupleDesc tupdesc = buildstate->tupdesc;
|
||||||
Datum *values = palloc(tupdesc->natts * sizeof(Datum));
|
|
||||||
bool *isnull = palloc(tupdesc->natts * sizeof(bool));
|
|
||||||
|
|
||||||
pgstat_progress_update_param(PROGRESS_CREATEIDX_SUBPHASE, PROGRESS_IVFFLAT_PHASE_LOAD);
|
pgstat_progress_update_param(PROGRESS_CREATEIDX_SUBPHASE, PROGRESS_IVFFLAT_PHASE_LOAD);
|
||||||
|
|
||||||
pgstat_progress_update_param(PROGRESS_CREATEIDX_TUPLES_TOTAL, buildstate->indtuples);
|
pgstat_progress_update_param(PROGRESS_CREATEIDX_TUPLES_TOTAL, buildstate->indtuples);
|
||||||
|
|
||||||
GetNextTuple(buildstate->sortstate, tupdesc, slot, values, isnull, &itup, &list);
|
GetNextTuple(buildstate->sortstate, tupdesc, slot, &itup, &list);
|
||||||
|
|
||||||
for (int i = 0; i < buildstate->centers->length; i++)
|
for (int i = 0; i < buildstate->centers->length; i++)
|
||||||
{
|
{
|
||||||
@@ -305,7 +297,7 @@ InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum)
|
|||||||
|
|
||||||
pgstat_progress_update_param(PROGRESS_CREATEIDX_TUPLES_DONE, ++inserted);
|
pgstat_progress_update_param(PROGRESS_CREATEIDX_TUPLES_DONE, ++inserted);
|
||||||
|
|
||||||
GetNextTuple(buildstate->sortstate, tupdesc, slot, values, isnull, &itup, &list);
|
GetNextTuple(buildstate->sortstate, tupdesc, slot, &itup, &list);
|
||||||
}
|
}
|
||||||
|
|
||||||
insertPage = BufferGetBlockNumber(buf);
|
insertPage = BufferGetBlockNumber(buf);
|
||||||
@@ -315,9 +307,6 @@ InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum)
|
|||||||
/* Set the start and insert pages */
|
/* Set the start and insert pages */
|
||||||
IvfflatUpdateList(index, buildstate->listInfo[i], insertPage, InvalidBlockNumber, startPage, forkNum);
|
IvfflatUpdateList(index, buildstate->listInfo[i], insertPage, InvalidBlockNumber, startPage, forkNum);
|
||||||
}
|
}
|
||||||
|
|
||||||
pfree(values);
|
|
||||||
pfree(isnull);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -368,11 +357,10 @@ InitBuildState(IvfflatBuildState * buildstate, Relation heap, Relation index, In
|
|||||||
errmsg("dimensions must be greater than one for this opclass")));
|
errmsg("dimensions must be greater than one for this opclass")));
|
||||||
|
|
||||||
/* Create tuple description for sorting */
|
/* Create tuple description for sorting */
|
||||||
buildstate->sortdesc = CreateTemplateTupleDesc(2 + buildstate->tupdesc->natts);
|
buildstate->sortdesc = CreateTemplateTupleDesc(3);
|
||||||
TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 1, "list", INT4OID, -1, 0);
|
TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 1, "list", INT4OID, -1, 0);
|
||||||
TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 2, "tid", TIDOID, -1, 0);
|
TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 2, "tid", TIDOID, -1, 0);
|
||||||
for (int i = 0; i < buildstate->tupdesc->natts; i++)
|
TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 3, "vector", buildstate->tupdesc->attrs[0].atttypid, -1, 0);
|
||||||
TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) (3 + i), NULL, buildstate->tupdesc->attrs[i].atttypid, -1, 0);
|
|
||||||
|
|
||||||
buildstate->slot = MakeSingleTupleTableSlot(buildstate->sortdesc, &TTSOpsVirtual);
|
buildstate->slot = MakeSingleTupleTableSlot(buildstate->sortdesc, &TTSOpsVirtual);
|
||||||
|
|
||||||
|
|||||||
@@ -17,13 +17,13 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
int ivfflat_probes;
|
int ivfflat_probes;
|
||||||
int ivfflat_iterative_search;
|
int ivfflat_iterative_scan;
|
||||||
int ivfflat_iterative_search_max_probes;
|
int ivfflat_max_probes;
|
||||||
static relopt_kind ivfflat_relopt_kind;
|
static relopt_kind ivfflat_relopt_kind;
|
||||||
|
|
||||||
static const struct config_enum_entry ivfflat_iterative_search_options[] = {
|
static const struct config_enum_entry ivfflat_iterative_scan_options[] = {
|
||||||
{"off", IVFFLAT_ITERATIVE_SEARCH_OFF, false},
|
{"off", IVFFLAT_ITERATIVE_SCAN_OFF, false},
|
||||||
{"on", IVFFLAT_ITERATIVE_SEARCH_RELAXED, false},
|
{"relaxed_order", IVFFLAT_ITERATIVE_SCAN_RELAXED, false},
|
||||||
{NULL, 0, false}
|
{NULL, 0, false}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -41,13 +41,14 @@ IvfflatInit(void)
|
|||||||
"Valid range is 1..lists.", &ivfflat_probes,
|
"Valid range is 1..lists.", &ivfflat_probes,
|
||||||
IVFFLAT_DEFAULT_PROBES, IVFFLAT_MIN_LISTS, IVFFLAT_MAX_LISTS, PGC_USERSET, 0, NULL, NULL, NULL);
|
IVFFLAT_DEFAULT_PROBES, IVFFLAT_MIN_LISTS, IVFFLAT_MAX_LISTS, PGC_USERSET, 0, NULL, NULL, NULL);
|
||||||
|
|
||||||
DefineCustomEnumVariable("ivfflat.iterative_search", "Sets whether to use iterative search",
|
DefineCustomEnumVariable("ivfflat.iterative_scan", "Sets the mode for iterative scans",
|
||||||
NULL, &ivfflat_iterative_search,
|
NULL, &ivfflat_iterative_scan,
|
||||||
IVFFLAT_ITERATIVE_SEARCH_OFF, ivfflat_iterative_search_options, PGC_USERSET, 0, NULL, NULL, NULL);
|
IVFFLAT_ITERATIVE_SCAN_OFF, ivfflat_iterative_scan_options, PGC_USERSET, 0, NULL, NULL, NULL);
|
||||||
|
|
||||||
DefineCustomIntVariable("ivfflat.iterative_search_max_probes", "Sets the max number of probes for iterative search",
|
/* If this is less than probes, probes is used */
|
||||||
"Zero sets to the number of lists", &ivfflat_iterative_search_max_probes,
|
DefineCustomIntVariable("ivfflat.max_probes", "Sets the max number of probes for iterative scans",
|
||||||
0, 0, IVFFLAT_MAX_LISTS, PGC_USERSET, 0, NULL, NULL, NULL);
|
NULL, &ivfflat_max_probes,
|
||||||
|
IVFFLAT_MAX_LISTS, IVFFLAT_MIN_LISTS, IVFFLAT_MAX_LISTS, PGC_USERSET, 0, NULL, NULL, NULL);
|
||||||
|
|
||||||
MarkGUCPrefixReserved("ivfflat");
|
MarkGUCPrefixReserved("ivfflat");
|
||||||
}
|
}
|
||||||
@@ -98,6 +99,10 @@ ivfflatcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
|
|||||||
*indexSelectivity = 0;
|
*indexSelectivity = 0;
|
||||||
*indexCorrelation = 0;
|
*indexCorrelation = 0;
|
||||||
*indexPages = 0;
|
*indexPages = 0;
|
||||||
|
#if PG_VERSION_NUM >= 180000
|
||||||
|
/* See "On disable_cost" thread on pgsql-hackers */
|
||||||
|
path->path.disabled_nodes = 2;
|
||||||
|
#endif
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,14 +80,14 @@
|
|||||||
|
|
||||||
/* Variables */
|
/* Variables */
|
||||||
extern int ivfflat_probes;
|
extern int ivfflat_probes;
|
||||||
extern int ivfflat_iterative_search;
|
extern int ivfflat_iterative_scan;
|
||||||
extern int ivfflat_iterative_search_max_probes;
|
extern int ivfflat_max_probes;
|
||||||
|
|
||||||
typedef enum IvfflatIterativeSearchType
|
typedef enum IvfflatIterativeScanMode
|
||||||
{
|
{
|
||||||
IVFFLAT_ITERATIVE_SEARCH_OFF,
|
IVFFLAT_ITERATIVE_SCAN_OFF,
|
||||||
IVFFLAT_ITERATIVE_SEARCH_RELAXED
|
IVFFLAT_ITERATIVE_SCAN_RELAXED
|
||||||
} IvfflatIterativeSearchType;
|
} IvfflatIterativeScanMode;
|
||||||
|
|
||||||
typedef struct VectorArrayData
|
typedef struct VectorArrayData
|
||||||
{
|
{
|
||||||
@@ -260,6 +260,7 @@ typedef struct IvfflatScanOpaqueData
|
|||||||
int dimensions;
|
int dimensions;
|
||||||
bool first;
|
bool first;
|
||||||
Datum value;
|
Datum value;
|
||||||
|
MemoryContext tmpCtx;
|
||||||
|
|
||||||
/* Sorting */
|
/* Sorting */
|
||||||
Tuplesortstate *sortstate;
|
Tuplesortstate *sortstate;
|
||||||
@@ -278,7 +279,7 @@ typedef struct IvfflatScanOpaqueData
|
|||||||
pairingheap *listQueue;
|
pairingheap *listQueue;
|
||||||
BlockNumber *listPages;
|
BlockNumber *listPages;
|
||||||
int listIndex;
|
int listIndex;
|
||||||
IvfflatScanList lists[FLEXIBLE_ARRAY_MEMBER]; /* must come last */
|
IvfflatScanList *lists;
|
||||||
} IvfflatScanOpaqueData;
|
} IvfflatScanOpaqueData;
|
||||||
|
|
||||||
typedef IvfflatScanOpaqueData * IvfflatScanOpaque;
|
typedef IvfflatScanOpaqueData * IvfflatScanOpaque;
|
||||||
|
|||||||
@@ -78,8 +78,6 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heap_tid, R
|
|||||||
BlockNumber insertPage = InvalidBlockNumber;
|
BlockNumber insertPage = InvalidBlockNumber;
|
||||||
ListInfo listInfo;
|
ListInfo listInfo;
|
||||||
BlockNumber originalInsertPage;
|
BlockNumber originalInsertPage;
|
||||||
TupleDesc tupdesc = RelationGetDescr(index);
|
|
||||||
Datum *newValues = palloc(tupdesc->natts * sizeof(Datum));
|
|
||||||
|
|
||||||
/* Detoast once for all calls */
|
/* Detoast once for all calls */
|
||||||
value = PointerGetDatum(PG_DETOAST_DATUM(values[0]));
|
value = PointerGetDatum(PG_DETOAST_DATUM(values[0]));
|
||||||
@@ -104,12 +102,8 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heap_tid, R
|
|||||||
Assert(BlockNumberIsValid(insertPage));
|
Assert(BlockNumberIsValid(insertPage));
|
||||||
originalInsertPage = insertPage;
|
originalInsertPage = insertPage;
|
||||||
|
|
||||||
newValues[0] = value;
|
|
||||||
for (int i = 1; i < tupdesc->natts; i++)
|
|
||||||
newValues[i] = values[i];
|
|
||||||
|
|
||||||
/* Form tuple */
|
/* Form tuple */
|
||||||
itup = index_form_tuple(tupdesc, newValues, isnull);
|
itup = index_form_tuple(RelationGetDescr(index), &value, isnull);
|
||||||
itup->t_tid = *heap_tid;
|
itup->t_tid = *heap_tid;
|
||||||
|
|
||||||
/* Get tuple size */
|
/* Get tuple size */
|
||||||
|
|||||||
@@ -10,10 +10,7 @@
|
|||||||
#include "miscadmin.h"
|
#include "miscadmin.h"
|
||||||
#include "pgstat.h"
|
#include "pgstat.h"
|
||||||
#include "storage/bufmgr.h"
|
#include "storage/bufmgr.h"
|
||||||
|
|
||||||
#ifdef IVFFLAT_MEMORY
|
|
||||||
#include "utils/memutils.h"
|
#include "utils/memutils.h"
|
||||||
#endif
|
|
||||||
|
|
||||||
#define GetScanList(ptr) pairingheap_container(IvfflatScanList, ph_node, ptr)
|
#define GetScanList(ptr) pairingheap_container(IvfflatScanList, ph_node, ptr)
|
||||||
#define GetScanListConst(ptr) pairingheap_const_container(IvfflatScanList, ph_node, ptr)
|
#define GetScanListConst(ptr) pairingheap_const_container(IvfflatScanList, ph_node, ptr)
|
||||||
@@ -117,7 +114,6 @@ GetScanItems(IndexScanDesc scan, Datum value)
|
|||||||
{
|
{
|
||||||
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
|
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
|
||||||
TupleDesc tupdesc = RelationGetDescr(scan->indexRelation);
|
TupleDesc tupdesc = RelationGetDescr(scan->indexRelation);
|
||||||
double tuples = 0;
|
|
||||||
TupleTableSlot *slot = so->vslot;
|
TupleTableSlot *slot = so->vslot;
|
||||||
int batchProbes = 0;
|
int batchProbes = 0;
|
||||||
|
|
||||||
@@ -164,8 +160,6 @@ GetScanItems(IndexScanDesc scan, Datum value)
|
|||||||
ExecStoreVirtualTuple(slot);
|
ExecStoreVirtualTuple(slot);
|
||||||
|
|
||||||
tuplesort_puttupleslot(so->sortstate, slot);
|
tuplesort_puttupleslot(so->sortstate, slot);
|
||||||
|
|
||||||
tuples++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
searchPage = IvfflatPageGetOpaque(page)->nextblkno;
|
searchPage = IvfflatPageGetOpaque(page)->nextblkno;
|
||||||
@@ -174,12 +168,6 @@ GetScanItems(IndexScanDesc scan, Datum value)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tuples < 100 && ivfflat_iterative_search == IVFFLAT_ITERATIVE_SEARCH_OFF)
|
|
||||||
ereport(DEBUG1,
|
|
||||||
(errmsg("index scan found few tuples"),
|
|
||||||
errdetail("Index may have been created with little data."),
|
|
||||||
errhint("Recreate the index and possibly decrease lists.")));
|
|
||||||
|
|
||||||
tuplesort_performsort(so->sortstate);
|
tuplesort_performsort(so->sortstate);
|
||||||
|
|
||||||
#if defined(IVFFLAT_MEMORY)
|
#if defined(IVFFLAT_MEMORY)
|
||||||
@@ -221,7 +209,13 @@ GetScanValue(IndexScanDesc scan)
|
|||||||
|
|
||||||
/* Normalize if needed */
|
/* Normalize if needed */
|
||||||
if (so->normprocinfo != NULL)
|
if (so->normprocinfo != NULL)
|
||||||
|
{
|
||||||
|
MemoryContext oldCtx = MemoryContextSwitchTo(so->tmpCtx);
|
||||||
|
|
||||||
value = IvfflatNormValue(so->typeInfo, so->collation, value);
|
value = IvfflatNormValue(so->typeInfo, so->collation, value);
|
||||||
|
|
||||||
|
MemoryContextSwitchTo(oldCtx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
@@ -253,26 +247,25 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys)
|
|||||||
int dimensions;
|
int dimensions;
|
||||||
int probes = ivfflat_probes;
|
int probes = ivfflat_probes;
|
||||||
int maxProbes;
|
int maxProbes;
|
||||||
|
MemoryContext oldCtx;
|
||||||
|
|
||||||
scan = RelationGetIndexScan(index, nkeys, norderbys);
|
scan = RelationGetIndexScan(index, nkeys, norderbys);
|
||||||
|
|
||||||
/* Get lists and dimensions from metapage */
|
/* Get lists and dimensions from metapage */
|
||||||
IvfflatGetMetaPageInfo(index, &lists, &dimensions);
|
IvfflatGetMetaPageInfo(index, &lists, &dimensions);
|
||||||
|
|
||||||
if (probes > lists)
|
if (ivfflat_iterative_scan != IVFFLAT_ITERATIVE_SCAN_OFF)
|
||||||
probes = lists;
|
maxProbes = Max(ivfflat_max_probes, probes);
|
||||||
|
|
||||||
if (ivfflat_iterative_search != IVFFLAT_ITERATIVE_SEARCH_OFF)
|
|
||||||
{
|
|
||||||
if (ivfflat_iterative_search_max_probes == 0)
|
|
||||||
maxProbes = lists;
|
|
||||||
else
|
|
||||||
maxProbes = Min(ivfflat_iterative_search_max_probes, lists);
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
maxProbes = probes;
|
maxProbes = probes;
|
||||||
|
|
||||||
so = (IvfflatScanOpaque) palloc(offsetof(IvfflatScanOpaqueData, lists) + maxProbes * sizeof(IvfflatScanList));
|
if (probes > lists)
|
||||||
|
probes = lists;
|
||||||
|
|
||||||
|
if (maxProbes > lists)
|
||||||
|
maxProbes = lists;
|
||||||
|
|
||||||
|
so = (IvfflatScanOpaque) palloc(sizeof(IvfflatScanOpaqueData));
|
||||||
so->typeInfo = IvfflatGetTypeInfo(index);
|
so->typeInfo = IvfflatGetTypeInfo(index);
|
||||||
so->first = true;
|
so->first = true;
|
||||||
so->probes = probes;
|
so->probes = probes;
|
||||||
@@ -284,6 +277,12 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys)
|
|||||||
so->normprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_NORM_PROC);
|
so->normprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_NORM_PROC);
|
||||||
so->collation = index->rd_indcollation[0];
|
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 */
|
/* Create tuple description for sorting */
|
||||||
so->tupdesc = CreateTemplateTupleDesc(2);
|
so->tupdesc = CreateTemplateTupleDesc(2);
|
||||||
TupleDescInitEntry(so->tupdesc, (AttrNumber) 1, "distance", FLOAT8OID, -1, 0);
|
TupleDescInitEntry(so->tupdesc, (AttrNumber) 1, "distance", FLOAT8OID, -1, 0);
|
||||||
@@ -306,6 +305,9 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys)
|
|||||||
so->listQueue = pairingheap_allocate(CompareLists, scan);
|
so->listQueue = pairingheap_allocate(CompareLists, scan);
|
||||||
so->listPages = palloc(maxProbes * sizeof(BlockNumber));
|
so->listPages = palloc(maxProbes * sizeof(BlockNumber));
|
||||||
so->listIndex = 0;
|
so->listIndex = 0;
|
||||||
|
so->lists = palloc(maxProbes * sizeof(IvfflatScanList));
|
||||||
|
|
||||||
|
MemoryContextSwitchTo(oldCtx);
|
||||||
|
|
||||||
scan->opaque = so;
|
scan->opaque = so;
|
||||||
|
|
||||||
@@ -368,8 +370,6 @@ ivfflatgettuple(IndexScanDesc scan, ScanDirection dir)
|
|||||||
IvfflatBench("GetScanItems", GetScanItems(scan, value));
|
IvfflatBench("GetScanItems", GetScanItems(scan, value));
|
||||||
so->first = false;
|
so->first = false;
|
||||||
so->value = value;
|
so->value = value;
|
||||||
|
|
||||||
/* TODO clean up if we allocated a new value */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
while (!tuplesort_gettupleslot(so->sortstate, true, false, so->mslot, NULL))
|
while (!tuplesort_gettupleslot(so->sortstate, true, false, so->mslot, NULL))
|
||||||
@@ -396,13 +396,10 @@ ivfflatendscan(IndexScanDesc scan)
|
|||||||
{
|
{
|
||||||
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
|
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
|
||||||
|
|
||||||
pairingheap_free(so->listQueue);
|
/* Free any temporary files */
|
||||||
pfree(so->listPages);
|
|
||||||
tuplesort_end(so->sortstate);
|
tuplesort_end(so->sortstate);
|
||||||
FreeAccessStrategy(so->bas);
|
|
||||||
FreeTupleDesc(so->tupdesc);
|
|
||||||
|
|
||||||
/* TODO Free vslot and mslot without freeing TupleDesc */
|
MemoryContextDelete(so->tmpCtx);
|
||||||
|
|
||||||
pfree(so);
|
pfree(so);
|
||||||
scan->opaque = NULL;
|
scan->opaque = NULL;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <math.h>
|
#include <math.h>
|
||||||
|
|
||||||
#include "catalog/pg_type.h"
|
#include "catalog/pg_type.h"
|
||||||
|
#include "common/shortest_dec.h"
|
||||||
#include "common/string.h"
|
#include "common/string.h"
|
||||||
#include "fmgr.h"
|
#include "fmgr.h"
|
||||||
#include "halfutils.h"
|
#include "halfutils.h"
|
||||||
@@ -12,17 +13,10 @@
|
|||||||
#include "sparsevec.h"
|
#include "sparsevec.h"
|
||||||
#include "utils/array.h"
|
#include "utils/array.h"
|
||||||
#include "utils/builtins.h"
|
#include "utils/builtins.h"
|
||||||
|
#include "utils/float.h"
|
||||||
#include "utils/lsyscache.h"
|
#include "utils/lsyscache.h"
|
||||||
#include "vector.h"
|
#include "vector.h"
|
||||||
|
|
||||||
#if PG_VERSION_NUM >= 120000
|
|
||||||
#include "common/shortest_dec.h"
|
|
||||||
#include "utils/float.h"
|
|
||||||
#else
|
|
||||||
#include <float.h>
|
|
||||||
#include "utils/builtins.h"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
typedef struct SparseInputElement
|
typedef struct SparseInputElement
|
||||||
{
|
{
|
||||||
int32 index;
|
int32 index;
|
||||||
|
|||||||
@@ -99,6 +99,32 @@ SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <+> (SELECT NULL::vector)) t2
|
|||||||
4
|
4
|
||||||
(1 row)
|
(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;
|
DROP TABLE t;
|
||||||
-- unlogged
|
-- unlogged
|
||||||
CREATE UNLOGGED TABLE t (val vector(3));
|
CREATE UNLOGGED TABLE t (val vector(3));
|
||||||
@@ -139,4 +165,31 @@ SET hnsw.ef_search = 0;
|
|||||||
ERROR: 0 is outside the valid range for parameter "hnsw.ef_search" (1 .. 1000)
|
ERROR: 0 is outside the valid range for parameter "hnsw.ef_search" (1 .. 1000)
|
||||||
SET hnsw.ef_search = 1001;
|
SET hnsw.ef_search = 1001;
|
||||||
ERROR: 1001 is outside the valid range for parameter "hnsw.ef_search" (1 .. 1000)
|
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)
|
||||||
|
SET hnsw.scan_mem_multiplier = 1001;
|
||||||
|
ERROR: 1001 is outside the valid range for parameter "hnsw.scan_mem_multiplier" (1 .. 1000)
|
||||||
DROP TABLE t;
|
DROP TABLE t;
|
||||||
|
|||||||
@@ -81,6 +81,37 @@ SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <=> (SELECT NULL::vector)) t2
|
|||||||
3
|
3
|
||||||
(1 row)
|
(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;
|
DROP TABLE t;
|
||||||
-- unlogged
|
-- unlogged
|
||||||
CREATE UNLOGGED TABLE t (val vector(3));
|
CREATE UNLOGGED TABLE t (val vector(3));
|
||||||
@@ -109,4 +140,27 @@ SHOW ivfflat.probes;
|
|||||||
1
|
1
|
||||||
(1 row)
|
(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;
|
DROP TABLE t;
|
||||||
|
|||||||
@@ -57,6 +57,23 @@ SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <+> (SELECT NULL::vector)) t2
|
|||||||
|
|
||||||
DROP TABLE t;
|
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
|
-- unlogged
|
||||||
|
|
||||||
CREATE UNLOGGED TABLE t (val vector(3));
|
CREATE UNLOGGED TABLE t (val vector(3));
|
||||||
@@ -81,4 +98,17 @@ SHOW hnsw.ef_search;
|
|||||||
SET hnsw.ef_search = 0;
|
SET hnsw.ef_search = 0;
|
||||||
SET hnsw.ef_search = 1001;
|
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;
|
||||||
|
SET hnsw.scan_mem_multiplier = 1001;
|
||||||
|
|
||||||
DROP TABLE t;
|
DROP TABLE t;
|
||||||
|
|||||||
@@ -44,6 +44,25 @@ SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <=> (SELECT NULL::vector)) t2
|
|||||||
|
|
||||||
DROP TABLE t;
|
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
|
-- unlogged
|
||||||
|
|
||||||
CREATE UNLOGGED TABLE t (val vector(3));
|
CREATE UNLOGGED TABLE t (val vector(3));
|
||||||
@@ -62,4 +81,16 @@ CREATE INDEX ON t USING ivfflat (val vector_l2_ops) WITH (lists = 32769);
|
|||||||
|
|
||||||
SHOW ivfflat.probes;
|
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;
|
DROP TABLE t;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ $node->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v vector_l2_ops
|
|||||||
my $count = $node->safe_psql("postgres", qq(
|
my $count = $node->safe_psql("postgres", qq(
|
||||||
SET enable_seqscan = off;
|
SET enable_seqscan = off;
|
||||||
SET ivfflat.probes = 10;
|
SET ivfflat.probes = 10;
|
||||||
SET ivfflat.iterative_search = on;
|
SET ivfflat.iterative_scan = relaxed_order;
|
||||||
SELECT COUNT(*) FROM (SELECT v FROM tst WHERE i % 10000 = 0 ORDER BY v <-> (SELECT v FROM tst LIMIT 1) LIMIT 11) t;
|
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);
|
is($count, 10);
|
||||||
@@ -39,8 +39,8 @@ foreach ((30, 50, 70))
|
|||||||
$count = $node->safe_psql("postgres", qq(
|
$count = $node->safe_psql("postgres", qq(
|
||||||
SET enable_seqscan = off;
|
SET enable_seqscan = off;
|
||||||
SET ivfflat.probes = 10;
|
SET ivfflat.probes = 10;
|
||||||
SET ivfflat.iterative_search = on;
|
SET ivfflat.iterative_scan = relaxed_order;
|
||||||
SET ivfflat.iterative_search_max_probes = $max_probes;
|
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;
|
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;
|
$sum += $count;
|
||||||
@@ -19,7 +19,7 @@ sub test_recall
|
|||||||
my $explain = $node->safe_psql("postgres", qq(
|
my $explain = $node->safe_psql("postgres", qq(
|
||||||
SET enable_seqscan = off;
|
SET enable_seqscan = off;
|
||||||
SET ivfflat.probes = $probes;
|
SET ivfflat.probes = $probes;
|
||||||
SET ivfflat.iterative_search = on;
|
SET ivfflat.iterative_scan = relaxed_order;
|
||||||
EXPLAIN ANALYZE SELECT i FROM tst WHERE i % $c = 0 ORDER BY v $operator '$queries[0]' LIMIT $limit;
|
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/);
|
like($explain, qr/Index Scan using idx on tst/);
|
||||||
@@ -29,7 +29,7 @@ sub test_recall
|
|||||||
my $actual = $node->safe_psql("postgres", qq(
|
my $actual = $node->safe_psql("postgres", qq(
|
||||||
SET enable_seqscan = off;
|
SET enable_seqscan = off;
|
||||||
SET ivfflat.probes = $probes;
|
SET ivfflat.probes = $probes;
|
||||||
SET ivfflat.iterative_search = on;
|
SET ivfflat.iterative_scan = relaxed_order;
|
||||||
SELECT i FROM tst WHERE i % $c = 0 ORDER BY v $operator '$queries[$i]' LIMIT $limit;
|
SELECT i FROM tst WHERE i % $c = 0 ORDER BY v $operator '$queries[$i]' LIMIT $limit;
|
||||||
));
|
));
|
||||||
my @actual_ids = split("\n", $actual);
|
my @actual_ids = split("\n", $actual);
|
||||||
@@ -48,7 +48,7 @@ sub test_recall
|
|||||||
$total += $limit;
|
$total += $limit;
|
||||||
}
|
}
|
||||||
|
|
||||||
cmp_ok($correct / $total, ">=", $min, $operator);
|
cmp_ok($correct / $total, ">=", $min, "$operator $c");
|
||||||
}
|
}
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
@@ -103,7 +103,7 @@ for my $i (0 .. $#operators)
|
|||||||
|
|
||||||
if ($c == 100)
|
if ($c == 100)
|
||||||
{
|
{
|
||||||
test_recall($c, 1, 0.58, $operator);
|
test_recall($c, 1, 0.57, $operator);
|
||||||
test_recall($c, 10, 0.98, $operator);
|
test_recall($c, 10, 0.98, $operator);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -26,8 +26,9 @@ $node->safe_psql("postgres", qq(
|
|||||||
|
|
||||||
my $count = $node->safe_psql("postgres", qq(
|
my $count = $node->safe_psql("postgres", qq(
|
||||||
SET enable_seqscan = off;
|
SET enable_seqscan = off;
|
||||||
SET hnsw.iterative_search = on;
|
SET hnsw.iterative_scan = relaxed_order;
|
||||||
SET work_mem = '8MB';
|
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;
|
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);
|
is($count, 10);
|
||||||
@@ -42,9 +43,9 @@ foreach ((30000, 50000, 70000))
|
|||||||
{
|
{
|
||||||
$count = $node->safe_psql("postgres", qq(
|
$count = $node->safe_psql("postgres", qq(
|
||||||
SET enable_seqscan = off;
|
SET enable_seqscan = off;
|
||||||
SET hnsw.iterative_search = on;
|
SET hnsw.iterative_scan = relaxed_order;
|
||||||
SET hnsw.iterative_search_max_tuples = $max_tuples;
|
SET hnsw.max_scan_tuples = $max_tuples;
|
||||||
SET work_mem = '8MB';
|
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;
|
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;
|
$sum += $count;
|
||||||
@@ -55,13 +56,4 @@ foreach ((30000, 50000, 70000))
|
|||||||
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_search = on;
|
|
||||||
SET client_min_messages = debug1;
|
|
||||||
SET work_mem = '2MB';
|
|
||||||
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 exceeded work_mem after \d+ tuples/);
|
|
||||||
|
|
||||||
done_testing();
|
done_testing();
|
||||||
@@ -10,18 +10,18 @@ my @expected;
|
|||||||
my $limit = 20;
|
my $limit = 20;
|
||||||
my $dim = 3;
|
my $dim = 3;
|
||||||
my $array_sql = join(",", ('random()') x $dim);
|
my $array_sql = join(",", ('random()') x $dim);
|
||||||
my @cs = (100, 1000);
|
my @cs = (50, 500);
|
||||||
|
|
||||||
sub test_recall
|
sub test_recall
|
||||||
{
|
{
|
||||||
my ($c, $ef_search, $min, $operator) = @_;
|
my ($c, $ef_search, $min, $operator, $mode) = @_;
|
||||||
my $correct = 0;
|
my $correct = 0;
|
||||||
my $total = 0;
|
my $total = 0;
|
||||||
|
|
||||||
my $explain = $node->safe_psql("postgres", qq(
|
my $explain = $node->safe_psql("postgres", qq(
|
||||||
SET enable_seqscan = off;
|
SET enable_seqscan = off;
|
||||||
SET hnsw.ef_search = $ef_search;
|
SET hnsw.ef_search = $ef_search;
|
||||||
SET hnsw.iterative_search = on;
|
SET hnsw.iterative_scan = $mode;
|
||||||
EXPLAIN ANALYZE SELECT i FROM tst WHERE i % $c = 0 ORDER BY v $operator '$queries[0]' LIMIT $limit;
|
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/);
|
like($explain, qr/Index Scan using idx on tst/);
|
||||||
@@ -31,7 +31,7 @@ sub test_recall
|
|||||||
my $actual = $node->safe_psql("postgres", qq(
|
my $actual = $node->safe_psql("postgres", qq(
|
||||||
SET enable_seqscan = off;
|
SET enable_seqscan = off;
|
||||||
SET hnsw.ef_search = $ef_search;
|
SET hnsw.ef_search = $ef_search;
|
||||||
SET hnsw.iterative_search = on;
|
SET hnsw.iterative_scan = $mode;
|
||||||
SELECT i FROM tst WHERE i % $c = 0 ORDER BY v $operator '$queries[$i]' LIMIT $limit;
|
SELECT i FROM tst WHERE i % $c = 0 ORDER BY v $operator '$queries[$i]' LIMIT $limit;
|
||||||
));
|
));
|
||||||
my @actual_ids = split("\n", $actual);
|
my @actual_ids = split("\n", $actual);
|
||||||
@@ -50,7 +50,7 @@ sub test_recall
|
|||||||
$total += $limit;
|
$total += $limit;
|
||||||
}
|
}
|
||||||
|
|
||||||
cmp_ok($correct / $total, ">=", $min, $operator);
|
cmp_ok($correct / $total, ">=", $min, "$operator $mode $c");
|
||||||
}
|
}
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
@@ -62,7 +62,7 @@ $node->start;
|
|||||||
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
|
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
|
||||||
$node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector($dim));");
|
$node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector($dim));");
|
||||||
$node->safe_psql("postgres",
|
$node->safe_psql("postgres",
|
||||||
"INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(1, 100000) i;"
|
"INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(1, 50000) i;"
|
||||||
);
|
);
|
||||||
|
|
||||||
# Generate queries
|
# Generate queries
|
||||||
@@ -108,21 +108,8 @@ for my $i (0 .. $#operators)
|
|||||||
push(@expected, $res);
|
push(@expected, $res);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($c == 100)
|
test_recall($c, 40, 0.99, $operator, "strict_order");
|
||||||
{
|
test_recall($c, 40, 0.99, $operator, "relaxed_order");
|
||||||
test_recall($c, 40, 0.99, $operator);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if ($operator eq "<->")
|
|
||||||
{
|
|
||||||
test_recall($c, 40, 0.99, $operator);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
test_recall($c, 40, 0.99, $operator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$node->safe_psql("postgres", "DROP INDEX idx;");
|
$node->safe_psql("postgres", "DROP INDEX idx;");
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
comment = 'vector data type and ivfflat and hnsw access methods'
|
comment = 'vector data type and ivfflat and hnsw access methods'
|
||||||
default_version = '0.7.4'
|
default_version = '0.8.0'
|
||||||
module_pathname = '$libdir/vector'
|
module_pathname = '$libdir/vector'
|
||||||
relocatable = true
|
relocatable = true
|
||||||
|
|||||||
Reference in New Issue
Block a user