Metadata Tagging

By the Syncfusion DevTutorials team · Last updated

Extracting and attaching metadata to chunks: title, section path, author, dates, versions and access tags; header injection, filter labels and metadata-aware assembly; indexed fields, path tags and time and place scoping.

Think about a book in a library. The book contains its main content, but it also has labels that describe it. These labels may show the title, subject, edition, publication date, and where the book belongs.

These details help the librarian find the correct book, distinguish the latest edition from an older one, and apply rules about who can use it.

A RAG system uses similar information called metadata. Metadata is information that describes a document or chunk.

Metadata may include the document’s title, section, update date, version, topic, and access permissions. The system stores this metadata with each chunk. During retrieval, metadata can help filter the results. For example, the system can search only current policies for a particular region or return documents that the user is allowed to view.

A Real-World Example

Brightgrid has different prices for different regions. Brightgrid Plus costs $3.99 in the US and €3.79 in Europe. The company also has internal articles that only support agents should see.

Without metadata, the search system may return the US price to a customer in Europe or show an internal support article to a customer.

To prevent this, Brightgrid adds metadata such as:

region audience updated status

Before sending information to the model, the system filters the results using the user’s region and access level. This helps customers receive information for the correct region and prevents customer-facing searches from returning agent-only content.

In simple words: metadata lets search ask not just "what matches?" but "what matches, for this person, right now?".

What You Will Learn

In this lesson, you will learn:

  1. How to extract title, section path, author, dates, versions and access tags.
  2. How to attach metadata to chunks: header injection, filter labels and metadata-aware context assembly.
  3. How to prepare metadata for filtering: indexed fields, keyword alignment, hierarchical paths, and time and place scoping.

In the RAG Lab's Chunker tab, the Extracted metadata line shows what was pulled out of the raw page: title, updated date, author and sections. Turn on Add a metadata header to each chunk to see headers added to every chunk.

A chunk carries its text plus metadata fields. The fields are stored in the index for filtering, and a short header is added to the text so the chunk makes sense on its own and the model can see its source and date.

Metadata Extraction

Title, Section Path, and Author Attribution

The basics every chunk should know:

  • Title of the document it came from.
  • Section path, the headings above it (from the document tree you built in the last lesson), like Returns and refunds policy > How long you have.
  • Author or owner: who wrote it, or which team is responsible. When something is wrong, you know whom to ask.
  • Source location: the file, URL or page number, so citations can link back.

Many documents already store this. Markdown files often start with front matter, a block of key: value lines between --- markers. Word and PDF files have built-in properties. Web pages have a <title> tag and meta tags.

Creation Dates, Modification Timestamps, and Versions

Dates are some of the most useful metadata in RAG:

  • created and updated dates let you prefer recent documents and show freshness in answers.
  • A version number or a status (current, draft, archived) lets you hide old versions completely.

Use one date format everywhere. YYYY-MM-DD (like 2026-03-01) is a good choice: it's unambiguous, and it sorts correctly as plain text.

Access Control & Permission Tags

Access tags say who may see a chunk: public, customers, agents, finance-team, or a list of user groups. They must be copied from the source system's permissions when you ingest, and kept up to date. Chapter 7 covers security fully. The key rule starts here: a chunk without correct access tags can leak.

Read front matter and headings into metadata

// Extract metadata from a document's front matter and headings.
const file = `---
title: Returns and refunds policy
author: Support Team
created: 2024-01-05
updated: 2026-03-01
version: 3
access: public
---
# Returns and refunds policy
## How long you have
You can return any product within 30 days.`;

