Compare commits

...

71 Commits

Author SHA1 Message Date
Andrew Kane
2627c5ff77 Version bump to 0.8.0 [skip ci] 2024-10-30 13:06:34 -07:00
Andrew Kane
34b3cfdc43 Updated min Postgres version in META.json [skip ci] 2024-10-30 13:06:12 -07:00
Andrew Kane
cd218aae5a Removed unneeded code 2024-10-30 13:05:10 -07:00
Andrew Kane
ba9367f86c Updated readme [skip ci] 2024-10-30 12:58:00 -07:00
Andrew Kane
9c20550a41 Updated readme 2024-10-30 12:54:42 -07:00
Andrew Kane
e3e74fe94e Updated readme [skip ci] 2024-10-29 00:04:35 -07:00
Andrew Kane
96a5a44632 Updated readme [skip ci] 2024-10-28 23:52:16 -07:00
Andrew Kane
67e1392a83 Updated readme [skip ci] 2024-10-28 22:58:56 -07:00
Andrew Kane
e530a1a026 Updated readme [skip ci] 2024-10-28 22:49:41 -07:00
Andrew Kane
6170e2645b Updated readme [skip ci] 2024-10-28 22:44:44 -07:00
Andrew Kane
e6bae175f1 Updated readme [skip ci] 2024-10-28 22:26:50 -07:00
Andrew Kane
52b777e04a Updated readme [skip ci] 2024-10-28 22:01:27 -07:00
Andrew Kane
307271214f Updated readme [skip ci] 2024-10-28 21:42:05 -07:00
Andrew Kane
6e9f74ddce Updated readme [skip ci] 2024-10-28 20:04:09 -07:00
Andrew Kane
258215ad97 Improved test [skip ci] 2024-10-28 15:54:39 -07:00
Andrew Kane
fb87b6da91 Fixed test 2024-10-28 13:55:26 -07:00
Andrew Kane
a2a0b377f0 Removed memory limit debug message from HNSW index scans (EXPLAIN ANALYZE can be used instead) 2024-10-28 13:46:49 -07:00
Andrew Kane
2f770307b8 Removed unused variable [skip ci] 2024-10-28 13:38:42 -07:00
Andrew Kane
c04e16ff5b Removed debug message from IVFFlat index scans [skip ci] 2024-10-28 13:36:57 -07:00
Andrew Kane
bd4d272f26 Updated changelog [skip ci] 2024-10-28 13:05:27 -07:00
Andrew Kane
8bb797cc2f Updated changelog [skip ci] 2024-10-28 13:00:55 -07:00
Andrew Kane
fe6ec03dac Improved filtering section [skip ci] 2024-10-28 12:08:27 -07:00
Andrew Kane
c1161f8889 Updated readme [skip ci] 2024-10-28 02:08:53 -07:00
Andrew Kane
c530a3c490 Updated comment [skip ci] 2024-10-28 00:56:10 -07:00
Andrew Kane
d8b9e8ef73 Updated readme [skip ci] 2024-10-28 00:55:25 -07:00
Andrew Kane
00894efed5 Updated readme [skip ci] 2024-10-28 00:45:26 -07:00
Andrew Kane
0aa0f6619b Updated readme [skip ci] 2024-10-28 00:43:39 -07:00
Andrew Kane
38d053001e Updated readme [skip ci] 2024-10-28 00:32:12 -07:00
Andrew Kane
ccb95407e7 Updated readme [skip ci] 2024-10-28 00:09:26 -07:00
Andrew Kane
04d5e934a1 Scan 2000 more tuples with default work_mem 2024-10-27 22:23:28 -07:00
Andrew Kane
b163b5b196 Moved code [skip ci] 2024-10-27 22:21:43 -07:00
Andrew Kane
6a30c1e824 Fixed test [skip ci] 2024-10-27 21:10:51 -07:00
Andrew Kane
2db1b19644 Use greater than [skip ci] 2024-10-27 21:07:22 -07:00
Andrew Kane
305d62146e Updated comment [skip ci] 2024-10-27 21:05:32 -07:00
Andrew Kane
f9d627c9a9 Updated default value of hnsw.scan_mem_multiplier [skip ci] 2024-10-27 21:05:04 -07:00
Andrew Kane
38f42820be Added test for hnsw.scan_mem_multiplier [skip ci] 2024-10-27 20:05:58 -07:00
Andrew Kane
15c8245b42 Updated readme [skip ci] 2024-10-27 19:30:40 -07:00
Andrew Kane
572a9ab404 Updated readme [skip ci] 2024-10-27 18:58:50 -07:00
Andrew Kane
00492d7e57 Ensure max memory fits into Size for HNSW index scans 2024-10-27 14:21:15 -07:00
Andrew Kane
857d716d9e Renamed iterative_search to iterative_scan 2024-10-27 14:02:22 -07:00
Andrew Kane
c5dd2af750 Added comments [skip ci] 2024-10-25 21:39:03 -07:00
Andrew Kane
78b877bdaf Revert "Renamed iterative_search to iterative_scan"
This reverts commit 7043cce893.
2024-10-24 20:32:07 -07:00
Andrew Kane
7043cce893 Renamed iterative_search to iterative_scan 2024-10-24 20:31:43 -07:00
Andrew Kane
62039d74f6 Added iterative search section to readme [skip ci] 2024-10-24 18:05:29 -07:00
Andrew Kane
ac6576e53a Added hnsw.search_mem_multiplier option 2024-10-24 18:02:20 -07:00
Andrew Kane
67eff41c44 Updated changelog [skip ci] 2024-10-23 15:12:40 -07:00
Andrew Kane
1291b12090 Added Postgres 18 to CI [skip ci] 2024-10-22 00:38:19 -07:00
Andrew Kane
24522700b8 Improved hnswrescan 2024-10-21 23:41:32 -07:00
Andrew Kane
bfb3a45b31 Use consistent order [skip ci] 2024-10-21 21:47:03 -07:00
Andrew Kane
e718eb8da4 Updated range and defaults for iterative search parameters 2024-10-21 20:38:50 -07:00
Andrew Kane
049972a4a3 Improved test output [skip ci] 2024-10-13 17:22:49 -07:00
Andrew Kane
61027645e9 Improved test output [skip ci] 2024-10-13 17:21:38 -07:00
Andrew Kane
a41b327b33 Speed up test [skip ci] 2024-10-13 17:12:12 -07:00
Andrew Kane
7f735ebd9b Added test for strict order [skip ci] 2024-10-13 17:04:03 -07:00
Andrew Kane
02b01e1ca9 Show tuples with memory usage [skip ci] 2024-10-12 22:05:13 -07:00
Andrew Kane
388e42f6e6 Fixed flaky test [skip ci] 2024-10-11 15:48:19 -07:00
Andrew Kane
bf379eed86 Use a memory context for IVFFlat index scans 2024-10-11 15:46:38 -07:00
Andrew Kane
e1bc929429 Simplify lists for IvfflatScanOpaque [skip ci] 2024-10-11 15:29:23 -07:00
Andrew Kane
38285aacc7 Revert "Updated IVFFlat to support multiple attributes (not enabled yet)"
This reverts commit 772ab69de6.
2024-10-11 15:01:54 -07:00
Andrew Kane
a2408e60fa Revert "Added IndexTuple to HNSW elements (first step to support multiple attributes)"
This reverts commit 53a8734bac.
2024-10-11 14:57:57 -07:00
Andrew Kane
53a8734bac Added IndexTuple to HNSW elements (first step to support multiple attributes) 2024-10-11 14:12:01 -07:00
Andrew Kane
7484625227 Added comments [skip ci] 2024-10-11 11:59:36 -07:00
Andrew Kane
d1ebb8db73 Use -1 for no limit for ivfflat.max_probes [skip ci] 2024-10-11 11:43:32 -07:00
Andrew Kane
42af8aa1d1 Updated GUC descriptions [skip ci] 2024-10-11 11:26:27 -07:00
Andrew Kane
9d15a76b60 Improved enum naming [skip ci] 2024-10-11 11:20:36 -07:00
Andrew Kane
a3a20f9816 Simplified GUC names [skip ci] 2024-10-11 11:18:01 -07:00
Andrew Kane
b26a21b848 Added regression tests for iterative search [skip ci] 2024-10-11 11:07:11 -07:00
Andrew Kane
2dc392ed6c Updated GUC names [skip ci] 2024-10-10 23:50:11 -07:00
Andrew Kane
960d2848cb Updated comment [skip ci] 2024-10-10 21:02:33 -07:00
Andrew Kane
8e88b481a6 Use a lower max allocation size than default to allow scanning more tuples for iterative search before exceeding work_mem [skip ci] 2024-10-10 20:57:57 -07:00
Andrew Kane
124018b8dd Added HnswInitSearchCandidate function 2024-10-10 19:30:47 -07:00
25 changed files with 463 additions and 217 deletions

