You load a document, call an index builder, and receive an answer. What happened to the document between those steps? LlamaIndex provides useful abstractions, but a reliable application needs to inspect the data inside them.
Before you begin: Understand chunking, embeddings, and retrieval. Python knowledge is useful for the optional example.
Follow the data, not just the helper calls
A document represents source material and metadata. A node is a unit derived from that material, often a text chunk with links back to its source. An index organizes nodes for retrieval. A retriever chooses candidates; a query engine can combine retrieval with response generation.
These are separate responsibilities. If the wrong chunk is retrieved, changing the writer's tone will not repair the evidence. If parsing dropped a table header, a good embedding model cannot recover its missing meaning.
Consider two fictional notices. N1 says W1 has capacity 12. N2 says W2 has capacity 8. Keep IDs attached throughout indexing so that “capacity 12” can be traced to W1. A paragraph without its source metadata is harder to verify or update.
What text did the index actually receive?
A retrieval library cannot recover a timetable that the document parser scrambled before indexing. Inspect extracted text and chunk boundaries before tuning the answer prompt. Tables, repeated headers and reading order can change the meaning of the content that enters the index.
Trace one known fact from its PDF location to the parsed text and then to its indexed node. Preserve the source reference along that path. The challenge makes ingestion observable so a downstream answer failure does not send you straight to the wrong component.
A PDF table has classes in rows and days in columns.
Inspect the extracted reading order before embedding the text.
You can catch a class being paired with the wrong day before retrieval and generation use it.
Inspect nodes before making a model call
This complete package example requires llama-index-core. It performs local document splitting, with no LLM or embedding provider. Its API follows the official document and node guides reviewed on September 10, 2026; package execution is not implied by the course's standard-library test count.
from llama_index.core import Document
from llama_index.core.node_parser import SentenceSplitter
documents = [
Document(
text="W1 has capacity 12. Booking status is unknown.",
metadata={"source_id": "N1", "version": "1"},
),
Document(
text="W2 has capacity 8. It starts at 10:00.",
metadata={"source_id": "N2", "version": "1"},
),
]
splitter = SentenceSplitter(chunk_size=128, chunk_overlap=16)
nodes = splitter.get_nodes_from_documents(documents)
for node in nodes:
print(node.metadata["source_id"], node.get_content())
assert {node.metadata["source_id"] for node in nodes} == {"N1", "N2"}
The chunk limits refer to the splitter's token accounting, not a promise of exactly 128 words per node. Short source documents can remain much smaller. Inspect the produced boundaries instead of assuming a setting guarantees coherent chunks.
Add retrieval deliberately
The VectorStoreIndex abstraction can embed and index documents or nodes. Choose the embedding integration explicitly and record its model identifier. The query embedding must be compatible with the indexed vectors. Do not rely on an old tutorial's default provider or model being the right choice today.
When adding a query engine, configure its generation model separately from the embedding model. Embeddings choose evidence; generation writes an answer from that evidence. A provider may charge for both operations. Keeping them separate also lets you evaluate retrieval without generating a paragraph for every test.
Persistence is another decision. Saving an index can avoid repeating ingestion, but you must also preserve source versions, transformation settings, and compatible embedding configuration. A saved index is not automatically a complete per-user application database.
Find the upstream defect
A retrieved node says “12” but has lost the workshop ID and the word “capacity.” Would a larger generation model reliably fix the answer?
Inspect the missing context
No. The evidence no longer identifies what 12 measures. Repair parsing or chunk construction so that the number stays with its label and source. Then verify retrieval and answer behavior separately. A larger model might guess the original relationship, but guessing is the defect the application was meant to avoid.
For practice, add a revised N1 and decide whether to replace the old version, retain both with dates, or expose an explicit conflict. Test that a deleted or unauthorized source cannot remain available through a stale index.
Practice with feedback
Inspect what actually entered the index
Answers about a PDF timetable are wrong. You have tuned the prompt three times.
Follow the data through loading and node parsing before making any model call.
Check your understanding
Your task
Write the ingestion inspection you run before any query.
These notes stay on this page. Download them before leaving. Code in this field is not executed.
What to include
- Both document and node level statistics are printed
- Random nodes are shown as text you actually read
- Metadata presence is asserted, not assumed
- Thresholds are stated in advance
Compare with a worked answer
Here is one way to answer. Check how it uses the information in the task.
import random, statistics as st
from llama_index.core import SimpleDirectoryReader
from llama_index.core.node_parser import SentenceSplitter
docs = SimpleDirectoryReader('data/timetables').load_data()
print(f'{len(docs)} documents')
for d in docs[:5]:
print(' ', d.metadata.get('file_name'), len(d.text), 'chars')
nodes = SentenceSplitter(chunk_size=800, chunk_overlap=100).get_nodes_from_documents(docs)
lens = [len(n.text) for n in nodes]
print(f'{len(nodes)} nodes | min {min(lens)} median {st.median(lens)} max {max(lens)}')
for n in random.sample(nodes, 3):
print('---', n.metadata.get('file_name'), 'p', n.metadata.get('page_label'))
print(n.text[:300])
for n in nodes:
assert n.metadata.get('file_name'), 'node without a source file'
assert n.metadata.get('page_label'), 'node without a page'
# Health thresholds that stop me before I touch a prompt:
# - fewer than 2 nodes per page of PDF: extraction is failing
# - median node length under 150 chars: the splitter is receiving fragments
# - any printed sample that reads as interleaved columns or is mostly
# whitespace: the loader is wrong for this file type
# My 12-page timetable produced 3 nodes and the samples were column-
# interleaved gibberish. Three prompt rewrites could never have fixed that;
# switching to a table-aware extractor did.When you are signed in, opening the challenge carries your edited working notes into its draft in this browser. The challenge has its own completion record. Practising here does not award points or mark it complete.
Next, explore how to present generated output while preserving the distinction between partial text and a completed answer.
Sources
Documents and nodes explains the data units. VectorStoreIndex describes indexing. Node parser usage covers splitting.