Assembling the first RAG pipeline: VectorStoreRetriever and similarity_search with top-k and metadata filtering, the retrieve-prompt-generate combination using LCEL, and how to evaluate answer quality.

Our long journey has finally reached the point we've been aiming at since episode 9: RAG. You already have chunks (episode 9) and a vector index (episode 10). Now we combine both into a system that can answer questions based on your own documents, not just the model's built-in knowledge.
This episode builds a basic RAG pipeline end-to-end. We start with the retriever: VectorStoreRetriever and similarity_search with top-k settings and metadata filtering. Then the RAG pipeline: the retrieve → prompt → generate flow assembled with LCEL. Finally, we discuss evaluation — how to judge whether the generated answers are truly high quality.
The bridge between a vector store and the pipeline is the retriever. vectorstore.as_retriever() wraps the vector store so you can fetch relevant documents with a consistent interface — and this retriever is itself a Runnable, so it can be plugged straight into an LCEL chain.
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
dokumen_relevan = retriever.invoke("Apa itu RAG?")
for doc in dokumen_relevan:
print(doc.page_content[:80])search_kwargs={"k": 3} requests the three most relevant documents per search. Because the retriever is a Runnable, an invoke call returns a list of Documents — ready to be consumed by the next component.
Behind the retriever is similarity search. similarity_search is the direct way to query the vector store without going through the retriever interface — useful when you need more control, for example fetching raw documents along with their similarity scores.
hasil = vectorstore.similarity_search_with_score(
"Bagaimana cara menyimpan vektor di Postgres?",
k=3,
)
for doc, skor in hasil:
print(round(skor, 4), doc.page_content[:60])Top-k determines how many documents are returned. A small value (2-4) produces focused context but can miss information; a large value gives more complete context but can pull the model off track. A value of 3-5 is generally a balanced starting point.
Info
The score returned by similarity_search_with_score is a distance, not a similarity — the smaller it is, the more relevant. Its exact interpretation depends on the metric your vector store uses.
The metadata from episode 9 pays off here: with filters, you restrict the search to a subset of documents — for example a specific source, date, or category — so results are more relevant and cheaper.
dokumen = vectorstore.similarity_search(
"Strategi investasi jangka panjang",
k=3,
filter={"sumber": "laporan-2026.pdf"},
)The filter syntax can differ between vector stores — Chroma uses a dict like the one above, while Qdrant uses more explicit conditions. The key: make sure metadata is written consistently during add_documents in episode 10, because no filter is useful if the metadata isn't structured.
Now let's assemble everything. The classic RAG pipeline consists of four steps: retrieve context, format the context into text, inject it into the prompt, and generate the answer. With LCEL the whole flow is just a few lines.
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import ChatOpenAI
prompt = ChatPromptTemplate.from_messages([
("system", "Jawab berdasarkan konteks di bawah. Jika konteks tidak cukup, katakan tidak tahu. Konteks: {context}"),
("human", "{pertanyaan}"),
])
model = ChatOpenAI(model="gpt-4o-mini")
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
rag_chain = (
{
"context": retriever | format_docs,
"pertanyaan": RunnablePassthrough(),
}
| prompt
| model
| StrOutputParser()
)
jawaban = rag_chain.invoke("Apa itu RAG dan bagaimana cara kerjanya?")
print(jawaban)This flow deserves a careful read. retriever | format_docs fetches documents then combines their contents into a single context block. RunnablePassthrough passes the original question through. Both are assembled into a dict filled into the prompt, then processed by the model and parser. This way, the format_docs function and the retriever's results can be tested separately before being combined.
RAG isn't a one-shot deal — quality has to be measured. Basic evaluation checks two things: whether the retrieved context is relevant, and whether the generated answer is correct and faithful to the context.
pertanyaan = "Apa peran checkpointer di LangGraph?"
konteks = retriever.invoke(pertanyaan)
for doc in konteks:
print(doc.metadata.get("sumber"), doc.page_content[:70])Start here: when an answer feels wrong, check the context first. If the context isn't relevant, the problem is in chunking (episode 9) or the embedder (episode 10), not the prompt. If the context is relevant but the answer strays, fix the system prompt. Also train a set of test questions and compare RAG answers against expected answers — regular manual comparison is far more valuable than no evaluation at all. You'll go deeper into more systematic evaluation frameworks — datasets, automatic evaluators, and regression testing — through LangSmith in episode 19.
Success
The golden rule of RAG debugging: trace backwards. Wrong answer → check the context. Wrong context → check chunks and embeddings. The prompt is the last component to blame, not the first.
Your basic RAG pipeline now works end-to-end: the retriever fetches relevant documents with top-k and metadata filtering, LCEL assembles retrieve → prompt → generate into a single chain, and you have an evaluation method to maintain quality. From here, the development possibilities only move forward to more advanced techniques.
Key takeaways:
as_retriever() wraps a vector store into a Runnable that plugs directly into an LCEL chain.search_kwargs={"k": N} sets top-k; similarity_search_with_score gives distance scores to judge relevance.RunnablePassthrough.In episode 12 we move on to sharper techniques: Advanced RAG — multi-query retrieval, reranking, hybrid search, and contextual retrieval for answering far harder questions. See you there!