Skip to main content
COSMICBYTEZLABS
NewsSecurityHOWTOsToolsStudyTraining
ProjectsChecklistsAI RankingsNewsletterStatusTagsAbout
Subscribe

Press Enter to search or Esc to close

News
Security
HOWTOs
Tools
Study
Training
Projects
Checklists
AI Rankings
Newsletter
Status
Tags
About
RSS Feed
Reading List
Subscribe

Stay in the Loop

Get the latest security alerts, tutorials, and tech insights delivered to your inbox.

Subscribe NowFree forever. No spam.
COSMICBYTEZLABS

Your trusted source for IT intelligence, cybersecurity insights, and hands-on technical guides.

429+ Articles
114+ Guides

CONTENT

  • Latest News
  • Security Alerts
  • HOWTOs
  • Projects
  • Exam Prep

RESOURCES

  • Search
  • Browse Tags
  • Newsletter Archive
  • Reading List
  • RSS Feed

COMPANY

  • About Us
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CosmicBytez Labs. All rights reserved.

System Status: Operational
  1. Home
  2. Projects
  3. Building a RAG System Without ML Embeddings
Building a RAG System Without ML Embeddings
PROJECTIntermediate

Building a RAG System Without ML Embeddings

Create a lightweight, offline-capable documentation search system using keyword-based scoring instead of ML embeddings. Perfect for air-gapped...

Dylan H.

Software Engineering

February 3, 2026
8 min read
2-3 hours

Overview

Retrieval-Augmented Generation (RAG) typically relies on ML embeddings to find semantically similar documents. But what if you need a RAG system that works offline, has zero dependencies, and gives you complete control over the search logic?

This project builds a lightweight RAG engine using keyword-based scoring that can search 160+ documents in milliseconds without any ML models.

Why Skip Embeddings?

ML EmbeddingsKeyword-Based
Requires ML libraries (transformers, sentence-transformers)Zero dependencies
Model loading takes secondsInstant startup
Needs GPU for speedRuns on any hardware
Black-box relevanceTransparent scoring
General-purposeDomain-optimized

What We're Building

┌─────────────────────────────────────────────────────────────┐
│                    RAG Engine Architecture                  │
├─────────────────────────────────────────────────────────────┤
│  Document Loading   →  Keyword Extraction  →  Caching       │
│         ↓                                                   │
│  Query Expansion    →  Relevance Scoring   →  Response      │
└─────────────────────────────────────────────────────────────┘

Project Setup

Create a new Node.js project:

mkdir rag-engine && cd rag-engine
npm init -y

No dependencies needed! We use only Node.js built-ins.

File Structure

rag-engine/
├── rag-engine.js      # Core search logic
├── cli.js             # Interactive CLI
├── test-rag.js        # Test suite
└── docs/              # Your markdown documentation
    ├── CONTAINERS/
    ├── WINDOWS/
    ├── SECURITY/
    └── ...

Core Components

1. Document Loading with Caching

Load markdown files with a 5-minute cache:

const fs = require('fs').promises;
const path = require('path');
 
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
let documentCache = null;
let cacheTimestamp = 0;
 
// Document categories with priority (lower = higher priority)
const HOWTO_FOLDERS = [
  { name: 'CONTAINERS', category: 'docker', priority: 1 },
  { name: 'WINDOWS', category: 'windows', priority: 2 },
  { name: 'SECURITY', category: 'security', priority: 2 },
  { name: 'NETWORKING', category: 'networking', priority: 2 },
  { name: 'LINUX', category: 'linux', priority: 3 },
  { name: 'DEPLOYMENT', category: 'deployment', priority: 2 },
];
 
async function loadDocuments() {
  // Return cached if valid
  if (documentCache && Date.now() - cacheTimestamp < CACHE_TTL) {
    return documentCache;
  }
 
  const documents = [];
 
  for (const folder of HOWTO_FOLDERS) {
    const folderPath = path.join(DOCS_BASE, folder.name);
 
    try {
      const files = await fs.readdir(folderPath);
 
      for (const file of files) {
        if (!file.endsWith('.md')) continue;
 
        const filePath = path.join(folderPath, file);
        const content = await fs.readFile(filePath, 'utf8');
 
        documents.push({
          title: extractTitle(content, file),
          folder: folder.name,
          category: folder.category,
          priority: folder.priority,
          path: filePath,
          content,
          keywords: extractKeywords(content, folder.name),
          sections: extractSections(content),
        });
      }
    } catch (err) {
      // Folder doesn't exist - skip
    }
  }
 
  documentCache = documents;
  cacheTimestamp = Date.now();
  return documents;
}

2. Keyword Extraction

Extract domain-specific keywords from documents:

