Compare commits

..

24 Commits

Author SHA1 Message Date
Andrew Kane
915a630d00 Revert "Free tuple"
This reverts commit 466ec1ac46.
2023-10-22 16:09:07 -07:00
Andrew Kane
20c24e2d18 Added comment [skip ci] 2023-10-22 11:18:58 -07:00
Andrew Kane
466ec1ac46 Free tuple 2023-10-22 11:15:14 -07:00
Andrew Kane
ff2bdfb3b4 Improved check [skip ci] 2023-10-22 11:15:00 -07:00
Andrew Kane
0a3669a680 Added support for index-only scans to HNSW 2023-09-02 18:54:46 -07:00
Andrew Kane
034d4acaea Removed comment [skip ci] 2023-09-02 18:23:08 -07:00
Andrew Kane
01f58e470a Revert "Use int64 for wlen"
This reverts commit dbef8d1ad1.
2023-09-02 18:09:10 -07:00
Andrew Kane
dbef8d1ad1 Use int64 for wlen 2023-09-02 17:55:48 -07:00
Andrew Kane
5c005cf57c Revert "No need to increment wlen when removing"
This reverts commit 6b2e215447.
2023-09-02 17:41:31 -07:00
Andrew Kane
5665a11a05 Updated comment [skip ci] 2023-09-02 15:34:33 -07:00
Andrew Kane
6b2e215447 No need to increment wlen when removing 2023-09-02 15:33:40 -07:00
Andrew Kane
0d86191eaf Improved test for cosine distance [skip ci] 2023-09-01 19:59:21 -07:00
Andrew Kane
cf9f7aeea9 Added another test for cosine distance [skip ci] 2023-09-01 19:57:28 -07:00
Andrew Kane
0b0e542ce6 Fixed auto-vectorization for vector_spherical_distance with MSVC 2023-09-01 18:42:37 -07:00
Andrew Kane
a4590d2d9d Simplified WAL tests [skip ci] 2023-09-01 15:49:52 -07:00
Andrew Kane
9ebec1529b Updated comments [skip ci] 2023-09-01 00:35:06 -07:00
Andrew Kane
77ff4c18f0 Updated comments [skip ci] 2023-09-01 00:32:42 -07:00
Andrew Kane
88dabaa41c Added test for IVFFlat insert recall 2023-09-01 00:30:02 -07:00
Andrew Kane
1809ffa52b Renamed test [skip ci] 2023-09-01 00:15:07 -07:00
Andrew Kane
024f283ee8 Updated header order [skip ci] 2023-09-01 00:14:03 -07:00
Andrew Kane
da3b2fab46 Updated readme [skip ci] 2023-08-31 22:20:13 -07:00
Andrew Kane
884026a23c Updated changelog [skip ci] 2023-08-29 10:13:05 -07:00
Andrew Kane
4d352e6c30 Updated changelog [skip ci] 2023-08-29 10:11:53 -07:00
Andrew Kane
a8e257e1f1 Added comments [skip ci] 2023-08-28 22:02:48 -07:00
16 changed files with 168 additions and 22 deletions

View File

@@ -1,13 +1,13 @@
## 0.5.0 (2023-08-28)
- Added HNSW index type
- Added support for parallel index builds
- Added support for parallel index builds for IVFFlat
- Added `l1_distance` function
- Added element-wise multiplication for vectors
- Added `sum` aggregate
- Improved performance of distance functions
- Fixed out of range results for cosine distance
- Fixed results for NULL and NaN distances
- Fixed results for NULL and NaN distances for IVFFlat
## 0.4.4 (2023-06-12)

View File

