Data framework

LlamaIndex

LlamaIndex connects a language model to your documents so it answers from them instead of guessing. The pieces are readers, node parsers, indexes, retrievers, and a query engine. A help-centre bot is the cleanest way to see all of them.

Intermediatellama-index-core 0.14

Readers, nodes, indexes, retrievers

AnalogyLlamaIndex is a filing cabinet for a model. Loaders put papers in. Splitters cut them. The index is the cabinet. The retriever pulls folders. The query engine writes the reply from those folders.
Layers
Query engine — writes the answer from the nodes
Retriever — finds the closest nodes
Index — stores embeddings
Node parser — cuts documents into chunks
Reader — loads files into Documents

The core package is small. Embeddings and vector databases are separate integrations. You can retrieve with a local embedding model and no API key, then attach a chat model when you want prose. Bad PDF extraction makes every later score meaningless. A clean markdown help centre does not need a fancy parser. A scanned invoice often does.

Starter

print("Document -> node parser -> VectorStoreIndex -> as_query_engine")
print("response.source_nodes  # the pages the answer used")

Example

A help centre, a wiki, and a folder of runbooks all become Documents with metadata, then an index.

PieceWhat it does
Reader / loaderTurns a file, page, or row into a Document with text and metadata.
Node parserSplits a document into chunks called nodes. This is where chunk size and overlap live.
IndexStores nodes so you can find them. A vector index embeds each node.
RetrieverGiven a question, returns the closest nodes.
Query engineRetrieves, builds a prompt, asks the LLM, and returns an answer plus sources.

The core package is small. Embeddings, OpenAI, and a specific vector database are separate integration packages. You can run retrieval with a local model such as all-MiniLM-L6-v2 and no API key, then attach a real chat model when you want generated answers.

A shop help centre

AnalogyMetadata is the tab on the folder. “Staff only” has to be a tab you filter on, or a customer will open that folder.
Data flow
Text + metadata→Embed→Index→Filter→Only customer pages

Build the index once, query it many times. Re-index a file when it changes. You do not retrain a model to update a price. One index per client, with a client_id filter on every query, is how you avoid a cross-client leak.

Starter

docs = [
    {"text": "Returns: 30 days if unused.", "audience": "customer"},
    {"text": "Staff can override a return up to $25.", "audience": "staff"},
]
visible = [d for d in docs if d["audience"] == "customer"]
print(visible[0]["text"])

Example

The staff override must not appear for a customer question about earbuds, even if the words “return” match.

from llama_index.core import Document, VectorStoreIndex, Settings
from llama_index.embeddings.huggingface import HuggingFaceEmbedding

Settings.embed_model = HuggingFaceEmbedding(model_name="BAAI/bge-small-en-v1.5")
# Settings.llm = ...  # attach a chat model when you want generated prose

docs = [
    Document(
        text="Returns: 30 days if unused, with receipt. Opened earbuds are final sale.",
        metadata={"page": "returns", "audience": "customer"},
    ),
    Document(
        text="Staff can override a return up to $25 with a manager code.",
        metadata={"page": "returns-staff", "audience": "staff"},
    ),
    Document(
        text="Shipping is free over $50 and $6 otherwise. Delivery takes 3 to 5 business days.",
        metadata={"page": "shipping", "audience": "customer"},
    ),
]
index = VectorStoreIndex.from_documents(docs)

Metadata is not decoration. The staff override must not be retrieved for a customer. Filter before you generate.

Ask, and show the page you used

AnalogyA citation is the page number in the margin. Users trust “returns” more than a fluent paragraph, and you can debug a bad answer by reading the node that was actually used.
Data flow
Question→Filter→Top k nodes→Prompt→Answer→source_nodes

Print the prompt once. If the node text is missing, or the instruction never says to refuse, no model choice will save you. `similarity_top_k` of 2 is a start. Raise it only when the answer is split across pages.

Starter

nodes = [
    {"page": "returns", "score": 0.82, "text": "30 days if unused."},
    {"page": "shipping", "score": 0.21, "text": "Free over $50."},
]
best = max(nodes, key=lambda n: n["score"])
print(best["page"], best["text"])

Example

“How long do I have to return earbuds?” should show the returns page next to the sentence, not only the sentence.

from llama_index.core.vector_stores import MetadataFilters, ExactMatchFilter

filters = MetadataFilters(filters=[ExactMatchFilter(key="audience", value="customer")])
engine = index.as_query_engine(similarity_top_k=2, filters=filters)
response = engine.query("How long do I have to return earbuds?")

print(response)
for node in response.source_nodes:
    print(node.metadata.get("page"), round(node.score or 0, 3))

