RAG: Pinecone, LangChain, OpenAI
Imports:
import * as dotenv from "dotenv";
import { Pinecone } from '@pinecone-database/pinecone';
import { DirectoryLoader } from "langchain/document_loaders/fs/directory";
import { PDFLoader } from "@langchain/community/document_loaders/fs/pdf";
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
import { OpenAIEmbeddings } from "@langchain/openai";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
1. Initialize Environment & Dependencies
Why? To load configuration and set up necessary APIs (Pinecone, LangChain, OpenAI).
- Load environment variables (
dotenv.config()). - Initialize Pinecone client with API key.
import * as dotenv from "dotenv";
import { Pinecone } from '@pinecone-database/pinecone';
dotenv.config();
const pc = new Pinecone({
apiKey: process.env.PINECONE_API_KEY,
});
const index = pc.index("mypineconeindex");
2. Check/Create Pinecone Index
Why? Pinecone requires an index to store and retrieve embeddings.
- List existing indexes to check if
"mypineconeindex"exists. - If not found, create a new index (dimension 1536 for OpenAI embeddings,
cosinesimilarity for retrieval).
const existingIndexs = await pc.listIndexes();
console.log(existingIndexs)
if (!existingIndexs.indexes?.find(ele => ele.name === 'mypineconeindex')) {
const createPineconeIndex = await pc.createIndex({
name: "mypineconeindex",
dimension: 1536,
metric: 'cosine',
spec: {
serverless: {
cloud: 'aws',
region: 'us-east-1'
}
}
});
console.log("Created pinecone index", createPineconeIndex);
//it takes a while to complete index creation.
You could optionally wait for 1min here
} else {
console.log(`Index with name 'mypineconeindex' already exists`)
}
3. Load Documents & Split into Chunks
Why split text into 1000-character chunks?
- Embeddings have token limits (e.g., OpenAI models have max token constraints).
- Better retrieval: Large text blocks reduce precision when searching for context.
- Efficient storage: Smaller chunks allow more accurate embeddings.
- PDFs are loaded from a directory (i have 2 PDFs inside "documents" folder)
- Split text into 1000-character chunks using
RecursiveCharacterTextSplitter.
const directoryLoader = new DirectoryLoader("./documents", {
".pdf": (path) => new PDFLoader(path),
});
const docs = await directoryLoader.load();
const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 1000 });
for (let doc of docs) {
const chunks = await textSplitter.createDocuments([doc.pageContent]);
const cleanChunk = chunks.map((chunk) => chunk.pageContent.replace(/\n/g, " "));
4. Generate & Store Embeddings
Why convert text to embeddings?
- Pinecone stores vector representations of text, not raw text.
- Embeddings enable semantic search instead of keyword-based search.
- Remove newlines (
\n) to improve embedding quality. - Convert each chunk into vector embeddings with
OpenAIEmbeddings. - Format data as Pinecone expects (
id,values,metadata). - Upsert embeddings into Pinecone in batches of 200 (prevents API rate limits).
- Checkout: langchain
let embeddingArray = await new OpenAIEmbeddings().embedDocuments(cleanChunk);
const upsertValues = embeddingArray.map((item, i) => ({
id: `doc-${doc.id || Date.now()}-${i}`,
values: item,
metadata: { text: cleanChunk[i], source: "pdf", pageIndex: i }
}));
const batchSize = 200;
for (let i = 0; i < upsertValues.length; i += batchSize) {
const batch = upsertValues.slice(i, i + batchSize);
console.log("Upserting batch", batch, i)
await index.upsert(batch);
}
5. Query Pinecone for Relevant Documents
Why retrieve similar vectors?
- Semantic search: Finds the most relevant context for answering queries.
- Efficient retrieval: Instead of searching raw text, we retrieve by vector similarity.
- Convert query into an embedding.
- Retrieve top 10 most relevant chunks.
let question = "what are the skills of kavyashree?";
const queryEmbedding = await new OpenAIEmbeddings().embedQuery(question);
onst queryResponse = await index.query({
vector: queryEmbedding,
topK: 10, // return top 10 similar vectors
includeMetadata: true, // include metadata so you can see additional info
includeValues: false // optionally include the vector values if needed
});
console.log("Query results:", queryResponse);
const context = queryResponse.matches
.map(match => match.metadata.text || "")
.join("\n");
6. Generate Response with LLM
Why use an LLM after retrieving relevant text?
- The query result contains only vector metadata, not readable text.
- OpenAI LLM converts the retrieved context into a coherent answer.
- Use
ChatOpenAIwith a structured prompt template.
const llm = new ChatOpenAI();
const promptTemplate = `You are an assistant that summarizes documents.
Context: {context}
Question: {question}
Answer:`;
const prompt = ChatPromptTemplate.fromTemplate(promptTemplate);
const chain = prompt.pipe(llm);
const humanReadableResult = await chain.invoke({context, question});
console.log("Answer:", humanReadableResult);
SUMMARY
Table of Key Modules & APIs
Load Environment & Initialize Client
- Load .env Variables
- Loads configuration variables (like the Pinecone API key) using dotenv.config().
- Creates a Pinecone client instance with your API key.
- Index Creation/Selection
Check Existing Indexes:
- Uses pc.listIndexes() to see if "mypineconeindex" exists.
- Create Index (if needed):
- If missing, creates it with a specified dimension (1536), cosine metric, and serverless specs.
- Document Loading & Preprocessing
Load PDFs:
Loads documents from a specified folder using DirectoryLoader in conjunction with PDFLoader.
Text Splitting:
Splits each document’s text into smaller chunks (~1000 characters) using RecursiveCharacterTextSplitter.
Clean Text:
Replaces newline characters in each chunk with spaces to improve embedding quality.
Embedding Generation & Upsert to Pinecone
Generate Embeddings:
- Converts each cleaned chunk into a vector using OpenAIEmbeddings().embedDocuments.
- Create Upsert Objects:
- For every embedding, creates an object with a unique ID, the vector values, and metadata (original chunk text, source, and chunk index).
- Batches the upsert objects (e.g., in groups of 200) and stores them into the Pinecone index using index.upsert().
- Querying & LLM Summarization
Embed the Query:
Converts the natural language question into a query embedding via OpenAIEmbeddings().embedQuery.
Query Pinecone:
Uses the query embedding to search the index for the top similar vectors.
Extract Context:
Gathers text from the metadata of retrieved matches to form a context string.
Build LLM Chain:
- Creates a prompt template using ChatPromptTemplate.fromTemplate that includes the {context} and {question} placeholders.
- Pipes the prompt to an instance of ChatOpenAI to create a chain.
Generate Answer:
Invokes the chain with the context and question, producing a human‑readable answer.
Output:
Logs the answer for display.
Comments
Post a Comment