@@ -162,7 +162,7 @@ You can add an index to use approximate nearest neighbor search, which trades so
Supported index types are:
- [IVFFlat](#ivfflat)
- [HNSW](#hnsw) - *added in 0.5.0*
- [HNSW](#hnsw) - added in 0.5.0
## IVFFlat
@@ -282,8 +282,8 @@ SELECT phase, tuples_done, tuples_total FROM pg_stat_progress_create_index;
The phases are:
1. `initializing`
2. `performing k-means` (IVFFlat only)
3. `assigning tuples` (IVFFlat only)
2. `performing k-means` - IVFFlat only
3. `assigning tuples` - IVFFlat only
4. `loading tuples`
Note: `tuples_done` and `tuples_total` are only populated during the `loading tuples` phase

View File

@@ -155,6 +155,15 @@ hnswvalidate(Oid opclassoid)
return true;
}
/*
* Checks if index-only scan is supported
*/
static bool
hnswcanreturn(Relation indexRelation, int attno)
{
return attno == 1 && !OidIsValid(index_getprocid(indexRelation, 1, HNSW_NORM_PROC));
}
/*
* Define index handler
*
@@ -196,7 +205,7 @@ hnswhandler(PG_FUNCTION_ARGS)
amroutine->aminsert = hnswinsert;
amroutine->ambulkdelete = hnswbulkdelete;
amroutine->amvacuumcleanup = hnswvacuumcleanup;
amroutine->amcanreturn = NULL; /* tuple not included in heapsort */
amroutine->amcanreturn = hnswcanreturn;
amroutine->amcostestimate = hnswcostestimate;
amroutine->amoptions = hnswoptions;
amroutine->amproperty = NULL; /* TODO AMPROP_DISTANCE_ORDERABLE */

View File

@@ -266,7 +266,7 @@ Buffer HnswNewBuffer(Relation index, ForkNumber forkNum);
void HnswInitPage(Buffer buf, Page page);
void HnswInitRegisterPage(Relation index, Buffer *buf, Page *page, GenericXLogState **state);
void HnswInit(void);
List *HnswSearchLayer(Datum q, List *ep, int ef, int lc, Relation index, FmgrInfo *procinfo, Oid collation, bool inserting, HnswElement skipElement);
List *HnswSearchLayer(Datum q, List *ep, int ef, int lc, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec, HnswElement skipElement);
HnswElement HnswGetEntryPoint(Relation index);
HnswElement HnswInitElement(ItemPointer tid, int m, double ml, int maxLevel);
void HnswFreeElement(HnswElement element);

View File

@@ -17,6 +17,7 @@ GetScanItems(IndexScanDesc scan, Datum q)
Relation index = scan->indexRelation;
FmgrInfo *procinfo = so->procinfo;
Oid collation = so->collation;
bool loadVec = scan->xs_want_itup;
List *ep;
List *w;
HnswElement entryPoint = HnswGetEntryPoint(index);
@@ -24,15 +25,15 @@ GetScanItems(IndexScanDesc scan, Datum q)
if (entryPoint == NULL)
return NIL;
ep = list_make1(HnswEntryCandidate(entryPoint, q, index, procinfo, collation, false));
ep = list_make1(HnswEntryCandidate(entryPoint, q, index, procinfo, collation, loadVec));
for (int lc = entryPoint->level; lc >= 1; lc--)
{
w = HnswSearchLayer(q, ep, 1, lc, index, procinfo, collation, false, NULL);
w = HnswSearchLayer(q, ep, 1, lc, index, procinfo, collation, loadVec, NULL);
ep = w;
}
return HnswSearchLayer(q, ep, hnsw_ef_search, 0, index, procinfo, collation, false, NULL);
return HnswSearchLayer(q, ep, hnsw_ef_search, 0, index, procinfo, collation, loadVec, NULL);
}
/*
@@ -83,6 +84,9 @@ hnswbeginscan(Relation index, int nkeys, int norderbys)
scan->opaque = so;
/* OK to always set since cheap */
scan->xs_itupdesc = RelationGetDescr(index);
return scan;
}
@@ -165,7 +169,7 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir)
ItemPointer tid;
BlockNumber indexblkno;
/* Move to next element if no valid heap tids */
/* Move to next element if no valid heap TIDs */
if (list_length(hc->element->heaptids) == 0)
{
so->w = list_delete_last(so->w);
@@ -177,6 +181,15 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir)
hc->element->heaptids = list_delete_last(hc->element->heaptids);
if (scan->xs_want_itup)
{
Datum value = PointerGetDatum(hc->element->vec);
bool isnull = false;
scan->xs_itup = index_form_tuple(scan->xs_itupdesc, &value, &isnull);
scan->xs_itup->t_tid = *tid;
}
MemoryContextSwitchTo(oldCtx);
#if PG_VERSION_NUM >= 120000
@@ -185,6 +198,7 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir)
scan->xs_ctup.t_self = *tid;
#endif
/* Unpin buffer */
if (BufferIsValid(so->buf))
ReleaseBuffer(so->buf);