function extractKeywords(content, folderName) {
  const keywords = new Set();
 
  // Add folder name as keyword
  keywords.add(folderName.toLowerCase());
 
  // Domain-specific terms to look for
  const terms = [
    // Containers
    'container', 'docker', 'image', 'dockerfile',
    // Security
    'security', 'password', 'encryption', 'firewall',
    // Networking
    'network', 'dns', 'vpn', 'subnet', 'vlan',
    // Windows
    'windows', 'powershell', 'registry', 'group policy',
    // Linux
    'linux', 'bash', 'systemd', 'apt', 'yum',
    // General
    'backup', 'restore', 'error', 'troubleshoot',
  ];
 
  const lowerContent = content.toLowerCase();
 
  for (const term of terms) {
    if (lowerContent.includes(term)) {
      keywords.add(term);
    }
  }
 
  // Extract headers as keywords
  const headers = content.match(/^#{1,3}\s+(.+)$/gm) || [];
  for (const header of headers) {
    const text = header.replace(/^#+\s+/, '').toLowerCase();
    text.split(/\s+/).forEach(word => {
      if (word.length > 3) keywords.add(word);
    });
  }
 
  return Array.from(keywords);
}

3. Query Expansion

The secret sauce - map queries to related terms:

const KEYWORD_MAP = {
  // Container ecosystem
  'container': ['docker', 'image', 'dockerfile', 'compose'],
  'docker': ['container', 'image', 'dockerfile', 'compose'],
 
  // Security domain
  'security': ['firewall', 'encryption', 'password', 'edr'],
  'firewall': ['fortigate', 'policy', 'rule', 'vpn'],
  'antivirus': ['edr', 'threat', 'malware', 'sentinelone'],
 
  // Networking
  'network': ['dns', 'ip', 'subnet', 'vlan', 'vpn'],
  'vpn': ['ssl', 'ipsec', 'remote', 'tunnel'],
 
  // Windows
  'windows': ['powershell', 'registry', 'gpo', 'driver'],
  'driver': ['update', 'bios', 'firmware'],
 
  // Error handling
  'error': ['troubleshoot', 'fix', 'problem', 'failed'],
  'install': ['deploy', 'setup', 'configure'],
};
 
function expandQuery(query) {
  const queryLower = query.toLowerCase();
  const expanded = new Set(
    queryLower.split(/\s+/).filter(w => w.length > 2)
  );
 
  // Add related terms from keyword map
  for (const [key, related] of Object.entries(KEYWORD_MAP)) {
    if (queryLower.includes(key)) {
      related.forEach(term => expanded.add(term));
    }
  }
 
  return Array.from(expanded);
}

Example expansion:

  • Query: "docker container backup"
  • Expanded: ["docker", "container", "backup", "image", "dockerfile", "compose", "restore", "database", "recovery"]

The Scoring Algorithm

This is where the magic happens:

async function search(query, options = {}) {
  const { maxResults = 5, category = null } = options;
  const documents = await loadDocuments();
  const queryTerms = expandQuery(query);
  const queryLower = query.toLowerCase();
 
  const scored = documents
    .filter(doc => !category || doc.category === category)
    .map(doc => {
      let score = 0;
 
      // Keyword matches from expansion (+3 each)
      for (const keyword of doc.keywords) {
        if (queryTerms.includes(keyword)) score += 3;
        if (queryLower.includes(keyword)) score += 2;
      }
 
      // Query terms in content/title
      for (const term of queryTerms) {
        if (doc.content.toLowerCase().includes(term)) score += 1;
        if (doc.title.toLowerCase().includes(term)) score += 3;
      }
 
      // Contextual boosts
      if (queryLower.match(/error|problem|fix|failed|issue/)) {
        if (doc.title.toLowerCase().includes('troubleshoot')) {
          score += 5;
        }
      }
 
      if (queryLower.match(/deploy|install|setup|configure/)) {
        if (doc.title.toLowerCase().match(/deployment|install|setup/)) {
          score += 5;
        }
      }
 
      // Priority penalty (lower priority = higher number)
      score -= doc.priority * 0.5;
 
      return { doc, score };
    });
 
  return scored
    .filter(s => s.score > 0)
    .sort((a, b) => b.score - a.score)
    .slice(0, maxResults)
    .map(s => s.doc);
}

Scoring Breakdown

FactorPointsLogic
Expanded keyword match+3Query term in doc keywords
Direct keyword in query+2Doc keyword found in query
Term in content+1Any match in body text
Term in title+3Match in document title
Troubleshoot boost+5Error query + troubleshoot doc
Deployment boost+5Install query + setup doc
Priority penalty-0.5/levelLower priority folders ranked lower

Response Generation

Build a formatted response with source attribution:

async function getResponse(query) {
  const docs = await search(query, { maxResults: 3 });
 
  if (docs.length === 0) {
    return {
      content: `No results found for: "${query}"`,
      sources: [],
      matched: false,
    };
  }
 
  const topDoc = docs[0];
 
  // Find best matching section
  let bestSection = findBestSection(topDoc, query);
 
  // Build response
  let response = `## ${topDoc.title}\n`;
  response += `*Source: ${topDoc.folder}*\n\n`;
 
  if (bestSection) {
    response += `### ${bestSection.title}\n\n`;
    response += bestSection.content.slice(0, 3000);
  } else {
    response += topDoc.content.slice(0, 3000);
  }
 
  // Add related documents
  if (docs.length > 1) {
    response += '\n\n---\n**Related:**\n';
    docs.slice(1).forEach(d => {
      response += `- ${d.title}\n`;
    });
  }
 
  return {
    content: response,
    sources: docs.map(d => ({
      title: d.title,
      folder: d.folder,
    })),
    matched: true,
  };
}

CLI Interface

Create an interactive command-line interface:

#!/usr/bin/env node
const readline = require('readline');
const { getResponse, getStats } = require('./rag-engine');
 
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});
 
