Compare commits

..

1 Commits

Author SHA1 Message Date
Andrew Kane
5db9581e74 Added versioning to tuples 2024-09-20 15:10:54 -07:00
9 changed files with 112 additions and 123 deletions

View File

@@ -129,6 +129,7 @@ struct HnswElementData
uint8 heaptidsLength;
uint8 level;
uint8 deleted;
uint8 version;
uint32 hash;
HnswNeighborsPtr neighbors;
BlockNumber blkno;
@@ -155,13 +156,12 @@ struct HnswNeighborArray
HnswCandidate items[FLEXIBLE_ARRAY_MEMBER];
};
typedef struct HnswSearchCandidate
typedef struct HnswPairingHeapNode
{
HnswCandidate *inner;
pairingheap_node c_node;
pairingheap_node w_node;
HnswElementPtr element;
float distance;
} HnswSearchCandidate;
} HnswPairingHeapNode;
/* HNSW index options */
typedef struct HnswOptions
@@ -306,10 +306,10 @@ typedef struct HnswElementTupleData
uint8 type;
uint8 level;
uint8 deleted;
uint8 unused;
uint8 version;
ItemPointerData heaptids[HNSW_HEAPTIDS];
ItemPointerData neighbortid;
uint16 unused2;
uint16 unused;
Vector data;
} HnswElementTupleData;
@@ -318,7 +318,7 @@ typedef HnswElementTupleData * HnswElementTuple;
typedef struct HnswNeighborTupleData
{
uint8 type;
uint8 unused;
uint8 version;
uint16 count;
ItemPointerData indextids[FLEXIBLE_ARRAY_MEMBER];
} HnswNeighborTupleData;
@@ -382,7 +382,7 @@ void *HnswAlloc(HnswAllocator * allocator, Size size);
HnswElement HnswInitElement(char *base, ItemPointer tid, int m, double ml, int maxLevel, HnswAllocator * alloc);
HnswElement HnswInitElementFromBlock(BlockNumber blkno, OffsetNumber offno);
void HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint, Relation index, FmgrInfo *procinfo, Oid collation, int m, int efConstruction, bool existing);
HnswSearchCandidate *HnswEntryCandidate(char *base, HnswElement em, Datum q, Relation rel, FmgrInfo *procinfo, Oid collation, bool loadVec);
HnswCandidate *HnswEntryCandidate(char *base, HnswElement em, Datum q, Relation rel, FmgrInfo *procinfo, Oid collation, bool loadVec);
void HnswUpdateMetaPage(Relation index, int updateEntry, HnswElement entryPoint, BlockNumber insertPage, ForkNumber forkNum, bool building);
void HnswSetNeighborTuple(char *base, HnswNeighborTuple ntup, HnswElement e, int m);
void HnswAddHeapTid(HnswElement element, ItemPointer heaptid);

View File

