AI Engineer Roadmap All lessons Interactive Quiz MernStackDev GitHub

Metadata

Play the interactive lesson: MetadataRead on GitHub
On this page
  1. Explain it like I'm new
  2. Watch it
  3. How it works
  4. Code example
  5. When to use it / when not to
  6. Common mistakes
  7. Recap
  8. Interview questions
  9. Go deeper

Level: Beginner · Reading time: 6 min · Phase: 1 — RAG Foundations

In one line: Metadata is extra information stored next to each chunk — like source file, page, date, author or access level — used for citations, filtering and debugging.

Before you start: Embeddings and Chunking. You should know that a chunk is a small piece of text and an embedding is a list of numbers that captures its meaning.

Explain it like I'm new

A photo on your phone is not just pixels. It also stores when it was taken, where, and on which camera. That is metadata: "data about data". You can search "photos from Paris in 2024" without looking at a single pixel.

In RAG, every chunk should carry metadata alongside its text and its embedding. Without it, a chunk is an orphan sentence: you cannot say where it came from, whether it is out of date, or whether this user is allowed to see it.

Words to know:

Metadata is usually a small dictionary of fields (names) and values:

{"source": "security-handbook.pdf", "page": 12, "section": "Tokens",
 "updated": "2025-03-01", "team": "security", "access": ["staff"]}

It powers four things:

  1. Citations: "according to security-handbook.pdf, page 12".
  2. Filtering: only search documents from 2025, or only this customer's files.
  3. Context: adding the document title or section heading to the chunk text makes vague chunks clearer.
  4. Maintenance: find and delete all chunks from a document when it changes.

Watch it

Metadata: Extra facts stored next to each chunk make search, citing and debugging easier.
Metadata: Extra facts stored next to each chunk make search, citing and debugging easier.

It plays on a loop. Read the caption in the box, one step at a time. Want to go at your own pace? Step through it with Previous and Next.

How it works

  1. Capture during loading: file name, URL, page number, heading path, timestamps.
  2. Derive extra fields: language, document type, product, or tags from rules or an LLM.
  3. Attach the dictionary to every chunk created from that document, plus chunk-level fields (position, parent_id).
  4. Store it in the vector database with the vector. Most vector databases index metadata so filters are fast.
  5. Use it at query time to filter, then pass source info into the prompt for citations.
flowchart LR
  D[PDF: handbook.pdf] --> L[Loader adds source, page]
  L --> C[Chunker adds section, chunk_index]
  C --> S[(Vector DB: vector + text + metadata)]
  S --> F[Filter: team = security]
  F --> P[Prompt shows: source, page]

Worked example

Suppose you load auth.md and it becomes two chunks. Each chunk gets its own metadata dictionary:

  1. Chunk auth.md#0 has text "Refresh tokens belong in an httpOnly cookie." and metadata {"source": "auth.md", "team": "security", "year": 2025}.
  2. Chunk auth.md#1 has text about something else and team set to platform.
  3. A user asks "where do refresh tokens go?" with the filter team = security.
  4. The database drops chunk #1 because its team does not match, then searches only #0.
  5. The answer cites the metadata: "(auth.md, chunk 0)".

Later auth.md changes. Because both chunks carry source: auth.md, you can find and delete both, then insert the new ones.

Code example

Stdlib demo: attach metadata during chunking, then cite it.

def chunk_with_metadata(doc_text, source, updated, team, size=200):
    from rag_chunkers import recursive_chunks          # from this phase's code/ folder
    chunks = []
    for i, text in enumerate(recursive_chunks(doc_text, size)):
        chunks.append({
            "id": f"{source}#{i}",
            "text": text,
            "meta": {"source": source, "chunk_index": i, "updated": updated, "team": team},
        })
    return chunks

def format_sources(hits):
    return "\n".join(f"[{n}] ({h['meta']['source']}, chunk {h['meta']['chunk_index']}) {h['text']}"
                     for n, h in enumerate(hits, 1))

The DOCS list in code/rag_bm25.py stores {"text": ..., "meta": {...}} items and filters on them.

In real projects (Chroma, pip install chromadb):

import chromadb

col = chromadb.Client().create_collection("docs")
col.add(ids=["auth#0"], documents=["Refresh tokens belong in an httpOnly cookie."],
        metadatas=[{"source": "auth.md", "team": "security", "year": 2025}])
res = col.query(query_texts=["where do refresh tokens go?"], n_results=1,
                where={"team": "security"})
print(res["metadatas"][0][0]["source"])

When to use it / when not to

Common mistakes

Recap

If you remember only three things:

Interview questions

1. What metadata would you store for each chunk? <details><summary>Answer</summary> Chunk ID, document ID, source (file or URL), position (page, section, index), timestamps, and fields used for filtering such as tenant, team, language or access level. </details>

2. How does metadata help with document updates? <details><summary>Answer</summary> With a document ID on every chunk you can delete all old chunks for that document and insert the new ones, avoiding stale or duplicate results. </details>

3. Why prepend titles or headings to chunk text? <details><summary>Answer</summary> A chunk like "Step 3: restart the service" is ambiguous alone. Adding "Guide: Resetting VPN > Step 3" gives the embedding and the LLM the missing context. </details>

Go deeper

Prev: Sliding Window Chunking · Next: Vector Databases · Back to phase: Phase 1