RAG Freshness Is an Indexing Contract

Aug 5 2026 · 9 min · Sieon

Stale RAG answers rarely start as prompt failures. They start when the index has no owner, no receipt for what was written, no delete path, and no freshness tests. The practical fix is to treat retrieval freshness as a contract around indexing, not a sentence inside the system prompt.

Why freshness failures feel like model failures

A stale answer usually arrives wearing a model-shaped disguise. The user asks a question, the assistant cites old policy text, and the first reaction is to tune the prompt. Add “prefer recent information.” Add “verify the latest document.” Add “do not use outdated context.” Those instructions can help the model express uncertainty, but they cannot make the retrieved context fresh.

RAG pipelines separate retrieval from generation. The application loads documents, splits them, embeds them, stores records in a vector database or retrieval system, and then gives selected context to the model. LangChain’s RAG tutorial describes this shape as a pipeline that connects loaders, vector stores, retrievers, and generation logic. OpenAI’s retrieval documentation describes the same broad pattern from the platform side, where files or vector stores supply context before the model answers. Those sources support the boring but important point: if retrieval selects stale records, the model is already downstream of the failure.

That is why the operational question should change. Do not start with “How do we prompt the model to avoid stale facts?” Start with “What contract proves this index represents the current source of truth?”

The freshness contract

Freshness is not a feeling. It is a set of promises the indexing system can make and prove.

A useful RAG freshness contract has five fields:

Field Question it answers Failure if missing
Source identity Which source record produced this chunk? Duplicate or orphaned chunks survive updates
Source version Which revision was indexed? Old and new text coexist silently
Write receipt When did the index accept the record? Operators cannot tell whether ingestion lagged or failed
Delete receipt Which prior records were removed or superseded? Deleted policy, pricing, or runbook text remains retrievable
Freshness test Which query proves the current answer path uses the new record? Evaluation passes on relevance while failing on currency

Pinecone’s upsert documentation is useful evidence here because it treats index updates as record writes. Weaviate’s batch import documentation makes the same engineering shape visible from another angle: ingestion is a data import path that has to move objects from a source into a retrieval system. Chroma’s collection documentation also frames retrieval storage around explicit collection data. The vendors differ in API details, but the mental model is stable. Indexing is a write path.

Once indexing is a write path, it deserves the same operational treatment as any other write path. It needs idempotency, retries, versioning, delete semantics, observability, and tests. If those sound heavier than a weekend RAG prototype, that is the point. Freshness becomes expensive exactly when the prototype becomes useful.

Ingestion is a write path, not a batch chore

The simplest RAG demo usually has a script named something like ingest.py. It reads files, chunks text, embeds chunks, and writes them to a vector store. That script is fine as a starting point. The mistake is allowing the script to remain a private batch chore after the application starts answering real questions.

A production-oriented ingestion path should emit a receipt per source record, not just log “uploaded 1,242 chunks.” A useful receipt says:

{
  "source_id": "policy/pricing.md",
  "source_version": "2026-08-05T12:10:00Z",
  "chunk_count": 18,
  "index_namespace": "prod-support",
  "embedding_model": "text-embedding-3-large",
  "write_started_at": "2026-08-05T12:11:03Z",
  "write_finished_at": "2026-08-05T12:11:19Z",
  "supersedes": "2026-08-04T09:42:00Z"
}

This receipt gives operators a way to distinguish four very different incidents. The source may not have changed. The crawler may not have seen the change. The index write may have failed. The retrieval query may be filtering the new record out. Without receipts, all four look like “the model hallucinated.”

Weaviate’s import guide explicitly shows importing objects from a data source, including streaming records from a file-like source. That matters because real ingestion is not only a vector database call. It is a pipeline from source systems to retrieval systems. The more systems in that path, the more valuable receipts become.

Deletes and replacements need receipts too

Most teams notice freshness when adding information. The harder bug is stale survival.

A document changes from “refunds are available for 30 days” to “refunds are available for 14 days.” A naive ingestion script writes new chunks with the new sentence. If the old chunks are still in the index, retrieval may return either version depending on embedding similarity, metadata filters, and reranking. The model can then cite a paragraph that is no longer true, even though the new document was successfully ingested.

The fix is not to ask the model to prefer the latest paragraph. The fix is to make replacement semantics explicit. If a source record is authoritative, a new source version should supersede the old source version. If the retrieval system cannot enforce that automatically, the ingestion layer must do it before the record becomes eligible for retrieval.

A minimal rule is enough for many teams:

freshness_contract:
  identity_key: source_id
  version_key: source_version
  replacement_policy: remove_prior_versions_before_publish
  retrieval_filter: latest_only
  required_receipts:
    - write_finished_at
    - superseded_versions
    - freshness_probe_result

The important word is “publish.” Do not expose half-indexed replacements to live retrieval. Stage the new version, delete or mark the old version, run a freshness probe, and then move the new version into the serving namespace or mark it as eligible. Pinecone’s upsert-oriented documentation and Chroma’s collection data model both support this record-centric way of thinking. The article’s recommendation goes one step further: treat those records as a controlled publication path.

Evaluation must test freshness, not only relevance