View File

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

View File

@@ -1,10 +1,10 @@
## 0.8.0 (unreleased)
## 0.8.0 (2024-10-30)
- Added support for iterative index scans
- 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
- Reduced memory usage for HNSW index scans
- Dropped support for Postgres 12
## 0.7.4 (2024-08-05)

View File

@@ -2,7 +2,7 @@
"name": "vector",
"abstract": "Open-source vector similarity search for Postgres",
"description": "Supports L2 distance, inner product, and cosine distance",
"version": "0.7.4",
"version": "0.8.0",
"maintainer": [
"Andrew Kane <andrew@ankane.org>"
],
@@ -12,7 +12,7 @@
"prereqs": {
"runtime": {
"requires": {
"PostgreSQL": "12.0.0"
"PostgreSQL": "13.0.0"
}
}
},
@@ -20,7 +20,7 @@
"vector": {
"file": "sql/vector.sql",
"docfile": "README.md",
"version": "0.7.4",
"version": "0.8.0",
"abstract": "Open-source vector similarity search for Postgres"
}
},

View File

@@ -1,5 +1,5 @@
EXTENSION = vector
EXTVERSION = 0.7.4
EXTVERSION = 0.8.0
MODULE_big = vector
DATA = $(wildcard sql/*--*--*.sql)

View File

@@ -1,5 +1,5 @@
EXTENSION = vector
EXTVERSION = 0.7.4
EXTVERSION = 0.8.0
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

116
README.md
View File

@@ -17,11 +17,11 @@ Plus [ACID](https://en.wikipedia.org/wiki/ACID) compliance, point-in-time recove
### Linux and Mac
Compile and install the extension (supports Postgres 12+)
Compile and install the extension (supports Postgres 13+)
```sh
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
make
make install # may need sudo
@@ -46,7 +46,7 @@ Then use `nmake` to build:
```cmd
set "PGROOT=C:\Program Files\PostgreSQL\16"
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
nmake /F Makefile.win
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
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
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
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
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
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
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
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
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
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
*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:
```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
docker build --pull --build-arg PG_MAJOR=17 -t myuser/pgvector .
```

View File

@@ -18,16 +18,17 @@
#define MarkGUCPrefixReserved(x) EmitWarningsOnPlaceholders(x)
#endif
static const struct config_enum_entry hnsw_iterative_search_options[] = {
{"off", HNSW_ITERATIVE_SEARCH_OFF, false},
{"on", HNSW_ITERATIVE_SEARCH_RELAXED, false},
{"strict", HNSW_ITERATIVE_SEARCH_STRICT, false},
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_search_max_tuples;
int hnsw_iterative_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;
@@ -78,14 +79,19 @@ HnswInit(void)
"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);
DefineCustomEnumVariable("hnsw.iterative_search", "Sets iterative search",
NULL, &hnsw_iterative_search,
HNSW_ITERATIVE_SEARCH_OFF, hnsw_iterative_search_options, PGC_USERSET, 0, 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, 0, NULL, NULL, NULL);
/* TODO Ensure ivfflat.max_probes uses same value for "all" */
DefineCustomIntVariable("hnsw.iterative_search_max_tuples", "Sets the max number of candidates to visit for iterative search",
"-1 means all", &hnsw_iterative_search_max_tuples,
-1, -1, INT_MAX, PGC_USERSET, 0, 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, 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");
}
@@ -131,6 +137,10 @@ 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;
}