View File

@@ -543,7 +543,7 @@ AddToVisited(HTAB *v, HnswCandidate * hc, Relation index, bool *found)
* Algorithm 2 from paper
*/
List *
HnswSearchLayer(Datum q, List *ep, int ef, int lc, Relation index, FmgrInfo *procinfo, Oid collation, bool inserting, HnswElement skipElement)
HnswSearchLayer(Datum q, List *ep, int ef, int lc, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec, HnswElement skipElement)
{
ListCell *lc2;
@@ -619,7 +619,7 @@ HnswSearchLayer(Datum q, List *ep, int ef, int lc, Relation index, FmgrInfo *pro
if (index == NULL)
eDistance = GetCandidateDistance(e, q, procinfo, collation);
else
HnswLoadElement(e->element, &eDistance, &q, index, procinfo, collation, inserting);
HnswLoadElement(e->element, &eDistance, &q, index, procinfo, collation, loadVec);
Assert(!e->element->deleted);

View File

@@ -10,8 +10,8 @@
#include "ivfflat.h"
#include "miscadmin.h"
#include "storage/bufmgr.h"
#include "utils/memutils.h"
#include "tcop/tcopprot.h"
#include "utils/memutils.h"
#if PG_VERSION_NUM >= 140000
#include "utils/backend_progress.h"

View File

@@ -343,6 +343,7 @@ ivfflatgettuple(IndexScanDesc scan, ScanDirection dir)
scan->xs_ctup.t_self = *tid;
#endif
/* Unpin buffer */
if (BufferIsValid(so->buf))
ReleaseBuffer(so->buf);

View File

@@ -695,6 +695,8 @@ vector_spherical_distance(PG_FUNCTION_ARGS)
{
Vector *a = PG_GETARG_VECTOR_P(0);
Vector *b = PG_GETARG_VECTOR_P(1);
float *ax = a->x;
float *bx = b->x;
float dp = 0.0;
double distance;
@@ -702,7 +704,7 @@ vector_spherical_distance(PG_FUNCTION_ARGS)
/* Auto-vectorized */
for (int i = 0; i < a->dim; i++)
dp += a->x[i] * b->x[i];
dp += ax[i] * bx[i];
distance = (double) dp;

View File

@@ -106,6 +106,12 @@ SELECT cosine_distance('[1,1]', '[1,1]');
0
(1 row)
SELECT cosine_distance('[1,0]', '[0,2]');
cosine_distance
-----------------
1
(1 row)
SELECT cosine_distance('[1,1]', '[-1,-1]');
cosine_distance
-----------------

View File

@@ -25,6 +25,7 @@ SELECT inner_product('[3e38]', '[3e38]');
SELECT cosine_distance('[1,2]', '[2,4]');
SELECT cosine_distance('[1,2]', '[0,0]');
SELECT cosine_distance('[1,1]', '[1,1]');
SELECT cosine_distance('[1,0]', '[0,2]');
SELECT cosine_distance('[1,1]', '[-1,-1]');
SELECT cosine_distance('[1,2]', '[3]');
SELECT cosine_distance('[1,1]', '[1.1,1.1]');

View File

@@ -19,8 +19,6 @@ sub test_index_replay
# Wait for replica to catch up
my $applname = $node_replica->name;
my $server_version_num = $node_primary->safe_psql("postgres", "SHOW server_version_num");
my $caughtup_query = "SELECT pg_current_wal_lsn() <= replay_lsn FROM pg_stat_replication WHERE application_name = '$applname';";
$node_primary->poll_query_until('postgres', $caughtup_query)
or die "Timed out while waiting for replica 1 to catch up";

View File

@@ -94,7 +94,7 @@ for my $i (0 .. $#operators)
# Test approximate results
if ($operator ne "<#>")
{
# TODO fix test
# TODO Fix test (uniform random vectors all have similar inner product)
test_recall(1, 0.71, $operator);
test_recall(10, 0.95, $operator);
}
@@ -115,7 +115,7 @@ for my $i (0 .. $#operators)
# Test approximate results
if ($operator ne "<#>")
{
# TODO fix test
# TODO Fix test (uniform random vectors all have similar inner product)
test_recall(1, 0.71, $operator);
test_recall(10, 0.95, $operator);
}

View File

@@ -19,8 +19,6 @@ sub test_index_replay
# Wait for replica to catch up
my $applname = $node_replica->name;
my $server_version_num = $node_primary->safe_psql("postgres", "SHOW server_version_num");
my $caughtup_query = "SELECT pg_current_wal_lsn() <= replay_lsn FROM pg_stat_replication WHERE application_name = '$applname';";
$node_primary->poll_query_until('postgres', $caughtup_query)
or die "Timed out while waiting for replica 1 to catch up";

View File

@@ -89,7 +89,7 @@ foreach (@queries)
test_recall(0.20, $limit, "before vacuum");
test_recall(0.95, 100, "before vacuum");
# TODO test concurrent inserts with vacuum
# TODO Test concurrent inserts with vacuum
$node->safe_psql("postgres", "VACUUM tst;");
test_recall(0.95, $limit, "after vacuum");

View File

@@ -0,0 +1,117 @@
use strict;
use warnings;
use PostgresNode;
use TestLib;
use Test::More;
my $node;
my @queries = ();
my @expected;
my $limit = 20;
sub test_recall
{
my ($probes, $min, $operator) = @_;
my $correct = 0;
my $total = 0;
my $explain = $node->safe_psql("postgres", qq(
SET enable_seqscan = off;
SET ivfflat.probes = $probes;
EXPLAIN ANALYZE SELECT i FROM tst 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;
SELECT i FROM tst ORDER BY v $operator '$queries[$i]' LIMIT $limit;
));
my @actual_ids = split("\n", $actual);
my %actual_set = map { $_ => 1 } @actual_ids;
my @expected_ids = split("\n", $expected[$i]);
foreach (@expected_ids)
{
if (exists($actual_set{$_}))
{
$correct++;
}
$total++;
}
}
cmp_ok($correct / $total, ">=", $min, $operator);
}
# Initialize node
$node = get_new_node('node');
$node->init;
$node->start;
# Create table
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
$node->safe_psql("postgres", "CREATE TABLE tst (i serial, v vector(3));");
# 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_ip_ops", "vector_cosine_ops");
for my $i (0 .. $#operators)
{
my $operator = $operators[$i];
my $opclass = $opclasses[$i];
# Add index
$node->safe_psql("postgres", "CREATE INDEX idx ON tst USING ivfflat (v $opclass);");
# Use concurrent inserts
$node->pgbench(
"--no-vacuum --client=10 --transactions=1000",
0,
[qr{actually processed}],
[qr{^$}],
"concurrent INSERTs",
{
"017_ivfflat_insert_recall_$opclass" => "INSERT INTO tst (v) SELECT ARRAY[random(), random(), random()] FROM generate_series(1, 10) i;"
}
);
# Get exact results
@expected = ();
foreach (@queries)
{
my $res = $node->safe_psql("postgres", qq(
SET enable_indexscan = off;
SELECT i FROM tst ORDER BY v $operator '$_' LIMIT $limit;
));
push(@expected, $res);
}
# Test approximate results
if ($operator ne "<#>")
{
# TODO Fix test (uniform random vectors all have similar inner product)
test_recall(1, 0.71, $operator);
test_recall(10, 0.95, $operator);
}
# Account for equal distances
test_recall(100, 0.9925, $operator);
$node->safe_psql("postgres", "DROP INDEX idx;");
$node->safe_psql("postgres", "TRUNCATE tst;");
}
done_testing();