RAG: Pinecone, LangChain, OpenAI

GITHUB

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, cosine similarity 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 ChatOpenAI with 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);


Output:



SUMMARY

Table of Key Modules & APIs









Load Environment & Initialize Client
  1. Load .env Variables
  2. Loads configuration variables (like the Pinecone API key) using dotenv.config().
Initialize Pinecone Client:
  1. Creates a Pinecone client instance with your API key.
  2. Index Creation/Selection
Check Existing Indexes:
  1. Uses pc.listIndexes() to see if "mypineconeindex" exists.
  2. Create Index (if needed):
  3. If missing, creates it with a specified dimension (1536), cosine metric, and serverless specs.
  4. 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:
  1. Converts each cleaned chunk into a vector using OpenAIEmbeddings().embedDocuments.
  2. Create Upsert Objects:
  3. For every embedding, creates an object with a unique ID, the vector values, and metadata (original chunk text, source, and chunk index).
Batch Upsert:
  1. Batches the upsert objects (e.g., in groups of 200) and stores them into the Pinecone index using index.upsert().
  2. 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:
  1. Creates a prompt template using ChatPromptTemplate.fromTemplate that includes the {context} and {question} placeholders.
  2. 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