Unrag
Examples

Documentation Search

A complete example of semantic search over markdown documentation.

This example shows how to build a documentation search feature from end to end. You'll ingest markdown files from a docs directory and expose a search endpoint that returns relevant sections.

Project structure

my-docs-site/
├── content/
│   └── docs/
│       ├── getting-started.mdx
│       ├── configuration.mdx
│       └── guides/
│           ├── deployment.mdx
│           └── authentication.mdx
├── scripts/
│   └── ingest-docs.ts
├── app/
│   └── api/
│       └── search/
│           └── route.ts
└── unrag.config.ts

Ingestion script

This script walks the docs directory, parses frontmatter, and indexes each file:

// scripts/ingest-docs.ts
import { createUnragEngine } from "../unrag.config";
import { readFile, readdir } from "fs/promises";
import matter from "gray-matter";
import path from "path";

async function findDocs(dir: string): Promise<string[]> {
  const entries = await readdir(dir, { withFileTypes: true, recursive: true });
  return entries
    .filter((e) => e.isFile() && /\.(md|mdx)$/.test(e.name))
    .map((e) => path.join(e.parentPath ?? dir, e.name));
}

async function main() {
  const engine = createUnragEngine();
  const docsDir = path.join(process.cwd(), "content/docs");
  const files = await findDocs(docsDir);

  console.log(`Indexing ${files.length} documentation files...\n`);

  for (const file of files) {
    const raw = await readFile(file, "utf8");
    const { data: frontmatter, content } = matter(raw);
    
    const relativePath = path.relative(docsDir, file);
    const sourceId = `docs:${relativePath.replace(/\.(md|mdx)$/, "")}`;

    const result = await engine.ingest({
      sourceId,
      content,
      metadata: {
        title: frontmatter.title ?? path.basename(file, path.extname(file)),
        description: frontmatter.description ?? null,
        path: relativePath,
      },
    });

    console.log(`${frontmatter.title ?? relativePath} (${result.chunkCount} chunks)`);
  }

  console.log("\nDone! Run your dev server and try searching.");
}

main().catch(console.error);

Run it with npx tsx scripts/ingest-docs.ts.

Search endpoint

The search endpoint receives a query and returns matching sections with their context:

// app/api/search/route.ts
import { createUnragEngine } from "@unrag/config";
import { NextResponse } from "next/server";

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const query = searchParams.get("q")?.trim();

  if (!query || query.length < 2) {
    return NextResponse.json(
      { error: "Query must be at least 2 characters" },
      { status: 400 }
    );
  }

  const engine = createUnragEngine();
  const result = await engine.retrieve({ 
    query, 
    topK: 10,
    scope: { sourceId: "docs:" }, // Only search docs
  });

  return NextResponse.json({
    query,
    results: result.chunks.map((chunk) => ({
      id: chunk.id,
      title: chunk.metadata.title,
      path: chunk.metadata.path,
      content: chunk.content,
      score: chunk.score,
    })),
    meta: {
      searchTimeMs: Math.round(result.durations.totalMs),
    },
  });
}

Frontend integration

A simple search component that calls the endpoint:

"use client";
import { useState } from "react";

type SearchResult = {
  id: string;
  title: string;
  path: string;
  content: string;
  score: number;
};

export function DocsSearch() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState<SearchResult[]>([]);
  const [loading, setLoading] = useState(false);

  const search = async () => {
    if (!query.trim()) return;
    
    setLoading(true);
    const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
    const data = await res.json();
    setResults(data.results ?? []);
    setLoading(false);
  };

  return (
    <div>
      <div className="flex gap-2">
        <input
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          onKeyDown={(e) => e.key === "Enter" && search()}
          placeholder="Search docs..."
          className="flex-1 px-3 py-2 border rounded"
        />
        <button onClick={search} disabled={loading} className="px-4 py-2 bg-blue-500 text-white rounded">
          {loading ? "..." : "Search"}
        </button>
      </div>
      
      <div className="mt-4 space-y-4">
        {results.map((result) => (
          <a 
            key={result.id} 
            href={`/docs/${result.path.replace(/\.(md|mdx)$/, "")}`}
            className="block p-4 border rounded hover:bg-gray-50"
          >
            <h3 className="font-medium">{result.title}</h3>
            <p className="text-sm text-gray-600 mt-1 line-clamp-2">
              {result.content}
            </p>
          </a>
        ))}
      </div>
    </div>
  );
}

Adding to your build process

Re-index docs automatically on every build:

{
  "scripts": {
    "build": "npm run ingest && next build",
    "ingest": "tsx scripts/ingest-docs.ts"
  }
}

Now your documentation search stays current with every deployment.

On this page

RAG handbook banner image

Free comprehensive guide

Complete RAG Handbook

Learn RAG from first principles to production operations. Tackle decisions, tradeoffs and failure modes in production RAG operations

The RAG handbook covers retrieval augmented generation from foundational principles through production deployment, including quality-latency-cost tradeoffs and operational considerations. Click to access the complete handbook.