// server/supabase/functions/search-knowledge/index.ts // 쿼리 텍스트 → OpenAI 임베딩 → match_knowledge_chunks RPC → 상위 청크 반환. // // 요청: // POST { query: "...", count?: number } // 응답: // { results: Array<{ id, document_id, chunk_index, content, similarity }> } import { corsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts' import { requireUser, authErrorResponse, type AuthError } from '../_shared/auth.ts' interface SearchRequest { query: string count?: number } // @ts-expect-error — Deno import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.103.0' // @ts-expect-error — Deno 런타임 전역 Deno.serve(async (req: Request) => { const preflight = handleCorsPreflightRequest(req) if (preflight) return preflight if (req.method !== 'POST') { return new Response(JSON.stringify({ error: 'Method not allowed' }), { status: 405, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }) } try { await requireUser(req) // 인증만 확인. RPC는 auth.uid() 기반 RLS const body = (await req.json()) as SearchRequest if (!body.query || body.query.trim().length === 0) { return new Response(JSON.stringify({ error: 'query 필요' }), { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }) } // @ts-expect-error — Deno.env const openaiKey = Deno.env.get('OPENAI_API_KEY') ?? '' if (!openaiKey) { return new Response( JSON.stringify({ error: 'openai_not_configured' }), { status: 503, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } // 1) 쿼리 임베딩 const embedResp = await fetch('https://api.openai.com/v1/embeddings', { method: 'POST', headers: { Authorization: `Bearer ${openaiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'text-embedding-3-small', input: body.query, dimensions: 1536 }) }) if (!embedResp.ok) { const errText = await embedResp.text() throw new Error(`임베딩 실패: ${errText.slice(0, 200)}`) } const embedData = (await embedResp.json()) as { data: Array<{ embedding: number[] }> } const queryEmbedding = embedData.data[0]?.embedding if (!queryEmbedding) { throw new Error('임베딩 응답이 비어있음') } // 2) RLS 컨텍스트에서 RPC 호출 (authenticated 유저 토큰 사용) const authHeader = req.headers.get('Authorization') ?? '' // @ts-expect-error — Deno.env const supabaseUrl = Deno.env.get('SUPABASE_URL') ?? '' // @ts-expect-error — Deno.env const anonKey = Deno.env.get('SUPABASE_ANON_KEY') ?? '' const userClient = createClient(supabaseUrl, anonKey, { global: { headers: { Authorization: authHeader } } }) const { data: matches, error: rpcErr } = await userClient.rpc('match_knowledge_chunks', { query_embedding: queryEmbedding, match_count: body.count ?? 5, similarity_threshold: 0.5 }) if (rpcErr) { throw new Error(`RPC 실패: ${rpcErr.message}`) } return new Response(JSON.stringify({ results: matches ?? [] }), { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }) } catch (err) { if (err && typeof err === 'object' && 'status' in err && 'message' in err) { return authErrorResponse(err as AuthError, corsHeaders) } const message = err instanceof Error ? err.message : 'Unknown error' return new Response(JSON.stringify({ error: message }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }) } })