View File

@@ -109,16 +109,17 @@
/* Variables */
extern int hnsw_ef_search;
extern int hnsw_iterative_search;
extern int hnsw_iterative_search_max_tuples;
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 HnswIterativeSearchType
typedef enum HnswIterativeScanMode
{
HNSW_ITERATIVE_SEARCH_OFF,
HNSW_ITERATIVE_SEARCH_RELAXED,
HNSW_ITERATIVE_SEARCH_STRICT
} HnswIterativeSearchType;
HNSW_ITERATIVE_SCAN_OFF,
HNSW_ITERATIVE_SCAN_RELAXED,
HNSW_ITERATIVE_SCAN_STRICT
} HnswIterativeScanMode;
typedef struct HnswElementData HnswElementData;
typedef struct HnswNeighborArray HnswNeighborArray;
@@ -372,6 +373,7 @@ typedef struct HnswScanOpaqueData
int m;
int64 tuples;
double previousDistance;
Size maxMemory;
MemoryContext tmpCtx;
/* Support functions */

View File

@@ -41,7 +41,7 @@ GetScanItems(IndexScanDesc scan, Datum value)
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;
}
#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
*/
@@ -110,21 +121,29 @@ 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);
so->first = true;
so->v.tids = NULL;
so->discarded = NULL;
so->tmpCtx = AllocSetContextCreate(CurrentMemoryContext,
"Hnsw scan temporary context",
ALLOCSET_DEFAULT_SIZES);
/* 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->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;
return scan;
@@ -138,13 +157,10 @@ hnswrescan(IndexScanDesc scan, ScanKey keys, int nkeys, ScanKey orderbys, int no
{
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;
/* 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);
@@ -204,7 +220,7 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir)
so->first = false;
#if defined(HNSW_MEMORY)
elog(INFO, "memory: %zu KB", MemoryContextMemAllocated(so->tmpCtx, false) / 1024);
ShowMemoryUsage(so);
#endif
}
@@ -217,15 +233,15 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir)
if (list_length(so->w) == 0)
{
if (hnsw_iterative_search == HNSW_ITERATIVE_SEARCH_OFF)
if (hnsw_iterative_scan == HNSW_ITERATIVE_SCAN_OFF)
break;
/* Empty index */
if (so->discarded == NULL)
break;
/* Reached max number of additional tuples */
if (hnsw_iterative_search_max_tuples != -1 && so->tuples >= hnsw_iterative_search_max_tuples)
/* Reached max number of tuples or memory limit */
if (so->tuples >= hnsw_max_scan_tuples || MemoryContextMemAllocated(so->tmpCtx, false) > so->maxMemory)
{
if (pairingheap_is_empty(so->discarded))
break;
@@ -233,21 +249,6 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir)
/* 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) > (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
{
/*
@@ -266,7 +267,7 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir)
UnlockPage(scan->indexRelation, HNSW_SCAN_LOCK, ShareLock);
#if defined(HNSW_MEMORY)
elog(INFO, "memory: %zu KB", MemoryContextMemAllocated(so->tmpCtx, false) / 1024);
ShowMemoryUsage(so);
#endif
}
@@ -283,7 +284,7 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir)
so->w = list_delete_last(so->w);
/* 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(sc);
@@ -294,7 +295,7 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir)
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)
continue;

View File

@@ -581,21 +581,34 @@ GetElementDistance(char *base, HnswElement element, HnswQuery * q, HnswSupport *
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
*/
HnswSearchCandidate *
HnswEntryCandidate(char *base, HnswElement entryPoint, HnswQuery * q, Relation index, HnswSupport * support, bool loadVec)
{
HnswSearchCandidate *sc = palloc(sizeof(HnswSearchCandidate));
bool inMemory = index == NULL;
double distance;
HnswPtrStore(base, sc->element, entryPoint);
if (inMemory)
sc->distance = GetElementDistance(base, entryPoint, q, support);
distance = GetElementDistance(base, entryPoint, q, support);
else
HnswLoadElement(entryPoint, &sc->distance, q, index, support, loadVec, NULL);
return sc;
HnswLoadElement(entryPoint, &distance, q, index, support, loadVec, NULL);
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);
/* OK to count elements instead of tuples */
if (tuples != NULL)
(*tuples)++;
}
@@ -876,6 +890,7 @@ HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation in
else
HnswLoadUnvisitedFromDisk(cElement, unvisited, &unvisitedLength, v, index, m, lm, lc);
/* OK to count elements instead of tuples */
if (tuples != NULL)
(*tuples) += unvisitedLength;
@@ -912,9 +927,7 @@ HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation in
if (discarded != NULL)
{
/* Create a new candidate */
e = palloc(sizeof(HnswSearchCandidate));
HnswPtrStore(base, e->element, eElement);
e->distance = eDistance;
e = HnswInitSearchCandidate(base, eElement, eDistance);
pairingheap_add(*discarded, &e->w_node);
}
@@ -926,9 +939,7 @@ HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation in
continue;
/* Create a new candidate */
e = palloc(sizeof(HnswSearchCandidate));
HnswPtrStore(base, e->element, eElement);
e->distance = eDistance;
e = HnswInitSearchCandidate(base, eElement, eDistance);
pairingheap_add(C, &e->c_node);
pairingheap_add(W, &e->w_node);

