mirror of
https://github.com/pgvector/pgvector.git
synced 2026-07-22 20:15:46 +08:00
Compare commits
7 Commits
hnsw-print
...
normalize_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5bb59d9f6 | ||
|
|
dd609f200b | ||
|
|
9514c152bc | ||
|
|
b391e40765 | ||
|
|
0054a9c40a | ||
|
|
9ed7e63fb7 | ||
|
|
47e361a93d |
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
@@ -78,7 +78,6 @@ jobs:
|
|||||||
nmake /NOLOGO /F Makefile.win uninstall
|
nmake /NOLOGO /F Makefile.win uninstall
|
||||||
shell: cmd
|
shell: cmd
|
||||||
i386:
|
i386:
|
||||||
if: ${{ !startsWith(github.ref_name, 'mac') && !startsWith(github.ref_name, 'windows') }}
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: debian:11
|
image: debian:11
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
- Added HNSW index type
|
- Added HNSW index type
|
||||||
- Added support for parallel index builds
|
- Added support for parallel index builds
|
||||||
- Added `l1_distance` function
|
- Added `l1_distance` function
|
||||||
|
- Added `normalize_l2` function
|
||||||
- Added element-wise multiplication for vectors
|
- Added element-wise multiplication for vectors
|
||||||
- Added `sum` aggregate
|
- Added `sum` aggregate
|
||||||
- Improved performance of distance functions
|
- Improved performance of distance functions
|
||||||
|
|||||||
1
Makefile
1
Makefile
@@ -4,7 +4,6 @@ EXTVERSION = 0.4.4
|
|||||||
MODULE_big = vector
|
MODULE_big = vector
|
||||||
DATA = $(wildcard sql/*--*.sql)
|
DATA = $(wildcard sql/*--*.sql)
|
||||||
OBJS = src/hnsw.o src/hnswbuild.o src/hnswinsert.o src/hnswscan.o src/hnswutils.o src/hnswvacuum.o src/ivfbuild.o src/ivfflat.o src/ivfinsert.o src/ivfkmeans.o src/ivfscan.o src/ivfutils.o src/ivfvacuum.o src/vector.o
|
OBJS = src/hnsw.o src/hnswbuild.o src/hnswinsert.o src/hnswscan.o src/hnswutils.o src/hnswvacuum.o src/ivfbuild.o src/ivfflat.o src/ivfinsert.o src/ivfkmeans.o src/ivfscan.o src/ivfutils.o src/ivfvacuum.o src/vector.o
|
||||||
HEADERS = src/vector.h
|
|
||||||
|
|
||||||
TESTS = $(wildcard test/sql/*.sql)
|
TESTS = $(wildcard test/sql/*.sql)
|
||||||
REGRESS = $(patsubst test/sql/%.sql,%,$(TESTS))
|
REGRESS = $(patsubst test/sql/%.sql,%,$(TESTS))
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ EXTENSION = vector
|
|||||||
EXTVERSION = 0.4.4
|
EXTVERSION = 0.4.4
|
||||||
|
|
||||||
OBJS = 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\vector.obj
|
OBJS = 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\vector.obj
|
||||||
HEADERS = src\vector.h
|
|
||||||
|
|
||||||
REGRESS = btree cast copy functions input ivfflat_cosine ivfflat_ip ivfflat_l2 ivfflat_options ivfflat_unlogged
|
REGRESS = btree cast copy functions input ivfflat_cosine ivfflat_ip ivfflat_l2 ivfflat_options ivfflat_unlogged
|
||||||
REGRESS_OPTS = --inputdir=test --load-extension=vector
|
REGRESS_OPTS = --inputdir=test --load-extension=vector
|
||||||
@@ -55,8 +54,6 @@ install:
|
|||||||
copy $(SHLIB) "$(PKGLIBDIR)"
|
copy $(SHLIB) "$(PKGLIBDIR)"
|
||||||
copy $(EXTENSION).control "$(SHAREDIR)\extension"
|
copy $(EXTENSION).control "$(SHAREDIR)\extension"
|
||||||
copy sql\$(EXTENSION)--*.sql "$(SHAREDIR)\extension"
|
copy sql\$(EXTENSION)--*.sql "$(SHAREDIR)\extension"
|
||||||
mkdir "$(INCLUDEDIR_SERVER)\extension\$(EXTENSION)"
|
|
||||||
copy $(HEADERS) "$(INCLUDEDIR_SERVER)\extension\$(EXTENSION)"
|
|
||||||
|
|
||||||
installcheck:
|
installcheck:
|
||||||
"$(BINDIR)\pg_regress" --bindir="$(BINDIR)" $(REGRESS_OPTS) $(REGRESS)
|
"$(BINDIR)\pg_regress" --bindir="$(BINDIR)" $(REGRESS_OPTS) $(REGRESS)
|
||||||
@@ -64,9 +61,7 @@ installcheck:
|
|||||||
uninstall:
|
uninstall:
|
||||||
del /f "$(PKGLIBDIR)\$(SHLIB)"
|
del /f "$(PKGLIBDIR)\$(SHLIB)"
|
||||||
del /f "$(SHAREDIR)\extension\$(EXTENSION).control"
|
del /f "$(SHAREDIR)\extension\$(EXTENSION).control"
|
||||||
del /f "$(SHAREDIR)\extension\$(EXTENSION)--*.sql"
|
del /f "$(SHAREDIR)\extension\vector--*.sql"
|
||||||
del /f "$(INCLUDEDIR_SERVER)\extension\$(EXTENSION)\*.h"
|
|
||||||
rmdir "$(INCLUDEDIR_SERVER)\extension\$(EXTENSION)"
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
del /f $(SHLIB) $(EXTENSION).lib $(EXTENSION).exp
|
del /f $(SHLIB) $(EXTENSION).lib $(EXTENSION).exp
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Open-source vector similarity search for Postgres
|
Open-source vector similarity search for Postgres
|
||||||
|
|
||||||
Store your vectors with the rest of your data. Supports:
|
Store all of your application data in one place. Supports:
|
||||||
|
|
||||||
- exact and approximate nearest neighbor search
|
- exact and approximate nearest neighbor search
|
||||||
- L2 distance, inner product, and cosine distance
|
- L2 distance, inner product, and cosine distance
|
||||||
@@ -392,6 +392,7 @@ cosine_distance(vector, vector) → double precision | cosine distance
|
|||||||
inner_product(vector, vector) → double precision | inner product
|
inner_product(vector, vector) → double precision | inner product
|
||||||
l2_distance(vector, vector) → double precision | Euclidean distance
|
l2_distance(vector, vector) → double precision | Euclidean distance
|
||||||
l1_distance(vector, vector) → double precision | taxicab distance [unreleased]
|
l1_distance(vector, vector) → double precision | taxicab distance [unreleased]
|
||||||
|
normalize_l2(vector) → vector | normalize with Euclidean norm [unreleased]
|
||||||
vector_dims(vector) → integer | number of dimensions
|
vector_dims(vector) → integer | number of dimensions
|
||||||
vector_norm(vector) → double precision | Euclidean norm
|
vector_norm(vector) → double precision | Euclidean norm
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
CREATE FUNCTION l1_distance(vector, vector) RETURNS float8
|
CREATE FUNCTION l1_distance(vector, vector) RETURNS float8
|
||||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION normalize_l2(vector) RETURNS vector
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
CREATE FUNCTION vector_mul(vector, vector) RETURNS vector
|
CREATE FUNCTION vector_mul(vector, vector) RETURNS vector
|
||||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ CREATE FUNCTION vector_dims(vector) RETURNS integer
|
|||||||
CREATE FUNCTION vector_norm(vector) RETURNS float8
|
CREATE FUNCTION vector_norm(vector) RETURNS float8
|
||||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
|
CREATE FUNCTION normalize_l2(vector) RETURNS vector
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
CREATE FUNCTION vector_add(vector, vector) RETURNS vector
|
CREATE FUNCTION vector_add(vector, vector) RETURNS vector
|
||||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
|
||||||
@@ -265,15 +268,15 @@ CREATE OPERATOR CLASS vector_ip_ops
|
|||||||
OPERATOR 1 <#> (vector, vector) FOR ORDER BY float_ops,
|
OPERATOR 1 <#> (vector, vector) FOR ORDER BY float_ops,
|
||||||
FUNCTION 1 vector_negative_inner_product(vector, vector),
|
FUNCTION 1 vector_negative_inner_product(vector, vector),
|
||||||
FUNCTION 3 vector_spherical_distance(vector, vector),
|
FUNCTION 3 vector_spherical_distance(vector, vector),
|
||||||
FUNCTION 4 vector_norm(vector);
|
FUNCTION 6 normalize_l2(vector);
|
||||||
|
|
||||||
CREATE OPERATOR CLASS vector_cosine_ops
|
CREATE OPERATOR CLASS vector_cosine_ops
|
||||||
FOR TYPE vector USING ivfflat AS
|
FOR TYPE vector USING ivfflat AS
|
||||||
OPERATOR 1 <=> (vector, vector) FOR ORDER BY float_ops,
|
OPERATOR 1 <=> (vector, vector) FOR ORDER BY float_ops,
|
||||||
FUNCTION 1 vector_negative_inner_product(vector, vector),
|
FUNCTION 1 vector_negative_inner_product(vector, vector),
|
||||||
FUNCTION 2 vector_norm(vector),
|
|
||||||
FUNCTION 3 vector_spherical_distance(vector, vector),
|
FUNCTION 3 vector_spherical_distance(vector, vector),
|
||||||
FUNCTION 4 vector_norm(vector);
|
FUNCTION 5 normalize_l2(vector),
|
||||||
|
FUNCTION 6 normalize_l2(vector);
|
||||||
|
|
||||||
CREATE OPERATOR CLASS vector_l2_ops
|
CREATE OPERATOR CLASS vector_l2_ops
|
||||||
FOR TYPE vector USING hnsw AS
|
FOR TYPE vector USING hnsw AS
|
||||||
@@ -289,4 +292,4 @@ CREATE OPERATOR CLASS vector_cosine_ops
|
|||||||
FOR TYPE vector USING hnsw AS
|
FOR TYPE vector USING hnsw AS
|
||||||
OPERATOR 1 <=> (vector, vector) FOR ORDER BY float_ops,
|
OPERATOR 1 <=> (vector, vector) FOR ORDER BY float_ops,
|
||||||
FUNCTION 1 vector_negative_inner_product(vector, vector),
|
FUNCTION 1 vector_negative_inner_product(vector, vector),
|
||||||
FUNCTION 2 vector_norm(vector);
|
FUNCTION 3 normalize_l2(vector);
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ HnswInit(void)
|
|||||||
);
|
);
|
||||||
|
|
||||||
DefineCustomIntVariable("hnsw.ef_search", "Sets the size of the dynamic candidate list for search",
|
DefineCustomIntVariable("hnsw.ef_search", "Sets the size of the dynamic candidate list for search",
|
||||||
"Valid range is 1..1000.", &hnsw_ef_search,
|
"Valid range is 10..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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ hnswhandler(PG_FUNCTION_ARGS)
|
|||||||
IndexAmRoutine *amroutine = makeNode(IndexAmRoutine);
|
IndexAmRoutine *amroutine = makeNode(IndexAmRoutine);
|
||||||
|
|
||||||
amroutine->amstrategies = 0;
|
amroutine->amstrategies = 0;
|
||||||
amroutine->amsupport = 2;
|
amroutine->amsupport = 3;
|
||||||
#if PG_VERSION_NUM >= 130000
|
#if PG_VERSION_NUM >= 130000
|
||||||
amroutine->amoptsprocnum = 0;
|
amroutine->amoptsprocnum = 0;
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
50
src/hnsw.h
50
src/hnsw.h
@@ -19,40 +19,32 @@
|
|||||||
/* Support functions */
|
/* Support functions */
|
||||||
#define HNSW_DISTANCE_PROC 1
|
#define HNSW_DISTANCE_PROC 1
|
||||||
#define HNSW_NORM_PROC 2
|
#define HNSW_NORM_PROC 2
|
||||||
|
#define HNSW_NORMALIZE_PROC 3
|
||||||
|
|
||||||
#define HNSW_VERSION 1
|
#define HNSW_VERSION 1
|
||||||
#define HNSW_MAGIC_NUMBER 0xA953A953
|
#define HNSW_MAGIC_NUMBER 0xA953A953
|
||||||
#define HNSW_PAGE_ID 0xFF90
|
#define HNSW_PAGE_ID 0xFF85
|
||||||
|
|
||||||
/* Preserved page numbers */
|
/* Preserved page numbers */
|
||||||
#define HNSW_METAPAGE_BLKNO 0
|
#define HNSW_METAPAGE_BLKNO 0
|
||||||
#define HNSW_HEAD_BLKNO 1 /* first element page */
|
#define HNSW_HEAD_BLKNO 1 /* first element page */
|
||||||
|
|
||||||
/* Must correspond to page numbers since page lock is used */
|
|
||||||
#define HNSW_UPDATE_LOCK 0
|
|
||||||
#define HNSW_SCAN_LOCK 1
|
|
||||||
|
|
||||||
/* HNSW parameters */
|
|
||||||
#define HNSW_DEFAULT_M 16
|
#define HNSW_DEFAULT_M 16
|
||||||
#define HNSW_MIN_M 2
|
#define HNSW_MIN_M 4
|
||||||
#define HNSW_MAX_M 100
|
#define HNSW_MAX_M 100
|
||||||
#define HNSW_DEFAULT_EF_CONSTRUCTION 40
|
#define HNSW_DEFAULT_EF_CONSTRUCTION 40
|
||||||
#define HNSW_MIN_EF_CONSTRUCTION 4
|
#define HNSW_MIN_EF_CONSTRUCTION 10
|
||||||
#define HNSW_MAX_EF_CONSTRUCTION 1000
|
#define HNSW_MAX_EF_CONSTRUCTION 1000
|
||||||
#define HNSW_DEFAULT_EF_SEARCH 40
|
#define HNSW_DEFAULT_EF_SEARCH 40
|
||||||
#define HNSW_MIN_EF_SEARCH 1
|
#define HNSW_MIN_EF_SEARCH 10
|
||||||
#define HNSW_MAX_EF_SEARCH 1000
|
#define HNSW_MAX_EF_SEARCH 1000
|
||||||
|
|
||||||
/* Tuple types */
|
|
||||||
#define HNSW_ELEMENT_TUPLE_TYPE 1
|
#define HNSW_ELEMENT_TUPLE_TYPE 1
|
||||||
#define HNSW_NEIGHBOR_TUPLE_TYPE 2
|
#define HNSW_NEIGHBOR_TUPLE_TYPE 2
|
||||||
|
|
||||||
/* Make graph robust against non-HOT updates */
|
/* Make graph robust against non-HOT updates */
|
||||||
#define HNSW_HEAPTIDS 10
|
#define HNSW_HEAPTIDS 10
|
||||||
|
|
||||||
#define HNSW_UPDATE_ENTRY_GREATER 1
|
|
||||||
#define HNSW_UPDATE_ENTRY_ALWAYS 2
|
|
||||||
|
|
||||||
/* Build phases */
|
/* Build phases */
|
||||||
/* PROGRESS_CREATEIDX_SUBPHASE_INITIALIZE is 1 */
|
/* PROGRESS_CREATEIDX_SUBPHASE_INITIALIZE is 1 */
|
||||||
#define PROGRESS_HNSW_PHASE_LOAD 2
|
#define PROGRESS_HNSW_PHASE_LOAD 2
|
||||||
@@ -77,13 +69,10 @@
|
|||||||
#define HnswIsElementTuple(tup) ((tup)->type == HNSW_ELEMENT_TUPLE_TYPE)
|
#define HnswIsElementTuple(tup) ((tup)->type == HNSW_ELEMENT_TUPLE_TYPE)
|
||||||
#define HnswIsNeighborTuple(tup) ((tup)->type == HNSW_NEIGHBOR_TUPLE_TYPE)
|
#define HnswIsNeighborTuple(tup) ((tup)->type == HNSW_NEIGHBOR_TUPLE_TYPE)
|
||||||
|
|
||||||
/* 2 * M connections for ground layer */
|
#define HnswGetLayerM(m, layer) (layer == 0 ? m * 2 : m)
|
||||||
#define HnswGetLayerM(m, layer) (layer == 0 ? (m) * 2 : (m))
|
|
||||||
|
|
||||||
/* Optimal ML from paper */
|
|
||||||
#define HnswGetMl(m) (1 / log(m))
|
#define HnswGetMl(m) (1 / log(m))
|
||||||
|
|
||||||
/* Ensure fits on page and in uint8 */
|
/* Ensure fits in uint8 */
|
||||||
#define HnswGetMaxLevel(m) Min(((BLCKSZ - MAXALIGN(SizeOfPageHeaderData) - MAXALIGN(sizeof(HnswPageOpaqueData)) - offsetof(HnswNeighborTupleData, indextids) - sizeof(ItemIdData)) / (sizeof(ItemPointerData)) / m) - 2, 255)
|
#define HnswGetMaxLevel(m) Min(((BLCKSZ - MAXALIGN(SizeOfPageHeaderData) - MAXALIGN(sizeof(HnswPageOpaqueData)) - offsetof(HnswNeighborTupleData, indextids) - sizeof(ItemIdData)) / (sizeof(ItemPointerData)) / m) - 2, 255)
|
||||||
|
|
||||||
/* Variables */
|
/* Variables */
|
||||||
@@ -118,6 +107,13 @@ typedef struct HnswNeighborArray
|
|||||||
HnswCandidate *items;
|
HnswCandidate *items;
|
||||||
} HnswNeighborArray;
|
} HnswNeighborArray;
|
||||||
|
|
||||||
|
typedef struct HnswUpdate
|
||||||
|
{
|
||||||
|
HnswCandidate hc;
|
||||||
|
int level;
|
||||||
|
int index;
|
||||||
|
} HnswUpdate;
|
||||||
|
|
||||||
typedef struct HnswPairingHeapNode
|
typedef struct HnswPairingHeapNode
|
||||||
{
|
{
|
||||||
pairingheap_node ph_node;
|
pairingheap_node ph_node;
|
||||||
@@ -152,6 +148,7 @@ typedef struct HnswBuildState
|
|||||||
/* Support functions */
|
/* Support functions */
|
||||||
FmgrInfo *procinfo;
|
FmgrInfo *procinfo;
|
||||||
FmgrInfo *normprocinfo;
|
FmgrInfo *normprocinfo;
|
||||||
|
FmgrInfo *normalizeprocinfo;
|
||||||
Oid collation;
|
Oid collation;
|
||||||
|
|
||||||
/* Variables */
|
/* Variables */
|
||||||
@@ -225,6 +222,7 @@ typedef struct HnswScanOpaqueData
|
|||||||
/* Support functions */
|
/* Support functions */
|
||||||
FmgrInfo *procinfo;
|
FmgrInfo *procinfo;
|
||||||
FmgrInfo *normprocinfo;
|
FmgrInfo *normprocinfo;
|
||||||
|
FmgrInfo *normalizeprocinfo;
|
||||||
Oid collation;
|
Oid collation;
|
||||||
} HnswScanOpaqueData;
|
} HnswScanOpaqueData;
|
||||||
|
|
||||||
@@ -260,31 +258,25 @@ typedef struct HnswVacuumState
|
|||||||
int HnswGetM(Relation index);
|
int HnswGetM(Relation index);
|
||||||
int HnswGetEfConstruction(Relation index);
|
int HnswGetEfConstruction(Relation index);
|
||||||
FmgrInfo *HnswOptionalProcInfo(Relation rel, uint16 procnum);
|
FmgrInfo *HnswOptionalProcInfo(Relation rel, uint16 procnum);
|
||||||
bool HnswNormValue(FmgrInfo *procinfo, Oid collation, Datum *value, Vector * result);
|
void HnswNormValue(FmgrInfo *procinfo, FmgrInfo *normalizeprocinfo, Oid collation, Datum *value, Vector * result);
|
||||||
void HnswCommitBuffer(Buffer buf, GenericXLogState *state);
|
void HnswCommitBuffer(Buffer buf, GenericXLogState *state);
|
||||||
Buffer HnswNewBuffer(Relation index, ForkNumber forkNum);
|
Buffer HnswNewBuffer(Relation index, ForkNumber forkNum);
|
||||||
void HnswInitPage(Buffer buf, Page page);
|
void HnswInitPage(Buffer buf, Page page);
|
||||||
void HnswInitRegisterPage(Relation index, Buffer *buf, Page *page, GenericXLogState **state);
|
void HnswInitRegisterPage(Relation index, Buffer *buf, Page *page, GenericXLogState **state);
|
||||||
void HnswInit(void);
|
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 inserting, BlockNumber *skipPage, OffsetNumber *skipOffno);
|
||||||
HnswElement HnswGetEntryPoint(Relation index);
|
HnswElement HnswGetEntryPoint(Relation index);
|
||||||
HnswElement HnswInitElement(ItemPointer tid, int m, double ml, int maxLevel);
|
HnswElement HnswInitElement(ItemPointer tid, int m, double ml, int maxLevel);
|
||||||
void HnswFreeElement(HnswElement element);
|
void HnswFreeElement(HnswElement element);
|
||||||
HnswElement HnswInitElementFromBlock(BlockNumber blkno, OffsetNumber offno);
|
HnswElement HnswInsertElement(HnswElement element, HnswElement entryPoint, Relation index, FmgrInfo *procinfo, Oid collation, int m, int efConstruction, List **updates, bool vacuuming);
|
||||||
void HnswInsertElement(HnswElement element, HnswElement entryPoint, Relation index, FmgrInfo *procinfo, Oid collation, int m, int efConstruction, bool existing);
|
HnswCandidate *HnswEntryCandidate(HnswElement em, Datum q, Relation rel, FmgrInfo *procinfo, Oid collation, bool loadvec);
|
||||||
HnswElement HnswFindDuplicate(HnswElement e);
|
void HnswUpdateMetaPage(Relation index, bool updateEntry, HnswElement entryPoint, BlockNumber insertPage, ForkNumber forkNum);
|
||||||
HnswCandidate *HnswEntryCandidate(HnswElement em, Datum q, Relation rel, FmgrInfo *procinfo, Oid collation, bool loadVec);
|
|
||||||
void HnswUpdateMetaPage(Relation index, int updateEntry, HnswElement entryPoint, BlockNumber insertPage, ForkNumber forkNum);
|
|
||||||
void HnswSetNeighborTuple(HnswNeighborTuple ntup, HnswElement e, int m);
|
void HnswSetNeighborTuple(HnswNeighborTuple ntup, HnswElement e, int m);
|
||||||
void HnswAddHeapTid(HnswElement element, ItemPointer heaptid);
|
void HnswAddHeapTid(HnswElement element, ItemPointer heaptid);
|
||||||
void HnswInitNeighbors(HnswElement element, int m);
|
void HnswInitNeighbors(HnswElement element, int m);
|
||||||
bool HnswInsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heap_tid, Relation heapRel);
|
bool HnswInsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heap_tid, Relation heapRel);
|
||||||
void HnswUpdateNeighborPages(Relation index, FmgrInfo *procinfo, Oid collation, HnswElement e, int m, bool checkExisting);
|
|
||||||
void HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool loadHeaptids, bool loadVec);
|
|
||||||
void HnswLoadElement(HnswElement element, float *distance, Datum *q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec);
|
void HnswLoadElement(HnswElement element, float *distance, Datum *q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec);
|
||||||
void HnswSetElementTuple(HnswElementTuple etup, HnswElement element);
|
void HnswSetElementTuple(HnswElementTuple etup, HnswElement element);
|
||||||
void HnswUpdateConnection(HnswElement element, HnswCandidate * hc, int m, int lc, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation);
|
|
||||||
void HnswLoadNeighbors(HnswElement element, Relation index);
|
|
||||||
|
|
||||||
/* Index access methods */
|
/* Index access methods */
|
||||||
IndexBuildResult *hnswbuild(Relation heap, Relation index, IndexInfo *indexInfo);
|
IndexBuildResult *hnswbuild(Relation heap, Relation index, IndexInfo *indexInfo);
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ CreateMetaPage(HnswBuildState * buildstate)
|
|||||||
metap->efConstruction = buildstate->efConstruction;
|
metap->efConstruction = buildstate->efConstruction;
|
||||||
metap->entryBlkno = InvalidBlockNumber;
|
metap->entryBlkno = InvalidBlockNumber;
|
||||||
metap->entryOffno = InvalidOffsetNumber;
|
metap->entryOffno = InvalidOffsetNumber;
|
||||||
metap->entryLevel = -1;
|
|
||||||
metap->insertPage = InvalidBlockNumber;
|
metap->insertPage = InvalidBlockNumber;
|
||||||
((PageHeader) page)->pd_lower =
|
((PageHeader) page)->pd_lower =
|
||||||
((char *) metap + sizeof(HnswMetaPageData)) - (char *) page;
|
((char *) metap + sizeof(HnswMetaPageData)) - (char *) page;
|
||||||
@@ -183,7 +182,7 @@ CreateElementPages(HnswBuildState * buildstate)
|
|||||||
GenericXLogFinish(state);
|
GenericXLogFinish(state);
|
||||||
UnlockReleaseBuffer(buf);
|
UnlockReleaseBuffer(buf);
|
||||||
|
|
||||||
HnswUpdateMetaPage(index, HNSW_UPDATE_ENTRY_ALWAYS, buildstate->entryPoint, insertPage, forkNum);
|
HnswUpdateMetaPage(index, true, buildstate->entryPoint, insertPage, forkNum);
|
||||||
|
|
||||||
pfree(etup);
|
pfree(etup);
|
||||||
pfree(ntup);
|
pfree(ntup);
|
||||||
@@ -279,33 +278,13 @@ InsertTuple(Relation index, Datum *values, HnswElement element, HnswBuildState *
|
|||||||
Datum value = PointerGetDatum(PG_DETOAST_DATUM(values[0]));
|
Datum value = PointerGetDatum(PG_DETOAST_DATUM(values[0]));
|
||||||
|
|
||||||
/* Normalize if needed */
|
/* Normalize if needed */
|
||||||
if (buildstate->normprocinfo != NULL)
|
HnswNormValue(buildstate->normprocinfo, buildstate->normalizeprocinfo, collation, &value, buildstate->normvec);
|
||||||
{
|
|
||||||
if (!HnswNormValue(buildstate->normprocinfo, collation, &value, buildstate->normvec))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Copy value to element so accessible outside of memory context */
|
/* Copy value to element so accessible outside of memory context */
|
||||||
memcpy(element->vec, DatumGetVector(value), VECTOR_SIZE(buildstate->dimensions));
|
memcpy(element->vec, DatumGetVector(value), VECTOR_SIZE(buildstate->dimensions));
|
||||||
|
|
||||||
/* Insert element in graph */
|
/* Insert element in graph */
|
||||||
HnswInsertElement(element, entryPoint, NULL, procinfo, collation, m, efConstruction, false);
|
*dup = HnswInsertElement(element, entryPoint, NULL, procinfo, collation, m, efConstruction, NULL, false);
|
||||||
|
|
||||||
/* Look for duplicate */
|
|
||||||
*dup = HnswFindDuplicate(element);
|
|
||||||
|
|
||||||
/* Update neighbors if needed */
|
|
||||||
if (*dup == NULL)
|
|
||||||
{
|
|
||||||
for (int lc = element->level; lc >= 0; lc--)
|
|
||||||
{
|
|
||||||
int lm = HnswGetLayerM(m, lc);
|
|
||||||
HnswNeighborArray *neighbors = &element->neighbors[lc];
|
|
||||||
|
|
||||||
for (int i = 0; i < neighbors->length; i++)
|
|
||||||
HnswUpdateConnection(element, &neighbors->items[i], lm, lc, NULL, NULL, procinfo, collation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Update entry point if needed */
|
/* Update entry point if needed */
|
||||||
if (*dup == NULL && (entryPoint == NULL || element->level > entryPoint->level))
|
if (*dup == NULL && (entryPoint == NULL || element->level > entryPoint->level))
|
||||||
@@ -424,15 +403,13 @@ InitBuildState(HnswBuildState * buildstate, Relation heap, Relation index, Index
|
|||||||
if (buildstate->dimensions > HNSW_MAX_DIM)
|
if (buildstate->dimensions > HNSW_MAX_DIM)
|
||||||
elog(ERROR, "column cannot have more than %d dimensions for hnsw index", HNSW_MAX_DIM);
|
elog(ERROR, "column cannot have more than %d dimensions for hnsw index", HNSW_MAX_DIM);
|
||||||
|
|
||||||
if (buildstate->efConstruction < 2 * buildstate->m)
|
|
||||||
elog(ERROR, "ef_construction must be greater than or equal to 2 * m");
|
|
||||||
|
|
||||||
buildstate->reltuples = 0;
|
buildstate->reltuples = 0;
|
||||||
buildstate->indtuples = 0;
|
buildstate->indtuples = 0;
|
||||||
|
|
||||||
/* Get support functions */
|
/* Get support functions */
|
||||||
buildstate->procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC);
|
buildstate->procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC);
|
||||||
buildstate->normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC);
|
buildstate->normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC);
|
||||||
|
buildstate->normalizeprocinfo = HnswOptionalProcInfo(index, HNSW_NORMALIZE_PROC);
|
||||||
buildstate->collation = index->rd_indcollation[0];
|
buildstate->collation = index->rd_indcollation[0];
|
||||||
|
|
||||||
buildstate->elements = NIL;
|
buildstate->elements = NIL;
|
||||||
|
|||||||
272
src/hnswinsert.c
272
src/hnswinsert.c
@@ -34,7 +34,7 @@ GetInsertPage(Relation index)
|
|||||||
* Check for a free offset
|
* Check for a free offset
|
||||||
*/
|
*/
|
||||||
static bool
|
static bool
|
||||||
HnswFreeOffset(Relation index, Buffer buf, Page page, HnswElement element, Size ntupSize, Buffer *nbuf, Page *npage, OffsetNumber *freeOffno, OffsetNumber *freeNeighborOffno, BlockNumber *newInsertPage)
|
HnswFreeOffset(Relation index, Buffer buf, Page page, HnswElement element, Size ntupSize, Buffer *nbuf, Page *npage, OffsetNumber *freeOffno, OffsetNumber *freeNeighborOffno, BlockNumber *firstFreePage)
|
||||||
{
|
{
|
||||||
OffsetNumber offno;
|
OffsetNumber offno;
|
||||||
OffsetNumber maxoffno = PageGetMaxOffsetNumber(page);
|
OffsetNumber maxoffno = PageGetMaxOffsetNumber(page);
|
||||||
@@ -49,15 +49,14 @@ HnswFreeOffset(Relation index, Buffer buf, Page page, HnswElement element, Size
|
|||||||
|
|
||||||
if (etup->deleted)
|
if (etup->deleted)
|
||||||
{
|
{
|
||||||
BlockNumber elementPage = BufferGetBlockNumber(buf);
|
|
||||||
BlockNumber neighborPage = ItemPointerGetBlockNumber(&etup->neighbortid);
|
BlockNumber neighborPage = ItemPointerGetBlockNumber(&etup->neighbortid);
|
||||||
OffsetNumber neighborOffno = ItemPointerGetOffsetNumber(&etup->neighbortid);
|
OffsetNumber neighborOffno = ItemPointerGetOffsetNumber(&etup->neighbortid);
|
||||||
ItemId itemid;
|
ItemId itemid;
|
||||||
|
|
||||||
if (!BlockNumberIsValid(*newInsertPage))
|
if (!BlockNumberIsValid(*firstFreePage))
|
||||||
*newInsertPage = elementPage;
|
*firstFreePage = neighborPage;
|
||||||
|
|
||||||
if (neighborPage == elementPage)
|
if (neighborPage == BufferGetBlockNumber(buf))
|
||||||
{
|
{
|
||||||
*nbuf = buf;
|
*nbuf = buf;
|
||||||
*npage = page;
|
*npage = page;
|
||||||
@@ -111,7 +110,7 @@ HnswInsertAppendPage(Relation index, Buffer *nbuf, Page *npage, GenericXLogState
|
|||||||
* Add to element and neighbor pages
|
* Add to element and neighbor pages
|
||||||
*/
|
*/
|
||||||
static void
|
static void
|
||||||
WriteNewElementPages(Relation index, HnswElement e, int m, BlockNumber insertPage, BlockNumber *updatedInsertPage)
|
WriteNewElementPages(Relation index, HnswElement e, int m)
|
||||||
{
|
{
|
||||||
Buffer buf;
|
Buffer buf;
|
||||||
Page page;
|
Page page;
|
||||||
@@ -119,24 +118,21 @@ WriteNewElementPages(Relation index, HnswElement e, int m, BlockNumber insertPag
|
|||||||
Size etupSize;
|
Size etupSize;
|
||||||
Size ntupSize;
|
Size ntupSize;
|
||||||
Size combinedSize;
|
Size combinedSize;
|
||||||
Size maxSize;
|
|
||||||
Size minCombinedSize;
|
|
||||||
HnswElementTuple etup;
|
HnswElementTuple etup;
|
||||||
BlockNumber currentPage = insertPage;
|
BlockNumber insertPage = GetInsertPage(index);
|
||||||
|
BlockNumber originalInsertPage = insertPage;
|
||||||
int dimensions = e->vec->dim;
|
int dimensions = e->vec->dim;
|
||||||
HnswNeighborTuple ntup;
|
HnswNeighborTuple ntup;
|
||||||
Buffer nbuf;
|
Buffer nbuf;
|
||||||
Page npage;
|
Page npage;
|
||||||
OffsetNumber freeOffno = InvalidOffsetNumber;
|
OffsetNumber freeOffno = InvalidOffsetNumber;
|
||||||
OffsetNumber freeNeighborOffno = InvalidOffsetNumber;
|
OffsetNumber freeNeighborOffno = InvalidOffsetNumber;
|
||||||
BlockNumber newInsertPage = InvalidBlockNumber;
|
BlockNumber firstFreePage = InvalidBlockNumber;
|
||||||
|
|
||||||
/* Calculate sizes */
|
/* Calculate sizes */
|
||||||
etupSize = HNSW_ELEMENT_TUPLE_SIZE(dimensions);
|
etupSize = HNSW_ELEMENT_TUPLE_SIZE(dimensions);
|
||||||
ntupSize = HNSW_NEIGHBOR_TUPLE_SIZE(e->level, m);
|
ntupSize = HNSW_NEIGHBOR_TUPLE_SIZE(e->level, m);
|
||||||
combinedSize = etupSize + ntupSize + sizeof(ItemIdData);
|
combinedSize = etupSize + ntupSize + sizeof(ItemIdData);
|
||||||
maxSize = BLCKSZ - MAXALIGN(SizeOfPageHeaderData) - MAXALIGN(sizeof(HnswPageOpaqueData));
|
|
||||||
minCombinedSize = etupSize + HNSW_NEIGHBOR_TUPLE_SIZE(0, m) + sizeof(ItemIdData);
|
|
||||||
|
|
||||||
/* Prepare element tuple */
|
/* Prepare element tuple */
|
||||||
etup = palloc0(etupSize);
|
etup = palloc0(etupSize);
|
||||||
@@ -146,22 +142,16 @@ WriteNewElementPages(Relation index, HnswElement e, int m, BlockNumber insertPag
|
|||||||
ntup = palloc0(ntupSize);
|
ntup = palloc0(ntupSize);
|
||||||
HnswSetNeighborTuple(ntup, e, m);
|
HnswSetNeighborTuple(ntup, e, m);
|
||||||
|
|
||||||
/* Find a page (or two if needed) to insert the tuples */
|
/* Find a page to insert the item */
|
||||||
for (;;)
|
for (;;)
|
||||||
{
|
{
|
||||||
buf = ReadBuffer(index, currentPage);
|
buf = ReadBuffer(index, insertPage);
|
||||||
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
|
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
|
||||||
|
|
||||||
state = GenericXLogStart(index);
|
state = GenericXLogStart(index);
|
||||||
page = GenericXLogRegisterBuffer(state, buf, 0);
|
page = GenericXLogRegisterBuffer(state, buf, 0);
|
||||||
|
|
||||||
/* Keep track of first page where element at level 0 can fit */
|
/* Space for both */
|
||||||
if (!BlockNumberIsValid(newInsertPage) && PageGetFreeSpace(page) >= minCombinedSize)
|
|
||||||
newInsertPage = currentPage;
|
|
||||||
|
|
||||||
/* First, try the fastest path */
|
|
||||||
/* Space for both tuples on the current page */
|
|
||||||
/* This can split existing tuples in rare cases */
|
|
||||||
if (PageGetFreeSpace(page) >= combinedSize)
|
if (PageGetFreeSpace(page) >= combinedSize)
|
||||||
{
|
{
|
||||||
nbuf = buf;
|
nbuf = buf;
|
||||||
@@ -169,8 +159,15 @@ WriteNewElementPages(Relation index, HnswElement e, int m, BlockNumber insertPag
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Next, try space from a deleted element */
|
/* Space for element but not neighbors and last page */
|
||||||
if (HnswFreeOffset(index, buf, page, e, ntupSize, &nbuf, &npage, &freeOffno, &freeNeighborOffno, &newInsertPage))
|
if (PageGetFreeSpace(page) >= etupSize && !BlockNumberIsValid(HnswPageGetOpaque(page)->nextblkno))
|
||||||
|
{
|
||||||
|
HnswInsertAppendPage(index, &nbuf, &npage, state, page);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Space from deleted item */
|
||||||
|
if (HnswFreeOffset(index, buf, page, e, ntupSize, &nbuf, &npage, &freeOffno, &freeNeighborOffno, &firstFreePage))
|
||||||
{
|
{
|
||||||
if (nbuf != buf)
|
if (nbuf != buf)
|
||||||
npage = GenericXLogRegisterBuffer(state, nbuf, 0);
|
npage = GenericXLogRegisterBuffer(state, nbuf, 0);
|
||||||
@@ -178,17 +175,9 @@ WriteNewElementPages(Relation index, HnswElement e, int m, BlockNumber insertPag
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Finally, try space for element only if last page */
|
insertPage = HnswPageGetOpaque(page)->nextblkno;
|
||||||
/* Skip if both tuples can fit on the same page */
|
|
||||||
if (combinedSize > maxSize && PageGetFreeSpace(page) >= etupSize && !BlockNumberIsValid(HnswPageGetOpaque(page)->nextblkno))
|
|
||||||
{
|
|
||||||
HnswInsertAppendPage(index, &nbuf, &npage, state, page);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPage = HnswPageGetOpaque(page)->nextblkno;
|
if (BlockNumberIsValid(insertPage))
|
||||||
|
|
||||||
if (BlockNumberIsValid(currentPage))
|
|
||||||
{
|
{
|
||||||
/* Move to next page */
|
/* Move to next page */
|
||||||
GenericXLogAbort(state);
|
GenericXLogAbort(state);
|
||||||
@@ -230,10 +219,7 @@ WriteNewElementPages(Relation index, HnswElement e, int m, BlockNumber insertPag
|
|||||||
e->blkno = BufferGetBlockNumber(buf);
|
e->blkno = BufferGetBlockNumber(buf);
|
||||||
e->neighborPage = BufferGetBlockNumber(nbuf);
|
e->neighborPage = BufferGetBlockNumber(nbuf);
|
||||||
|
|
||||||
/* Added tuple to new page if newInsertPage is not set */
|
insertPage = e->neighborPage;
|
||||||
/* So can set to neighbor page instead of element page */
|
|
||||||
if (!BlockNumberIsValid(newInsertPage))
|
|
||||||
newInsertPage = e->neighborPage;
|
|
||||||
|
|
||||||
if (OffsetNumberIsValid(freeOffno))
|
if (OffsetNumberIsValid(freeOffno))
|
||||||
{
|
{
|
||||||
@@ -279,126 +265,75 @@ WriteNewElementPages(Relation index, HnswElement e, int m, BlockNumber insertPag
|
|||||||
UnlockReleaseBuffer(nbuf);
|
UnlockReleaseBuffer(nbuf);
|
||||||
|
|
||||||
/* Update the insert page */
|
/* Update the insert page */
|
||||||
if (BlockNumberIsValid(newInsertPage) && newInsertPage != insertPage)
|
if (insertPage != originalInsertPage && (!OffsetNumberIsValid(freeOffno) || firstFreePage == insertPage))
|
||||||
*updatedInsertPage = newInsertPage;
|
HnswUpdateMetaPage(index, false, NULL, insertPage, MAIN_FORKNUM);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Check if connection already exists
|
* Calculate index for update
|
||||||
*/
|
*/
|
||||||
static bool
|
static int
|
||||||
ConnectionExists(HnswElement e, HnswNeighborTuple ntup, int startIdx, int lm)
|
HnswGetIndex(HnswUpdate * update, int m)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < lm; i++)
|
return (update->hc.element->level - update->level) * m + update->index;
|
||||||
{
|
|
||||||
ItemPointer indextid = &ntup->indextids[startIdx + i];
|
|
||||||
|
|
||||||
if (!ItemPointerIsValid(indextid))
|
|
||||||
break;
|
|
||||||
|
|
||||||
if (ItemPointerGetBlockNumber(indextid) == e->blkno && ItemPointerGetOffsetNumber(indextid) == e->offno)
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Update neighbors
|
* Update neighbors
|
||||||
*/
|
*/
|
||||||
void
|
static void
|
||||||
HnswUpdateNeighborPages(Relation index, FmgrInfo *procinfo, Oid collation, HnswElement e, int m, bool checkExisting)
|
UpdateNeighborPages(Relation index, HnswElement e, int m, List *updates)
|
||||||
{
|
{
|
||||||
for (int lc = e->level; lc >= 0; lc--)
|
ListCell *lc;
|
||||||
|
|
||||||
|
/* Could update multiple at once for same element */
|
||||||
|
/* but should only happen a low percent of time, so keep simple for now */
|
||||||
|
foreach(lc, updates)
|
||||||
{
|
{
|
||||||
int lm = HnswGetLayerM(m, lc);
|
Buffer buf;
|
||||||
HnswNeighborArray *neighbors = &e->neighbors[lc];
|
Page page;
|
||||||
|
GenericXLogState *state;
|
||||||
|
HnswUpdate *update = lfirst(lc);
|
||||||
|
ItemId itemid;
|
||||||
|
HnswNeighborTuple ntup;
|
||||||
|
Size ntupSize;
|
||||||
|
int idx;
|
||||||
|
OffsetNumber offno = update->hc.element->neighborOffno;
|
||||||
|
|
||||||
for (int i = 0; i < neighbors->length; i++)
|
/* Register page */
|
||||||
|
buf = ReadBuffer(index, update->hc.element->neighborPage);
|
||||||
|
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
|
||||||
|
state = GenericXLogStart(index);
|
||||||
|
page = GenericXLogRegisterBuffer(state, buf, 0);
|
||||||
|
|
||||||
|
/* Get tuple */
|
||||||
|
itemid = PageGetItemId(page, offno);
|
||||||
|
ntup = (HnswNeighborTuple) PageGetItem(page, itemid);
|
||||||
|
ntupSize = ItemIdGetLength(itemid);
|
||||||
|
|
||||||
|
/* Calculate index */
|
||||||
|
idx = HnswGetIndex(update, m);
|
||||||
|
|
||||||
|
/* Make robust to issues */
|
||||||
|
if (idx < ntup->count)
|
||||||
{
|
{
|
||||||
HnswCandidate *hc = &neighbors->items[i];
|
ItemPointer indextid = &ntup->indextids[idx];
|
||||||
Buffer buf;
|
|
||||||
Page page;
|
|
||||||
GenericXLogState *state;
|
|
||||||
ItemId itemid;
|
|
||||||
HnswNeighborTuple ntup;
|
|
||||||
Size ntupSize;
|
|
||||||
int idx = -1;
|
|
||||||
int startIdx;
|
|
||||||
OffsetNumber offno = hc->element->neighborOffno;
|
|
||||||
|
|
||||||
/* Get latest neighbors since they may have changed */
|
/* Update neighbor */
|
||||||
/* Do not lock yet since selecting neighbors can take time */
|
ItemPointerSet(indextid, e->blkno, e->offno);
|
||||||
HnswLoadNeighbors(hc->element, index);
|
|
||||||
|
|
||||||
/*
|
/* Overwrite tuple */
|
||||||
* Could improve performance for vacuuming by checking neighbors
|
if (!PageIndexTupleOverwrite(page, offno, (Item) ntup, ntupSize))
|
||||||
* against list of elements being deleted to find index. It's
|
elog(ERROR, "failed to add index item to \"%s\"", RelationGetRelationName(index));
|
||||||
* important to exclude already deleted elements for this since
|
|
||||||
* they can be replaced at any time.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* Select neighbors */
|
/* Commit */
|
||||||
HnswUpdateConnection(e, hc, lm, lc, &idx, index, procinfo, collation);
|
MarkBufferDirty(buf);
|
||||||
|
GenericXLogFinish(state);
|
||||||
/* New element was not selected as a neighbor */
|
|
||||||
if (idx == -1)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
/* Register page */
|
|
||||||
buf = ReadBuffer(index, hc->element->neighborPage);
|
|
||||||
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
|
|
||||||
state = GenericXLogStart(index);
|
|
||||||
page = GenericXLogRegisterBuffer(state, buf, 0);
|
|
||||||
|
|
||||||
/* Get tuple */
|
|
||||||
itemid = PageGetItemId(page, offno);
|
|
||||||
ntup = (HnswNeighborTuple) PageGetItem(page, itemid);
|
|
||||||
ntupSize = ItemIdGetLength(itemid);
|
|
||||||
|
|
||||||
/* Calculate index for update */
|
|
||||||
startIdx = (hc->element->level - lc) * m;
|
|
||||||
|
|
||||||
/* Check for existing connection */
|
|
||||||
if (checkExisting && ConnectionExists(e, ntup, startIdx, lm))
|
|
||||||
idx = -1;
|
|
||||||
else if (idx == -2)
|
|
||||||
{
|
|
||||||
/* Find free offset if still exists */
|
|
||||||
/* TODO Retry updating connections if not */
|
|
||||||
for (int j = 0; j < lm; j++)
|
|
||||||
{
|
|
||||||
if (!ItemPointerIsValid(&ntup->indextids[startIdx + j]))
|
|
||||||
{
|
|
||||||
idx = startIdx + j;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
idx += startIdx;
|
|
||||||
|
|
||||||
/* Make robust to issues */
|
|
||||||
if (idx >= 0 && idx < ntup->count)
|
|
||||||
{
|
|
||||||
ItemPointer indextid = &ntup->indextids[idx];
|
|
||||||
|
|
||||||
/* Update neighbor */
|
|
||||||
ItemPointerSet(indextid, e->blkno, e->offno);
|
|
||||||
|
|
||||||
/* Overwrite tuple */
|
|
||||||
if (!PageIndexTupleOverwrite(page, offno, (Item) ntup, ntupSize))
|
|
||||||
elog(ERROR, "failed to add index item to \"%s\"", RelationGetRelationName(index));
|
|
||||||
|
|
||||||
/* Commit */
|
|
||||||
MarkBufferDirty(buf);
|
|
||||||
GenericXLogFinish(state);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
GenericXLogAbort(state);
|
|
||||||
|
|
||||||
UnlockReleaseBuffer(buf);
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
GenericXLogAbort(state);
|
||||||
|
|
||||||
|
UnlockReleaseBuffer(buf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,10 +391,8 @@ HnswAddDuplicate(Relation index, HnswElement element, HnswElement dup)
|
|||||||
* Write changes to disk
|
* Write changes to disk
|
||||||
*/
|
*/
|
||||||
static void
|
static void
|
||||||
WriteElement(Relation index, FmgrInfo *procinfo, Oid collation, HnswElement element, int m, int efConstruction, HnswElement dup, HnswElement entryPoint)
|
WriteElement(Relation index, HnswElement element, int m, List *updates, HnswElement dup, HnswElement entryPoint)
|
||||||
{
|
{
|
||||||
BlockNumber newInsertPage = InvalidBlockNumber;
|
|
||||||
|
|
||||||
/* Try to add to existing page */
|
/* Try to add to existing page */
|
||||||
if (dup != NULL)
|
if (dup != NULL)
|
||||||
{
|
{
|
||||||
@@ -467,19 +400,13 @@ WriteElement(Relation index, FmgrInfo *procinfo, Oid collation, HnswElement elem
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Write element and neighbor tuples */
|
/* If fails, take this path */
|
||||||
WriteNewElementPages(index, element, m, GetInsertPage(index), &newInsertPage);
|
WriteNewElementPages(index, element, m);
|
||||||
|
UpdateNeighborPages(index, element, m, updates);
|
||||||
/* Update insert page if needed */
|
|
||||||
if (BlockNumberIsValid(newInsertPage))
|
|
||||||
HnswUpdateMetaPage(index, 0, NULL, newInsertPage, MAIN_FORKNUM);
|
|
||||||
|
|
||||||
/* Update neighbors */
|
|
||||||
HnswUpdateNeighborPages(index, procinfo, collation, element, m, false);
|
|
||||||
|
|
||||||
/* Update metapage if needed */
|
/* Update metapage if needed */
|
||||||
if (entryPoint == NULL || element->level > entryPoint->level)
|
if (entryPoint == NULL || element->level > entryPoint->level)
|
||||||
HnswUpdateMetaPage(index, HNSW_UPDATE_ENTRY_GREATER, element, InvalidBlockNumber, MAIN_FORKNUM);
|
HnswUpdateMetaPage(index, true, element, InvalidBlockNumber, MAIN_FORKNUM);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -490,6 +417,7 @@ HnswInsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heap_ti
|
|||||||
{
|
{
|
||||||
Datum value;
|
Datum value;
|
||||||
FmgrInfo *normprocinfo;
|
FmgrInfo *normprocinfo;
|
||||||
|
FmgrInfo *normalizeprocinfo;
|
||||||
HnswElement entryPoint;
|
HnswElement entryPoint;
|
||||||
HnswElement element;
|
HnswElement element;
|
||||||
int m = HnswGetM(index);
|
int m = HnswGetM(index);
|
||||||
@@ -497,59 +425,29 @@ HnswInsertTuple(Relation index, Datum *values, bool *isnull, ItemPointer heap_ti
|
|||||||
double ml = HnswGetMl(m);
|
double ml = HnswGetMl(m);
|
||||||
FmgrInfo *procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC);
|
FmgrInfo *procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC);
|
||||||
Oid collation = index->rd_indcollation[0];
|
Oid collation = index->rd_indcollation[0];
|
||||||
|
List *updates = NIL;
|
||||||
HnswElement dup;
|
HnswElement dup;
|
||||||
LOCKMODE lockmode = ShareLock;
|
|
||||||
|
|
||||||
/* Detoast once for all calls */
|
/* Detoast once for all calls */
|
||||||
value = PointerGetDatum(PG_DETOAST_DATUM(values[0]));
|
value = PointerGetDatum(PG_DETOAST_DATUM(values[0]));
|
||||||
|
|
||||||
/* Normalize if needed */
|
/* Normalize if needed */
|
||||||
normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC);
|
normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC);
|
||||||
if (normprocinfo != NULL)
|
normalizeprocinfo = HnswOptionalProcInfo(index, HNSW_NORMALIZE_PROC);
|
||||||
{
|
HnswNormValue(normprocinfo, normalizeprocinfo, collation, &value, NULL);
|
||||||
if (!HnswNormValue(normprocinfo, collation, &value, NULL))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Create an element */
|
/* Create an element */
|
||||||
element = HnswInitElement(heap_tid, m, ml, HnswGetMaxLevel(m));
|
element = HnswInitElement(heap_tid, m, ml, HnswGetMaxLevel(m));
|
||||||
element->vec = DatumGetVector(value);
|
element->vec = DatumGetVector(value);
|
||||||
|
|
||||||
/*
|
|
||||||
* Get a shared lock. This allows vacuum to ensure no in-flight inserts
|
|
||||||
* before repairing graph. Use a page lock so it does not interfere with
|
|
||||||
* buffer lock (or reads when vacuuming).
|
|
||||||
*/
|
|
||||||
LockPage(index, HNSW_UPDATE_LOCK, lockmode);
|
|
||||||
|
|
||||||
/* Get entry point */
|
/* Get entry point */
|
||||||
entryPoint = HnswGetEntryPoint(index);
|
entryPoint = HnswGetEntryPoint(index);
|
||||||
|
|
||||||
/* Prevent concurrent inserts when likely updating entry point */
|
|
||||||
if (entryPoint == NULL || element->level > entryPoint->level)
|
|
||||||
{
|
|
||||||
/* Release shared lock */
|
|
||||||
UnlockPage(index, HNSW_UPDATE_LOCK, lockmode);
|
|
||||||
|
|
||||||
/* Get exclusive lock */
|
|
||||||
lockmode = ExclusiveLock;
|
|
||||||
LockPage(index, HNSW_UPDATE_LOCK, lockmode);
|
|
||||||
|
|
||||||
/* Get latest entry point after lock is acquired */
|
|
||||||
entryPoint = HnswGetEntryPoint(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Insert element in graph */
|
/* Insert element in graph */
|
||||||
HnswInsertElement(element, entryPoint, index, procinfo, collation, m, efConstruction, false);
|
dup = HnswInsertElement(element, entryPoint, index, procinfo, collation, m, efConstruction, &updates, false);
|
||||||
|
|
||||||
/* Look for duplicate */
|
|
||||||
dup = HnswFindDuplicate(element);
|
|
||||||
|
|
||||||
/* Write to disk */
|
/* Write to disk */
|
||||||
WriteElement(index, procinfo, collation, element, m, efConstruction, dup, entryPoint);
|
WriteElement(index, element, m, updates, dup, entryPoint);
|
||||||
|
|
||||||
/* Release lock */
|
|
||||||
UnlockPage(index, HNSW_UPDATE_LOCK, lockmode);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,35 +4,34 @@
|
|||||||
#include "hnsw.h"
|
#include "hnsw.h"
|
||||||
#include "pgstat.h"
|
#include "pgstat.h"
|
||||||
#include "storage/bufmgr.h"
|
#include "storage/bufmgr.h"
|
||||||
#include "storage/lmgr.h"
|
|
||||||
#include "utils/memutils.h"
|
#include "utils/memutils.h"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Algorithm 5 from paper
|
* Algorithm 5 from paper
|
||||||
*/
|
*/
|
||||||
static List *
|
static void
|
||||||
GetScanItems(IndexScanDesc scan, Datum q)
|
GetScanItems(IndexScanDesc scan, Datum q)
|
||||||
{
|
{
|
||||||
HnswScanOpaque so = (HnswScanOpaque) scan->opaque;
|
HnswScanOpaque so = (HnswScanOpaque) scan->opaque;
|
||||||
Relation index = scan->indexRelation;
|
Relation index = scan->indexRelation;
|
||||||
FmgrInfo *procinfo = so->procinfo;
|
FmgrInfo *procinfo = so->procinfo;
|
||||||
Oid collation = so->collation;
|
Oid collation = so->collation;
|
||||||
List *ep;
|
List *ep = NIL;
|
||||||
List *w;
|
List *w;
|
||||||
HnswElement entryPoint = HnswGetEntryPoint(index);
|
HnswElement entryPoint = HnswGetEntryPoint(index);
|
||||||
|
|
||||||
if (entryPoint == NULL)
|
if (entryPoint == NULL)
|
||||||
return NIL;
|
return;
|
||||||
|
|
||||||
ep = list_make1(HnswEntryCandidate(entryPoint, q, index, procinfo, collation, false));
|
ep = lappend(ep, HnswEntryCandidate(entryPoint, q, index, procinfo, collation, false));
|
||||||
|
|
||||||
for (int lc = entryPoint->level; lc >= 1; lc--)
|
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, false, NULL, NULL);
|
||||||
ep = w;
|
ep = w;
|
||||||
}
|
}
|
||||||
|
|
||||||
return HnswSearchLayer(q, ep, hnsw_ef_search, 0, index, procinfo, collation, false, NULL);
|
so->w = HnswSearchLayer(q, ep, hnsw_ef_search, 0, index, procinfo, collation, false, NULL, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -79,6 +78,7 @@ hnswbeginscan(Relation index, int nkeys, int norderbys)
|
|||||||
/* Set support functions */
|
/* Set support functions */
|
||||||
so->procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC);
|
so->procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC);
|
||||||
so->normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC);
|
so->normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC);
|
||||||
|
so->normalizeprocinfo = HnswOptionalProcInfo(index, HNSW_NORMALIZE_PROC);
|
||||||
so->collation = index->rd_indcollation[0];
|
so->collation = index->rd_indcollation[0];
|
||||||
|
|
||||||
scan->opaque = so;
|
scan->opaque = so;
|
||||||
@@ -141,21 +141,10 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir)
|
|||||||
Assert(!VARATT_IS_EXTENDED(DatumGetPointer(value)));
|
Assert(!VARATT_IS_EXTENDED(DatumGetPointer(value)));
|
||||||
|
|
||||||
/* Fine if normalization fails */
|
/* Fine if normalization fails */
|
||||||
if (so->normprocinfo != NULL)
|
HnswNormValue(so->normprocinfo, so->normalizeprocinfo, so->collation, &value, NULL);
|
||||||
HnswNormValue(so->normprocinfo, so->collation, &value, NULL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
GetScanItems(scan, value);
|
||||||
* Get a shared lock. This allows vacuum to ensure no in-flight scans
|
|
||||||
* before marking tuples as deleted.
|
|
||||||
*/
|
|
||||||
LockPage(scan->indexRelation, HNSW_SCAN_LOCK, ShareLock);
|
|
||||||
|
|
||||||
so->w = GetScanItems(scan, value);
|
|
||||||
|
|
||||||
/* Release shared lock */
|
|
||||||
UnlockPage(scan->indexRelation, HNSW_SCAN_LOCK, ShareLock);
|
|
||||||
|
|
||||||
so->first = false;
|
so->first = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
417
src/hnswutils.c
417
src/hnswutils.c
@@ -7,7 +7,7 @@
|
|||||||
#include "vector.h"
|
#include "vector.h"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Get the max number of connections in an upper layer for each element in the index
|
* Get the number of connection in the index
|
||||||
*/
|
*/
|
||||||
int
|
int
|
||||||
HnswGetM(Relation index)
|
HnswGetM(Relation index)
|
||||||
@@ -47,17 +47,26 @@ HnswOptionalProcInfo(Relation rel, uint16 procnum)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Divide by the norm
|
* Normalize a vector
|
||||||
*
|
|
||||||
* Returns false if value should not be indexed
|
|
||||||
*
|
*
|
||||||
* The caller needs to free the pointer stored in value
|
* The caller needs to free the pointer stored in value
|
||||||
* if it's different than the original value
|
* if it's different than the original value
|
||||||
*/
|
*/
|
||||||
bool
|
void
|
||||||
HnswNormValue(FmgrInfo *procinfo, Oid collation, Datum *value, Vector * result)
|
HnswNormValue(FmgrInfo *procinfo, FmgrInfo *normalizeprocinfo, Oid collation, Datum *value, Vector * result)
|
||||||
{
|
{
|
||||||
double norm = DatumGetFloat8(FunctionCall1Coll(procinfo, collation, *value));
|
double norm;
|
||||||
|
|
||||||
|
if (normalizeprocinfo != NULL)
|
||||||
|
{
|
||||||
|
*value = FunctionCall1Coll(normalizeprocinfo, collation, *value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (procinfo == NULL)
|
||||||
|
return;
|
||||||
|
|
||||||
|
norm = DatumGetFloat8(FunctionCall1Coll(procinfo, collation, *value));
|
||||||
|
|
||||||
if (norm > 0)
|
if (norm > 0)
|
||||||
{
|
{
|
||||||
@@ -70,11 +79,7 @@ HnswNormValue(FmgrInfo *procinfo, Oid collation, Datum *value, Vector * result)
|
|||||||
result->x[i] = v->x[i] / norm;
|
result->x[i] = v->x[i] / norm;
|
||||||
|
|
||||||
*value = PointerGetDatum(result);
|
*value = PointerGetDatum(result);
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -197,8 +202,8 @@ HnswAddHeapTid(HnswElement element, ItemPointer heaptid)
|
|||||||
/*
|
/*
|
||||||
* Allocate an element from block and offset numbers
|
* Allocate an element from block and offset numbers
|
||||||
*/
|
*/
|
||||||
HnswElement
|
static HnswElement
|
||||||
HnswInitElementFromBlock(BlockNumber blkno, OffsetNumber offno)
|
InitElementFromBlock(BlockNumber blkno, OffsetNumber offno)
|
||||||
{
|
{
|
||||||
HnswElement element = palloc(sizeof(HnswElementData));
|
HnswElement element = palloc(sizeof(HnswElementData));
|
||||||
|
|
||||||
@@ -226,7 +231,7 @@ HnswGetEntryPoint(Relation index)
|
|||||||
metap = HnswPageGetMeta(page);
|
metap = HnswPageGetMeta(page);
|
||||||
|
|
||||||
if (BlockNumberIsValid(metap->entryBlkno))
|
if (BlockNumberIsValid(metap->entryBlkno))
|
||||||
entryPoint = HnswInitElementFromBlock(metap->entryBlkno, metap->entryOffno);
|
entryPoint = InitElementFromBlock(metap->entryBlkno, metap->entryOffno);
|
||||||
|
|
||||||
UnlockReleaseBuffer(buf);
|
UnlockReleaseBuffer(buf);
|
||||||
|
|
||||||
@@ -234,12 +239,22 @@ HnswGetEntryPoint(Relation index)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Update the metapage info
|
* Update the metapage
|
||||||
*/
|
*/
|
||||||
static void
|
void
|
||||||
HnswUpdateMetaPageInfo(Page page, int updateEntry, HnswElement entryPoint, BlockNumber insertPage)
|
HnswUpdateMetaPage(Relation index, bool updateEntry, HnswElement entryPoint, BlockNumber insertPage, ForkNumber forkNum)
|
||||||
{
|
{
|
||||||
HnswMetaPage metap = HnswPageGetMeta(page);
|
Buffer buf;
|
||||||
|
Page page;
|
||||||
|
GenericXLogState *state;
|
||||||
|
HnswMetaPage metap;
|
||||||
|
|
||||||
|
buf = ReadBufferExtended(index, forkNum, HNSW_METAPAGE_BLKNO, RBM_NORMAL, NULL);
|
||||||
|
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
|
||||||
|
state = GenericXLogStart(index);
|
||||||
|
page = GenericXLogRegisterBuffer(state, buf, 0);
|
||||||
|
|
||||||
|
metap = HnswPageGetMeta(page);
|
||||||
|
|
||||||
if (updateEntry)
|
if (updateEntry)
|
||||||
{
|
{
|
||||||
@@ -249,7 +264,7 @@ HnswUpdateMetaPageInfo(Page page, int updateEntry, HnswElement entryPoint, Block
|
|||||||
metap->entryOffno = InvalidOffsetNumber;
|
metap->entryOffno = InvalidOffsetNumber;
|
||||||
metap->entryLevel = -1;
|
metap->entryLevel = -1;
|
||||||
}
|
}
|
||||||
else if (entryPoint->level > metap->entryLevel || updateEntry == HNSW_UPDATE_ENTRY_ALWAYS)
|
else
|
||||||
{
|
{
|
||||||
metap->entryBlkno = entryPoint->blkno;
|
metap->entryBlkno = entryPoint->blkno;
|
||||||
metap->entryOffno = entryPoint->offno;
|
metap->entryOffno = entryPoint->offno;
|
||||||
@@ -259,24 +274,6 @@ HnswUpdateMetaPageInfo(Page page, int updateEntry, HnswElement entryPoint, Block
|
|||||||
|
|
||||||
if (BlockNumberIsValid(insertPage))
|
if (BlockNumberIsValid(insertPage))
|
||||||
metap->insertPage = insertPage;
|
metap->insertPage = insertPage;
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Update the metapage
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
HnswUpdateMetaPage(Relation index, int updateEntry, HnswElement entryPoint, BlockNumber insertPage, ForkNumber forkNum)
|
|
||||||
{
|
|
||||||
Buffer buf;
|
|
||||||
Page page;
|
|
||||||
GenericXLogState *state;
|
|
||||||
|
|
||||||
buf = ReadBufferExtended(index, forkNum, HNSW_METAPAGE_BLKNO, RBM_NORMAL, NULL);
|
|
||||||
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
|
|
||||||
state = GenericXLogStart(index);
|
|
||||||
page = GenericXLogRegisterBuffer(state, buf, 0);
|
|
||||||
|
|
||||||
HnswUpdateMetaPageInfo(page, updateEntry, entryPoint, insertPage);
|
|
||||||
|
|
||||||
HnswCommitBuffer(buf, state);
|
HnswCommitBuffer(buf, state);
|
||||||
}
|
}
|
||||||
@@ -364,7 +361,7 @@ LoadNeighborsFromPage(HnswElement element, Relation index, Page page)
|
|||||||
if (!ItemPointerIsValid(indextid))
|
if (!ItemPointerIsValid(indextid))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
e = HnswInitElementFromBlock(ItemPointerGetBlockNumber(indextid), ItemPointerGetOffsetNumber(indextid));
|
e = InitElementFromBlock(ItemPointerGetBlockNumber(indextid), ItemPointerGetOffsetNumber(indextid));
|
||||||
|
|
||||||
/* Calculate level based on offset */
|
/* Calculate level based on offset */
|
||||||
level = element->level - i / m;
|
level = element->level - i / m;
|
||||||
@@ -380,8 +377,8 @@ LoadNeighborsFromPage(HnswElement element, Relation index, Page page)
|
|||||||
/*
|
/*
|
||||||
* Load neighbors
|
* Load neighbors
|
||||||
*/
|
*/
|
||||||
void
|
static void
|
||||||
HnswLoadNeighbors(HnswElement element, Relation index)
|
LoadNeighbors(HnswElement element, Relation index)
|
||||||
{
|
{
|
||||||
Buffer buf;
|
Buffer buf;
|
||||||
Page page;
|
Page page;
|
||||||
@@ -395,37 +392,6 @@ HnswLoadNeighbors(HnswElement element, Relation index)
|
|||||||
UnlockReleaseBuffer(buf);
|
UnlockReleaseBuffer(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Load an element from a tuple
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool loadHeaptids, bool loadVec)
|
|
||||||
{
|
|
||||||
element->level = etup->level;
|
|
||||||
element->deleted = etup->deleted;
|
|
||||||
element->neighborPage = ItemPointerGetBlockNumber(&etup->neighbortid);
|
|
||||||
element->neighborOffno = ItemPointerGetOffsetNumber(&etup->neighbortid);
|
|
||||||
element->heaptids = NIL;
|
|
||||||
|
|
||||||
if (loadHeaptids)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < HNSW_HEAPTIDS; i++)
|
|
||||||
{
|
|
||||||
/* Can stop at first invalid */
|
|
||||||
if (!ItemPointerIsValid(&etup->heaptids[i]))
|
|
||||||
break;
|
|
||||||
|
|
||||||
HnswAddHeapTid(element, &etup->heaptids[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loadVec)
|
|
||||||
{
|
|
||||||
element->vec = palloc(VECTOR_SIZE(etup->vec.dim));
|
|
||||||
memcpy(element->vec, &etup->vec, VECTOR_SIZE(etup->vec.dim));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Load an element and optionally get its distance from q
|
* Load an element and optionally get its distance from q
|
||||||
*/
|
*/
|
||||||
@@ -446,7 +412,25 @@ HnswLoadElement(HnswElement element, float *distance, Datum *q, Relation index,
|
|||||||
Assert(HnswIsElementTuple(etup));
|
Assert(HnswIsElementTuple(etup));
|
||||||
|
|
||||||
/* Load element */
|
/* Load element */
|
||||||
HnswLoadElementFromTuple(element, etup, true, loadVec);
|
element->heaptids = NIL;
|
||||||
|
for (int i = 0; i < HNSW_HEAPTIDS; i++)
|
||||||
|
{
|
||||||
|
/* Can stop at first invalid */
|
||||||
|
if (!ItemPointerIsValid(&etup->heaptids[i]))
|
||||||
|
break;
|
||||||
|
|
||||||
|
HnswAddHeapTid(element, &etup->heaptids[i]);
|
||||||
|
}
|
||||||
|
element->level = etup->level;
|
||||||
|
element->neighborPage = ItemPointerGetBlockNumber(&etup->neighbortid);
|
||||||
|
element->neighborOffno = ItemPointerGetOffsetNumber(&etup->neighbortid);
|
||||||
|
element->deleted = etup->deleted;
|
||||||
|
|
||||||
|
if (loadVec)
|
||||||
|
{
|
||||||
|
element->vec = palloc(VECTOR_SIZE(etup->vec.dim));
|
||||||
|
memcpy(element->vec, &etup->vec, VECTOR_SIZE(etup->vec.dim));
|
||||||
|
}
|
||||||
|
|
||||||
/* Calculate distance */
|
/* Calculate distance */
|
||||||
if (distance != NULL)
|
if (distance != NULL)
|
||||||
@@ -468,7 +452,7 @@ GetCandidateDistance(HnswCandidate * hc, Datum q, FmgrInfo *procinfo, Oid collat
|
|||||||
* Create a candidate for the entry point
|
* Create a candidate for the entry point
|
||||||
*/
|
*/
|
||||||
HnswCandidate *
|
HnswCandidate *
|
||||||
HnswEntryCandidate(HnswElement entryPoint, Datum q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec)
|
HnswEntryCandidate(HnswElement entryPoint, Datum q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadvec)
|
||||||
{
|
{
|
||||||
HnswCandidate *hc = palloc(sizeof(HnswCandidate));
|
HnswCandidate *hc = palloc(sizeof(HnswCandidate));
|
||||||
|
|
||||||
@@ -476,7 +460,7 @@ HnswEntryCandidate(HnswElement entryPoint, Datum q, Relation index, FmgrInfo *pr
|
|||||||
if (index == NULL)
|
if (index == NULL)
|
||||||
hc->distance = GetCandidateDistance(hc, q, procinfo, collation);
|
hc->distance = GetCandidateDistance(hc, q, procinfo, collation);
|
||||||
else
|
else
|
||||||
HnswLoadElement(hc->element, &hc->distance, &q, index, procinfo, collation, loadVec);
|
HnswLoadElement(hc->element, &hc->distance, &q, index, procinfo, collation, loadvec);
|
||||||
return hc;
|
return hc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,7 +527,7 @@ AddToVisited(HTAB *v, HnswCandidate * hc, Relation index, bool *found)
|
|||||||
* Algorithm 2 from paper
|
* Algorithm 2 from paper
|
||||||
*/
|
*/
|
||||||
List *
|
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 inserting, BlockNumber *skipPage, OffsetNumber *skipOffno)
|
||||||
{
|
{
|
||||||
ListCell *lc2;
|
ListCell *lc2;
|
||||||
|
|
||||||
@@ -579,13 +563,7 @@ HnswSearchLayer(Datum q, List *ep, int ef, int lc, Relation index, FmgrInfo *pro
|
|||||||
pairingheap_add(C, &(CreatePairingHeapNode(hc)->ph_node));
|
pairingheap_add(C, &(CreatePairingHeapNode(hc)->ph_node));
|
||||||
pairingheap_add(W, &(CreatePairingHeapNode(hc)->ph_node));
|
pairingheap_add(W, &(CreatePairingHeapNode(hc)->ph_node));
|
||||||
|
|
||||||
/*
|
wlen++;
|
||||||
* Do not count elements being deleted towards ef when vacuuming. It
|
|
||||||
* would be ideal to do this for inserts as well, but this could
|
|
||||||
* affect insert performance.
|
|
||||||
*/
|
|
||||||
if (skipElement == NULL || list_length(hc->element->heaptids) != 0)
|
|
||||||
wlen++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
while (!pairingheap_is_empty(C))
|
while (!pairingheap_is_empty(C))
|
||||||
@@ -598,7 +576,7 @@ HnswSearchLayer(Datum q, List *ep, int ef, int lc, Relation index, FmgrInfo *pro
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
if (c->element->neighbors == NULL)
|
if (c->element->neighbors == NULL)
|
||||||
HnswLoadNeighbors(c->element, index);
|
LoadNeighbors(c->element, index);
|
||||||
|
|
||||||
/* Get the neighborhood at layer lc */
|
/* Get the neighborhood at layer lc */
|
||||||
neighborhood = &c->element->neighbors[lc];
|
neighborhood = &c->element->neighbors[lc];
|
||||||
@@ -621,10 +599,16 @@ HnswSearchLayer(Datum q, List *ep, int ef, int lc, Relation index, FmgrInfo *pro
|
|||||||
else
|
else
|
||||||
HnswLoadElement(e->element, &eDistance, &q, index, procinfo, collation, inserting);
|
HnswLoadElement(e->element, &eDistance, &q, index, procinfo, collation, inserting);
|
||||||
|
|
||||||
Assert(!e->element->deleted);
|
/* Skip if fully deleted */
|
||||||
|
if (e->element->deleted)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
/* Skip for inserts if deleting */
|
||||||
|
if (inserting && list_length(e->element->heaptids) == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
/* Skip self for vacuuming update */
|
/* Skip self for vacuuming update */
|
||||||
if (skipElement != NULL && e->element->blkno == skipElement->blkno && e->element->offno == skipElement->offno)
|
if (skipPage != NULL && e->element->neighborPage == *skipPage && e->element->neighborOffno == *skipOffno)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
/* Make robust to issues */
|
/* Make robust to issues */
|
||||||
@@ -641,20 +625,11 @@ HnswSearchLayer(Datum q, List *ep, int ef, int lc, Relation index, FmgrInfo *pro
|
|||||||
|
|
||||||
pairingheap_add(C, &(CreatePairingHeapNode(ec)->ph_node));
|
pairingheap_add(C, &(CreatePairingHeapNode(ec)->ph_node));
|
||||||
pairingheap_add(W, &(CreatePairingHeapNode(ec)->ph_node));
|
pairingheap_add(W, &(CreatePairingHeapNode(ec)->ph_node));
|
||||||
|
wlen++;
|
||||||
|
|
||||||
/*
|
/* No need to decrement wlen */
|
||||||
* Do not count elements being deleted towards ef when
|
if (wlen > ef)
|
||||||
* vacuuming. It would be ideal to do this for inserts as
|
pairingheap_remove_first(W);
|
||||||
* well, but this could affect insert performance.
|
|
||||||
*/
|
|
||||||
if (skipElement == NULL || list_length(e->element->heaptids) != 0)
|
|
||||||
{
|
|
||||||
wlen++;
|
|
||||||
|
|
||||||
/* No need to decrement wlen */
|
|
||||||
if (wlen > ef)
|
|
||||||
pairingheap_remove_first(W);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -733,7 +708,7 @@ SelectNeighbors(List *c, int m, int lc, FmgrInfo *procinfo, Oid collation, HnswC
|
|||||||
List *w = list_copy(c);
|
List *w = list_copy(c);
|
||||||
pairingheap *wd;
|
pairingheap *wd;
|
||||||
|
|
||||||
if (list_length(w) <= m)
|
if (list_length(w) < m)
|
||||||
return w;
|
return w;
|
||||||
|
|
||||||
wd = pairingheap_allocate(CompareNearestCandidates, NULL);
|
wd = pairingheap_allocate(CompareNearestCandidates, NULL);
|
||||||
@@ -773,14 +748,14 @@ SelectNeighbors(List *c, int m, int lc, FmgrInfo *procinfo, Oid collation, HnswC
|
|||||||
/*
|
/*
|
||||||
* Find duplicate element
|
* Find duplicate element
|
||||||
*/
|
*/
|
||||||
HnswElement
|
static HnswElement
|
||||||
HnswFindDuplicate(HnswElement e)
|
HnswFindDuplicate(HnswElement e, List *neighbors)
|
||||||
{
|
{
|
||||||
HnswNeighborArray *neighbors = &e->neighbors[0];
|
ListCell *lc;
|
||||||
|
|
||||||
for (int i = 0; i < neighbors->length; i++)
|
foreach(lc, neighbors)
|
||||||
{
|
{
|
||||||
HnswCandidate *neighbor = &neighbors->items[i];
|
HnswCandidate *neighbor = lfirst(lc);
|
||||||
|
|
||||||
/* Exit early since ordered by distance */
|
/* Exit early since ordered by distance */
|
||||||
if (vector_cmp_internal(e->vec, neighbor->element->vec) != 0)
|
if (vector_cmp_internal(e->vec, neighbor->element->vec) != 0)
|
||||||
@@ -829,134 +804,144 @@ CompareCandidateDistances(const void *a, const void *b)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create update
|
||||||
|
*/
|
||||||
|
static HnswUpdate *
|
||||||
|
CreateUpdate(HnswCandidate * hc, int level, int index)
|
||||||
|
{
|
||||||
|
HnswUpdate *update = palloc(sizeof(HnswUpdate));
|
||||||
|
|
||||||
|
update->hc = *hc;
|
||||||
|
update->level = level;
|
||||||
|
update->index = index;
|
||||||
|
return update;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Update connections
|
* Update connections
|
||||||
*/
|
*/
|
||||||
void
|
static void
|
||||||
HnswUpdateConnection(HnswElement element, HnswCandidate * hc, int m, int lc, int *updateIdx, Relation index, FmgrInfo *procinfo, Oid collation)
|
UpdateConnections(HnswElement element, List *neighbors, int m, int lc, List **updates, Relation index, FmgrInfo *procinfo, Oid collation)
|
||||||
{
|
{
|
||||||
HnswNeighborArray *currentNeighbors = &hc->element->neighbors[lc];
|
ListCell *lc2;
|
||||||
|
|
||||||
HnswCandidate hc2;
|
foreach(lc2, neighbors)
|
||||||
|
|
||||||
hc2.element = element;
|
|
||||||
hc2.distance = hc->distance;
|
|
||||||
|
|
||||||
if (currentNeighbors->length < m)
|
|
||||||
{
|
{
|
||||||
currentNeighbors->items[currentNeighbors->length++] = hc2;
|
HnswCandidate *hc = (HnswCandidate *) lfirst(lc2);
|
||||||
|
HnswNeighborArray *currentNeighbors = &hc->element->neighbors[lc];
|
||||||
|
|
||||||
/* Track update */
|
HnswCandidate hc2;
|
||||||
if (updateIdx != NULL)
|
|
||||||
*updateIdx = -2;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
/* Shrink connections */
|
|
||||||
HnswCandidate *pruned = NULL;
|
|
||||||
|
|
||||||
/* Load elements on insert */
|
hc2.element = element;
|
||||||
if (index != NULL)
|
hc2.distance = hc->distance;
|
||||||
|
|
||||||
|
if (currentNeighbors->length < m)
|
||||||
{
|
{
|
||||||
Datum q = PointerGetDatum(hc->element->vec);
|
currentNeighbors->items[currentNeighbors->length++] = hc2;
|
||||||
|
|
||||||
|
/* Track updates */
|
||||||
|
if (updates != NULL)
|
||||||
|
*updates = lappend(*updates, CreateUpdate(hc, lc, currentNeighbors->length - 1));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* Shrink connections */
|
||||||
|
HnswCandidate *pruned = NULL;
|
||||||
|
List *c = NIL;
|
||||||
|
|
||||||
|
/* Load elements on insert */
|
||||||
|
if (index != NULL)
|
||||||
|
{
|
||||||
|
Datum q = PointerGetDatum(hc->element->vec);
|
||||||
|
|
||||||
|
for (int i = 0; i < currentNeighbors->length; i++)
|
||||||
|
{
|
||||||
|
HnswCandidate *hc3 = ¤tNeighbors->items[i];
|
||||||
|
|
||||||
|
if (hc3->element->vec == NULL)
|
||||||
|
HnswLoadElement(hc3->element, &hc3->distance, &q, index, procinfo, collation, true);
|
||||||
|
else
|
||||||
|
hc3->distance = GetCandidateDistance(hc3, q, procinfo, collation);
|
||||||
|
|
||||||
|
/* Prune element if being deleted */
|
||||||
|
if (list_length(hc3->element->heaptids) == 0)
|
||||||
|
{
|
||||||
|
pruned = ¤tNeighbors->items[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pruned == NULL)
|
||||||
|
{
|
||||||
|
/* Add and sort candidates */
|
||||||
|
for (int i = 0; i < currentNeighbors->length; i++)
|
||||||
|
c = lappend(c, ¤tNeighbors->items[i]);
|
||||||
|
c = lappend(c, &hc2);
|
||||||
|
list_sort(c, CompareCandidateDistances);
|
||||||
|
|
||||||
|
SelectNeighbors(c, m, lc, procinfo, collation, &pruned);
|
||||||
|
|
||||||
|
/* Should not happen */
|
||||||
|
if (pruned == NULL)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Find and replace the pruned element */
|
||||||
for (int i = 0; i < currentNeighbors->length; i++)
|
for (int i = 0; i < currentNeighbors->length; i++)
|
||||||
{
|
{
|
||||||
HnswCandidate *hc3 = ¤tNeighbors->items[i];
|
if (currentNeighbors->items[i].element == pruned->element)
|
||||||
|
|
||||||
if (hc3->element->vec == NULL)
|
|
||||||
HnswLoadElement(hc3->element, &hc3->distance, &q, index, procinfo, collation, true);
|
|
||||||
else
|
|
||||||
hc3->distance = GetCandidateDistance(hc3, q, procinfo, collation);
|
|
||||||
|
|
||||||
/* Prune element if being deleted */
|
|
||||||
if (list_length(hc3->element->heaptids) == 0)
|
|
||||||
{
|
{
|
||||||
pruned = ¤tNeighbors->items[i];
|
currentNeighbors->items[i] = hc2;
|
||||||
|
|
||||||
|
/* Track updates */
|
||||||
|
if (updates != NULL)
|
||||||
|
*updates = lappend(*updates, CreateUpdate(hc, lc, i));
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pruned == NULL)
|
|
||||||
{
|
|
||||||
List *c = NIL;
|
|
||||||
|
|
||||||
/* Add and sort candidates */
|
|
||||||
for (int i = 0; i < currentNeighbors->length; i++)
|
|
||||||
c = lappend(c, ¤tNeighbors->items[i]);
|
|
||||||
c = lappend(c, &hc2);
|
|
||||||
list_sort(c, CompareCandidateDistances);
|
|
||||||
|
|
||||||
SelectNeighbors(c, m, lc, procinfo, collation, &pruned);
|
|
||||||
|
|
||||||
/* Should not happen */
|
|
||||||
if (pruned == NULL)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Find and replace the pruned element */
|
|
||||||
for (int i = 0; i < currentNeighbors->length; i++)
|
|
||||||
{
|
|
||||||
if (currentNeighbors->items[i].element == pruned->element)
|
|
||||||
{
|
|
||||||
currentNeighbors->items[i] = hc2;
|
|
||||||
|
|
||||||
/* Track update */
|
|
||||||
if (updateIdx != NULL)
|
|
||||||
*updateIdx = i;
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Remove elements being deleted
|
|
||||||
*/
|
|
||||||
static List *
|
|
||||||
RemoveElementsBeingDeleted(List *w)
|
|
||||||
{
|
|
||||||
ListCell *lc2;
|
|
||||||
List *w2 = NIL;
|
|
||||||
|
|
||||||
foreach(lc2, w)
|
|
||||||
{
|
|
||||||
HnswCandidate *hc = (HnswCandidate *) lfirst(lc2);
|
|
||||||
|
|
||||||
if (list_length(hc->element->heaptids) != 0)
|
|
||||||
w2 = lappend(w2, hc);
|
|
||||||
}
|
|
||||||
|
|
||||||
return w2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Algorithm 1 from paper
|
* Algorithm 1 from paper
|
||||||
*/
|
*/
|
||||||
void
|
HnswElement
|
||||||
HnswInsertElement(HnswElement element, HnswElement entryPoint, Relation index, FmgrInfo *procinfo, Oid collation, int m, int efConstruction, bool existing)
|
HnswInsertElement(HnswElement element, HnswElement entryPoint, Relation index, FmgrInfo *procinfo, Oid collation, int m, int efConstruction, List **updates, bool vacuuming)
|
||||||
{
|
{
|
||||||
List *ep;
|
List *ep = NIL;
|
||||||
List *w;
|
List *w;
|
||||||
int level = element->level;
|
int level = element->level;
|
||||||
int entryLevel;
|
int entryLevel;
|
||||||
|
List **newNeighbors = palloc(sizeof(List *) * (level + 1));
|
||||||
Datum q = PointerGetDatum(element->vec);
|
Datum q = PointerGetDatum(element->vec);
|
||||||
HnswElement skipElement = existing ? element : NULL;
|
HnswElement dup;
|
||||||
|
BlockNumber *skipPage = vacuuming ? &element->neighborPage : NULL;
|
||||||
/* No neighbors if no entry point */
|
OffsetNumber *skipOffno = vacuuming ? &element->neighborOffno : NULL;
|
||||||
if (entryPoint == NULL)
|
bool removeEntryPoint;
|
||||||
return;
|
HnswCandidate *entryCandidate;
|
||||||
|
|
||||||
/* Get entry point and level */
|
/* Get entry point and level */
|
||||||
ep = list_make1(HnswEntryCandidate(entryPoint, q, index, procinfo, collation, true));
|
if (entryPoint != NULL)
|
||||||
entryLevel = entryPoint->level;
|
{
|
||||||
|
entryCandidate = HnswEntryCandidate(entryPoint, q, index, procinfo, collation, true);
|
||||||
|
ep = lappend(ep, entryCandidate);
|
||||||
|
entryLevel = entryPoint->level;
|
||||||
|
removeEntryPoint = vacuuming && list_length(entryPoint->heaptids) == 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
entryLevel = -1;
|
||||||
|
removeEntryPoint = false;
|
||||||
|
}
|
||||||
|
|
||||||
/* 1st phase: greedy search to insert level */
|
/* 1st phase: greedy search to insert level */
|
||||||
for (int lc = entryLevel; lc >= level + 1; lc--)
|
for (int lc = entryLevel; lc >= level + 1; lc--)
|
||||||
{
|
{
|
||||||
w = HnswSearchLayer(q, ep, 1, lc, index, procinfo, collation, true, skipElement);
|
w = HnswSearchLayer(q, ep, 1, lc, index, procinfo, collation, true, skipPage, skipOffno);
|
||||||
ep = w;
|
ep = w;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -967,19 +952,35 @@ HnswInsertElement(HnswElement element, HnswElement entryPoint, Relation index, F
|
|||||||
for (int lc = level; lc >= 0; lc--)
|
for (int lc = level; lc >= 0; lc--)
|
||||||
{
|
{
|
||||||
int lm = HnswGetLayerM(m, lc);
|
int lm = HnswGetLayerM(m, lc);
|
||||||
List *neighbors;
|
|
||||||
|
|
||||||
w = HnswSearchLayer(q, ep, efConstruction, lc, index, procinfo, collation, true, skipElement);
|
w = HnswSearchLayer(q, ep, efConstruction, lc, index, procinfo, collation, true, skipPage, skipOffno);
|
||||||
|
|
||||||
/* Elements being deleted can help with search */
|
/* Remove entry point if it's being deleted */
|
||||||
/* but should be removed before selecting neighbors */
|
if (removeEntryPoint)
|
||||||
if (index != NULL)
|
w = list_delete_ptr(w, entryCandidate);
|
||||||
w = RemoveElementsBeingDeleted(w);
|
|
||||||
|
|
||||||
neighbors = SelectNeighbors(w, lm, lc, procinfo, collation, NULL);
|
|
||||||
|
|
||||||
AddConnections(element, neighbors, lm, lc);
|
|
||||||
|
|
||||||
|
newNeighbors[lc] = SelectNeighbors(w, lm, lc, procinfo, collation, NULL);
|
||||||
ep = w;
|
ep = w;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Look for duplicate */
|
||||||
|
if (level >= 0 && !vacuuming)
|
||||||
|
{
|
||||||
|
dup = HnswFindDuplicate(element, newNeighbors[0]);
|
||||||
|
if (dup != NULL)
|
||||||
|
return dup;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Update connections */
|
||||||
|
for (int lc = level; lc >= 0; lc--)
|
||||||
|
{
|
||||||
|
int lm = HnswGetLayerM(m, lc);
|
||||||
|
|
||||||
|
AddConnections(element, newNeighbors[lc], lm, lc);
|
||||||
|
|
||||||
|
if (!vacuuming)
|
||||||
|
UpdateConnections(element, newNeighbors[lc], lm, lc, updates, index, procinfo, collation);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|||||||
248
src/hnswvacuum.c
248
src/hnswvacuum.c
@@ -5,11 +5,10 @@
|
|||||||
#include "commands/vacuum.h"
|
#include "commands/vacuum.h"
|
||||||
#include "hnsw.h"
|
#include "hnsw.h"
|
||||||
#include "storage/bufmgr.h"
|
#include "storage/bufmgr.h"
|
||||||
#include "storage/lmgr.h"
|
|
||||||
#include "utils/memutils.h"
|
#include "utils/memutils.h"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Check if deleted list contains an index TID
|
* Check if deleted list contains an index tid
|
||||||
*/
|
*/
|
||||||
static bool
|
static bool
|
||||||
DeletedContains(HTAB *deleted, ItemPointer indextid)
|
DeletedContains(HTAB *deleted, ItemPointer indextid)
|
||||||
@@ -33,7 +32,6 @@ RemoveHeapTids(HnswVacuumState * vacuumstate)
|
|||||||
Relation index = vacuumstate->index;
|
Relation index = vacuumstate->index;
|
||||||
BufferAccessStrategy bas = vacuumstate->bas;
|
BufferAccessStrategy bas = vacuumstate->bas;
|
||||||
HnswElement entryPoint = HnswGetEntryPoint(vacuumstate->index);
|
HnswElement entryPoint = HnswGetEntryPoint(vacuumstate->index);
|
||||||
IndexBulkDeleteResult *stats = vacuumstate->stats;
|
|
||||||
|
|
||||||
/* Store separately since highestPoint.level is uint8 */
|
/* Store separately since highestPoint.level is uint8 */
|
||||||
int highestLevel = -1;
|
int highestLevel = -1;
|
||||||
@@ -79,15 +77,11 @@ RemoveHeapTids(HnswVacuumState * vacuumstate)
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
if (vacuumstate->callback(&etup->heaptids[i], vacuumstate->callback_state))
|
if (vacuumstate->callback(&etup->heaptids[i], vacuumstate->callback_state))
|
||||||
{
|
|
||||||
itemUpdated = true;
|
itemUpdated = true;
|
||||||
stats->tuples_removed++;
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
/* Move to front of list */
|
/* Move to front of list */
|
||||||
etup->heaptids[idx++] = etup->heaptids[i];
|
etup->heaptids[idx++] = etup->heaptids[i];
|
||||||
stats->num_index_tuples++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +109,7 @@ RemoveHeapTids(HnswVacuumState * vacuumstate)
|
|||||||
|
|
||||||
(void) hash_search(vacuumstate->deleted, &ip, HASH_ENTER, NULL);
|
(void) hash_search(vacuumstate->deleted, &ip, HASH_ENTER, NULL);
|
||||||
}
|
}
|
||||||
else if (etup->level > highestLevel && !(entryPoint != NULL && blkno == entryPoint->blkno && offno == entryPoint->offno))
|
else if (etup->level > highestLevel && !(blkno == entryPoint->blkno && offno == entryPoint->offno))
|
||||||
{
|
{
|
||||||
/* Keep track of highest non-entry point */
|
/* Keep track of highest non-entry point */
|
||||||
highestPoint->blkno = blkno;
|
highestPoint->blkno = blkno;
|
||||||
@@ -175,11 +169,6 @@ NeedsUpdated(HnswVacuumState * vacuumstate, HnswElement element)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Also update if layer 0 is not full */
|
|
||||||
/* This could indicate too many candidates being deleted during insert */
|
|
||||||
if (!needsUpdated)
|
|
||||||
needsUpdated = !ItemPointerIsValid(&ntup->indextids[ntup->count - 1]);
|
|
||||||
|
|
||||||
UnlockReleaseBuffer(buf);
|
UnlockReleaseBuffer(buf);
|
||||||
|
|
||||||
return needsUpdated;
|
return needsUpdated;
|
||||||
@@ -189,7 +178,7 @@ NeedsUpdated(HnswVacuumState * vacuumstate, HnswElement element)
|
|||||||
* Repair graph for a single element
|
* Repair graph for a single element
|
||||||
*/
|
*/
|
||||||
static void
|
static void
|
||||||
RepairGraphElement(HnswVacuumState * vacuumstate, HnswElement element, HnswElement entryPoint)
|
RepairGraphElement(HnswVacuumState * vacuumstate, HnswElement element)
|
||||||
{
|
{
|
||||||
Relation index = vacuumstate->index;
|
Relation index = vacuumstate->index;
|
||||||
Buffer buf;
|
Buffer buf;
|
||||||
@@ -199,20 +188,42 @@ RepairGraphElement(HnswVacuumState * vacuumstate, HnswElement element, HnswEleme
|
|||||||
int efConstruction = vacuumstate->efConstruction;
|
int efConstruction = vacuumstate->efConstruction;
|
||||||
FmgrInfo *procinfo = vacuumstate->procinfo;
|
FmgrInfo *procinfo = vacuumstate->procinfo;
|
||||||
Oid collation = vacuumstate->collation;
|
Oid collation = vacuumstate->collation;
|
||||||
|
HnswElement entryPoint;
|
||||||
BufferAccessStrategy bas = vacuumstate->bas;
|
BufferAccessStrategy bas = vacuumstate->bas;
|
||||||
HnswNeighborTuple ntup = vacuumstate->ntup;
|
HnswNeighborTuple ntup = vacuumstate->ntup;
|
||||||
Size ntupSize = HNSW_NEIGHBOR_TUPLE_SIZE(element->level, m);
|
Size ntupSize = HNSW_NEIGHBOR_TUPLE_SIZE(element->level, m);
|
||||||
|
|
||||||
/* Skip if element is entry point */
|
/* Check if any neighbors point to deleted values */
|
||||||
if (entryPoint != NULL && element->blkno == entryPoint->blkno && element->offno == entryPoint->offno)
|
if (!NeedsUpdated(vacuumstate, element))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
/* Refresh entry point for each element */
|
||||||
|
entryPoint = HnswGetEntryPoint(index);
|
||||||
|
|
||||||
|
/* Special case for entry point */
|
||||||
|
if (element->blkno == entryPoint->blkno && element->offno == entryPoint->offno)
|
||||||
|
{
|
||||||
|
if (BlockNumberIsValid(vacuumstate->highestPoint.blkno))
|
||||||
|
{
|
||||||
|
/* Already updated */
|
||||||
|
if (vacuumstate->highestPoint.blkno == element->blkno && vacuumstate->highestPoint.offno == element->offno)
|
||||||
|
return;
|
||||||
|
|
||||||
|
entryPoint = &vacuumstate->highestPoint;
|
||||||
|
|
||||||
|
/* Reset neighbors from previous update */
|
||||||
|
entryPoint->neighbors = NULL;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
entryPoint = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
/* Init fields */
|
/* Init fields */
|
||||||
HnswInitNeighbors(element, m);
|
HnswInitNeighbors(element, m);
|
||||||
element->heaptids = NIL;
|
element->heaptids = NIL;
|
||||||
|
|
||||||
/* Add element to graph, skipping itself */
|
/* Add element to graph, skipping itself */
|
||||||
HnswInsertElement(element, entryPoint, index, procinfo, collation, m, efConstruction, true);
|
HnswInsertElement(element, entryPoint, index, procinfo, collation, m, efConstruction, NULL, true);
|
||||||
|
|
||||||
/* Update neighbor tuple */
|
/* Update neighbor tuple */
|
||||||
/* Do this before getting page to minimize locking */
|
/* Do this before getting page to minimize locking */
|
||||||
@@ -232,9 +243,6 @@ RepairGraphElement(HnswVacuumState * vacuumstate, HnswElement element, HnswEleme
|
|||||||
MarkBufferDirty(buf);
|
MarkBufferDirty(buf);
|
||||||
GenericXLogFinish(state);
|
GenericXLogFinish(state);
|
||||||
UnlockReleaseBuffer(buf);
|
UnlockReleaseBuffer(buf);
|
||||||
|
|
||||||
/* Update neighbors */
|
|
||||||
HnswUpdateNeighborPages(index, procinfo, collation, element, m, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -248,35 +256,16 @@ RepairGraphEntryPoint(HnswVacuumState * vacuumstate)
|
|||||||
HnswElement entryPoint;
|
HnswElement entryPoint;
|
||||||
MemoryContext oldCtx = MemoryContextSwitchTo(vacuumstate->tmpCtx);
|
MemoryContext oldCtx = MemoryContextSwitchTo(vacuumstate->tmpCtx);
|
||||||
|
|
||||||
if (!BlockNumberIsValid(highestPoint->blkno))
|
/* Repair graph for highest non-entry point */
|
||||||
highestPoint = NULL;
|
/* This may not be the highest with new inserts, but should be fine */
|
||||||
|
if (BlockNumberIsValid(highestPoint->blkno))
|
||||||
/*
|
|
||||||
* Repair graph for highest non-entry point. Highest point may be outdated
|
|
||||||
* due to inserts that happen during and after RemoveHeapTids.
|
|
||||||
*/
|
|
||||||
if (highestPoint != NULL)
|
|
||||||
{
|
{
|
||||||
/* Get a shared lock */
|
|
||||||
LockPage(index, HNSW_UPDATE_LOCK, ShareLock);
|
|
||||||
|
|
||||||
/* Load element */
|
|
||||||
HnswLoadElement(highestPoint, NULL, NULL, index, vacuumstate->procinfo, vacuumstate->collation, true);
|
HnswLoadElement(highestPoint, NULL, NULL, index, vacuumstate->procinfo, vacuumstate->collation, true);
|
||||||
|
RepairGraphElement(vacuumstate, highestPoint);
|
||||||
/* Repair if needed */
|
|
||||||
if (NeedsUpdated(vacuumstate, highestPoint))
|
|
||||||
RepairGraphElement(vacuumstate, highestPoint, HnswGetEntryPoint(index));
|
|
||||||
|
|
||||||
/* Release lock */
|
|
||||||
UnlockPage(index, HNSW_UPDATE_LOCK, ShareLock);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Prevent concurrent inserts when possibly updating entry point */
|
/* See if entry point needs updated */
|
||||||
LockPage(index, HNSW_UPDATE_LOCK, ExclusiveLock);
|
|
||||||
|
|
||||||
/* Get latest entry point */
|
|
||||||
entryPoint = HnswGetEntryPoint(index);
|
entryPoint = HnswGetEntryPoint(index);
|
||||||
|
|
||||||
if (entryPoint != NULL)
|
if (entryPoint != NULL)
|
||||||
{
|
{
|
||||||
ItemPointerData epData;
|
ItemPointerData epData;
|
||||||
@@ -284,37 +273,15 @@ RepairGraphEntryPoint(HnswVacuumState * vacuumstate)
|
|||||||
ItemPointerSet(&epData, entryPoint->blkno, entryPoint->offno);
|
ItemPointerSet(&epData, entryPoint->blkno, entryPoint->offno);
|
||||||
|
|
||||||
if (DeletedContains(vacuumstate->deleted, &epData))
|
if (DeletedContains(vacuumstate->deleted, &epData))
|
||||||
{
|
HnswUpdateMetaPage(index, true, highestPoint, InvalidBlockNumber, MAIN_FORKNUM);
|
||||||
/*
|
|
||||||
* Replace the entry point with the highest point. If highest
|
|
||||||
* point is outdated and empty, the entry point will be empty
|
|
||||||
* until an element is repaired.
|
|
||||||
*/
|
|
||||||
HnswUpdateMetaPage(index, HNSW_UPDATE_ENTRY_ALWAYS, highestPoint, InvalidBlockNumber, MAIN_FORKNUM);
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
/*
|
/* Highest point will be used to repair */
|
||||||
* Repair the entry point with the highest point. If highest point
|
|
||||||
* is outdated, this can remove connections at higher levels in
|
|
||||||
* the graph until they are repaired, but this should be fine.
|
|
||||||
*/
|
|
||||||
HnswLoadElement(entryPoint, NULL, NULL, index, vacuumstate->procinfo, vacuumstate->collation, true);
|
HnswLoadElement(entryPoint, NULL, NULL, index, vacuumstate->procinfo, vacuumstate->collation, true);
|
||||||
|
RepairGraphElement(vacuumstate, entryPoint);
|
||||||
if (NeedsUpdated(vacuumstate, entryPoint))
|
|
||||||
{
|
|
||||||
/* Reset neighbors from previous update */
|
|
||||||
if (highestPoint != NULL)
|
|
||||||
highestPoint->neighbors = NULL;
|
|
||||||
|
|
||||||
RepairGraphElement(vacuumstate, entryPoint, highestPoint);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Release lock */
|
|
||||||
UnlockPage(index, HNSW_UPDATE_LOCK, ExclusiveLock);
|
|
||||||
|
|
||||||
/* Reset memory context */
|
/* Reset memory context */
|
||||||
MemoryContextSwitchTo(oldCtx);
|
MemoryContextSwitchTo(oldCtx);
|
||||||
MemoryContextReset(vacuumstate->tmpCtx);
|
MemoryContextReset(vacuumstate->tmpCtx);
|
||||||
@@ -330,11 +297,6 @@ RepairGraph(HnswVacuumState * vacuumstate)
|
|||||||
BufferAccessStrategy bas = vacuumstate->bas;
|
BufferAccessStrategy bas = vacuumstate->bas;
|
||||||
BlockNumber blkno = HNSW_HEAD_BLKNO;
|
BlockNumber blkno = HNSW_HEAD_BLKNO;
|
||||||
|
|
||||||
/* Wait for inserts to complete */
|
|
||||||
LockPage(index, HNSW_UPDATE_LOCK, ExclusiveLock);
|
|
||||||
UnlockPage(index, HNSW_UPDATE_LOCK, ExclusiveLock);
|
|
||||||
|
|
||||||
/* Repair entry point first */
|
|
||||||
RepairGraphEntryPoint(vacuumstate);
|
RepairGraphEntryPoint(vacuumstate);
|
||||||
|
|
||||||
while (BlockNumberIsValid(blkno))
|
while (BlockNumberIsValid(blkno))
|
||||||
@@ -371,8 +333,14 @@ RepairGraph(HnswVacuumState * vacuumstate)
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
/* Create an element */
|
/* Create an element */
|
||||||
element = HnswInitElementFromBlock(blkno, offno);
|
element = palloc(sizeof(HnswElementData));
|
||||||
HnswLoadElementFromTuple(element, etup, false, true);
|
element->neighborPage = ItemPointerGetBlockNumber(&etup->neighbortid);
|
||||||
|
element->neighborOffno = ItemPointerGetOffsetNumber(&etup->neighbortid);
|
||||||
|
element->level = etup->level;
|
||||||
|
element->blkno = blkno;
|
||||||
|
element->offno = offno;
|
||||||
|
element->vec = palloc(VECTOR_SIZE(etup->vec.dim));
|
||||||
|
memcpy(element->vec, &etup->vec, VECTOR_SIZE(etup->vec.dim));
|
||||||
|
|
||||||
elements = lappend(elements, element);
|
elements = lappend(elements, element);
|
||||||
}
|
}
|
||||||
@@ -383,48 +351,7 @@ RepairGraph(HnswVacuumState * vacuumstate)
|
|||||||
|
|
||||||
/* Update neighbor pages */
|
/* Update neighbor pages */
|
||||||
foreach(lc2, elements)
|
foreach(lc2, elements)
|
||||||
{
|
RepairGraphElement(vacuumstate, (HnswElement) lfirst(lc2));
|
||||||
HnswElement element = (HnswElement) lfirst(lc2);
|
|
||||||
HnswElement entryPoint;
|
|
||||||
LOCKMODE lockmode = ShareLock;
|
|
||||||
|
|
||||||
/* Check if any neighbors point to deleted values */
|
|
||||||
if (!NeedsUpdated(vacuumstate, element))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
/* Get a shared lock */
|
|
||||||
LockPage(index, HNSW_UPDATE_LOCK, lockmode);
|
|
||||||
|
|
||||||
/* Refresh entry point for each element */
|
|
||||||
entryPoint = HnswGetEntryPoint(index);
|
|
||||||
|
|
||||||
/* Prevent concurrent inserts when likely updating entry point */
|
|
||||||
if (entryPoint == NULL || element->level > entryPoint->level)
|
|
||||||
{
|
|
||||||
/* Release shared lock */
|
|
||||||
UnlockPage(index, HNSW_UPDATE_LOCK, lockmode);
|
|
||||||
|
|
||||||
/* Get exclusive lock */
|
|
||||||
lockmode = ExclusiveLock;
|
|
||||||
LockPage(index, HNSW_UPDATE_LOCK, lockmode);
|
|
||||||
|
|
||||||
/* Get latest entry point after lock is acquired */
|
|
||||||
entryPoint = HnswGetEntryPoint(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Repair connections */
|
|
||||||
RepairGraphElement(vacuumstate, element, entryPoint);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Update metapage if needed. Should only happen if entry point
|
|
||||||
* was replaced and highest point was outdated.
|
|
||||||
*/
|
|
||||||
if (entryPoint == NULL || element->level > entryPoint->level)
|
|
||||||
HnswUpdateMetaPage(index, HNSW_UPDATE_ENTRY_GREATER, element, InvalidBlockNumber, MAIN_FORKNUM);
|
|
||||||
|
|
||||||
/* Release lock */
|
|
||||||
UnlockPage(index, HNSW_UPDATE_LOCK, lockmode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reset memory context */
|
/* Reset memory context */
|
||||||
MemoryContextSwitchTo(oldCtx);
|
MemoryContextSwitchTo(oldCtx);
|
||||||
@@ -442,10 +369,7 @@ MarkDeleted(HnswVacuumState * vacuumstate)
|
|||||||
BlockNumber insertPage = InvalidBlockNumber;
|
BlockNumber insertPage = InvalidBlockNumber;
|
||||||
Relation index = vacuumstate->index;
|
Relation index = vacuumstate->index;
|
||||||
BufferAccessStrategy bas = vacuumstate->bas;
|
BufferAccessStrategy bas = vacuumstate->bas;
|
||||||
|
IndexBulkDeleteResult *stats = vacuumstate->stats;
|
||||||
/* Wait for selects to complete */
|
|
||||||
LockPage(index, HNSW_SCAN_LOCK, ExclusiveLock);
|
|
||||||
UnlockPage(index, HNSW_SCAN_LOCK, ExclusiveLock);
|
|
||||||
|
|
||||||
while (BlockNumberIsValid(blkno))
|
while (BlockNumberIsValid(blkno))
|
||||||
{
|
{
|
||||||
@@ -489,17 +413,17 @@ MarkDeleted(HnswVacuumState * vacuumstate)
|
|||||||
|
|
||||||
/* Skip deleted tuples */
|
/* Skip deleted tuples */
|
||||||
if (etup->deleted)
|
if (etup->deleted)
|
||||||
{
|
|
||||||
/* Set to first free page */
|
|
||||||
if (!BlockNumberIsValid(insertPage))
|
|
||||||
insertPage = blkno;
|
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
|
|
||||||
/* Skip live tuples */
|
/* Skip live tuples */
|
||||||
if (ItemPointerIsValid(&etup->heaptids[0]))
|
if (ItemPointerIsValid(&etup->heaptids[0]))
|
||||||
|
{
|
||||||
|
stats->num_index_tuples++;
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Update stats */
|
||||||
|
stats->tuples_removed++;
|
||||||
|
|
||||||
/* Calculate sizes */
|
/* Calculate sizes */
|
||||||
etupSize = HNSW_ELEMENT_TUPLE_SIZE(etup->vec.dim);
|
etupSize = HNSW_ELEMENT_TUPLE_SIZE(etup->vec.dim);
|
||||||
@@ -562,8 +486,7 @@ MarkDeleted(HnswVacuumState * vacuumstate)
|
|||||||
UnlockReleaseBuffer(buf);
|
UnlockReleaseBuffer(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Update insert page last, after everything has been marked as deleted */
|
HnswUpdateMetaPage(index, false, NULL, insertPage, MAIN_FORKNUM);
|
||||||
HnswUpdateMetaPage(index, 0, NULL, insertPage, MAIN_FORKNUM);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -611,67 +534,6 @@ FreeVacuumState(HnswVacuumState * vacuumstate)
|
|||||||
MemoryContextDelete(vacuumstate->tmpCtx);
|
MemoryContextDelete(vacuumstate->tmpCtx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Print graph
|
|
||||||
*/
|
|
||||||
#ifdef HNSW_DEBUG
|
|
||||||
static void
|
|
||||||
PrintGraph(HnswVacuumState * vacuumstate)
|
|
||||||
{
|
|
||||||
BlockNumber blkno = HNSW_HEAD_BLKNO;
|
|
||||||
Relation index = vacuumstate->index;
|
|
||||||
|
|
||||||
while (BlockNumberIsValid(blkno))
|
|
||||||
{
|
|
||||||
Buffer buf;
|
|
||||||
Page page;
|
|
||||||
OffsetNumber offno;
|
|
||||||
OffsetNumber maxoffno;
|
|
||||||
|
|
||||||
buf = ReadBuffer(index, blkno);
|
|
||||||
LockBuffer(buf, BUFFER_LOCK_SHARE);
|
|
||||||
page = BufferGetPage(buf);
|
|
||||||
maxoffno = PageGetMaxOffsetNumber(page);
|
|
||||||
|
|
||||||
for (offno = FirstOffsetNumber; offno <= maxoffno; offno = OffsetNumberNext(offno))
|
|
||||||
{
|
|
||||||
HnswElementTuple etup = (HnswElementTuple) PageGetItem(page, PageGetItemId(page, offno));
|
|
||||||
HnswElement element;
|
|
||||||
|
|
||||||
/* Skip neighbor tuples */
|
|
||||||
if (!HnswIsElementTuple(etup))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
/* Skip deleted tuples */
|
|
||||||
if (etup->deleted)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
element = HnswInitElementFromBlock(blkno, offno);
|
|
||||||
HnswLoadElementFromTuple(element, etup, false, true);
|
|
||||||
HnswLoadNeighbors(element, index);
|
|
||||||
|
|
||||||
elog(INFO, "element (%d,%d)", element->blkno, element->offno);
|
|
||||||
|
|
||||||
for (int lc = element->level; lc >= 0; lc--)
|
|
||||||
{
|
|
||||||
HnswNeighborArray *neighbors = &element->neighbors[lc];
|
|
||||||
|
|
||||||
for (int i = 0; i < neighbors->length; i++)
|
|
||||||
{
|
|
||||||
HnswElement e = neighbors->items[i].element;
|
|
||||||
|
|
||||||
elog(INFO, "%d: (%d,%d)", lc, e->blkno, e->offno);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
blkno = HnswPageGetOpaque(page)->nextblkno;
|
|
||||||
|
|
||||||
UnlockReleaseBuffer(buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Bulk delete tuples from the index
|
* Bulk delete tuples from the index
|
||||||
*/
|
*/
|
||||||
@@ -692,10 +554,6 @@ hnswbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
|
|||||||
/* Pass 3: Mark as deleted */
|
/* Pass 3: Mark as deleted */
|
||||||
MarkDeleted(&vacuumstate);
|
MarkDeleted(&vacuumstate);
|
||||||
|
|
||||||
#ifdef HNSW_DEBUG
|
|
||||||
PrintGraph(&vacuumstate);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
FreeVacuumState(&vacuumstate);
|
FreeVacuumState(&vacuumstate);
|
||||||
|
|
||||||
return vacuumstate.stats;
|
return vacuumstate.stats;
|
||||||
|
|||||||
@@ -75,11 +75,7 @@ AddSample(Datum *values, IvfflatBuildState * buildstate)
|
|||||||
* Normalize with KMEANS_NORM_PROC since spherical distance function
|
* Normalize with KMEANS_NORM_PROC since spherical distance function
|
||||||
* expects unit vectors
|
* expects unit vectors
|
||||||
*/
|
*/
|
||||||
if (buildstate->kmeansnormprocinfo != NULL)
|
IvfflatNormValue(buildstate->kmeansnormprocinfo, buildstate->kmeansnormalizeprocinfo, buildstate->collation, &value, buildstate->normvec);
|
||||||
{
|
|
||||||
if (!IvfflatNormValue(buildstate->kmeansnormprocinfo, buildstate->collation, &value, buildstate->normvec))
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (samples->length < targsamples)
|
if (samples->length < targsamples)
|
||||||
{
|
{
|
||||||
@@ -176,11 +172,7 @@ AddTupleToSort(Relation index, ItemPointer tid, Datum *values, IvfflatBuildState
|
|||||||
Datum value = PointerGetDatum(PG_DETOAST_DATUM(values[0]));
|
Datum value = PointerGetDatum(PG_DETOAST_DATUM(values[0]));
|
||||||
|
|
||||||
/* Normalize if needed */
|
/* Normalize if needed */
|
||||||
if (buildstate->normprocinfo != NULL)
|
IvfflatNormValue(buildstate->normprocinfo, buildstate->normalizeprocinfo, buildstate->collation, &value, buildstate->normvec);
|
||||||
{
|
|
||||||
if (!IvfflatNormValue(buildstate->normprocinfo, buildstate->collation, &value, buildstate->normvec))
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Find the list that minimizes the distance */
|
/* Find the list that minimizes the distance */
|
||||||
for (int i = 0; i < centers->length; i++)
|
for (int i = 0; i < centers->length; i++)
|
||||||
@@ -368,6 +360,8 @@ InitBuildState(IvfflatBuildState * buildstate, Relation heap, Relation index, In
|
|||||||
buildstate->procinfo = index_getprocinfo(index, 1, IVFFLAT_DISTANCE_PROC);
|
buildstate->procinfo = index_getprocinfo(index, 1, IVFFLAT_DISTANCE_PROC);
|
||||||
buildstate->normprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_NORM_PROC);
|
buildstate->normprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_NORM_PROC);
|
||||||
buildstate->kmeansnormprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_KMEANS_NORM_PROC);
|
buildstate->kmeansnormprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_KMEANS_NORM_PROC);
|
||||||
|
buildstate->normalizeprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_NORMALIZE_PROC);
|
||||||
|
buildstate->kmeansnormalizeprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_KMEANS_NORMALIZE_PROC);
|
||||||
buildstate->collation = index->rd_indcollation[0];
|
buildstate->collation = index->rd_indcollation[0];
|
||||||
|
|
||||||
/* Require more than one dimension for spherical k-means */
|
/* Require more than one dimension for spherical k-means */
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ IvfflatInit(void)
|
|||||||
{
|
{
|
||||||
ivfflat_relopt_kind = add_reloption_kind();
|
ivfflat_relopt_kind = add_reloption_kind();
|
||||||
add_int_reloption(ivfflat_relopt_kind, "lists", "Number of inverted lists",
|
add_int_reloption(ivfflat_relopt_kind, "lists", "Number of inverted lists",
|
||||||
IVFFLAT_DEFAULT_LISTS, IVFFLAT_MIN_LISTS, IVFFLAT_MAX_LISTS
|
IVFFLAT_DEFAULT_LISTS, 1, IVFFLAT_MAX_LISTS
|
||||||
#if PG_VERSION_NUM >= 130000
|
#if PG_VERSION_NUM >= 130000
|
||||||
,AccessExclusiveLock
|
,AccessExclusiveLock
|
||||||
#endif
|
#endif
|
||||||
@@ -32,7 +32,7 @@ IvfflatInit(void)
|
|||||||
|
|
||||||
DefineCustomIntVariable("ivfflat.probes", "Sets the number of probes",
|
DefineCustomIntVariable("ivfflat.probes", "Sets the number of probes",
|
||||||
"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);
|
1, 1, IVFFLAT_MAX_LISTS, PGC_USERSET, 0, NULL, NULL, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -194,7 +194,7 @@ ivfflathandler(PG_FUNCTION_ARGS)
|
|||||||
IndexAmRoutine *amroutine = makeNode(IndexAmRoutine);
|
IndexAmRoutine *amroutine = makeNode(IndexAmRoutine);
|
||||||
|
|
||||||
amroutine->amstrategies = 0;
|
amroutine->amstrategies = 0;
|
||||||
amroutine->amsupport = 4;
|
amroutine->amsupport = 6;
|
||||||
#if PG_VERSION_NUM >= 130000
|
#if PG_VERSION_NUM >= 130000
|
||||||
amroutine->amoptsprocnum = 0;
|
amroutine->amoptsprocnum = 0;
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -31,6 +31,8 @@
|
|||||||
#define IVFFLAT_NORM_PROC 2
|
#define IVFFLAT_NORM_PROC 2
|
||||||
#define IVFFLAT_KMEANS_DISTANCE_PROC 3
|
#define IVFFLAT_KMEANS_DISTANCE_PROC 3
|
||||||
#define IVFFLAT_KMEANS_NORM_PROC 4
|
#define IVFFLAT_KMEANS_NORM_PROC 4
|
||||||
|
#define IVFFLAT_NORMALIZE_PROC 5
|
||||||
|
#define IVFFLAT_KMEANS_NORMALIZE_PROC 6
|
||||||
|
|
||||||
#define IVFFLAT_VERSION 1
|
#define IVFFLAT_VERSION 1
|
||||||
#define IVFFLAT_MAGIC_NUMBER 0x14FF1A7
|
#define IVFFLAT_MAGIC_NUMBER 0x14FF1A7
|
||||||
@@ -40,11 +42,8 @@
|
|||||||
#define IVFFLAT_METAPAGE_BLKNO 0
|
#define IVFFLAT_METAPAGE_BLKNO 0
|
||||||
#define IVFFLAT_HEAD_BLKNO 1 /* first list page */
|
#define IVFFLAT_HEAD_BLKNO 1 /* first list page */
|
||||||
|
|
||||||
/* IVFFlat parameters */
|
|
||||||
#define IVFFLAT_DEFAULT_LISTS 100
|
#define IVFFLAT_DEFAULT_LISTS 100
|
||||||
#define IVFFLAT_MIN_LISTS 1
|
|
||||||
#define IVFFLAT_MAX_LISTS 32768
|
#define IVFFLAT_MAX_LISTS 32768
|
||||||
#define IVFFLAT_DEFAULT_PROBES 1
|
|
||||||
|
|
||||||
/* Build phases */
|
/* Build phases */
|
||||||
/* PROGRESS_CREATEIDX_SUBPHASE_INITIALIZE is 1 */
|
/* PROGRESS_CREATEIDX_SUBPHASE_INITIALIZE is 1 */
|
||||||
@@ -175,6 +174,8 @@ typedef struct IvfflatBuildState
|
|||||||
FmgrInfo *procinfo;
|
FmgrInfo *procinfo;
|
||||||
FmgrInfo *normprocinfo;
|
FmgrInfo *normprocinfo;
|
||||||
FmgrInfo *kmeansnormprocinfo;
|
FmgrInfo *kmeansnormprocinfo;
|
||||||
|
FmgrInfo *normalizeprocinfo;
|
||||||
|
FmgrInfo *kmeansnormalizeprocinfo;
|
||||||
Oid collation;
|
Oid collation;
|
||||||
|
|
||||||
/* Variables */
|
/* Variables */
|
||||||
@@ -256,6 +257,7 @@ typedef struct IvfflatScanOpaqueData
|
|||||||
/* Support functions */
|
/* Support functions */
|
||||||
FmgrInfo *procinfo;
|
FmgrInfo *procinfo;
|
||||||
FmgrInfo *normprocinfo;
|
FmgrInfo *normprocinfo;
|
||||||
|
FmgrInfo *normalizeprocinfo;
|
||||||
Oid collation;
|
Oid collation;
|
||||||
|
|
||||||
/* Lists */
|
/* Lists */
|
||||||
@@ -276,7 +278,7 @@ void VectorArrayFree(VectorArray arr);
|
|||||||
void PrintVectorArray(char *msg, VectorArray arr);
|
void PrintVectorArray(char *msg, VectorArray arr);
|
||||||
void IvfflatKmeans(Relation index, VectorArray samples, VectorArray centers);
|
void IvfflatKmeans(Relation index, VectorArray samples, VectorArray centers);
|
||||||
FmgrInfo *IvfflatOptionalProcInfo(Relation rel, uint16 procnum);
|
FmgrInfo *IvfflatOptionalProcInfo(Relation rel, uint16 procnum);
|
||||||
bool IvfflatNormValue(FmgrInfo *procinfo, Oid collation, Datum *value, Vector * result);
|
void IvfflatNormValue(FmgrInfo *procinfo, FmgrInfo *normalizeprocinfo, Oid collation, Datum *value, Vector * result);
|
||||||
int IvfflatGetLists(Relation index);
|
int IvfflatGetLists(Relation index);
|
||||||
void IvfflatUpdateList(Relation index, ListInfo listInfo, BlockNumber insertPage, BlockNumber originalInsertPage, BlockNumber startPage, ForkNumber forkNum);
|
void IvfflatUpdateList(Relation index, ListInfo listInfo, BlockNumber insertPage, BlockNumber originalInsertPage, BlockNumber startPage, ForkNumber forkNum);
|
||||||
void IvfflatCommitBuffer(Buffer buf, GenericXLogState *state);
|
void IvfflatCommitBuffer(Buffer buf, GenericXLogState *state);
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ InsertTuple(Relation rel, Datum *values, bool *isnull, ItemPointer heap_tid, Rel
|
|||||||
IndexTuple itup;
|
IndexTuple itup;
|
||||||
Datum value;
|
Datum value;
|
||||||
FmgrInfo *normprocinfo;
|
FmgrInfo *normprocinfo;
|
||||||
|
FmgrInfo *normalizeprocinfo;
|
||||||
Buffer buf;
|
Buffer buf;
|
||||||
Page page;
|
Page page;
|
||||||
GenericXLogState *state;
|
GenericXLogState *state;
|
||||||
@@ -81,11 +82,8 @@ InsertTuple(Relation rel, Datum *values, bool *isnull, ItemPointer heap_tid, Rel
|
|||||||
|
|
||||||
/* Normalize if needed */
|
/* Normalize if needed */
|
||||||
normprocinfo = IvfflatOptionalProcInfo(rel, IVFFLAT_NORM_PROC);
|
normprocinfo = IvfflatOptionalProcInfo(rel, IVFFLAT_NORM_PROC);
|
||||||
if (normprocinfo != NULL)
|
normalizeprocinfo = IvfflatOptionalProcInfo(rel, IVFFLAT_NORMALIZE_PROC);
|
||||||
{
|
IvfflatNormValue(normprocinfo, normalizeprocinfo, rel->rd_indcollation[0], &value, NULL);
|
||||||
if (!IvfflatNormValue(normprocinfo, rel->rd_indcollation[0], &value, NULL))
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Find the insert page - sets the page and list info */
|
/* Find the insert page - sets the page and list info */
|
||||||
FindInsertPage(rel, values, &insertPage, &listInfo);
|
FindInsertPage(rel, values, &insertPage, &listInfo);
|
||||||
|
|||||||
@@ -232,6 +232,7 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys)
|
|||||||
/* Set support functions */
|
/* Set support functions */
|
||||||
so->procinfo = index_getprocinfo(index, 1, IVFFLAT_DISTANCE_PROC);
|
so->procinfo = index_getprocinfo(index, 1, IVFFLAT_DISTANCE_PROC);
|
||||||
so->normprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_NORM_PROC);
|
so->normprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_NORM_PROC);
|
||||||
|
so->normalizeprocinfo = IvfflatOptionalProcInfo(index, IVFFLAT_NORMALIZE_PROC);
|
||||||
so->collation = index->rd_indcollation[0];
|
so->collation = index->rd_indcollation[0];
|
||||||
|
|
||||||
/* Create tuple description for sorting */
|
/* Create tuple description for sorting */
|
||||||
@@ -319,8 +320,7 @@ ivfflatgettuple(IndexScanDesc scan, ScanDirection dir)
|
|||||||
Assert(!VARATT_IS_EXTENDED(DatumGetPointer(value)));
|
Assert(!VARATT_IS_EXTENDED(DatumGetPointer(value)));
|
||||||
|
|
||||||
/* Fine if normalization fails */
|
/* Fine if normalization fails */
|
||||||
if (so->normprocinfo != NULL)
|
IvfflatNormValue(so->normprocinfo, so->normalizeprocinfo, so->collation, &value, NULL);
|
||||||
IvfflatNormValue(so->normprocinfo, so->collation, &value, NULL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IvfflatBench("GetScanLists", GetScanLists(scan, value));
|
IvfflatBench("GetScanLists", GetScanLists(scan, value));
|
||||||
|
|||||||
@@ -66,17 +66,26 @@ IvfflatOptionalProcInfo(Relation rel, uint16 procnum)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Divide by the norm
|
* Normalize a vector
|
||||||
*
|
|
||||||
* Returns false if value should not be indexed
|
|
||||||
*
|
*
|
||||||
* The caller needs to free the pointer stored in value
|
* The caller needs to free the pointer stored in value
|
||||||
* if it's different than the original value
|
* if it's different than the original value
|
||||||
*/
|
*/
|
||||||
bool
|
void
|
||||||
IvfflatNormValue(FmgrInfo *procinfo, Oid collation, Datum *value, Vector * result)
|
IvfflatNormValue(FmgrInfo *procinfo, FmgrInfo *normalizeprocinfo, Oid collation, Datum *value, Vector * result)
|
||||||
{
|
{
|
||||||
double norm = DatumGetFloat8(FunctionCall1Coll(procinfo, collation, *value));
|
double norm;
|
||||||
|
|
||||||
|
if (normalizeprocinfo != NULL)
|
||||||
|
{
|
||||||
|
*value = FunctionCall1Coll(normalizeprocinfo, collation, *value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (procinfo == NULL)
|
||||||
|
return;
|
||||||
|
|
||||||
|
norm = DatumGetFloat8(FunctionCall1Coll(procinfo, collation, *value));
|
||||||
|
|
||||||
if (norm > 0)
|
if (norm > 0)
|
||||||
{
|
{
|
||||||
@@ -89,11 +98,7 @@ IvfflatNormValue(FmgrInfo *procinfo, Oid collation, Datum *value, Vector * resul
|
|||||||
result->x[i] = v->x[i] / norm;
|
result->x[i] = v->x[i] / norm;
|
||||||
|
|
||||||
*value = PointerGetDatum(result);
|
*value = PointerGetDatum(result);
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
70
src/vector.c
70
src/vector.c
@@ -15,10 +15,6 @@
|
|||||||
#include "utils/numeric.h"
|
#include "utils/numeric.h"
|
||||||
#include "vector.h"
|
#include "vector.h"
|
||||||
|
|
||||||
#if PG_VERSION_NUM >= 160000
|
|
||||||
#include "varatt.h"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if PG_VERSION_NUM >= 120000
|
#if PG_VERSION_NUM >= 120000
|
||||||
#include "common/shortest_dec.h"
|
#include "common/shortest_dec.h"
|
||||||
#include "utils/float.h"
|
#include "utils/float.h"
|
||||||
@@ -39,7 +35,6 @@ PG_MODULE_MAGIC;
|
|||||||
/*
|
/*
|
||||||
* Initialize index options and variables
|
* Initialize index options and variables
|
||||||
*/
|
*/
|
||||||
PGDLLEXPORT void _PG_init(void);
|
|
||||||
void
|
void
|
||||||
_PG_init(void)
|
_PG_init(void)
|
||||||
{
|
{
|
||||||
@@ -105,23 +100,6 @@ CheckElement(float value)
|
|||||||
errmsg("infinite value not allowed in vector")));
|
errmsg("infinite value not allowed in vector")));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Allocate and initialize a new vector
|
|
||||||
*/
|
|
||||||
Vector *
|
|
||||||
InitVector(int dim)
|
|
||||||
{
|
|
||||||
Vector *result;
|
|
||||||
int size;
|
|
||||||
|
|
||||||
size = VECTOR_SIZE(dim);
|
|
||||||
result = (Vector *) palloc0(size);
|
|
||||||
SET_VARSIZE(result, size);
|
|
||||||
result->dim = dim;
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Check for whitespace, since array_isspace() is static
|
* Check for whitespace, since array_isspace() is static
|
||||||
*/
|
*/
|
||||||
@@ -755,16 +733,62 @@ vector_dims(PG_FUNCTION_ARGS)
|
|||||||
PGDLLEXPORT PG_FUNCTION_INFO_V1(vector_norm);
|
PGDLLEXPORT PG_FUNCTION_INFO_V1(vector_norm);
|
||||||
Datum
|
Datum
|
||||||
vector_norm(PG_FUNCTION_ARGS)
|
vector_norm(PG_FUNCTION_ARGS)
|
||||||
|
{
|
||||||
|
Vector *a = PG_GETARG_VECTOR_P(0);
|
||||||
|
float *ax = a->x;
|
||||||
|
float norm = 0.0;
|
||||||
|
|
||||||
|
/* Auto-vectorized */
|
||||||
|
for (int i = 0; i < a->dim; i++)
|
||||||
|
norm += ax[i] * ax[i];
|
||||||
|
|
||||||
|
PG_RETURN_FLOAT8(sqrt((double) norm));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Normalize a vector with the L2 norm
|
||||||
|
*/
|
||||||
|
PGDLLEXPORT PG_FUNCTION_INFO_V1(normalize_l2);
|
||||||
|
Datum
|
||||||
|
normalize_l2(PG_FUNCTION_ARGS)
|
||||||
{
|
{
|
||||||
Vector *a = PG_GETARG_VECTOR_P(0);
|
Vector *a = PG_GETARG_VECTOR_P(0);
|
||||||
float *ax = a->x;
|
float *ax = a->x;
|
||||||
double norm = 0.0;
|
double norm = 0.0;
|
||||||
|
#ifdef _MSC_VER
|
||||||
|
/* Fix precision on Windows */
|
||||||
|
double normf;
|
||||||
|
#else
|
||||||
|
float normf;
|
||||||
|
#endif
|
||||||
|
Vector *result;
|
||||||
|
float *rx;
|
||||||
|
|
||||||
|
result = InitVector(a->dim);
|
||||||
|
rx = result->x;
|
||||||
|
|
||||||
/* Auto-vectorized */
|
/* Auto-vectorized */
|
||||||
for (int i = 0; i < a->dim; i++)
|
for (int i = 0; i < a->dim; i++)
|
||||||
norm += (double) ax[i] * (double) ax[i];
|
norm += (double) ax[i] * (double) ax[i];
|
||||||
|
|
||||||
PG_RETURN_FLOAT8(sqrt(norm));
|
norm = sqrt(norm);
|
||||||
|
normf = norm;
|
||||||
|
|
||||||
|
if (normf > 0)
|
||||||
|
{
|
||||||
|
/* Auto-vectorized */
|
||||||
|
for (int i = 0, imax = a->dim; i < imax; i++)
|
||||||
|
rx[i] = ax[i] / normf;
|
||||||
|
|
||||||
|
/* Check for overflow */
|
||||||
|
for (int i = 0, imax = a->dim; i < imax; i++)
|
||||||
|
{
|
||||||
|
if (isinf(rx[i]))
|
||||||
|
float_overflow_error();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PG_RETURN_POINTER(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
27
src/vector.h
27
src/vector.h
@@ -1,6 +1,12 @@
|
|||||||
#ifndef VECTOR_H
|
#ifndef VECTOR_H
|
||||||
#define VECTOR_H
|
#define VECTOR_H
|
||||||
|
|
||||||
|
#include "postgres.h"
|
||||||
|
|
||||||
|
#if PG_VERSION_NUM >= 160000
|
||||||
|
#include "varatt.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#define VECTOR_MAX_DIM 16000
|
#define VECTOR_MAX_DIM 16000
|
||||||
|
|
||||||
#define VECTOR_SIZE(_dim) (offsetof(Vector, x) + sizeof(float)*(_dim))
|
#define VECTOR_SIZE(_dim) (offsetof(Vector, x) + sizeof(float)*(_dim))
|
||||||
@@ -8,6 +14,9 @@
|
|||||||
#define PG_GETARG_VECTOR_P(x) DatumGetVector(PG_GETARG_DATUM(x))
|
#define PG_GETARG_VECTOR_P(x) DatumGetVector(PG_GETARG_DATUM(x))
|
||||||
#define PG_RETURN_VECTOR_P(x) PG_RETURN_POINTER(x)
|
#define PG_RETURN_VECTOR_P(x) PG_RETURN_POINTER(x)
|
||||||
|
|
||||||
|
/* Exported functions */
|
||||||
|
PGDLLEXPORT void _PG_init(void);
|
||||||
|
|
||||||
typedef struct Vector
|
typedef struct Vector
|
||||||
{
|
{
|
||||||
int32 vl_len_; /* varlena header (do not touch directly!) */
|
int32 vl_len_; /* varlena header (do not touch directly!) */
|
||||||
@@ -16,8 +25,24 @@ typedef struct Vector
|
|||||||
float x[FLEXIBLE_ARRAY_MEMBER];
|
float x[FLEXIBLE_ARRAY_MEMBER];
|
||||||
} Vector;
|
} Vector;
|
||||||
|
|
||||||
Vector *InitVector(int dim);
|
|
||||||
void PrintVector(char *msg, Vector * vector);
|
void PrintVector(char *msg, Vector * vector);
|
||||||
int vector_cmp_internal(Vector * a, Vector * b);
|
int vector_cmp_internal(Vector * a, Vector * b);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Allocate and initialize a new vector
|
||||||
|
*/
|
||||||
|
static inline Vector *
|
||||||
|
InitVector(int dim)
|
||||||
|
{
|
||||||
|
Vector *result;
|
||||||
|
int size;
|
||||||
|
|
||||||
|
size = VECTOR_SIZE(dim);
|
||||||
|
result = (Vector *) palloc0(size);
|
||||||
|
SET_VARSIZE(result, size);
|
||||||
|
result->dim = dim;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -48,10 +48,34 @@ SELECT vector_norm('[0,1]');
|
|||||||
1
|
1
|
||||||
(1 row)
|
(1 row)
|
||||||
|
|
||||||
SELECT vector_norm('[3e37,4e37]')::real;
|
SELECT normalize_l2('[3,4]');
|
||||||
vector_norm
|
normalize_l2
|
||||||
-------------
|
--------------
|
||||||
5e+37
|
[0.6,0.8]
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
SELECT normalize_l2('[3,0]');
|
||||||
|
normalize_l2
|
||||||
|
--------------
|
||||||
|
[1,0]
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
SELECT normalize_l2('[0,0.1]');
|
||||||
|
normalize_l2
|
||||||
|
--------------
|
||||||
|
[0,1]
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
SELECT normalize_l2('[0,0]');
|
||||||
|
normalize_l2
|
||||||
|
--------------
|
||||||
|
[0,0]
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
SELECT normalize_l2('[3e38]');
|
||||||
|
normalize_l2
|
||||||
|
--------------
|
||||||
|
[1]
|
||||||
(1 row)
|
(1 row)
|
||||||
|
|
||||||
SELECT l2_distance('[0,0]', '[3,4]');
|
SELECT l2_distance('[0,0]', '[3,4]');
|
||||||
|
|||||||
@@ -9,18 +9,19 @@ SELECT * FROM t ORDER BY val <=> '[3,3,3]';
|
|||||||
[1,1,1]
|
[1,1,1]
|
||||||
[1,2,3]
|
[1,2,3]
|
||||||
[1,2,4]
|
[1,2,4]
|
||||||
(3 rows)
|
[0,0,0]
|
||||||
|
(4 rows)
|
||||||
|
|
||||||
SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <=> '[0,0,0]') t2;
|
SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <=> '[0,0,0]') t2;
|
||||||
count
|
count
|
||||||
-------
|
-------
|
||||||
3
|
4
|
||||||
(1 row)
|
(1 row)
|
||||||
|
|
||||||
SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <=> (SELECT NULL::vector)) t2;
|
SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <=> (SELECT NULL::vector)) t2;
|
||||||
count
|
count
|
||||||
-------
|
-------
|
||||||
3
|
4
|
||||||
(1 row)
|
(1 row)
|
||||||
|
|
||||||
DROP TABLE t;
|
DROP TABLE t;
|
||||||
|
|||||||
@@ -27,10 +27,4 @@ SELECT COUNT(*) FROM t;
|
|||||||
5
|
5
|
||||||
(1 row)
|
(1 row)
|
||||||
|
|
||||||
TRUNCATE t;
|
|
||||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
|
||||||
val
|
|
||||||
-----
|
|
||||||
(0 rows)
|
|
||||||
|
|
||||||
DROP TABLE t;
|
DROP TABLE t;
|
||||||
|
|||||||
@@ -1,26 +1,25 @@
|
|||||||
|
SET enable_seqscan = off;
|
||||||
CREATE TABLE t (val vector(3));
|
CREATE TABLE t (val vector(3));
|
||||||
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (m = 1);
|
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (m = 3);
|
||||||
ERROR: value 1 out of bounds for option "m"
|
ERROR: value 3 out of bounds for option "m"
|
||||||
DETAIL: Valid values are between "2" and "100".
|
DETAIL: Valid values are between "4" and "100".
|
||||||
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (m = 101);
|
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (m = 101);
|
||||||
ERROR: value 101 out of bounds for option "m"
|
ERROR: value 101 out of bounds for option "m"
|
||||||
DETAIL: Valid values are between "2" and "100".
|
DETAIL: Valid values are between "4" and "100".
|
||||||
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (ef_construction = 3);
|
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (ef_construction = 9);
|
||||||
ERROR: value 3 out of bounds for option "ef_construction"
|
ERROR: value 9 out of bounds for option "ef_construction"
|
||||||
DETAIL: Valid values are between "4" and "1000".
|
DETAIL: Valid values are between "10" and "1000".
|
||||||
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (ef_construction = 1001);
|
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (ef_construction = 1001);
|
||||||
ERROR: value 1001 out of bounds for option "ef_construction"
|
ERROR: value 1001 out of bounds for option "ef_construction"
|
||||||
DETAIL: Valid values are between "4" and "1000".
|
DETAIL: Valid values are between "10" and "1000".
|
||||||
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (m = 16, ef_construction = 31);
|
|
||||||
ERROR: ef_construction must be greater than or equal to 2 * m
|
|
||||||
SHOW hnsw.ef_search;
|
SHOW hnsw.ef_search;
|
||||||
hnsw.ef_search
|
hnsw.ef_search
|
||||||
----------------
|
----------------
|
||||||
40
|
40
|
||||||
(1 row)
|
(1 row)
|
||||||
|
|
||||||
SET hnsw.ef_search = 0;
|
SET hnsw.ef_search = 9;
|
||||||
ERROR: 0 is outside the valid range for parameter "hnsw.ef_search" (1 .. 1000)
|
ERROR: 9 is outside the valid range for parameter "hnsw.ef_search" (10 .. 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" (10 .. 1000)
|
||||||
DROP TABLE t;
|
DROP TABLE t;
|
||||||
|
|||||||
@@ -9,18 +9,19 @@ SELECT * FROM t ORDER BY val <=> '[3,3,3]';
|
|||||||
[1,1,1]
|
[1,1,1]
|
||||||
[1,2,3]
|
[1,2,3]
|
||||||
[1,2,4]
|
[1,2,4]
|
||||||
(3 rows)
|
[0,0,0]
|
||||||
|
(4 rows)
|
||||||
|
|
||||||
SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <=> '[0,0,0]') t2;
|
SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <=> '[0,0,0]') t2;
|
||||||
count
|
count
|
||||||
-------
|
-------
|
||||||
3
|
4
|
||||||
(1 row)
|
(1 row)
|
||||||
|
|
||||||
SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <=> (SELECT NULL::vector)) t2;
|
SELECT COUNT(*) FROM (SELECT * FROM t ORDER BY val <=> (SELECT NULL::vector)) t2;
|
||||||
count
|
count
|
||||||
-------
|
-------
|
||||||
3
|
4
|
||||||
(1 row)
|
(1 row)
|
||||||
|
|
||||||
DROP TABLE t;
|
DROP TABLE t;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
SET enable_seqscan = off;
|
SET enable_seqscan = off;
|
||||||
CREATE TABLE t (val vector(3));
|
CREATE TABLE t (val vector(3));
|
||||||
INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL);
|
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 = 1);
|
CREATE INDEX ON t USING ivfflat (val) WITH (lists = 1);
|
||||||
INSERT INTO t (val) VALUES ('[1,2,4]');
|
INSERT INTO t (val) VALUES ('[1,2,4]');
|
||||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
||||||
val
|
val
|
||||||
@@ -27,13 +27,4 @@ SELECT COUNT(*) FROM t;
|
|||||||
5
|
5
|
||||||
(1 row)
|
(1 row)
|
||||||
|
|
||||||
TRUNCATE t;
|
|
||||||
NOTICE: ivfflat index created with little data
|
|
||||||
DETAIL: This will cause low recall.
|
|
||||||
HINT: Drop the index until the table has more data.
|
|
||||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
|
||||||
val
|
|
||||||
-----
|
|
||||||
(0 rows)
|
|
||||||
|
|
||||||
DROP TABLE t;
|
DROP TABLE t;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
SET enable_seqscan = off;
|
||||||
CREATE TABLE t (val vector(3));
|
CREATE TABLE t (val vector(3));
|
||||||
CREATE INDEX ON t USING ivfflat (val vector_l2_ops) WITH (lists = 0);
|
CREATE INDEX ON t USING ivfflat (val) WITH (lists = 0);
|
||||||
ERROR: value 0 out of bounds for option "lists"
|
ERROR: value 0 out of bounds for option "lists"
|
||||||
DETAIL: Valid values are between "1" and "32768".
|
DETAIL: Valid values are between "1" and "32768".
|
||||||
CREATE INDEX ON t USING ivfflat (val vector_l2_ops) WITH (lists = 32769);
|
CREATE INDEX ON t USING ivfflat (val) WITH (lists = 32769);
|
||||||
ERROR: value 32769 out of bounds for option "lists"
|
ERROR: value 32769 out of bounds for option "lists"
|
||||||
DETAIL: Valid values are between "1" and "32768".
|
DETAIL: Valid values are between "1" and "32768".
|
||||||
SHOW ivfflat.probes;
|
SHOW ivfflat.probes;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
SET enable_seqscan = off;
|
SET enable_seqscan = off;
|
||||||
CREATE UNLOGGED TABLE t (val vector(3));
|
CREATE UNLOGGED TABLE t (val vector(3));
|
||||||
INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL);
|
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 = 1);
|
CREATE INDEX ON t USING ivfflat (val) WITH (lists = 1);
|
||||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
||||||
val
|
val
|
||||||
---------
|
---------
|
||||||
|
|||||||
@@ -11,7 +11,12 @@ SELECT vector_dims('[1,2,3]');
|
|||||||
SELECT round(vector_norm('[1,1]')::numeric, 5);
|
SELECT round(vector_norm('[1,1]')::numeric, 5);
|
||||||
SELECT vector_norm('[3,4]');
|
SELECT vector_norm('[3,4]');
|
||||||
SELECT vector_norm('[0,1]');
|
SELECT vector_norm('[0,1]');
|
||||||
SELECT vector_norm('[3e37,4e37]')::real;
|
|
||||||
|
SELECT normalize_l2('[3,4]');
|
||||||
|
SELECT normalize_l2('[3,0]');
|
||||||
|
SELECT normalize_l2('[0,0.1]');
|
||||||
|
SELECT normalize_l2('[0,0]');
|
||||||
|
SELECT normalize_l2('[3e38]');
|
||||||
|
|
||||||
SELECT l2_distance('[0,0]', '[3,4]');
|
SELECT l2_distance('[0,0]', '[3,4]');
|
||||||
SELECT l2_distance('[0,0]', '[0,1]');
|
SELECT l2_distance('[0,0]', '[0,1]');
|
||||||
|
|||||||
@@ -10,7 +10,4 @@ SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
|||||||
SELECT * FROM t ORDER BY val <-> (SELECT NULL::vector);
|
SELECT * FROM t ORDER BY val <-> (SELECT NULL::vector);
|
||||||
SELECT COUNT(*) FROM t;
|
SELECT COUNT(*) FROM t;
|
||||||
|
|
||||||
TRUNCATE t;
|
|
||||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
|
||||||
|
|
||||||
DROP TABLE t;
|
DROP TABLE t;
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
|
SET enable_seqscan = off;
|
||||||
|
|
||||||
CREATE TABLE t (val vector(3));
|
CREATE TABLE t (val vector(3));
|
||||||
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (m = 1);
|
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (m = 3);
|
||||||
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (m = 101);
|
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (m = 101);
|
||||||
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (ef_construction = 3);
|
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (ef_construction = 9);
|
||||||
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (ef_construction = 1001);
|
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (ef_construction = 1001);
|
||||||
CREATE INDEX ON t USING hnsw (val vector_l2_ops) WITH (m = 16, ef_construction = 31);
|
|
||||||
|
|
||||||
SHOW hnsw.ef_search;
|
SHOW hnsw.ef_search;
|
||||||
|
|
||||||
SET hnsw.ef_search = 0;
|
SET hnsw.ef_search = 9;
|
||||||
SET hnsw.ef_search = 1001;
|
SET hnsw.ef_search = 1001;
|
||||||
|
|
||||||
DROP TABLE t;
|
DROP TABLE t;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ SET enable_seqscan = off;
|
|||||||
|
|
||||||
CREATE TABLE t (val vector(3));
|
CREATE TABLE t (val vector(3));
|
||||||
INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL);
|
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 = 1);
|
CREATE INDEX ON t USING ivfflat (val) WITH (lists = 1);
|
||||||
|
|
||||||
INSERT INTO t (val) VALUES ('[1,2,4]');
|
INSERT INTO t (val) VALUES ('[1,2,4]');
|
||||||
|
|
||||||
@@ -10,7 +10,4 @@ SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
|||||||
SELECT * FROM t ORDER BY val <-> (SELECT NULL::vector);
|
SELECT * FROM t ORDER BY val <-> (SELECT NULL::vector);
|
||||||
SELECT COUNT(*) FROM t;
|
SELECT COUNT(*) FROM t;
|
||||||
|
|
||||||
TRUNCATE t;
|
|
||||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
|
||||||
|
|
||||||
DROP TABLE t;
|
DROP TABLE t;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
SET enable_seqscan = off;
|
||||||
|
|
||||||
CREATE TABLE t (val vector(3));
|
CREATE TABLE t (val vector(3));
|
||||||
CREATE INDEX ON t USING ivfflat (val vector_l2_ops) WITH (lists = 0);
|
CREATE INDEX ON t USING ivfflat (val) WITH (lists = 0);
|
||||||
CREATE INDEX ON t USING ivfflat (val vector_l2_ops) WITH (lists = 32769);
|
CREATE INDEX ON t USING ivfflat (val) WITH (lists = 32769);
|
||||||
|
|
||||||
SHOW ivfflat.probes;
|
SHOW ivfflat.probes;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ SET enable_seqscan = off;
|
|||||||
|
|
||||||
CREATE UNLOGGED TABLE t (val vector(3));
|
CREATE UNLOGGED TABLE t (val vector(3));
|
||||||
INSERT INTO t (val) VALUES ('[0,0,0]'), ('[1,2,3]'), ('[1,1,1]'), (NULL);
|
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 = 1);
|
CREATE INDEX ON t USING ivfflat (val) WITH (lists = 1);
|
||||||
|
|
||||||
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
SELECT * FROM t ORDER BY val <-> '[3,3,3]';
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,7 @@ sub test_index_replay
|
|||||||
or die "Timed out while waiting for replica 1 to catch up";
|
or die "Timed out while waiting for replica 1 to catch up";
|
||||||
|
|
||||||
my @r = ();
|
my @r = ();
|
||||||
for (1 .. $dim)
|
for (1 .. $dim) {
|
||||||
{
|
|
||||||
push(@r, rand());
|
push(@r, rand());
|
||||||
}
|
}
|
||||||
my $sql = join(",", @r);
|
my $sql = join(",", @r);
|
||||||
@@ -53,13 +52,11 @@ my $array_sql = join(",", ('random()') x $dim);
|
|||||||
# Initialize primary node
|
# Initialize primary node
|
||||||
$node_primary = get_new_node('primary');
|
$node_primary = get_new_node('primary');
|
||||||
$node_primary->init(allows_streaming => 1);
|
$node_primary->init(allows_streaming => 1);
|
||||||
if ($dim > 32)
|
if ($dim > 32) {
|
||||||
{
|
|
||||||
# TODO use wal_keep_segments for Postgres < 13
|
# TODO use wal_keep_segments for Postgres < 13
|
||||||
$node_primary->append_conf('postgresql.conf', qq(wal_keep_size = 1GB));
|
$node_primary->append_conf('postgresql.conf', qq(wal_keep_size = 1GB));
|
||||||
}
|
}
|
||||||
if ($dim > 1500)
|
if ($dim > 1500) {
|
||||||
{
|
|
||||||
$node_primary->append_conf('postgresql.conf', qq(maintenance_work_mem = 128MB));
|
$node_primary->append_conf('postgresql.conf', qq(maintenance_work_mem = 128MB));
|
||||||
}
|
}
|
||||||
$node_primary->start;
|
$node_primary->start;
|
||||||
@@ -70,7 +67,8 @@ $node_primary->backup($backup_name);
|
|||||||
|
|
||||||
# Create streaming replica linking to primary
|
# Create streaming replica linking to primary
|
||||||
$node_replica = get_new_node('replica');
|
$node_replica = get_new_node('replica');
|
||||||
$node_replica->init_from_backup($node_primary, $backup_name, has_streaming => 1);
|
$node_replica->init_from_backup($node_primary, $backup_name,
|
||||||
|
has_streaming => 1);
|
||||||
$node_replica->start;
|
$node_replica->start;
|
||||||
|
|
||||||
# Create ivfflat index on primary
|
# Create ivfflat index on primary
|
||||||
@@ -79,7 +77,7 @@ $node_primary->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector($dim));
|
|||||||
$node_primary->safe_psql("postgres",
|
$node_primary->safe_psql("postgres",
|
||||||
"INSERT INTO tst SELECT i % 10, 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_primary->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v vector_l2_ops);");
|
$node_primary->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v);");
|
||||||
|
|
||||||
# Test that queries give same result
|
# Test that queries give same result
|
||||||
test_index_replay('initial');
|
test_index_replay('initial');
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ use Test::More;
|
|||||||
my $dim = 3;
|
my $dim = 3;
|
||||||
|
|
||||||
my @r = ();
|
my @r = ();
|
||||||
for (1 .. $dim)
|
for (1 .. $dim) {
|
||||||
{
|
|
||||||
my $v = int(rand(1000)) + 1;
|
my $v = int(rand(1000)) + 1;
|
||||||
push(@r, "i % $v");
|
push(@r, "i % $v");
|
||||||
}
|
}
|
||||||
@@ -25,7 +24,7 @@ $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 % 10, 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);");
|
||||||
|
|
||||||
# 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');");
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ sub test_recall
|
|||||||
));
|
));
|
||||||
like($explain, qr/Index Scan using idx on tst/);
|
like($explain, qr/Index Scan using idx on tst/);
|
||||||
|
|
||||||
for my $i (0 .. $#queries)
|
for my $i (0 .. $#queries) {
|
||||||
{
|
|
||||||
my $actual = $node->safe_psql("postgres", qq(
|
my $actual = $node->safe_psql("postgres", qq(
|
||||||
SET enable_seqscan = off;
|
SET enable_seqscan = off;
|
||||||
SET ivfflat.probes = $probes;
|
SET ivfflat.probes = $probes;
|
||||||
@@ -34,10 +33,8 @@ sub test_recall
|
|||||||
|
|
||||||
my @expected_ids = split("\n", $expected[$i]);
|
my @expected_ids = split("\n", $expected[$i]);
|
||||||
|
|
||||||
foreach (@expected_ids)
|
foreach (@expected_ids) {
|
||||||
{
|
if (exists($actual_set{$_})) {
|
||||||
if (exists($actual_set{$_}))
|
|
||||||
{
|
|
||||||
$correct++;
|
$correct++;
|
||||||
}
|
}
|
||||||
$total++;
|
$total++;
|
||||||
@@ -60,8 +57,7 @@ $node->safe_psql("postgres",
|
|||||||
);
|
);
|
||||||
|
|
||||||
# Generate queries
|
# Generate queries
|
||||||
for (1 .. 20)
|
for (1..20) {
|
||||||
{
|
|
||||||
my $r1 = rand();
|
my $r1 = rand();
|
||||||
my $r2 = rand();
|
my $r2 = rand();
|
||||||
my $r3 = rand();
|
my $r3 = rand();
|
||||||
@@ -70,21 +66,26 @@ for (1 .. 20)
|
|||||||
|
|
||||||
# Check each index type
|
# Check each index type
|
||||||
my @operators = ("<->", "<#>", "<=>");
|
my @operators = ("<->", "<#>", "<=>");
|
||||||
my @opclasses = ("vector_l2_ops", "vector_ip_ops", "vector_cosine_ops");
|
|
||||||
|
|
||||||
for my $i (0 .. $#operators)
|
foreach (@operators) {
|
||||||
{
|
my $operator = $_;
|
||||||
my $operator = $operators[$i];
|
|
||||||
my $opclass = $opclasses[$i];
|
|
||||||
|
|
||||||
# Get exact results
|
# Get exact results
|
||||||
@expected = ();
|
@expected = ();
|
||||||
foreach (@queries)
|
foreach (@queries) {
|
||||||
{
|
|
||||||
my $res = $node->safe_psql("postgres", "SELECT i FROM tst ORDER BY v $operator '$_' LIMIT $limit;");
|
my $res = $node->safe_psql("postgres", "SELECT i FROM tst ORDER BY v $operator '$_' LIMIT $limit;");
|
||||||
push(@expected, $res);
|
push(@expected, $res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
my $opclass;
|
||||||
|
if ($operator eq "<->") {
|
||||||
|
$opclass = "vector_l2_ops";
|
||||||
|
} elsif ($operator eq "<#>") {
|
||||||
|
$opclass = "vector_ip_ops";
|
||||||
|
} else {
|
||||||
|
$opclass = "vector_cosine_ops";
|
||||||
|
}
|
||||||
|
|
||||||
# Build index serially
|
# Build index serially
|
||||||
$node->safe_psql("postgres", qq(
|
$node->safe_psql("postgres", qq(
|
||||||
SET max_parallel_maintenance_workers = 0;
|
SET max_parallel_maintenance_workers = 0;
|
||||||
@@ -92,14 +93,13 @@ for my $i (0 .. $#operators)
|
|||||||
));
|
));
|
||||||
|
|
||||||
# Test approximate results
|
# Test approximate results
|
||||||
if ($operator ne "<#>")
|
if ($operator ne "<#>") {
|
||||||
{
|
|
||||||
# TODO fix test
|
# TODO fix test
|
||||||
test_recall(1, 0.71, $operator);
|
test_recall(1, 0.75, $operator);
|
||||||
test_recall(10, 0.95, $operator);
|
test_recall(10, 0.95, $operator);
|
||||||
}
|
}
|
||||||
# Account for equal distances
|
# Account for equal distances
|
||||||
test_recall(100, 0.9925, $operator);
|
test_recall(100, 0.995, $operator);
|
||||||
|
|
||||||
$node->safe_psql("postgres", "DROP INDEX idx;");
|
$node->safe_psql("postgres", "DROP INDEX idx;");
|
||||||
|
|
||||||
@@ -113,14 +113,13 @@ for my $i (0 .. $#operators)
|
|||||||
like($stderr, qr/using \d+ parallel workers/);
|
like($stderr, qr/using \d+ parallel workers/);
|
||||||
|
|
||||||
# Test approximate results
|
# Test approximate results
|
||||||
if ($operator ne "<#>")
|
if ($operator ne "<#>") {
|
||||||
{
|
|
||||||
# TODO fix test
|
# TODO fix test
|
||||||
test_recall(1, 0.71, $operator);
|
test_recall(1, 0.75, $operator);
|
||||||
test_recall(10, 0.95, $operator);
|
test_recall(10, 0.95, $operator);
|
||||||
}
|
}
|
||||||
# Account for equal distances
|
# Account for equal distances
|
||||||
test_recall(100, 0.9925, $operator);
|
test_recall(100, 0.995, $operator);
|
||||||
|
|
||||||
$node->safe_psql("postgres", "DROP INDEX idx;");
|
$node->safe_psql("postgres", "DROP INDEX idx;");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ sub test_centers
|
|||||||
{
|
{
|
||||||
my ($lists, $min) = @_;
|
my ($lists, $min) = @_;
|
||||||
|
|
||||||
my ($ret, $stdout, $stderr) = $node->psql("postgres", "CREATE INDEX ON tst USING ivfflat (v vector_l2_ops) WITH (lists = $lists);");
|
my ($ret, $stdout, $stderr) = $node->psql("postgres", "CREATE INDEX ON tst USING ivfflat (v) WITH (lists = $lists);");
|
||||||
is($ret, 0, $stderr);
|
is($ret, 0, $stderr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,21 +18,24 @@ $node->safe_psql("postgres",
|
|||||||
|
|
||||||
# Check each index type
|
# Check each index type
|
||||||
my @operators = ("<->", "<#>", "<=>");
|
my @operators = ("<->", "<#>", "<=>");
|
||||||
my @opclasses = ("vector_l2_ops", "vector_ip_ops", "vector_cosine_ops");
|
foreach (@operators) {
|
||||||
|
my $operator = $_;
|
||||||
for my $i (0 .. $#operators)
|
|
||||||
{
|
|
||||||
my $operator = $operators[$i];
|
|
||||||
my $opclass = $opclasses[$i];
|
|
||||||
|
|
||||||
# Add index
|
# Add index
|
||||||
|
my $opclass;
|
||||||
|
if ($operator eq "<->") {
|
||||||
|
$opclass = "vector_l2_ops";
|
||||||
|
} elsif ($operator eq "<#>") {
|
||||||
|
$opclass = "vector_ip_ops";
|
||||||
|
} else {
|
||||||
|
$opclass = "vector_cosine_ops";
|
||||||
|
}
|
||||||
$node->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v $opclass);");
|
$node->safe_psql("postgres", "CREATE INDEX ON tst USING ivfflat (v $opclass);");
|
||||||
|
|
||||||
# Test 100% recall
|
# Test 100% recall
|
||||||
for (1 .. 20)
|
for (1..20) {
|
||||||
{
|
my $i = int(rand() * 100000);
|
||||||
my $id = int(rand() * 100000);
|
my $query = $node->safe_psql("postgres", "SELECT v FROM tst WHERE i = $i;");
|
||||||
my $query = $node->safe_psql("postgres", "SELECT v FROM tst WHERE i = $id;");
|
|
||||||
my $res = $node->safe_psql("postgres", qq(
|
my $res = $node->safe_psql("postgres", qq(
|
||||||
SET enable_seqscan = off;
|
SET enable_seqscan = off;
|
||||||
SELECT v FROM tst ORDER BY v <-> '$query' LIMIT 1;
|
SELECT v FROM tst ORDER BY v <-> '$query' LIMIT 1;
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ $node->safe_psql("postgres",
|
|||||||
"INSERT INTO tst SELECT ARRAY[random(), random(), random()] FROM generate_series(1, 100000) i;"
|
"INSERT INTO tst SELECT ARRAY[random(), random(), random()] FROM generate_series(1, 100000) i;"
|
||||||
);
|
);
|
||||||
|
|
||||||
$node->safe_psql("postgres", "CREATE INDEX lists50 ON tst USING ivfflat (v vector_l2_ops) WITH (lists = 50);");
|
$node->safe_psql("postgres", "CREATE INDEX lists50 ON tst USING ivfflat (v) WITH (lists = 50);");
|
||||||
$node->safe_psql("postgres", "CREATE INDEX lists100 ON tst USING ivfflat (v vector_l2_ops) WITH (lists = 100);");
|
$node->safe_psql("postgres", "CREATE INDEX lists100 ON tst USING ivfflat (v) WITH (lists = 100);");
|
||||||
|
|
||||||
# Test prefers more lists
|
# Test prefers more lists
|
||||||
my $res = $node->safe_psql("postgres", "EXPLAIN SELECT v FROM tst ORDER BY v <-> '[0.5,0.5,0.5]' LIMIT 10;");
|
my $res = $node->safe_psql("postgres", "EXPLAIN SELECT v FROM tst ORDER BY v <-> '[0.5,0.5,0.5]' LIMIT 10;");
|
||||||
@@ -26,7 +26,7 @@ unlike($res, qr/lists50/);
|
|||||||
|
|
||||||
# Test errors with too much memory
|
# Test errors with too much memory
|
||||||
my ($ret, $stdout, $stderr) = $node->psql("postgres",
|
my ($ret, $stdout, $stderr) = $node->psql("postgres",
|
||||||
"CREATE INDEX lists10000 ON tst USING ivfflat (v vector_l2_ops) WITH (lists = 10000);"
|
"CREATE INDEX lists10000 ON tst USING ivfflat (v) WITH (lists = 10000);"
|
||||||
);
|
);
|
||||||
like($stderr, qr/memory required is/);
|
like($stderr, qr/memory required is/);
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ $node->safe_psql("postgres", "CREATE TABLE tst (v vector($dim));");
|
|||||||
$node->safe_psql("postgres",
|
$node->safe_psql("postgres",
|
||||||
"INSERT INTO tst SELECT ARRAY[$array_sql] FROM generate_series(1, 10000) i;"
|
"INSERT INTO tst SELECT ARRAY[$array_sql] FROM generate_series(1, 10000) 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);");
|
||||||
|
|
||||||
$node->pgbench(
|
$node->pgbench(
|
||||||
"--no-vacuum --client=5 --transactions=100",
|
"--no-vacuum --client=5 --transactions=100",
|
||||||
@@ -28,7 +28,7 @@ $node->pgbench(
|
|||||||
[qr{^$}],
|
[qr{^$}],
|
||||||
"concurrent INSERTs",
|
"concurrent INSERTs",
|
||||||
{
|
{
|
||||||
"007_ivfflat_inserts" => "INSERT INTO tst SELECT ARRAY[$array_sql] FROM generate_series(1, 10) i;"
|
"007_inserts" => "INSERT INTO tst SELECT ARRAY[$array_sql] FROM generate_series(1, 10) i;"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,7 @@ sub test_aggregate
|
|||||||
|
|
||||||
# Test matches real for avg
|
# Test matches real for avg
|
||||||
# Cannot test sum since sum(real) varies between calls
|
# Cannot test sum since sum(real) varies between calls
|
||||||
if ($agg eq 'avg')
|
if ($agg eq 'avg') {
|
||||||
{
|
|
||||||
my $r1 = $node->safe_psql("postgres", "SELECT $agg(r1)::float4 FROM tst;");
|
my $r1 = $node->safe_psql("postgres", "SELECT $agg(r1)::float4 FROM tst;");
|
||||||
my $r2 = $node->safe_psql("postgres", "SELECT $agg(r2)::float4 FROM tst;");
|
my $r2 = $node->safe_psql("postgres", "SELECT $agg(r2)::float4 FROM tst;");
|
||||||
my $r3 = $node->safe_psql("postgres", "SELECT $agg(r3)::float4 FROM tst;");
|
my $r3 = $node->safe_psql("postgres", "SELECT $agg(r3)::float4 FROM tst;");
|
||||||
|
|||||||
@@ -26,8 +26,7 @@ sub test_index_replay
|
|||||||
or die "Timed out while waiting for replica 1 to catch up";
|
or die "Timed out while waiting for replica 1 to catch up";
|
||||||
|
|
||||||
my @r = ();
|
my @r = ();
|
||||||
for (1 .. $dim)
|
for (1 .. $dim) {
|
||||||
{
|
|
||||||
push(@r, rand());
|
push(@r, rand());
|
||||||
}
|
}
|
||||||
my $sql = join(",", @r);
|
my $sql = join(",", @r);
|
||||||
@@ -53,13 +52,11 @@ my $array_sql = join(",", ('random()') x $dim);
|
|||||||
# Initialize primary node
|
# Initialize primary node
|
||||||
$node_primary = get_new_node('primary');
|
$node_primary = get_new_node('primary');
|
||||||
$node_primary->init(allows_streaming => 1);
|
$node_primary->init(allows_streaming => 1);
|
||||||
if ($dim > 32)
|
if ($dim > 32) {
|
||||||
{
|
|
||||||
# TODO use wal_keep_segments for Postgres < 13
|
# TODO use wal_keep_segments for Postgres < 13
|
||||||
$node_primary->append_conf('postgresql.conf', qq(wal_keep_size = 1GB));
|
$node_primary->append_conf('postgresql.conf', qq(wal_keep_size = 1GB));
|
||||||
}
|
}
|
||||||
if ($dim > 1500)
|
if ($dim > 1500) {
|
||||||
{
|
|
||||||
$node_primary->append_conf('postgresql.conf', qq(maintenance_work_mem = 128MB));
|
$node_primary->append_conf('postgresql.conf', qq(maintenance_work_mem = 128MB));
|
||||||
}
|
}
|
||||||
$node_primary->start;
|
$node_primary->start;
|
||||||
@@ -70,7 +67,8 @@ $node_primary->backup($backup_name);
|
|||||||
|
|
||||||
# Create streaming replica linking to primary
|
# Create streaming replica linking to primary
|
||||||
$node_replica = get_new_node('replica');
|
$node_replica = get_new_node('replica');
|
||||||
$node_replica->init_from_backup($node_primary, $backup_name, has_streaming => 1);
|
$node_replica->init_from_backup($node_primary, $backup_name,
|
||||||
|
has_streaming => 1);
|
||||||
$node_replica->start;
|
$node_replica->start;
|
||||||
|
|
||||||
# Create hnsw index on primary
|
# Create hnsw index on primary
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ use Test::More;
|
|||||||
my $dim = 3;
|
my $dim = 3;
|
||||||
|
|
||||||
my @r = ();
|
my @r = ();
|
||||||
for (1 .. $dim)
|
for (1 .. $dim) {
|
||||||
{
|
|
||||||
my $v = int(rand(1000)) + 1;
|
my $v = int(rand(1000)) + 1;
|
||||||
push(@r, "i % $v");
|
push(@r, "i % $v");
|
||||||
}
|
}
|
||||||
@@ -23,7 +22,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, 10000) i;"
|
"INSERT INTO tst SELECT i % 10, ARRAY[$array_sql] FROM generate_series(1, 10000) i;"
|
||||||
);
|
);
|
||||||
$node->safe_psql("postgres", "CREATE INDEX ON tst USING hnsw (v vector_l2_ops);");
|
$node->safe_psql("postgres", "CREATE INDEX ON tst USING hnsw (v vector_l2_ops);");
|
||||||
|
|
||||||
@@ -34,21 +33,11 @@ my $size = $node->safe_psql("postgres", "SELECT pg_total_relation_size('tst_v_id
|
|||||||
$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",
|
$node->safe_psql("postgres",
|
||||||
"INSERT INTO tst SELECT i, ARRAY[$array_sql] FROM generate_series(1, 10000) i;"
|
"INSERT INTO tst SELECT i % 10, ARRAY[$array_sql] FROM generate_series(1, 10000) i;"
|
||||||
);
|
);
|
||||||
|
|
||||||
# Check size
|
# Check size
|
||||||
# May increase some due to different levels
|
|
||||||
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');");
|
||||||
cmp_ok($new_size, "<=", $size * 1.02, "size does not increase too much");
|
cmp_ok($new_size, "<=", $size * 1.01, "size does not increase too much");
|
||||||
|
|
||||||
# Delete all but one
|
|
||||||
$node->safe_psql("postgres", "DELETE FROM tst WHERE i != 123;");
|
|
||||||
$node->safe_psql("postgres", "VACUUM tst;");
|
|
||||||
my $res = $node->safe_psql("postgres", qq(
|
|
||||||
SET enable_seqscan = off;
|
|
||||||
SELECT i FROM tst ORDER BY v <-> '[0,0,0]' LIMIT 10;
|
|
||||||
));
|
|
||||||
is($res, 123);
|
|
||||||
|
|
||||||
done_testing();
|
done_testing();
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ sub test_recall
|
|||||||
));
|
));
|
||||||
like($explain, qr/Index Scan/);
|
like($explain, qr/Index Scan/);
|
||||||
|
|
||||||
for my $i (0 .. $#queries)
|
for my $i (0 .. $#queries) {
|
||||||
{
|
|
||||||
my $actual = $node->safe_psql("postgres", qq(
|
my $actual = $node->safe_psql("postgres", qq(
|
||||||
SET enable_seqscan = off;
|
SET enable_seqscan = off;
|
||||||
SELECT i FROM tst ORDER BY v $operator '$queries[$i]' LIMIT $limit;
|
SELECT i FROM tst ORDER BY v $operator '$queries[$i]' LIMIT $limit;
|
||||||
@@ -32,10 +31,8 @@ sub test_recall
|
|||||||
|
|
||||||
my @expected_ids = split("\n", $expected[$i]);
|
my @expected_ids = split("\n", $expected[$i]);
|
||||||
|
|
||||||
foreach (@expected_ids)
|
foreach (@expected_ids) {
|
||||||
{
|
if (exists($actual_set{$_})) {
|
||||||
if (exists($actual_set{$_}))
|
|
||||||
{
|
|
||||||
$correct++;
|
$correct++;
|
||||||
}
|
}
|
||||||
$total++;
|
$total++;
|
||||||
@@ -58,8 +55,7 @@ $node->safe_psql("postgres",
|
|||||||
);
|
);
|
||||||
|
|
||||||
# Generate queries
|
# Generate queries
|
||||||
for (1 .. 20)
|
for (1..20) {
|
||||||
{
|
|
||||||
my $r1 = rand();
|
my $r1 = rand();
|
||||||
my $r2 = rand();
|
my $r2 = rand();
|
||||||
my $r3 = rand();
|
my $r3 = rand();
|
||||||
@@ -68,26 +64,33 @@ for (1 .. 20)
|
|||||||
|
|
||||||
# Check each index type
|
# Check each index type
|
||||||
my @operators = ("<->", "<#>", "<=>");
|
my @operators = ("<->", "<#>", "<=>");
|
||||||
my @opclasses = ("vector_l2_ops", "vector_ip_ops", "vector_cosine_ops");
|
|
||||||
|
|
||||||
for my $i (0 .. $#operators)
|
foreach (@operators) {
|
||||||
{
|
my $operator = $_;
|
||||||
my $operator = $operators[$i];
|
|
||||||
my $opclass = $opclasses[$i];
|
|
||||||
|
|
||||||
# Get exact results
|
# Get exact results
|
||||||
@expected = ();
|
@expected = ();
|
||||||
foreach (@queries)
|
foreach (@queries) {
|
||||||
{
|
|
||||||
my $res = $node->safe_psql("postgres", "SELECT i FROM tst ORDER BY v $operator '$_' LIMIT $limit;");
|
my $res = $node->safe_psql("postgres", "SELECT i FROM tst ORDER BY v $operator '$_' LIMIT $limit;");
|
||||||
push(@expected, $res);
|
push(@expected, $res);
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add index
|
# Add index
|
||||||
|
my $opclass;
|
||||||
|
if ($operator eq "<->") {
|
||||||
|
$opclass = "vector_l2_ops";
|
||||||
|
} elsif ($operator eq "<#>") {
|
||||||
|
$opclass = "vector_ip_ops";
|
||||||
|
} else {
|
||||||
|
$opclass = "vector_cosine_ops";
|
||||||
|
}
|
||||||
$node->safe_psql("postgres", "CREATE INDEX ON tst USING hnsw (v $opclass);");
|
$node->safe_psql("postgres", "CREATE INDEX ON tst USING hnsw (v $opclass);");
|
||||||
|
|
||||||
my $min = $operator eq "<#>" ? 0.80 : 0.99;
|
if ($operator eq "<#>") {
|
||||||
test_recall($min, $operator);
|
test_recall(0.80, $operator);
|
||||||
|
} else {
|
||||||
|
test_recall(0.99, $operator);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
done_testing();
|
done_testing();
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ sub test_recall
|
|||||||
));
|
));
|
||||||
like($explain, qr/Index Scan/);
|
like($explain, qr/Index Scan/);
|
||||||
|
|
||||||
for my $i (0 .. $#queries)
|
for my $i (0 .. $#queries) {
|
||||||
{
|
|
||||||
my $actual = $node->safe_psql("postgres", qq(
|
my $actual = $node->safe_psql("postgres", qq(
|
||||||
SET enable_seqscan = off;
|
SET enable_seqscan = off;
|
||||||
SELECT i FROM tst ORDER BY v $operator '$queries[$i]' LIMIT $limit;
|
SELECT i FROM tst ORDER BY v $operator '$queries[$i]' LIMIT $limit;
|
||||||
@@ -32,10 +31,8 @@ sub test_recall
|
|||||||
|
|
||||||
my @expected_ids = split("\n", $expected[$i]);
|
my @expected_ids = split("\n", $expected[$i]);
|
||||||
|
|
||||||
foreach (@expected_ids)
|
foreach (@expected_ids) {
|
||||||
{
|
if (exists($actual_set{$_})) {
|
||||||
if (exists($actual_set{$_}))
|
|
||||||
{
|
|
||||||
$correct++;
|
$correct++;
|
||||||
}
|
}
|
||||||
$total++;
|
$total++;
|
||||||
@@ -52,11 +49,10 @@ $node->start;
|
|||||||
|
|
||||||
# Create table
|
# Create table
|
||||||
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
|
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
|
||||||
$node->safe_psql("postgres", "CREATE TABLE tst (i serial, v vector(3));");
|
$node->safe_psql("postgres", "CREATE TABLE tst (i int4, v vector(3));");
|
||||||
|
|
||||||
# Generate queries
|
# Generate queries
|
||||||
for (1 .. 20)
|
for (1..20) {
|
||||||
{
|
|
||||||
my $r1 = rand();
|
my $r1 = rand();
|
||||||
my $r2 = rand();
|
my $r2 = rand();
|
||||||
my $r3 = rand();
|
my $r3 = rand();
|
||||||
@@ -65,32 +61,28 @@ for (1 .. 20)
|
|||||||
|
|
||||||
# Check each index type
|
# Check each index type
|
||||||
my @operators = ("<->", "<#>", "<=>");
|
my @operators = ("<->", "<#>", "<=>");
|
||||||
my @opclasses = ("vector_l2_ops", "vector_ip_ops", "vector_cosine_ops");
|
|
||||||
|
|
||||||
for my $i (0 .. $#operators)
|
foreach (@operators) {
|
||||||
{
|
my $operator = $_;
|
||||||
my $operator = $operators[$i];
|
|
||||||
my $opclass = $opclasses[$i];
|
|
||||||
|
|
||||||
# Add index
|
# Add index
|
||||||
|
my $opclass;
|
||||||
|
if ($operator eq "<->") {
|
||||||
|
$opclass = "vector_l2_ops";
|
||||||
|
} elsif ($operator eq "<#>") {
|
||||||
|
$opclass = "vector_ip_ops";
|
||||||
|
} else {
|
||||||
|
$opclass = "vector_cosine_ops";
|
||||||
|
}
|
||||||
$node->safe_psql("postgres", "CREATE INDEX idx ON tst USING hnsw (v $opclass);");
|
$node->safe_psql("postgres", "CREATE INDEX idx ON tst USING hnsw (v $opclass);");
|
||||||
|
|
||||||
# Use concurrent inserts
|
$node->safe_psql("postgres",
|
||||||
$node->pgbench(
|
"INSERT INTO tst SELECT i, ARRAY[random(), random(), random()] FROM generate_series(1, 10000) i;"
|
||||||
"--no-vacuum --client=10 --transactions=1000",
|
|
||||||
0,
|
|
||||||
[qr{actually processed}],
|
|
||||||
[qr{^$}],
|
|
||||||
"concurrent INSERTs",
|
|
||||||
{
|
|
||||||
"013_hnsw_insert_recall_$opclass" => "INSERT INTO tst (v) VALUES (ARRAY[random(), random(), random()]);"
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
# Get exact results
|
# Get exact results
|
||||||
@expected = ();
|
@expected = ();
|
||||||
foreach (@queries)
|
foreach (@queries) {
|
||||||
{
|
|
||||||
my $res = $node->safe_psql("postgres", qq(
|
my $res = $node->safe_psql("postgres", qq(
|
||||||
SET enable_indexscan = off;
|
SET enable_indexscan = off;
|
||||||
SELECT i FROM tst ORDER BY v $operator '$_' LIMIT $limit;
|
SELECT i FROM tst ORDER BY v $operator '$_' LIMIT $limit;
|
||||||
@@ -98,8 +90,11 @@ for my $i (0 .. $#operators)
|
|||||||
push(@expected, $res);
|
push(@expected, $res);
|
||||||
}
|
}
|
||||||
|
|
||||||
my $min = $operator eq "<#>" ? 0.80 : 0.99;
|
if ($operator eq "<#>") {
|
||||||
test_recall($min, $operator);
|
test_recall(0.80, $operator);
|
||||||
|
} else {
|
||||||
|
test_recall(0.99, $operator);
|
||||||
|
}
|
||||||
|
|
||||||
$node->safe_psql("postgres", "DROP INDEX idx;");
|
$node->safe_psql("postgres", "DROP INDEX idx;");
|
||||||
$node->safe_psql("postgres", "TRUNCATE tst;");
|
$node->safe_psql("postgres", "TRUNCATE tst;");
|
||||||
|
|||||||
@@ -17,8 +17,22 @@ $node->start;
|
|||||||
# Create table and index
|
# Create table and index
|
||||||
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
|
$node->safe_psql("postgres", "CREATE EXTENSION vector;");
|
||||||
$node->safe_psql("postgres", "CREATE TABLE tst (v vector($dim));");
|
$node->safe_psql("postgres", "CREATE TABLE tst (v vector($dim));");
|
||||||
|
$node->safe_psql("postgres",
|
||||||
|
"INSERT INTO tst SELECT ARRAY[$array_sql] FROM generate_series(1, 100) i;"
|
||||||
|
);
|
||||||
$node->safe_psql("postgres", "CREATE INDEX ON tst USING hnsw (v vector_l2_ops);");
|
$node->safe_psql("postgres", "CREATE INDEX ON tst USING hnsw (v vector_l2_ops);");
|
||||||
|
|
||||||
|
$node->pgbench(
|
||||||
|
"--no-vacuum --client=5 --transactions=100",
|
||||||
|
0,
|
||||||
|
[qr{actually processed}],
|
||||||
|
[qr{^$}],
|
||||||
|
"concurrent INSERTs",
|
||||||
|
{
|
||||||
|
"007_inserts" => "INSERT INTO tst SELECT ARRAY[$array_sql] FROM generate_series(1, 10) i;"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
sub idx_scan
|
sub idx_scan
|
||||||
{
|
{
|
||||||
# Stats do not update instantaneously
|
# Stats do not update instantaneously
|
||||||
@@ -27,48 +41,18 @@ sub idx_scan
|
|||||||
$node->safe_psql("postgres", "SELECT idx_scan FROM pg_stat_user_indexes WHERE indexrelid = 'tst_v_idx'::regclass;");
|
$node->safe_psql("postgres", "SELECT idx_scan FROM pg_stat_user_indexes WHERE indexrelid = 'tst_v_idx'::regclass;");
|
||||||
}
|
}
|
||||||
|
|
||||||
for my $i (1 .. 20)
|
my $expected = 100 + 5 * 100 * 10;
|
||||||
{
|
|
||||||
$node->pgbench(
|
|
||||||
"--no-vacuum --client=10 --transactions=1",
|
|
||||||
0,
|
|
||||||
[qr{actually processed}],
|
|
||||||
[qr{^$}],
|
|
||||||
"concurrent INSERTs",
|
|
||||||
{
|
|
||||||
"014_hnsw_inserts_$i" => "INSERT INTO tst VALUES (ARRAY[$array_sql]);"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
my $count = $node->safe_psql("postgres", qq(
|
my $count = $node->safe_psql("postgres", "SELECT COUNT(*) FROM tst;");
|
||||||
SET enable_seqscan = off;
|
is($count, $expected);
|
||||||
SELECT COUNT(*) FROM (SELECT v FROM tst ORDER BY v <-> (SELECT v FROM tst LIMIT 1)) t;
|
is(idx_scan(), 0);
|
||||||
));
|
|
||||||
is($count, 10);
|
|
||||||
|
|
||||||
$node->safe_psql("postgres", "TRUNCATE tst;");
|
$count = $node->safe_psql("postgres", qq(
|
||||||
}
|
|
||||||
|
|
||||||
$node->pgbench(
|
|
||||||
"--no-vacuum --client=20 --transactions=5",
|
|
||||||
0,
|
|
||||||
[qr{actually processed}],
|
|
||||||
[qr{^$}],
|
|
||||||
"concurrent INSERTs",
|
|
||||||
{
|
|
||||||
"014_hnsw_inserts" => "INSERT INTO tst SELECT ARRAY[$array_sql] FROM generate_series(1, 10) i;"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
my $count = $node->safe_psql("postgres", qq(
|
|
||||||
SET enable_seqscan = off;
|
SET enable_seqscan = off;
|
||||||
SET hnsw.ef_search = 1000;
|
SET hnsw.ef_search = 400;
|
||||||
SELECT COUNT(*) FROM (SELECT v FROM tst ORDER BY v <-> (SELECT v FROM tst LIMIT 1)) t;
|
SELECT COUNT(*) FROM (SELECT v FROM tst ORDER BY v <-> (SELECT v FROM tst LIMIT 1)) t;
|
||||||
));
|
));
|
||||||
# Elements may lose all incoming connections with the HNSW algorithm
|
is($count, 400);
|
||||||
# Vacuuming can fix this if one of the elements neighbors is deleted
|
is(idx_scan(), 1);
|
||||||
cmp_ok($count, ">=", 997);
|
|
||||||
|
|
||||||
is(idx_scan(), 21);
|
|
||||||
|
|
||||||
done_testing();
|
done_testing();
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
use strict;
|
|
||||||
use warnings;
|
|
||||||
use PostgresNode;
|
|
||||||
use TestLib;
|
|
||||||
use Test::More;
|
|
||||||
|
|
||||||
# Initialize node
|
|
||||||
my $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 (v vector(3));");
|
|
||||||
|
|
||||||
sub insert_vectors
|
|
||||||
{
|
|
||||||
for my $i (1 .. 20)
|
|
||||||
{
|
|
||||||
$node->safe_psql("postgres", "INSERT INTO tst VALUES ('[1,1,1]');");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sub test_duplicates
|
|
||||||
{
|
|
||||||
my $res = $node->safe_psql("postgres", qq(
|
|
||||||
SET enable_seqscan = off;
|
|
||||||
SET hnsw.ef_search = 1;
|
|
||||||
SELECT COUNT(*) FROM (SELECT * FROM tst ORDER BY v <-> '[1,1,1]') t;
|
|
||||||
));
|
|
||||||
is($res, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test duplicates with build
|
|
||||||
insert_vectors();
|
|
||||||
$node->safe_psql("postgres", "CREATE INDEX idx ON tst USING hnsw (v vector_l2_ops);");
|
|
||||||
test_duplicates();
|
|
||||||
|
|
||||||
# Reset
|
|
||||||
$node->safe_psql("postgres", "TRUNCATE tst;");
|
|
||||||
|
|
||||||
# Test duplicates with inserts
|
|
||||||
insert_vectors();
|
|
||||||
test_duplicates();
|
|
||||||
|
|
||||||
# Test fallback path for inserts
|
|
||||||
$node->pgbench(
|
|
||||||
"--no-vacuum --client=5 --transactions=100",
|
|
||||||
0,
|
|
||||||
[qr{actually processed}],
|
|
||||||
[qr{^$}],
|
|
||||||
"concurrent INSERTs",
|
|
||||||
{
|
|
||||||
"015_hnsw_duplicates" => "INSERT INTO tst VALUES ('[1,1,1]');"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
done_testing();
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
use strict;
|
|
||||||
use warnings;
|
|
||||||
use PostgresNode;
|
|
||||||
use TestLib;
|
|
||||||
use Test::More;
|
|
||||||
|
|
||||||
my $node;
|
|
||||||
my @queries = ();
|
|
||||||
my @expected;
|
|
||||||
my $limit = 20;
|
|
||||||
|
|
||||||
sub test_recall
|
|
||||||
{
|
|
||||||
my ($min, $ef_search, $test_name) = @_;
|
|
||||||
my $correct = 0;
|
|
||||||
my $total = 0;
|
|
||||||
|
|
||||||
my $explain = $node->safe_psql("postgres", qq(
|
|
||||||
SET enable_seqscan = off;
|
|
||||||
SET hnsw.ef_search = $ef_search;
|
|
||||||
EXPLAIN ANALYZE SELECT i FROM tst ORDER BY v <-> '$queries[0]' LIMIT $limit;
|
|
||||||
));
|
|
||||||
like($explain, qr/Index Scan/);
|
|
||||||
|
|
||||||
for my $i (0 .. $#queries)
|
|
||||||
{
|
|
||||||
my $actual = $node->safe_psql("postgres", qq(
|
|
||||||
SET enable_seqscan = off;
|
|
||||||
SET hnsw.ef_search = $ef_search;
|
|
||||||
SELECT i FROM tst ORDER BY v <-> '$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, $test_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
# 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 int4, v vector(3));");
|
|
||||||
$node->safe_psql("postgres", "ALTER TABLE tst SET (autovacuum_enabled = false);");
|
|
||||||
$node->safe_psql("postgres",
|
|
||||||
"INSERT INTO tst SELECT i, ARRAY[random(), random(), random()] FROM generate_series(1, 10000) i;"
|
|
||||||
);
|
|
||||||
|
|
||||||
# Add index
|
|
||||||
$node->safe_psql("postgres", "CREATE INDEX ON tst USING hnsw (v vector_l2_ops) WITH (m = 4, ef_construction = 8);");
|
|
||||||
|
|
||||||
# Delete data
|
|
||||||
$node->safe_psql("postgres", "DELETE FROM tst WHERE i > 2500;");
|
|
||||||
|
|
||||||
# Generate queries
|
|
||||||
for (1 .. 20)
|
|
||||||
{
|
|
||||||
my $r1 = rand();
|
|
||||||
my $r2 = rand();
|
|
||||||
my $r3 = rand();
|
|
||||||
push(@queries, "[$r1,$r2,$r3]");
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get exact results
|
|
||||||
@expected = ();
|
|
||||||
foreach (@queries)
|
|
||||||
{
|
|
||||||
my $res = $node->safe_psql("postgres", qq(
|
|
||||||
SET enable_indexscan = off;
|
|
||||||
SELECT i FROM tst ORDER BY v <-> '$_' LIMIT $limit;
|
|
||||||
));
|
|
||||||
push(@expected, $res);
|
|
||||||
}
|
|
||||||
|
|
||||||
test_recall(0.20, $limit, "before vacuum");
|
|
||||||
test_recall(0.95, 100, "before vacuum");
|
|
||||||
|
|
||||||
# TODO test concurrent inserts with vacuum
|
|
||||||
$node->safe_psql("postgres", "VACUUM tst;");
|
|
||||||
|
|
||||||
test_recall(0.95, $limit, "after vacuum");
|
|
||||||
|
|
||||||
done_testing();
|
|
||||||
Reference in New Issue
Block a user