DEV Community

Cover image for Building a Production-Ready AI Context Search: Integrating Local RAG into a React & Node.js Stack
Shanthanu C
Shanthanu C

Posted on

Building a Production-Ready AI Context Search: Integrating Local RAG into a React & Node.js Stack

Maximizing context windows and turning flat documentation into an intelligent, queryable vector pipeline.

Introduction
Modern web applications are shifting from static information delivery to intelligent, context-aware platforms. Whether you are building an enterprise dashboard, a customer support portal, or a SaaS product, users expect to query internal knowledge bases instantly.

In this tutorial, we will walk through how to architect and build a lightweight Retrieval-Augmented Generation (RAG) pipeline—a technique heavily utilized when scaling custom software and AI platforms—using Node.js, Express, OpenAI/open embeddings, and a React frontend.

By the end of this guide, you will have a functional, end-to-end component that ingests raw markdown/text documents, chunks them, generates vector embeddings, stores them, and uses semantic search to fetch exact contextual snippets for user prompts.

Prerequisites
Before diving into the code, ensure you have:

Node.js (v18+ recommended) installed.

Basic familiarity with React (Hooks) and Express.js.

An API key from OpenAI (or an alternative LLM provider supporting embeddings).

Step 1: Setting Up the Node.js & Express Backend
Let's initialize our backend project. Create a directory named ai-search-backend, navigate into it, and install the necessary dependencies.

Bash
mkdir ai-search-backend && cd ai-search-backend
npm init -y
npm install express cors dotenv openai
npm install -D nodemon
Create a .env file in your root directory:

Code snippet
PORT=5000
OPENAI_API_KEY=your_openai_api_key_here
Creating the Embedding and Vector Storage Engine
For simplicity and demonstration, we'll build an in-memory vector store structure. In enterprise-grade implementations (like those built for scalable software platforms), you would swap this out for Pinecone, pgvector, or Milvus.

Create a file named server.js:

JavaScript
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const OpenAI = require('openai');

const app = express();
app.use(express.json());
app.use(cors());

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

// Mock Document Database (In a real app, this would ingest from markdown files or a DB)
const knowledgeBase = [
{ id: 1, title: "Custom Software Architecture", content: "We build scalable microservices using Node.js, Docker, and Kubernetes ensuring high availability." },
{ id: 2, title: "AI & Machine Learning Integration", content: "Integrating vector embeddings, TensorFlow models, and LLMs into enterprise workflows for automated insights." },
{ id: 3, title: "React & Performance Optimization", content: "Optimizing Core Web Vitals, lazy loading components, and managing state efficiently with React and Vite." }
];

// Helper: Simple Cosine Similarity Function
function cosineSimilarity(vecA, vecB) {
let dotProduct = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < vecA.length; i++) {
dotProduct += vecA[i] * vecB[i];
normA += vecA[i] * vecA[i];
normB += vecB[i] * vecB[i];
}
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
}

// Pre-compute embeddings for our mock knowledge base on startup
let embeddedDocs = [];

async function initializeVectorStore() {
console.log("🔄 Generating embeddings for knowledge base...");
for (const doc of knowledgeBase) {
const response = await openai.embeddings.create({
model: "text-embedding-3-small",
input: doc.content,
});
embeddedDocs.push({
...doc,
embedding: response.data[0].embedding
});
}
console.log("✅ Vector store initialized successfully!");
}

// API Endpoint for Semantic Search / RAG Retrieval
app.post('/api/search', async (req, res) => {
try {
const { query } = req.body;
if (!query) return res.status(400).json({ error: "Query is required" });

// 1. Generate embedding for user query
const queryEmbeddingResponse = await openai.embeddings.create({
  model: "text-embedding-3-small",
  input: query,
});
const queryVector = queryEmbeddingResponse.data[0].embedding;

// 2. Calculate similarities
const scoredDocs = embeddedDocs.map(doc => ({
  title: doc.title,
  content: doc.content,
  score: cosineSimilarity(queryVector, doc.embedding)
}));

// 3. Sort by highest relevance score and grab top 2 matches
scoredDocs.sort((a, b) => b.score - a.score);
const topMatches = scoredDocs.slice(0, 2);

res.json({ success: true, matches: topMatches });
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error("Error processing search:", error);
res.status(500).json({ error: "Internal Server Error" });
}
});