A good help bot shows the page id next to the answer. Users trust “returns” more than a fluent paragraph, and you can debug a bad answer by reading the node that was actually retrieved.

Look at the prompt once. Print the prompt the query engine would send. If the node text is missing, or the instruction says nothing about refusing, no model choice will save you.

Refuse when the pages do not know

AnalogyRefusal is a valid answer. It is the librarian saying the shelf does not have that book, instead of inventing a book.
Data flow
Retrieved text→Does it contain the fact?→Answer→or exactly: I don't know

Put the refusal sentence in the prompt and demand that exact string, so your UI can branch on it. A chat engine can keep turns, but still embed only the new question. Do not embed the whole transcript as the query or you will retrieve whatever you talked about last Tuesday.

Starter

context = "Shipping is free over $50."
question = "Do you price-match?"
print("I don't know" if "price" not in context.lower() else context)

Example

“Do you price-match?” with no price-match page should be “I don’t know,” not a made-up yes.

from llama_index.core import PromptTemplate

qa = PromptTemplate(
    "Context:\n{context_str}\n\n"
    "Answer the question using only the context. "
    "If the context does not contain the answer, reply exactly: I don't know.\n"
    "Question: {query_str}\nAnswer:"
)
engine = index.as_query_engine(text_qa_template=qa, similarity_top_k=2, filters=filters)
print(engine.query("Do you price-match other stores?"))

Where this shows up at work

  • Support: policy pages in, a link to the page out. Agents stop pasting outdated macros.
  • Internal search: runbooks and a wiki. Staff need the paragraph, not a paraphrase that drops a step.
  • Docs that change weekly: re-index the changed file. You do not retrain a model to update a price.
  • Contracts: one index per client, metadata client_id, filters on every query. Cross-client retrieval is a data leak.

LlamaIndex, LangChain, and a vector database

AnalogyLlamaIndex is the filing cabinet. LangChain is the office manager who decides when to open it. Many teams use both.
Data flow
Documents→LlamaIndex retrieve→Chunks→LangGraph decides→Answer

You do not have to adopt the whole framework. A vector index is a convenience over embed, store, and search. If you already have Postgres with pgvector, keep it and use LlamaIndex only for readers. Hosted parsers help on messy PDFs. They are overhead on markdown.

Starter

def tool_retrieve(question: str) -> str:
    return "Returns: 30 days if unused."  # the cabinet

print("agent may call tool_retrieve, then answer")

Example

A support agent that sometimes must look up a policy, and sometimes must look up an order, wants a graph around retrieval, not retrieval as the whole app.

LlamaIndex is strongest when the job is “documents in, grounded answer out”: loaders, parsing, indexes, citations. LangChain is strongest when the job is “compose a model, tools, and an agent”. Many teams use LlamaIndex (or a plain vector store) for retrieval and LangGraph for the agent that decides when to retrieve.

You do not have to adopt the whole framework. VectorStoreIndex is a convenience over embed → store → search. If you already have Postgres with pgvector, you can keep it and only use LlamaIndex’s readers.

Hosted add-ons exist for hard documents: LlamaParse for messy PDFs, LlamaCloud for a managed index. A clean markdown help centre does not need them. A scanned invoice PDF often does, because bad extraction makes every later score meaningless.

Intermediate patterns

AnalogyIntermediate retrieval is about the window you show the model. Find a small precise chunk, then hand it the surrounding section so the exception clause is still visible.
Data flow
Small chunk hits→Expand to parent section→Router picks an index→Answer

A router sends shipping questions to the shipping index, not the company dump. Sentence-window or parent chunks fix the “30 days without the exception” bug. Re-index changed files. Do not rebuild the world for one edited paragraph.

Starter

hit = "30 days"
parent = "Returns: 30 days if unused. Opened earbuds are final sale."
print("send the parent, not only:", hit)

Example

Contracts: one index per client. Runbooks: retrieve the step, then show the heading above it so step 3 is not missing step 2’s warning.

  • Sentence window or parent chunks: retrieve a small chunk for precision, then pass the surrounding section to the model so it sees the exception clause.
  • Router: a question about shipping should hit the shipping index, not the entire company dump.
  • Chat engine: index.as_chat_engine() keeps turns. Still send only the new question through retrieval; do not embed the whole transcript as the query.

Practice

  • Add a gift-card page and ask a question that should hit it.
  • Remove the audience filter and show how a customer can see the staff override.
  • Ask a question the docs do not answer and confirm the refusal string.
  • Print source_nodes and decide if top-k should be 2 or 4.