Retrieval-Augmented Generation: Embeddings, Chunking and Vector Stores
Fine-tuning a model on your documents is the answer most people reach for and usually the wrong one. Retrieval is the other answer: leave the weights alone, find the relevant few paragraphs at question time, and put them in the prompt. By the end of this lesson you will be able to build that pipeline out of pieces that run entirely on your machine, explain each design decision in it, and describe the four ways it produces an answer that is fluent, sourced and wrong.
Two memories
Section titled “Two memories”The idea has a paper. Lewis and colleagues, in 2020, described “a general-purpose fine-tuning recipe for retrieval-augmented generation (RAG) — models which combine pre-trained parametric and non-parametric memory for language generation”, motivated by the observation that although large pretrained models “store factual knowledge in their parameters”, their “ability to access and precisely manipulate knowledge is still limited”, and that “providing provenance for their decisions and updating their world knowledge remain open research problems”.
Strip the vocabulary and you have the whole design argument. Parametric memory is the weights: enormous, fixed at training time, and impossible to audit or update. Non-parametric memory is an index you own: small, changeable this afternoon, and able to say where an answer came from. Retrieval is the machinery that puts the second in front of the first.
Everything below is engineering on top of that sentence.
Two pipelines, run at different times
- Ingest: readDocuments to plain text, keeping the structure: file, heading path, position.
- Ingest: chunkSplit into passages small enough to embed usefully and large enough to answer from.
- Ingest: embedOne vector per chunk, from a local embedding server.
- Ingest: indexVectors and metadata into a local store you can back up.
- Query: embed the questionSame model, same dimensions, query-side instruction prefix.
- Query: retrieveNearest neighbours, generously: tens of candidates, not five.
- Query: rerankA cross-encoder reads question and candidate together and reorders them.
- Query: answerThe top few chunks in the prompt, with instructions to cite and to refuse.
- Query: verifyCheck that every citation names a chunk that was actually supplied.
Embeddings, and the rules that come with them
Section titled “Embeddings, and the rules that come with them”Part 2 established what an embedding is: a vector positioned so that texts about similar things sit near each other. A retrieval system is that idea plus bookkeeping.
This course uses Qwen3-Embedding-0.6B, Apache-2.0 licensed, which its card describes as
supporting over 100 languages including programming languages, with a context of 32,000
tokens and an output dimension configurable up to 1024 through Matryoshka representation
learning, so you can ask for a shorter vector and trade a little quality for a smaller
index. The card recommends formatting queries as Instruct: {task_description}\nQuery:{query}
and reports that instructions typically improve performance by one to five per cent, and
that English instructions should be used even in multilingual settings.
Four rules follow from how embeddings work, and breaking any of them produces a system that retrieves plausible rubbish.
One model, both sides. The vector for the question and the vectors for the chunks must come from the same model at the same dimension. Change the embedding model and the whole index is worthless; rebuild it. Write the model name and dimension into the database when you create it, and refuse to query when they disagree.
Asymmetry is deliberate. The card’s convention puts the instruction on the query and not on the document. That is not an oversight: a question and the passage answering it do not look alike, and the instruction prefix is what tells the model to embed a query as a query. Applying it to both sides quietly changes the geometry.
Similarity is not relevance. Two passages about the same topic sit close together whether or not either answers the question. This is why the reranker below exists.
The index is a copy. Embedding a document does not link to it; it snapshots it. A document edited after ingestion is still in the index in its old form, and nothing will tell you.
RunnableAll tracks
llama-server \ --model ~/models/qwen3-embedding-0.6b/Qwen3-Embedding-0.6B-Q8_0.gguf \ --alias qwen3-embedding \ --embedding \ --ctx-size 8192 \ --host 127.0.0.1 \ --port 8090The README documents --embedding, --embeddings as restricting the server to “only support
embedding use case”, with both an OpenAI-compatible /v1/embeddings endpoint and a native
/embedding one. Run it on its own port rather than sharing with the chat model: it is a
different model, it is tiny, and you want to be able to restart one without the other.
Chunking is the decision that decides everything else
Section titled “Chunking is the decision that decides everything else”A chunk is the unit of retrieval and the unit of citation. Choosing its size is choosing what your system can answer.
Too small and each chunk embeds cleanly but lacks the context to answer from: you retrieve the sentence containing the phrase and lose the paragraph that qualified it. Too large and the chunk’s vector is an average of several topics, so it matches everything weakly and nothing strongly, and you waste context sending three pages to answer one question.
The strategies worth knowing, in the order you should try them:
- Structural. Split on the document’s own boundaries: headings, sections, list items, table rows. Documents that have structure should be chunked by it, because the author already grouped related sentences for you.
- Fixed window with overlap. Pack text into windows of a few hundred tokens with an overlap of ten to twenty per cent, so a fact that lands on a boundary appears whole in one of the two neighbouring chunks. This is the default when there is no structure.
- Sentence window. Embed single sentences for precise matching but return the surrounding paragraph for the answer. More machinery, better results on dense reference text.
- Parent document. Embed small, return the whole section the small chunk came from. The same trade in a coarser form.
The project in this part uses structural splitting on headings, then packs paragraphs into windows with overlap, and prepends the heading path to each chunk’s text. That last detail matters more than it looks: a chunk that begins “The window is 02:00 to 04:00 on Sundays” matches almost nothing, and the same chunk beginning “Service runbook › Backups › The window is 02:00 to 04:00 on Sundays” matches a question about backups.
Store metadata with every chunk: source path, heading path, chunk index, and a hash of the source file. The first three are your citations. The fourth is how you detect that a document changed and its chunks need replacing.
A vector store that is a file
Section titled “A vector store that is a file”You can run a vector database. On one machine, for a few thousand documents, you should not have to.
This course uses sqlite-vec, which its documentation describes as “a vector search SQLite extension that runs anywhere”, dual-licensed MIT and Apache-2. The reasons are the ones that matter for a course about owning your own stack: the entire index is one SQLite file you can copy, back up and delete; there is no server to run, secure or keep alive; the query language is SQL you already know; and the chunks and their vectors live in the same file, so a citation is a join rather than a second lookup in a second system.
The whole retrieval store
- chunks tableOrdinary SQLite: chunk id, source path, heading path, text, source hash.your citations
- vec0 virtual tableThe vectors, one row per chunk, joined back by id.the search
- meta tableEmbedding model name, dimension, ingestion date. Checked on every query.the safety catch
- One .db fileCopy it, back it up, ship it to another machine. There is no other state.
A vector table is created as a virtual table, with the dimension fixed at creation and the distance metric chosen there too:
Pseudocode — not a real command
create virtual table vec_chunks using vec0( chunk_id integer primary key, embedding float[1024] distance_metric=cosine);and a nearest-neighbour search is a match with a k, joined back to the ordinary table
that holds the text:
Pseudocode — not a real command
with knn as ( select chunk_id, distance from vec_chunks where embedding match :query_vector and k = 50)select chunks.source, chunks.heading, chunks.text, knn.distancefrom knn left join chunks on chunks.id = knn.chunk_idorder by knn.distance;From Python the extension is loaded onto an ordinary connection, and vectors are passed as compact blobs:
Fragment — not complete on its own
import sqlite3, sqlite_vecfrom sqlite_vec import serialize_float32
db = sqlite3.connect("qa-index.db")db.enable_load_extension(True)sqlite_vec.load(db)db.enable_load_extension(False)# ... then pass serialize_float32(vector) as the query parameterReranking: the step most tutorials skip
Section titled “Reranking: the step most tutorials skip”Vector search is a cheap approximation. It compares two vectors that were computed independently, so nothing in the comparison ever looked at the question and the passage at the same time. A reranker does exactly that, and it is much better at it.
Qwen3-Reranker-0.6B, Apache-2.0 and multilingual, is prompted with an instruction, a
query and a document, and judges “whether the Document meets the requirements based on the
Query”, scoring from the logits of the “yes” and “no” tokens. Its card documents a default
instruction of “Given a web search query, retrieve relevant passages that answer the query”,
and reports its own evaluation on “the top-100 candidates retrieved by dense embedding model
Qwen3-Embedding-0.6B”, which is the pairing this course uses at a smaller candidate count.
llama-server exposes it: --rerank, --reranking “enable reranking endpoint on server”, with
/rerank, /v1/rerank and /v1/reranking as aliases.
The shape of the pipeline follows from the cost. Retrieval over ten thousand chunks is a vector comparison per chunk and is effectively free. Reranking is a forward pass over the query and the candidate together, so its cost is candidates multiplied by chunk length, in prefill. Retrieve fifty candidates cheaply, rerank them, keep five. Retrieving five and reranking them changes nothing, because the mistake you are trying to fix was made when the right chunk failed to reach the top five.
Citations, grounding and the refusal
Section titled “Citations, grounding and the refusal”An answer without a citation cannot be checked, and a system that cannot say “not in these documents” will invent something instead.
Both are prompt-and-validation problems, which is where the previous lesson pays off. Give every chunk you put in the prompt a short identifier. Require the answer to be an object with a list of claims, each carrying the identifiers it rests on, plus a boolean saying whether the documents supported an answer at all. Then verify in code: every identifier the model returned must be one you supplied. A citation to a chunk that was not in the prompt is a fabrication and should be treated as a failed answer, not shown to a reader with a footnote.
The refusal path needs the same care. “Answer only from the documents below; if they do not contain the answer, say so” is a start, and it will be ignored some of the time. Making the refusal a field in a schema, rather than a sentence the model must remember to produce, moves it from a request to a structural option. Then measure how often it is used correctly, with questions you know the corpus cannot answer. The project in this part includes those questions for exactly this reason.
The four ways this goes wrong
Section titled “The four ways this goes wrong”The right chunk is not retrieved. The answer is in your corpus and the search did not find it. Causes: chunking that split the fact from its context, a query phrased unlike the document, an embedding model weak in that language or domain. Diagnose by searching the index by hand for a phrase you know is in the answer, and see where the correct chunk ranks.
The index is stale. The document changed and the vectors did not. This produces the worst possible failure mode, a correct-looking answer with a real citation to text that no longer exists. Store a hash of each source file and re-ingest what changed; make the ingest script cheap enough to run from a scheduled task.
Confident nonsense from irrelevant context. Retrieval always returns its top k, even when the best match is unrelated. The model is then handed three irrelevant passages and a question, and generally answers anyway. A distance threshold and the reranker’s score are both defences; the refusal path is the last one.
Questions retrieval cannot answer. “How many machines are listed?”, “What changed between these two policies?”, “Summarise everything about backups.” These need every chunk, or a comparison across chunks, and nearest-neighbour search over a top-k will not deliver it. Know this class exists so that you recognise it in an evaluation instead of blaming the model.
When a fine-tune is the better tool
Section titled “When a fine-tune is the better tool”The division is clean enough to state as a rule.
Retrieval changes what the model reads. Use it for facts: things that are true today, change next month, are specific to you, and need to be cited. Adding a document is a file copy and an ingest run.
Fine-tuning changes how the model behaves. Use it for form: a house output format, a domain vocabulary, a tone, a task shape the model keeps getting slightly wrong. Part 13 covers it, and the evaluation set from this part’s lab is how you will tell whether it helped.
Attempting the first with the second is the classic waste: a fine-tune on a thousand company documents produces a model that writes in your house style and still cannot tell you what is in document 634. Attempting the second with the first is subtler but common: no quantity of retrieved examples reliably fixes a model that will not follow your output format, which is what constrained decoding and fine-tuning are for.
The honest answer for a mature system is usually both, in that order: retrieval first, because it is cheaper and reversible, and a fine-tune afterwards for the behaviour that retrieval never fixed.
Evaluate retrieval before evaluating the generated answer
Section titled “Evaluate retrieval before evaluating the generated answer”Create a question table with the authoritative document and passage that should support each answer. Include questions whose answer is absent. Run retrieval alone and inspect whether the supporting passage appears in the candidates and in the final context. This isolates extraction, chunking, indexing and ranking from generation.
For an incorrect answer, walk backwards: did the source contain the fact, was it extracted intact, was the right chunk retrieved, and did the generator preserve it? A table split across chunks may require different extraction or chunking; a correct passage ranked too low may require retrieval changes. A fluent answer that contradicts supplied evidence is a generation or instruction-following failure.
Document changes also need a lifecycle. Keep source identifiers, revisions and access permissions with chunks. Remove obsolete or forbidden chunks from retrieval and rebuild embeddings when the embedding contract changes. Evaluate stale-answer cases after an update. A RAG system is current only if its ingestion and deletion paths maintain the index; adding retrieval once does not keep facts fresh automatically.
Retrieval gives a model a second, non-parametric memory that you own and can update. The pipeline is ingest — read, chunk, embed, index — and query — embed, retrieve, rerank, answer, verify. Chunking is the decision that determines what the system can answer: split on the document’s own structure, overlap, and keep the heading path in the text. Query and document must share an embedding model and dimension, and the query-side instruction prefix belongs on the query only. sqlite-vec keeps the vectors and the text in one file with no server, which is the right shape for one machine. A reranker reading query and candidate together fixes the ordering that independent vectors got wrong, so retrieve widely and rerank down. Citations must be verified in code, and the refusal path must be a field, not a hope. Four failure modes recur: the chunk that was not retrieved, the stale index, confident answers from irrelevant context, and questions no top-k search can serve. Retrieval is for facts; fine-tuning is for behaviour.
Check your understanding
Sources for this lesson
7 verified · checked 2026-09-08
- 01Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks (Lewis et al., arXiv:2005.11401)§ Abstractarxiv.org/abs/2005.114012026-09-08
- 02Qwen3-Embedding-0.6B model card§ Model overview; instruction format; dimensionshuggingface.co/Qwen/Qwen3-Embedding-0.6B2026-09-08
- 03Qwen3-Reranker-0.6B model card§ Model overview; prompt format; evaluation setuphuggingface.co/Qwen/Qwen3-Reranker-0.6B2026-09-08
- 04llama.cpp — llama-server README§ --embedding and /v1/embeddings; --reranking and /v1/rerankgithub.com/ggml-org/llama.cpp/blob/master/tools/server/README.md2026-09-08
- 05sqlite-vec — a vector search SQLite extension§ Overview; vec0 virtual tablealexgarcia.xyz/sqlite-vec2026-09-08
- 06sqlite-vec — KNN queries§ MATCH and k; distance metrics; joining to source rowsalexgarcia.xyz/sqlite-vec/features/knn.html2026-09-08
- 07sqlite-vec — Python§ Installation; loading the extension; serialize_float32alexgarcia.xyz/sqlite-vec/python.html2026-09-08
Every technical claim on this page was checked against the official documentation of the tool, vendor or model publisher on the date shown, at the version pinned for the course. Where the course disagrees with folklore, the source is how you can tell which one to trust.