Spaces:
Sleeping
Sleeping
File size: 4,617 Bytes
bea55e2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | import { NextRequest, NextResponse } from 'next/server';
import {
generateTextEmbedding,
generateImageEmbedding,
queryChroma,
NVIDIA_API_KEY,
} from '@/lib/ai';
import { api, API_BASE } from '@/lib/api';
const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY || '';
const SUPABASE_TOKEN = process.env.SUPABASE_TOKEN || '';
const EDGE_FUNCTIONS_URL = 'https://tcwdbokruvlizkxcpkzj.supabase.co/functions/v1';
const PROJECT = 'tcwdbokruvlizkxcpkzj';
const COOKIE_NAME = 'cellex_session_id';
/**
* Smart Search API — NVIDIA + Chroma powered semantic search
*
* POST /api/smart-search
* Body: {
* query: string, // text search query
* imageUrl?: string, // optional image URL for visual search
* limit?: number, // max results (default 20)
* }
*
* Flow:
* 1. Accept text query or image URL
* 2. Generate embedding via NVIDIA (embed-qa-4 for text, neva-22b for image)
* 3. Query Chroma Vector DB for similar product IDs
* 4. Hydrate with full product data from Supabase
* 5. Return ranked results with similarity scores
*
* Fallback: If NVIDIA/Chroma unavailable, fall back to Supabase text search.
*/
export async function POST(request: NextRequest) {
if (!SUPABASE_ANON_KEY) {
return NextResponse.json({ success: false, error: 'SUPABASE_ANON_KEY not set' }, { status: 500 });
}
let body: any;
try { body = await request.json(); } catch {
return NextResponse.json({ success: false, error: 'Invalid JSON' }, { status: 400 });
}
const { query, imageUrl, limit = 20 } = body;
if (!query && !imageUrl) {
return NextResponse.json({ success: false, error: 'Query or imageUrl required' }, { status: 400 });
}
const startTime = Date.now();
// === Step 1: Generate embedding ===
let embedding: number[] = [];
let aiDescription = '';
if (imageUrl) {
// Image search: use NVIDIA NeVA-22B to describe the image, then embed
const result = await generateImageEmbedding(imageUrl, query);
embedding = result.embedding;
aiDescription = result.description;
} else if (query) {
// Text search: use NVIDIA embed-qa-4
embedding = await generateTextEmbedding(query);
}
// === Step 2: Query Chroma Vector DB ===
let chromaResults: Array<{ id: string; score: number }> = [];
if (embedding.length > 0) {
chromaResults = await queryChroma(embedding, limit);
}
// === Step 3: Hydrate with Supabase data ===
if (chromaResults.length > 0) {
const productIds = chromaResults.map(r => r.id);
const products = await hydrateProducts(productIds);
// Attach similarity scores
const scoreMap = new Map(chromaResults.map(r => [r.id, r.score]));
const rankedProducts = products.map(p => ({
...p,
_relevanceScore: scoreMap.get(String(p.id)) || 0,
}));
return NextResponse.json({
success: true,
source: 'nvidia-chroma',
query: query || aiDescription,
products: rankedProducts,
latencyMs: Date.now() - startTime,
aiDescription: aiDescription || undefined,
});
}
// === Fallback: Supabase text search ===
const fallbackResp = await fetch(`${EDGE_FUNCTIONS_URL}/products`, {
method: 'POST',
headers: { 'apikey': SUPABASE_ANON_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ op: 'search', query: query || '' }),
}).then(r => r.json()).catch(() => ({ success: false }));
return NextResponse.json({
...fallbackResp,
source: 'supabase-fallback',
latencyMs: Date.now() - startTime,
});
}
/**
* Hydrate product IDs with full product data from Supabase.
*/
async function hydrateProducts(productIds: string[]): Promise<any[]> {
if (!productIds.length) return [];
const sqlHeaders: Record<string, string> = {
'Authorization': `Bearer ${SUPABASE_TOKEN}`,
'Content-Type': 'application/json',
'User-Agent': 'Mozilla/5.0',
};
try {
const ids = productIds.map(id => `'${id}'`).join(',');
const resp = await fetch(`https://api.supabase.com/v1/projects/${PROJECT}/database/query`, {
method: 'POST',
headers: sqlHeaders,
body: JSON.stringify({
query: `SELECT id, name, price, image_url, category, seller_id, units_sold, description FROM products WHERE id IN (${ids});`,
}),
});
const data = await resp.json();
if (!Array.isArray(data)) return [];
const productMap = new Map(data.map((p: any) => [String(p.id), p]));
return productIds
.map(id => productMap.get(id))
.filter(Boolean);
} catch (err) {
console.error('[smart-search] hydrateProducts failed:', err);
return [];
}
}
|