A course tutor should split material at instructional boundaries, then treat every chunk as a versioned, observable record. The deciding constraint is freshness: a beautifully sized chunk is still wrong if retrieval serves last week's lesson after an instructor revises a definition.
For media-rich courses, I use four signals in the chunk contract: source version, content hash, publication state, and indexed-at time. Those fields make stale retrieval visible. They also let an ingestion worker replace changed segments without pretending that character count is meaning.
Tiny chunks raise recall candidates but strip away teaching context. Huge chunks preserve context but can bury the exact explanation a learner needs. The useful unit is the smallest independently teachable segment — a definition with its qualification, a worked step with its inputs, or a transcript passage anchored to the slide it explains.
What changes when chunk freshness becomes observable?
The before model is familiar: upload a course document, split it every N tokens, embed the pieces, and hope the retriever finds the right one. In that model, an editor can replace lesson 8 while old vectors remain searchable. A learner then gets an answer that looks grounded because it has a citation, yet the citation points to retired material. The retrieval layer has no way to explain which revision won.
The after model is a short pipeline you can say aloud: parse the source into instructional units; assign stable logical IDs; hash the normalized content; publish one active version per unit; index it; record when the index acknowledged it; and filter retrieval to active versions. The vector index is a derived view, not the source of truth.
That distinction matters.
A freshness dashboard can compare the source catalog with the index catalog. It should answer concrete questions: How many active lesson segments lack an indexed version? How old is the oldest indexing lag? Did a revision produce a new hash? Can every retrieved chunk be traced to a currently published source? These are operational checks, not model vibes.
The four signals have separate jobs. sourceVersion orders editorial revisions. contentHash detects an actual content change even if someone forgets to bump a human-facing label. publicationState keeps drafts and retired lessons out of learner retrieval. indexedAt measures propagation lag. Don't collapse them into one timestamp — a timestamp can't explain whether content changed, whether it was approved, or whether the index has caught up.
How should an online course tutor set retrieval chunk boundaries?
Start with the course's teaching structure, not a universal token target. A heading alone is usually too thin. A full module is usually too broad. Keep a concept statement with the caveat that changes its meaning; keep a quiz explanation with the relevant answer choices; keep a transcript segment with the slide or demonstration it describes. Then impose a size ceiling as a guardrail for the embedding and generation systems you actually run. I'm not sure there is a defensible universal ceiling across models and course formats; a small labeled sample from your own lessons resolves that uncertainty.
Media makes boundaries trickier because the text may be distributed across captions, slide notes, OCR, and instructor prose. Treat those as aligned evidence for one instructional unit when they explain the same moment. Store their offsets separately so a citation can open the video at the right time or highlight the right page. If a slide changes but the narration does not, the unit's combined hash changes and the ingestion job replaces the searchable version.
Consider a lesson that introduces confidence intervals. The instructor first states the concept, then works a numerical example across three slides, then gives a warning about interpreting repeated samples. A blind 500-token split might separate the warning from the claim it qualifies. One giant lesson chunk avoids that split but forces every query to retrieve unrelated setup. A better boundary keeps the definition and warning together, while the worked example becomes another chunk that carries a relation back to the concept. Retrieval can return both when the learner asks for an explanation plus an example. Now follow a revision through the system. Version 6 has a definition unit and an example unit. An editor changes two sentences in the warning, publishes version 7, and leaves the worked arithmetic untouched. The definition unit receives a new content hash and indexed ID; the example retains its hash. During ingestion, a trace shows version 7 published at 09:00, the changed unit indexed at 09:02:14, and the active pointer moved only after metadata verification. Until that move, queries continue to see the complete version 6 unit rather than a half-written mix. Afterward, a retrieval log that reports version 6 is plainly stale. Without the logical ID, hash, and timestamps, the same report degrades into “the tutor cited the wrong paragraph,” which gives an operator no clean place to start. With them, the operator can compare the source catalog, active pointer, and retrieved indexed ID in one trace.
This is the trade-off: semantic boundaries improve coherence, but they require parsers that understand headings, timestamps, and content roles. Fixed windows are easier to operate and remain useful for noisy, continuously arriving transcripts whose structure isn't trustworthy. For a live webinar feed, stick with overlapping time windows until editorial structure exists; for published courseware, prefer stable instructional units because revisions and citations need durable identity.
A copyable TypeScript chunk contract
The contract below is deliberately independent of a database or vector service. The example values are design choices for one hypothetical statistics course, not benchmark results. Notice that the logical ID excludes the revision while the indexed ID includes the content hash. That gives updates a stable join key and each searchable version an immutable identity.
import { createHash } from "node:crypto"; type PublicationState = "draft" | "published" | "retired"; type LessonUnit = { courseId: string; lessonId: string; unitKey: string; sourceVersion: number; title: string; text: string; startSeconds?: number; endSeconds?: number; publicationState: PublicationState; publishedAt?: string; }; type SearchChunk = LessonUnit & { logicalId: string; indexedId: string; contentHash: string; indexedAt: string; }; const normalize = (text: string): string => text.normalize("NFKC").replace(/\s+/g, " " ).trim(); const sha256 = (text: string): string => createHash("sha256").update(text).digest("hex"); function buildSearchChunk( unit: LessonUnit, indexedAt: string ): SearchChunk { if (unit.publicationState !== "published" || !unit.publishedAt) { throw new Error("Only published lesson units can enter learner search"); } const normalizedText = normalize(unit.text); const contentHash = sha256(normalizedText); const logicalId = [unit.courseId, unit.lessonId, unit.unitKey].join(":"); return { ...unit, text: normalizedText, logicalId, contentHash, indexedId: `${logicalId}:${contentHash.slice(0, 16)}`, indexedAt }; } const chunk = buildSearchChunk( { courseId: "stats-201", lessonId: "confidence-intervals", unitKey: "definition-and-caveat", sourceVersion: 7, title: "What a 95% confidence interval means", text: "A confidence interval describes a procedure...", startSeconds: 184, endSeconds: 276, publicationState: "published", publishedAt: "2026-08-28T09:00:00Z" }, "2026-08-28T09:02:14Z" ); An ingestion worker should upsert the new indexedId, verify the stored metadata, switch the active pointer for logicalId, and only then retire the previous indexed version. The sequence keeps a searchable active version during an update. Make the operation idempotent: receiving the same indexedId twice should converge on one record rather than duplicate it.
Logs should carry logicalId, indexedId, sourceVersion, and an ingestion run ID. A counter can track published chunks, indexed chunks, and failed client-side validation separately. A histogram can track publication-to-index lag. Alert on a sustained catalog mismatch or on lag that violates your team's freshness objective; don't alert on every individual retry. The exact threshold belongs to the course's publishing promise, not an arbitrary number copied from another system.
Can overlapping chunks solve missing context by themselves?
Overlap helps when a necessary sentence lands near a mechanical boundary. It doesn't establish ownership, publication state, or revision lineage. It can also multiply near-duplicate results, so several top matches may repeat the same paragraph while crowding out a complementary example. Use overlap as a local continuity tool after choosing meaningful units, not as the data model.
There is a second objection: re-indexing an entire lesson sounds simpler than tracking changed units. It often is simpler, and for a small catalog with infrequent publishing it may be the right call. The catch is that whole-lesson replacement gives you a coarse failure domain and weakens freshness diagnosis. Once publishing is frequent or lessons include long media transcripts, content hashes and stable unit IDs let the worker skip unchanged material and tell operators exactly what is pending.
The retrieval query should filter to published, active records before ranking. Return provenance with the answer: course, lesson, unit, source version, and a resolvable page or time range. A citation without a version is incomplete because the learner and the index may be looking at different revisions.
Keep the final decision rule crisp. Use fixed overlapping windows for unstructured live text. Use instructional units for edited courseware. In both cases, attach freshness signals and trace IDs so a wrong answer can be investigated as a retrieval event rather than dismissed as model behavior.
