{"id":198,"date":"2026-10-31T13:07:56","date_gmt":"2026-10-31T13:07:56","guid":{"rendered":"https:\/\/devdojo.co.in\/?p=198"},"modified":"2026-10-31T13:07:57","modified_gmt":"2026-10-31T13:07:57","slug":"rag-tutorial-python-pgvector","status":"publish","type":"post","link":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/31\/rag-tutorial-python-pgvector\/","title":{"rendered":"RAG Tutorial: Build a RAG App with Python and pgvector"},"content":{"rendered":"\n<div class=\"dd-post\">\n<style>\n.single article:has(.dd-hero) .entry-media{display:none}\n.dd-post{--navy:#0b1020;--blue:#3178c6;--violet:#7c5cff;--ink:#1f2433;--muted:#5b6275;--line:#e3e6ef;font-size:17px;line-height:1.75;color:var(--ink)}\n.dd-post h2{margin:2.2em 0 .6em;font-size:1.6em;line-height:1.3;scroll-margin-top:90px}\n.dd-post h3{margin:1.6em 0 .5em;font-size:1.22em;scroll-margin-top:90px}\n.dd-post a{color:var(--blue);text-decoration:underline;text-underline-offset:3px}\n.dd-post pre{background:#0f1430;color:#e6e9f5;padding:18px 20px;border-radius:12px;overflow-x:auto;line-height:1.6;border:1px solid #232a52;margin:1.2em 0;font-size:14.5px !important}\n.dd-post pre code{font-size:1em !important;background:none;color:inherit;padding:0;font-family:\"JetBrains Mono\",\"Fira Code\",Consolas,Menlo,monospace;white-space:pre}\n.dd-post :not(pre)>code{background:#eef1fb;color:#2b3a8f;padding:2px 6px;border-radius:5px;font-size:.9em}\n.dd-post .dd-hero{margin:0 0 1.6em;border-radius:16px;overflow:hidden;box-shadow:0 10px 30px rgba(11,16,32,.25)}\n.dd-post .dd-hero svg{display:block;width:100%;height:auto}\n.dd-post .dd-hero .node{transition:transform .25s,filter .25s;transform-box:fill-box;transform-origin:center;cursor:pointer}\n.dd-post .dd-hero .node:hover{transform:scale(1.08);filter:drop-shadow(0 0 10px #7c5cff)}\n.dd-post .dd-summary{background:linear-gradient(135deg,#eef3ff,#f4efff);border:1px solid #d6dcf5;border-left:5px solid var(--violet);border-radius:12px;padding:18px 22px;margin:1.4em 0}\n.dd-post .dd-summary strong.lbl{display:block;font-size:1.05em;margin-bottom:6px;color:#3a2fa0}\n.dd-post .dd-summary ul{margin:.3em 0 0 1.1em}\n.dd-post .dd-toc{background:#f8f9fc;border:1px solid var(--line);border-radius:12px;padding:16px 22px;margin:1.4em 0}\n.dd-post .dd-toc ol{margin:.4em 0 0 1.2em;columns:2;column-gap:28px}\n.dd-post .dd-toc li{break-inside:avoid;margin:.15em 0}\n@media(max-width:640px){.dd-post .dd-toc ol{columns:1}}\n.dd-post .dd-tip,.dd-post .dd-note,.dd-post .dd-warn{border-radius:10px;padding:14px 18px;margin:1.3em 0}\n.dd-post .dd-tip{background:#ecfbf3;border-left:4px solid #1f9d61}\n.dd-post .dd-note{background:#eef4ff;border-left:4px solid var(--blue)}\n.dd-post .dd-warn{background:#fff6e8;border-left:4px solid #e08a00}\n.dd-post .dd-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:14px;margin:1.3em 0}\n.dd-post .dd-card{border:1px solid var(--line);border-radius:12px;padding:14px 16px;background:#fff;box-shadow:0 2px 8px rgba(20,30,70,.05)}\n.dd-post .dd-card b{display:block;color:#3a2fa0;margin-bottom:4px}\n.dd-post table{width:100%;border-collapse:collapse;margin:1.3em 0;font-size:.95em}\n.dd-post th,.dd-post td{border:1px solid var(--line);padding:10px 12px;text-align:left;vertical-align:top}\n.dd-post th{background:#151b3b;color:#fff}\n.dd-post tr:nth-child(even) td{background:#f8f9fc}\n.dd-post .dd-tablewrap{overflow-x:auto}\n.dd-post figure{margin:1.6em 0;text-align:center}\n.dd-post figure svg{width:100%;height:auto;border-radius:12px}\n.dd-post figcaption{font-size:.9em;color:var(--muted);margin-top:8px}\n.dd-post details{border:1px solid var(--line);border-radius:10px;padding:12px 16px;margin:.7em 0;background:#fff}\n.dd-post details summary{cursor:pointer;font-weight:600}\n.dd-post details[open] summary{margin-bottom:8px;color:#3a2fa0}\n.dd-post .dd-tabs{border:1px solid var(--line);border-radius:12px;padding:6px 14px 16px;margin:1.4em 0;background:#fbfbfe}\n.dd-post .dd-tabs input{position:absolute;opacity:0;pointer-events:none}\n.dd-post .dd-tabs label{display:inline-block;padding:8px 14px;margin:8px 6px 0 0;border-radius:999px;border:1px solid #cfd5ea;cursor:pointer;font-weight:600;font-size:.95em;background:#fff}\n.dd-post .dd-tabs .panel{display:none;margin-top:12px}\n.dd-post .dd-tabs #ck1:checked~.labels label[for=ck1],.dd-post .dd-tabs #ck2:checked~.labels label[for=ck2],.dd-post .dd-tabs #ck3:checked~.labels label[for=ck3],.dd-post .dd-tabs #ck4:checked~.labels label[for=ck4]{background:var(--violet);color:#fff;border-color:var(--violet)}\n.dd-post .dd-tabs #ck1:checked~.p1,.dd-post .dd-tabs #ck2:checked~.p2,.dd-post .dd-tabs #ck3:checked~.p3,.dd-post .dd-tabs #ck4:checked~.p4{display:block}\n.dd-post .dd-cta{background:linear-gradient(135deg,#0b1020,#151b3b);color:#e8ebff;border-radius:16px;padding:24px 26px;margin:2em 0;text-align:center}\n.dd-post .dd-cta h3{color:#fff;margin-top:0}\n.dd-post .dd-cta a{color:#a99bff}\n<\/style>\n<div class=\"dd-hero\"><svg viewBox=\"0 0 1200 520\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" role=\"img\" aria-labelledby=\"ddh-t ddh-d\">\n<title id=\"ddh-t\">RAG Tutorial: Build a RAG App with Python and pgvector<\/title>\n<desc id=\"ddh-d\">Animated diagram: documents flow into pgvector, the best matches go to an AI model, which returns a grounded answer.<\/desc>\n<defs>\n<linearGradient id=\"ddh-bg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"#0b1020\"\/><stop offset=\"1\" stop-color=\"#151b3b\"\/><\/linearGradient>\n<linearGradient id=\"ddh-ac\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\"><stop offset=\"0\" stop-color=\"#3178c6\"\/><stop offset=\"1\" stop-color=\"#7c5cff\"\/><\/linearGradient>\n<radialGradient id=\"ddh-glow\" cx=\".5\" cy=\".5\" r=\".5\"><stop offset=\"0\" stop-color=\"#7c5cff\" stop-opacity=\".35\"\/><stop offset=\"1\" stop-color=\"#7c5cff\" stop-opacity=\"0\"\/><\/radialGradient>\n<\/defs>\n<rect x=\"0\" y=\"-60\" width=\"1200\" height=\"640\" fill=\"url(#ddh-bg)\"\/>\n<circle cx=\"1000\" cy=\"90\" r=\"220\" fill=\"url(#ddh-glow)\"\/>\n<circle cx=\"140\" cy=\"470\" r=\"180\" fill=\"url(#ddh-glow)\"\/>\n<g fill=\"#7c5cff\" opacity=\".25\"><circle cx=\"80\" cy=\"60\" r=\"2\"\/><circle cx=\"300\" cy=\"40\" r=\"2\"\/><circle cx=\"620\" cy=\"30\" r=\"2\"\/><circle cx=\"1140\" cy=\"300\" r=\"2\"\/><circle cx=\"900\" cy=\"480\" r=\"2\"\/><circle cx=\"520\" cy=\"500\" r=\"2\"\/><\/g>\n<text x=\"60\" y=\"110\" fill=\"#ffffff\" font-family=\"Segoe UI,Roboto,Arial,sans-serif\" font-size=\"52\" font-weight=\"800\">RAG Tutorial<\/text>\n<text x=\"60\" y=\"165\" fill=\"#c9d1ff\" font-family=\"Segoe UI,Roboto,Arial,sans-serif\" font-size=\"30\" font-weight=\"600\">Build a RAG App with Python &amp; pgvector<\/text>\n<rect x=\"60\" y=\"188\" width=\"200\" height=\"5\" rx=\"2.5\" fill=\"url(#ddh-ac)\"\/>\n<text x=\"60\" y=\"230\" fill=\"#8f99c9\" font-family=\"Segoe UI,Roboto,Arial,sans-serif\" font-size=\"20\">Chunk \u2192 Embed \u2192 Store \u2192 Search \u2192 Answer<\/text>\n<g fill=\"none\" stroke=\"#3a4486\" stroke-width=\"2.5\" stroke-dasharray=\"7 7\">\n<path id=\"ddh-p1\" d=\"M190 390 L420 390\"\/>\n<path id=\"ddh-p2\" d=\"M540 390 L760 390\"\/>\n<path id=\"ddh-p3\" d=\"M880 390 L1060 390\"\/>\n<\/g>\n<g font-family=\"Segoe UI,Roboto,Arial,sans-serif\" text-anchor=\"middle\">\n<g class=\"node\"><rect x=\"70\" y=\"330\" width=\"120\" height=\"120\" rx=\"18\" fill=\"#1b2350\" stroke=\"#3178c6\" stroke-width=\"2.5\"\/><rect x=\"100\" y=\"352\" width=\"60\" height=\"74\" rx=\"6\" fill=\"#e8ecff\"\/><g stroke=\"#7c86b8\" stroke-width=\"3\"><line x1=\"110\" y1=\"368\" x2=\"150\" y2=\"368\"\/><line x1=\"110\" y1=\"382\" x2=\"150\" y2=\"382\"\/><line x1=\"110\" y1=\"396\" x2=\"140\" y2=\"396\"\/><line x1=\"110\" y1=\"410\" x2=\"148\" y2=\"410\"\/><\/g><text x=\"130\" y=\"478\" fill=\"#c9d1ff\" font-size=\"18\">Your docs<\/text><\/g>\n<g class=\"node\"><rect x=\"420\" y=\"330\" width=\"120\" height=\"120\" rx=\"18\" fill=\"#1b2350\" stroke=\"#7c5cff\" stroke-width=\"2.5\"\/><ellipse cx=\"480\" cy=\"362\" rx=\"34\" ry=\"11\" fill=\"#7c5cff\"\/><path d=\"M446 362 v50 a34 11 0 0 0 68 0 v-50\" fill=\"#5a43c9\"\/><ellipse cx=\"480\" cy=\"387\" rx=\"34\" ry=\"11\" fill=\"none\" stroke=\"#b3a6ff\" stroke-width=\"2\"\/><text x=\"480\" y=\"478\" fill=\"#c9d1ff\" font-size=\"18\">pgvector<\/text><\/g>\n<g class=\"node\"><rect x=\"760\" y=\"330\" width=\"120\" height=\"120\" rx=\"18\" fill=\"#1b2350\" stroke=\"#3178c6\" stroke-width=\"2.5\"\/><circle cx=\"820\" cy=\"385\" r=\"30\" fill=\"none\" stroke=\"#7cc4ff\" stroke-width=\"3\"\/><g fill=\"#7cc4ff\"><circle cx=\"808\" cy=\"378\" r=\"5\"\/><circle cx=\"832\" cy=\"378\" r=\"5\"\/><\/g><path d=\"M806 396 q14 12 28 0\" stroke=\"#7cc4ff\" stroke-width=\"3\" fill=\"none\"\/><text x=\"820\" y=\"478\" fill=\"#c9d1ff\" font-size=\"18\">AI model<\/text><\/g>\n<g class=\"node\"><rect x=\"1060\" y=\"330\" width=\"100\" height=\"120\" rx=\"18\" fill=\"#1b2350\" stroke=\"#7c5cff\" stroke-width=\"2.5\"\/><path d=\"M1085 360 h50 a8 8 0 0 1 8 8 v30 a8 8 0 0 1 -8 8 h-30 l-14 12 v-12 h-6 a8 8 0 0 1 -8 -8 v-30 a8 8 0 0 1 8 -8z\" fill=\"#7c5cff\"\/><path d=\"M1096 384 l8 8 l16 -16\" stroke=\"#fff\" stroke-width=\"3.5\" fill=\"none\"\/><text x=\"1110\" y=\"478\" fill=\"#c9d1ff\" font-size=\"18\">Answer<\/text><\/g>\n<\/g>\n<g><circle r=\"7\" fill=\"#7cc4ff\"><animateMotion dur=\"2.6s\" repeatCount=\"indefinite\"><mpath href=\"#ddh-p1\"\/><\/animateMotion><\/circle>\n<circle r=\"7\" fill=\"#b3a6ff\"><animateMotion dur=\"2.6s\" begin=\"0.9s\" repeatCount=\"indefinite\"><mpath href=\"#ddh-p2\"\/><\/animateMotion><\/circle>\n<circle r=\"7\" fill=\"#7cc4ff\"><animateMotion dur=\"2.6s\" begin=\"1.7s\" repeatCount=\"indefinite\"><mpath href=\"#ddh-p3\"\/><\/animateMotion><\/circle><\/g>\n<g font-family=\"Consolas,Menlo,monospace\" font-size=\"15\" fill=\"#7c86b8\"><text x=\"250\" y=\"375\">embed()<\/text><text x=\"595\" y=\"375\">top 5<\/text><text x=\"925\" y=\"375\">grounded<\/text><\/g>\n<\/svg><\/div>\n<p>Large language models are smart, but they do not know <em>your<\/em> data \u2014 your company handbook, your product docs or last week&#8217;s support tickets. Ask them about it and they guess. This <strong>RAG tutorial<\/strong> shows you how to fix that by building a small, real Retrieval-Augmented Generation (RAG) app in Python, using PostgreSQL with the <code>pgvector<\/code> extension as the search engine and the OpenAI API for embeddings and answers.<\/p>\n<p>By the end you will have a working command-line app that reads a folder of documents, stores them as vectors, finds the right pieces for any question and writes an answer that cites its sources. No heavy frameworks \u2014 around 150 lines of plain Python that you can fully understand.<\/p>\n<div class=\"dd-summary\"><strong class=\"lbl\">Quick Summary<\/strong>\n<ul>\n<li><b>RAG<\/b> = search your own documents first, then give the best matches to an AI model so it answers from facts, not memory.<\/li>\n<li>We split documents into <b>chunks<\/b>, turn each chunk into an <b>embedding<\/b> (a list of numbers that captures meaning) and store them in <b>PostgreSQL + pgvector<\/b>.<\/li>\n<li>At question time we embed the question, run a <b>similarity search<\/b> with the <code>&lt;=&gt;<\/code> operator and send the top 5 chunks to the model.<\/li>\n<li>Stack: Python 3.10+, <code>openai<\/code>, <code>psycopg<\/code> 3, <code>pgvector<\/code>, Docker image <code>pgvector\/pgvector:pg18<\/code>.<\/li>\n<li>Includes a FastAPI endpoint, a common errors table and best practices for production.<\/li>\n<\/ul><\/div>\n<nav class=\"dd-toc\" aria-label=\"Table of contents\"><strong>Table of Contents<\/strong>\n<ol>\n<li><a href=\"#what-is-rag\">What is RAG?<\/a><\/li>\n<li><a href=\"#how-rag-works\">How a RAG app works<\/a><\/li>\n<li><a href=\"#why-pgvector\">Why pgvector for RAG?<\/a><\/li>\n<li><a href=\"#setup\">Step 1: Set up PostgreSQL + pgvector<\/a><\/li>\n<li><a href=\"#python-setup\">Step 2: Python project setup<\/a><\/li>\n<li><a href=\"#chunking\">Step 3: Split documents into chunks<\/a><\/li>\n<li><a href=\"#embeddings\">Step 4: Create and store embeddings<\/a><\/li>\n<li><a href=\"#search\">Step 5: Search with pgvector<\/a><\/li>\n<li><a href=\"#answer\">Step 6: Generate the answer<\/a><\/li>\n<li><a href=\"#fastapi\">Step 7: Serve it with FastAPI<\/a><\/li>\n<li><a href=\"#errors\">Common errors and fixes<\/a><\/li>\n<li><a href=\"#best-practices\">Best practices<\/a><\/li>\n<li><a href=\"#faq\">FAQ<\/a><\/li>\n<li><a href=\"#conclusion\">Conclusion<\/a><\/li>\n<\/ol><\/nav>\n<\/div>\n\n\n\n<div class=\"dd-post\">\n<h2 id=\"what-is-rag\">What is RAG? (Retrieval-Augmented Generation explained simply)<\/h2>\n<p>Think of an open-book exam. A student who memorised everything may still mix up facts. A student who is allowed to open the textbook, find the right page and then write the answer will be far more accurate. RAG gives an AI model that open book.<\/p>\n<p>RAG has three simple parts:<\/p>\n<div class=\"dd-cards\">\n<div class=\"dd-card\"><b>Retrieval<\/b>Find the pieces of your documents that are most related to the question.<\/div>\n<div class=\"dd-card\"><b>Augmentation<\/b>Paste those pieces into the prompt, next to the question.<\/div>\n<div class=\"dd-card\"><b>Generation<\/b>The AI model writes an answer using only that context.<\/div>\n<\/div>\n<p>Why not just paste all your documents into the prompt? Because it gets slow and expensive fast, and models pay less attention to details buried in very long prompts. Why not fine-tune the model? Fine-tuning teaches <em>style and behaviour<\/em> well, but it is a poor way to teach facts that change every week. RAG lets you update knowledge by simply re-indexing a file.<\/p>\n<div class=\"dd-note\"><b>Key idea:<\/b> the model is not &#8220;learning&#8221; your data. Each time a question comes in, we look up the relevant text and hand it over. That is why RAG answers can point to their sources.<\/div>\n\n<h2 id=\"how-rag-works\">How a RAG app works (two pipelines)<\/h2>\n<p>Every RAG system has two separate flows. The first one prepares your data. The second one runs each time a user asks something.<\/p>\n<figure><svg viewBox=\"0 0 900 300\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" role=\"img\" aria-labelledby=\"f1t\"><title id=\"f1t\">Indexing pipeline: load files, split into chunks, create embeddings, store in PostgreSQL with pgvector<\/title>\n<rect width=\"900\" height=\"300\" fill=\"#f6f7fd\"\/>\n<g font-family=\"Segoe UI,Roboto,Arial,sans-serif\" text-anchor=\"middle\">\n<g><rect x=\"30\" y=\"90\" width=\"160\" height=\"110\" rx=\"14\" fill=\"#fff\" stroke=\"#3178c6\" stroke-width=\"2\"\/><text x=\"110\" y=\"135\" font-size=\"20\" font-weight=\"700\" fill=\"#1f2433\">1. Load<\/text><text x=\"110\" y=\"165\" font-size=\"15\" fill=\"#5b6275\">.md, .txt, .pdf<\/text><\/g>\n<g><rect x=\"250\" y=\"90\" width=\"160\" height=\"110\" rx=\"14\" fill=\"#fff\" stroke=\"#3178c6\" stroke-width=\"2\"\/><text x=\"330\" y=\"135\" font-size=\"20\" font-weight=\"700\" fill=\"#1f2433\">2. Chunk<\/text><text x=\"330\" y=\"165\" font-size=\"15\" fill=\"#5b6275\">~800 chars + overlap<\/text><\/g>\n<g><rect x=\"470\" y=\"90\" width=\"160\" height=\"110\" rx=\"14\" fill=\"#fff\" stroke=\"#7c5cff\" stroke-width=\"2\"\/><text x=\"550\" y=\"135\" font-size=\"20\" font-weight=\"700\" fill=\"#1f2433\">3. Embed<\/text><text x=\"550\" y=\"165\" font-size=\"15\" fill=\"#5b6275\">text \u2192 1536 numbers<\/text><\/g>\n<g><rect x=\"690\" y=\"90\" width=\"180\" height=\"110\" rx=\"14\" fill=\"#151b3b\"\/><text x=\"780\" y=\"135\" font-size=\"20\" font-weight=\"700\" fill=\"#fff\">4. Store<\/text><text x=\"780\" y=\"165\" font-size=\"15\" fill=\"#c9d1ff\">Postgres + pgvector<\/text><\/g>\n<g stroke=\"#7c5cff\" stroke-width=\"3\" fill=\"#7c5cff\"><line x1=\"192\" y1=\"145\" x2=\"240\" y2=\"145\"\/><path d=\"M240 138 l10 7 l-10 7z\"\/><line x1=\"412\" y1=\"145\" x2=\"460\" y2=\"145\"\/><path d=\"M460 138 l10 7 l-10 7z\"\/><line x1=\"632\" y1=\"145\" x2=\"680\" y2=\"145\"\/><path d=\"M680 138 l10 7 l-10 7z\"\/><\/g>\n<text x=\"450\" y=\"50\" font-size=\"22\" font-weight=\"700\" fill=\"#151b3b\">Step A \u2014 Indexing (run once, or when docs change)<\/text>\n<text x=\"450\" y=\"250\" font-size=\"15\" fill=\"#5b6275\">Every chunk is saved with its text, its source file and its embedding vector.<\/text>\n<\/g><\/svg><figcaption>Figure 1: The indexing pipeline turns your documents into searchable vectors stored in PostgreSQL.<\/figcaption><\/figure>\n<p>In the <b>indexing pipeline<\/b> we load files, split them into small chunks, and convert each chunk into an <b>embedding<\/b>. An embedding is a list of numbers (1,536 of them for OpenAI&#8217;s <code>text-embedding-3-small<\/code>) that represents the meaning of the text. Texts with similar meaning get similar numbers.<\/p>\n<figure><svg viewBox=\"0 0 900 320\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" role=\"img\" aria-labelledby=\"f3t\"><title id=\"f3t\">Illustration of vector space: chunks about similar topics sit close together, and the question lands near the refund chunks<\/title>\n<rect width=\"900\" height=\"320\" fill=\"#0f1430\"\/>\n<g font-family=\"Segoe UI,Roboto,Arial,sans-serif\">\n<line x1=\"60\" y1=\"280\" x2=\"860\" y2=\"280\" stroke=\"#323b6e\" stroke-width=\"2\"\/><line x1=\"60\" y1=\"280\" x2=\"60\" y2=\"30\" stroke=\"#323b6e\" stroke-width=\"2\"\/>\n<g fill=\"#3178c6\"><circle cx=\"180\" cy=\"90\" r=\"9\"\/><circle cx=\"215\" cy=\"120\" r=\"9\"\/><circle cx=\"160\" cy=\"135\" r=\"9\"\/><\/g>\n<text x=\"130\" y=\"70\" fill=\"#7cc4ff\" font-size=\"16\">Shipping chunks<\/text>\n<g fill=\"#7c5cff\"><circle cx=\"560\" cy=\"160\" r=\"9\"\/><circle cx=\"600\" cy=\"190\" r=\"9\"\/><circle cx=\"545\" cy=\"205\" r=\"9\"\/><\/g>\n<text x=\"610\" y=\"150\" fill=\"#b3a6ff\" font-size=\"16\">Refund chunks<\/text>\n<g fill=\"#1f9d61\"><circle cx=\"330\" cy=\"240\" r=\"9\"\/><circle cx=\"370\" cy=\"225\" r=\"9\"\/><\/g>\n<text x=\"300\" y=\"270\" fill=\"#6fe0a6\" font-size=\"16\">Login chunks<\/text>\n<circle cx=\"585\" cy=\"225\" r=\"11\" fill=\"#ffcc4d\"><animate attributeName=\"r\" values=\"9;14;9\" dur=\"2s\" repeatCount=\"indefinite\"\/><\/circle>\n<circle cx=\"585\" cy=\"225\" r=\"55\" fill=\"none\" stroke=\"#ffcc4d\" stroke-dasharray=\"5 6\" stroke-width=\"2\"\/>\n<text x=\"650\" y=\"250\" fill=\"#ffcc4d\" font-size=\"16\">Your question<\/text>\n<text x=\"70\" y=\"305\" fill=\"#8f99c9\" font-size=\"14\">Real embeddings have 1536 dimensions \u2014 this is a 2D picture of the same idea.<\/text>\n<\/g><\/svg><figcaption>Figure 2: Embeddings place text with similar meaning close together, so &#8220;nearest neighbours&#8221; means &#8220;most relevant text&#8221;.<\/figcaption><\/figure>\n<p>In the <b>query pipeline<\/b> we embed the user&#8217;s question with the same model, ask the database for the closest chunks, and send those chunks plus the question to a chat model.<\/p>\n<figure><svg viewBox=\"0 0 900 340\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" role=\"img\" aria-labelledby=\"f2t\"><title id=\"f2t\">Query flow: question is embedded, pgvector finds the closest chunks, the chunks and question go to the AI model, which writes the answer<\/title>\n<rect width=\"900\" height=\"340\" fill=\"#f6f7fd\"\/>\n<g font-family=\"Segoe UI,Roboto,Arial,sans-serif\" text-anchor=\"middle\">\n<text x=\"450\" y=\"45\" font-size=\"22\" font-weight=\"700\" fill=\"#151b3b\">Step B \u2014 Answering a question (every request)<\/text>\n<g><rect x=\"30\" y=\"80\" width=\"190\" height=\"80\" rx=\"14\" fill=\"#fff\" stroke=\"#3178c6\" stroke-width=\"2\"\/><text x=\"125\" y=\"115\" font-size=\"17\" font-weight=\"700\" fill=\"#1f2433\">User question<\/text><text x=\"125\" y=\"140\" font-size=\"14\" fill=\"#5b6275\">&#8220;How do refunds work?&#8221;<\/text><\/g>\n<g><rect x=\"270\" y=\"80\" width=\"170\" height=\"80\" rx=\"14\" fill=\"#fff\" stroke=\"#7c5cff\" stroke-width=\"2\"\/><text x=\"355\" y=\"115\" font-size=\"17\" font-weight=\"700\" fill=\"#1f2433\">Embed question<\/text><text x=\"355\" y=\"140\" font-size=\"14\" fill=\"#5b6275\">same model as docs<\/text><\/g>\n<g><rect x=\"490\" y=\"80\" width=\"180\" height=\"80\" rx=\"14\" fill=\"#151b3b\"\/><text x=\"580\" y=\"115\" font-size=\"17\" font-weight=\"700\" fill=\"#fff\">Vector search<\/text><text x=\"580\" y=\"140\" font-size=\"14\" fill=\"#c9d1ff\">ORDER BY embedding &lt;=&gt; q<\/text><\/g>\n<g><rect x=\"720\" y=\"80\" width=\"150\" height=\"80\" rx=\"14\" fill=\"#fff\" stroke=\"#3178c6\" stroke-width=\"2\"\/><text x=\"795\" y=\"115\" font-size=\"17\" font-weight=\"700\" fill=\"#1f2433\">Top 5 chunks<\/text><text x=\"795\" y=\"140\" font-size=\"14\" fill=\"#5b6275\">most similar text<\/text><\/g>\n<g><rect x=\"470\" y=\"220\" width=\"220\" height=\"80\" rx=\"14\" fill=\"#fff\" stroke=\"#7c5cff\" stroke-width=\"2\"\/><text x=\"580\" y=\"255\" font-size=\"17\" font-weight=\"700\" fill=\"#1f2433\">AI model (LLM)<\/text><text x=\"580\" y=\"280\" font-size=\"14\" fill=\"#5b6275\">question + chunks in prompt<\/text><\/g>\n<g><rect x=\"190\" y=\"220\" width=\"220\" height=\"80\" rx=\"14\" fill=\"#e9fbf1\" stroke=\"#1f9d61\" stroke-width=\"2\"\/><text x=\"300\" y=\"255\" font-size=\"17\" font-weight=\"700\" fill=\"#1f2433\">Grounded answer<\/text><text x=\"300\" y=\"280\" font-size=\"14\" fill=\"#5b6275\">with source file names<\/text><\/g>\n<g stroke=\"#7c5cff\" stroke-width=\"3\" fill=\"#7c5cff\"><line x1=\"222\" y1=\"120\" x2=\"260\" y2=\"120\"\/><path d=\"M260 113 l10 7 l-10 7z\"\/><line x1=\"442\" y1=\"120\" x2=\"480\" y2=\"120\"\/><path d=\"M480 113 l10 7 l-10 7z\"\/><line x1=\"672\" y1=\"120\" x2=\"710\" y2=\"120\"\/><path d=\"M710 113 l10 7 l-10 7z\"\/><path d=\"M795 162 V260 H702\" fill=\"none\"\/><path d=\"M702 253 l-10 7 l10 7z\"\/><line x1=\"468\" y1=\"260\" x2=\"422\" y2=\"260\"\/><path d=\"M422 253 l-10 7 l10 7z\"\/><\/g>\n<\/g><\/svg><figcaption>Figure 3: At question time, RAG finds the most relevant chunks first and only then asks the AI model to answer.<\/figcaption><\/figure>\n<\/div>\n\n\n\n<div class=\"dd-post\">\n<h2 id=\"why-pgvector\">Why pgvector for RAG?<\/h2>\n<p>There are many dedicated vector databases, but if you already use PostgreSQL, <a href=\"https:\/\/github.com\/pgvector\/pgvector\" target=\"_blank\" rel=\"noopener\">pgvector<\/a> is often the simplest choice. It is an open-source extension that adds a <code>vector<\/code> column type, distance operators and fast approximate indexes (HNSW and IVFFlat) to Postgres.<\/p>\n<div class=\"dd-tablewrap\"><table>\n<thead><tr><th>Need<\/th><th>What pgvector gives you<\/th><\/tr><\/thead>\n<tbody>\n<tr><td>One database for everything<\/td><td>Vectors live next to your users, orders and permissions tables. Joins and filters just work.<\/td><\/tr>\n<tr><td>Fast similarity search<\/td><td>HNSW indexes for approximate nearest-neighbour search on millions of rows.<\/td><\/tr>\n<tr><td>Familiar tools<\/td><td>Backups, migrations, SQL, transactions \u2014 nothing new to learn.<\/td><\/tr>\n<tr><td>Low cost<\/td><td>Runs on any managed Postgres that supports the extension, or in a local Docker container.<\/td><\/tr>\n<\/tbody><\/table><\/div>\n<p>New to modern Postgres? Our guide to <a href=\"https:\/\/devdojo.co.in\/index.php\/2026\/10\/09\/postgresql-19-new-features\/\">PostgreSQL 19 new features<\/a> covers what&#8217;s changed recently.<\/p>\n\n<h2 id=\"setup\">Step 1: Set up PostgreSQL with pgvector<\/h2>\n<p>The quickest way is the official Docker image, which ships Postgres with pgvector already compiled. Change <code>pg18<\/code> to match the Postgres major version you want.<\/p>\n<pre><code class=\"language-bash\">docker run -d --name rag-db \\\n  -e POSTGRES_PASSWORD=postgres \\\n  -e POSTGRES_DB=rag \\\n  -p 5432:5432 \\\n  pgvector\/pgvector:pg18<\/code><\/pre>\n<p>Now create the extension and a table for our chunks. Save this as <code>schema.sql<\/code>:<\/p>\n<pre><code class=\"language-sql\">CREATE EXTENSION IF NOT EXISTS vector;\n\nCREATE TABLE IF NOT EXISTS chunks (\n    id         bigserial PRIMARY KEY,\n    source     text        NOT NULL,          -- file name the chunk came from\n    chunk_no   int         NOT NULL,          -- position inside the file\n    content    text        NOT NULL,          -- the actual text\n    embedding  vector(1536) NOT NULL,         -- text-embedding-3-small size\n    created_at timestamptz DEFAULT now(),\n    UNIQUE (source, chunk_no)\n);\n\n-- HNSW index for fast cosine-similarity search\nCREATE INDEX IF NOT EXISTS chunks_embedding_idx\n    ON chunks USING hnsw (embedding vector_cosine_ops);<\/code><\/pre>\n<pre><code class=\"language-bash\">docker exec -i rag-db psql -U postgres -d rag &lt; schema.sql<\/code><\/pre>\n<div class=\"dd-tip\"><b>Tip:<\/b> the number in <code>vector(1536)<\/code> must match your embedding model&#8217;s output size exactly. If you switch to a different model later, you need a new column (or table) and a full re-index.<\/div>\n<p>Prefer running the database alongside your app? See our <a href=\"https:\/\/devdojo.co.in\/index.php\/2026\/10\/10\/docker-compose-tutorial\/\">Docker Compose tutorial<\/a> and just swap the image for <code>pgvector\/pgvector:pg18<\/code>.<\/p>\n<\/div>\n\n\n\n<div class=\"dd-post\">\n<h2 id=\"python-setup\">Step 2: Python project setup<\/h2>\n<p>Create a fresh folder and a virtual environment, then install four packages:<\/p>\n<pre><code class=\"language-bash\">mkdir rag-app &amp;&amp; cd rag-app\npython -m venv .venv\nsource .venv\/bin\/activate        # Windows: .venv\\Scripts\\activate\n\npip install openai &quot;psycopg[binary]&quot; pgvector numpy<\/code><\/pre>\n<p>Set your OpenAI API key and database URL as environment variables (never hard-code keys in source files):<\/p>\n<pre><code class=\"language-bash\">export OPENAI_API_KEY=&quot;sk-...&quot;\nexport DATABASE_URL=&quot;postgresql:\/\/postgres:postgres@localhost:5432\/rag&quot;<\/code><\/pre>\n<p>Put a few <code>.md<\/code> or <code>.txt<\/code> files in a <code>docs\/<\/code> folder \u2014 for example your project README, an FAQ, or a policy document. Then create <code>db.py<\/code> for a shared connection helper:<\/p>\n<pre><code class=\"language-python\"># db.py\nimport os\nimport psycopg\nfrom pgvector.psycopg import register_vector\n\ndef get_conn() -&gt; psycopg.Connection:\n    conn = psycopg.connect(os.environ[&quot;DATABASE_URL&quot;], autocommit=True)\n    register_vector(conn)   # teaches psycopg how to send\/receive vectors\n    return conn<\/code><\/pre>\n\n<h2 id=\"chunking\">Step 3: Split documents into chunks<\/h2>\n<p>Embedding a whole 40-page file as one vector would blur all its topics together. Instead we split each file into chunks of roughly 800 characters, with a small overlap so ideas that cross a boundary are not lost. Try the tabs below to compare popular strategies:<\/p>\n<div class=\"dd-tabs\" role=\"group\" aria-label=\"Chunking strategies\">\n<input type=\"radio\" name=\"ddck\" id=\"ck1\" checked><input type=\"radio\" name=\"ddck\" id=\"ck2\"><input type=\"radio\" name=\"ddck\" id=\"ck3\"><input type=\"radio\" name=\"ddck\" id=\"ck4\">\n<div class=\"labels\"><label for=\"ck1\">Fixed size<\/label><label for=\"ck2\">Paragraphs<\/label><label for=\"ck3\">Markdown headings<\/label><label for=\"ck4\">Semantic<\/label><\/div>\n<div class=\"panel p1\"><p><b>Fixed size with overlap<\/b> \u2014 cut every N characters, repeat the last 100\u2013200 characters in the next chunk. <br><b>Good for:<\/b> plain text, quick prototypes. <b>Watch out:<\/b> may cut a sentence in half (overlap softens this).<\/p><\/div>\n<div class=\"panel p2\"><p><b>Paragraph-aware<\/b> \u2014 keep whole paragraphs together and pack them until the size limit. This is what our code does. <br><b>Good for:<\/b> articles, docs, FAQs. <b>Watch out:<\/b> one giant paragraph still needs a hard split.<\/p><\/div>\n<div class=\"panel p3\"><p><b>Split by Markdown headings<\/b> \u2014 every <code>##<\/code> section becomes a chunk, and the heading is added to the chunk text. <br><b>Good for:<\/b> technical documentation. <b>Watch out:<\/b> very short sections carry little meaning \u2014 merge them.<\/p><\/div>\n<div class=\"panel p4\"><p><b>Semantic chunking<\/b> \u2014 embed sentences and start a new chunk where the meaning changes sharply. <br><b>Good for:<\/b> long, mixed documents. <b>Watch out:<\/b> more embedding calls and more complexity; start simple first.<\/p><\/div>\n<\/div>\n<p>Here is a simple paragraph-aware chunker with overlap \u2014 no libraries needed:<\/p>\n<pre><code class=\"language-python\"># chunker.py\ndef chunk_text(text: str, max_chars: int = 800, overlap: int = 150) -&gt; list[str]:\n    &quot;&quot;&quot;Pack paragraphs into chunks of about max_chars, with overlap.&quot;&quot;&quot;\n    paragraphs = [p.strip() for p in text.split(&quot;\\n\\n&quot;) if p.strip()]\n    chunks: list[str] = []\n    current = &quot;&quot;\n\n    for para in paragraphs:\n        # Very long paragraph: hard-split it first\n        while len(para) &gt; max_chars:\n            chunks.append(para[:max_chars])\n            para = para[max_chars - overlap:]\n\n        if len(current) + len(para) + 2 &lt;= max_chars:\n            current = f&quot;{current}\\n\\n{para}&quot; if current else para\n        else:\n            if current:\n                chunks.append(current)\n            tail = current[-overlap:] if current else &quot;&quot;\n            current = f&quot;{tail}\\n\\n{para}&quot; if tail else para\n\n    if current:\n        chunks.append(current)\n    return chunks\n\n\nif __name__ == &quot;__main__&quot;:\n    sample = &quot;First paragraph about refunds.\\n\\nSecond paragraph about shipping.&quot;\n    for i, c in enumerate(chunk_text(sample, max_chars=40, overlap=10)):\n        print(i, repr(c))<\/code><\/pre>\n<div class=\"dd-note\"><b>How big should chunks be?<\/b> 500\u20131,000 characters (roughly 100\u2013250 words) is a good starting point. Smaller chunks give more precise matches; bigger chunks give the model more context. Measure with real questions and adjust.<\/div>\n<\/div>\n\n\n\n<div class=\"dd-post\">\n<h2 id=\"embeddings\">Step 4: Create and store embeddings<\/h2>\n<p>Now the indexing script. It reads every file in <code>docs\/<\/code>, chunks it, sends the chunks to the embeddings endpoint in batches, and upserts them into Postgres. We use <code>text-embedding-3-small<\/code> because it is cheap, fast and returns 1,536 dimensions \u2014 matching our table.<\/p>\n<pre><code class=\"language-python\"># ingest.py\nfrom pathlib import Path\n\nimport numpy as np\nfrom openai import OpenAI\n\nfrom chunker import chunk_text\nfrom db import get_conn\n\nclient = OpenAI()                      # reads OPENAI_API_KEY from the environment\nEMBED_MODEL = &quot;text-embedding-3-small&quot;\nBATCH_SIZE = 100\n\n\ndef embed(texts: list[str]) -&gt; list[np.ndarray]:\n    resp = client.embeddings.create(model=EMBED_MODEL, input=texts)\n    return [np.array(item.embedding, dtype=np.float32) for item in resp.data]\n\n\ndef ingest(folder: str = &quot;docs&quot;) -&gt; None:\n    conn = get_conn()\n    files = [p for p in Path(folder).rglob(&quot;*&quot;) if p.suffix in {&quot;.md&quot;, &quot;.txt&quot;}]\n\n    for path in files:\n        chunks = chunk_text(path.read_text(encoding=&quot;utf-8&quot;))\n        # Remove old chunks for this file so re-running stays clean\n        conn.execute(&quot;DELETE FROM chunks WHERE source = %s&quot;, (str(path),))\n\n        for start in range(0, len(chunks), BATCH_SIZE):\n            batch = chunks[start:start + BATCH_SIZE]\n            vectors = embed(batch)\n            with conn.cursor() as cur:\n                cur.executemany(\n                    &quot;&quot;&quot;INSERT INTO chunks (source, chunk_no, content, embedding)\n                       VALUES (%s, %s, %s, %s)&quot;&quot;&quot;,\n                    [(str(path), start + i, text, vec)\n                     for i, (text, vec) in enumerate(zip(batch, vectors))],\n                )\n        print(f&quot;Indexed {len(chunks):&gt;3} chunks from {path}&quot;)\n\n\nif __name__ == &quot;__main__&quot;:\n    ingest()<\/code><\/pre>\n<pre><code class=\"language-bash\">python ingest.py\n# Indexed  12 chunks from docs\/refund-policy.md\n# Indexed   7 chunks from docs\/shipping.md<\/code><\/pre>\n<div class=\"dd-tip\"><b>Tip:<\/b> the embeddings endpoint accepts a list of inputs, so batching 100 chunks per request is much faster (and fewer requests) than calling it once per chunk.<\/div>\n\n<h2 id=\"search\">Step 5: Search with pgvector<\/h2>\n<p>pgvector gives you three main distance operators. For OpenAI embeddings, cosine distance is the usual choice.<\/p>\n<div class=\"dd-tablewrap\"><table>\n<thead><tr><th>Operator<\/th><th>Meaning<\/th><th>Index ops class<\/th><\/tr><\/thead>\n<tbody>\n<tr><td><code>&lt;=&gt;<\/code><\/td><td>Cosine distance (0 = same direction)<\/td><td><code>vector_cosine_ops<\/code><\/td><\/tr>\n<tr><td><code>&lt;-&gt;<\/code><\/td><td>Euclidean (L2) distance<\/td><td><code>vector_l2_ops<\/code><\/td><\/tr>\n<tr><td><code>&lt;#&gt;<\/code><\/td><td>Negative inner product<\/td><td><code>vector_ip_ops<\/code><\/td><\/tr>\n<\/tbody><\/table><\/div>\n<p>The operator in your query must match the ops class of your index, otherwise Postgres will skip the index and scan every row. Here is the retrieval function:<\/p>\n<pre><code class=\"language-python\"># retrieve.py\nfrom dataclasses import dataclass\n\nfrom db import get_conn\nfrom ingest import embed\n\n\n@dataclass\nclass Hit:\n    source: str\n    content: str\n    similarity: float\n\n\ndef search(question: str, k: int = 5, min_similarity: float = 0.3) -&gt; list[Hit]:\n    q_vec = embed([question])[0]\n    conn = get_conn()\n    rows = conn.execute(\n        &quot;&quot;&quot;SELECT source, content, 1 - (embedding &lt;=&gt; %s) AS similarity\n           FROM chunks\n           ORDER BY embedding &lt;=&gt; %s\n           LIMIT %s&quot;&quot;&quot;,\n        (q_vec, q_vec, k),\n    ).fetchall()\n    return [Hit(*r) for r in rows if r[2] &gt;= min_similarity]\n\n\nif __name__ == &quot;__main__&quot;:\n    for hit in search(&quot;How many days do I have to ask for a refund?&quot;):\n        print(f&quot;{hit.similarity:.3f}  {hit.source}  {hit.content[:70]!r}&quot;)<\/code><\/pre>\n<p>Run it and you should see the refund-policy chunks at the top with the highest similarity scores. This step alone \u2014 &#8220;semantic search&#8221; \u2014 is already useful, even before an AI model writes anything.<\/p>\n<div class=\"dd-note\"><b>Why <code>1 - distance<\/code>?<\/b> Cosine <em>distance<\/em> is small when texts are similar. Subtracting from 1 turns it into a <em>similarity<\/em> score that is easier to read (closer to 1 = more similar).<\/div>\n<\/div>\n\n\n\n<div class=\"dd-post\">\n<h2 id=\"answer\">Step 6: Generate the answer with the retrieved context<\/h2>\n<p>Now we put the &#8220;G&#8221; in RAG. We build a prompt that contains the numbered chunks and clear rules: answer only from the context, say &#8220;I don&#8217;t know&#8221; when the answer is missing, and cite sources. We use the OpenAI <a href=\"https:\/\/platform.openai.com\/docs\/api-reference\/responses\" target=\"_blank\" rel=\"noopener\">Responses API<\/a>; the chat model name comes from an environment variable so you can switch models without touching code.<\/p>\n<pre><code class=\"language-python\"># rag.py\nimport os\nimport sys\n\nfrom openai import OpenAI\n\nfrom retrieve import search\n\nclient = OpenAI()\nCHAT_MODEL = os.getenv(&quot;CHAT_MODEL&quot;, &quot;gpt-5-mini&quot;)\n\nINSTRUCTIONS = &quot;&quot;&quot;You are a helpful assistant for our documentation.\nAnswer ONLY using the context below.\nIf the answer is not in the context, reply: &quot;I don&#x27;t know based on the docs.&quot;\nCite sources like [1], [2] after the sentences that use them.\nKeep answers short and clear.&quot;&quot;&quot;\n\n\ndef ask(question: str) -&gt; str:\n    hits = search(question, k=5)\n    if not hits:\n        return &quot;I don&#x27;t know based on the docs.&quot;\n\n    context = &quot;\\n\\n&quot;.join(\n        f&quot;[{i}] (source: {h.source})\\n{h.content}&quot; for i, h in enumerate(hits, 1)\n    )\n    response = client.responses.create(\n        model=CHAT_MODEL,\n        instructions=INSTRUCTIONS,\n        input=f&quot;Context:\\n{context}\\n\\nQuestion: {question}&quot;,\n    )\n    sources = &quot;\\n&quot;.join(f&quot;[{i}] {h.source}&quot; for i, h in enumerate(hits, 1))\n    return f&quot;{response.output_text}\\n\\nSources:\\n{sources}&quot;\n\n\nif __name__ == &quot;__main__&quot;:\n    question = &quot; &quot;.join(sys.argv[1:]) or &quot;What is the refund window?&quot;\n    print(ask(question))<\/code><\/pre>\n<pre><code class=\"language-bash\">python rag.py &quot;Can I get a refund after 20 days?&quot;\n# Yes. Refunds are accepted within 30 days of delivery if the item is unused [1].\n# Digital products are not refundable once downloaded [2].\n#\n# Sources:\n# [1] docs\/refund-policy.md\n# [2] docs\/refund-policy.md<\/code><\/pre>\n<p>That&#8217;s a complete RAG pipeline: chunk \u2192 embed \u2192 store \u2192 search \u2192 answer. Each piece is small and easy to replace. Want the model to decide <em>when<\/em> to search, or combine search with other tools? That&#8217;s where agents come in \u2014 see our <a href=\"https:\/\/devdojo.co.in\/index.php\/2026\/10\/08\/openai-agents-sdk-python\/\">OpenAI Agents SDK tutorial<\/a> and plug <code>search()<\/code> in as a tool.<\/p>\n\n<h2 id=\"fastapi\">Step 7: Serve your RAG app with FastAPI<\/h2>\n<p>To use RAG from a website or mobile app, wrap <code>ask()<\/code> in a tiny API:<\/p>\n<pre><code class=\"language-bash\">pip install &quot;fastapi[standard]&quot;<\/code><\/pre>\n<pre><code class=\"language-python\"># api.py\nfrom fastapi import FastAPI\nfrom pydantic import BaseModel, Field\n\nfrom rag import ask\n\napp = FastAPI(title=&quot;Docs RAG API&quot;)\n\n\nclass Question(BaseModel):\n    question: str = Field(min_length=3, max_length=500)\n\n\n@app.post(&quot;\/ask&quot;)\ndef ask_endpoint(body: Question) -&gt; dict:\n    return {&quot;answer&quot;: ask(body.question)}<\/code><\/pre>\n<pre><code class=\"language-bash\">fastapi dev api.py\n# In another terminal:\ncurl -X POST http:\/\/127.0.0.1:8000\/ask \\\n  -H &quot;Content-Type: application\/json&quot; \\\n  -d &#x27;{&quot;question&quot;: &quot;How long does shipping take to India?&quot;}&#x27;<\/code><\/pre>\n<p>FastAPI also gives you interactive docs at <code>http:\/\/127.0.0.1:8000\/docs<\/code> for free, which is handy for testing questions.<\/p>\n<\/div>\n\n\n\n<div class=\"dd-post\">\n<h2 id=\"errors\">Common errors and fixes<\/h2>\n<div class=\"dd-tablewrap\"><table>\n<thead><tr><th>Error \/ symptom<\/th><th>Why it happens<\/th><th>Fix<\/th><\/tr><\/thead>\n<tbody>\n<tr><td><code>type \"vector\" does not exist<\/code><\/td><td>The extension is not enabled in this database.<\/td><td>Run <code>CREATE EXTENSION vector;<\/code> in the same database your app connects to.<\/td><\/tr>\n<tr><td><code>expected 1536 dimensions, not 3072<\/code><\/td><td>Embedding model output size doesn&#8217;t match the column (e.g. you used <code>text-embedding-3-large<\/code>).<\/td><td>Use the matching model, pass <code>dimensions=1536<\/code>, or change the column and re-index.<\/td><\/tr>\n<tr><td><code>cannot adapt type 'numpy.ndarray'<\/code> or <code>operator does not exist: vector &lt;=&gt; double precision[]<\/code><\/td><td><code>register_vector(conn)<\/code> was not called on that connection.<\/td><td>Always create connections through a helper like <code>get_conn()<\/code> that registers the type.<\/td><\/tr>\n<tr><td>Search is slow on big tables<\/td><td>Index not used \u2014 wrong operator, or no <code>ORDER BY ... LIMIT<\/code>.<\/td><td>Match <code>&lt;=&gt;<\/code> with <code>vector_cosine_ops<\/code>, keep <code>ORDER BY embedding &lt;=&gt; q LIMIT k<\/code>, check with <code>EXPLAIN<\/code>.<\/td><\/tr>\n<tr><td><code>openai.RateLimitError<\/code> (429)<\/td><td>Too many requests or tokens per minute while indexing.<\/td><td>Batch inputs, add retry with exponential backoff, or check your usage limits.<\/td><\/tr>\n<tr><td>Answers are made up<\/td><td>Weak instructions or irrelevant chunks passed in.<\/td><td>Tell the model to answer only from context, use a similarity threshold, and return &#8220;I don&#8217;t know&#8221;.<\/td><\/tr>\n<tr><td>Right document, wrong answer<\/td><td>Chunks too small, the key sentence was cut off.<\/td><td>Increase chunk size or overlap; include the section heading in each chunk.<\/td><\/tr>\n<\/tbody><\/table><\/div>\n\n<h2 id=\"best-practices\">RAG best practices for production<\/h2>\n<ul>\n<li><b>Build a small test set first.<\/b> Write 20\u201330 real questions with expected answers. Re-run them every time you change chunk size, model or prompt.<\/li>\n<li><b>Store metadata.<\/b> Keep source, title, URL, date and access level next to each chunk so you can filter (<code>WHERE team_id = %s<\/code>) and show links.<\/li>\n<li><b>Respect permissions.<\/b> Filter chunks by the user&#8217;s access rights in SQL <em>before<\/em> they reach the model \u2014 never rely on the prompt to hide data.<\/li>\n<li><b>Try hybrid search.<\/b> Combine vector search with Postgres full-text search (<code>tsvector<\/code>) so exact words like error codes and product names still match.<\/li>\n<li><b>Re-index on change.<\/b> Store a hash of each file and only re-embed files that changed. It saves time and money.<\/li>\n<li><b>Tune the index.<\/b> For HNSW, raising <code>hnsw.ef_search<\/code> (default 40) improves recall at some speed cost: <code>SET hnsw.ef_search = 100;<\/code><\/li>\n<li><b>Treat documents as untrusted.<\/b> Text inside documents can contain instructions (&#8220;ignore previous rules\u2026&#8221;). Keep your rules in the instructions and never give the model powerful tools based only on retrieved text.<\/li>\n<li><b>Log everything.<\/b> Save the question, retrieved chunk IDs and the answer. When an answer is wrong you&#8217;ll know whether retrieval or generation failed.<\/li>\n<\/ul>\n<div class=\"dd-warn\"><b>Cost note:<\/b> embedding is a one-time cost per chunk and is usually very cheap. Most of your spending comes from the chat model, so keep <code>k<\/code> small (3\u20135 chunks) and chunks focused.<\/div>\n<\/div>\n\n\n\n<div class=\"dd-post\">\n<h2 id=\"faq\">FAQ<\/h2>\n<details><summary>What is RAG in simple words?<\/summary><p>RAG (Retrieval-Augmented Generation) means the app first <em>searches<\/em> your documents for relevant text and then asks an AI model to answer using that text. It&#8217;s like letting the model take an open-book exam.<\/p><\/details>\n<details><summary>Do I need a dedicated vector database for RAG?<\/summary><p>No. For most apps up to millions of chunks, PostgreSQL with pgvector works well and keeps everything in one place. Dedicated vector databases can help at very large scale or for special features.<\/p><\/details>\n<details><summary>Is RAG better than fine-tuning?<\/summary><p>They solve different problems. RAG is best for adding facts that change often and need sources. Fine-tuning is better for changing tone, format or behaviour. Many teams use RAG first and fine-tune only if needed.<\/p><\/details>\n<details><summary>Can I use a free or local embedding model instead of OpenAI?<\/summary><p>Yes. Any embedding model works as long as you use the same model for documents and questions, and your <code>vector(N)<\/code> column matches its output size. Open-source models via sentence-transformers or Ollama are popular local options.<\/p><\/details>\n<details><summary>How many chunks should I send to the model?<\/summary><p>Start with 3\u20135. More chunks add cost and can distract the model. If answers miss information, improve chunking or search quality before raising <code>k<\/code>.<\/p><\/details>\n<details><summary>How do I handle PDFs?<\/summary><p>Extract the text first (for example with the <code>pypdf<\/code> or <code>pymupdf<\/code> libraries), then pass the extracted text to the same chunker. Scanned PDFs need OCR before chunking.<\/p><\/details>\n\n<h2 id=\"conclusion\">Conclusion<\/h2>\n<p>You&#8217;ve just built a real RAG app from scratch: documents are chunked, turned into embeddings, stored in PostgreSQL with pgvector, searched by meaning and passed to an AI model that answers with sources. That&#8217;s the same core design used by documentation bots, support assistants and internal knowledge search tools at many companies.<\/p>\n<p>From here, try adding hybrid search, metadata filters and a small evaluation set. Each improvement is just a few lines of Python or SQL on top of what you built in this RAG tutorial.<\/p>\n<div class=\"dd-cta\"><h3>New posts every day on DevDojo<\/h3><p>Practical, beginner-friendly guides on AI, backend, frontend and DevOps. Bookmark <a href=\"https:\/\/devdojo.co.in\/\">devdojo.co.in<\/a> and drop your RAG questions in the comments below \u2014 we read every one.<\/p><\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Step-by-step RAG tutorial: build a Retrieval-Augmented Generation app with Python, PostgreSQL pgvector and OpenAI embeddings, with runnable code and fixes.<\/p>\n","protected":false},"author":1,"featured_media":197,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[38,69,9],"tags":[89,90,87,70,86,88],"class_list":["post-198","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai","category-databases","category-python","tag-embeddings","tag-openai-api","tag-pgvector","tag-postgresql","tag-rag","tag-vector-search"],"aioseo_notices":[],"aioseo_head":"\n\t\t<!-- All in One SEO 5.0.4 - aioseo.com -->\n\t<meta name=\"description\" content=\"Step-by-step RAG tutorial: build a Retrieval-Augmented Generation app with Python, PostgreSQL pgvector and OpenAI embeddings, with runnable code and fixes.\" \/>\n\t<meta name=\"robots\" content=\"max-image-preview:large\" \/>\n\t<meta name=\"author\" content=\"Lokendra\"\/>\n\t<link rel=\"canonical\" href=\"https:\/\/devdojo.co.in\/index.php\/2026\/10\/31\/rag-tutorial-python-pgvector\/\" \/>\n\t\t<meta name=\"generator\" content=\"All in One SEO (AIOSEO) 5.0.4\" \/>\n\t\t<meta property=\"og:locale\" content=\"en_US\" \/>\n\t\t<meta property=\"og:site_name\" content=\"DevDojo - Empowering Developers\" \/>\n\t\t<meta property=\"og:type\" content=\"article\" \/>\n\t\t<meta property=\"og:title\" content=\"RAG Tutorial: Build a RAG App with Python &amp; pgvector\" \/>\n\t\t<meta property=\"og:description\" content=\"Step-by-step RAG tutorial: build a Retrieval-Augmented Generation app with Python, PostgreSQL pgvector and OpenAI embeddings, with runnable code and fixes.\" \/>\n\t\t<meta property=\"og:url\" content=\"https:\/\/devdojo.co.in\/index.php\/2026\/10\/31\/rag-tutorial-python-pgvector\/\" \/>\n\t\t<meta property=\"article:published_time\" content=\"2026-10-31T13:07:56+00:00\" \/>\n\t\t<meta property=\"article:modified_time\" content=\"2026-10-31T13:07:57+00:00\" \/>\n\t\t<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n\t\t<meta name=\"twitter:title\" content=\"RAG Tutorial: Build a RAG App with Python &amp; pgvector\" \/>\n\t\t<meta name=\"twitter:description\" content=\"Step-by-step RAG tutorial: build a Retrieval-Augmented Generation app with Python, PostgreSQL pgvector and OpenAI embeddings, with runnable code and fixes.\" \/>\n\t\t<script type=\"application\/ld+json\" class=\"aioseo-schema\">\n\t\t\t{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"BlogPosting\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/31\\\/rag-tutorial-python-pgvector\\\/#blogposting\",\"name\":\"RAG Tutorial: Build a RAG App with Python & pgvector\",\"headline\":\"RAG Tutorial: Build a RAG App with Python and pgvector\",\"author\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/author\\\/lokendra\\\/#author\"},\"publisher\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#organization\"},\"image\":{\"@type\":\"ImageObject\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/rag-tutorial-python-pgvector.jpg\",\"width\":1200,\"height\":630,\"caption\":\"RAG tutorial banner: documents flow into pgvector, then an AI model returns a grounded answer\"},\"datePublished\":\"2026-10-31T13:07:56+00:00\",\"dateModified\":\"2026-10-31T13:07:57+00:00\",\"inLanguage\":\"en-US\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/31\\\/rag-tutorial-python-pgvector\\\/#webpage\"},\"isPartOf\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/31\\\/rag-tutorial-python-pgvector\\\/#webpage\"},\"articleSection\":\"AI, Databases, Python, Embeddings, OpenAI API, pgvector, PostgreSQL, RAG, Vector Search\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/31\\\/rag-tutorial-python-pgvector\\\/#breadcrumblist\",\"itemListElement\":[{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#listItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/devdojo.co.in\\\/\",\"nextItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/backend\\\/#listItem\",\"name\":\"Backend\"}},{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/backend\\\/#listItem\",\"position\":2,\"name\":\"Backend\",\"item\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/backend\\\/\",\"nextItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/backend\\\/python\\\/#listItem\",\"name\":\"Python\"},\"previousItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#listItem\",\"name\":\"Home\"}},{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/backend\\\/python\\\/#listItem\",\"position\":3,\"name\":\"Python\",\"item\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/backend\\\/python\\\/\",\"nextItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/31\\\/rag-tutorial-python-pgvector\\\/#listItem\",\"name\":\"RAG Tutorial: Build a RAG App with Python and pgvector\"},\"previousItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/backend\\\/#listItem\",\"name\":\"Backend\"}},{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/31\\\/rag-tutorial-python-pgvector\\\/#listItem\",\"position\":4,\"name\":\"RAG Tutorial: Build a RAG App with Python and pgvector\",\"previousItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/backend\\\/python\\\/#listItem\",\"name\":\"Python\"}}]},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#organization\",\"name\":\"DevDojo\",\"description\":\"Empowering Developers\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/\"},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/author\\\/lokendra\\\/#author\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/author\\\/lokendra\\\/\",\"name\":\"Lokendra\",\"image\":{\"@type\":\"ImageObject\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/31\\\/rag-tutorial-python-pgvector\\\/#authorImage\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/99f102f0cc9080237ce54b91304fd1b3b6fa29925cbaeba93cd796872e58b45c?s=96&d=mm&r=g\",\"width\":96,\"height\":96,\"caption\":\"Lokendra\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/31\\\/rag-tutorial-python-pgvector\\\/#webpage\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/31\\\/rag-tutorial-python-pgvector\\\/\",\"name\":\"RAG Tutorial: Build a RAG App with Python & pgvector\",\"description\":\"Step-by-step RAG tutorial: build a Retrieval-Augmented Generation app with Python, PostgreSQL pgvector and OpenAI embeddings, with runnable code and fixes.\",\"inLanguage\":\"en-US\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#website\"},\"breadcrumb\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/31\\\/rag-tutorial-python-pgvector\\\/#breadcrumblist\"},\"author\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/author\\\/lokendra\\\/#author\"},\"creator\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/author\\\/lokendra\\\/#author\"},\"image\":{\"@type\":\"ImageObject\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/rag-tutorial-python-pgvector.jpg\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/31\\\/rag-tutorial-python-pgvector\\\/#mainImage\",\"width\":1200,\"height\":630,\"caption\":\"RAG tutorial banner: documents flow into pgvector, then an AI model returns a grounded answer\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/31\\\/rag-tutorial-python-pgvector\\\/#mainImage\"},\"datePublished\":\"2026-10-31T13:07:56+00:00\",\"dateModified\":\"2026-10-31T13:07:57+00:00\"},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#website\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/\",\"name\":\"DevDojo\",\"description\":\"Empowering Developers\",\"inLanguage\":\"en-US\",\"publisher\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#organization\"}}]}\n\t\t<\/script>\n\t\t<!-- All in One SEO -->\n\n","aioseo_head_json":{"title":"RAG Tutorial: Build a RAG App with Python & pgvector","description":"Step-by-step RAG tutorial: build a Retrieval-Augmented Generation app with Python, PostgreSQL pgvector and OpenAI embeddings, with runnable code and fixes.","canonical_url":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/31\/rag-tutorial-python-pgvector\/","robots":"max-image-preview:large","keywords":"","webmasterTools":{"miscellaneous":""},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"BlogPosting","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/31\/rag-tutorial-python-pgvector\/#blogposting","name":"RAG Tutorial: Build a RAG App with Python & pgvector","headline":"RAG Tutorial: Build a RAG App with Python and pgvector","author":{"@id":"https:\/\/devdojo.co.in\/index.php\/author\/lokendra\/#author"},"publisher":{"@id":"https:\/\/devdojo.co.in\/#organization"},"image":{"@type":"ImageObject","url":"https:\/\/devdojo.co.in\/wp-content\/uploads\/2026\/10\/rag-tutorial-python-pgvector.jpg","width":1200,"height":630,"caption":"RAG tutorial banner: documents flow into pgvector, then an AI model returns a grounded answer"},"datePublished":"2026-10-31T13:07:56+00:00","dateModified":"2026-10-31T13:07:57+00:00","inLanguage":"en-US","mainEntityOfPage":{"@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/31\/rag-tutorial-python-pgvector\/#webpage"},"isPartOf":{"@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/31\/rag-tutorial-python-pgvector\/#webpage"},"articleSection":"AI, Databases, Python, Embeddings, OpenAI API, pgvector, PostgreSQL, RAG, Vector Search"},{"@type":"BreadcrumbList","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/31\/rag-tutorial-python-pgvector\/#breadcrumblist","itemListElement":[{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/#listItem","position":1,"name":"Home","item":"https:\/\/devdojo.co.in\/","nextItem":{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/#listItem","name":"Backend"}},{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/#listItem","position":2,"name":"Backend","item":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/","nextItem":{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/python\/#listItem","name":"Python"},"previousItem":{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/#listItem","name":"Home"}},{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/python\/#listItem","position":3,"name":"Python","item":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/python\/","nextItem":{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/31\/rag-tutorial-python-pgvector\/#listItem","name":"RAG Tutorial: Build a RAG App with Python and pgvector"},"previousItem":{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/#listItem","name":"Backend"}},{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/31\/rag-tutorial-python-pgvector\/#listItem","position":4,"name":"RAG Tutorial: Build a RAG App with Python and pgvector","previousItem":{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/python\/#listItem","name":"Python"}}]},{"@type":"Organization","@id":"https:\/\/devdojo.co.in\/#organization","name":"DevDojo","description":"Empowering Developers","url":"https:\/\/devdojo.co.in\/"},{"@type":"Person","@id":"https:\/\/devdojo.co.in\/index.php\/author\/lokendra\/#author","url":"https:\/\/devdojo.co.in\/index.php\/author\/lokendra\/","name":"Lokendra","image":{"@type":"ImageObject","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/31\/rag-tutorial-python-pgvector\/#authorImage","url":"https:\/\/secure.gravatar.com\/avatar\/99f102f0cc9080237ce54b91304fd1b3b6fa29925cbaeba93cd796872e58b45c?s=96&d=mm&r=g","width":96,"height":96,"caption":"Lokendra"}},{"@type":"WebPage","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/31\/rag-tutorial-python-pgvector\/#webpage","url":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/31\/rag-tutorial-python-pgvector\/","name":"RAG Tutorial: Build a RAG App with Python & pgvector","description":"Step-by-step RAG tutorial: build a Retrieval-Augmented Generation app with Python, PostgreSQL pgvector and OpenAI embeddings, with runnable code and fixes.","inLanguage":"en-US","isPartOf":{"@id":"https:\/\/devdojo.co.in\/#website"},"breadcrumb":{"@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/31\/rag-tutorial-python-pgvector\/#breadcrumblist"},"author":{"@id":"https:\/\/devdojo.co.in\/index.php\/author\/lokendra\/#author"},"creator":{"@id":"https:\/\/devdojo.co.in\/index.php\/author\/lokendra\/#author"},"image":{"@type":"ImageObject","url":"https:\/\/devdojo.co.in\/wp-content\/uploads\/2026\/10\/rag-tutorial-python-pgvector.jpg","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/31\/rag-tutorial-python-pgvector\/#mainImage","width":1200,"height":630,"caption":"RAG tutorial banner: documents flow into pgvector, then an AI model returns a grounded answer"},"primaryImageOfPage":{"@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/31\/rag-tutorial-python-pgvector\/#mainImage"},"datePublished":"2026-10-31T13:07:56+00:00","dateModified":"2026-10-31T13:07:57+00:00"},{"@type":"WebSite","@id":"https:\/\/devdojo.co.in\/#website","url":"https:\/\/devdojo.co.in\/","name":"DevDojo","description":"Empowering Developers","inLanguage":"en-US","publisher":{"@id":"https:\/\/devdojo.co.in\/#organization"}}]},"og:locale":"en_US","og:site_name":"DevDojo - Empowering Developers","og:type":"article","og:title":"RAG Tutorial: Build a RAG App with Python &amp; pgvector","og:description":"Step-by-step RAG tutorial: build a Retrieval-Augmented Generation app with Python, PostgreSQL pgvector and OpenAI embeddings, with runnable code and fixes.","og:url":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/31\/rag-tutorial-python-pgvector\/","article:published_time":"2026-10-31T13:07:56+00:00","article:modified_time":"2026-10-31T13:07:57+00:00","twitter:card":"summary_large_image","twitter:title":"RAG Tutorial: Build a RAG App with Python &amp; pgvector","twitter:description":"Step-by-step RAG tutorial: build a Retrieval-Augmented Generation app with Python, PostgreSQL pgvector and OpenAI embeddings, with runnable code and fixes."},"aioseo_meta_data":{"post_id":"198","title":"RAG Tutorial: Build a RAG App with Python & pgvector","description":"Step-by-step RAG tutorial: build a Retrieval-Augmented Generation app with Python, PostgreSQL pgvector and OpenAI embeddings, with runnable code and fixes.","keywords":null,"keyphrases":{"focus":{"keyphrase":"RAG tutorial","score":75},"additional":[]},"primary_term":null,"canonical_url":null,"og_title":"RAG Tutorial: Build a RAG App with Python & pgvector","og_description":"Step-by-step RAG tutorial: build a Retrieval-Augmented Generation app with Python, PostgreSQL pgvector and OpenAI embeddings, with runnable code and fixes.","og_object_type":"default","og_image_type":"default","og_image_url":null,"og_image_width":null,"og_image_height":null,"og_image_custom_url":null,"og_image_custom_fields":null,"og_video":"","og_custom_url":null,"og_article_section":null,"og_article_tags":null,"twitter_use_og":false,"twitter_card":"default","twitter_image_type":"default","twitter_image_url":null,"twitter_image_custom_url":null,"twitter_image_custom_fields":null,"twitter_title":null,"twitter_description":null,"schema":{"blockGraphs":[],"customGraphs":[],"default":{"data":{"Article":[],"Course":[],"Dataset":[],"FAQPage":[],"Movie":[],"Person":[],"Product":[],"ProductReview":[],"Car":[],"Recipe":[],"Service":[],"SoftwareApplication":[],"WebPage":[]},"graphName":"BlogPosting","isEnabled":true},"graphs":[]},"schema_type":"default","schema_type_options":null,"pillar_content":false,"robots_default":true,"robots_noindex":false,"robots_noarchive":false,"robots_nosnippet":false,"robots_nofollow":false,"robots_noimageindex":false,"robots_noodp":false,"robots_notranslate":false,"robots_max_snippet":"-1","robots_max_videopreview":"-1","robots_max_imagepreview":"large","priority":0,"frequency":"default","local_seo":null,"breadcrumb_settings":null,"limit_modified_date":false,"ai":{"faqs":[],"keyPoints":[],"schemas":[],"titles":[],"descriptions":[],"socialPosts":{"email":{"subject":"","preview":"","content":""},"linkedin":[],"twitter":[],"facebook":[],"instagram":[]}},"created":"2026-10-13 12:57:59","updated":"2026-10-31 13:40:20","focus_keyword":"RAG tutorial","additional_keywords":null,"truseo_locale":null,"seo_analyzer_scan_date":null},"aioseo_breadcrumb":"<div class=\"aioseo-breadcrumbs\"><span class=\"aioseo-breadcrumb\">\n\t\t\t<a href=\"https:\/\/devdojo.co.in\/\" title=\"Home\">Home<\/a>\n\t\t<\/span><span class=\"aioseo-breadcrumb-separator\">\u00bb<\/span><span class=\"aioseo-breadcrumb\">\n\t\t\t<a href=\"https:\/\/devdojo.co.in\/index.php\/category\/backend\/\" title=\"Backend\">Backend<\/a>\n\t\t<\/span><span class=\"aioseo-breadcrumb-separator\">\u00bb<\/span><span class=\"aioseo-breadcrumb\">\n\t\t\t<a href=\"https:\/\/devdojo.co.in\/index.php\/category\/backend\/python\/\" title=\"Python\">Python<\/a>\n\t\t<\/span><span class=\"aioseo-breadcrumb-separator\">\u00bb<\/span><span class=\"aioseo-breadcrumb\">\n\t\t\tRAG Tutorial: Build a RAG App with Python and pgvector\n\t\t<\/span><\/div>","aioseo_breadcrumb_json":[{"label":"Home","link":"https:\/\/devdojo.co.in\/"},{"label":"Backend","link":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/"},{"label":"Python","link":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/python\/"},{"label":"RAG Tutorial: Build a RAG App with Python and pgvector","link":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/31\/rag-tutorial-python-pgvector\/"}],"_links":{"self":[{"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/posts\/198","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/comments?post=198"}],"version-history":[{"count":1,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/posts\/198\/revisions"}],"predecessor-version":[{"id":279,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/posts\/198\/revisions\/279"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/media\/197"}],"wp:attachment":[{"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/media?parent=198"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/categories?post=198"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/tags?post=198"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}