Files
klz-cables.com/app/api/sync-qdrant/route.ts
Marc Mintel 8e99c9d121
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Failing after 55s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
feat: automated Qdrant sync with Mistral embeddings + Kabelhandbuch ingestion
- Switch embedding API from OpenRouter to Mistral mistral-embed (1024-dim, EU/DSGVO)
- Add afterChange/afterDelete hooks to Posts.ts and Pages.ts for live sync
- Integrate kabelhandbuch.txt parsing into /api/sync-qdrant boot route
- Add .gitignore entries for kabelhandbuch.txt
2026-03-07 15:39:10 +01:00

166 lines
4.8 KiB
TypeScript

import { NextResponse } from 'next/server';
import { getPayload } from 'payload';
import configPromise from '../../../payload.config';
import { upsertProductVector } from '../../../src/lib/qdrant';
export const dynamic = 'force-dynamic';
export const maxDuration = 120;
/**
* Internal endpoint called by the warmup script on every dev boot.
* Syncs posts, pages, and products from Payload CMS into Qdrant.
* NOT for form entries, media, or users.
*/
export async function GET() {
const results = { products: 0, posts: 0, pages: 0, errors: [] as string[] };
try {
const payload = await getPayload({ config: configPromise });
// ── Products ──
const { docs: products } = await payload.find({
collection: 'products',
limit: 1000,
depth: 0,
where: { _status: { equals: 'published' } },
});
for (const product of products) {
try {
const contentText = `${product.title} - SKU: ${product.sku}\n${product.description || ''}`;
await upsertProductVector(String(product.id), contentText, {
type: 'product',
data: {
title: product.title,
sku: product.sku,
slug: product.slug,
description: product.description,
},
});
results.products++;
} catch (e: any) {
results.errors.push(`product:${product.sku}: ${e.message}`);
}
}
// ── Posts ──
const { docs: posts } = await payload.find({
collection: 'posts',
limit: 1000,
depth: 0,
where: { _status: { equals: 'published' } },
});
for (const post of posts) {
try {
const contentText = [
`Blog-Artikel: ${post.title}`,
post.excerpt ? `Zusammenfassung: ${post.excerpt}` : '',
post.category ? `Kategorie: ${post.category}` : '',
]
.filter(Boolean)
.join('\n');
await upsertProductVector(`post_${post.id}`, contentText, {
type: 'knowledge',
content: contentText,
data: {
title: post.title,
slug: post.slug,
},
});
results.posts++;
} catch (e: any) {
results.errors.push(`post:${post.slug}: ${e.message}`);
}
}
// ── Pages ──
const { docs: pages } = await payload.find({
collection: 'pages',
limit: 1000,
depth: 0,
where: { _status: { equals: 'published' } },
});
for (const page of pages) {
try {
const contentText = [
`Seite: ${page.title}`,
page.excerpt ? `Beschreibung: ${page.excerpt}` : '',
]
.filter(Boolean)
.join('\n');
await upsertProductVector(`page_${page.id}`, contentText, {
type: 'knowledge',
content: contentText,
data: {
title: page.title,
slug: page.slug,
},
});
results.pages++;
} catch (e: any) {
results.errors.push(`page:${page.slug}: ${e.message}`);
}
}
// ── Kabelhandbuch (Static Text) ──
const os = require('os');
const path = require('path');
const fs = require('fs');
const crypto = await import('crypto');
const txtPath = path.join(process.cwd(), 'kabelhandbuch.txt');
let manualChunks = 0;
if (fs.existsSync(txtPath)) {
try {
const text = fs.readFileSync(txtPath, 'utf8');
const chunks = text
.split(/\n\s*\n/)
.map((c: string) => c.trim())
.filter((c: string) => c.length > 50);
for (let i = 0; i < chunks.length; i++) {
const chunkText = chunks[i];
const syntheticId = crypto.randomUUID();
await upsertProductVector(syntheticId, chunkText, {
type: 'knowledge',
content: chunkText,
data: {
title: `Kabelhandbuch Wissen - Bereich ${i + 1}`,
source: 'Kabelhandbuch KLZ.pdf',
},
});
manualChunks++;
}
console.log(`[Qdrant Sync] ✅ ${manualChunks} Kabelhandbuch-Chunks synced`);
} catch (e: any) {
results.errors.push(`kabelhandbuch: ${e.message}`);
}
} else {
console.log(`[Qdrant Sync] ⚠️ skipped Kabelhandbuch: ${txtPath} not found`);
}
console.log(
`[Qdrant Sync] ✅ ${results.products} products, ${results.posts} posts, ${results.pages} pages synced, ${manualChunks} manual chunks synced`,
);
return NextResponse.json({
success: true,
synced: {
products: results.products,
posts: results.posts,
pages: results.pages,
},
errors: results.errors.length > 0 ? results.errors : undefined,
});
} catch (error: any) {
console.error('[Qdrant Sync] ❌ Fatal error:', error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}