async function main() {
  console.log('\n========================================');
  console.log('   OFFLINE RAG - Documentation Search');
  console.log('========================================\n');
 
  const stats = await getStats();
  console.log(`Loaded ${stats.total} documents\n`);
  console.log('Commands: /stats, /list, /quit\n');
 
  askQuestion();
}
 
function askQuestion() {
  rl.question('> ', async (input) => {
    const query = input.trim();
 
    if (!query) {
      askQuestion();
      return;
    }
 
    if (query === '/quit') {
      console.log('Goodbye!');
      rl.close();
      return;
    }
 
    if (query === '/stats') {
      const stats = await getStats();
      console.log(JSON.stringify(stats, null, 2));
      askQuestion();
      return;
    }
 
    // Search and respond
    console.log('\nSearching...\n');
    const response = await getResponse(query);
 
    console.log('─'.repeat(50));
    console.log(response.content);
    console.log('─'.repeat(50));
 
    if (response.sources.length > 0) {
      console.log('\nSources:');
      response.sources.forEach(s => {
        console.log(`  - ${s.title} [${s.folder}]`);
      });
    }
 
    console.log();
    askQuestion();
  });
}
 
main();

Usage Examples

Programmatic Usage

const { search, getResponse, getStats } = require('./rag-engine');
 
// Search for documents
const results = await search('docker container backup');
console.log('Found:', results.map(r => r.title));
 
// Get formatted response
const response = await getResponse('how to update BIOS');
console.log(response.content);
 
// Get statistics
const stats = await getStats();
console.log(`${stats.total} documents loaded`);

CLI Session

> docker container not starting

──────────────────────────────────────────────────
## HOWTO- Troubleshoot BC Container Issues

*Source: CONTAINERS*

### Common Startup Problems

1. **Port conflicts** - Check if ports 80/443 are in use
2. **Memory limits** - Containers need at least 4GB RAM
3. **License issues** - Verify license file is mounted

...

──────────────────────────────────────────────────

Sources:
  - HOWTO- Troubleshoot BC Container Issues [CONTAINERS]
  - HOWTO- Docker Container Backup [CONTAINERS]

Performance

With 161 documents (1.2MB total):

MetricValue
Cold start~50ms
Cached query~5ms
Memory usage~15MB
Cache TTL5 minutes

When to Use This Approach

Good fit:

  • Air-gapped environments
  • Domain-specific documentation
  • Full control over search logic needed
  • Simple deployment (single file)
  • No ML infrastructure available

Consider embeddings instead:

  • General-purpose search
  • Semantic similarity needed
  • Large document corpus (10K+)
  • Multi-language support

Extending the System

Add Web Interface

Wrap the engine in an Express API:

const express = require('express');
const { search, getResponse } = require('./rag-engine');
 
const app = express();
app.use(express.json());
 
app.post('/api/search', async (req, res) => {
  const { query, maxResults = 5 } = req.body;
  const results = await search(query, { maxResults });
  res.json({ results });
});
 
app.post('/api/chat', async (req, res) => {
  const { query } = req.body;
  const response = await getResponse(query);
  res.json(response);
});
 
app.listen(3000);

Add Category Filtering

// Search only in security docs
const securityDocs = await search('firewall rules', {
  category: 'security',
  maxResults: 10
});

Export Chat History

function exportToMarkdown(history) {
  let md = '# Chat Export\n\n';
 
  for (const entry of history) {
    md += `## Q: ${entry.query}\n\n`;
    md += `${entry.response}\n\n`;
    md += `---\n\n`;
  }
 
  return md;
}

Next Steps

  • Building a SIEM with Open-Source Tools
  • Network Monitoring Basics
  • Docker Security Fundamentals
#RAG#Node.js#Search#Documentation#AI

Related Articles

Build a Collaborative IPS with CrowdSec

Deploy CrowdSec on a Linux server to get community-powered intrusion prevention — block brute-force attacks, credential stuffing, and vulnerability scanners using crowd-sourced threat intelligence and automatic firewall enforcement.

10 min read

Keycloak SSO: Self-Hosted Identity Provider for Your Homelab

Deploy Keycloak with Docker Compose and PostgreSQL to build a centralised single sign-on platform for your homelab services, with OIDC integration for...

11 min read

HashiCorp Vault: Secrets Management for Your Homelab and

Deploy HashiCorp Vault to centrally manage secrets, certificates, and dynamic credentials — eliminating hardcoded passwords from your infrastructure with...

12 min read
Back to all Projects