A RAG evaluation set that only asks stable questions will miss freshness failures. “What does the refund policy say?” might pass in January and fail in August for reasons the evaluation never encoded. The expected answer changed, but the test did not.

LangSmith’s RAG evaluation tutorial is useful because it frames RAG quality as something to evaluate with examples and graders. That is the right foundation, but freshness requires a specific class of examples. The evaluation set should include questions whose expected answer depends on the newest source version, recent deletion, or corrected metadata.

A practical freshness eval has cases like these:

Scenario Test query Expected behavior
Source update “What is the current refund window?” Answer uses the new policy version
Source deletion “Do we still support the legacy endpoint?” Answer says the old endpoint is no longer supported
Metadata change “Which regions support feature X?” Retrieval filters to the current region list
Partial ingestion “What changed in the latest runbook?” System refuses or marks uncertainty until indexing completes

The last row is easy to overlook. Sometimes the safest answer during indexing is not an answer. If the system knows that ingestion is in progress for a source, it can delay serving that source, answer with a freshness caveat, or route the request to a fallback. That is a product decision, but it can only be made if the indexing control plane exposes state.

A minimal production pattern

You do not need a large platform team to improve RAG freshness. You need a small contract that sits between source systems and retrieval.

One minimal pattern looks like this:

from dataclasses import dataclass
from datetime import datetime, timezone

@dataclass(frozen=True)
class SourceRecord:
    source_id: str
    source_version: str
    text: str

@dataclass(frozen=True)
class IndexReceipt:
    source_id: str
    source_version: str
    chunk_count: int
    write_finished_at: str
    freshness_probe: str


def index_source(record: SourceRecord, vector_store) -> IndexReceipt:
    chunks = split_into_chunks(record.text)
    vector_store.remove_where(source_id=record.source_id)
    vector_store.add(
        ids=[f"{record.source_id}:{record.source_version}:{i}" for i, _ in enumerate(chunks)],
        documents=chunks,
        metadatas=[{
            "source_id": record.source_id,
            "source_version": record.source_version,
            "latest": True,
        } for _ in chunks],
    )
    probe = run_freshness_probe(record.source_id, record.source_version)
    return IndexReceipt(
        source_id=record.source_id,
        source_version=record.source_version,
        chunk_count=len(chunks),
        write_finished_at=datetime.now(timezone.utc).isoformat(),
        freshness_probe=probe,
    )

This is not a complete implementation. It intentionally hides chunking, embedding, retries, and vector store client details. The point is the shape. Replacement happens before serving. Metadata carries identity and version. A probe confirms the current version is retrievable. A receipt survives after the write.

For small systems, the receipt can be a database row or even an append-only JSONL file. For larger systems, it can become an ingestion table, event stream, or workflow state machine. The interface matters more than the storage choice.

What reranking can and cannot solve

Reranking can improve which retrieved chunks are most relevant to the question. It cannot reliably remove obsolete records if the index still serves them as eligible context. A reranker is not a source-of-truth manager.

The same is true for prompt instructions, answer validators, and citation formatting. They are useful downstream controls. They can make stale context easier to notice. They can refuse unsupported answers. They can force the assistant to show where an answer came from. But they cannot prove the index has completed the latest write, deleted prior versions, or filtered to the serving namespace.

That boundary is healthy. Generation should be responsible for reasoning over context. Retrieval should be responsible for selecting context. Indexing should be responsible for making current context eligible. If all three responsibilities blur into the prompt, nobody owns freshness.

Decision rule

When a RAG system gives a stale answer, do not begin by editing the prompt. Ask for the indexing receipt.

If there is no receipt, build one. If there is a receipt but no delete record, fix replacement semantics. If there is a delete record but no freshness eval, add a test that fails on the stale answer. If all three exist, then investigate retrieval ranking and prompt behavior.

The sentence worth remembering is simple: freshness is not a model preference, it is an indexing contract.

FAQ

Is this just cache invalidation with embeddings?

It is related, but not identical. Cache invalidation usually decides whether a stored response or object is still usable. RAG freshness decides whether source-derived chunks are eligible to become model context. The contract needs cache-like thinking, but it also needs source identity, source version, retrieval filters, and evaluation cases.

Do small teams need a dedicated indexing service?

No. Small teams need explicit ownership before they need a service boundary. A script that writes receipts, removes prior versions, and runs freshness probes is already better than an invisible batch job. Create a service when multiple applications, sources, or teams need the same contract.

What should be logged for each indexed document?

Log source identity, source version, chunk count, embedding model, index namespace or collection, write start and finish time, superseded versions, and freshness probe result. Those fields let an operator answer whether the stale response came from source discovery, ingestion, replacement, retrieval filtering, or generation.

Can better reranking solve stale answers?

Only if the stale record should still be eligible and the ranking is wrong. If the stale record should have been deleted, superseded, or filtered out, reranking is the wrong layer. Fix the indexing contract first, then tune ranking.

References

  1. Pinecone Docs: Upsert records
  2. Weaviate Documentation: Batch import
  3. Chroma Docs: Add data
  4. LangChain Docs: Retrieval Augmented Generation (RAG)
  5. LangSmith Docs: Evaluate a RAG application
  6. OpenAI API Docs: Retrieval