@@ -36,7 +36,7 @@ GetInsertPage(Relation index)
* Check for a free offset
*/
static bool
HnswFreeOffset(Relation index, Buffer buf, Page page, HnswElement element, Size etupSize, Size ntupSize, Buffer *nbuf, Page *npage, OffsetNumber *freeOffno, OffsetNumber *freeNeighborOffno, BlockNumber *newInsertPage)
HnswFreeOffset(Relation index, Buffer buf, Page page, HnswElement element, Size etupSize, Size ntupSize, Buffer *nbuf, Page *npage, OffsetNumber *freeOffno, OffsetNumber *freeNeighborOffno, BlockNumber *newInsertPage, uint8 *tupleVersion)
{
OffsetNumber offno;
OffsetNumber maxoffno = PageGetMaxOffsetNumber(page);
@@ -98,6 +98,7 @@ HnswFreeOffset(Relation index, Buffer buf, Page page, HnswElement element, Size
{
*freeOffno = offno;
*freeNeighborOffno = neighborOffno;
*tupleVersion = etup->version;
return true;
}
else if (*nbuf != buf)
@@ -153,6 +154,7 @@ AddElementOnDisk(Relation index, HnswElement e, int m, BlockNumber insertPage, B
OffsetNumber freeOffno = InvalidOffsetNumber;
OffsetNumber freeNeighborOffno = InvalidOffsetNumber;
BlockNumber newInsertPage = InvalidBlockNumber;
uint8 tupleVersion;
char *base = NULL;
/* Calculate sizes */
@@ -202,7 +204,7 @@ AddElementOnDisk(Relation index, HnswElement e, int m, BlockNumber insertPage, B
}
/* Next, try space from a deleted element */
if (HnswFreeOffset(index, buf, page, e, etupSize, ntupSize, &nbuf, &npage, &freeOffno, &freeNeighborOffno, &newInsertPage))
if (HnswFreeOffset(index, buf, page, e, etupSize, ntupSize, &nbuf, &npage, &freeOffno, &freeNeighborOffno, &newInsertPage, &tupleVersion))
{
if (nbuf != buf)
{
@@ -212,6 +214,10 @@ AddElementOnDisk(Relation index, HnswElement e, int m, BlockNumber insertPage, B
npage = GenericXLogRegisterBuffer(state, nbuf, 0);
}
/* Set tuple version */
etup->version = tupleVersion;
ntup->version = tupleVersion;
break;
}

View File

@@ -161,14 +161,14 @@ hnswgettuple(IndexScanDesc scan, ScanDirection dir)
so->first = false;
#if defined(HNSW_MEMORY)
elog(INFO, "memory: %zu KB", MemoryContextMemAllocated(so->tmpCtx, false) / 1024);
elog(INFO, "memory: %zu MB", MemoryContextMemAllocated(so->tmpCtx, false) / (1024 * 1024));
#endif
}
while (list_length(so->w) > 0)
{
char *base = NULL;
HnswSearchCandidate *hc = llast(so->w);
HnswCandidate *hc = llast(so->w);
HnswElement element = HnswPtrAccess(base, hc->element);
ItemPointer heaptid;

View File

@@ -253,6 +253,8 @@ HnswInitElement(char *base, ItemPointer heaptid, int m, double ml, int maxLevel,
element->level = level;
element->deleted = 0;
/* Start at one to make it easier to find issues */
element->version = 1;
HnswInitNeighbors(base, element, m, allocator);
@@ -405,6 +407,7 @@ HnswSetElementTuple(char *base, HnswElementTuple etup, HnswElement element)
etup->type = HNSW_ELEMENT_TUPLE_TYPE;
etup->level = element->level;
etup->deleted = 0;
etup->version = element->version;
for (int i = 0; i < HNSW_HEAPTIDS; i++)
{
if (i < element->heaptidsLength)
@@ -447,6 +450,7 @@ HnswSetNeighborTuple(char *base, HnswNeighborTuple ntup, HnswElement e, int m)
}
ntup->count = idx;
ntup->version = e->version;
}
/*
@@ -520,6 +524,7 @@ HnswLoadElementFromTuple(HnswElement element, HnswElementTuple etup, bool loadHe
{
element->level = etup->level;
element->deleted = etup->deleted;
element->version = etup->version;
element->neighborPage = ItemPointerGetBlockNumber(&etup->neighbortid);
element->neighborOffno = ItemPointerGetOffsetNumber(&etup->neighbortid);
element->heaptidsLength = 0;
@@ -608,10 +613,10 @@ GetElementDistance(char *base, HnswElement element, Datum q, FmgrInfo *procinfo,
/*
* Create a candidate for the entry point
*/
HnswSearchCandidate *
HnswCandidate *
HnswEntryCandidate(char *base, HnswElement entryPoint, Datum q, Relation index, FmgrInfo *procinfo, Oid collation, bool loadVec)
{
HnswSearchCandidate *hc = palloc(sizeof(HnswSearchCandidate));
HnswCandidate *hc = palloc(sizeof(HnswCandidate));
HnswPtrStore(base, hc->element, entryPoint);
if (index == NULL)
@@ -621,8 +626,8 @@ HnswEntryCandidate(char *base, HnswElement entryPoint, Datum q, Relation index,
return hc;
}
#define HnswGetSearchCandidate(membername, ptr) pairingheap_container(HnswSearchCandidate, membername, ptr)
#define HnswGetSearchCandidateConst(membername, ptr) pairingheap_const_container(HnswSearchCandidate, membername, ptr)
#define HnswGetPairingHeapCandidate(membername, ptr) (pairingheap_container(HnswPairingHeapNode, membername, ptr)->inner)
#define HnswGetPairingHeapCandidateConst(membername, ptr) (pairingheap_const_container(HnswPairingHeapNode, membername, ptr)->inner)
/*
* Compare candidate distances
@@ -630,10 +635,10 @@ HnswEntryCandidate(char *base, HnswElement entryPoint, Datum q, Relation index,
static int
CompareNearestCandidates(const pairingheap_node *a, const pairingheap_node *b, void *arg)
{
if (HnswGetSearchCandidateConst(c_node, a)->distance < HnswGetSearchCandidateConst(c_node, b)->distance)
if (HnswGetPairingHeapCandidateConst(c_node, a)->distance < HnswGetPairingHeapCandidateConst(c_node, b)->distance)
return 1;
if (HnswGetSearchCandidateConst(c_node, a)->distance > HnswGetSearchCandidateConst(c_node, b)->distance)
if (HnswGetPairingHeapCandidateConst(c_node, a)->distance > HnswGetPairingHeapCandidateConst(c_node, b)->distance)
return -1;
return 0;
@@ -645,15 +650,27 @@ CompareNearestCandidates(const pairingheap_node *a, const pairingheap_node *b, v
static int
CompareFurthestCandidates(const pairingheap_node *a, const pairingheap_node *b, void *arg)
{
if (HnswGetSearchCandidateConst(w_node, a)->distance < HnswGetSearchCandidateConst(w_node, b)->distance)
if (HnswGetPairingHeapCandidateConst(w_node, a)->distance < HnswGetPairingHeapCandidateConst(w_node, b)->distance)
return -1;
if (HnswGetSearchCandidateConst(w_node, a)->distance > HnswGetSearchCandidateConst(w_node, b)->distance)
if (HnswGetPairingHeapCandidateConst(w_node, a)->distance > HnswGetPairingHeapCandidateConst(w_node, b)->distance)
return 1;
return 0;
}
/*
* Create a pairing heap node for a candidate
*/
static HnswPairingHeapNode *
CreatePairingHeapNode(HnswCandidate * c)
{
HnswPairingHeapNode *node = palloc(sizeof(HnswPairingHeapNode));
node->inner = c;
return node;
}
/*
* Init visited
*/
@@ -754,20 +771,30 @@ HnswLoadUnvisitedFromDisk(HnswElement element, HnswUnvisited * unvisited, int *u
int start;
ItemPointerData indextids[HNSW_MAX_M * 2];
*unvisitedLength = 0;
buf = ReadBuffer(index, element->neighborPage);
LockBuffer(buf, BUFFER_LOCK_SHARE);
page = BufferGetPage(buf);
ntup = (HnswNeighborTuple) PageGetItem(page, PageGetItemId(page, element->neighborOffno));
start = (element->level - lc) * m;
/*
* Ensure the neighbor tuple has not been deleted or replaced between
* index scan iterations
*/
if (ntup->version != element->version)
{
UnlockReleaseBuffer(buf);
return;
}
/* Copy to minimize lock time */
start = (element->level - lc) * m;
memcpy(&indextids, ntup->indextids + start, lm * sizeof(ItemPointerData));
UnlockReleaseBuffer(buf);
*unvisitedLength = 0;
for (int i = 0; i < lm; i++)
{
ItemPointer indextid = &indextids[i];
@@ -813,13 +840,15 @@ HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, F
/* Add entry points to v, C, and W */
foreach(lc2, ep)
{
HnswSearchCandidate *hc = (HnswSearchCandidate *) lfirst(lc2);
HnswCandidate *hc = (HnswCandidate *) lfirst(lc2);
bool found;
HnswPairingHeapNode *node;
AddToVisited(base, &v, hc->element, index, &found);
pairingheap_add(C, &hc->c_node);
pairingheap_add(W, &hc->w_node);
node = CreatePairingHeapNode(hc);
pairingheap_add(C, &node->c_node);
pairingheap_add(W, &node->w_node);
/*
* Do not count elements being deleted towards ef when vacuuming. It
@@ -832,8 +861,8 @@ HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, F
while (!pairingheap_is_empty(C))
{
HnswSearchCandidate *c = HnswGetSearchCandidate(c_node, pairingheap_remove_first(C));
HnswSearchCandidate *f = HnswGetSearchCandidate(w_node, pairingheap_first(W));
HnswCandidate *c = HnswGetPairingHeapCandidate(c_node, pairingheap_remove_first(C));
HnswCandidate *f = HnswGetPairingHeapCandidate(w_node, pairingheap_first(W));
HnswElement cElement;
if (c->distance > f->distance)
@@ -849,16 +878,20 @@ HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, F
for (int i = 0; i < unvisitedLength; i++)
{
HnswElement eElement;
HnswSearchCandidate *e;
HnswCandidate *e;
HnswPairingHeapNode *node;
float eDistance;
bool alwaysAdd = wlen < ef;
f = HnswGetSearchCandidate(w_node, pairingheap_first(W));
f = HnswGetPairingHeapCandidate(w_node, pairingheap_first(W));
if (index == NULL)
{
eElement = unvisited[i].element;
eDistance = GetElementDistance(base, eElement, q, procinfo, collation);
if (!(eDistance < f->distance || alwaysAdd))
continue;
}
else
{
@@ -874,9 +907,6 @@ HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, F
continue;
}
if (!(eDistance < f->distance || alwaysAdd))
continue;
Assert(!eElement->deleted);
/* Make robust to issues */
@@ -884,11 +914,13 @@ HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, F
continue;
/* Create a new candidate */
e = palloc(sizeof(HnswSearchCandidate));
e = palloc(sizeof(HnswCandidate));
HnswPtrStore(base, e->element, eElement);
e->distance = eDistance;
pairingheap_add(C, &e->c_node);
pairingheap_add(W, &e->w_node);
node = CreatePairingHeapNode(e);
pairingheap_add(C, &node->c_node);
pairingheap_add(W, &node->w_node);
/*
* Do not count elements being deleted towards ef when vacuuming.
@@ -909,7 +941,7 @@ HnswSearchLayer(char *base, Datum q, List *ep, int ef, int lc, Relation index, F
/* Add each element of W to w */
while (!pairingheap_is_empty(W))
{
HnswSearchCandidate *hc = HnswGetSearchCandidate(w_node, pairingheap_remove_first(W));
HnswCandidate *hc = HnswGetPairingHeapCandidate(w_node, pairingheap_remove_first(W));
w = lappend(w, hc);
}
@@ -1290,27 +1322,16 @@ HnswFindElementNeighbors(char *base, HnswElement element, HnswElement entryPoint
{
int lm = HnswGetLayerM(m, lc);
List *neighbors;
List *lw = NIL;
ListCell *lc2;
List *lw;
w = HnswSearchLayer(base, q, ep, efConstruction, lc, index, procinfo, collation, m, true, skipElement);
/* Convert search candidates to candidates */
foreach(lc2, w)
{
HnswSearchCandidate *sc = lfirst(lc2);
HnswCandidate *hc = palloc(sizeof(HnswCandidate));
hc->element = sc->element;
hc->distance = sc->distance;
lw = lappend(lw, hc);
}
/* Elements being deleted or skipped can help with search */
/* but should be removed before selecting neighbors */
if (index != NULL)
lw = RemoveElements(base, lw, skipElement);
lw = RemoveElements(base, w, skipElement);
else
lw = w;
/*
* Candidates are sorted, but not deterministically. Could set

View File

@@ -527,6 +527,11 @@ MarkDeleted(HnswVacuumState * vacuumstate)
for (int i = 0; i < ntup->count; i++)
ItemPointerSetInvalid(&ntup->indextids[i]);
/* Increment version */
/* This is used to avoid incorrect reads for iterative scans */
etup->version++;
ntup->version = etup->version;
/*
* We modified the tuples in place, no need to call
* PageIndexTupleOverwrite

View File

@@ -57,39 +57,6 @@ ivfflatbuildphasename(int64 phasenum)
}
}
/*
* Estimate the number of probes needed to get requested tuples
*/
static int
EstimateProbes(PlannerInfo *root, IndexPath *path, int lists)
{
double selectivity = 1;
ListCell *lc;
double tuplesPerList;
/* Cannot estimate without limit */
/* limit_tuples includes offset */
if (root->limit_tuples < 0)
return 0;
/* Get the selectivity of non-index conditions */
foreach(lc, path->indexinfo->indrestrictinfo)
{
RestrictInfo *rinfo = lfirst(lc);
/* Skip DEFAULT_INEQ_SEL since it may be a distance filter */
if (rinfo->norm_selec >= 0 && rinfo->norm_selec <= 1 && rinfo->norm_selec != (Selectivity) DEFAULT_INEQ_SEL)
selectivity *= rinfo->norm_selec;
}
tuplesPerList = path->indexinfo->tuples * selectivity / (double) lists;
if (tuplesPerList == 0)
return lists;
/* No need to cap at this point */
return root->limit_tuples / tuplesPerList;
}
/*
* Estimate the cost of an index scan
*/
@@ -101,7 +68,6 @@ ivfflatcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
{
GenericCosts costs;
int lists;
int probes;
double ratio;
double spc_seq_page_cost;
Relation index;
@@ -123,10 +89,8 @@ ivfflatcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
IvfflatGetMetaPageInfo(index, &lists, NULL);
index_close(index, NoLock);
probes = Max(ivfflat_probes, EstimateProbes(root, path, lists));
/* Get the ratio of lists that we need to visit */
ratio = ((double) probes) / lists;
ratio = ((double) ivfflat_probes) / lists;
if (ratio > 1.0)
ratio = 1.0;

View File

@@ -253,9 +253,8 @@ typedef struct IvfflatScanOpaqueData
/* Sorting */
Tuplesortstate *sortstate;
TupleDesc tupdesc;
TupleTableSlot *vslot;
TupleTableSlot *mslot;
BufferAccessStrategy bas;
TupleTableSlot *slot;
bool isnull;
/* Support functions */
FmgrInfo *procinfo;

View File

@@ -15,19 +15,16 @@
#include "utils/memutils.h"
#endif
#define GetScanList(ptr) pairingheap_container(IvfflatScanList, ph_node, ptr)
#define GetScanListConst(ptr) pairingheap_const_container(IvfflatScanList, ph_node, ptr)
/*
* Compare list distances
*/
static int
CompareLists(const pairingheap_node *a, const pairingheap_node *b, void *arg)
{
if (GetScanListConst(a)->distance > GetScanListConst(b)->distance)
if (((const IvfflatScanList *) a)->distance > ((const IvfflatScanList *) b)->distance)
return 1;
if (GetScanListConst(a)->distance < GetScanListConst(b)->distance)
if (((const IvfflatScanList *) a)->distance < ((const IvfflatScanList *) b)->distance)
return -1;
return 0;
@@ -79,14 +76,14 @@ GetScanLists(IndexScanDesc scan, Datum value)
/* Calculate max distance */
if (listCount == so->probes)
maxDistance = GetScanList(pairingheap_first(so->listQueue))->distance;
maxDistance = ((IvfflatScanList *) pairingheap_first(so->listQueue))->distance;
}
else if (distance < maxDistance)
{
IvfflatScanList *scanlist;
/* Remove */
scanlist = GetScanList(pairingheap_remove_first(so->listQueue));
scanlist = (IvfflatScanList *) pairingheap_remove_first(so->listQueue);
/* Reuse */
scanlist->startPage = list->startPage;
@@ -94,7 +91,7 @@ GetScanLists(IndexScanDesc scan, Datum value)
pairingheap_add(so->listQueue, &scanlist->ph_node);
/* Update max distance */
maxDistance = GetScanList(pairingheap_first(so->listQueue))->distance;
maxDistance = ((IvfflatScanList *) pairingheap_first(so->listQueue))->distance;
}
}
@@ -113,12 +110,19 @@ GetScanItems(IndexScanDesc scan, Datum value)
IvfflatScanOpaque so = (IvfflatScanOpaque) scan->opaque;
TupleDesc tupdesc = RelationGetDescr(scan->indexRelation);
double tuples = 0;
TupleTableSlot *slot = so->vslot;
TupleTableSlot *slot = MakeSingleTupleTableSlot(so->tupdesc, &TTSOpsVirtual);
/*
* Reuse same set of shared buffers for scan
*
* See postgres/src/backend/storage/buffer/README for description
*/
BufferAccessStrategy bas = GetAccessStrategy(BAS_BULKREAD);
/* Search closest probes lists */
while (!pairingheap_is_empty(so->listQueue))
{
BlockNumber searchPage = GetScanList(pairingheap_remove_first(so->listQueue))->startPage;
BlockNumber searchPage = ((IvfflatScanList *) pairingheap_remove_first(so->listQueue))->startPage;
/* Search all entry pages for list */
while (BlockNumberIsValid(searchPage))
@@ -127,7 +131,7 @@ GetScanItems(IndexScanDesc scan, Datum value)
Page page;
OffsetNumber maxoffno;
buf = ReadBufferExtended(scan->indexRelation, MAIN_FORKNUM, searchPage, RBM_NORMAL, so->bas);
buf = ReadBufferExtended(scan->indexRelation, MAIN_FORKNUM, searchPage, RBM_NORMAL, bas);
LockBuffer(buf, BUFFER_LOCK_SHARE);
page = BufferGetPage(buf);
maxoffno = PageGetMaxOffsetNumber(page);
@@ -166,6 +170,8 @@ GetScanItems(IndexScanDesc scan, Datum value)
}
}
FreeAccessStrategy(bas);
if (tuples < 100)
ereport(DEBUG1,
(errmsg("index scan found few tuples"),
@@ -268,16 +274,7 @@ ivfflatbeginscan(Relation index, int nkeys, int norderbys)
/* Prep sort */
so->sortstate = InitScanSortState(so->tupdesc);
/* Need separate slots for puttuple and gettuple */
so->vslot = MakeSingleTupleTableSlot(so->tupdesc, &TTSOpsVirtual);
so->mslot = MakeSingleTupleTableSlot(so->tupdesc, &TTSOpsMinimalTuple);
/*
* Reuse same set of shared buffers for scan
*
* See postgres/src/backend/storage/buffer/README for description
*/
so->bas = GetAccessStrategy(BAS_BULKREAD);
so->slot = MakeSingleTupleTableSlot(so->tupdesc, &TTSOpsMinimalTuple);
so->listQueue = pairingheap_allocate(CompareLists, scan);
@@ -351,10 +348,9 @@ ivfflatgettuple(IndexScanDesc scan, ScanDirection dir)
pfree(DatumGetPointer(value));
}
if (tuplesort_gettupleslot(so->sortstate, true, false, so->mslot, NULL))
if (tuplesort_gettupleslot(so->sortstate, true, false, so->slot, NULL))
{
bool isnull;
ItemPointer heaptid = (ItemPointer) DatumGetPointer(slot_getattr(so->mslot, 2, &isnull));
ItemPointer heaptid = (ItemPointer) DatumGetPointer(slot_getattr(so->slot, 2, &so->isnull));
scan->xs_heaptid = *heaptid;
scan->xs_recheck = false;
@@ -375,10 +371,6 @@ ivfflatendscan(IndexScanDesc scan)
pairingheap_free(so->listQueue);
tuplesort_end(so->sortstate);
FreeAccessStrategy(so->bas);
FreeTupleDesc(so->tupdesc);
/* TODO Free vslot and mslot without freeing TupleDesc */
pfree(so);
scan->opaque = NULL;

View File

@@ -5,7 +5,7 @@ use PostgreSQL::Test::Utils;
use Test::More;
my $dim = 3;
my $nc = 100;
my $nc = 50;
my $limit = 20;
my $array_sql = join(",", ('random()') x $dim);
@@ -102,13 +102,15 @@ $node->safe_psql("postgres", "CREATE INDEX attribute_idx ON tst (c);");
$explain = $node->safe_psql("postgres", qq(
EXPLAIN ANALYZE SELECT i FROM tst WHERE c = $c ORDER BY v <-> '$query' LIMIT $limit;
));
like($explain, qr/Index Scan on attribute_idx/);
# TODO Use attribute index
like($explain, qr/Index Scan using idx/);
# Test partial index
$node->safe_psql("postgres", "CREATE INDEX partial_idx ON tst USING ivfflat (v vector_l2_ops) WITH (lists = 5) WHERE (c = $c);");
$explain = $node->safe_psql("postgres", qq(
EXPLAIN ANALYZE SELECT i FROM tst WHERE c = $c ORDER BY v <-> '$query' LIMIT $limit;
));
like($explain, qr/Index Scan using partial_idx/);
# TODO Use partial index
like($explain, qr/Index Scan using idx/);
done_testing();