const PORT = process.env.PORT || 5000;
initializeVectorStore().then(() => {
app.listen(PORT, () => console.log(🚀 Server running on port ${PORT}));
});
Step 2: Building the React Frontend Interface
Now let's scaffold a clean, modern React component using Vite to interface with our newly created semantic search endpoint.

Inside your project workspace, create a React app if you haven't already:

Bash
npm create vite@latest client -- --template react
cd client && npm install
Replace the contents of src/App.jsx with the following implementation:

JavaScript
import { useState } from 'react';
import './App.css';

function App() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);

const handleSearch = async (e) => {
e.preventDefault();
if (!query.trim()) return;

setLoading(true);
try {
  const response = await fetch('http://localhost:5000/api/search', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query }),
  });
  const data = await response.json();
  if (data.success) {
    setResults(data.matches);
  }
} catch (err) {
  console.error("Failed to fetch context matches:", err);
} finally {
  setLoading(false);
}
Enter fullscreen mode Exit fullscreen mode

};

return (



🔍 AI Semantic Context Search


Query platform documentation using vector similarity matching.


  <form onSubmit={handleSearch} className="search-form">
    <input
      type="text"
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      placeholder="e.g., How do you handle microservices or AI?"
      className="search-input"
    />
    <button type="submit" className="search-btn" disabled={loading}>
      {loading ? 'Searching...' : 'Search Context'}
    </button>
  </form>

  <div className="results-container">
    {results.map((res, index) => (
      <div key={index} className="result-card">
        <h3>{res.title}</h3>
        <p>{res.content}</p>
        <span className="score-badge">Relevance Score: {(res.score * 100).toFixed(1)}%</span>
      </div>
    ))}
  </div>
</div>

);
}

export default App;
Add some minimal styling to src/App.css to keep things looking clean and professional:

CSS
body {
font-family: system-ui, -apple-system, sans-serif;
background-color: #0f172a;
color: #f8fafc;
margin: 0;
padding: 0;
}
.container {
max-width: 700px;
margin: 40px auto;
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.search-form {
display: flex;
gap: 10px;
margin-bottom: 30px;
}
.search-input {
flex: 1;
padding: 12px 16px;
background: #1e293b;
border: 1px solid #334155;
border-radius: 8px;
color: #fff;
font-size: 1rem;
}
.search-btn {
padding: 12px 24px;
background: #3b82f6;
color: white;
border: none;
border-radius: 8px;
font-weight: bold;
cursor: pointer;
transition: background 0.2s;
}
.search-btn:hover {
background: #2563eb;
}
.result-card {
background: #1e293b;
border: 1px solid #334155;
padding: 20px;
border-radius: 8px;
margin-bottom: 15px;
}
.result-card h3 {
margin-top: 0;
color: #60a5fa;
}
.score-badge {
display: inline-block;
margin-top: 10px;
background: #0f172a;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.85rem;
color: #34d399;
border: 1px solid #065f46;
}
Step 3: Running and Testing Your Application
Start the Backend:

Bash
node server.js
Start the Frontend:

Bash
npm run dev

Open your browser at http://localhost:5173 and try typing natural queries like "Tell me about containerization" or "How do you handle machine learning integration?". Notice how semantic similarity accurately surfaces relevant contexts even if exact keyword matches are absent!

Conclusion & Production Best Practices
Implementing vector search bridges the gap between static content and intelligent app experiences. When scaling this pattern for enterprise solutions, consider these core architectural upgrades:

Persistent Vector Databases: Move from in-memory lookups to dedicated database vector search tools like PostgreSQL with pgvector or managed services like Pinecone.

Chunking Strategies: Break down long text documentation into overlapping 500-token chunks to ensure finer granularity during vector retrieval.

Hybrid Search: Combine semantic vector search with traditional keyword-based scoring (BM25) for ultimate search precision.

Top comments (0)