const [, front, body] = file.split("---\n");
const meta = Object.fromEntries(
  front
    .trim()
    .split("\n")
    .map((l) => l.split(": "))
    .map(([k, v]) => [k, /^\d+$/.test(v) ? Number(v) : v]),
);
meta.sections = (body.match(/^## .*/gm) ?? []).map((h) => h.slice(3));
console.log(JSON.stringify(meta, null, 2));

Expected output:

{
  "title": "Returns and refunds policy",
  "author": "Support Team",
  "created": "2024-01-05",
  "updated": "2026-03-01",
  "version": 3,
  "access": "public",
  "sections": [
    "How long you have"
  ]
}

Attaching Metadata to Chunks

Chunk Metadata Header Injection

Metadata can be stored in two places, and good systems use both:

  • As fields next to the chunk in the index. They are used for filtering and sorting, and aren't seen by the embedding model.
  • As a short text header added to the start of the chunk's text, like [Brightgrid Plus subscription > Price]. This is header injection. It helps the embedding capture what the chunk is about. A chunk that just says "It costs $3.99 a month" doesn't say what costs that. It also helps the model, which sees the source in the prompt.

Attach fields and a header to each chunk

// Attach metadata to each chunk, and add a short header so the text stands on its own.
const doc = {
  title: "Brightgrid Plus subscription",
  updated: "2026-07-01",
  access: "public",
};
const chunks = [
  { section: "Price", text: "It costs $3.99 a month or $39 a year." },
  { section: "Cancelling", text: "Cancel any time in Account, then Subscription." },
];

const withMeta = chunks.map((c, i) => ({
  id: `brightgrid-plus#${i + 1}`,
  metadata: {
    title: doc.title,
    sectionPath: `${doc.title} > ${c.section}`,
    updated: doc.updated,
    access: doc.access,
  },
  // The header is part of the text that gets embedded and shown to the model.
  text: `[${doc.title} > ${c.section}] ${c.text}`,
}));

console.log(JSON.stringify(withMeta[0], null, 2));
console.log(withMeta[1].text);

Expected output:

{
  "id": "brightgrid-plus#1",
  "metadata": {
    "title": "Brightgrid Plus subscription",
    "sectionPath": "Brightgrid Plus subscription > Price",
    "updated": "2026-07-01",
    "access": "public"
  },
  "text": "[Brightgrid Plus subscription > Price] It costs $3.99 a month or $39 a year."
}
[Brightgrid Plus subscription > Cancelling] Cancel any time in Account, then Subscription.

See metadata headers on chunks

Filtering Labels (Region, Role, Product Tier)

Filter labels are short, fixed values you can match exactly: region: EU, audience: agent, tier: plus. They answer "is this chunk meant for this user?". Keep the list of allowed values small and consistent. EU, eu and Europe must not all appear.

Filter chunks by region, role and tier

// Filtering labels: only keep chunks this user is allowed and meant to see.
const chunks = [
  {
    id: "plus-price-us",
    region: "US",
    roles: ["customer"],
    tier: "plus",
    text: "Plus costs $3.99 a month.",
  },
  {
    id: "plus-price-eu",
    region: "EU",
    roles: ["customer"],
    tier: "plus",
    text: "Plus costs EUR 3.79 a month.",
  },
  {
    id: "refund-override",
    region: "US",
    roles: ["agent"],
    tier: "any",
    text: "Agents may refund up to $100 without approval.",
  },
  {
    id: "free-schedules",
    region: "any",
    roles: ["customer"],
    tier: "free",
    text: "Free accounts get 5 schedules.",
  },
];

function allowed(c, user) {
  const regionOk = c.region === "any" || c.region === user.region;
  const roleOk = c.roles.includes(user.role);
  const tierOk = c.tier === "any" || c.tier === user.tier;
  return regionOk && roleOk && tierOk;
}

const user = { region: "EU", role: "customer", tier: "plus" };
console.log(
  "EU Plus customer sees:",
  chunks
    .filter((c) => allowed(c, user))
    .map((c) => c.id)
    .join(", "),
);
const agent = { region: "US", role: "agent", tier: "any" };
console.log(
  "US agent sees:        ",
  chunks
    .filter((c) => allowed(c, agent))
    .map((c) => c.id)
    .join(", "),
);

Expected output:

EU Plus customer sees: plus-price-eu
US agent sees:         refund-override

Metadata-Aware Context Assembly

When chunks are packed into the prompt, metadata should come along:

  • Put the date and source type in each source tag, so the model, and the user, can see how fresh and how trusted it is.
  • Order by trust: current articles first, then tickets. Leave archived ones out.
  • Tell the model in its instructions: "If sources disagree, prefer the most recently updated article."

Order by trust and show metadata in the prompt

// Metadata-aware context assembly: show dates and sources, and put current facts first.
const retrieved = [
  {
    id: "returns-policy-2023#1",
    updated: "2023-02-10",
    status: "archived",
    text: "Returns within 14 days.",
  },
  {
    id: "returns-policy#1",
    updated: "2026-03-01",
    status: "current",
    text: "Returns within 30 days.",
  },
  {
    id: "ticket-3120#2",
    updated: "2023-05-04",
    status: "current",
    kind: "ticket",
    text: "Returns only within 14 days.",
  },
];

const trust = (c) => (c.status === "archived" ? 0 : c.kind === "ticket" ? 1 : 2);
const ordered = retrieved
  .filter((c) => c.status !== "archived")
  .sort((a, b) => trust(b) - trust(a) || b.updated.localeCompare(a.updated));

const context = ordered
  .map(
    (c, i) =>
      `<source id="S${i + 1}" chunk="${c.id}" updated="${c.updated}" type="${c.kind ?? "article"}">${c.text}</source>`,
  )
  .join("\n");
console.log(context);

Expected output:

<source id="S1" chunk="returns-policy#1" updated="2026-03-01" type="article">Returns within 30 days.</source>
<source id="S2" chunk="ticket-3120#2" updated="2023-05-04" type="ticket">Returns only within 14 days.</source>

Metadata Filtering Preparation

Indexing Metadata Fields in Vector Databases

Vector databases store a payload next to each vector: a small record of metadata fields. To filter fast, you tell the database which fields to index. Without an index on a field, the database must check every record, which is slow at scale. Index the fields you filter on often, like region, audience, status and updated.

Structured Keyword Filter Alignment

Filters only work if the values match exactly. Decide the allowed values once and use them everywhere: in ingestion, in the index, and in the code that builds filters from the user's profile. If a user's profile says region: "Europe" but chunks say region: "EU", the filter silently returns nothing.

Hierarchical Path Tagging

A path tag like products/lamps/lumo/setup stores where a chunk sits in a hierarchy. A prefix match then filters a whole branch at once: everything under products/lamps covers every lamp, while products/lamps/lumo/setup covers only Lumo setup.

Spatial & Temporal Metadata Scoping

Some facts are true only somewhere or only at some time:

  • Spatial: a price for the EU, a law for California, a shipping rule for Canada.
  • Temporal: a promotion valid in November, a policy in force from March 2026, an old procedure that ended in 2023.

Store region and the dates validFrom and validTo. Then filter by the user's location and today's date, or by the date the user asks about.

Filter by path prefix and valid dates

// Hierarchical paths and time scoping: filter by "where in the docs" and "when it applies".
const chunks = [
  { id: "a", path: "products/lamps/lumo/setup", validFrom: "2024-01-01", validTo: null },
  {
    id: "b",
    path: "products/lamps/lumo/troubleshooting",
    validFrom: "2024-01-01",
    validTo: null,
  },
  { id: "c", path: "products/hubs/setup", validFrom: "2024-01-01", validTo: null },
  {
    id: "d",
    path: "products/lamps/lumo/setup",
    validFrom: "2022-01-01",
    validTo: "2023-12-31",
  },
];

const under = (prefix) => (c) => c.path === prefix || c.path.startsWith(prefix + "/");
const validOn = (date) => (c) =>
  c.validFrom <= date && (c.validTo === null || date <= c.validTo);

const today = "2026-10-09";
console.log(
  "lamps, today:     ",
  chunks
    .filter(under("products/lamps"))
    .filter(validOn(today))
    .map((c) => c.id)
    .join(", "),
);
console.log(
  "lumo setup, today:",
  chunks
    .filter(under("products/lamps/lumo/setup"))
    .filter(validOn(today))
    .map((c) => c.id)
    .join(", "),
);
console.log(
  "lumo setup, 2023: ",
  chunks
    .filter(under("products/lamps/lumo/setup"))
    .filter(validOn("2023-06-01"))
    .map((c) => c.id)
    .join(", "),
);

Expected output:

lamps, today:      a, b
lumo setup, today: a
lumo setup, 2023:  d

Store the date the fact applies (validFrom), not only the date the file was edited (updated). A small typo fix shouldn't make an old policy look brand new.

Never put access rules only in the text header. The model might ignore them, and a clever prompt might trick it. Access must be enforced by filtering before retrieval, using metadata fields.

Try how dates show up in the lab. Ask about returns with archived articles included, and compare the dates on the sources:

Compare source dates

Common mistakes

  • Missing access tags. Restricted chunks become searchable by everyone.
  • Inconsistent values. Filters quietly match nothing, or the wrong things.
  • No dates. Old and new versions look the same to search and to the model.
  • Headers that are too long. A 40-word header on a 30-word chunk drowns the content. Keep headers short.
  • Metadata not updated when the source changes, so a deleted permission or a new version never reaches the index.

Exercises

Each exercise ends with a value on the last line. Press Check to compare it with the expected answer.

What to Remember

  • Metadata says where a chunk came from, when it applies, who may see it and what it's about.
  • Extract title, section path, author, created and updated dates, version, status and access tags.
  • Attach metadata as fields (for filtering) and as a short text header (for embeddings and the model).
  • Use small, consistent filter labels such as region, role and tier, and index the fields you filter on.
  • Use path prefixes for branches of the hierarchy, and validFrom and validTo with region for time and place.
  • Enforce access with metadata filters before retrieval, never with instructions alone.

Next: Chunking Strategies, the different ways to cut documents into pieces.