mirror of
https://github.com/pgvector/pgvector.git
synced 2026-07-22 20:15:46 +08:00
Compare commits
43 Commits
ci-arm
...
hnsw-strea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea689b92b2 | ||
|
|
51dad9ff2f | ||
|
|
d06e75f78c | ||
|
|
8e1a2715ea | ||
|
|
25fae23fc3 | ||
|
|
46d3164a30 | ||
|
|
f2b2040306 | ||
|
|
c46f078e3c | ||
|
|
c0f6570c4a | ||
|
|
1d3d0f46ac | ||
|
|
caabac33b8 | ||
|
|
8b9333d468 | ||
|
|
87f5e40495 | ||
|
|
8798c1474c | ||
|
|
1b337ad97d | ||
|
|
0047630baf | ||
|
|
8b253359ab | ||
|
|
8de5f55b0b | ||
|
|
351db562af | ||
|
|
e1c2d03dba | ||
|
|
ba8e29600b | ||
|
|
49e05fb5ba | ||
|
|
9b42662188 | ||
|
|
7265927fd6 | ||
|
|
ab57217f48 | ||
|
|
5f6e031ccc | ||
|
|
1a1221f905 | ||
|
|
40c3e402c7 | ||
|
|
058248fdcc | ||
|
|
73c5145b77 | ||
|
|
ec4a23fe49 | ||
|
|
38207f5640 | ||
|
|
4e35c6abe3 | ||
|
|
11e4d040d9 | ||
|
|
b2fa625255 | ||
|
|
a8e699c927 | ||
|
|
91541fece6 | ||
|
|
f3de487da2 | ||
|
|
721d4b7e3f | ||
|
|
28066d8fe4 | ||
|
|
495041e43b | ||
|
|
52c385c03a | ||
|
|
80cbd32dab |
103
.github/workflows/build.yml
vendored
103
.github/workflows/build.yml
vendored
@@ -8,10 +8,18 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
|
# - postgres: 18
|
||||||
|
# os: ubuntu-24.04
|
||||||
|
- postgres: 17
|
||||||
|
os: ubuntu-24.04
|
||||||
- postgres: 16
|
- postgres: 16
|
||||||
os: ubuntu-24.04-arm
|
os: ubuntu-22.04
|
||||||
|
- postgres: 15
|
||||||
|
os: ubuntu-22.04
|
||||||
- postgres: 14
|
- postgres: 14
|
||||||
os: ubuntu-22.04-arm
|
os: ubuntu-20.04
|
||||||
|
- postgres: 13
|
||||||
|
os: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: ankane/setup-postgres@v1
|
- uses: ankane/setup-postgres@v1
|
||||||
@@ -31,3 +39,94 @@ jobs:
|
|||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install libipc-run-perl
|
sudo apt-get install libipc-run-perl
|
||||||
- run: make prove_installcheck
|
- run: make prove_installcheck
|
||||||
|
mac:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
if: ${{ !startsWith(github.ref_name, 'windows') }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- postgres: 16
|
||||||
|
os: macos-14
|
||||||
|
- postgres: 14
|
||||||
|
os: macos-12
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ankane/setup-postgres@v1
|
||||||
|
with:
|
||||||
|
postgres-version: ${{ matrix.postgres }}
|
||||||
|
- run: make
|
||||||
|
env:
|
||||||
|
PG_CFLAGS: -DUSE_ASSERT_CHECKING -Wall -Wextra -Werror -Wno-unused-parameter
|
||||||
|
- run: make install
|
||||||
|
- run: make installcheck
|
||||||
|
- if: ${{ failure() }}
|
||||||
|
run: cat regression.diffs
|
||||||
|
# Homebrew Postgres does not enable TAP tests, so need to download
|
||||||
|
- run: |
|
||||||
|
brew install cpanm
|
||||||
|
cpanm --notest IPC::Run
|
||||||
|
wget -q https://github.com/postgres/postgres/archive/refs/tags/$TAG.tar.gz
|
||||||
|
tar xf $TAG.tar.gz
|
||||||
|
mv postgres-$TAG postgres
|
||||||
|
env:
|
||||||
|
TAG: ${{ matrix.postgres == 16 && 'REL_16_2' || 'REL_14_11' }}
|
||||||
|
- run: make prove_installcheck PROVE_FLAGS="-I ./postgres/src/test/perl -I ./test/perl"
|
||||||
|
env:
|
||||||
|
PERL5LIB: /Users/runner/perl5/lib/perl5
|
||||||
|
- run: make clean && $(brew --prefix llvm@15)/bin/scan-build --status-bugs make
|
||||||
|
env:
|
||||||
|
PG_CFLAGS: -DUSE_ASSERT_CHECKING
|
||||||
|
windows:
|
||||||
|
runs-on: windows-latest
|
||||||
|
if: ${{ !startsWith(github.ref_name, 'mac') }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ankane/setup-postgres@v1
|
||||||
|
with:
|
||||||
|
postgres-version: 14
|
||||||
|
- run: |
|
||||||
|
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" && ^
|
||||||
|
nmake /NOLOGO /F Makefile.win && ^
|
||||||
|
nmake /NOLOGO /F Makefile.win install && ^
|
||||||
|
nmake /NOLOGO /F Makefile.win installcheck && ^
|
||||||
|
nmake /NOLOGO /F Makefile.win clean && ^
|
||||||
|
nmake /NOLOGO /F Makefile.win uninstall
|
||||||
|
shell: cmd
|
||||||
|
- if: ${{ failure() }}
|
||||||
|
run: cat regression.diffs
|
||||||
|
i386:
|
||||||
|
if: ${{ !startsWith(github.ref_name, 'mac') && !startsWith(github.ref_name, 'windows') }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: debian:12
|
||||||
|
options: --platform linux/386
|
||||||
|
steps:
|
||||||
|
- run: apt-get update && apt-get install -y build-essential git libipc-run-perl postgresql-15 postgresql-server-dev-15 sudo
|
||||||
|
- run: service postgresql start
|
||||||
|
- run: |
|
||||||
|
git clone https://github.com/${{ github.repository }}.git pgvector
|
||||||
|
cd pgvector
|
||||||
|
git fetch origin ${{ github.ref }}
|
||||||
|
git reset --hard FETCH_HEAD
|
||||||
|
make
|
||||||
|
make install
|
||||||
|
chown -R postgres .
|
||||||
|
sudo -u postgres make installcheck
|
||||||
|
sudo -u postgres make prove_installcheck
|
||||||
|
env:
|
||||||
|
PG_CFLAGS: -DUSE_ASSERT_CHECKING -Wall -Wextra -Werror -Wno-unused-parameter -Wno-sign-compare
|
||||||
|
- if: ${{ failure() }}
|
||||||
|
run: cat pgvector/regression.diffs
|
||||||
|
valgrind:
|
||||||
|
if: ${{ !startsWith(github.ref_name, 'mac') && !startsWith(github.ref_name, 'windows') }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ankane/setup-postgres-valgrind@v1
|
||||||
|
with:
|
||||||
|
postgres-version: 16
|
||||||
|
check-ub: yes
|
||||||
|
- run: make OPTFLAGS=""
|
||||||
|
- run: sudo --preserve-env=PG_CONFIG make install
|
||||||
|
- run: make installcheck
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
## 0.8.0 (2024-10-30)
|
## 0.8.0 (unreleased)
|
||||||
|
|
||||||
- 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 for better index selection when filtering
|
- Improved cost estimation
|
||||||
- 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
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
|||||||
Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
|
Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
|
||||||
|
|
||||||
Portions Copyright (c) 1994, The Regents of the University of California
|
Portions Copyright (c) 1994, The Regents of the University of California
|
||||||
|
|
||||||
|
|||||||
@@ -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.8.0",
|
"version": "0.7.4",
|
||||||
"maintainer": [
|
"maintainer": [
|
||||||
"Andrew Kane <andrew@ankane.org>"
|
"Andrew Kane <andrew@ankane.org>"
|
||||||
],
|
],
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"prereqs": {
|
"prereqs": {
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"requires": {
|
"requires": {
|
||||||
"PostgreSQL": "13.0.0"
|
"PostgreSQL": "12.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
"vector": {
|
"vector": {
|
||||||
"file": "sql/vector.sql",
|
"file": "sql/vector.sql",
|
||||||
"docfile": "README.md",
|
"docfile": "README.md",
|
||||||
"version": "0.8.0",
|
"version": "0.7.4",
|
||||||
"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.8.0
|
EXTVERSION = 0.7.4
|
||||||
|
|
||||||
MODULE_big = vector
|
MODULE_big = vector
|
||||||
DATA = $(wildcard sql/*--*--*.sql)
|
DATA = $(wildcard sql/*--*--*.sql)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
EXTENSION = vector
|
EXTENSION = vector
|
||||||
EXTVERSION = 0.8.0
|
EXTVERSION = 0.7.4
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
128
README.md
128
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 13+)
|
Compile and install the extension (supports Postgres 12+)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd /tmp
|
cd /tmp
|
||||||
git clone --branch v0.8.0 https://github.com/pgvector/pgvector.git
|
git clone --branch v0.7.4 https://github.com/pgvector/pgvector.git
|
||||||
cd pgvector
|
cd pgvector
|
||||||
make
|
make
|
||||||
make install # may need sudo
|
make install # may need sudo
|
||||||
@@ -46,13 +46,13 @@ 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.8.0 https://github.com/pgvector/pgvector.git
|
git clone --branch v0.7.4 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
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: Postgres 17 is not supported with MSVC yet due to an [upstream issue](https://www.postgresql.org/message-id/flat/CAOdR5yF0krWrxycA04rgUKCgKugRvGWzzGLAhDZ9bzNv8g0Lag%40mail.gmail.com)
|
Note: Postgres 17 is not supported yet due to an upstream issue
|
||||||
|
|
||||||
See the [installation notes](#installation-notes---windows) if you run into issues
|
See the [installation notes](#installation-notes---windows) if you run into issues
|
||||||
|
|
||||||
@@ -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)
|
Check [indexing progress](https://www.postgresql.org/docs/current/progress-reporting.html#CREATE-INDEX-PROGRESS-REPORTING) with Postgres 12+
|
||||||
|
|
||||||
```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)
|
Check [indexing progress](https://www.postgresql.org/docs/current/progress-reporting.html#CREATE-INDEX-PROGRESS-REPORTING) with Postgres 12+
|
||||||
|
|
||||||
```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,115 +427,92 @@ 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;
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
Create an index on one [or more](https://www.postgresql.org/docs/current/indexes-multicolumn.html) of the `WHERE` columns for exact search
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
CREATE INDEX ON items (category_id);
|
CREATE INDEX ON items (category_id);
|
||||||
```
|
```
|
||||||
|
|
||||||
For multiple columns, consider a [multicolumn index](https://www.postgresql.org/docs/current/indexes-multicolumn.html).
|
Or a [partial index](https://www.postgresql.org/docs/current/indexes-partial.html) on the vector column for approximate search
|
||||||
|
|
||||||
```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);
|
||||||
```
|
```
|
||||||
|
|
||||||
If filtering by many different values, consider [partitioning](https://www.postgresql.org/docs/current/ddl-partitioning.html).
|
Use [partitioning](https://www.postgresql.org/docs/current/ddl-partitioning.html) for approximate search on many different values of the `WHERE` columns
|
||||||
|
|
||||||
```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
|
## Iterative Search [unreleased]
|
||||||
|
|
||||||
*Added in 0.8.0*
|
*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`).
|
With approximate indexes, you can end up with less results than expected due to filtering conditions in the query.
|
||||||
|
|
||||||
Iterative scans can use strict or relaxed ordering.
|
Starting with 0.8.0, you can enable iterative search. If too few results from the initial index scan match the query filters, it will resume scanning until enough results are found. This can significantly improve recall (at the cost of speed).
|
||||||
|
|
||||||
Strict ensures results are in the exact order by distance
|
```tsql
|
||||||
|
SET hnsw.streaming = on;
|
||||||
```sql
|
-- or
|
||||||
SET hnsw.iterative_scan = strict_order;
|
SET ivfflat.streaming = on;
|
||||||
```
|
```
|
||||||
|
|
||||||
Relaxed allows results to be slightly out of order by distance, but provides better recall
|
However, there are some important caveats.
|
||||||
|
|
||||||
|
### Iterative Caveats
|
||||||
|
|
||||||
|
With iterative search, it’s possible for rows to be slightly out of order by distance. For strict ordering, use:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
SET hnsw.iterative_scan = relaxed_order;
|
WITH approx_order AS MATERIALIZED (
|
||||||
# or
|
SELECT *, embedding <-> '[1,2,3]' AS distance FROM items WHERE ... ORDER BY distance LIMIT 5
|
||||||
SET ivfflat.iterative_scan = relaxed_order;
|
) SELECT * FROM approx_order ORDER BY distance;
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
For distance filters, use a CTE and place the filter outside it.
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
WITH relaxed_results AS MATERIALIZED (
|
WITH approx_order AS MATERIALIZED (
|
||||||
SELECT id, embedding <-> '[1,2,3]' AS distance FROM items WHERE category_id = 123 ORDER BY distance LIMIT 5
|
SELECT *, embedding <-> '[1,2,3]' AS distance FROM items WHERE ... ORDER BY distance LIMIT 5
|
||||||
) SELECT * FROM relaxed_results ORDER BY distance;
|
) SELECT * FROM approx_order WHERE distance < 0.1 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)
|
### Iterative Options
|
||||||
|
|
||||||
```sql
|
Since scanning a large portion of the index is expensive, there are options to control when the scan ends.
|
||||||
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
|
#### HNSW
|
||||||
|
|
||||||
Specify the max number of tuples to visit (20,000 by default)
|
Specify the max number of additional tuples visited
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
SET hnsw.max_scan_tuples = 20000;
|
SET hnsw.ef_stream = 10000;
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: This is approximate and does not affect the initial scan
|
The scan will also end if reaches `work_mem`. You can see when this happens by enabling debug messages.
|
||||||
|
|
||||||
Specify the max amount of memory to use, as a multiple of `work_mem` (1 by default)
|
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
SET hnsw.scan_mem_multiplier = 2;
|
SET client_min_messages = debug1;
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: Try increasing this if increasing `hnsw.max_scan_tuples` does not improve recall
|
```text
|
||||||
|
DEBUG: hnsw index scan exceeded work_mem after 10000 tuples
|
||||||
|
HINT: Increase work_mem to scan more tuples.
|
||||||
|
```
|
||||||
|
|
||||||
|
If the server has enough memory, you can adjust this with:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SET work_mem = '8MB';
|
||||||
|
```
|
||||||
|
|
||||||
#### IVFFlat
|
#### IVFFlat
|
||||||
|
|
||||||
@@ -545,8 +522,6 @@ Specify the max number of probes
|
|||||||
SET ivfflat.max_probes = 100;
|
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*
|
||||||
@@ -801,12 +776,8 @@ C | [pgvector-c](https://github.com/pgvector/pgvector-c)
|
|||||||
C++ | [pgvector-cpp](https://github.com/pgvector/pgvector-cpp)
|
C++ | [pgvector-cpp](https://github.com/pgvector/pgvector-cpp)
|
||||||
C#, F#, Visual Basic | [pgvector-dotnet](https://github.com/pgvector/pgvector-dotnet)
|
C#, F#, Visual Basic | [pgvector-dotnet](https://github.com/pgvector/pgvector-dotnet)
|
||||||
Crystal | [pgvector-crystal](https://github.com/pgvector/pgvector-crystal)
|
Crystal | [pgvector-crystal](https://github.com/pgvector/pgvector-crystal)
|
||||||
D | [pgvector-d](https://github.com/pgvector/pgvector-d)
|
|
||||||
Dart | [pgvector-dart](https://github.com/pgvector/pgvector-dart)
|
Dart | [pgvector-dart](https://github.com/pgvector/pgvector-dart)
|
||||||
Elixir | [pgvector-elixir](https://github.com/pgvector/pgvector-elixir)
|
Elixir | [pgvector-elixir](https://github.com/pgvector/pgvector-elixir)
|
||||||
Erlang | [pgvector-erlang](https://github.com/pgvector/pgvector-erlang)
|
|
||||||
Fortran | [pgvector-fortran](https://github.com/pgvector/pgvector-fortran)
|
|
||||||
Gleam | [pgvector-gleam](https://github.com/pgvector/pgvector-gleam)
|
|
||||||
Go | [pgvector-go](https://github.com/pgvector/pgvector-go)
|
Go | [pgvector-go](https://github.com/pgvector/pgvector-go)
|
||||||
Haskell | [pgvector-haskell](https://github.com/pgvector/pgvector-haskell)
|
Haskell | [pgvector-haskell](https://github.com/pgvector/pgvector-haskell)
|
||||||
Java, Kotlin, Groovy, Scala | [pgvector-java](https://github.com/pgvector/pgvector-java)
|
Java, Kotlin, Groovy, Scala | [pgvector-java](https://github.com/pgvector/pgvector-java)
|
||||||
@@ -820,7 +791,6 @@ Perl | [pgvector-perl](https://github.com/pgvector/pgvector-perl)
|
|||||||
PHP | [pgvector-php](https://github.com/pgvector/pgvector-php)
|
PHP | [pgvector-php](https://github.com/pgvector/pgvector-php)
|
||||||
Python | [pgvector-python](https://github.com/pgvector/pgvector-python)
|
Python | [pgvector-python](https://github.com/pgvector/pgvector-python)
|
||||||
R | [pgvector-r](https://github.com/pgvector/pgvector-r)
|
R | [pgvector-r](https://github.com/pgvector/pgvector-r)
|
||||||
Raku | [pgvector-raku](https://github.com/pgvector/pgvector-raku)
|
|
||||||
Ruby | [pgvector-ruby](https://github.com/pgvector/pgvector-ruby), [Neighbor](https://github.com/ankane/neighbor)
|
Ruby | [pgvector-ruby](https://github.com/pgvector/pgvector-ruby), [Neighbor](https://github.com/ankane/neighbor)
|
||||||
Rust | [pgvector-rust](https://github.com/pgvector/pgvector-rust)
|
Rust | [pgvector-rust](https://github.com/pgvector/pgvector-rust)
|
||||||
Swift | [pgvector-swift](https://github.com/pgvector/pgvector-swift)
|
Swift | [pgvector-swift](https://github.com/pgvector/pgvector-swift)
|
||||||
@@ -942,7 +912,7 @@ ALTER TABLE items ALTER COLUMN embedding SET STORAGE PLAIN;
|
|||||||
|
|
||||||
#### Why are there less results for a query after adding an HNSW index?
|
#### Why are there less results for a query after adding an HNSW index?
|
||||||
|
|
||||||
Results are limited by the size of the dynamic candidate list (`hnsw.ef_search`), which is 40 by default. There may be even less results due to dead tuples or filtering conditions in the query. Enabling [iterative index scans](#iterative-index-scans) can help address this.
|
Results are limited by the size of the dynamic candidate list (`hnsw.ef_search`). There may be even less results due to dead tuples or filtering conditions in the query. We recommend setting `hnsw.ef_search` to at least twice the `LIMIT` of the query. If you need more than 500 results, use an IVFFlat index instead.
|
||||||
|
|
||||||
Also, note that `NULL` vectors are not indexed (as well as zero vectors for cosine distance).
|
Also, note that `NULL` vectors are not indexed (as well as zero vectors for cosine distance).
|
||||||
|
|
||||||
@@ -954,7 +924,7 @@ The index was likely created with too little data for the number of lists. Drop
|
|||||||
DROP INDEX index_name;
|
DROP INDEX index_name;
|
||||||
```
|
```
|
||||||
|
|
||||||
Results can also be limited by the number of probes (`ivfflat.probes`). Enabling [iterative index scans](#iterative-index-scans) can address this.
|
Results can also be limited by the number of probes (`ivfflat.probes`).
|
||||||
|
|
||||||
Also, note that `NULL` vectors are not indexed (as well as zero vectors for cosine distance).
|
Also, note that `NULL` vectors are not indexed (as well as zero vectors for cosine distance).
|
||||||
|
|
||||||
@@ -1158,7 +1128,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.8.0 https://github.com/pgvector/pgvector.git
|
git clone --branch v0.7.4 https://github.com/pgvector/pgvector.git
|
||||||
cd pgvector
|
cd pgvector
|
||||||
docker build --pull --build-arg PG_MAJOR=17 -t myuser/pgvector .
|
docker build --pull --build-arg PG_MAJOR=17 -t myuser/pgvector .
|
||||||
```
|
```
|
||||||
@@ -1208,7 +1178,7 @@ Note: Replace `17` with your Postgres server version
|
|||||||
Install the FreeBSD package with:
|
Install the FreeBSD package with:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pkg install postgresql16-pgvector
|
pkg install postgresql15-pgvector
|
||||||
```
|
```
|
||||||
|
|
||||||
or the port with:
|
or the port with:
|
||||||
|
|||||||
40
src/hnsw.c
40
src/hnsw.c
@@ -18,17 +18,18 @@
|
|||||||
#define MarkGUCPrefixReserved(x) EmitWarningsOnPlaceholders(x)
|
#define MarkGUCPrefixReserved(x) EmitWarningsOnPlaceholders(x)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
static const struct config_enum_entry hnsw_iterative_scan_options[] = {
|
static const struct config_enum_entry hnsw_iterative_search_options[] = {
|
||||||
{"off", HNSW_ITERATIVE_SCAN_OFF, false},
|
{"off", HNSW_ITERATIVE_SEARCH_OFF, false},
|
||||||
{"relaxed_order", HNSW_ITERATIVE_SCAN_RELAXED, false},
|
{"strict", HNSW_ITERATIVE_SEARCH_STRICT, false},
|
||||||
{"strict_order", HNSW_ITERATIVE_SCAN_STRICT, false},
|
{"relaxed", HNSW_ITERATIVE_SEARCH_RELAXED, false},
|
||||||
|
/* TODO Change to strict before merging */
|
||||||
|
{"on", HNSW_ITERATIVE_SEARCH_RELAXED, false},
|
||||||
{NULL, 0, false}
|
{NULL, 0, false}
|
||||||
};
|
};
|
||||||
|
|
||||||
int hnsw_ef_search;
|
int hnsw_ef_search;
|
||||||
int hnsw_iterative_scan;
|
int hnsw_max_iterative_tuples;
|
||||||
int hnsw_max_scan_tuples;
|
int hnsw_iterative_search;
|
||||||
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;
|
||||||
|
|
||||||
@@ -79,19 +80,16 @@ 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_scan", "Sets the mode for iterative scans",
|
/* TODO Change name */
|
||||||
NULL, &hnsw_iterative_scan,
|
DefineCustomEnumVariable("hnsw.streaming", "Iterative search mode",
|
||||||
HNSW_ITERATIVE_SCAN_OFF, hnsw_iterative_scan_options, PGC_USERSET, 0, NULL, NULL, NULL);
|
NULL, &hnsw_iterative_search,
|
||||||
|
HNSW_ITERATIVE_SEARCH_OFF, hnsw_iterative_search_options, PGC_USERSET, 0, NULL, NULL, NULL);
|
||||||
|
|
||||||
/* This is approximate and does not affect the initial scan */
|
/* TODO Change name */
|
||||||
DefineCustomIntVariable("hnsw.max_scan_tuples", "Sets the max number of tuples to visit for iterative scans",
|
/* TODO Ensure ivfflat.max_probes uses same value for "all" */
|
||||||
NULL, &hnsw_max_scan_tuples,
|
DefineCustomIntVariable("hnsw.ef_stream", "Sets the max number of additional candidates to visit for streaming search",
|
||||||
20000, 1, INT_MAX, PGC_USERSET, 0, NULL, NULL, NULL);
|
"-1 means all", &hnsw_max_iterative_tuples,
|
||||||
|
HNSW_DEFAULT_EF_STREAM, HNSW_MIN_EF_STREAM, HNSW_MAX_EF_STREAM, 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");
|
||||||
}
|
}
|
||||||
@@ -137,10 +135,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
21
src/hnsw.h
21
src/hnsw.h
@@ -42,6 +42,9 @@
|
|||||||
#define HNSW_DEFAULT_EF_SEARCH 40
|
#define HNSW_DEFAULT_EF_SEARCH 40
|
||||||
#define HNSW_MIN_EF_SEARCH 1
|
#define HNSW_MIN_EF_SEARCH 1
|
||||||
#define HNSW_MAX_EF_SEARCH 1000
|
#define HNSW_MAX_EF_SEARCH 1000
|
||||||
|
#define HNSW_DEFAULT_EF_STREAM -1
|
||||||
|
#define HNSW_MIN_EF_STREAM -1
|
||||||
|
#define HNSW_MAX_EF_STREAM INT_MAX
|
||||||
|
|
||||||
/* Tuple types */
|
/* Tuple types */
|
||||||
#define HNSW_ELEMENT_TUPLE_TYPE 1
|
#define HNSW_ELEMENT_TUPLE_TYPE 1
|
||||||
@@ -109,24 +112,23 @@
|
|||||||
|
|
||||||
/* Variables */
|
/* Variables */
|
||||||
extern int hnsw_ef_search;
|
extern int hnsw_ef_search;
|
||||||
extern int hnsw_iterative_scan;
|
extern int hnsw_max_iterative_tuples;
|
||||||
extern int hnsw_max_scan_tuples;
|
extern int hnsw_iterative_search;
|
||||||
extern double hnsw_scan_mem_multiplier;
|
|
||||||
extern int hnsw_lock_tranche_id;
|
extern int hnsw_lock_tranche_id;
|
||||||
|
|
||||||
typedef enum HnswIterativeScanMode
|
typedef enum HnswIterativeSearchType
|
||||||
{
|
{
|
||||||
HNSW_ITERATIVE_SCAN_OFF,
|
HNSW_ITERATIVE_SEARCH_OFF,
|
||||||
HNSW_ITERATIVE_SCAN_RELAXED,
|
HNSW_ITERATIVE_SEARCH_STRICT,
|
||||||
HNSW_ITERATIVE_SCAN_STRICT
|
HNSW_ITERATIVE_SEARCH_RELAXED
|
||||||
} HnswIterativeScanMode;
|
} HnswIterativeSearchType;
|
||||||
|
|
||||||
typedef struct HnswElementData HnswElementData;
|
typedef struct HnswElementData HnswElementData;
|
||||||
typedef struct HnswNeighborArray HnswNeighborArray;
|
typedef struct HnswNeighborArray HnswNeighborArray;
|
||||||
|
|
||||||
#define HnswPtrDeclare(type, relptrtype, ptrtype) \
|
#define HnswPtrDeclare(type, relptrtype, ptrtype) \
|
||||||
relptr_declare(type, relptrtype); \
|
relptr_declare(type, relptrtype); \
|
||||||
typedef union { type *ptr; relptrtype relptr; } ptrtype
|
typedef union { type *ptr; relptrtype relptr; } ptrtype;
|
||||||
|
|
||||||
/* Pointers that can be absolute or relative */
|
/* Pointers that can be absolute or relative */
|
||||||
/* Use char for DatumPtr so works with Pointer */
|
/* Use char for DatumPtr so works with Pointer */
|
||||||
@@ -373,7 +375,6 @@ 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_scan != HNSW_ITERATIVE_SCAN_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_search != HNSW_ITERATIVE_SEARCH_OFF ? &so->discarded : NULL, true, &so->tuples);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -102,17 +102,6 @@ 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
|
||||||
*/
|
*/
|
||||||
@@ -121,29 +110,21 @@ 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;
|
||||||
@@ -157,10 +138,13 @@ 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);
|
||||||
@@ -220,7 +204,7 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir)
|
|||||||
so->first = false;
|
so->first = false;
|
||||||
|
|
||||||
#if defined(HNSW_MEMORY)
|
#if defined(HNSW_MEMORY)
|
||||||
ShowMemoryUsage(so);
|
elog(INFO, "memory: %zu KB", MemoryContextMemAllocated(so->tmpCtx, false) / 1024);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,15 +217,15 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir)
|
|||||||
|
|
||||||
if (list_length(so->w) == 0)
|
if (list_length(so->w) == 0)
|
||||||
{
|
{
|
||||||
if (hnsw_iterative_scan == HNSW_ITERATIVE_SCAN_OFF)
|
if (hnsw_iterative_search == HNSW_ITERATIVE_SEARCH_OFF)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
/* Empty index */
|
/* Empty index */
|
||||||
if (so->discarded == NULL)
|
if (so->discarded == NULL)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
/* Reached max number of tuples or memory limit */
|
/* Reached max number of additional tuples */
|
||||||
if (so->tuples >= hnsw_max_scan_tuples || MemoryContextMemAllocated(so->tmpCtx, false) > so->maxMemory)
|
if (hnsw_max_iterative_tuples != -1 && so->tuples >= hnsw_ef_search + hnsw_max_iterative_tuples)
|
||||||
{
|
{
|
||||||
if (pairingheap_is_empty(so->discarded))
|
if (pairingheap_is_empty(so->discarded))
|
||||||
break;
|
break;
|
||||||
@@ -249,6 +233,21 @@ 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
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
@@ -267,7 +266,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)
|
||||||
ShowMemoryUsage(so);
|
elog(INFO, "memory: %zu KB", MemoryContextMemAllocated(so->tmpCtx, false) / 1024);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,7 +283,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_scan != HNSW_ITERATIVE_SCAN_OFF)
|
if (hnsw_iterative_search != HNSW_ITERATIVE_SEARCH_OFF)
|
||||||
{
|
{
|
||||||
pfree(element);
|
pfree(element);
|
||||||
pfree(sc);
|
pfree(sc);
|
||||||
@@ -295,7 +294,7 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir)
|
|||||||
|
|
||||||
heaptid = &element->heaptids[--element->heaptidsLength];
|
heaptid = &element->heaptids[--element->heaptidsLength];
|
||||||
|
|
||||||
if (hnsw_iterative_scan == HNSW_ITERATIVE_SCAN_STRICT)
|
if (hnsw_iterative_search == HNSW_ITERATIVE_SEARCH_STRICT)
|
||||||
{
|
{
|
||||||
if (sc->distance < so->previousDistance)
|
if (sc->distance < so->previousDistance)
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -581,34 +581,21 @@ 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)
|
||||||
distance = GetElementDistance(base, entryPoint, q, support);
|
sc->distance = GetElementDistance(base, entryPoint, q, support);
|
||||||
else
|
else
|
||||||
HnswLoadElement(entryPoint, &distance, q, index, support, loadVec, NULL);
|
HnswLoadElement(entryPoint, &sc->distance, q, index, support, loadVec, NULL);
|
||||||
|
return sc;
|
||||||
return HnswInitSearchCandidate(base, entryPoint, distance);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -857,7 +844,6 @@ 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)++;
|
||||||
}
|
}
|
||||||
@@ -890,7 +876,6 @@ 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;
|
||||||
|
|
||||||
@@ -927,7 +912,9 @@ 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 = HnswInitSearchCandidate(base, eElement, eDistance);
|
e = palloc(sizeof(HnswSearchCandidate));
|
||||||
|
HnswPtrStore(base, e->element, eElement);
|
||||||
|
e->distance = eDistance;
|
||||||
pairingheap_add(*discarded, &e->w_node);
|
pairingheap_add(*discarded, &e->w_node);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -939,7 +926,9 @@ HnswSearchLayer(char *base, HnswQuery * q, List *ep, int ef, int lc, Relation in
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
/* Create a new candidate */
|
/* Create a new candidate */
|
||||||
e = HnswInitSearchCandidate(base, eElement, eDistance);
|
e = palloc(sizeof(HnswSearchCandidate));
|
||||||
|
HnswPtrStore(base, e->element, eElement);
|
||||||
|
e->distance = eDistance;
|
||||||
pairingheap_add(C, &e->c_node);
|
pairingheap_add(C, &e->c_node);
|
||||||
pairingheap_add(W, &e->w_node);
|
pairingheap_add(W, &e->w_node);
|
||||||
|
|
||||||
@@ -1393,7 +1382,7 @@ hnsw_halfvec_support(PG_FUNCTION_ARGS)
|
|||||||
};
|
};
|
||||||
|
|
||||||
PG_RETURN_POINTER(&typeInfo);
|
PG_RETURN_POINTER(&typeInfo);
|
||||||
}
|
};
|
||||||
|
|
||||||
FUNCTION_PREFIX PG_FUNCTION_INFO_V1(hnsw_bit_support);
|
FUNCTION_PREFIX PG_FUNCTION_INFO_V1(hnsw_bit_support);
|
||||||
Datum
|
Datum
|
||||||
@@ -1406,7 +1395,7 @@ hnsw_bit_support(PG_FUNCTION_ARGS)
|
|||||||
};
|
};
|
||||||
|
|
||||||
PG_RETURN_POINTER(&typeInfo);
|
PG_RETURN_POINTER(&typeInfo);
|
||||||
}
|
};
|
||||||
|
|
||||||
FUNCTION_PREFIX PG_FUNCTION_INFO_V1(hnsw_sparsevec_support);
|
FUNCTION_PREFIX PG_FUNCTION_INFO_V1(hnsw_sparsevec_support);
|
||||||
Datum
|
Datum
|
||||||
@@ -1419,4 +1408,4 @@ hnsw_sparsevec_support(PG_FUNCTION_ARGS)
|
|||||||
};
|
};
|
||||||
|
|
||||||
PG_RETURN_POINTER(&typeInfo);
|
PG_RETURN_POINTER(&typeInfo);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -228,11 +228,11 @@ BuildCallback(Relation index, ItemPointer tid, Datum *values,
|
|||||||
static inline void
|
static inline void
|
||||||
GetNextTuple(Tuplesortstate *sortstate, TupleDesc tupdesc, TupleTableSlot *slot, IndexTuple *itup, int *list)
|
GetNextTuple(Tuplesortstate *sortstate, TupleDesc tupdesc, TupleTableSlot *slot, IndexTuple *itup, int *list)
|
||||||
{
|
{
|
||||||
if (tuplesort_gettupleslot(sortstate, true, false, slot, NULL))
|
|
||||||
{
|
|
||||||
Datum value;
|
Datum value;
|
||||||
bool isnull;
|
bool isnull;
|
||||||
|
|
||||||
|
if (tuplesort_gettupleslot(sortstate, true, false, slot, NULL))
|
||||||
|
{
|
||||||
*list = DatumGetInt32(slot_getattr(slot, 1, &isnull));
|
*list = DatumGetInt32(slot_getattr(slot, 1, &isnull));
|
||||||
value = slot_getattr(slot, 3, &isnull);
|
value = slot_getattr(slot, 3, &isnull);
|
||||||
|
|
||||||
@@ -254,8 +254,8 @@ InsertTuples(Relation index, IvfflatBuildState * buildstate, ForkNumber forkNum)
|
|||||||
IndexTuple itup = NULL; /* silence compiler warning */
|
IndexTuple itup = NULL; /* silence compiler warning */
|
||||||
int64 inserted = 0;
|
int64 inserted = 0;
|
||||||
|
|
||||||
TupleTableSlot *slot = MakeSingleTupleTableSlot(buildstate->sortdesc, &TTSOpsMinimalTuple);
|
TupleTableSlot *slot = MakeSingleTupleTableSlot(buildstate->tupdesc, &TTSOpsMinimalTuple);
|
||||||
TupleDesc tupdesc = buildstate->tupdesc;
|
TupleDesc tupdesc = RelationGetDescr(index);
|
||||||
|
|
||||||
pgstat_progress_update_param(PROGRESS_CREATEIDX_SUBPHASE, PROGRESS_IVFFLAT_PHASE_LOAD);
|
pgstat_progress_update_param(PROGRESS_CREATEIDX_SUBPHASE, PROGRESS_IVFFLAT_PHASE_LOAD);
|
||||||
|
|
||||||
@@ -319,7 +319,6 @@ InitBuildState(IvfflatBuildState * buildstate, Relation heap, Relation index, In
|
|||||||
buildstate->index = index;
|
buildstate->index = index;
|
||||||
buildstate->indexInfo = indexInfo;
|
buildstate->indexInfo = indexInfo;
|
||||||
buildstate->typeInfo = IvfflatGetTypeInfo(index);
|
buildstate->typeInfo = IvfflatGetTypeInfo(index);
|
||||||
buildstate->tupdesc = RelationGetDescr(index);
|
|
||||||
|
|
||||||
buildstate->lists = IvfflatGetLists(index);
|
buildstate->lists = IvfflatGetLists(index);
|
||||||
buildstate->dimensions = TupleDescAttr(index->rd_att, 0)->atttypmod;
|
buildstate->dimensions = TupleDescAttr(index->rd_att, 0)->atttypmod;
|
||||||
@@ -357,12 +356,12 @@ InitBuildState(IvfflatBuildState * buildstate, Relation heap, Relation index, In
|
|||||||
errmsg("dimensions must be greater than one for this opclass")));
|
errmsg("dimensions must be greater than one for this opclass")));
|
||||||
|
|
||||||
/* Create tuple description for sorting */
|
/* Create tuple description for sorting */
|
||||||
buildstate->sortdesc = CreateTemplateTupleDesc(3);
|
buildstate->tupdesc = CreateTemplateTupleDesc(3);
|
||||||
TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 1, "list", INT4OID, -1, 0);
|
TupleDescInitEntry(buildstate->tupdesc, (AttrNumber) 1, "list", INT4OID, -1, 0);
|
||||||
TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 2, "tid", TIDOID, -1, 0);
|
TupleDescInitEntry(buildstate->tupdesc, (AttrNumber) 2, "tid", TIDOID, -1, 0);
|
||||||
TupleDescInitEntry(buildstate->sortdesc, (AttrNumber) 3, "vector", TupleDescAttr(buildstate->tupdesc, 0)->atttypid, -1, 0);
|
TupleDescInitEntry(buildstate->tupdesc, (AttrNumber) 3, "vector", RelationGetDescr(index)->attrs[0].atttypid, -1, 0);
|
||||||
|
|
||||||
buildstate->slot = MakeSingleTupleTableSlot(buildstate->sortdesc, &TTSOpsVirtual);
|
buildstate->slot = MakeSingleTupleTableSlot(buildstate->tupdesc, &TTSOpsVirtual);
|
||||||
|
|
||||||
buildstate->centers = VectorArrayInit(buildstate->lists, buildstate->dimensions, buildstate->typeInfo->itemSize(buildstate->dimensions));
|
buildstate->centers = VectorArrayInit(buildstate->lists, buildstate->dimensions, buildstate->typeInfo->itemSize(buildstate->dimensions));
|
||||||
buildstate->listInfo = palloc(sizeof(ListInfo) * buildstate->lists);
|
buildstate->listInfo = palloc(sizeof(ListInfo) * buildstate->lists);
|
||||||
@@ -634,7 +633,7 @@ IvfflatParallelScanAndSort(IvfflatSpool * ivfspool, IvfflatShared * ivfshared, S
|
|||||||
InitBuildState(&buildstate, ivfspool->heap, ivfspool->index, indexInfo);
|
InitBuildState(&buildstate, ivfspool->heap, ivfspool->index, indexInfo);
|
||||||
memcpy(buildstate.centers->items, ivfcenters, buildstate.centers->itemsize * buildstate.centers->maxlen);
|
memcpy(buildstate.centers->items, ivfcenters, buildstate.centers->itemsize * buildstate.centers->maxlen);
|
||||||
buildstate.centers->length = buildstate.centers->maxlen;
|
buildstate.centers->length = buildstate.centers->maxlen;
|
||||||
ivfspool->sortstate = InitBuildSortState(buildstate.sortdesc, sortmem, coordinate);
|
ivfspool->sortstate = InitBuildSortState(buildstate.tupdesc, sortmem, coordinate);
|
||||||
buildstate.sortstate = ivfspool->sortstate;
|
buildstate.sortstate = ivfspool->sortstate;
|
||||||
scan = table_beginscan_parallel(ivfspool->heap,
|
scan = table_beginscan_parallel(ivfspool->heap,
|
||||||
ParallelTableScanFromIvfflatShared(ivfshared));
|
ParallelTableScanFromIvfflatShared(ivfshared));
|
||||||
@@ -951,7 +950,7 @@ AssignTuples(IvfflatBuildState * buildstate)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Begin serial/leader tuplesort */
|
/* Begin serial/leader tuplesort */
|
||||||
buildstate->sortstate = InitBuildSortState(buildstate->sortdesc, maintenance_work_mem, coordinate);
|
buildstate->sortstate = InitBuildSortState(buildstate->tupdesc, maintenance_work_mem, coordinate);
|
||||||
|
|
||||||
/* Add tuples to sort */
|
/* Add tuples to sort */
|
||||||
if (buildstate->heap != NULL)
|
if (buildstate->heap != NULL)
|
||||||
|
|||||||
@@ -17,16 +17,8 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
int ivfflat_probes;
|
int ivfflat_probes;
|
||||||
int ivfflat_iterative_scan;
|
|
||||||
int ivfflat_max_probes;
|
|
||||||
static relopt_kind ivfflat_relopt_kind;
|
static relopt_kind ivfflat_relopt_kind;
|
||||||
|
|
||||||
static const struct config_enum_entry ivfflat_iterative_scan_options[] = {
|
|
||||||
{"off", IVFFLAT_ITERATIVE_SCAN_OFF, false},
|
|
||||||
{"relaxed_order", IVFFLAT_ITERATIVE_SCAN_RELAXED, false},
|
|
||||||
{NULL, 0, false}
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Initialize index options and variables
|
* Initialize index options and variables
|
||||||
*/
|
*/
|
||||||
@@ -41,15 +33,6 @@ 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_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);
|
|
||||||
|
|
||||||
/* 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");
|
MarkGUCPrefixReserved("ivfflat");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,10 +82,6 @@ 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,6 @@
|
|||||||
|
|
||||||
/* Variables */
|
/* Variables */
|
||||||
extern int ivfflat_probes;
|
extern int ivfflat_probes;
|
||||||
extern int ivfflat_iterative_scan;
|
|
||||||
extern int ivfflat_max_probes;
|
|
||||||
|
|
||||||
typedef enum IvfflatIterativeScanMode
|
|
||||||
{
|
|
||||||
IVFFLAT_ITERATIVE_SCAN_OFF,
|
|
||||||
IVFFLAT_ITERATIVE_SCAN_RELAXED
|
|
||||||
} IvfflatIterativeScanMode;
|
|
||||||
|
|
||||||
typedef struct VectorArrayData
|
typedef struct VectorArrayData
|
||||||
{
|
{
|
||||||
@@ -173,7 +165,6 @@ typedef struct IvfflatBuildState
|
|||||||
Relation index;
|
Relation index;
|
||||||
IndexInfo *indexInfo;
|
IndexInfo *indexInfo;
|
||||||
const IvfflatTypeInfo *typeInfo;
|
const IvfflatTypeInfo *typeInfo;
|
||||||
TupleDesc tupdesc;
|
|
||||||
|
|
||||||
/* Settings */
|
/* Settings */
|
||||||
int dimensions;
|
int dimensions;
|
||||||
@@ -207,7 +198,7 @@ typedef struct IvfflatBuildState
|
|||||||
|
|
||||||
/* Sorting */
|
/* Sorting */
|
||||||
Tuplesortstate *sortstate;
|
Tuplesortstate *sortstate;
|
||||||
TupleDesc sortdesc;
|
TupleDesc tupdesc;
|
||||||
TupleTableSlot *slot;
|
TupleTableSlot *slot;
|
||||||
|
|
||||||
/* Memory */
|
/* Memory */
|
||||||
@@ -256,11 +247,8 @@ typedef struct IvfflatScanOpaqueData
|
|||||||
{
|
{
|
||||||
const IvfflatTypeInfo *typeInfo;
|
const IvfflatTypeInfo *typeInfo;
|
||||||
int probes;
|
int probes;
|
||||||
int maxProbes;
|
|
||||||
int dimensions;
|
int dimensions;
|
||||||
bool first;
|
bool first;
|
||||||
Datum value;
|
|
||||||
MemoryContext tmpCtx;
|
|
||||||
|
|
||||||
/* Sorting */
|
/* Sorting */
|
||||||
Tuplesortstate *sortstate;
|
Tuplesortstate *sortstate;
|
||||||
@@ -277,9 +265,7 @@ typedef struct IvfflatScanOpaqueData
|
|||||||
|
|
||||||
/* Lists */
|
/* Lists */
|
||||||
pairingheap *listQueue;
|
pairingheap *listQueue;
|
||||||
BlockNumber *listPages;
|
IvfflatScanList lists[FLEXIBLE_ARRAY_MEMBER]; /* must come last */
|
||||||
int listIndex;
|
|
||||||
IvfflatScanList *lists;
|
|
||||||
} IvfflatScanOpaqueData;
|
} IvfflatScanOpaqueData;
|
||||||
|
|
||||||
typedef IvfflatScanOpaqueData * IvfflatScanOpaque;
|
typedef IvfflatScanOpaqueData * IvfflatScanOpaque;
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ InsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heap_tid, R
|
|||||||
IvfflatGetMetaPageInfo(index, NULL, NULL);
|
IvfflatGetMetaPageInfo(index, NULL, NULL);
|
||||||
|
|
||||||
/* Find the insert page - sets the page and list info */
|
/* Find the insert page - sets the page and list info */
|
||||||
FindInsertPage(index, &value, &insertPage, &listInfo);
|
FindInsertPage(index, values, &insertPage, &listInfo);
|
||||||
Assert(BlockNumberIsValid(insertPage));
|
Assert(BlockNumberIsValid(insertPage));
|
||||||
originalInsertPage = insertPage;
|
originalInsertPage = insertPage;
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,10 @@
|
|||||||
#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)
|
||||||
@@ -62,7 +65,7 @@ GetScanLists(IndexScanDesc scan, Datum value)
|
|||||||
/* Use procinfo from the index instead of scan key for performance */
|
/* Use procinfo from the index instead of scan key for performance */
|
||||||
distance = DatumGetFloat8(so->distfunc(so->procinfo, so->collation, PointerGetDatum(&list->center), value));
|
distance = DatumGetFloat8(so->distfunc(so->procinfo, so->collation, PointerGetDatum(&list->center), value));
|
||||||
|
|
||||||
if (listCount < so->maxProbes)
|
if (listCount < so->probes)
|
||||||
{
|
{
|
||||||
IvfflatScanList *scanlist;
|
IvfflatScanList *scanlist;
|
||||||
|
|
||||||
@@ -75,7 +78,7 @@ GetScanLists(IndexScanDesc scan, Datum value)
|
|||||||
pairingheap_add(so->listQueue, &scanlist->ph_node);
|
pairingheap_add(so->listQueue, &scanlist->ph_node);
|
||||||
|
|
||||||
/* Calculate max distance */
|
/* Calculate max distance */
|
||||||
if (listCount == so->maxProbes)
|
if (listCount == so->probes)
|
||||||
maxDistance = GetScanList(pairingheap_first(so->listQueue))->distance;
|
maxDistance = GetScanList(pairingheap_first(so->listQueue))->distance;
|
||||||
}
|
}
|
||||||
else if (distance < maxDistance)
|
else if (distance < maxDistance)
|
||||||
@@ -99,11 +102,6 @@ GetScanLists(IndexScanDesc scan, Datum value)
|
|||||||
|
|
||||||
UnlockReleaseBuffer(cbuf);
|
UnlockReleaseBuffer(cbuf);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = listCount - 1; i >= 0; i--)
|
|
||||||
so->listPages[i] = GetScanList(pairingheap_remove_first(so->listQueue))->startPage;
|
|
||||||
|
|
||||||
Assert(pairingheap_is_empty(so->listQueue));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -114,15 +112,13 @@ 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;
|
|
||||||
|
|
||||||
tuplesort_reset(so->sortstate);
|
|
||||||
|
|
||||||
/* Search closest probes lists */
|
/* Search closest probes lists */
|
||||||
while (so->listIndex < so->maxProbes && (++batchProbes) <= so->probes)
|
while (!pairingheap_is_empty(so->listQueue))
|
||||||
{
|
{
|
||||||
BlockNumber searchPage = so->listPages[so->listIndex++];
|
BlockNumber searchPage = GetScanList(pairingheap_remove_first(so->listQueue))->startPage;
|
||||||
|
|
||||||
/* Search all entry pages for list */
|
/* Search all entry pages for list */
|
||||||
while (BlockNumberIsValid(searchPage))
|
while (BlockNumberIsValid(searchPage))
|
||||||
@@ -160,6 +156,8 @@ 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;
|
||||||
@@ -168,11 +166,13 @@ GetScanItems(IndexScanDesc scan, Datum value)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tuplesort_performsort(so->sortstate);
|
if (tuples < 100)
|
||||||
|
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.")));
|
||||||
|
|
||||||
#if defined(IVFFLAT_MEMORY)
|
tuplesort_performsort(so->sortstate);
|
||||||
elog(INFO, "memory: %zu MB", MemoryContextMemAllocated(CurrentMemoryContext, true) / (1024 * 1024));
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -209,13 +209,7 @@ 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;
|
||||||
@@ -246,30 +240,19 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys)
|
|||||||
int lists;
|
int lists;
|
||||||
int dimensions;
|
int dimensions;
|
||||||
int probes = ivfflat_probes;
|
int probes = ivfflat_probes;
|
||||||
int maxProbes;
|
|
||||||
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 (ivfflat_iterative_scan != IVFFLAT_ITERATIVE_SCAN_OFF)
|
|
||||||
maxProbes = Max(ivfflat_max_probes, probes);
|
|
||||||
else
|
|
||||||
maxProbes = probes;
|
|
||||||
|
|
||||||
if (probes > lists)
|
if (probes > lists)
|
||||||
probes = lists;
|
probes = lists;
|
||||||
|
|
||||||
if (maxProbes > lists)
|
so = (IvfflatScanOpaque) palloc(offsetof(IvfflatScanOpaqueData, lists) + probes * sizeof(IvfflatScanList));
|
||||||
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;
|
||||||
so->maxProbes = maxProbes;
|
|
||||||
so->dimensions = dimensions;
|
so->dimensions = dimensions;
|
||||||
|
|
||||||
/* Set support functions */
|
/* Set support functions */
|
||||||
@@ -277,12 +260,6 @@ 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);
|
||||||
@@ -303,11 +280,6 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys)
|
|||||||
so->bas = GetAccessStrategy(BAS_BULKREAD);
|
so->bas = GetAccessStrategy(BAS_BULKREAD);
|
||||||
|
|
||||||
so->listQueue = pairingheap_allocate(CompareLists, scan);
|
so->listQueue = pairingheap_allocate(CompareLists, scan);
|
||||||
so->listPages = palloc(maxProbes * sizeof(BlockNumber));
|
|
||||||
so->listIndex = 0;
|
|
||||||
so->lists = palloc(maxProbes * sizeof(IvfflatScanList));
|
|
||||||
|
|
||||||
MemoryContextSwitchTo(oldCtx);
|
|
||||||
|
|
||||||
scan->opaque = so;
|
scan->opaque = so;
|
||||||
|
|
||||||
@@ -322,9 +294,11 @@ ivfflatrescan(IndexScanDesc scan, ScanKey keys, int nkeys, ScanKey orderbys, int
|
|||||||
{
|
{
|
||||||
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
|
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
|
||||||
|
|
||||||
|
if (!so->first)
|
||||||
|
tuplesort_reset(so->sortstate);
|
||||||
|
|
||||||
so->first = true;
|
so->first = true;
|
||||||
pairingheap_reset(so->listQueue);
|
pairingheap_reset(so->listQueue);
|
||||||
so->listIndex = 0;
|
|
||||||
|
|
||||||
if (keys && scan->numberOfKeys > 0)
|
if (keys && scan->numberOfKeys > 0)
|
||||||
memmove(scan->keyData, keys, scan->numberOfKeys * sizeof(ScanKeyData));
|
memmove(scan->keyData, keys, scan->numberOfKeys * sizeof(ScanKeyData));
|
||||||
@@ -340,8 +314,6 @@ bool
|
|||||||
ivfflatgettuple(IndexScanDesc scan, ScanDirection dir)
|
ivfflatgettuple(IndexScanDesc scan, ScanDirection dir)
|
||||||
{
|
{
|
||||||
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
|
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
|
||||||
ItemPointer heaptid;
|
|
||||||
bool isnull;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Index can be used to scan backward, but Postgres doesn't support
|
* Index can be used to scan backward, but Postgres doesn't support
|
||||||
@@ -369,23 +341,28 @@ ivfflatgettuple(IndexScanDesc scan, ScanDirection dir)
|
|||||||
IvfflatBench("GetScanLists", GetScanLists(scan, value));
|
IvfflatBench("GetScanLists", GetScanLists(scan, value));
|
||||||
IvfflatBench("GetScanItems", GetScanItems(scan, value));
|
IvfflatBench("GetScanItems", GetScanItems(scan, value));
|
||||||
so->first = false;
|
so->first = false;
|
||||||
so->value = value;
|
|
||||||
|
#if defined(IVFFLAT_MEMORY)
|
||||||
|
elog(INFO, "memory: %zu MB", MemoryContextMemAllocated(CurrentMemoryContext, true) / (1024 * 1024));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Clean up if we allocated a new value */
|
||||||
|
if (value != scan->orderByData->sk_argument)
|
||||||
|
pfree(DatumGetPointer(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
while (!tuplesort_gettupleslot(so->sortstate, true, false, so->mslot, NULL))
|
if (tuplesort_gettupleslot(so->sortstate, true, false, so->mslot, NULL))
|
||||||
{
|
{
|
||||||
if (so->listIndex == so->maxProbes)
|
bool isnull;
|
||||||
return false;
|
ItemPointer heaptid = (ItemPointer) DatumGetPointer(slot_getattr(so->mslot, 2, &isnull));
|
||||||
|
|
||||||
IvfflatBench("GetScanItems", GetScanItems(scan, so->value));
|
|
||||||
}
|
|
||||||
|
|
||||||
heaptid = (ItemPointer) DatumGetPointer(slot_getattr(so->mslot, 2, &isnull));
|
|
||||||
|
|
||||||
scan->xs_heaptid = *heaptid;
|
scan->xs_heaptid = *heaptid;
|
||||||
scan->xs_recheck = false;
|
scan->xs_recheck = false;
|
||||||
scan->xs_recheckorderby = false;
|
scan->xs_recheckorderby = false;
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -396,10 +373,12 @@ ivfflatendscan(IndexScanDesc scan)
|
|||||||
{
|
{
|
||||||
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
|
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
|
||||||
|
|
||||||
/* Free any temporary files */
|
pairingheap_free(so->listQueue);
|
||||||
tuplesort_end(so->sortstate);
|
tuplesort_end(so->sortstate);
|
||||||
|
FreeAccessStrategy(so->bas);
|
||||||
|
FreeTupleDesc(so->tupdesc);
|
||||||
|
|
||||||
MemoryContextDelete(so->tmpCtx);
|
/* TODO Free vslot and mslot without freeing TupleDesc */
|
||||||
|
|
||||||
pfree(so);
|
pfree(so);
|
||||||
scan->opaque = NULL;
|
scan->opaque = NULL;
|
||||||
|
|||||||
@@ -355,7 +355,7 @@ ivfflat_halfvec_support(PG_FUNCTION_ARGS)
|
|||||||
};
|
};
|
||||||
|
|
||||||
PG_RETURN_POINTER(&typeInfo);
|
PG_RETURN_POINTER(&typeInfo);
|
||||||
}
|
};
|
||||||
|
|
||||||
FUNCTION_PREFIX PG_FUNCTION_INFO_V1(ivfflat_bit_support);
|
FUNCTION_PREFIX PG_FUNCTION_INFO_V1(ivfflat_bit_support);
|
||||||
Datum
|
Datum
|
||||||
@@ -370,4 +370,4 @@ ivfflat_bit_support(PG_FUNCTION_ARGS)
|
|||||||
};
|
};
|
||||||
|
|
||||||
PG_RETURN_POINTER(&typeInfo);
|
PG_RETURN_POINTER(&typeInfo);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ ivfflatbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
|
|||||||
Page cpage;
|
Page cpage;
|
||||||
OffsetNumber coffno;
|
OffsetNumber coffno;
|
||||||
OffsetNumber cmaxoffno;
|
OffsetNumber cmaxoffno;
|
||||||
BlockNumber listPages[MaxOffsetNumber];
|
BlockNumber startPages[MaxOffsetNumber];
|
||||||
ListInfo listInfo;
|
ListInfo listInfo;
|
||||||
|
|
||||||
cbuf = ReadBuffer(index, blkno);
|
cbuf = ReadBuffer(index, blkno);
|
||||||
@@ -40,7 +40,7 @@ ivfflatbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
|
|||||||
{
|
{
|
||||||
IvfflatList list = (IvfflatList) PageGetItem(cpage, PageGetItemId(cpage, coffno));
|
IvfflatList list = (IvfflatList) PageGetItem(cpage, PageGetItemId(cpage, coffno));
|
||||||
|
|
||||||
listPages[coffno - FirstOffsetNumber] = list->startPage;
|
startPages[coffno - FirstOffsetNumber] = list->startPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
listInfo.blkno = blkno;
|
listInfo.blkno = blkno;
|
||||||
@@ -50,7 +50,7 @@ ivfflatbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
|
|||||||
|
|
||||||
for (coffno = FirstOffsetNumber; coffno <= cmaxoffno; coffno = OffsetNumberNext(coffno))
|
for (coffno = FirstOffsetNumber; coffno <= cmaxoffno; coffno = OffsetNumberNext(coffno))
|
||||||
{
|
{
|
||||||
BlockNumber searchPage = listPages[coffno - FirstOffsetNumber];
|
BlockNumber searchPage = startPages[coffno - FirstOffsetNumber];
|
||||||
BlockNumber insertPage = InvalidBlockNumber;
|
BlockNumber insertPage = InvalidBlockNumber;
|
||||||
|
|
||||||
/* Iterate over entry pages */
|
/* Iterate over entry pages */
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
#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"
|
||||||
@@ -13,10 +12,17 @@
|
|||||||
#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,32 +99,6 @@ 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));
|
||||||
@@ -165,31 +139,4 @@ 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,37 +81,6 @@ 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));
|
||||||
@@ -140,27 +109,4 @@ 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,23 +57,6 @@ 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));
|
||||||
@@ -98,17 +81,4 @@ 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,25 +44,6 @@ 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));
|
||||||
@@ -81,16 +62,4 @@ 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;
|
||||||
|
|||||||
@@ -6,7 +6,13 @@ use Test::More;
|
|||||||
|
|
||||||
my $dim = 3;
|
my $dim = 3;
|
||||||
|
|
||||||
my $array_sql = join(",", ('random()') x $dim);
|
my @r = ();
|
||||||
|
for (1 .. $dim)
|
||||||
|
{
|
||||||
|
my $v = int(rand(1000)) + 1;
|
||||||
|
push(@r, "i % $v");
|
||||||
|
}
|
||||||
|
my $array_sql = join(", ", @r);
|
||||||
|
|
||||||
# Initialize node
|
# Initialize node
|
||||||
my $node = PostgreSQL::Test::Cluster->new('node');
|
my $node = PostgreSQL::Test::Cluster->new('node');
|
||||||
@@ -17,20 +23,19 @@ $node->start;
|
|||||||
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
|
$node->safe_psql("postgres", "CREATE 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 % 10, ARRAY[$array_sql] FROM generate_series(1, 100000) i;"
|
||||||
);
|
);
|
||||||
$node->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v vector_l2_ops);");
|
$node->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v vector_l2_ops);");
|
||||||
|
|
||||||
# Get size
|
# Get size
|
||||||
my $size = $node->safe_psql("postgres", "SELECT pg_total_relation_size('tst_v_idx');");
|
my $size = $node->safe_psql("postgres", "SELECT pg_total_relation_size('tst_v_idx');");
|
||||||
|
|
||||||
# Store values
|
|
||||||
$node->safe_psql("postgres", "CREATE TABLE tmp AS SELECT * FROM tst;");
|
|
||||||
|
|
||||||
# Delete all, vacuum, and insert same data
|
# Delete all, vacuum, and insert same data
|
||||||
$node->safe_psql("postgres", "DELETE FROM tst;");
|
$node->safe_psql("postgres", "DELETE FROM tst;");
|
||||||
$node->safe_psql("postgres", "VACUUM tst;");
|
$node->safe_psql("postgres", "VACUUM tst;");
|
||||||
$node->safe_psql("postgres", "INSERT INTO tst SELECT * FROM tmp;");
|
$node->safe_psql("postgres",
|
||||||
|
"INSERT INTO tst SELECT i % 10, ARRAY[$array_sql] FROM generate_series(1, 100000) i;"
|
||||||
|
);
|
||||||
|
|
||||||
# Check size
|
# Check size
|
||||||
my $new_size = $node->safe_psql("postgres", "SELECT pg_total_relation_size('tst_v_idx');");
|
my $new_size = $node->safe_psql("postgres", "SELECT pg_total_relation_size('tst_v_idx');");
|
||||||
|
|||||||
@@ -26,26 +26,25 @@ $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_scan = relaxed_order;
|
SET hnsw.streaming = on;
|
||||||
SET hnsw.max_scan_tuples = 100000;
|
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 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);
|
||||||
|
|
||||||
foreach ((30000, 50000, 70000))
|
foreach ((30000, 50000, 70000))
|
||||||
{
|
{
|
||||||
my $max_tuples = $_;
|
my $ef_stream = $_;
|
||||||
my $expected = $max_tuples / 10000;
|
my $expected = $ef_stream / 10000;
|
||||||
my $sum = 0;
|
my $sum = 0;
|
||||||
|
|
||||||
for my $i (1 .. 20)
|
for my $i (1 .. 20)
|
||||||
{
|
{
|
||||||
$count = $node->safe_psql("postgres", qq(
|
$count = $node->safe_psql("postgres", qq(
|
||||||
SET enable_seqscan = off;
|
SET enable_seqscan = off;
|
||||||
SET hnsw.iterative_scan = relaxed_order;
|
SET hnsw.streaming = on;
|
||||||
SET hnsw.max_scan_tuples = $max_tuples;
|
SET hnsw.ef_stream = $ef_stream;
|
||||||
SET hnsw.scan_mem_multiplier = 2;
|
SET work_mem = '8MB';
|
||||||
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;
|
||||||
@@ -56,4 +55,13 @@ 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.streaming = 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();
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
use strict;
|
|
||||||
use warnings FATAL => 'all';
|
|
||||||
use PostgreSQL::Test::Cluster;
|
|
||||||
use PostgreSQL::Test::Utils;
|
|
||||||
use Test::More;
|
|
||||||
|
|
||||||
my $dim = 3;
|
|
||||||
my $array_sql = join(",", ('random()') x $dim);
|
|
||||||
|
|
||||||
# Initialize node
|
|
||||||
my $node = PostgreSQL::Test::Cluster->new('node');
|
|
||||||
$node->init;
|
|
||||||
$node->start;
|
|
||||||
|
|
||||||
# Create table
|
|
||||||
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
|
|
||||||
$node->safe_psql("postgres", "CREATE TABLE tst (i int4 PRIMARY KEY, v vector($dim));");
|
|
||||||
$node->safe_psql("postgres",
|
|
||||||
"INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(1, 100000) i;"
|
|
||||||
);
|
|
||||||
$node->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v vector_l2_ops);");
|
|
||||||
|
|
||||||
my $count = $node->safe_psql("postgres", qq(
|
|
||||||
SET enable_seqscan = off;
|
|
||||||
SET ivfflat.probes = 10;
|
|
||||||
SET ivfflat.iterative_scan = relaxed_order;
|
|
||||||
SELECT COUNT(*) FROM (SELECT v FROM tst WHERE i % 10000 = 0 ORDER BY v <-> (SELECT v FROM tst LIMIT 1) LIMIT 11) t;
|
|
||||||
));
|
|
||||||
is($count, 10);
|
|
||||||
|
|
||||||
foreach ((30, 50, 70))
|
|
||||||
{
|
|
||||||
my $max_probes = $_;
|
|
||||||
my $expected = $max_probes / 10;
|
|
||||||
my $sum = 0;
|
|
||||||
|
|
||||||
for my $i (1 .. 20)
|
|
||||||
{
|
|
||||||
$count = $node->safe_psql("postgres", qq(
|
|
||||||
SET enable_seqscan = off;
|
|
||||||
SET ivfflat.probes = 10;
|
|
||||||
SET ivfflat.iterative_scan = relaxed_order;
|
|
||||||
SET ivfflat.max_probes = $max_probes;
|
|
||||||
SELECT COUNT(*) FROM (SELECT v FROM tst WHERE i % 10000 = 0 ORDER BY v <-> (SELECT v FROM tst WHERE i = $i) LIMIT 11) t;
|
|
||||||
));
|
|
||||||
$sum += $count;
|
|
||||||
}
|
|
||||||
|
|
||||||
my $avg = $sum / 20;
|
|
||||||
cmp_ok($avg, '>', $expected - 2);
|
|
||||||
cmp_ok($avg, '<', $expected + 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
done_testing();
|
|
||||||
@@ -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 = (50, 500);
|
my @cs = (100, 1000);
|
||||||
|
|
||||||
sub test_recall
|
sub test_recall
|
||||||
{
|
{
|
||||||
my ($c, $ef_search, $min, $operator, $mode) = @_;
|
my ($c, $ef_search, $min, $operator) = @_;
|
||||||
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_scan = $mode;
|
SET hnsw.streaming = on;
|
||||||
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_scan = $mode;
|
SET hnsw.streaming = on;
|
||||||
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 $mode $c");
|
cmp_ok($correct / $total, ">=", $min, $operator);
|
||||||
}
|
}
|
||||||
|
|
||||||
# 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, 50000) i;"
|
"INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(1, 100000) i;"
|
||||||
);
|
);
|
||||||
|
|
||||||
# Generate queries
|
# Generate queries
|
||||||
@@ -108,8 +108,21 @@ for my $i (0 .. $#operators)
|
|||||||
push(@expected, $res);
|
push(@expected, $res);
|
||||||
}
|
}
|
||||||
|
|
||||||
test_recall($c, 40, 0.99, $operator, "strict_order");
|
if ($c == 100)
|
||||||
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,125 +0,0 @@
|
|||||||
use strict;
|
|
||||||
use warnings FATAL => 'all';
|
|
||||||
use PostgreSQL::Test::Cluster;
|
|
||||||
use PostgreSQL::Test::Utils;
|
|
||||||
use Test::More;
|
|
||||||
|
|
||||||
my $node;
|
|
||||||
my @queries = ();
|
|
||||||
my @expected;
|
|
||||||
my $limit = 20;
|
|
||||||
my @cs = (100, 1000);
|
|
||||||
|
|
||||||
sub test_recall
|
|
||||||
{
|
|
||||||
my ($c, $probes, $min, $operator) = @_;
|
|
||||||
my $correct = 0;
|
|
||||||
my $total = 0;
|
|
||||||
|
|
||||||
my $explain = $node->safe_psql("postgres", qq(
|
|
||||||
SET enable_seqscan = off;
|
|
||||||
SET ivfflat.probes = $probes;
|
|
||||||
SET ivfflat.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/);
|
|
||||||
|
|
||||||
for my $i (0 .. $#queries)
|
|
||||||
{
|
|
||||||
my $actual = $node->safe_psql("postgres", qq(
|
|
||||||
SET enable_seqscan = off;
|
|
||||||
SET ivfflat.probes = $probes;
|
|
||||||
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);
|
|
||||||
|
|
||||||
my @expected_ids = split("\n", $expected[$i]);
|
|
||||||
my %expected_set = map { $_ => 1 } @expected_ids;
|
|
||||||
|
|
||||||
foreach (@actual_ids)
|
|
||||||
{
|
|
||||||
if (exists($expected_set{$_}))
|
|
||||||
{
|
|
||||||
$correct++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$total += $limit;
|
|
||||||
}
|
|
||||||
|
|
||||||
cmp_ok($correct / $total, ">=", $min, "$operator $c");
|
|
||||||
}
|
|
||||||
|
|
||||||
# Initialize node
|
|
||||||
$node = PostgreSQL::Test::Cluster->new('node');
|
|
||||||
$node->init;
|
|
||||||
$node->start;
|
|
||||||
|
|
||||||
# Create table
|
|
||||||
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
|
|
||||||
$node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector(3));");
|
|
||||||
$node->safe_psql("postgres",
|
|
||||||
"INSERT INTO tst SELECT i, ARRAY[random(), random(), random()] FROM generate_series(1, 100000) i;"
|
|
||||||
);
|
|
||||||
|
|
||||||
# Generate queries
|
|
||||||
for (1 .. 20)
|
|
||||||
{
|
|
||||||
my $r1 = rand();
|
|
||||||
my $r2 = rand();
|
|
||||||
my $r3 = rand();
|
|
||||||
push(@queries, "[$r1,$r2,$r3]");
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check each index type
|
|
||||||
my @operators = ("<->", "<=>");
|
|
||||||
my @opclasses = ("vector_l2_ops", "vector_cosine_ops");
|
|
||||||
|
|
||||||
for my $i (0 .. $#operators)
|
|
||||||
{
|
|
||||||
my $operator = $operators[$i];
|
|
||||||
my $opclass = $opclasses[$i];
|
|
||||||
|
|
||||||
$node->safe_psql("postgres", "CREATE INDEX idx ON tst USING ivfflat (v $opclass);");
|
|
||||||
|
|
||||||
foreach (@cs)
|
|
||||||
{
|
|
||||||
my $c = $_;
|
|
||||||
|
|
||||||
# Get exact results
|
|
||||||
@expected = ();
|
|
||||||
foreach (@queries)
|
|
||||||
{
|
|
||||||
my $res = $node->safe_psql("postgres", qq(
|
|
||||||
SET enable_indexscan = off;
|
|
||||||
WITH top AS (
|
|
||||||
SELECT v $operator '$_' AS distance FROM tst WHERE i % $c = 0 ORDER BY distance LIMIT $limit
|
|
||||||
)
|
|
||||||
SELECT i FROM tst WHERE (v $operator '$_') <= (SELECT MAX(distance) FROM top)
|
|
||||||
));
|
|
||||||
push(@expected, $res);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($c == 100)
|
|
||||||
{
|
|
||||||
test_recall($c, 1, 0.57, $operator);
|
|
||||||
test_recall($c, 10, 0.98, $operator);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if ($operator eq "<->")
|
|
||||||
{
|
|
||||||
test_recall($c, 1, 0.80, $operator);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
test_recall($c, 1, 0.88, $operator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$node->safe_psql("postgres", "DROP INDEX idx;");
|
|
||||||
}
|
|
||||||
|
|
||||||
done_testing();
|
|
||||||
@@ -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.8.0'
|
default_version = '0.7.4'
|
||||||
module_pathname = '$libdir/vector'
|
module_pathname = '$libdir/vector'
|
||||||
relocatable = true
|
relocatable = true
|
||||||
|
|||||||
Reference in New Issue
Block a user