Independent teaching notes. Not affiliated with xAI or Grok.

Practice page · Week 3

Tombstones in retrieval

When a document changes, the old version has to stop answering. A tombstone marks old chunks dead so the query path skips them, without rebuilding the whole index.

Extracted from Week 3: Search and retrieval. Northline and Mill City Permits are fictional practice. Portfolio policy.

The failure this prevents

Northline's leave policy was 12 weeks. Version 2 says 16. If both versions stay live, the 12-week chunk and the 16-week chunk answer the same question on alternate queries, and rank decides which one a person sees. Rank is not an access control and not a freshness control.

Stable ids and a version

Give every chunk a stable id: the doc_id plus a section key you control, not a random uuid minted on every ingest run. Random ids make every edit look like a new document, and the old chunk stays searchable. Store a version integer on each chunk.

When version 2 arrives, the worker upserts the new chunks, then tombstones chunks of that doc_id whose version is lower. Deleting them immediately is also fine if your store makes it easy. What is not fine is leaving both live and hoping rank saves you. Tombstones are easier to audit than silent deletes, because you can see what was retired and when.

In the lab

The lab's Index in retrieve.py is a dict, not an approximate-nearest-neighbor structure. upsert replaces by chunk_id. Tombstoning is one method:

def tombstone_doc(self, doc_id, *, below_version=None):
    for row in self.chunks.values():
        if row["doc_id"] != doc_id:
            continue
        if below_version is None or row["version"] < below_version:
            row["tombstoned"] = True

The query skips any row with tombstoned set, in the same filter that applies the ACL. That is why the two belong together: see ACL before rank.

The leave edit, step by step

  1. Start: leave-1, doc leave, version 1, "Parental leave is 12 weeks for every employee."
  2. Publish version 2. The worker upserts leave-2, version 2, same ACL.
  3. The worker calls tombstone_doc("leave", below_version=2). leave-1 is now dead. comp-1 is untouched.
  4. An employee asks about parental leave. Candidates: leave-2 only. The answer says 16 weeks because v1 is dead, not because someone deleted the index by hand.

test_employee_does_not_see_compensation_or_old_leave asserts that leave-2 is present, leave-1 is absent, and the text says 16 weeks. That test is the spec.

When to rebuild, and when not to

You do not rebuild an index because one page changed, if the store supports insert and delete. pgvector's HNSW index is maintained as rows change. A full rebuild is the right tool when you change the embedding model or the dimension, because every vector is stale at once. It is the wrong tool for a typo fix. If your engine only supports rebuilds, batch the day's edits, rebuild on a schedule, and keep tombstones in the metadata so a query during the batch still drops dead versions. "Eventually consistent" with no bound is how a shopper gets the old rule the afternoon you changed it.

In a case

Mill City Permits answers only from a named list of published guide pages. When a guide page changes, the same rule applies: the old version is tombstoned, and the answer comes from the page the owner published now. A question outside the list still gets "outside this corpus."

This studio's retrieval checks run on a three-document toy. They prove the filter and the tombstone still work. Do not report them as a recall number for a real search system.

Go to Week 3: Search

Last technically reviewed: October 2026. Tested with: Python 3, stdlib, SQLite, stand-in model. Not a live provider.