This episode covers designing metadata schemas for RAG: id conventions, timestamps, sources, tenants and namespaces, then chunking strategies that determine retrieval accuracy: chunk size, overlap, and choosing the right granularity.

So far we have focused on how ChromaDB works. Episode 10 flips the focus to what you put in — data modeling and chunking strategy. One truth is often overlooked: retrieval quality is not determined by the vector database, but by the quality of the data that goes into it. Even the best embedding function cannot save a bad chunk.
We will cover the recommended metadata schema for RAG, structured id conventions, then chunking strategies: size, overlap, and granularity. Treat this episode as your data architecture design before you build for production.
Metadata is the context that accompanies every document. For a healthy RAG application, at minimum store:
metadata = {
"source": "https://blog.example.com/docker-intro",
"timestamp": "2026-07-15T08:30:00Z",
"title": "Pengenalan Docker untuk Pemula",
"category": "devops",
"tenant": "tutorial",
}The metadata example above enables filters like where={"category": "devops"} while also displaying the title in retrieval results without opening the original document.
The biggest temptation: storing every attribute of the source. Metadata that is never used in queries only wastes storage and slows serialization. A simple rule: store only what you will filter on, display in the UI, or use as extra context for the LLM.
Info
A metadata schema is not a decision you can change on a whim. Changing the schema after a lot of data means migrating the entire collection. Design it carefully up front — episode 11 covers the tools for migration if you are already past that point.
IDs in ChromaDB do not have to be sequential numbers — they can be any string. Take advantage of this to create self-describing IDs, especially for deduplication when a pipeline is re-run:
import hashlib
def buat_id(source, chunk_index):
hash_teks = hashlib.md5(source.encode()).hexdigest()[:8]
return f"chunk-{hash_teks}-{chunk_index:04d}"
ids = [buat_id("docker-intro", i) for i in range(5)]buat_id(source, chunk_index) produces deterministic IDs: re-running the same pipeline produces the same IDs, so the upsert from episode 4 updates instead of duplicating. This is an important pattern for repeated ingestion.
Add a prefix that marks the data type or tenant:
id_dok = f"{tenant}:{source}:{chunk_index}"id_dok = f"{tenant}:{source}:{chunk_index}" creates IDs like tutorial:docker-intro:0003. When troubleshooting or auditing, an ID immediately tells you where a document came from without opening its contents.
Embedding models have a context length limit — text that is too long embedded as a single vector loses meaning detail. Additionally, retrieval works best when each vector represents one focused idea. That is why long documents must be split into smaller chunks.
def chunk_paragraf(teks):
return [p.strip() for p in teks.split("\n\n") if p.strip()]
chunks = chunk_paragraf(dokumen)The function chunk_paragraf(teks) splits a document by paragraph — the simplest approach, already far better than storing the whole document.
There is no magic number, but a commonly used guideline: 250-500 tokens per chunk for general content, and smaller (100-200 tokens) for dense technical content. Too large a size makes one vector mix many topics; too small cuts off context.
The main consideration is your embedding model. Check the model's token limit — chunks should stay below that limit with room for overlap.
When a document is split, related sentences can end up in different chunks. Overlap — including some of the last sentences from the previous chunk — preserves continuity:
def chunk_overlap(teks, ukuran=300, overlap=50):
tokens = teks.split()
hasil = []
for i in range(0, len(tokens), ukuran - overlap):
potongan = tokens[i : i + ukuran]
hasil.append(" ".join(potongan))
if i + ukuran >= len(tokens):
break
return hasilchunk_overlap(teks, ukuran=300, overlap=50) advances 250 words per iteration with 50 words of overlap. Common overlap values: 10-20 percent of the chunk size.
Granularity depends on the type of questions you will answer:
The best way to choose: do not guess. Take a sample of 50-100 real questions, run retrieval for several granularities, and measure which is most accurate. This low-cost experiment pays off far more than picking a number from someone else's tutorial.
Summarizing this episode into one healthy ingestion pattern:
for idx, chunk in enumerate(chunk_overlap(dokumen)):
collection.upsert(
ids=[f"{tenant}:{source}:{idx:04d}"],
documents=[chunk],
metadatas=[{
"source": source,
"timestamp": timestamp,
"title": judul,
"tenant": tenant,
"chunk_index": idx,
}],
)The pattern above — deterministic IDs, complete metadata, and collection.upsert(...) — produces a clean collection that deduplicates automatically when re-run and is easy to filter. This is the template we will use for the rest of the series.
Episode 10 closes out the data phase: metadata schemas that support filtering, self-describing and deterministic id conventions, and a chunking strategy with size, overlap, and granularity tailored to your question types. Your RAG retrieval quality now depends on the quality of the data you design — and you know how to design it.
Key takeaways:
upsert deduplicate.In the next episode, episode 11, we will discuss persistence, import and export — how PersistentClient works and its storage location, backup and restore strategies, snapshots, and import-export data using parquet and collection dump-load for migrating between environments. Your data is starting to be valuable; time to protect it.