View File

@@ -138,7 +138,7 @@ SampleRows(IvfflatBuildState * buildstate)
* Add tuple to sort
*/
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 minDistance = DBL_MAX;
@@ -184,11 +184,6 @@ AddTupleToSort(Relation index, ItemPointer tid, Datum *values, bool *isnull, Ivf
slot->tts_isnull[1] = false;
slot->tts_values[2] = value;
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);
/*
@@ -220,7 +215,7 @@ BuildCallback(Relation index, ItemPointer tid, Datum *values,
oldCtx = MemoryContextSwitchTo(buildstate->tmpCtx);
/* Add tuple to sort */
AddTupleToSort(index, tid, values, isnull, buildstate);
AddTupleToSort(index, tid, values, buildstate);
/* Reset memory context */
MemoryContextSwitchTo(oldCtx);
@@ -231,20 +226,19 @@ BuildCallback(Relation index, ItemPointer tid, Datum *values,
* Get index tuple from sort state
*/
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))
{
bool unused;
Datum value;
bool isnull;
*list = DatumGetInt32(slot_getattr(slot, 1, &unused));
for (int i = 0; i < tupdesc->natts; i++)
values[i] = slot_getattr(slot, 3 + i, &isnull[i]);
*list = DatumGetInt32(slot_getattr(slot, 1, &isnull));
value = slot_getattr(slot, 3, &isnull);
/* Form the index tuple */
*itup = index_form_tuple(tupdesc, values, isnull);
(*itup)->t_tid = *((ItemPointer) DatumGetPointer(slot_getattr(slot, 2, &unused)));
*itup = index_form_tuple(tupdesc, &value, &isnull);
(*itup)->t_tid = *((ItemPointer) DatumGetPointer(slot_getattr(slot, 2, &isnull)));
}
else
*list = -1;
@@ -262,14 +256,12 @@ InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum)
TupleTableSlot *slot = MakeSingleTupleTableSlot(buildstate->sortdesc, &TTSOpsMinimalTuple);
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_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++)
{
@@ -305,7 +297,7 @@ InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum)
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);
@@ -315,9 +307,6 @@ InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum)
/* Set the start and insert pages */
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")));
/* 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) 2, "tid", TIDOID, -1, 0);
for (int i = 0; i < buildstate->tupdesc->natts; i++)
TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) (3 + i), NULL, buildstate->tupdesc->attrs[i].atttypid, -1, 0);
TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 3, "vector", buildstate->tupdesc->attrs[0].atttypid, -1, 0);
buildstate->slot = MakeSingleTupleTableSlot(buildstate->sortdesc, &TTSOpsVirtual);

View File

@@ -17,13 +17,13 @@
#endif
int ivfflat_probes;
int ivfflat_iterative_search;
int ivfflat_iterative_search_max_probes;
int ivfflat_iterative_scan;
int ivfflat_max_probes;
static relopt_kind ivfflat_relopt_kind;
static const struct config_enum_entry ivfflat_iterative_search_options[] = {
{"off", IVFFLAT_ITERATIVE_SEARCH_OFF, false},
{"on", IVFFLAT_ITERATIVE_SEARCH_RELAXED, false},
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}
};
@@ -41,13 +41,14 @@ IvfflatInit(void)
"Valid range is 1..lists.", &ivfflat_probes,
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",
NULL, &ivfflat_iterative_search,
IVFFLAT_ITERATIVE_SEARCH_OFF, ivfflat_iterative_search_options, 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, 0, NULL, NULL, NULL);
DefineCustomIntVariable("ivfflat.iterative_search_max_probes", "Sets the max number of probes for iterative search",
"Zero sets to the number of lists", &ivfflat_iterative_search_max_probes,
0, 0, IVFFLAT_MAX_LISTS, PGC_USERSET, 0, 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, 0, NULL, NULL, NULL);
MarkGUCPrefixReserved("ivfflat");
}
@@ -98,6 +99,10 @@ 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;
}

View File

@@ -80,14 +80,14 @@
/* Variables */
extern int ivfflat_probes;
extern int ivfflat_iterative_search;
extern int ivfflat_iterative_search_max_probes;
extern int ivfflat_iterative_scan;
extern int ivfflat_max_probes;
typedef enum IvfflatIterativeSearchType
typedef enum IvfflatIterativeScanMode
{
IVFFLAT_ITERATIVE_SEARCH_OFF,
IVFFLAT_ITERATIVE_SEARCH_RELAXED
} IvfflatIterativeSearchType;
IVFFLAT_ITERATIVE_SCAN_OFF,
IVFFLAT_ITERATIVE_SCAN_RELAXED
} IvfflatIterativeScanMode;
typedef struct VectorArrayData
{
@@ -260,6 +260,7 @@ typedef struct IvfflatScanOpaqueData
int dimensions;
bool first;
Datum value;
MemoryContext tmpCtx;
/* Sorting */
Tuplesortstate *sortstate;
@@ -278,7 +279,7 @@ typedef struct IvfflatScanOpaqueData
pairingheap *listQueue;
BlockNumber *listPages;
int listIndex;
IvfflatScanList lists[FLEXIBLE_ARRAY_MEMBER]; /* must come last */
IvfflatScanList *lists;
} IvfflatScanOpaqueData;
typedef IvfflatScanOpaqueData * IvfflatScanOpaque;

