UnRAG
Examples

Admin Re-ingest Endpoint

An admin endpoint for re-indexing individual documents on demand.

When content is edited through a CMS, admin panel, or external system, you need a way to update the search index. This example shows how to build a secure admin endpoint that re-ingests specific documents.

The use case

Imagine you have a help center where support team members edit articles through an admin interface. When they save an article, the search index should update to reflect the changes. Rather than re-indexing everything, you target the specific document that changed.

Admin endpoint

This endpoint accepts a document identifier and content, then re-ingests it:

// app/api/admin/reingest/route.ts
import { createUnragEngine } from "@unrag/config";
import { verifyAdminToken } from "@/lib/auth";

export async function POST(request: Request) {
  // Verify the request is from an authenticated admin
  const authHeader = request.headers.get("authorization");
  const isAdmin = await verifyAdminToken(authHeader);
  
  if (!isAdmin) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }
  
  // Parse the request body
  const body = await request.json().catch(() => null);
  
  if (!body?.sourceId || typeof body.sourceId !== "string") {
    return Response.json(
      { error: "Missing or invalid sourceId" },
      { status: 400 }
    );
  }
  
  if (!body?.content || typeof body.content !== "string") {
    return Response.json(
      { error: "Missing or invalid content" },
      { status: 400 }
    );
  }
  
  try {
    const engine = createUnragEngine();
    
    const result = await engine.ingest({
      sourceId: body.sourceId,
      content: body.content,
      metadata: {
        ...body.metadata,
        reindexedAt: new Date().toISOString(),
        reindexedBy: body.adminId ?? "unknown",
      },
    });
    
    return Response.json({
      success: true,
      documentId: result.documentId,
      chunkCount: result.chunkCount,
      embeddingModel: result.embeddingModel,
      durationMs: result.durations.totalMs,
    });
  } catch (error) {
    console.error("Reingest failed:", error);
    return Response.json(
      { error: "Ingestion failed. Check server logs." },
      { status: 500 }
    );
  }
}

Calling from your CMS

When content is saved in your admin interface, call the reingest endpoint:

// lib/cms/on-save.ts
async function onArticleSave(article: Article) {
  // Save to your database first
  await db.articles.update(article);
  
  // Then update the search index
  const response = await fetch("/api/admin/reingest", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": `Bearer ${getAdminToken()}`,
    },
    body: JSON.stringify({
      sourceId: `help:${article.id}`,
      content: `# ${article.title}\n\n${article.body}`,
      metadata: {
        articleId: article.id,
        title: article.title,
        category: article.category,
        lastUpdated: article.updatedAt,
      },
      adminId: getCurrentAdminId(),
    }),
  });
  
  if (!response.ok) {
    console.error("Failed to update search index:", await response.text());
    // Decide how to handle: retry, queue, alert, etc.
  }
}

Webhook integration

If your CMS supports webhooks, set up automatic reindexing:

// app/api/webhooks/cms/route.ts
import { createUnragEngine } from "@unrag/config";
import { verifyWebhookSignature } from "@/lib/webhooks";

export async function POST(request: Request) {
  // Verify the webhook is legitimate
  const signature = request.headers.get("x-cms-signature");
  const body = await request.text();
  
  if (!verifyWebhookSignature(signature, body)) {
    return Response.json({ error: "Invalid signature" }, { status: 401 });
  }
  
  const payload = JSON.parse(body);
  
  // Handle different event types
  if (payload.event === "article.updated" || payload.event === "article.created") {
    const engine = createUnragEngine();
    
    await engine.ingest({
      sourceId: `help:${payload.article.id}`,
      content: payload.article.content,
      metadata: {
        title: payload.article.title,
        category: payload.article.category,
      },
    });
    
    return Response.json({ success: true });
  }
  
  if (payload.event === "article.deleted") {
    // You might want to add a delete capability to your store adapter
    // For now, the orphaned embeddings don't affect search accuracy
    return Response.json({ success: true, action: "ignored" });
  }
  
  return Response.json({ success: true, action: "unhandled" });
}

Batch reindexing

Sometimes you need to reindex multiple documents at once:

// app/api/admin/reingest-batch/route.ts
export async function POST(request: Request) {
  const { documents } = await request.json();
  
  if (!Array.isArray(documents) || documents.length === 0) {
    return Response.json({ error: "No documents provided" }, { status: 400 });
  }
  
  const engine = createUnragEngine();
  const results = [];
  
  for (const doc of documents) {
    try {
      const result = await engine.ingest({
        sourceId: doc.sourceId,
        content: doc.content,
        metadata: doc.metadata,
      });
      results.push({ sourceId: doc.sourceId, success: true, chunkCount: result.chunkCount });
    } catch (error) {
      results.push({ sourceId: doc.sourceId, success: false, error: error.message });
    }
  }
  
  return Response.json({
    total: documents.length,
    successful: results.filter((r) => r.success).length,
    failed: results.filter((r) => !r.success).length,
    results,
  });
}

This makes it easy to build admin UIs that let you select multiple articles and reindex them all at once.

On this page