Courses / GenAI and Agents Fast Track
Session 3 of 8
RAG basics: chunks, embeddings, a vector store and citations
Overview
Chapter 3 — RAG Basics: Chunks, Embeddings, a Vector Store and Citations
Course: GenAI and Agents Fast Track
Prerequisites: basic Python; how to send messages to a model with a helper such as chat() and read its reply; what a token and a context window are.
What the reader can do after this unit:
- Split the pages of a PDF into overlapping chunks that each remember their page number
- Turn chunks and a question into vectors, and rank the chunks by cosine similarity in a small vector store with a page filter
- Build a prompt from the top chunks, get an answer that names its pages, and check that every cited page was actually retrieved
Core question this unit answers: How does a question about a PDF become an answer that names the pages it came from?
Connections:
- Builds on: sending messages to a model from Python, and the idea that a model answers from what it was given in its context window
- Leads into: measuring whether the answers are right and refusing to answer when no page supports one (Chapter 4)
Notes
Student Notes — Chapter 3: RAG Basics: Chunks, Embeddings, a Vector Store and Citations
GenAI and Agents Fast Track · Chapter 3
Before you start
You need Python, a text editor and the chat() helper that sends messages to a model (a file named app/llm.py that reads the address, key and model name from environment values). Install the PDF reader with pip install pypdf. The chunking, similarity and store code in this chapter runs on its own with no model at all. Only the last step, turning text into vectors and asking the model for an answer, needs an endpoint, and for vectors it needs a second environment value, LLM_EMBED_MODEL, holding the name of an embedding model that your endpoint lists. A hosted provider may charge by the token for both. A text-based PDF of three or more pages that you may use (your own notes will do) is needed for the optional project task.
Summary
This chapter builds the first half of a question-answering assistant for a PDF. A model answers from its training, so it cannot know what is on page 4 of your policy; retrieval-augmented generation (RAG) fixes that by finding the right passages first and handing them to the model. You split each page into overlapping chunks that remember their page, turn every chunk and the question into a vector so that similar meanings sit close together, and keep the vectors in a small store that can rank them and filter by page. You then build a prompt from the best chunks and return the answer together with its cited pages. The chapter stops at a working answer with pages; measuring whether the answers are right is the next chapter.
Key concepts
| Term | Definition |
|---|---|
| RAG | Retrieval-augmented generation: the program first finds passages in your own documents, puts them in the prompt, and the model answers from those passages instead of from memory alone. |
| Chunk | A piece of a document cut to a size that can be stored and searched on its own. Here, each chunk holds some text and the page it came from. |
| Chunk size and overlap | Size is how much text one chunk holds; overlap is how much of the end of one chunk is repeated at the start of the next, so a sentence cut at a boundary is more likely to appear whole in one of the two chunks. |
| Embedding | A list of numbers that places a piece of text so that texts with similar meanings get lists that are close together. |
| Vector | A list of numbers. An embedding is a vector. |
| Cosine similarity | A score from -1 to 1 that compares the direction of two vectors: 1 means they point the same way, 0 means they are at right angles (no shared direction), and the length of the vectors does not change the score. |
| Vector store | A small database that keeps each chunk beside its vector, and can add new items, return the closest items to a query vector, and filter by a field such as the page. |
| Top-k | The k best-scoring items returned by a query, for example the top 3 chunks. |
| Retrieval | Finding the chunks most likely to answer a question: embed the question, query the store, and take the top-k. |
| Source label | A tag such as [page 5] placed before each chunk in the prompt so the model can say which page a fact came from. |
| Citation | A named source in an answer; here, the page numbers the answer relies on. |
| Unsupported citation | A page named in the answer that was not among the retrieved chunks, so nothing the model was given supports it. |
Explanations
1. Why a model needs your documents
A shop has a delivery policy as a PDF. Page 4 says that orders above a set amount ship free and smaller orders pay a flat fee. A customer asks the shop’s assistant, “Do I have to pay for delivery?” and the assistant answers, “Delivery is free on all orders.” The sentence is fluent and sure of itself. It is also wrong, because the model never saw page 4. (The policy and the answer are invented for this example.)
This is the closed-book exam from Chapter 1. The model answers from training, and one cloud provider’s explanation of RAG lists two problems that follow: a model may state something false when it lacks the answer, and may give a dated or general reply where a specific, up-to-date one was wanted. Its training data is static and has a cut-off date. A private policy is not part of what a model was trained on. Picture a new colleague asked about a policy they have never read: they answer fluently from general knowledge and are wrong for this shop.
Two quick fixes fail. Retraining the model on your PDF is a large job that you would repeat each time the PDF changes. Pasting the whole PDF into every prompt runs into the context window: a long policy may not fit, and Chapter 1’s point still stands, because one provider’s documentation notes that more context is not automatically better and that accuracy and recall can degrade as the token count grows.
An open-book exam gives the picture that works. The student does not carry the whole library into the hall; they flip to the right page, read it, and then write the answer. A librarian who hands you the right page before you speak is the same idea. That is retrieval-augmented generation, or RAG: the model refers to an authoritative knowledge base outside its training data before it responds, which is how one cloud provider puts it. The same provider notes a second benefit: the answer can carry source attribution, with citations that a reader can follow back to the document.
RAG has two phases. Prepare once per document: read the pages, cut them into chunks, turn each chunk into a vector, and store the vectors. Then, for each question: turn the question into a vector, find the closest chunks, put them in a prompt, and let the model answer. The rest of this chapter takes those steps one by one: sections 2 to 4 prepare, section 5 answers.
2. Splitting a PDF into chunks
Start with what goes wrong without a good split. Suppose a page is cut into pieces with no care about where the cuts fall, and one piece ends like this and the next begins like this:
Piece 1 ends: "...Items may be returned within 30"
Piece 2 begins: "days if unused. Refunds are paid to the original card..."
A question about the return window finds piece 1, which holds “within 30” and no unit, or piece 2, which holds “days if unused” and no number. The assistant answers with half a rule. Nothing is broken in the code; the cut fell in the middle of the fact.
Think of cutting a long scroll into index cards. If you cut with scissors at every 500th character, some sentences are cut in two. A careful clerk copies the last line of each card onto the top of the next, so a sentence on the boundary is whole on at least one card. That repeated line is the overlap.
A chunk is a piece of a document cut to a size that can be stored and searched on its own. One vendor’s guide describes chunking as breaking large text into smaller segments, and gives two reasons it is needed: an embedding model has a context window, and a text longer than that may be truncated, so important context is lost before the text is turned into a vector; and the chunks themselves must contain the information a search needs. The size is a trade-off. A short chunk, such as a sentence, gives a vector about that sentence’s specific meaning. A long chunk captures broader themes but, as the same guide says, may dilute the significance of individual sentences and make precise matches harder. The guide also recommends starting with fixed-size chunking and changing it only after you have seen it fail.
Our chunker is fixed-size and works on characters, which keeps the code short (vendors usually count tokens). It has two rules. First, a chunk never crosses a page, so the page number stored with the chunk stays exact; that is what makes citations possible later. Second, the chunk size and overlap are settings, here 500 and 100 characters.
def split_page(page, text, size=500, overlap=100):
if overlap >= size:
raise ValueError("overlap must be smaller than size")
step = size - overlap
chunks, start = [], 0
while start < len(text):
chunks.append({"page": page, "start": start, "text": text[start:start + size]})
if start + size >= len(text):
break
start += step
return chunks
Work one page by hand. A page has 1,200 characters, the size is 500 and the overlap is 100, so each new chunk starts 400 characters after the last. The chunks start at 0, 400 and 800, which gives pieces of 500, 500 and 400 characters. Characters 400 to 499 appear in both chunk 1 and chunk 2, and characters 800 to 899 in both chunk 2 and chunk 3. A page of 450 characters gives a single chunk. The count for a page longer than the size is 1 + (length - size) / step, with the division rounded up to a whole number: for 1,350 characters that is 1 + (850 / 400 rounded up to 3) = 4 chunks.
The pages themselves come from a PDF reader. With the pypdf package, extract_text() returns the text of a page:
from pypdf import PdfReader
def read_pages(path):
reader = PdfReader(path)
return [{"page": number, "text": (page.extract_text() or "").strip()}
for number, page in enumerate(reader.pages, start=1)]
Page numbers start at 1 here, because a reader counts that way. Here is the failure to expect. You load a scanned handbook and every page comes back with empty text, so there are no chunks and the assistant says it has no information. The pypdf documentation explains why: a scanned PDF is a container for images of pages, not text, and it recommends using OCR software directly for such files, because errors can add up when text is pulled out of a scan’s hidden OCR layer. So check the text length of every page before chunking, and fix the file, not the chunker.
3. Embeddings: text as vectors, similarity as distance
Here are three sentences:
- “Can I send back an unused lamp?” (the question)
- “Items may be returned within 30 days if unused.” (an invented policy sentence)
- “Our office is closed on public holidays.” (unrelated)
Search by shared words would find almost nothing for the question: “send back” and “returned” have no word in common. A person sees at once that sentences 1 and 2 are about the same thing. We need a way for a program to see it as well.
Think of a city map. Places that are alike sit near each other: all the cafes in one district, all the hospitals in another. A library works the same way: books on cooking stand on neighbouring shelves. An embedding gives every piece of text a position on such a map, so two texts about the same thing get nearby positions. One provider’s guide defines it plainly: an embedding is a vector (a list) of floating point numbers, and the distance between two vectors measures their relatedness, with small distances suggesting high relatedness.
A real map has two numbers per place; a real embedding has many more. To see the arithmetic, use two numbers. Let the question be the vector [1, 2], and let three chunks be A = [2, 4], B = [4, 2] and C = [2, -1]. Compare them by cosine similarity, which measures the angle between two vectors and ignores how long they are:
cosine(x, y) = (x · y) / (|x| times |y|), where x · y multiplies the matching numbers and adds them, and |x| is the square root of the sum of the squares.
- Question and A: the dot product is 1 x 2 + 2 x 4 = 10. The lengths are the square root of 5 (about 2.236) and the square root of 20 (about 4.472), and their product is 10. Score 10 / 10 = 1.0: the same direction.
- Question and B: the dot product is 1 x 4 + 2 x 2 = 8, and the lengths multiply to 10 again. Score 0.8.
- Question and C: the dot product is 1 x 2 + 2 x (-1) = 0. Score 0.0: unrelated.
So A is the best match, B is close and C is not a match. Notice that A is twice as long as the question and still scores 1.0: only direction counts. One provider’s guide adds that its embeddings are normalized to length 1, and that for such vectors cosine similarity and Euclidean distance give identical rankings; the choice of distance function typically does not matter much.
Getting the vectors needs a model. With the openai package, the call is client.embeddings.create(model=..., input=[...]), and each item in the reply holds one vector:
import os
from app.llm import get_client
def embed(texts):
reply = get_client().embeddings.create(model=os.environ["LLM_EMBED_MODEL"], input=texts)
return [item.embedding for item in reply.data]
Embedding calls are billed, like other calls, by the number of tokens in the input, on one provider’s pricing note, so embed each chunk once and keep the result.
Here is the failure to avoid. You embed all your chunks with one embedding model, later switch the question to a different one, and the store raises an error because the lists have different lengths; or, if the lengths happen to match, the scores are meaningless, because two models draw two different maps. Two people who draw maps of the same town with different scales and landmarks cannot compare a place’s position between their maps. The Chroma database’s guide states the first half of the rule: the dimension of the query embedding must match the dimension of the embeddings in the collection. The rule to keep: chunks and questions are embedded by the same model, and if you change the model, embed everything again.
4. A local vector store: add, query, filter
Picture a box of index cards. Each card holds the chunk’s text, the page it came from, and its list of numbers. Add puts a new card in the box. Query takes a question’s list of numbers and hands back the k cards whose numbers are closest. Filter says “only look at cards from pages 10 to 20” before ranking. That box is a vector store. A librarian’s card catalogue is a second picture: the catalogue is not the books, it is a quick way to point at them, here by meaning.
Real vector databases do the same three things at scale. The Chroma guide shows a where filter on a metadata field such as page, and says a query returns 10 results by default unless you pass n_results. Our store is a plain Python class that keeps a list and ranks by cosine similarity; it is enough for one PDF.
import math
def cosine(a, b):
if len(a) != len(b):
raise ValueError("vectors have different lengths")
dot = sum(x * y for x, y in zip(a, b))
return dot / (math.sqrt(sum(x * x for x in a)) * math.sqrt(sum(y * y for y in b)))
class Store:
def __init__(self):
self.items = []
def add(self, chunks, vectors):
for chunk, vector in zip(chunks, vectors):
self.items.append({"chunk": chunk, "vector": vector})
def query(self, vector, k=3, pages=None):
found = [(cosine(vector, item["vector"]), item["chunk"]) for item in self.items
if pages is None or item["chunk"]["page"] in pages]
return sorted(found, key=lambda pair: pair[0], reverse=True)[:k]
Work the toy example. Chunk A is on page 2 with vector [2, 4]; chunks B and C are on page 5 with vectors [4, 2] and [2, -1]. Query with [1, 2] and k = 2: the scores are 1.0 for A and 0.8 for B, so the top 2 are A (page 2) and B (page 5). Query again with pages={5} and k = 1: only B and C are considered, and B wins with 0.8. A filter changes who is a candidate, not how the scores are worked out.
Two things to watch. A filter that matches nothing returns an empty list, so the next step must handle “no chunks” and not assume there is always a first result. And k is a choice: too small and the answer chunk may be missed; too large and the prompt fills with chunks that do not help. Three is a starting point, to be tested.
5. Retrieve, then generate: the answer with its cited pages
Read the finished prompt first. This is what the model receives for the question “Do I have to pay for delivery?”, with two retrieved chunks (the text is invented):
System: Answer the question using only the sources below. Cite the page of every fact
as [page N]. If the sources do not contain the answer, say so.
Sources:
[page 4] Orders above the free-delivery amount ship free. Smaller orders pay a flat fee.
[page 6] Delivery to remote areas may take longer.
Question: Do I have to pay for delivery?
A good reply would be: “Orders above the free-delivery amount ship free, and smaller orders pay a flat fee [page 4].” The pages are named because we labelled each source, as a witness statement is stamped with the page of the file it came from. A second picture: a student’s essay with footnotes. The reader can check each claim at its page.
The cited answer has four steps, in this order:
- Embed the question with the same model used for the chunks.
- Query the store for the top-k chunks.
- Build the prompt: the instruction, the chunks with their
[page N]labels, and the question. - Ask the model with a low temperature, and return its text together with the pages that were retrieved.
import re
from app.llm import chat, reply_text
from app.rag.embed import embed
SYSTEM = ("Answer the question using only the sources below. Cite the page of every fact "
"as [page N]. If the sources do not contain the answer, say so.")
def build_messages(question, hits):
sources = "\n\n".join(f"[page {chunk['page']}] {chunk['text']}" for _, chunk in hits)
return [{"role": "system", "content": SYSTEM},
{"role": "user", "content": f"Sources:\n{sources}\n\nQuestion: {question}"}]
def cited_pages(text):
return sorted({int(n) for n in re.findall(r"\[page (\d+)\]", text)})
def answer(question, store, k=3, ask=chat, embed_fn=embed):
hits = store.query(embed_fn([question])[0], k=k)
text = reply_text(ask(build_messages(question, hits), temperature=0))
retrieved = sorted({chunk["page"] for _, chunk in hits})
unsupported = [p for p in cited_pages(text) if p not in retrieved]
return {"answer": text, "pages": retrieved, "unsupported": unsupported}
Now the failure that makes the last two lines worth writing. The store returned chunks from pages 3, 4 and 7, and the model’s answer ends “[page 12]”. Page 12 was never in the prompt. The model produced a citation that looks right and points at nothing it was shown, like a student who copies a footnote number from a book they never opened. The instruction in the prompt asks for citations but does not enforce them; a model can still write a page it was not given. So the code compares the pages named in the answer with the pages retrieved. Any page named but not retrieved goes in unsupported, and the application can drop it, flag it, or show only the pages that were retrieved. The result of answer() is the text, the pages it was given, and a list of pages to distrust.
What this step does not do is just as important. It does not check that the retrieved chunks really answer the question; the best of three poor chunks is still returned. It does not refuse when the PDF has no answer; the instruction asks the model to say so, and a model may ignore it. And it has no measure of how often it is right. Those three gaps are the subject of the next chapter.
Three situations to recognise
- An assistant answers “Delivery is free on all orders” while page 4 of the policy says otherwise. The model never saw page 4. Retrieval puts page 4 in the prompt and the label lets the answer name it.
- A return rule is split between two chunks and the assistant quotes “within 30” without the unit. The cut fell inside the fact. Overlap makes the boundary sentence whole in at least one chunk.
- An answer ends “[page 12]” although the store returned pages 3, 4 and 7. The citation points at a page the model never saw. Compare cited pages with retrieved pages and flag the difference.
What this means for the design you write:
| If the situation is | Do this | Because |
|---|---|---|
| The facts are in your documents, not the model | Retrieve passages first and answer from them | The model has not seen your documents |
| A fact can fall on a chunk boundary | Use overlap, and keep each chunk inside one page | A boundary can cut a fact in half, and the page stays exact |
| A page’s text comes back empty | Fix the file (a text version, or OCR) before chunking | A scan holds images, not text |
| Questions and chunks must be comparable | Embed both with the same model, and re-embed all after a change | Different models give different maps, and often different lengths |
| The answer names pages | Compare them with the retrieved pages | An instruction asks for citations; it does not enforce them |
Flashcards
Say the answer aloud before revealing it. Speaking is a different skill from reading; practise both.
-
Why not paste the whole PDF into every prompt?
- A long document may not fit in the context window, and even when it fits, accuracy and recall can degrade as the token count grows. RAG sends only the few passages that match the question.
-
What are the two phases of RAG?
- Prepare once per document: read the pages, cut them into chunks, embed each chunk and store the vectors. Then for each question: embed the question, query the store, build a prompt from the top chunks and let the model answer.
-
What is a chunk, and why keep it inside one page?
- It is a piece of a document cut to a size that can be stored and searched on its own. Keeping it inside one page means the page number stored with it is exact, which is what makes page citations possible.
-
What does overlap do?
- It repeats the end of one chunk at the start of the next, so a sentence on a boundary appears whole in at least one chunk instead of being cut in half.
-
How many chunks does a 1,350-character page give with size 500 and overlap 100?
- The step is 400, so chunks start at 0, 400, 800 and 1,200: four chunks, the last one 150 characters long.
-
What is an embedding?
- A list of numbers that places a piece of text so that texts with similar meanings get nearby lists. The distance between two lists measures how related the texts are.
-
What does cosine similarity measure, and what does a score of 1.0 mean?
- It compares the direction of two vectors and ignores their length. A score of 1.0 means the vectors point the same way, 0 means unrelated.
-
Why must chunks and questions use the same embedding model?
- Two models draw two different maps, even when their lists have the same length. Mixing them gives an error (when the lengths differ) or scores that mean nothing, so after a model change you embed everything again.
-
What are add, query and filter in a vector store?
- Add stores a chunk with its vector; query returns the top-k closest items to a query vector; filter limits the candidates, for example to chosen pages, before ranking.
-
Why label each source with its page in the prompt?
- The label lets the model say which page a fact came from, so the answer carries citations that a reader can check.
-
Why compare cited pages with retrieved pages?
- A prompt instruction asks for citations but does not enforce them, so a model can name a page it never saw. A page that is cited but not retrieved is unsupported and should be flagged.
Model answer
The core question this unit answers: “How does a question about a PDF become an answer that names the pages it came from?”
Five beats a good answer covers; hit all five, in this order:
- Chunk — split each page into overlapping chunks that keep their page number
- Embed — turn each chunk and the question into vectors with the same model
- Store — keep the vectors and their pages in a store
- Retrieve — query the store for the top-k closest chunks
- Cite — put the labelled chunks in the prompt and return the answer with its pages, checked
The full answer, said aloud:
(Chunk) “I read the PDF page by page and split each page into overlapping chunks, never across a page, so each chunk keeps an exact page number.
(Embed) I turn every chunk into a vector with an embedding model, and I embed the question with the same model, so that similar meanings land close together.
(Store) I keep each chunk with its vector and its page in a vector store that can add, query and filter.
(Retrieve) For a question I query the store for the top-k chunks by cosine similarity.
(Cite) I put those chunks into the prompt with their page labels, ask the model to answer only from them, return the answer with the pages, and check that every cited page was one I retrieved.“
Skip any of these five and the answer is incomplete. An interviewer or examiner who hears only “you put the PDF in a vector database” will ask how the pages are kept and what stops a made-up citation. Practise until all five beats come out in order without notes.
Why this matters
On the job: many teams want to ask questions of their own documents: policies, manuals, contracts, handbooks. In our view, a useful first version is usually what you build in this chapter: chunk, embed, store, retrieve, answer with pages. Knowing each step is what lets you debug it when an answer is wrong.
In interviews and reviews: “walk me through a RAG pipeline” is a common question for an AI engineering role. The five-beat answer above is built for it.
As a builder: the page number you keep at chunking time is the citation you show at the end, so a decision in step one shapes the last step. The next chapter will measure how often the answers are right, and it needs exactly the outputs built here: the answer, the retrieved pages and the unsupported list.
After this unit
Hands-on project. Three tasks: (1) cut three invented pages into chunks and print one row per chunk; (2) write a cosine function and a small store, and rank five invented chunk vectors for two questions, with and without a page filter; (3) optionally, run the whole assistant on a PDF of your own and record five questions with the page you expect each answer to come from. The full brief is in the project file.
What’s next. Chapter 4 asks how you know the answers are right. It scores them against a small set of questions with known pages, and adds guardrails so that the assistant refuses when no page supports an answer. Keep your five questions and the pages you expect: that chapter starts from them.
Project
Hands-on Project — Chapter 3: Chunk, Rank, Answer With Pages
Objective
Build the retrieval half of a PDF question-answering assistant: a chunker that keeps page numbers, a small vector store that ranks by cosine similarity with a page filter, and, optionally, the full path from a real PDF to an answer with its pages.
What you need: Python 3 and a text editor. Tasks 1 and 2 use only Python’s standard library: no account, no API key, no model and no cost. Task 3 is optional. It needs pip install pypdf, a text-based PDF of three or more pages that you are free to use, and a model endpoint that offers a chat model and an embedding model (set LLM_BASE_URL, LLM_API_KEY, LLM_MODEL and LLM_EMBED_MODEL in your terminal). A hosted provider may charge by the token for both chat and embeddings, so check its rates first. Every page length and vector in Tasks 1 and 2 is invented for the exercise.
You write every line yourself. This brief gives the facts and the checks; the finished programs and the expected rankings are not in it. Work through the tasks in order and check each output against its checks before moving on.
Task 1 — Chunk three pages
Create chunk.py with a function split_page(page, text, size, overlap) that returns a list of chunks, each a dictionary holding page, start and text, as in the notes. Chunks are cut by characters. A chunk never crosses a page, the first chunk of a page starts at 0, and each following chunk starts size - overlap characters after the one before. Stop adding chunks once a chunk reaches the end of the page. If overlap is not smaller than size, raise an error.
Three invented pages. Build the text of each page as a string of exactly the stated length in which the character at position i is the letter chr(97 + i % 26):
| Page | Length in characters |
|---|---|
| 1 | 430 |
| 2 | 1,000 |
| 3 | 1,250 |
Use size 400 and overlap 100. Print one row per chunk with its page, its start and its length, then the total number of chunks.
Scope: three pages, one size and one overlap. Stop once the table is printed and the checks below hold.
Checks:
- Every chunk row shows its page, and no chunk’s text is longer than 400 characters.
- Within one page, the first 100 characters of each chunk after the first are identical to the last 100 characters of the chunk before it, and your program checks this and prints the result.
- The last chunk of each page ends exactly at the end of the page text.
- Calling
split_pagewith overlap 400 and size 400 raises an error, and a short test of yours shows it.
Output: the printed table with the total (copy it into chunks-output.txt) and your chunk.py.
Task 2 — Rank chunk vectors with a page filter
Create store.py with a function cosine(a, b) and a class Store with add(chunks, vectors) and query(vector, k, pages), as in the notes. cosine must raise an error when the two vectors have different lengths. query returns the top k pairs of (score, chunk), highest score first, and considers only chunks whose page is in pages when pages is given.
Five invented chunks, each with its page and a three-number vector:
| Chunk | Page | Vector |
|---|---|---|
| c1 | 1 | [1, 0, 0] |
| c2 | 2 | [1, 1, 0] |
| c3 | 2 | [0, 1, 0] |
| c4 | 3 | [0, 1, 1] |
| c5 | 3 | [0, 0, 1] |
Run two queries and print the chunk names, pages and scores to four decimal places:
- The query vector
[2, 1, 0], the top 3 chunks, no page filter. - The query vector
[0, 1, 2], the top 2 chunks, only pages 1 and 2.
Scope: five chunks and two queries. Stop there.
Checks:
- Each result list is sorted from the highest score to the lowest.
- Every result of query 2 is on page 1 or 2, and each list has exactly
kentries. - Every score lies between -1 and 1.
- A call to
cosinewith[1, 2]and[1, 2, 3]raises an error, and a short test of yours shows it. - Write two sentences in
store-notes.txt: one saying how the result of query 2 differs from the result of the same query with no filter, and why, and one saying what the store returns if the filter matches no chunk.
Output: the printed results (copy them into ranking-output.txt), store-notes.txt and your store.py.
Task 3 — Run the whole assistant on a PDF (optional)
Using the files from the notes (app/llm.py, app/rag/pdf.py, chunk.py, embed.py, store.py and answer.py), load a text-based PDF of your own, chunk it with size 500 and overlap 100, embed the chunks, fill the store and answer questions.
First, before you run anything, read the PDF yourself and write five questions that it answers, each with the page number you expect the answer to come from. Then run answer() for three of the questions.
Scope: one PDF, five questions written down, three questions run. Stop there.
Checks:
- Your five questions and expected pages are written before any answer is produced.
- For each question you ran, your output shows the answer, the pages that were retrieved, the unsupported list, and whether your expected page is among the retrieved pages.
- No key or address appears in any file you keep.
- At least one run is done with
k = 3and one with a differentk, and a sentence says what changed.
Output: a file questions.md with the five questions and expected pages, and a file assistant-run.md with the three runs.
Keep questions.md and your working files: the next chapter measures how often the assistant finds the expected page and how often its answers are supported, starting from your five questions.
TechEazy Consulting training material — not affiliated with or endorsed by OpenAI, Anthropic or any other company named in this course.