Replace in-memory storage with a real production database that persists forever
Why Pinecone — The Problem With What We Have Now
Your RAG system currently uses MemoryVectorStore. It stores all data in RAM.
Current problems:
→ Restart the server → all data is lost
→ 10 users load different PDFs → memory gets full
→ Deploy to production → every restart requires re-indexing everything
→ 1 million document chunks → RAM overflow
Pinecone solution:
→ Data is stored permanently in the cloud
→ Restart the server → data remains available
→ Billions of vectors → search in milliseconds
→ Free tier available → perfect for development
→ Web dashboard → view your data visually (similar to MongoDB Atlas)
What is Pinecone?
Pinecone is a managed cloud vector database. Your workflow is simple:
- Create a Pinecone account (free)
- Create an index (similar to a table in SQL)
- Upsert vectors (store embeddings)
- Query vectors (search)
What Pinecone does:
→ Stores your data on AWS, GCP, or Azure
→ Automatically manages HNSW indexing
→ Performs millisecond searches across billions of vectors
→ Provides a web dashboard to view and manage your data
Step 1 — Pinecone Account Setup
1. Go to: https://app.pinecone.io
2. Sign up — free account, no credit card needed
3. Get API Key:
Database → API Keys → Create API Key
Copy the key → looks like: "pcsk_abc123..."
4. Index banao:
Database → Indexes → Create Index
Name: pdf-chatbot
Dimensions: 1536 ← must match text-embedding-3-small
Metric: cosine ← best for text
Type: Serverless ← free, auto-scales
Cloud: AWS
Region: us-east-1
Click Create Index — done.
Step 2 — Install Packages
In langchain-production-rag folder:
npm install @pinecone-database/pinecone @langchain/pinecone
@pinecone-database/pinecone → official Pinecone Node.js SDK v8.x
@langchain/pinecone → LangChain's Pinecone integration provides PineconeStore class
{ "name": "langchain-production-rag", "version": "1.0.0", "type": "module", "main": "index.js", "scripts": { "start": "node src/rag.js", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "description": "", "dependencies": { "@langchain/community": "^1.1.29", "@langchain/core": "^1.2.3", "@langchain/langgraph": "^1.4.8", "@langchain/openai": "^1.5.5", "@langchain/pinecone": "^1.0.3", "@langchain/textsplitters": "^1.0.1", "@pinecone-database/pinecone": "^8.1.0", "cors": "^2.8.6", "dotenv": "^16.4.5", "express": "^5.2.1", "langchain": "^1.5.3", "pdf-parse": "^2.4.5" } }
Step 3 — Update .env
OPENAI_API_KEY=sk-proj-your-openai-key
PINECONE_API_KEY=pcsk_your-pinecone-key
PINECONE_INDEX=pdf-chatbot
Step 4 — Create src/pineconeStore.js
This file replaces MemoryVectorStore with PineconeStore:
// ═══════════════════════════════════════════════════════════════════════════ // IMPORTS // ═══════════════════════════════════════════════════════════════════════════
import { Pinecone } from "@pinecone-database/pinecone"; // Official Pinecone SDK (v8.x) // gives us a client to connect to, write to, and query a Pinecone vector database
import { OpenAIEmbeddings } from "@langchain/openai"; // OpenAIEmbeddings = converts text → a 1536-number vector // used both to embed the PDF chunks (once) and to embed the user's query (every search)
import { Document } from "@langchain/core/documents"; // Document = LangChain's standard document class // shape: { pageContent: "text...", metadata: { page: 0, source: "file.pdf" } } // keeping search results in this shape means the rest of the app (createRAGAgent, // searchTool in api.js) doesn't need to know or care that Pinecone is being used
import * as dotenv from "dotenv"; dotenv.config(); // loads PINECONE_API_KEY and PINECONE_INDEX (and other secrets) from the .env file // into process.env, so they can be read below
// ═══════════════════════════════════════════════════════════════════════════ // SETUP // ═══════════════════════════════════════════════════════════════════════════
const pineconeClient = new Pinecone({ apiKey: process.env.PINECONE_API_KEY, // reads the API key from .env // example value: "pcsk_abc123..." }); // pineconeClient = an authenticated connection object used to talk to Pinecone's cloud API
const embeddings = new OpenAIEmbeddings({ model: "text-embedding-3-small", // this model produces 1536-dimensional vectors // IMPORTANT: this must match the dimension configured when the Pinecone index was created, // otherwise upserts/queries will fail });
// ═══════════════════════════════════════════════════════════════════════════ // HELPER — Get Pinecone Index reference // Conceptually like opening a connection to one specific table in a database // ═══════════════════════════════════════════════════════════════════════════
function getPineconeIndex() { const indexName = process.env.PINECONE_INDEX; // example value: "pdf-chatbot"
if (!indexName) { throw new Error("PINECONE_INDEX is not set in .env file"); }
return pineconeClient.index(indexName); // returns an Index object pointing at the "pdf-chatbot" index // this does NOT create the index — it must already exist in the Pinecone dashboard // // example return value (internal object): // Index { name: "pdf-chatbot", host: "pdf-chatbot-xxx.svc.pinecone.io" } }
// ═══════════════════════════════════════════════════════════════════════════ // HELPER — Build a retriever object for a given namespace // Both buildPineconeVectorStore() and loadExistingVectorStore() need the exact // same "search this namespace" behavior, so it lives here once and gets reused. // ═══════════════════════════════════════════════════════════════════════════
function createRetriever(pineconeIndex, namespace) { // pineconeIndex = the Index object returned by getPineconeIndex() // namespace = which namespace inside that index to search within
return { namespace, // exposed so callers can check/log which namespace this retriever is bound to
invoke: async (query, k = 5) => { // invoke(query, k) = search for the top k most similar chunks to `query` // query = the search text (usually the user's question) // k = how many results to return (defaults to 5) // // example call: retriever.invoke("what are the pension benefits?", 5)
const queryVector = await embeddings.embedQuery(query); // embeds the query text using the SAME model used for the stored documents // this MUST match — otherwise the vectors are not comparable to each other // example queryVector: [0.19, -0.82, 0.38, ...] (1536 numbers total)
const results = await pineconeIndex.namespace(namespace || "").query({ vector: queryVector, // the query vector to compare against every stored vector
topK: k, // return only the top k most similar vectors
includeMetadata: true, // include the metadata we stored (text, source, page) in the results // without this flag, Pinecone only returns ids + similarity scores }); // results.matches = array of matching vectors, sorted by similarity score (highest first) // example results.matches: // [ // { // id: "sample-chunk-2", // score: 0.89, // cosine similarity — 1.0 = identical meaning // metadata: { // text: "APY gives guaranteed pension...", // source: "./sample.pdf", // page: 0 // } // }, // { id: "sample-chunk-7", score: 0.81, metadata: {...} }, // ... // ]
return results.matches.map(match => new Document({ pageContent: match.metadata?.text || "", // the original chunk text, pulled back out of the stored metadata
metadata: { source: match.metadata?.source || "", page: match.metadata?.page || 0, score: match.score, // similarity score for this match — higher means more relevant // example: 0.89 (very relevant), 0.45 (weakly relevant) }, })); // returns an array of Document objects — same shape LangChain retrievers normally return // this is what lets createRAGAgent()'s searchTool work without any changes }, }; // example returned retriever object: // { // namespace: "sample", // invoke: async (query, k) => Document[] // } }
// ═══════════════════════════════════════════════════════════════════════════ // FUNCTION: buildPineconeVectorStore // Step 1: clear out any old vectors for this namespace // Step 2: embed all chunks using OpenAI // Step 3: upsert (insert/update) those vectors into Pinecone // Step 4: return a simple retriever interface for searching them // ═══════════════════════════════════════════════════════════════════════════
export async function buildPineconeVectorStore(chunks, namespace) { // chunks = array of LangChain Document objects produced by the text splitter // example: // [ // Document { pageContent: "APY gives pension...", metadata: { page: 0, source: "./sample.pdf" } }, // Document { pageContent: "subscriber gets Rs.1000...", metadata: { page: 1 } }, // ] // // namespace = string used to keep this PDF's vectors separate from other PDFs // example: "sample" for sample.pdf // all vectors for this PDF are written into this namespace only // vectors belonging to other namespaces are never touched
if (!chunks || chunks.length === 0) { throw new Error("No chunks provided to store in Pinecone"); }
console.log(`\n💾 Storing ${chunks.length} chunks in Pinecone...`); console.log(` Index: ${process.env.PINECONE_INDEX}`); console.log(` Namespace: ${namespace || "default"}`); console.log(` Embedding with OpenAI text-embedding-3-small...`);
const pineconeIndex = getPineconeIndex(); // get a reference to the "pdf-chatbot" index
// ── STEP 1: Clear existing vectors in this namespace ──────────────────── try { await pineconeIndex.namespace(namespace || "").deleteAll(); // deletes ALL existing vectors currently in this namespace // this stops old PDF data from mixing together with newly uploaded PDF data // vectors in OTHER namespaces are completely unaffected console.log(` Cleared existing vectors in namespace "${namespace}"`); } catch (err) { // this throws if the namespace is already empty / doesn't exist yet — that's fine, just continue console.log(` Namespace was empty or doesn't exist yet — continuing`); }
// ── STEP 2: Embed all chunks using OpenAI ─────────────────────────────── const texts = chunks.map(chunk => chunk.pageContent); // pulls out just the plain text from each Document object // example texts: // [ // "APY gives guaranteed pension of Rs.1000-5000...", // "Subscriber must be between 18-40 years of age...", // ...12 more strings // ]
const vectors = await embeddings.embedDocuments(texts); // embedDocuments() batch-embeds all texts in one (or a few) API calls // returns an array of vectors, one per input text, in the same order // example return value: // [ // [0.023, -0.089, 0.041, ...], // 1536 numbers for chunk 0 // [0.071, 0.042, -0.013, ...], // 1536 numbers for chunk 1 // ... // ] console.log(` Got ${vectors.length} embeddings from OpenAI`);
// ── STEP 3: Build records to upsert into Pinecone ─────────────────────── const records = chunks.map((chunk, i) => ({ id: `${namespace || "doc"}-chunk-${i}`, // unique ID for this vector inside Pinecone // example: "sample-chunk-0", "sample-chunk-1", "sample-chunk-2" // must be unique within the namespace
values: vectors[i], // the 1536-number vector generated for this exact chunk // example: [0.023, -0.089, 0.041, ...]
metadata: { text: chunk.pageContent, // stores the original chunk text so it can be retrieved again after a search // Pinecone stores this alongside the vector itself // example: "APY gives guaranteed pension of Rs.1000-5000..."
source: chunk.metadata.source || "", // which file this chunk originally came from // example: "./sample.pdf"
page: chunk.metadata.page ?? 0, // which page number in the PDF this chunk came from // example: 0, 1, 2, ...
chunkIndex: i, // the position of this chunk within the overall sequence // example: 0, 1, 2, ..., 11 }, })); // records = array of plain objects ready to be sent to Pinecone // example single record: // { // id: "sample-chunk-0", // values: [0.023, -0.089, ...], // 1536 numbers // metadata: { // text: "APY gives guaranteed pension...", // source: "./sample.pdf", // page: 0, // chunkIndex: 0 // } // }
// ── STEP 4: Upsert records in batches ─────────────────────────────────── const BATCH_SIZE = 100; // upserts 100 records at a time // Pinecone recommends batch sizes of roughly 100-200 for best performance // this also avoids "payload too large" errors on big documents with many chunks
for (let i = 0; i < records.length; i += BATCH_SIZE) { const batch = records.slice(i, i + BATCH_SIZE); // takes the next 100 records (or fewer, for the final leftover batch) // example: records 0-99, then 100-199, then remaining ones, etc.
await pineconeIndex.namespace(namespace || "").upsert({ records: batch }); // .namespace(namespace) = targets this specific namespace inside the index // .upsert(batch) = inserts new vectors or updates existing ones with the same id // // "upsert" = "update if it exists, insert if it doesn't" // if a vector with the same id already exists → it gets overwritten // if it doesn't exist yet → it gets inserted fresh
console.log(` Upserted batch ${Math.floor(i / BATCH_SIZE) + 1}: records ${i} to ${Math.min(i + BATCH_SIZE, records.length) - 1}`); }
console.log(` ✅ All ${records.length} chunks stored in Pinecone!\n`);
// ── STEP 5: Return a simple retriever interface ───────────────────────── // we build our own retriever object (instead of using LangChain's PineconeStore wrapper) // so the exact same query/response shape is used everywhere in the app return createRetriever(pineconeIndex, namespace); // example returned retriever object: // { // namespace: "sample", // invoke: async (query, k) => Document[] // } }
// ═══════════════════════════════════════════════════════════════════════════ // FUNCTION: loadExistingVectorStore // Reconnects to data that was already indexed into Pinecone in a previous run. // Use this after a server restart — no re-embedding or re-uploading needed. // ═══════════════════════════════════════════════════════════════════════════
export async function loadExistingVectorStore(namespace) { // namespace = which namespace to reconnect to // example: "sample" // // This is the key advantage of using Pinecone over an in-memory store: // in-memory store → server restarts → data is gone → PDF must be re-indexed // Pinecone store → server restarts → data is still safe in the cloud → just reconnect
console.log(`\n🔄 Reconnecting to Pinecone namespace: "${namespace}"`);
const pineconeIndex = getPineconeIndex(); // get a reference to the index — the data is already sitting there in the cloud
const stats = await pineconeIndex.namespace(namespace || "").describeIndexStats(); // checks how many vectors currently exist in this namespace // example stats: { namespaces: { "sample": { recordCount: 12 } }, dimension: 1536, totalRecordCount: 12 }
console.log(` Found vectors in Pinecone — reconnected\n`);
// returns the exact same retriever interface used by buildPineconeVectorStore() return createRetriever(pineconeIndex, namespace); }
// ═══════════════════════════════════════════════════════════════════════════ // FUNCTION: getIndexStats // Shows how many vectors currently exist in Pinecone, broken down by namespace. // Conceptually similar to: SELECT COUNT(*) ... GROUP BY namespace in SQL // ═══════════════════════════════════════════════════════════════════════════
export async function getIndexStats() { const pineconeIndex = getPineconeIndex();
const stats = await pineconeIndex.describeIndexStats(); // calls the Pinecone API to get index-level statistics // // example return value: // { // namespaces: { // "sample": { recordCount: 12 }, // "my-document": { recordCount: 25 } // }, // dimension: 1536, // indexFullness: 0.0, // totalRecordCount: 37 // }
return { totalVectors: stats.totalRecordCount, // total number of vectors across every namespace combined // example: 37
namespaces: stats.namespaces, // per-namespace breakdown // example: { "sample": { recordCount: 12 } }
dimension: stats.dimension, // the vector dimension used by this index — should be 1536 for text-embedding-3-small }; }
Step 5 — Update src/api.js
Step 6 — Run and Test
node src/api.js
Load a PDF:
curl -X POST http://localhost:3001/api/load-pdf \
-H "Content-Type: application/json" \
-d '{"pdfPath": "./sample.pdf"}'
Check Pinecone stats:
curl http://localhost:3001/api/stats
Expected output:
{
"success": true,
"pineconeIndex": "pdf-chatbot",
"totalVectors": 25,
"namespaces": {
"sample": { "recordCount": 25 }
},
"dimension": 1536
}
Now restart the server — ask a question — data still works!
# Stop server (Ctrl+C)
# Restart
node src/api.js
frontend file changes
update usePDFLoader.js filePinecone Dashboard — Visual View
Pinecone's dashboard shows many of the same things that MongoDB Atlas does.
app.pinecone.io → your project → Indexes → pdf-chatbot
┌─────────────────────────────────────────────────────┐
│ Index: pdf-chatbot │
│ Dimension: 1536 │ Metric: cosine │ Serverless │
├─────────────────────────────────────────────────────┤
│ Total Vectors: 25 │
│ │
│ Namespaces: │
│ → sample 25 vectors │
│ → my-document 12 vectors │
├─────────────────────────────────────────────────────┤
│ [Browse Vectors] [Query Console] [Metrics] │
└─────────────────────────────────────────────────────┘
Browse Vectors → View each vector's ID, metadata, and vector values.
MemoryVectorStore vs PineconeStore
Feature
MemoryVectorStore
PineconeStore
Storage
RAM
(temporary)
Cloud
(permanent)
Server
restart
Data gone ❌
Data safe ✅
Max
vectors
~50K (RAM
limited)
Billions ✅
Cost
Free
Free tier
available
Setup
Zero setup
Account +
index required
Speed
Fast (local)
~50–100 ms
(network)
Dashboard
None ❌
Full web UI ✅
Production
ready
No ❌
Yes ✅
Code
change needed
N/A
Just swap the
import ✅
3-Line Summary
- Pinecone is a managed cloud vector database — vectors stored permanently in the cloud so data survives server restarts, scales to billions of vectors, and comes with a web dashboard to visually browse your stored data.
- The code change is minimal — replace
MemoryVectorStore.fromDocuments()withPineconeStore.fromDocuments()and add a namespace parameter — everything else (retriever, agent, chat routes) stays exactly the same. - Namespaces in Pinecone work like folders — different PDFs go into different namespaces within the same index so their vectors never mix, and you can delete one PDF's data without affecting others.
Module 7.4 — Complete ✅
Phase 7 — Complete 🎉
✅ Module 7.1 — Production RAG System with LangChain
✅ Module 7.2 — Streaming, Error Handling & Express API
✅ Module 7.3 — Next.js Frontend
✅ Module 7.4 — Pinecone Cloud Vector Database
Coming up — Phase 8: AI Agents
Advanced agent concepts including the ReAct pattern, multi-agent systems, agent memory, and real-world projects such as a Resume Analyzer and a Research Agent.
No comments:
Post a Comment