AI-powered search is exploding right now, and Retrieval-Augmented Generation (RAG) is at the heart of it. From chatbots that understand your company policies to internal knowledge assistants, RAG lets you query private documents using natural language—securely and accurately.
In this article, I’ll walk you through how I built a fully local document RAG application using:
Node.js (Express)
Supabase (Postgres + pgvector)
OpenAI Embeddings + Chat Models
pdf-parse (PDF text extraction)
Let’s break it down.
💡 What We’re Building
Imagine placing a PDF like Policies.pdf inside a folder.
Now you want to ask:
“What is the leave policy?” “How many casual leaves are allowed?” “What is the work-from-home guideline?”
This RAG system:
Reads the PDF
Extracts text
Breaks text into overlapping chunks
Converts chunks into embeddings
Stores them in Supabase pgvector
Uses semantic search to find relevant chunks
Generates accurate answers using OpenAI
All from your local machine.
🏗️ Architecture Overview
Here’s how the flow works end-to-end:
MyDocs/ PDFs
↓
pdf-parse extracts text
↓
Chunking (1000 chars + 200 overlap)
↓
OpenAI Embeddings (1536-d vectors)
↓
Supabase (pgvector)
↓
Semantic Search (match_documents RPC)
↓
LLM (GPT-4.1-mini or any model)
↓
Answer to UserSimple, modular, and scalable.
📂 Folder Structure
Document-RAG-App/
│
├── MyDocs/
│ ├── Policies.pdf # Local documents
│
├── index.js # Main RAG backend
├── package.json
├── .env # API keys
└── README.md
Just drop your PDFs into MyDocs/ and hit the indexing API.
🔧 Setting Up the Project
Step 1: Install Dependencies
npm install express cors dotenv openai @supabase/supabase-js pdf-parse
Step 2: Environment Variables
Create .env:
OPENAI_API_KEY=your_openai_key
SUPABASE_URL=https://your-project-url.supabase.co
SUPABASE_ANON_KEY=your_anon_key
PORT=3000
🗄️ Supabase Setup (Vector DB)
Supabase provides Postgres + pgvector, a perfect fit for RAG apps.
1. Enable the vector extension
create extension if not exists vector;2. Create the document table
create table if not exists "MyPDFDocuments" (
id bigserial primary key,
content text,
embedding vector(1536),
title text,
source text,
path text,
created_at timestamptz default now()
);3. Create a vector index (recommended)
create index if not exists mypdfdocuments_embedding_idx
on "MyPDFDocuments"
using ivfflat (embedding vector_cosine_ops)
with (lists = 100);4. Add the semantic search RPC function
create or replace function match_documents(
query_embedding vector(1536),
match_threshold float,
match_count int
)
returns table (
id bigint,
content text,
similarity float
)
language plpgsql
as $$
beginreturn query
select
d.id,
d.content,
1 - (d.embedding <=> query_embedding) as similarity
from "MyPDFDocuments" d
where 1 - (d.embedding <=> query_embedding) > match_threshold
order by similarity desc
limit match_count;
end;
$$;
Join the conversation! Your thoughts help the community grow.