pg_recall_guard 0.1.0

This Release
pg_recall_guard 0.1.0
Date
Status
Testing
Latest Stable
pg_recall_guard 0.2.0 —
Other Releases
Abstract
Watch vector indexes for recall drift against a baseline you approved
Description
A degraded approximate-nearest-neighbour index does not fail. It returns k plausible neighbours and never mentions that four of the ten good ones were left out — no error, no log entry, no alert. pg_recall_guard measures the recall of a vector index against exact ground truth, records the number you accepted as good, and reports when it drifts. It finds indexes through pg_amop.amoppurpose = 'o', a property of PostgreSQL rather than of any extension, so the same code covers hnsw and ivfflat (pgvector), diskann (pgvectorscale), gist and spgist without naming any of them. Existing recall tooling is tied to the index that ships it: each vector extension has an incentive to measure its own and none to measure the others'. This is the neutral one.
Released By
manu15
License
PostgreSQL
Resources
Special Files
Tags

Extensions

pg_recall_guard 0.1.0
Measure and watch recall of any PostgreSQL vector index

README

pg_recall_guard

Watch vector indexes for recall drift against a baseline you approved.

A degraded ANN index does not fail. It returns k plausible neighbours and never mentions that four of the ten good ones were left out. No error, no log entry, no alert — you find out when a user tells you the search “feels worse”, or when you happen to run a benchmark months later.

This extension measures the recall of any vector index against exact ground truth, records the number you accepted as good, and tells you when it drifts.

CREATE EXTENSION pg_recall_guard;

-- What can be measured? Discovered from the catalog, no configuration.
SELECT index_name, table_name, access_method, operator
  FROM recall_guard.vector_indexes;

--  index_name | table_name | access_method | operator
-- ------------+------------+---------------+----------
--  items_hnsw | items      | hnsw          | <=>
--  docs_dann  | docs       | diskann       | <=>

-- Record what you consider good.
SELECT recall_guard.approve('items_hnsw', p_k => 10, p_sample_size => 100);

-- Later — from pg_cron, from your monitoring, from a shell:
SELECT * FROM recall_guard.check();

--  index_name | baseline | current |  drift  | verdict
-- ------------+----------+---------+---------+---------
--  items_hnsw |   0.9800 |  0.8100 | -0.1700 | critico

Works with any vector index, because it never names one

Indexes are found through pg_amop.amoppurpose = 'o' — access methods that can answer an ORDER BY on a distance operator. That is a PostgreSQL property, not an extension’s. Today it covers hnsw and ivfflat (pgvector), diskann (pgvectorscale), gist and spgist; tomorrow it covers whatever ships next, with no change here.

Verified on PostgreSQL 19beta2 against pgvector 0.8.6 and pgvectorscale 0.9.0, over 38,352 vector(768) rows:

hnsw.ef_search measured recall
1 0.0000
10 0.9000
40 (default) 1.0000

It is built so it cannot flatter itself

Two failure modes would produce a reassuring 1.0000 that means nothing. Both are checked rather than assumed:

  • The indexed side must actually use the index. If the planner ignores it, the measurement compares the index against itself and always scores perfect. The plan is inspected and the function raises instead of returning a number.
  • The ground truth must not use it. Same reasoning, other direction.

There is a third trap that is easy to miss. Sample queries are drawn from rows of the table itself, and a stored vector always finds itself at distance zero — a free hit in every single query, which with k = 10 means recall can never drop below 0.1 no matter how broken the index is. pg_recall_guard asks for k+1 neighbours and discards the originating ctid from both sides. That is the difference between measuring the index and measuring that a vector equals itself: at ef_search = 1 the naive version reports 0.1000, this one reports 0.0000.

Cost

check() runs one exact sequential scan per sampled query, so it is not free and it is not meant to run on every request. It is meant to run on a schedule, on a sample, the way you would run ANALYZE — the degradation it looks for is gradual and silent, so the only moment it gets caught is when something bothers to look.

Start with p_sample_size => 30 and raise it if the numbers move around between runs.

Install

make install    # or: make PG_CONFIG=/path/to/pg_config install

Requires the tsm_system_rows extension (ships with PostgreSQL contrib) for sampling.

Prior art, and where this differs

VectorChord 0.5 added vchordrq_evaluate_query_recall and query sampling — a good design, and this extension borrows the shape of it. But by their own documentation it is “not supported by vchordg”: it works for their IVF+RaBitQ index only. That is a rational choice for a vendor and a gap for everyone else, since each vector extension has an incentive to measure its own index and none has an incentive to measure the others'.

pg_recall_guard is the neutral version: it belongs to no index, so it can watch all of them.

Maturity

0.1.0, released as testing. The mechanism is verified and the numbers above are real, but they come from one machine, one dataset of 38,352 vectors, two index types and a single PostgreSQL version (19beta2). Nobody has run this against a production workload yet, and the sampling strategy — drawing query vectors from rows of the table — is a reasonable proxy for real traffic, not real traffic.

Treat the recall numbers it reports as trustworthy and its coverage as unproven. Reports from other datasets, dimensions and index parameters are the most useful thing anyone could send.

License

PostgreSQL License.