Short answer: when a deleted logistics record still appears in semantic search, stop returning it at the authoritative read boundary immediately, then remove every derived vector and cached result asynchronously with a verifiable completion record. A vector delete alone is not an erasure protocol. It can miss duplicate embeddings, race an indexing worker, or leave an old search response in a cache. For GDPR erasure, distinguish the time at which a record becomes inaccessible from the time its copies have actually been erased; the latter needs an inventory and evidence, not an empty search page.
What must deletion mean for a near-duplicate lookup?
Consider a shipment exception record containing a recipient address and a free-text delivery note. A semantic matcher embeds the note, retrieves likely duplicate exceptions, and then shows the underlying records to an operator. One source record may generate several chunks; the same record may be represented in a regional index and a rebuild in progress. Deleting the source row while leaving any of those derived entries searchable creates a disclosure path. Removing only the vectors may leave the note in the source store or in a response cache.
The invariant is narrower than "the index has caught up": no user-visible result may reference a record whose authoritative state is erased or pending erasure. A second invariant covers propagation: every derived copy must be enumerated, removed, and checked before marking the erasure workflow complete. Access suppression is a fast boundary; physical deletion is a separate obligation. Article 17 of the GDPR describes the right to erasure and its exceptions, while Article 19 addresses notification to recipients where applicable. A privacy decision about retention exceptions belongs to the responsible legal process, not to a similarity threshold.
There is a cost consequence here. In a logistics corpus, embedding every repeated status update makes both candidate search and deletion fan-out larger. Choose the unit of indexing deliberately: one vector per eligible record reduces deletion targets but can flatten meaningful note differences; chunking improves retrieval granularity while multiplying IDs that must be tracked. That is an architectural trade-off, not a benchmark claim.
Why does a deleted document still appear in search results?
First trace the displayed result backward: response cache, reranker input, candidate index, embedding job, and source record. Log stable internal record IDs and index generation IDs, without logging the address or note itself. A fresh query against the candidate index and a replay of the full application path answer different questions. A hit in only the latter points toward response caching or a downstream materialized view. A hit in the index may be an unremoved chunk, a replica that has not applied deletion, or a writer that reinserted a vector after the delete.
The race is easy to overlook. An embedding worker reads a record, the erasure request arrives, the worker finishes its expensive computation, and it publishes the old vector. A delete-by-vector-ID issued between those steps appeared to succeed. The record is back. Do not infer durable erasure from one successful delete acknowledgement. Suppose the matcher indexed a shipment exception as a summary vector and two note chunks, and a regional rebuild is consuming a snapshot taken before the erasure request. Deleting the three active IDs still leaves the rebuild's input eligible unless its publish step checks the authoritative state; meanwhile, a cached response might hold the old note even if both indexes are clean. Each location has its own deletion confirmation and its own opportunity to lag. The useful incident question is therefore not "did the delete request succeed?" but "which path produced this candidate, from which source generation, and at what point was permission to expose it checked?" Without those identifiers, a clean query after a manual retry cannot distinguish a fixed race from a temporarily hidden result.
One acknowledgement isn't proof.
| Decision | Index-cost effect | Failure boundary | Appropriate use |
|---|---|---|---|
| Delete vectors by tracked IDs only | No extra read filter; cost grows with copies per record | Late writers, stale caches, and forgotten IDs can reintroduce results | Derived cleanup after access has already been blocked |
| Filter candidates against authoritative record state | Adds state checks to the serving path | A filter omitted from another retrieval path can expose a hit | Immediate suppression while cleanup proceeds |
| Rebuild an index from eligible source records | Pays rebuild and temporary storage costs | Publishing an old snapshot can restore erased records | Recovery or periodic reconciliation with a guarded publish step |
The table does not offer three interchangeable fixes. Use the state check as the visibility gate, maintain an explicit ID-to-derived-copy inventory for cleanup, and treat rebuilds as a way to verify or recover the materialized index. If the state lookup is temporarily unavailable, fail closed for affected results.
A missing state row is not permission to display personal data.
How should the critical path prevent reinsertion?
Give each source record a monotonic generation and a durable erasure state. The following Python sketch deliberately leaves persistence and search behind interfaces: a production transaction must atomically record the state change and an outbox event in the authoritative store, while the index writer must check the current generation immediately before publishing. The response gate applies to every candidate, including those fetched from caches. A generation check alone is insufficient if the check and publish can race; enforce a per-record fence at publication, or recheck after publication and delete a vector that lost the race.
from dataclasses import dataclass
from typing import Iterable, Protocol
@dataclass(frozen=True)
class State:
generation: int
erased: bool
@dataclass(frozen=True)
class Candidate:
record_id: str
generation: int
score: float
class StateStore(Protocol):
def get(self, record_id: str) -> State | None: ...
def visible(candidates: Iterable[Candidate], store: StateStore) -> list[Candidate]:
result = []
for candidate in candidates:
state = store.get(candidate.record_id)
if state is not None and not state.erased and state.generation == candidate.generation:
result.append(candidate)
return result
That function is the last gate before a candidate becomes a displayed record or enters a generated answer. It is not the deletion worker. The worker consumes an erasure event, deletes all tracked vector IDs for the record across active and rebuilding generations, invalidates cached responses, and records individual outcomes; retrying a delete should remain harmless. Keep the tombstone long enough to fence delayed jobs and rebuilds under the applicable retention policy. If permanent removal of the tombstone is required, replace it with an equivalent durable exclusion mechanism before dropping it. Otherwise an old snapshot can make an erased record look eligible again.
For a retrieval-augmented response, suppress the record before assembling context, not only when formatting citations. Once a deleted note enters a model prompt, hiding its citation cannot retract the content it influenced. Retrieval-augmented generation explicitly depends on retrieved external material; this is why the candidate boundary matters even when the final output contains no document IDs.
What proves completion, and when is a rebuild worth it?
An empty result for one query proves little. Test the old exact phrase and close semantic paraphrases, but also query by every tracked derived ID across index generations and check that the source and serving caches have been cleared under the applicable policy. Record the erasure request ID, source-state transition, derived-copy inventory, delete acknowledgements, retry state, and final verification. The verifier should be able to distinguish "not currently returned" from "all known copies removed." Measure outstanding erasures, age of the oldest pending erasure, late-write rejections, inventory mismatches, and filter failures. Alert on a stalled workflow, not merely on a high search latency percentile.
Rebuilds are attractive when per-record cleanup history is incomplete, but they can temporarily double index storage and write work, which matters when near-duplicate detection is applied to large streams of status events. Build from an eligibility-filtered snapshot, replay state changes through a defined cutover point, and refuse publication if the new index contains IDs marked erased. That cost may be justified for recovery; it is a poor primary response to each individual request.
The rejected design is "delete the vector and trust search to confirm it." It is adequate only when the vectors are disposable, no personal data or derived output can be exposed, and delayed writers and caches are outside the system's requirements. For erasure-sensitive logistics matching, make visibility depend on authoritative state and make cleanup auditable. Those two guarantees solve different problems.
Top comments (0)