Skip to main content

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

CI

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:

  1. Hard-delete — forces the physical reclaim (e.g. pgvector REINDEX + VACUUM) so the ghost vectors are actually gone, not just hidden.
  2. 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_reclaim can wait on long-running transactions. REINDEX INDEX CONCURRENTLY must 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 bounded lock_timeout (default 5 s, reclaim_lock_timeout_ms; 0 disables). On timeout the run degrades to residue: PENDING-lock-timeout — a transient, retryable state, not the permanent UNVERIFIED-managed — and forget() returns instead of hanging. A timed-out REINDEX CONCURRENTLY can leave an INVALID index behind; drop it (DROP INDEX …) or a later run will flag it as UNVERIFIED-index-not-rebuilt.
  • Retry is an opt-in poll: forget(..., retries=N, backoff_ms=M) re-attempts the reclaim while the residue is a transient PENDING-* (e.g. the blocking txn clears between attempts). It never turns an UNVERIFIED-* or a visibility FAIL into a pass, and total added wait is bounded.
  • Zero-risk mode: force=False does delete + verify only (no REINDEX/VACUUM), so it can never block on a lock. It reports residue: PENDING (compaction not performed).
  • reclaim_statement_timeout_ms (default 0 / off) is an extra hard cap on reclaim runtime. Leave it off unless you want to bound total work — unlike lock_timeout (which targets waits), it can cancel a legitimately long VACUUM/REINDEX.

Non-goals

  • Not a signed erasure certificate / crypto-shred / cryptographic proofForgetReport is 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

vector_forget-0.1.0.tar.gz (24.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

vector_forget-0.1.0-py3-none-any.whl (19.4 kB view details)

Uploaded Python 3

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

Hashes for vector_forget-0.1.0.tar.gz
Algorithm Hash digest
SHA256 1d0f76e80dbaef26775edf84ba0f174e512633fd43dfd221c0be70aebaa81d86
MD5 392776ed9214fcd52ad43f8069c0129f
BLAKE2b-256 2d8340a564181853211324e2931e7ad1f1187bd5160e8a39234019f2e842c41b

See more details on using hashes here.

Provenance

The following attestation bundles were made for vector_forget-0.1.0.tar.gz:

Publisher: publish.yml on privatebydefault/vector-forget

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for vector_forget-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5f5e9f4938761bd11957aa5a3e105ad31a327ee8287b9164100a685f087b2df5
MD5 853c6247c543013dad5216bba8937dd7
BLAKE2b-256 be2368032fa47805ef3c8d10a80f3bf93f071d423de40c2441e97186549568c6

See more details on using hashes here.

Provenance

The following attestation bundles were made for vector_forget-0.1.0-py3-none-any.whl:

Publisher: publish.yml on privatebydefault/vector-forget

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page