Imagine this scenario: A new customer service representative is handling an unusual return request for items damaged during shipping. Instead of resolving the case quickly, they have to open a shared network drive filled with thirty PDF files spanning hundreds of pages of internal guidelines, legal disclosures, and operational manuals. Searching for the keyword "damaged packaging return" in standard Windows Search returns either zero results or twenty unrelated folders. Frustrated, the representative spends an hour manually reading through dense legal text or, worse, makes an "educated guess" that later costs the company money in refund disputes or lost margins.
If this employee copied the internal manual text and pasted it into public ChatGPT to ask for a solution, they would risk exposing confidential corporate procedures and customer details. If they asked a public chatbot without providing the text, the model would try to answer using its general training data and would likely fabricate (hallucinate) specific local regulatory timelines or company policies.
At the nolimeo technology studio, we solve this information barrier by deploying Retrieval-Augmented Generation (RAG) systems. This architecture combines the semantic search capabilities of vector databases with the generative strengths of Large Language Models (LLMs). The result is an internal AI assistant that finds the relevant paragraph in a large document quickly and drafts a natural human response, supported by direct references to the exact page and section of the internal guideline.
1. The RAG Architecture: How It Works Under the Hood
Standard language models are closed systems. Their knowledge is limited to their training cutoff date, and they do not have access to internal data stored on your SharePoint, Confluence, ERP, or local network drives.
Retrieval-Augmented Generation (RAG) solves this by retrieving the most relevant sections of your corporate documents before sending the user's question to the LLM. The system appends these sections directly to the query as context. The model then functions as an analytical reader rather than guessing from memory: it reviews the provided text and answers strictly based on the source material.
[ User Question ]
│
├─► [ Generate Question Embedding ]
│ │
│ (Similarity Search)
│ ▼
├─► [ Retrieve Most Relevant Chunks from pgvector ]
│ │
│ (Prompt Augmentation)
│ ▼
└─► [ Combine Question + Context Chunks ] ──► [ LLM (e.g., GPT-4o) ] ──► [ Fact-based Answer with Citations ]
The process is split into two primary phases:
- Document Indexing (Offline Phase): Company guidelines are parsed, stripped of formatting clutter, divided into logical segments (chunks), converted into numerical vectors (embeddings), and stored in a vector database.
- Retrieval and Generation (Online Phase): The user asks a question. The system converts the question into a vector, locates the semantically closest document chunks, builds an augmented prompt for the LLM, and the model generates a secure, factually accurate answer.
2. Technical Implementation: RAG Pipeline in TypeScript
Below is a production-ready backend implementation written in TypeScript. This code demonstrates how to chunk document text, generate embeddings using the official OpenAI API, store them, and perform semantic searches to produce grounded answers.
Step A: Chunking and Indexing Documents in pgvector
For semantic search to work effectively, we must divide documents into optimal segments. If we index an entire handbook as a single block, the embedding will be too broad. If chunks are too small (e.g., individual sentences), they will lose valuable context. We use chunking with a defined size and overlap to ensure that critical information on chunk borders is preserved.
// src/services/rag-indexer.ts
import { OpenAI } from "openai";
import { createClient } from "@supabase/supabase-js";
interface DocumentChunk {
documentId: string;
content: string;
chunkIndex: number;
embedding: number[];
metadata: Record<string, any>;
}
export class SafeDocumentIndexer {
private openai: OpenAI;
private supabase;
constructor() {
const apiKey = process.env.OPENAI_API_KEY;
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
if (!apiKey || !supabaseUrl || !supabaseKey) {
throw new Error("Critical error: Missing configuration environment variables.");
}
this.openai = new OpenAI({ apiKey });
this.supabase = createClient(supabaseUrl, supabaseKey);
}
/**
* Splits a long text into smaller logical chunks with a defined overlap
*/
public chunkText(text: string, chunkSize: number = 800, overlap: number = 150): string[] {
const chunks: string[] = [];
let currentIndex = 0;
const normalizedText = text.replace(/\s+/g, " ").trim();
while (currentIndex < normalizedText.length) {
const chunk = normalizedText.substring(currentIndex, currentIndex + chunkSize);
chunks.push(chunk);
currentIndex += (chunkSize - overlap);
}
return chunks;
}
/**
* Generates a vector embedding for a given text segment
*/
public async generateEmbedding(text: string): Promise<number[]> {
const response = await this.openai.embeddings.create({
model: "text-embedding-3-small", // 1536-dimensional high-efficiency embedding
input: text,
});
const embedding = response.data[0]?.embedding;
if (!embedding) {
throw new Error("Failed to generate embedding via OpenAI.");
}
return embedding;
}
/**
* Chunks a document and stores it with embeddings in a pgvector database
*/
public async indexDocument(documentId: string, title: string, fullText: string, category: string): Promise<void> {
const rawChunks = this.chunkText(fullText);
const dbChunks: DocumentChunk[] = [];
console.log(`Starting indexing for document: ${title}, Chunks: ${rawChunks.length}`);
for (let i = 0; i < rawChunks.length; i++) {
const content = rawChunks[i];
const embedding = await this.generateEmbedding(content);
dbChunks.push({
documentId,
content,
chunkIndex: i,
embedding,
metadata: {
title,
category,
source_length: fullText.length,
indexed_at: new Date().toISOString()
}
});
}
// Bulk insert into the PostgreSQL table equipped with pgvector
const { error } = await this.supabase
.from("document_chunks")
.insert(
dbChunks.map(chunk => ({
document_id: chunk.documentId,
content: chunk.content,
chunk_index: chunk.chunkIndex,
embedding: chunk.embedding,
metadata: chunk.metadata
}))
);
if (error) {
console.error("Error saving chunks to pgvector:", error);
throw new Error(`Index saving failed: ${error.message}`);
}
console.log(`Document "${title}" indexed successfully.`);
}
}
Step B: Semantic Context Retrieval and Answer Generation
When a user asks a question, the system first retrieves the semantically closest text blocks using cosine similarity vectors in our database, which are then used as strict boundaries for the LLM response.
// src/services/rag-query-engine.ts
import { OpenAI } from "openai";
import { createClient } from "@supabase/supabase-js";
interface SearchResult {
content: string;
similarity: number;
metadata: {
title: string;
category: string;
};
}
export class SafeRAGQueryEngine {
private openai: OpenAI;
private supabase;
constructor() {
this.openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
this.supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!);
}
/**
* Retrieves the top-K most relevant document chunks using cosine similarity in pgvector
*/
private async searchRelevantContext(queryEmbedding: number[], limit: number = 3, minSimilarity: number = 0.65): Promise<SearchResult[]> {
// Call PostgreSQL RPC function match_chunks for vector comparison
const { data, error } = await this.supabase.rpc("match_chunks", {
query_embedding: queryEmbedding,
match_threshold: minSimilarity,
match_count: limit
});
if (error) {
console.error("Error during RPC match_chunks:", error);
throw new Error("Error during semantic database search.");
}
return data || [];
}
/**
* Processes a user query, performs semantic search, and returns a structured response with citations
*/
public async askEnterpriseAssistant(userQuery: string): Promise<{ answer: string; sources: string[] }> {
try {
// 1. Generate embedding for the user's query
const queryResponse = await this.openai.embeddings.create({
model: "text-embedding-3-small",
input: userQuery,
});
const queryEmbedding = queryResponse.data[0]?.embedding;
if (!queryEmbedding) {
throw new Error("Failed to generate question embedding.");
}
// 2. Query semantic context from pgvector database
const contextMatches = await this.searchRelevantContext(queryEmbedding);
if (contextMatches.length === 0) {
return {
answer: "I could not find any relevant information in the official company guidelines for your query. I cannot answer to prevent speculation.",
sources: []
};
}
// 3. Compile context block for the LLM
const contextText = contextMatches
.map((match, index) => `[Source ${index + 1} - Document: ${match.metadata.title}]:\n${match.content}`)
.join("\n\n");
// 4. Prompt augmentation - LLM receives strict instructions to cite sources and avoid speculation
const systemPrompt = `You are an internal corporate assistant that answers queries strictly based on the attached company guidelines.
Your response must meet these criteria:
1. Answer strictly based on the provided context. If the context does not contain the answer, state that clearly and do not speculate.
2. Provide exact citations in the format [Source X].
3. Do not use any information from your training data that is not supported by the context.
4. Respond in a professional, objective tone in English.`;
const response = await this.openai.chat.completions.create({
model: "gpt-4o",
temperature: 0.0, // Low temperature ensures strict fidelity to the source text
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: `Available company guidelines:\n${contextText}\n\nEmployee question: ${userQuery}` }
]
});
const answer = response.choices[0]?.message?.content || "Error generating response.";
const uniqueSources = Array.from(new Set(contextMatches.map(m => m.metadata.title)));
return {
answer,
sources: uniqueSources
};
} catch (error) {
console.error("RAG query pipeline failed:", error);
throw new Error("System failure of the internal assistant.");
}
}
}
3. Why pgvector in PostgreSQL Wins Over Standalone Vector Databases
When designing RAG systems, many development teams use standalone vector databases (like Pinecone or Milvus). However, for established enterprises, this approach introduces significant complexity: licensing costs, the need to sync data between your primary PostgreSQL database and the vector DB, and security risks.
At nolimeo, we integrate vector search directly into the relational database using the pgvector extension in PostgreSQL (or Supabase).
Advantage 1: Row-Level Security (RLS) for Document Permissions
The biggest risk of shared company search is unauthorized access to restricted data. If the AI assistant has access to payroll policies, executive contracts, and standard operating procedures, a general employee should not be able to query the salaries of their colleagues.
By using pgvector in PostgreSQL, we can define Row-Level Security (RLS) rules ensuring that the semantic search only accesses documents the logged-in user has permissions to view based on their role in the system.
-- SQL definition of RLS for the document_chunks table in PostgreSQL
ALTER TABLE document_chunks ENABLE ROW LEVEL SECURITY;
CREATE POLICY user_access_policy ON document_chunks
FOR SELECT
USING (
-- Chunk is accessible only if the user's role matches document permissions
EXISTS (
SELECT 1 FROM documents d
WHERE d.id = document_chunks.document_id
AND d.required_role <= auth.jwt()->>'user_role'
)
);
Advantage 2: Database Integrity and Transactions
When an employee updates an internal document in the ERP or CMS, the change is committed in pgvector within a single database transaction. This prevents orphaned vector chunks from being retrieved and cited by the assistant after a document has been deleted.
4. Production Challenges in Enterprise RAG Systems
RAG systems look straightforward on paper, but transitioning from a prototype to a stable enterprise deployment introduces engineering challenges. A robust system must handle the following edge cases:
4.1. Managing Stale Data
If a health and safety policy changes, older versions must be invalidated quickly. The RAG system should not mix guidelines from 2022 with updated compliance rules from 2026.
- Our Solution: Every document is indexed with a version number and active status flag. The search filter restricts queries to current document versions. When a new manual is uploaded, the system automatically replaces or deletes the chunks associated with that document ID.
4.2. The "Lost in the Middle" Phenomenon
Large language models tend to pay close attention to the beginning and end of a provided context block, often overlooking information placed in the middle of a massive text input.
- Our Solution: We implement strict limits on retrieved context chunks (typically top-3 to top-5) and use Re-ranking models (e.g., Cohere ReRank). Re-ranking reorganizes the retrieved chunks by relevance before passing them to the LLM, ensuring that critical passages are placed at the beginning of the context window.
4.3. Restricting Low-Similarity Matches
If an employee asks a query unrelated to company business, a vector search will still return results because cosine similarity always calculates the "closest" vectors, even if they are unrelated.
- Our Solution: We enforce a minimum similarity score threshold (
match_threshold). If the best match falls below 0.65 similarity, the query process aborts, and the assistant responds: "I could not find relevant information in the company documents for your query." This prevents the model from speculating or fabricating answers.
Conclusion: Invest in Precise Knowledge Retrieval
Intelligent internal search powered by RAG and pgvector improves operational efficiency. Employees no longer waste time searching through hundreds of pages of documentation, but instead work directly with verified facts. This minimizes errors, accelerates onboarding, and secures corporate know-how.
When implementing these systems, it is essential to avoid fragile no-code workarounds. Your corporate knowledge base is valuable intellectual property that must remain under your direct technology control.
We are nolimeo—a boutique software engineering studio. Our clients do not pay for bloated agency overhead or talk to juniors. We design and program every project as senior developers. We focus on secure, clean code, enterprise software stability, and reliable integrations of modern tech, including enterprise AI.
Ready to deploy a secure internal search engine for your company guidelines, integrate pgvector database queries, or implement a custom AI assistant that stays grounded in your documents? Contact us and we’ll review your search flow, document structure, and retrieval architecture.
