Hard-delete AND verify erasure of a data subject from your vector store — the GDPR right-to-be-forgotten that a plain .delete() fails.
Project description
vector-forget
Hard-delete and verify erasure of a data subject from your vector store.
When a GDPR / CCPA "right to be forgotten" request lands, store.delete() is not
enough — most vector databases soft-delete: the vectors vanish from search while the
raw bytes stay on disk, reconstructible. (See Ghost Vectors, arXiv:2606.18497 — deleted
embeddings recovered at high rates from raw index files.) A single-subject delete also
never trips the background compaction/vacuum that would actually reclaim them.
vector-forget does the two things a plain delete doesn't:
- Hard-delete — forces the physical reclaim (e.g. pgvector
REINDEX+VACUUM) so the ghost vectors are actually gone, not just hidden. - Verify + report — proves it with the backend's own residue signal and hands you a
structured
ForgetReport.
⚠️ Alpha / v0.1 — pgvector only. Qdrant, Weaviate and framework integrations are deliberately deferred (see Non-goals).
Install
pip install "vector-forget[pgvector]"
Requires Python 3.9+ and Postgres with the vector extension. The physical-residue proof
(the wedge) additionally needs pgstattuple and table-owner / MAINTAIN rights — i.e. a
self-hosted / owned Postgres. On managed Postgres it still deletes and verifies
visibility, but the residue gate degrades honestly to UNVERIFIED-managed (see below).
Quickstart (pgvector)
import psycopg
from vector_forget import stamp, forget, verify
from vector_forget.pgvector import PgVectorStore
# 0) connect to your Postgres (self-hosted, for the full residue proof)
conn = psycopg.connect("postgresql://user:pass@localhost:5432/mydb")
store = PgVectorStore(conn, table="documents", subject_col="subject_id")
# 1) at INGEST — tag each row so it can be forgotten later (one-line shim)
metadata = stamp(metadata, subject_id="user_42") # wrap your existing add_documents/insert
# 2) on an ERASURE request — hard-delete + force physical reclaim + verify
report = forget("user_42", store)
print(report.to_json(indent=2))
print("erased & verified:", report.ok) # True only on visibility PASS + VERIFIED
# safe on a hot prod DB — delete + verify only, no REINDEX/VACUUM (residue stays PENDING)
report = forget("user_42", store, force=False)
# standalone proof pass, no delete
report = verify("user_42", store)
What it honestly claims — and does not
ForgetReport carries this boundary in the report itself (not just the docs):
Scoped to the live store(s) you pointed it at, at the time it ran. Does not cover backups/snapshots, replicas, WAL/logs, other stores (caches, a second DB, object storage, logs), embedding-provider-side copies, or data baked into fine-tuned model weights. It is a removal report, not a signed certificate of erasure.
On managed Postgres (RDS/Neon/Supabase) the physical-residue check may be unavailable →
the report says residue: UNVERIFIED-managed rather than claiming more than it can prove.
Under-claiming is the design rule.
Package-provenance attestations (PEP 740) emitted by the PyPI publish pipeline attest the
wheel's build origin — they are not an erasure attestation and never sign a
ForgetReport. Don't conflate a signed package with a signed proof of deletion.
Residue states
residue |
meaning |
|---|---|
VERIFIED |
dead tuples reclaimed + index rebuilt (not "prior bytes overwritten") |
PENDING-lock-timeout |
reclaim hit the bounded lock wait — transient, retry |
PENDING-<pin> |
dead tuples pinned by a long txn / replication slot / prepared xact |
UNVERIFIED-managed |
managed PG / least-privilege role blocked the reclaim |
UNVERIFIED-index-not-rebuilt |
a vector index left invalid (ghost nodes may survive) |
UNVERIFIED-dead-tuples-remain |
tuples remained, no transient pin found |
report.ok is True only on visibility PASS and residue == VERIFIED.
Operational caveats
force_reclaimcan wait on long-running transactions.REINDEX INDEX CONCURRENTLYmust wait for older snapshots to finish — correct Postgres behaviour, but on a busy DB it could otherwise block indefinitely. The reclaim connection therefore runs with a boundedlock_timeout(default 5 s,reclaim_lock_timeout_ms;0disables). On timeout the run degrades toresidue: PENDING-lock-timeout— a transient, retryable state, not the permanentUNVERIFIED-managed— andforget()returns instead of hanging. A timed-outREINDEX CONCURRENTLYcan leave anINVALIDindex behind; drop it (DROP INDEX …) or a later run will flag it asUNVERIFIED-index-not-rebuilt.- Retry is an opt-in poll:
forget(..., retries=N, backoff_ms=M)re-attempts the reclaim while the residue is a transientPENDING-*(e.g. the blocking txn clears between attempts). It never turns anUNVERIFIED-*or a visibilityFAILinto a pass, and total added wait is bounded. - Zero-risk mode:
force=Falsedoes delete + verify only (noREINDEX/VACUUM), so it can never block on a lock. It reportsresidue: PENDING(compaction not performed). reclaim_statement_timeout_ms(default0/ off) is an extra hard cap on reclaim runtime. Leave it off unless you want to bound total work — unlikelock_timeout(which targets waits), it can cancel a legitimately longVACUUM/REINDEX.
Non-goals
- Not a signed erasure certificate / crypto-shred / cryptographic proof —
ForgetReportis deliberately an unsigned removal report. - Not machine-unlearning of fine-tuned weights — out of a library's reach; PII baked into model weights is explicitly out of scope.
- Not a hosted service or daemon — library only.
- v0.1 is pgvector-only — Qdrant, Weaviate, and framework (LangChain/LlamaIndex) integrations are deferred.
Status
Alpha (v0.1).
License
MIT © privatebydefault
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file vector_forget-0.1.0.tar.gz.
File metadata
- Download URL: vector_forget-0.1.0.tar.gz
- Upload date:
- Size: 24.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1d0f76e80dbaef26775edf84ba0f174e512633fd43dfd221c0be70aebaa81d86
|
|
| MD5 |
392776ed9214fcd52ad43f8069c0129f
|
|
| BLAKE2b-256 |
2d8340a564181853211324e2931e7ad1f1187bd5160e8a39234019f2e842c41b
|
Provenance
The following attestation bundles were made for vector_forget-0.1.0.tar.gz:
Publisher:
publish.yml on privatebydefault/vector-forget
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vector_forget-0.1.0.tar.gz -
Subject digest:
1d0f76e80dbaef26775edf84ba0f174e512633fd43dfd221c0be70aebaa81d86 - Sigstore transparency entry: 2164058810
- Sigstore integration time:
-
Permalink:
privatebydefault/vector-forget@f179ca54ec5f12d09a18f23e999e1385b5d56351 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/privatebydefault
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f179ca54ec5f12d09a18f23e999e1385b5d56351 -
Trigger Event:
release
-
Statement type:
File details
Details for the file vector_forget-0.1.0-py3-none-any.whl.
File metadata
- Download URL: vector_forget-0.1.0-py3-none-any.whl
- Upload date:
- Size: 19.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5f5e9f4938761bd11957aa5a3e105ad31a327ee8287b9164100a685f087b2df5
|
|
| MD5 |
853c6247c543013dad5216bba8937dd7
|
|
| BLAKE2b-256 |
be2368032fa47805ef3c8d10a80f3bf93f071d423de40c2441e97186549568c6
|
Provenance
The following attestation bundles were made for vector_forget-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on privatebydefault/vector-forget
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vector_forget-0.1.0-py3-none-any.whl -
Subject digest:
5f5e9f4938761bd11957aa5a3e105ad31a327ee8287b9164100a685f087b2df5 - Sigstore transparency entry: 2164059021
- Sigstore integration time:
-
Permalink:
privatebydefault/vector-forget@f179ca54ec5f12d09a18f23e999e1385b5d56351 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/privatebydefault
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f179ca54ec5f12d09a18f23e999e1385b5d56351 -
Trigger Event:
release
-
Statement type: