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