I’m working on creating a chatbot that can answer questions based on documents that users upload. The flow works like this: users upload files (PDF, text files, markdown), then my app breaks these files into smaller pieces and turns them into vectors using OpenAI embeddings. These vectors get stored in Pinecone database.
When someone asks a question, the bot searches for the most relevant chunk and uses that context to generate an answer with ChatGPT.
The issue I’m facing is that my current setup only retrieves one matching document chunk, which often doesn’t provide enough information for good answers. I need to figure out how to get more relevant chunks so the chatbot has better context to work with.
import { Request, Response } from "express";
import asyncHandler from 'express-async-handler';
import { v4 as uuidv4 } from 'uuid';
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { PineconeClient } from '@pinecone-database/pinecone';
import { OpenAIEmbeddings } from 'langchain/embeddings/openai';
import { ChatOpenAI } from "langchain/chat_models/openai";
import { PineconeStore } from 'langchain/vectorstores/pinecone';
import { HumanMessage, SystemMessage } from "langchain/schema";
/**
* Process uploaded documents and create vector embeddings
*/
export const createDocumentEmbeddings = asyncHandler(
async (req: Request, res: Response) => {
const userId: string = req.body.userId as string;
const fileContent: string = req.body.fileContent as string;
const fileId: string = uuidv4();
const pineconeClient = new PineconeClient();
const apiKey: string = process.env.PINECONE_KEY ?? '';
await pineconeClient.init({
environment: process.env.PINECONE_ENV ?? '',
apiKey: apiKey,
});
try {
const textSplitter = new RecursiveCharacterTextSplitter();
const documents = await textSplitter.createDocuments([fileContent], [{
file: fileId,
user: userId
}]);
console.log('building vector database...');
const embedder = new OpenAIEmbeddings();
const indexName = pineconeClient.Index(process.env.PINECONE_INDEX_NAME ?? '');
const storeConfig = {
pineconeIndex: indexName,
namespace: process.env.PINECONE_NS ?? '',
textKey: 'content',
};
const vectorStore = await PineconeStore.fromDocuments(documents, embedder, storeConfig);
res.json({success: true, documentId: fileId})
} catch (error) {
console.error('Document processing failed:', error);
res.status(500).send('Failed to process document upload.');
}
}
);
/**
* Search for relevant documents and generate AI response
*/
export const queryKnowledgeBase = async (req: Request, res: Response) => {
const userId: string = req.body.userId as string;
const userQuestion: string = req.body.userQuestion as string;
const maxResults: number = (req.body.maxResults as number) || 1;
const pineconeClient = new PineconeClient();
await pineconeClient.init({
apiKey: process.env.PINECONE_KEY ?? '',
environment: process.env.PINECONE_ENV ?? '',
});
const index = pineconeClient.Index(process.env.PINECONE_INDEX_NAME ?? '');
const store = await PineconeStore.fromExistingIndex(
new OpenAIEmbeddings(),
{
pineconeIndex: index,
namespace: process.env.PINECONE_NS ?? '',
textKey: 'content'
}
);
try {
const matchingDocs = await store.similaritySearch(userQuestion, maxResults, {
user: userId,
});
const aiChat = new ChatOpenAI({ temperature: 0.3 });
const aiResponse = await aiChat.call([
new SystemMessage(
`Here is the relevant information from the user's documents:
${matchingDocs[0].pageContent}
Please use this context to answer the user's question. Respond in the same language as the question.`
),
new HumanMessage(userQuestion),
]);
res.json(aiResponse);
} catch (error) {
console.error('Search failed: ' + error)
}
};