{"componentChunkName":"component---src-gatsby-theme-chronoblog-templates-note-js","path":"/notes/rag-openai-embeddings-pgvector-langchain/","result":{"data":{"mdx":{"parent":{"__typename":"File","fields":{"gitLogLatestDate":"2026-06-02 14:49:11 +0200"}},"id":"fa98b725-151f-5759-93b4-ed6b839046cc","excerpt":"Retrieval-Augmented Generation (RAG) is a practical pattern: store knowledge as embeddings, retrieve the most relevant chunks with semantic…","frontmatter":{"title":"RAG with OpenAI Embeddings, pgvector and LangChain","date":"2026-06-02 12:00:00 UTC","job_ad":null,"job_ad_id":null,"job_ad_url":null,"tags":["openai","rag","embeddings","postgres","langchain","node"],"cover":null},"fields":{"slug":"/notes/rag-openai-embeddings-pgvector-langchain/","readingTime":{"text":"4 min read"}},"body":"function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }\n\nfunction _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }\n\nfunction _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }\n\n/* @jsxRuntime classic */\n\n/* @jsx mdx */\nvar _frontmatter = {\n  \"title\": \"RAG with OpenAI Embeddings, pgvector and LangChain\",\n  \"date\": \"2026-06-02 12:00:00 UTC\",\n  \"tags\": [\"openai\", \"rag\", \"embeddings\", \"postgres\", \"langchain\", \"node\"],\n  \"canonical_url\": \"https://sevic.dev/notes/rag-openai-embeddings-pgvector-langchain/\"\n};\nvar layoutProps = {\n  _frontmatter: _frontmatter\n};\nvar MDXLayout = \"wrapper\";\nreturn function MDXContent(_ref) {\n  var components = _ref.components,\n      props = _objectWithoutProperties(_ref, [\"components\"]);\n\n  return mdx(MDXLayout, _extends({}, layoutProps, props, {\n    components: components,\n    mdxType: \"MDXLayout\"\n  }), mdx(\"p\", null, \"Retrieval-Augmented Generation (RAG) is a practical pattern: store knowledge as embeddings, retrieve the most relevant chunks with semantic search, then generate an answer grounded in that context.\"), mdx(\"p\", null, \"This guide shows a simple end-to-end flow with OpenAI embeddings, PostgreSQL + pgvector, and LangChain chunking.\"), mdx(\"h3\", {\n    \"id\": \"architecture-at-a-glance\"\n  }, \"Architecture at a glance\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-mermaid\"\n  }), \"sequenceDiagram\\n  participant U as User\\n  participant I as Ingestion\\n  participant E as OpenAI Embeddings\\n  participant V as pgvector (PostgreSQL)\\n  participant R as RAG App\\n  participant L as OpenAI Model\\n\\n  I->>I: Load documents\\n  I->>I: Chunk with LangChain\\n  I->>E: Embed chunks (batch)\\n  E-->>I: Chunk vectors\\n  I->>V: Store vectors + content\\n\\n  U->>R: Ask question\\n  R->>E: Embed question (single)\\n  E-->>R: Query vector\\n  R->>V: Semantic search (top-k)\\n  V-->>R: Relevant chunks\\n  R->>L: Send question + context\\n  L-->>R: Grounded answer\\n  R-->>U: Final response\\n\")), mdx(\"h3\", {\n    \"id\": \"prerequisites\"\n  }, \"Prerequisites\"), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"OpenAI account\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Generated API key\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Enabled billing\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Node.js version 26\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"PostgreSQL with pgvector extension enabled\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"npm packages: \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"openai\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"langchain\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"pg\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"pgvector\"))), mdx(\"h3\", {\n    \"id\": \"what-are-embeddings\"\n  }, \"What are embeddings?\"), mdx(\"p\", null, \"Embeddings are numeric vectors that represent the semantic meaning of text. Similar text should produce vectors that are close in vector space.\"), mdx(\"p\", null, \"In practice:\"), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Convert document chunks to vectors and store them in pgvector\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Convert a user question to a vector\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Run a nearest-neighbor search to find the most relevant chunks\")), mdx(\"h3\", {\n    \"id\": \"openai-client-setup\"\n  }, \"OpenAI client setup\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"import OpenAI from 'openai';\\n\\nconst client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\\n\")), mdx(\"h3\", {\n    \"id\": \"embedding-one-input-element\"\n  }, \"Embedding one input element\"), mdx(\"p\", null, \"Use a single string when embedding a user query.\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"const response = await client.embeddings.create({\\n  model: 'text-embedding-3-small',\\n  input: 'How do I connect pgvector to PostgreSQL?',\\n});\\n\\nconst queryEmbedding = response.data[0].embedding;\\nconsole.log(queryEmbedding.length);\\n\")), mdx(\"h3\", {\n    \"id\": \"embedding-multiple-input-elements\"\n  }, \"Embedding multiple input elements\"), mdx(\"p\", null, \"Use an array to embed multiple chunks in one API call.\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"const chunks = [\\n  'pgvector adds vector similarity search to PostgreSQL.',\\n  'LangChain helps split long documents into retrieval-friendly chunks.',\\n  'RAG retrieves context first, then asks an LLM to answer.',\\n];\\n\\nconst response = await client.embeddings.create({\\n  model: 'text-embedding-3-small',\\n  input: chunks,\\n});\\n\\nconst rows = response.data.map((item, index) => ({\\n  text: chunks[index],\\n  embedding: item.embedding,\\n}));\\n\\nconsole.log(rows.length); // 3\\n\")), mdx(\"h3\", {\n    \"id\": \"chunking-documents-with-langchain\"\n  }, \"Chunking documents with LangChain\"), mdx(\"p\", null, \"Chunking makes retrieval more precise. Instead of embedding one large document, split it into smaller overlapping parts.\\nStart with \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"chunkSize: 800\"), \" and \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"chunkOverlap: 120\"), \", then adjust based on your document style and answer quality.\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';\\n\\nconst splitter = new RecursiveCharacterTextSplitter({\\n  chunkSize: 800,\\n  chunkOverlap: 120,\\n});\\n\\nconst docs = await splitter.createDocuments([\\n  `RAG combines retrieval and generation. Store chunks as vectors and fetch similar chunks at query time.`,\\n]);\\n\\nconsole.log(docs.map((doc) => doc.pageContent));\\n\")), mdx(\"h3\", {\n    \"id\": \"store-embeddings-in-pgvector\"\n  }, \"Store embeddings in pgvector\"), mdx(\"p\", null, \"Create a table with a vector column. \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"text-embedding-3-small\"), \" outputs 1536 dimensions.\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-sql\"\n  }), \"CREATE EXTENSION IF NOT EXISTS vector;\\n\\nCREATE TABLE IF NOT EXISTS rag_chunks (\\n  id BIGSERIAL PRIMARY KEY,\\n  content TEXT NOT NULL,\\n  embedding VECTOR(1536) NOT NULL,\\n  source TEXT,\\n  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\\n);\\n\")), mdx(\"p\", null, \"Insert chunk vectors from Node.js:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"import pg from 'pg';\\nimport pgvector from 'pgvector/pg';\\n\\nconst pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });\\nawait pgvector.registerTypes(pool);\\n\\nawait pool.query(\\n  `INSERT INTO rag_chunks (content, embedding, source)\\n   VALUES ($1, $2, $3)`,\\n  ['Chunk content', pgvector.toSql(queryEmbedding), 'notes.md']\\n);\\n\")), mdx(\"h3\", {\n    \"id\": \"semantic-search-in-pgvector\"\n  }, \"Semantic search in pgvector\"), mdx(\"p\", null, \"Embed the user question, then retrieve nearest chunks using cosine distance.\\nLower \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"distance\"), \" means a closer semantic match.\\n\", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"top-k\"), \" means how many nearest chunks you return (in this query, \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"k=4\"), \" with \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"LIMIT 4\"), \").\\nYou can also use a simple threshold (for example \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"0.4\"), \") to discard weak matches.\\nAs a starting point, many setups work well in the \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"0.35\"), \" to \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"0.45\"), \" range for cosine distance, then tune with real questions from your domain.\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"const searchResult = await pool.query(\\n  `SELECT id, content, source, embedding <=> $1::vector AS distance\\n   FROM rag_chunks\\n   ORDER BY embedding <=> $1::vector\\n   LIMIT 4`,\\n  [pgvector.toSql(queryEmbedding)]\\n);\\n\\nconst contextChunks = searchResult.rows.map((row) => row.content);\\n\")), mdx(\"p\", null, \"Threshold filtering example:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"const DISTANCE_THRESHOLD = 0.4;\\nconst filteredChunks = searchResult.rows\\n  .filter((row) => Number(row.distance) <= DISTANCE_THRESHOLD)\\n  .map((row) => row.content);\\n\")), mdx(\"p\", null, \"If no chunks pass the threshold, skip answer generation and return a fallback message:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"if (filteredChunks.length === 0) {\\n  console.log('I do not have enough context to answer this.');\\n  process.exit(0);\\n}\\n\")), mdx(\"h3\", {\n    \"id\": \"generate-answer-from-retrieved-context\"\n  }, \"Generate answer from retrieved context\"), mdx(\"p\", null, \"Use retrieved chunks as grounded context for the final model call.\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"const context = contextChunks.join('\\\\n\\\\n---\\\\n\\\\n');\\n\\nconst answer = await client.responses.create({\\n  model: 'gpt-5.5',\\n  instructions:\\n    'Answer only from the provided context. If context is insufficient, respond with: I do not have enough context to answer this.',\\n  input: `Context:\\\\n${context}\\\\n\\\\nQuestion: How does pgvector semantic search work?`,\\n});\\n\\nconsole.log(answer.output_text);\\n\")), mdx(\"h3\", {\n    \"id\": \"demo\"\n  }, \"Demo\"), mdx(\"p\", null, \"Runnable scripts for this post live in the \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"rag-openai-embeddings-pgvector-demo\"), \" folder in the private demos repository. Get access via \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://sevic.dev/demos\"\n  }), \"code demos\"), \".\"));\n}\n;\nMDXContent.isMDXComponent = true;"}},"pageContext":{"id":"fa98b725-151f-5759-93b4-ed6b839046cc"}},"staticQueryHashes":["1961101537","2542493696"]}