View File

@@ -78,8 +78,6 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heap_tid, R
BlockNumber insertPage = InvalidBlockNumber;
ListInfo listInfo;
BlockNumber originalInsertPage;
TupleDesc tupdesc = RelationGetDescr(index);
Datum *newValues = palloc(tupdesc->natts * sizeof(Datum));
/* Detoast once for all calls */
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));
originalInsertPage = insertPage;
newValues[0] = value;
for (int i = 1; i < tupdesc->natts; i++)
newValues[i] = values[i];
/* Form tuple */
itup = index_form_tuple(tupdesc, newValues, isnull);
itup = index_form_tuple(RelationGetDescr(index), &value, isnull);
itup->t_tid = *heap_tid;
/* Get tuple size */

View File

@@ -10,10 +10,7 @@
#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)
@@ -117,7 +114,6 @@ GetScanItems(IndexScanDesc scan, Datum value)
{
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
TupleDesc tupdesc = RelationGetDescr(scan->indexRelation);
double tuples = 0;
TupleTableSlot *slot = so->vslot;
int batchProbes = 0;
@@ -164,8 +160,6 @@ GetScanItems(IndexScanDesc scan, Datum value)
ExecStoreVirtualTuple(slot);
tuplesort_puttupleslot(so->sortstate, slot);
tuples++;
}
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);
#if defined(IVFFLAT_MEMORY)
@@ -221,7 +209,13 @@ 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;
@@ -253,26 +247,25 @@ 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 (probes > lists)
probes = lists;
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);
}
if (ivfflat_iterative_scan != IVFFLAT_ITERATIVE_SCAN_OFF)
maxProbes = Max(ivfflat_max_probes, probes);
else
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->first = true;
so->probes = probes;
@@ -284,6 +277,12 @@ 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);
@@ -306,6 +305,9 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys)
so->listQueue = pairingheap_allocate(CompareLists, scan);
so->listPages = palloc(maxProbes * sizeof(BlockNumber));
so->listIndex = 0;
so->lists = palloc(maxProbes * sizeof(IvfflatScanList));
MemoryContextSwitchTo(oldCtx);
scan->opaque = so;
@@ -368,8 +370,6 @@ 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))
@@ -396,13 +396,10 @@ ivfflatendscan(IndexScanDesc scan)
{
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
pairingheap_free(so->listQueue);
pfree(so->listPages);
/* Free any temporary files */
tuplesort_end(so->sortstate);
FreeAccessStrategy(so->bas);
FreeTupleDesc(so->tupdesc);
/* TODO Free vslot and mslot without freeing TupleDesc */
MemoryContextDelete(so->tmpCtx);
pfree(so);
scan->opaque = NULL;

View File

@@ -4,6 +4,7 @@
#include <math.h>
#include "catalog/pg_type.h"
#include "common/shortest_dec.h"
#include "common/string.h"
#include "fmgr.h"
#include "halfutils.h"
@@ -12,17 +13,10 @@
#include "sparsevec.h"
#include "utils/array.h"
#include "utils/builtins.h"
#include "utils/float.h"
#include "utils/lsyscache.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
{
int32 index;

View File

@@ -99,6 +99,32 @@ 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));
@@ -139,4 +165,31 @@ 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)
SET hnsw.scan_mem_multiplier = 1001;
ERROR: 1001 is outside the valid range for parameter "hnsw.scan_mem_multiplier" (1 .. 1000)
DROP TABLE t;

