mirror of
https://github.com/pgvector/pgvector.git
synced 2026-07-03 11:10:56 +08:00
972 lines
26 KiB
C
972 lines
26 KiB
C
#include "postgres.h"
|
|
|
|
#include <math.h>
|
|
|
|
#include "access/parallel.h"
|
|
#include "access/xact.h"
|
|
#include "catalog/index.h"
|
|
#include "hnsw.h"
|
|
#include "miscadmin.h"
|
|
#include "lib/pairingheap.h"
|
|
#include "nodes/pg_list.h"
|
|
#include "storage/bufmgr.h"
|
|
#include "tcop/tcopprot.h"
|
|
#include "utils/datum.h"
|
|
#include "utils/memutils.h"
|
|
|
|
#if PG_VERSION_NUM >= 140000
|
|
#include "utils/backend_progress.h"
|
|
#elif PG_VERSION_NUM >= 120000
|
|
#include "pgstat.h"
|
|
#endif
|
|
|
|
#if PG_VERSION_NUM >= 120000
|
|
#include "access/tableam.h"
|
|
#include "commands/progress.h"
|
|
#else
|
|
#define PROGRESS_CREATEIDX_TUPLES_DONE 0
|
|
#endif
|
|
|
|
#if PG_VERSION_NUM >= 130000
|
|
#define CALLBACK_ITEM_POINTER ItemPointer tid
|
|
#else
|
|
#define CALLBACK_ITEM_POINTER HeapTuple hup
|
|
#endif
|
|
|
|
#if PG_VERSION_NUM >= 120000
|
|
#define UpdateProgress(index, val) pgstat_progress_update_param(index, val)
|
|
#else
|
|
#define UpdateProgress(index, val) ((void)val)
|
|
#endif
|
|
|
|
#if PG_VERSION_NUM >= 140000
|
|
#include "utils/backend_status.h"
|
|
#include "utils/wait_event.h"
|
|
#endif
|
|
|
|
#if PG_VERSION_NUM >= 120000
|
|
#include "access/table.h"
|
|
#include "optimizer/optimizer.h"
|
|
#else
|
|
#include "access/heapam.h"
|
|
#include "optimizer/planner.h"
|
|
#include "pgstat.h"
|
|
#endif
|
|
|
|
#define PARALLEL_KEY_HNSW_SHARED UINT64CONST(0xA000000000000001)
|
|
#define PARALLEL_KEY_QUERY_TEXT UINT64CONST(0xA000000000000002)
|
|
|
|
#define LIST_MAX_LENGTH ((1 << 26) - 1)
|
|
|
|
#if PG_VERSION_NUM < 130000
|
|
#define GENERATIONCHUNK_RAWSIZE (SIZEOF_SIZE_T + SIZEOF_VOID_P * 2)
|
|
#endif
|
|
|
|
/*
|
|
* Create the metapage
|
|
*/
|
|
static void
|
|
CreateMetaPage(HnswBuildState * buildstate)
|
|
{
|
|
Relation index = buildstate->index;
|
|
ForkNumber forkNum = buildstate->forkNum;
|
|
Buffer buf;
|
|
Page page;
|
|
HnswMetaPage metap;
|
|
|
|
buf = HnswNewBuffer(index, forkNum);
|
|
page = BufferGetPage(buf);
|
|
HnswInitPage(buf, page);
|
|
|
|
/* Set metapage data */
|
|
metap = HnswPageGetMeta(page);
|
|
metap->magicNumber = HNSW_MAGIC_NUMBER;
|
|
metap->version = HNSW_VERSION;
|
|
metap->dimensions = buildstate->dimensions;
|
|
metap->m = buildstate->m;
|
|
metap->efConstruction = buildstate->efConstruction;
|
|
metap->entryBlkno = InvalidBlockNumber;
|
|
metap->entryOffno = InvalidOffsetNumber;
|
|
metap->entryLevel = -1;
|
|
metap->insertPage = InvalidBlockNumber;
|
|
((PageHeader) page)->pd_lower =
|
|
((char *) metap + sizeof(HnswMetaPageData)) - (char *) page;
|
|
|
|
MarkBufferDirty(buf);
|
|
UnlockReleaseBuffer(buf);
|
|
}
|
|
|
|
/*
|
|
* Add a new page
|
|
*/
|
|
static void
|
|
HnswBuildAppendPage(Relation index, Buffer *buf, Page *page, ForkNumber forkNum)
|
|
{
|
|
/* Add a new page */
|
|
Buffer newbuf = HnswNewBuffer(index, forkNum);
|
|
|
|
/* Update previous page */
|
|
HnswPageGetOpaque(*page)->nextblkno = BufferGetBlockNumber(newbuf);
|
|
|
|
/* Commit */
|
|
MarkBufferDirty(*buf);
|
|
UnlockReleaseBuffer(*buf);
|
|
|
|
/* Can take a while, so ensure we can interrupt */
|
|
/* Needs to be called when no buffer locks are held */
|
|
LockBuffer(newbuf, BUFFER_LOCK_UNLOCK);
|
|
CHECK_FOR_INTERRUPTS();
|
|
LockBuffer(newbuf, BUFFER_LOCK_EXCLUSIVE);
|
|
|
|
/* Prepare new page */
|
|
*buf = newbuf;
|
|
*page = BufferGetPage(*buf);
|
|
HnswInitPage(*buf, *page);
|
|
}
|
|
|
|
/*
|
|
* Create element pages
|
|
*/
|
|
static void
|
|
CreateElementPages(HnswBuildState * buildstate)
|
|
{
|
|
Relation index = buildstate->index;
|
|
ForkNumber forkNum = buildstate->forkNum;
|
|
Size etupAllocSize;
|
|
Size maxSize;
|
|
HnswElementTuple etup;
|
|
HnswNeighborTuple ntup;
|
|
BlockNumber insertPage;
|
|
Buffer buf;
|
|
Page page;
|
|
ListCell *lc;
|
|
|
|
/* Calculate sizes */
|
|
etupAllocSize = BLCKSZ;
|
|
maxSize = HNSW_MAX_SIZE;
|
|
|
|
/* Allocate once */
|
|
etup = palloc0(etupAllocSize);
|
|
ntup = palloc0(BLCKSZ);
|
|
|
|
/* Prepare first page */
|
|
buf = HnswNewBuffer(index, forkNum);
|
|
page = BufferGetPage(buf);
|
|
HnswInitPage(buf, page);
|
|
|
|
foreach(lc, buildstate->elements)
|
|
{
|
|
HnswElement element = lfirst(lc);
|
|
Size etupSize;
|
|
Size ntupSize;
|
|
Size combinedSize;
|
|
|
|
/* Zero memory for each element */
|
|
MemSet(etup, 0, etupAllocSize);
|
|
|
|
/* Calculate sizes */
|
|
etupSize = HNSW_ELEMENT_TUPLE_SIZE(VARSIZE_ANY(DatumGetPointer(element->value)));
|
|
ntupSize = HNSW_NEIGHBOR_TUPLE_SIZE(element->level, buildstate->m);
|
|
combinedSize = etupSize + ntupSize + sizeof(ItemIdData);
|
|
|
|
/* Initial size check */
|
|
if (etupSize > etupAllocSize)
|
|
elog(ERROR, "index tuple too large");
|
|
|
|
HnswSetElementTuple(etup, element);
|
|
|
|
/* Keep element and neighbors on the same page if possible */
|
|
if (PageGetFreeSpace(page) < etupSize || (combinedSize <= maxSize && PageGetFreeSpace(page) < combinedSize))
|
|
HnswBuildAppendPage(index, &buf, &page, forkNum);
|
|
|
|
/* Calculate offsets */
|
|
element->blkno = BufferGetBlockNumber(buf);
|
|
element->offno = OffsetNumberNext(PageGetMaxOffsetNumber(page));
|
|
if (combinedSize <= maxSize)
|
|
{
|
|
element->neighborPage = element->blkno;
|
|
element->neighborOffno = OffsetNumberNext(element->offno);
|
|
}
|
|
else
|
|
{
|
|
element->neighborPage = element->blkno + 1;
|
|
element->neighborOffno = FirstOffsetNumber;
|
|
}
|
|
|
|
ItemPointerSet(&etup->neighbortid, element->neighborPage, element->neighborOffno);
|
|
|
|
/* Add element */
|
|
if (PageAddItem(page, (Item) etup, etupSize, InvalidOffsetNumber, false, false) != element->offno)
|
|
elog(ERROR, "failed to add index item to \"%s\"", RelationGetRelationName(index));
|
|
|
|
/* Add new page if needed */
|
|
if (PageGetFreeSpace(page) < ntupSize)
|
|
HnswBuildAppendPage(index, &buf, &page, forkNum);
|
|
|
|
/* Add placeholder for neighbors */
|
|
if (PageAddItem(page, (Item) ntup, ntupSize, InvalidOffsetNumber, false, false) != element->neighborOffno)
|
|
elog(ERROR, "failed to add index item to \"%s\"", RelationGetRelationName(index));
|
|
}
|
|
|
|
insertPage = BufferGetBlockNumber(buf);
|
|
|
|
/* Commit */
|
|
MarkBufferDirty(buf);
|
|
UnlockReleaseBuffer(buf);
|
|
|
|
HnswUpdateMetaPage(index, HNSW_UPDATE_ENTRY_ALWAYS, buildstate->entryPoint, insertPage, forkNum, true);
|
|
|
|
pfree(etup);
|
|
pfree(ntup);
|
|
}
|
|
|
|
/*
|
|
* Create neighbor pages
|
|
*/
|
|
static void
|
|
CreateNeighborPages(HnswBuildState * buildstate)
|
|
{
|
|
Relation index = buildstate->index;
|
|
ForkNumber forkNum = buildstate->forkNum;
|
|
int m = buildstate->m;
|
|
ListCell *lc;
|
|
HnswNeighborTuple ntup;
|
|
|
|
/* Allocate once */
|
|
ntup = palloc0(BLCKSZ);
|
|
|
|
foreach(lc, buildstate->elements)
|
|
{
|
|
HnswElement e = lfirst(lc);
|
|
Buffer buf;
|
|
Page page;
|
|
Size ntupSize = HNSW_NEIGHBOR_TUPLE_SIZE(e->level, m);
|
|
|
|
/* Can take a while, so ensure we can interrupt */
|
|
/* Needs to be called when no buffer locks are held */
|
|
CHECK_FOR_INTERRUPTS();
|
|
|
|
buf = ReadBufferExtended(index, forkNum, e->neighborPage, RBM_NORMAL, NULL);
|
|
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
|
|
page = BufferGetPage(buf);
|
|
|
|
HnswSetNeighborTuple(ntup, e, m);
|
|
|
|
if (!PageIndexTupleOverwrite(page, e->neighborOffno, (Item) ntup, ntupSize))
|
|
elog(ERROR, "failed to add index item to \"%s\"", RelationGetRelationName(index));
|
|
|
|
/* Commit */
|
|
MarkBufferDirty(buf);
|
|
UnlockReleaseBuffer(buf);
|
|
}
|
|
|
|
pfree(ntup);
|
|
}
|
|
|
|
/*
|
|
* Show memory usage
|
|
*/
|
|
static void
|
|
ShowMemoryUsage(HnswBuildState * buildstate)
|
|
{
|
|
#ifdef HNSW_MEMORY
|
|
#if PG_VERSION_NUM >= 130000
|
|
elog(INFO, "graph memory: %zu MB, total memory: %zu MB",
|
|
MemoryContextMemAllocated(buildstate->graphCtx, false) / (1024 * 1024),
|
|
MemoryContextMemAllocated(CurrentMemoryContext, true) / (1024 * 1024));
|
|
#else
|
|
MemoryContextStats(CurrentMemoryContext);
|
|
elog(INFO, "estimated memory: %zu MB", buildstate->memoryUsed / (1024 * 1024));
|
|
#endif
|
|
#endif
|
|
}
|
|
|
|
/*
|
|
* Flush pages
|
|
*/
|
|
static void
|
|
FlushPages(HnswBuildState * buildstate)
|
|
{
|
|
ShowMemoryUsage(buildstate);
|
|
|
|
CreateMetaPage(buildstate);
|
|
CreateElementPages(buildstate);
|
|
CreateNeighborPages(buildstate);
|
|
|
|
buildstate->flushed = true;
|
|
MemoryContextReset(buildstate->graphCtx);
|
|
}
|
|
|
|
#if PG_VERSION_NUM < 130000
|
|
/*
|
|
* Get the memory used by an element
|
|
*/
|
|
static long
|
|
HnswElementMemory(HnswElement e, int m)
|
|
{
|
|
long elementSize = sizeof(HnswElementData);
|
|
|
|
elementSize += sizeof(HnswNeighborArray) * (e->level + 1);
|
|
elementSize += sizeof(HnswCandidate) * (m * (e->level + 2));
|
|
elementSize += sizeof(List);
|
|
elementSize += sizeof(ItemPointerData) + SIZEOF_VOID_P;
|
|
elementSize += VARSIZE_ANY(DatumGetPointer(e->value));
|
|
/* Each allocation has a chunk header */
|
|
elementSize += (e->level + 7) * GENERATIONCHUNK_RAWSIZE;
|
|
/* Add an extra 5% for alignment and other overhead */
|
|
return elementSize * 1.05;
|
|
}
|
|
#endif
|
|
|
|
/*
|
|
* Insert tuple into in-memory graph
|
|
*/
|
|
static bool
|
|
InsertTupleInMemory(Relation index, Datum *values, ItemPointer heaptid, HnswBuildState * buildstate)
|
|
{
|
|
FmgrInfo *procinfo = buildstate->procinfo;
|
|
Oid collation = buildstate->collation;
|
|
HnswElement entryPoint = buildstate->entryPoint;
|
|
int efConstruction = buildstate->efConstruction;
|
|
int m = buildstate->m;
|
|
MemoryContext oldCtx;
|
|
HnswElement element;
|
|
HnswElement dup;
|
|
|
|
/* Detoast once for all calls */
|
|
Datum value = PointerGetDatum(PG_DETOAST_DATUM(values[0]));
|
|
|
|
/* Normalize if needed */
|
|
if (buildstate->normprocinfo != NULL)
|
|
{
|
|
if (!HnswNormValue(buildstate->normprocinfo, collation, &value, buildstate->normvec))
|
|
return false;
|
|
}
|
|
|
|
/* Allocate element in graph memory context */
|
|
oldCtx = MemoryContextSwitchTo(buildstate->graphCtx);
|
|
element = HnswInitElement(heaptid, buildstate->m, buildstate->ml, buildstate->maxLevel);
|
|
element->value = datumCopy(value, false, -1);
|
|
MemoryContextSwitchTo(oldCtx);
|
|
|
|
/* Insert element in graph */
|
|
HnswInsertElement(element, entryPoint, NULL, procinfo, collation, m, efConstruction, 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 */
|
|
if (dup == NULL && (entryPoint == NULL || element->level > entryPoint->level))
|
|
buildstate->entryPoint = element;
|
|
|
|
/* Add to graph memory context */
|
|
oldCtx = MemoryContextSwitchTo(buildstate->graphCtx);
|
|
|
|
if (dup != NULL)
|
|
{
|
|
HnswAddHeapTid(dup, heaptid);
|
|
HnswFreeElement(element);
|
|
|
|
#if PG_VERSION_NUM >= 130000
|
|
buildstate->memoryUsed = MemoryContextMemAllocated(buildstate->graphCtx, false);
|
|
#else
|
|
buildstate->memoryUsed += sizeof(ItemPointerData) + SIZEOF_VOID_P + GENERATIONCHUNK_RAWSIZE;
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
buildstate->elements = lappend(buildstate->elements, element);
|
|
|
|
#if PG_VERSION_NUM >= 130000
|
|
buildstate->memoryUsed = MemoryContextMemAllocated(buildstate->graphCtx, false);
|
|
#else
|
|
buildstate->memoryUsed += HnswElementMemory(element, buildstate->m);
|
|
#endif
|
|
}
|
|
|
|
MemoryContextSwitchTo(oldCtx);
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Callback for table_index_build_scan
|
|
*/
|
|
static void
|
|
BuildCallback(Relation index, CALLBACK_ITEM_POINTER, Datum *values,
|
|
bool *isnull, bool tupleIsAlive, void *state)
|
|
{
|
|
HnswBuildState *buildstate = (HnswBuildState *) state;
|
|
MemoryContext oldCtx;
|
|
|
|
#if PG_VERSION_NUM < 130000
|
|
ItemPointer tid = &hup->t_self;
|
|
#endif
|
|
|
|
/* Skip nulls */
|
|
if (isnull[0])
|
|
return;
|
|
|
|
if (!buildstate->flushed && (buildstate->memoryUsed >= buildstate->memoryTotal || list_length(buildstate->elements) == LIST_MAX_LENGTH))
|
|
{
|
|
if (buildstate->memoryUsed >= buildstate->memoryTotal)
|
|
ereport(NOTICE,
|
|
(errmsg("hnsw graph no longer fits into maintenance_work_mem after " INT64_FORMAT " tuples", (int64) buildstate->indtuples),
|
|
errdetail("Building will take significantly more time."),
|
|
errhint("Increase maintenance_work_mem to speed up builds.")));
|
|
|
|
FlushPages(buildstate);
|
|
}
|
|
|
|
oldCtx = MemoryContextSwitchTo(buildstate->tmpCtx);
|
|
|
|
if (buildstate->flushed)
|
|
{
|
|
if (HnswInsertTuple(index, values, isnull, tid, buildstate->heap, true))
|
|
{
|
|
if (buildstate->hnswshared)
|
|
{
|
|
HnswShared *hnswshared = buildstate->hnswshared;
|
|
|
|
SpinLockAcquire(&hnswshared->mutex);
|
|
UpdateProgress(PROGRESS_CREATEIDX_TUPLES_DONE, ++hnswshared->indtuples);
|
|
SpinLockRelease(&hnswshared->mutex);
|
|
}
|
|
else
|
|
UpdateProgress(PROGRESS_CREATEIDX_TUPLES_DONE, ++buildstate->indtuples);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (InsertTupleInMemory(index, values, tid, buildstate))
|
|
UpdateProgress(PROGRESS_CREATEIDX_TUPLES_DONE, ++buildstate->indtuples);
|
|
}
|
|
|
|
/* Reset memory context */
|
|
MemoryContextSwitchTo(oldCtx);
|
|
MemoryContextReset(buildstate->tmpCtx);
|
|
}
|
|
|
|
/*
|
|
* Initialize the build state
|
|
*/
|
|
static void
|
|
InitBuildState(HnswBuildState * buildstate, Relation heap, Relation index, IndexInfo *indexInfo, ForkNumber forkNum)
|
|
{
|
|
buildstate->heap = heap;
|
|
buildstate->index = index;
|
|
buildstate->indexInfo = indexInfo;
|
|
buildstate->forkNum = forkNum;
|
|
|
|
buildstate->m = HnswGetM(index);
|
|
buildstate->efConstruction = HnswGetEfConstruction(index);
|
|
buildstate->dimensions = TupleDescAttr(index->rd_att, 0)->atttypmod;
|
|
|
|
/* Require column to have dimensions to be indexed */
|
|
if (buildstate->dimensions < 0)
|
|
elog(ERROR, "column does not have dimensions");
|
|
|
|
if (buildstate->dimensions > 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->indtuples = 0;
|
|
|
|
/* Get support functions */
|
|
buildstate->procinfo = index_getprocinfo(index, 1, HNSW_DISTANCE_PROC);
|
|
buildstate->normprocinfo = HnswOptionalProcInfo(index, HNSW_NORM_PROC);
|
|
buildstate->collation = index->rd_indcollation[0];
|
|
|
|
buildstate->elements = NIL;
|
|
buildstate->entryPoint = NULL;
|
|
buildstate->ml = HnswGetMl(buildstate->m);
|
|
buildstate->maxLevel = HnswGetMaxLevel(buildstate->m);
|
|
buildstate->memoryUsed = 0;
|
|
buildstate->memoryTotal = maintenance_work_mem * 1024L;
|
|
buildstate->flushed = false;
|
|
|
|
/* Reuse for each tuple */
|
|
buildstate->normvec = InitVector(buildstate->dimensions);
|
|
|
|
buildstate->graphCtx = GenerationContextCreate(CurrentMemoryContext,
|
|
"Hnsw build graph context",
|
|
#if PG_VERSION_NUM >= 150000
|
|
1024 * 1024, 1024 * 1024,
|
|
#endif
|
|
1024 * 1024);
|
|
buildstate->tmpCtx = AllocSetContextCreate(CurrentMemoryContext,
|
|
"Hnsw build temporary context",
|
|
ALLOCSET_DEFAULT_SIZES);
|
|
|
|
buildstate->hnswleader = NULL;
|
|
buildstate->hnswshared = NULL;
|
|
}
|
|
|
|
/*
|
|
* Free resources
|
|
*/
|
|
static void
|
|
FreeBuildState(HnswBuildState * buildstate)
|
|
{
|
|
pfree(buildstate->normvec);
|
|
MemoryContextDelete(buildstate->graphCtx);
|
|
MemoryContextDelete(buildstate->tmpCtx);
|
|
}
|
|
|
|
/*
|
|
* Within leader, wait for end of heap scan
|
|
*/
|
|
static double
|
|
ParallelHeapScan(HnswBuildState * buildstate)
|
|
{
|
|
HnswShared *hnswshared = buildstate->hnswleader->hnswshared;
|
|
int nparticipanttuplesorts;
|
|
double reltuples;
|
|
|
|
nparticipanttuplesorts = buildstate->hnswleader->nparticipanttuplesorts;
|
|
for (;;)
|
|
{
|
|
SpinLockAcquire(&hnswshared->mutex);
|
|
if (hnswshared->nparticipantsdone == nparticipanttuplesorts)
|
|
{
|
|
buildstate->indtuples = hnswshared->indtuples;
|
|
reltuples = hnswshared->reltuples;
|
|
SpinLockRelease(&hnswshared->mutex);
|
|
break;
|
|
}
|
|
SpinLockRelease(&hnswshared->mutex);
|
|
|
|
ConditionVariableSleep(&hnswshared->workersdonecv,
|
|
WAIT_EVENT_PARALLEL_CREATE_INDEX_SCAN);
|
|
}
|
|
|
|
ConditionVariableCancelSleep();
|
|
|
|
return reltuples;
|
|
}
|
|
|
|
/*
|
|
* Perform a worker's portion of a parallel insert
|
|
*/
|
|
static void
|
|
HnswParallelScanAndInsert(HnswSpool * hnswspool, HnswShared * hnswshared, bool progress)
|
|
{
|
|
HnswBuildState buildstate;
|
|
#if PG_VERSION_NUM >= 120000
|
|
TableScanDesc scan;
|
|
#else
|
|
HeapScanDesc scan;
|
|
#endif
|
|
double reltuples;
|
|
IndexInfo *indexInfo;
|
|
|
|
/* Join parallel scan */
|
|
indexInfo = BuildIndexInfo(hnswspool->index);
|
|
indexInfo->ii_Concurrent = hnswshared->isconcurrent;
|
|
InitBuildState(&buildstate, hnswspool->heap, hnswspool->index, indexInfo, MAIN_FORKNUM);
|
|
/* TODO Support in-memory builds */
|
|
buildstate.memoryTotal = 0;
|
|
buildstate.flushed = true;
|
|
buildstate.hnswshared = hnswshared;
|
|
#if PG_VERSION_NUM >= 120000
|
|
scan = table_beginscan_parallel(hnswspool->heap,
|
|
ParallelTableScanFromHnswShared(hnswshared));
|
|
reltuples = table_index_build_scan(hnswspool->heap, hnswspool->index, indexInfo,
|
|
true, progress, BuildCallback,
|
|
(void *) &buildstate, scan);
|
|
#else
|
|
scan = heap_beginscan_parallel(hnswspool->heap, &hnswshared->heapdesc);
|
|
reltuples = IndexBuildHeapScan(hnswspool->heap, hnswspool->index, indexInfo,
|
|
true, BuildCallback,
|
|
(void *) &buildstate, scan);
|
|
#endif
|
|
|
|
/* Record statistics */
|
|
SpinLockAcquire(&hnswshared->mutex);
|
|
hnswshared->nparticipantsdone++;
|
|
hnswshared->reltuples += reltuples;
|
|
SpinLockRelease(&hnswshared->mutex);
|
|
|
|
/* Log statistics */
|
|
if (progress)
|
|
ereport(DEBUG1, (errmsg("leader processed " INT64_FORMAT " tuples", (int64) reltuples)));
|
|
else
|
|
ereport(DEBUG1, (errmsg("worker processed " INT64_FORMAT " tuples", (int64) reltuples)));
|
|
|
|
/* Notify leader */
|
|
ConditionVariableSignal(&hnswshared->workersdonecv);
|
|
|
|
FreeBuildState(&buildstate);
|
|
}
|
|
|
|
/*
|
|
* Perform work within a launched parallel process
|
|
*/
|
|
void
|
|
HnswParallelBuildMain(dsm_segment *seg, shm_toc *toc)
|
|
{
|
|
char *sharedquery;
|
|
HnswSpool *hnswspool;
|
|
HnswShared *hnswshared;
|
|
Relation heapRel;
|
|
Relation indexRel;
|
|
LOCKMODE heapLockmode;
|
|
LOCKMODE indexLockmode;
|
|
|
|
/* Set debug_query_string for individual workers first */
|
|
sharedquery = shm_toc_lookup(toc, PARALLEL_KEY_QUERY_TEXT, true);
|
|
debug_query_string = sharedquery;
|
|
|
|
/* Report the query string from leader */
|
|
pgstat_report_activity(STATE_RUNNING, debug_query_string);
|
|
|
|
/* Look up shared state */
|
|
hnswshared = shm_toc_lookup(toc, PARALLEL_KEY_HNSW_SHARED, false);
|
|
|
|
/* Open relations using lock modes known to be obtained by index.c */
|
|
if (!hnswshared->isconcurrent)
|
|
{
|
|
heapLockmode = ShareLock;
|
|
indexLockmode = AccessExclusiveLock;
|
|
}
|
|
else
|
|
{
|
|
heapLockmode = ShareUpdateExclusiveLock;
|
|
indexLockmode = RowExclusiveLock;
|
|
}
|
|
|
|
/* Open relations within worker */
|
|
#if PG_VERSION_NUM >= 120000
|
|
heapRel = table_open(hnswshared->heaprelid, heapLockmode);
|
|
#else
|
|
heapRel = heap_open(hnswshared->heaprelid, heapLockmode);
|
|
#endif
|
|
indexRel = index_open(hnswshared->indexrelid, indexLockmode);
|
|
|
|
/* Initialize worker's own spool */
|
|
hnswspool = (HnswSpool *) palloc0(sizeof(HnswSpool));
|
|
hnswspool->heap = heapRel;
|
|
hnswspool->index = indexRel;
|
|
|
|
/* Perform inserts */
|
|
HnswParallelScanAndInsert(hnswspool, hnswshared, false);
|
|
|
|
/* Close relations within worker */
|
|
index_close(indexRel, indexLockmode);
|
|
#if PG_VERSION_NUM >= 120000
|
|
table_close(heapRel, heapLockmode);
|
|
#else
|
|
heap_close(heapRel, heapLockmode);
|
|
#endif
|
|
}
|
|
|
|
/*
|
|
* End parallel build
|
|
*/
|
|
static void
|
|
HnswEndParallel(HnswLeader * hnswleader)
|
|
{
|
|
/* Shutdown worker processes */
|
|
WaitForParallelWorkersToFinish(hnswleader->pcxt);
|
|
|
|
/* Free last reference to MVCC snapshot, if one was used */
|
|
if (IsMVCCSnapshot(hnswleader->snapshot))
|
|
UnregisterSnapshot(hnswleader->snapshot);
|
|
DestroyParallelContext(hnswleader->pcxt);
|
|
ExitParallelMode();
|
|
}
|
|
|
|
/*
|
|
* Return size of shared memory required for parallel index build
|
|
*/
|
|
static Size
|
|
ParallelEstimateShared(Relation heap, Snapshot snapshot)
|
|
{
|
|
#if PG_VERSION_NUM >= 120000
|
|
return add_size(BUFFERALIGN(sizeof(HnswShared)), table_parallelscan_estimate(heap, snapshot));
|
|
#else
|
|
if (!IsMVCCSnapshot(snapshot))
|
|
{
|
|
Assert(snapshot == SnapshotAny);
|
|
return sizeof(HnswShared);
|
|
}
|
|
|
|
return add_size(offsetof(HnswShared, heapdesc) +
|
|
offsetof(ParallelHeapScanDescData, phs_snapshot_data),
|
|
EstimateSnapshotSpace(snapshot));
|
|
#endif
|
|
}
|
|
|
|
/*
|
|
* Within leader, participate as a parallel worker
|
|
*/
|
|
static void
|
|
HnswLeaderParticipateAsWorker(HnswBuildState * buildstate)
|
|
{
|
|
HnswLeader *hnswleader = buildstate->hnswleader;
|
|
HnswSpool *leaderworker;
|
|
|
|
/* Allocate memory and initialize private spool */
|
|
leaderworker = (HnswSpool *) palloc0(sizeof(HnswSpool));
|
|
leaderworker->heap = buildstate->heap;
|
|
leaderworker->index = buildstate->index;
|
|
|
|
/* Perform work common to all participants */
|
|
HnswParallelScanAndInsert(leaderworker, hnswleader->hnswshared, true);
|
|
}
|
|
|
|
/*
|
|
* Begin parallel build
|
|
*/
|
|
static void
|
|
HnswBeginParallel(HnswBuildState * buildstate, bool isconcurrent, int request)
|
|
{
|
|
ParallelContext *pcxt;
|
|
int scantuplesortstates;
|
|
Snapshot snapshot;
|
|
Size esthnswshared;
|
|
HnswShared *hnswshared;
|
|
HnswLeader *hnswleader = (HnswLeader *) palloc0(sizeof(HnswLeader));
|
|
bool leaderparticipates = true;
|
|
int querylen;
|
|
|
|
#ifdef DISABLE_LEADER_PARTICIPATION
|
|
leaderparticipates = false;
|
|
#endif
|
|
|
|
/* Enter parallel mode and create context */
|
|
EnterParallelMode();
|
|
Assert(request > 0);
|
|
#if PG_VERSION_NUM >= 120000
|
|
pcxt = CreateParallelContext("vector", "HnswParallelBuildMain", request);
|
|
#else
|
|
pcxt = CreateParallelContext("vector", "HnswParallelBuildMain", request, true);
|
|
#endif
|
|
|
|
scantuplesortstates = leaderparticipates ? request + 1 : request;
|
|
|
|
/* Get snapshot for table scan */
|
|
if (!isconcurrent)
|
|
snapshot = SnapshotAny;
|
|
else
|
|
snapshot = RegisterSnapshot(GetTransactionSnapshot());
|
|
|
|
/* Estimate size of workspaces */
|
|
esthnswshared = ParallelEstimateShared(buildstate->heap, snapshot);
|
|
shm_toc_estimate_chunk(&pcxt->estimator, esthnswshared);
|
|
shm_toc_estimate_keys(&pcxt->estimator, 1);
|
|
|
|
/* Finally, estimate PARALLEL_KEY_QUERY_TEXT space */
|
|
if (debug_query_string)
|
|
{
|
|
querylen = strlen(debug_query_string);
|
|
shm_toc_estimate_chunk(&pcxt->estimator, querylen + 1);
|
|
shm_toc_estimate_keys(&pcxt->estimator, 1);
|
|
}
|
|
else
|
|
querylen = 0; /* keep compiler quiet */
|
|
|
|
/* Everyone's had a chance to ask for space, so now create the DSM */
|
|
InitializeParallelDSM(pcxt);
|
|
|
|
/* If no DSM segment was available, back out (do serial build) */
|
|
if (pcxt->seg == NULL)
|
|
{
|
|
if (IsMVCCSnapshot(snapshot))
|
|
UnregisterSnapshot(snapshot);
|
|
DestroyParallelContext(pcxt);
|
|
ExitParallelMode();
|
|
return;
|
|
}
|
|
|
|
/* Store shared build state, for which we reserved space */
|
|
hnswshared = (HnswShared *) shm_toc_allocate(pcxt->toc, esthnswshared);
|
|
/* Initialize immutable state */
|
|
hnswshared->heaprelid = RelationGetRelid(buildstate->heap);
|
|
hnswshared->indexrelid = RelationGetRelid(buildstate->index);
|
|
hnswshared->isconcurrent = isconcurrent;
|
|
hnswshared->scantuplesortstates = scantuplesortstates;
|
|
ConditionVariableInit(&hnswshared->workersdonecv);
|
|
SpinLockInit(&hnswshared->mutex);
|
|
/* Initialize mutable state */
|
|
hnswshared->nparticipantsdone = 0;
|
|
hnswshared->reltuples = 0;
|
|
hnswshared->indtuples = 0;
|
|
#if PG_VERSION_NUM >= 120000
|
|
table_parallelscan_initialize(buildstate->heap,
|
|
ParallelTableScanFromHnswShared(hnswshared),
|
|
snapshot);
|
|
#else
|
|
heap_parallelscan_initialize(&hnswshared->heapdesc, buildstate->heap, snapshot);
|
|
#endif
|
|
|
|
shm_toc_insert(pcxt->toc, PARALLEL_KEY_HNSW_SHARED, hnswshared);
|
|
|
|
/* Store query string for workers */
|
|
if (debug_query_string)
|
|
{
|
|
char *sharedquery;
|
|
|
|
sharedquery = (char *) shm_toc_allocate(pcxt->toc, querylen + 1);
|
|
memcpy(sharedquery, debug_query_string, querylen + 1);
|
|
shm_toc_insert(pcxt->toc, PARALLEL_KEY_QUERY_TEXT, sharedquery);
|
|
}
|
|
|
|
/* Launch workers, saving status for leader/caller */
|
|
LaunchParallelWorkers(pcxt);
|
|
hnswleader->pcxt = pcxt;
|
|
hnswleader->nparticipanttuplesorts = pcxt->nworkers_launched;
|
|
if (leaderparticipates)
|
|
hnswleader->nparticipanttuplesorts++;
|
|
hnswleader->hnswshared = hnswshared;
|
|
hnswleader->snapshot = snapshot;
|
|
|
|
/* If no workers were successfully launched, back out (do serial build) */
|
|
if (pcxt->nworkers_launched == 0)
|
|
{
|
|
HnswEndParallel(hnswleader);
|
|
return;
|
|
}
|
|
|
|
/* Log participants */
|
|
ereport(DEBUG1, (errmsg("using %d parallel workers", pcxt->nworkers_launched)));
|
|
|
|
/* Save leader state now that it's clear build will be parallel */
|
|
buildstate->hnswleader = hnswleader;
|
|
|
|
/* Join heap scan ourselves */
|
|
if (leaderparticipates)
|
|
HnswLeaderParticipateAsWorker(buildstate);
|
|
|
|
/* Wait for all launched workers */
|
|
WaitForParallelWorkersToAttach(pcxt);
|
|
}
|
|
|
|
/*
|
|
* Build graph
|
|
*/
|
|
static void
|
|
BuildGraph(HnswBuildState * buildstate, ForkNumber forkNum)
|
|
{
|
|
int parallel_workers = 0;
|
|
|
|
UpdateProgress(PROGRESS_CREATEIDX_SUBPHASE, PROGRESS_HNSW_PHASE_LOAD);
|
|
|
|
/* Calculate parallel workers */
|
|
if (hnsw_enable_parallel_build)
|
|
parallel_workers = plan_create_index_workers(RelationGetRelid(buildstate->heap), RelationGetRelid(buildstate->index));
|
|
|
|
/* Attempt to launch parallel worker scan when required */
|
|
if (parallel_workers > 0)
|
|
{
|
|
/* TODO Support in-memory builds */
|
|
FlushPages(buildstate);
|
|
HnswBeginParallel(buildstate, buildstate->indexInfo->ii_Concurrent, parallel_workers);
|
|
}
|
|
|
|
/* Add tuples to sort */
|
|
if (buildstate->hnswleader)
|
|
buildstate->reltuples = ParallelHeapScan(buildstate);
|
|
else
|
|
{
|
|
#if PG_VERSION_NUM >= 120000
|
|
buildstate->reltuples = table_index_build_scan(buildstate->heap, buildstate->index, buildstate->indexInfo,
|
|
true, true, BuildCallback, (void *) buildstate, NULL);
|
|
#else
|
|
buildstate->reltuples = IndexBuildHeapScan(buildstate->heap, buildstate->index, buildstate->indexInfo,
|
|
true, BuildCallback, (void *) buildstate, NULL);
|
|
#endif
|
|
}
|
|
|
|
/* End parallel build */
|
|
if (buildstate->hnswleader)
|
|
HnswEndParallel(buildstate->hnswleader);
|
|
}
|
|
|
|
#if PG_VERSION_NUM < 110008
|
|
void
|
|
log_newpage_range(Relation rel, ForkNumber forkNum, BlockNumber startblk, BlockNumber endblk, bool page_std)
|
|
{
|
|
for (BlockNumber blkno = startblk; blkno < endblk; blkno++)
|
|
{
|
|
Buffer buf = ReadBufferExtended(rel, forkNum, blkno, RBM_NORMAL, NULL);
|
|
|
|
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
|
|
MarkBufferDirty(buf);
|
|
log_newpage_buffer(buf, page_std);
|
|
UnlockReleaseBuffer(buf);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/*
|
|
* Build the index
|
|
*/
|
|
static void
|
|
BuildIndex(Relation heap, Relation index, IndexInfo *indexInfo,
|
|
HnswBuildState * buildstate, ForkNumber forkNum)
|
|
{
|
|
#ifdef HNSW_MEMORY
|
|
SeedRandom(42);
|
|
#endif
|
|
|
|
InitBuildState(buildstate, heap, index, indexInfo, forkNum);
|
|
|
|
if (buildstate->heap != NULL)
|
|
BuildGraph(buildstate, forkNum);
|
|
|
|
if (!buildstate->flushed)
|
|
FlushPages(buildstate);
|
|
|
|
if (RelationNeedsWAL(index))
|
|
log_newpage_range(index, forkNum, 0, RelationGetNumberOfBlocks(index), true);
|
|
|
|
FreeBuildState(buildstate);
|
|
}
|
|
|
|
/*
|
|
* Build the index for a logged table
|
|
*/
|
|
IndexBuildResult *
|
|
hnswbuild(Relation heap, Relation index, IndexInfo *indexInfo)
|
|
{
|
|
IndexBuildResult *result;
|
|
HnswBuildState buildstate;
|
|
|
|
BuildIndex(heap, index, indexInfo, &buildstate, MAIN_FORKNUM);
|
|
|
|
result = (IndexBuildResult *) palloc(sizeof(IndexBuildResult));
|
|
result->heap_tuples = buildstate.reltuples;
|
|
result->index_tuples = buildstate.indtuples;
|
|
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
* Build the index for an unlogged table
|
|
*/
|
|
void
|
|
hnswbuildempty(Relation index)
|
|
{
|
|
IndexInfo *indexInfo = BuildIndexInfo(index);
|
|
HnswBuildState buildstate;
|
|
|
|
BuildIndex(NULL, index, indexInfo, &buildstate, INIT_FORKNUM);
|
|
}
|