View File

@@ -81,6 +81,37 @@ 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));
@@ -109,4 +140,27 @@ 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;

View File

@@ -57,6 +57,23 @@ 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));
@@ -81,4 +98,17 @@ 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;
SET hnsw.scan_mem_multiplier = 1001;
DROP TABLE t;

View File

@@ -44,6 +44,25 @@ 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));
@@ -62,4 +81,16 @@ 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;

View File

@@ -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(
SET enable_seqscan = off;
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;
));
is($count, 10);
@@ -39,8 +39,8 @@ foreach ((30, 50, 70))
$count = $node->safe_psql("postgres", qq(
SET enable_seqscan = off;
SET ivfflat.probes = 10;
SET ivfflat.iterative_search = on;
SET ivfflat.iterative_search_max_probes = $max_probes;
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;

View File

@@ -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_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;
));
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_search = on;
SET ivfflat.iterative_scan = relaxed_order;
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);
cmp_ok($correct / $total, ">=", $min, "$operator $c");
}
# Initialize node
@@ -103,7 +103,7 @@ for my $i (0 .. $#operators)
if ($c == 100)
{
test_recall($c, 1, 0.58, $operator);
test_recall($c, 1, 0.57, $operator);
test_recall($c, 10, 0.98, $operator);
}
else

View File

@@ -26,8 +26,9 @@ $node->safe_psql("postgres", qq(
my $count = $node->safe_psql("postgres", qq(
SET enable_seqscan = off;
SET hnsw.iterative_search = on;
SET work_mem = '8MB';
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);
@@ -42,9 +43,9 @@ foreach ((30000, 50000, 70000))
{
$count = $node->safe_psql("postgres", qq(
SET enable_seqscan = off;
SET hnsw.iterative_search = on;
SET hnsw.iterative_search_max_tuples = $max_tuples;
SET work_mem = '8MB';
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;
@@ -55,13 +56,4 @@ foreach ((30000, 50000, 70000))
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();

View File

@@ -10,18 +10,18 @@ my @expected;
my $limit = 20;
my $dim = 3;
my $array_sql = join(",", ('random()') x $dim);
my @cs = (100, 1000);
my @cs = (50, 500);
sub test_recall
{
my ($c, $ef_search, $min, $operator) = @_;
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_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;
));
like($explain, qr/Index Scan using idx on tst/);
@@ -31,7 +31,7 @@ sub test_recall
my $actual = $node->safe_psql("postgres", qq(
SET enable_seqscan = off;
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;
));
my @actual_ids = split("\n", $actual);
@@ -50,7 +50,7 @@ sub test_recall
$total += $limit;
}
cmp_ok($correct / $total, ">=", $min, $operator);
cmp_ok($correct / $total, ">=", $min, "$operator $mode $c");
}
# Initialize node
@@ -62,7 +62,7 @@ $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, ARRAY[$array_sql] FROM generate_series(1, 50000) i;"
);
# Generate queries
@@ -108,21 +108,8 @@ for my $i (0 .. $#operators)
push(@expected, $res);
}
if ($c == 100)
{
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);
}
}
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;");

View File

@@ -1,4 +1,4 @@
comment = 'vector data type and ivfflat and hnsw access methods'
default_version = '0.7.4'
default_version = '0.8.0'
module_pathname = '$libdir/vector'
relocatable = true