Supabase Edge Functions & AI/Vector
Edge Functions — Serverless tại Edge
Tổng quan
Supabase Edge Functions là các server-side TypeScript functions chạy trên Deno runtime, được phân phối toàn cầu tại edge -- gần với người dùng nhất để giảm latency. Mỗi function được deploy độc lập và có thể gọi từ client SDK hoặc HTTP request.
jsr: hoặc npm: specifiers. Cold starts có thể xảy ra -- thiết kế function ngắn, idempotent. Service role key thường được dùng trong Edge Function để bypass RLS khi cần truy cập toàn bộ dữ liệu.Đặc điểm chính
| Đặc điểm | Mô tả |
|---|---|
| Runtime | Supabase Edge Runtime (Deno-compatible, TypeScript first) |
| Global distribution | Triển khai trên mạng lưới edge toàn cầu, gần user nhất |
| WASM support | Hỗ trợ WebAssembly modules |
| Local dev parity | Supabase CLI cung cấp runtime giống production |
| Secrets management | Lưu credentials trong project secrets, truy cập qua Deno.env.get() |
| Time limit | Hàm được thiết kế cho tác vụ ngắn (thường dưới vài phút) |
Quickstart
Tạo, deploy và gọi Edge Function đơn giản:
# Tạo function mới
supabase functions new hello-world
# Phát triển và test local
supabase functions serve
# Deploy lên production
supabase functions deploy hello-world
# Xóa function
supabase functions delete hello-world
# Danh sách functions
supabase functions list
Cấu trúc function cơ bản:
// supabase/functions/hello-world/index.ts
interface RequestBody {
name: string
}
Deno.serve(async (req: Request) => {
// Đọc body
const { name } = await req.json() as RequestBody
// Xử lý logic
const data = {
message: `Xin chào ${name}!`,
timestamp: new Date().toISOString()
}
// Trả về response
return new Response(JSON.stringify(data), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
})
})
Gọi function từ client:
// Gọi từ supabase-js
const { data, error } = await supabase.functions.invoke('hello-world', {
body: { name: 'Vinh' }
})
// Gọi trực tiếp qua HTTP
const response = await fetch(
'https://[PROJECT_REF].supabase.co/functions/v1/hello-world',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${ANON_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'Vinh' })
}
)
CORS Handling
Khi Edge Function được gọi từ browser (frontend), cần xử lý CORS:
Deno.serve(async (req: Request) => {
// Handle preflight OPTIONS request
if (req.method === 'OPTIONS') {
return new Response('ok', {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
'Access-Control-Allow-Headers': 'Authorization, Content-Type, apikey'
}
})
}
// Handle actual request
const data = await req.json()
return new Response(JSON.stringify({ success: true, data }), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
})
})
Luôn handle OPTIONS method cùng với các CORS headers trong mọi function được gọi từ browser.
Environment Variables & Secrets
Tuyệt đối không hardcode API keys trong code. Dùng Supabase Secrets:
# Set secrets qua CLI
supabase secrets set OPENAI_API_KEY=sk-xxx
supabase secrets set STRIPE_SECRET_KEY=sk_live_xxx
supabase secrets set RESEND_API_KEY=re_xxx
# Set từ file .env
supabase secrets set --env-file .env.production
# Liệt kê secrets (chỉ hiển thị tên, không hiển thị giá trị)
supabase secrets list
# Xóa secret
supabase secrets unset MY_SECRET
// Truy cập secrets trong Edge Function
const OPENAI_KEY = Deno.env.get('OPENAI_API_KEY')!
const STRIPE_SECRET = Deno.env.get('STRIPE_SECRET_KEY')!
const SUPABASE_URL = Deno.env.get('SUPABASE_URL')!
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
Tích hợp với Supabase Services
Edge Function có thể tương tác với tất cả dịch vụ Supabase thông qua service role key:
import { createClient } from 'jsr:@supabase/supabase-js@2'
Deno.serve(async (req: Request) => {
// Tạo Supabase client với service role để bypass RLS
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
// Query database
const { data: users } = await supabase
.from('profiles')
.select('id, email, full_name')
.order('created_at', { ascending: false })
// Upload file lên Storage
const { data: upload } = await supabase.storage
.from('exports')
.upload('report.csv', csvContent)
// Gọi Auth admin API (tạo user, xóa user...)
const { data: newUser } = await supabase.auth.admin.createUser({
email: 'new@example.com',
password: 'secure-password',
email_confirm: true
})
return new Response(JSON.stringify({ users, upload }), {
headers: { 'Content-Type': 'application/json' }
})
})
Khi nào dùng Edge Functions?
Edge Functions phù hợp cho nhiều use case:
| Use case | Ví dụ cụ thể |
|---|---|
| Webhook receivers | Stripe payment webhook, GitHub push events, Slack slash commands |
| API proxies | Gọi OpenAI API (giấu API key), Resend transactional email |
| Custom business logic | Logic validation phức tạp, tính toán không nên chạy trên client |
| Image/File processing | Resize ảnh server-side, generate PDF, extract metadata |
| Sending emails | Gửi email xác nhận đơn hàng, welcome email, password reset |
| AI inference | Gọi OpenAI, HuggingFace, Claude từ edge |
| Bots | Discord Bot, Telegram Bot, Slack Bot |
| Scheduled tasks | Cron jobs gọi Edge Function định kỳ |
Use Case Chi Tiết
1. Stripe Webhook Handler
// supabase/functions/stripe-webhook/index.ts
import { createClient } from 'jsr:@supabase/supabase-js@2'
import Stripe from 'npm:stripe'
const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY')!)
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
Deno.serve(async (req: Request) => {
const signature = req.headers.get('stripe-signature')!
const body = await req.text()
// Verify webhook signature
const event = stripe.webhooks.constructEvent(
body,
signature,
Deno.env.get('STRIPE_WEBHOOK_SECRET')!
)
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object
const userId = session.client_reference_id
// Cập nhật subscription trong database
await supabase.from('subscriptions').upsert({
user_id: userId,
stripe_customer_id: session.customer,
status: 'active',
plan: session.metadata.plan,
current_period_end: new Date(session.expires_at! * 1000).toISOString()
})
break
}
case 'customer.subscription.deleted': {
const subscription = event.data.object
await supabase.from('subscriptions')
.update({ status: 'canceled' })
.eq('stripe_customer_id', subscription.customer)
break
}
}
return new Response(JSON.stringify({ received: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
})
2. OpenAI API Proxy
// supabase/functions/openai-proxy/index.ts
Deno.serve(async (req: Request) => {
const { prompt, max_tokens } = await req.json()
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'gpt-4o',
messages: [{ role: 'user', content: prompt }],
max_tokens: max_tokens || 500
})
})
const data = await response.json()
return new Response(JSON.stringify(data), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
})
})
3. Transactional Email với Resend
// supabase/functions/send-email/index.ts
import { createClient } from 'jsr:@supabase/supabase-js@2'
const RESEND_API_KEY = Deno.env.get('RESEND_API_KEY')!
Deno.serve(async (req: Request) => {
const { to, subject, html } = await req.json()
const response = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${RESEND_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
from: 'My App <noreply@myapp.com>',
to: [to],
subject: subject,
html: html
})
})
const data = await response.json()
return new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' }
})
})
4. Image Processing Pipeline
// supabase/functions/process-image/index.ts
import { createClient } from 'jsr:@supabase/supabase-js@2'
Deno.serve(async (req: Request) => {
const { imagePath, operations } = await req.json()
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
// Download ảnh gốc
const { data: imageBlob } = await supabase.storage
.from('uploads')
.download(imagePath)
// Xử lý ảnh (ví dụ: thêm watermark, resize, compress)
// ... processing logic ...
// Upload ảnh đã xử lý
await supabase.storage
.from('processed')
.upload(imagePath, processedBlob, { upsert: true })
return new Response(JSON.stringify({ success: true }), {
headers: { 'Content-Type': 'application/json' }
})
})
Background Tasks & Cron
Edge Functions phù hợp cho tác vụ ngắn (vài giây đến vài phút). Với tác vụ dài hạn, kết hợp với Cron để chạy định kỳ:
-- Gọi Edge Function mỗi giờ để đồng bộ dữ liệu
SELECT cron.schedule(
'hourly-sync',
'0 * * * *',
$$
SELECT net.http_post(
url := 'https://[PROJECT_REF].supabase.co/functions/v1/sync-data',
headers := '{"Authorization": "Bearer [SERVICE_ROLE_KEY]"}'::jsonb,
body := '{"source": "external-api"}'::jsonb
)
$$
);
-- Dọn dẹp dữ liệu cũ mỗi ngày lúc 3:00 AM
SELECT cron.schedule(
'daily-cleanup',
'0 3 * * *',
$$
SELECT net.http_post(
url := 'https://[PROJECT_REF].supabase.co/functions/v1/cleanup',
headers := '{"Authorization": "Bearer [SERVICE_ROLE_KEY]"}'::jsonb
)
$$
);
-- Xem danh sách cron jobs
SELECT * FROM cron.job;
-- Hủy cron job
SELECT cron.unschedule('hourly-sync');
Cron & Queues — Lập lịch và Hàng đợi
pg_cron — Scheduled Jobs trong Postgres
pg_cron cho phép lập lịch thực thi SQL hoặc gọi HTTP request định kỳ ngay trong database:
-- Enable extension
CREATE EXTENSION IF NOT EXISTS pg_cron;
-- Tạo job chạy SQL mỗi phút
SELECT cron.schedule(
'cleanup-temp-tables',
'* * * * *',
'DELETE FROM temp_data WHERE created_at < now() - interval ''1 hour'''
);
-- Tạo job chạy vào 2:00 AM mỗi Chủ Nhật
SELECT cron.schedule(
'weekly-report',
'0 2 * * 0',
$$
SELECT net.http_post(
url := 'https://[REF].supabase.co/functions/v1/generate-report',
headers := '{"Authorization": "Bearer [KEY]"}'::jsonb
)
$$
);
Giới hạn: Khuyến nghị không quá 8 jobs chạy đồng thời, mỗi job không quá 10 phút.
pgmq — Message Queue trong Postgres
pgmq cung cấp message queue bền vững (persistent) ngay trong Postgres, không cần Redis hay RabbitMQ:
-- Enable extension và tạo queue
CREATE EXTENSION IF NOT EXISTS pgmq;
SELECT pgmq.create('email_queue');
-- Gửi message vào queue
SELECT pgmq.send('email_queue', '{
"to": "user@example.com",
"subject": "Welcome!",
"template": "welcome_email"
}');
-- Consumer: đọc messages (visibility timeout 60 giây)
SELECT msg_id, read_ct, enqueued_at, vt, message
FROM pgmq.read('email_queue', 10, 60);
-- Xóa message sau khi xử lý thành công
SELECT pgmq.delete('email_queue', 42); -- msg_id = 42
-- Archive message (lưu lại để debug)
SELECT pgmq.archive('email_queue', 42);
-- Xóa queue
SELECT pgmq.drop_queue('email_queue');
Kiến trúc bất đồng bộ với Edge Functions + Queue:
Client gửi request -> Edge Function nhận và gửi message vào queue
|
Cron job đọc queue mỗi phút -> xử lý từng message
|
Kết quả lưu vào database -> Client poll hoặc subscribe
AI & Vectors — Ứng dụng Trí tuệ Nhân tạo
Tổng quan
Supabase cung cấp open source toolkit để phát triển ứng dụng AI trên Postgres và pgvector. Triết lý: "Vector database tốt nhất là database bạn đã có sẵn."
pgvector — Thiết lập và Index
-- Enable extension
CREATE EXTENSION IF NOT EXISTS vector;
-- Tạo bảng lưu documents với embedding
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
embedding VECTOR(1536), -- 1536 dimensions (OpenAI text-embedding-3-small)
metadata JSONB DEFAULT '{}',
owner_id UUID REFERENCES auth.users(id),
created_at TIMESTAMPTZ DEFAULT now()
);
-- HNSW index — nhanh nhưng tốn RAM (khuyên dùng cho production)
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);
-- IVFFlat index — ít tốn RAM hơn, chậm hơn HNSW
-- CREATE INDEX ON documents
-- USING ivfflat (embedding vector_cosine_ops)
-- WITH (lists = 100);
So sánh index types:
| Index | Tốc độ | RAM | Phù hợp |
|---|---|---|---|
| HNSW | Rất nhanh | Cao | Production, dataset lớn |
| IVFFlat | Khá nhanh | Thấp | Dataset vừa, tiết kiệm RAM |
3 Loại Tìm kiếm
Supabase hỗ trợ 3 loại tìm kiếm, có thể dùng riêng hoặc kết hợp:
| Loại Search | Cơ chế | Dùng khi |
|---|---|---|
| Semantic Search | Tìm theo ý nghĩa (vector similarity) | FAQ, documentation, chatbot knowledge base |
| Keyword Search | Tìm theo từ khóa chính xác (Full Text Search) | Product catalog, exact matching |
| Hybrid Search | Kết hợp semantic + keyword | Cho kết quả toàn diện và chính xác nhất |
-- Semantic Search: tìm documents tương tự với query embedding
CREATE OR REPLACE FUNCTION match_documents(
query_embedding VECTOR(1536),
match_threshold FLOAT DEFAULT 0.78,
match_count INT DEFAULT 10
)
RETURNS TABLE (
id BIGINT,
title TEXT,
content TEXT,
similarity FLOAT
)
LANGUAGE SQL STABLE
AS $$
SELECT
id,
title,
content,
1 - (embedding <=> query_embedding) AS similarity
FROM documents
WHERE 1 - (embedding <=> query_embedding) > match_threshold
ORDER BY embedding <=> query_embedding
LIMIT match_count;
$$;
-- Keyword Search (Full Text Search)
SELECT id, title, ts_rank(to_tsvector('english', content), query) AS rank
FROM documents,
websearch_to_tsquery('english', 'supabase authentication') AS query
WHERE to_tsvector('english', content) @@ query
ORDER BY rank DESC
LIMIT 10;
// Semantic search từ JavaScript client
const { data, error } = await supabase.rpc('match_documents', {
query_embedding: embedding,
match_threshold: 0.78,
match_count: 10
})
// Keyword search từ JavaScript client
const { data, error } = await supabase
.from('documents')
.select('id, title, content')
.textSearch('content', 'supabase authentication', {
type: 'websearch',
config: 'english'
})
Generate Embeddings với Edge Functions
Sử dụng Edge Function để tạo embeddings an toàn (không expose API key):
// supabase/functions/generate-embedding/index.ts
import { createClient } from 'jsr:@supabase/supabase-js@2'
const OPENAI_KEY = Deno.env.get('OPENAI_API_KEY')!
Deno.serve(async (req: Request) => {
const { text } = await req.json()
// Gọi OpenAI Embeddings API
const response = await fetch('https://api.openai.com/v1/embeddings', {
method: 'POST',
headers: {
'Authorization': `Bearer ${OPENAI_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'text-embedding-3-small',
input: text
})
})
const { data } = await response.json()
const embedding = data[0].embedding // [1536 số float]
return new Response(JSON.stringify({ embedding }), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
})
})
RAG (Retrieval Augmented Generation)
Kiến trúc RAG cho phép LLM trả lời dựa trên dữ liệu của bạn:
1. EMBED 2. RETRIEVE 3. AUGMENT 4. GENERATE
Document ─> Query embedding ─> Tìm top-k docs ─> Context + Query ─> LLM ─> Answer
│ │ │ │
│ user query pgvector prompt:
│ -> embedding similarity "Answer based
│ (OpenAI) search on context:"
└─> Vector DB
(pgvector)
// supabase/functions/rag-query/index.ts
import { createClient } from 'jsr:@supabase/supabase-js@2'
const OPENAI_KEY = Deno.env.get('OPENAI_API_KEY')!
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
Deno.serve(async (req: Request) => {
const { query, userId } = await req.json()
// Step 1: Tạo embedding cho câu hỏi
const embedRes = await fetch('https://api.openai.com/v1/embeddings', {
method: 'POST',
headers: {
'Authorization': `Bearer ${OPENAI_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'text-embedding-3-small',
input: query
})
})
const { data: [{ embedding }] } = await embedRes.json()
// Step 2: Tìm documents liên quan (RLS-aware)
const { data: documents } = await supabase.rpc('match_documents', {
query_embedding: embedding,
match_count: 5
})
// Step 3 & 4: Gửi context + query cho LLM
const context = documents
.map(d => `[${d.title}]\n${d.content}`)
.join('\n\n---\n\n')
const chatRes = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${OPENAI_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'gpt-4o',
messages: [
{
role: 'system',
content: `Bạn là trợ lý AI. Chỉ trả lời dựa trên context được cung cấp.
Nếu context không đủ thông tin, hãy nói "Tôi không có đủ thông tin để trả lời."
Luôn trích dẫn nguồn từ title của document.`
},
{
role: 'user',
content: `Context:\n${context}\n\nCâu hỏi: ${query}`
}
],
temperature: 0.3
})
})
const answer = await chatRes.json()
return new Response(JSON.stringify({
answer: answer.choices[0].message.content,
sources: documents.map(d => ({ title: d.title, similarity: d.similarity }))
}), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
})
})
RAG với Row Level Security
Supabase hỗ trợ RAG with RLS -- đảm bảo mỗi user chỉ search được documents họ có quyền truy cập:
-- Policy: user chỉ search được documents của chính họ hoặc public
CREATE POLICY "Users can only search own docs"
ON documents FOR SELECT
USING (
auth.uid() = owner_id
OR is_public = true
);
-- Policy cho tổ chức: team members có thể search documents của team
CREATE POLICY "Team members can search team docs"
ON documents FOR SELECT
USING (
team_id IN (
SELECT team_id FROM team_members WHERE user_id = auth.uid()
)
);
Khi gọi match_documents() từ Edge Function với service role key, RLS bị bypass. Để RLS hoạt động, gọi function với user JWT:
// Dùng user's JWT thay vì service role key
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!,
{
global: {
headers: {
Authorization: `Bearer ${userJWT}`
}
}
}
)
Integrations
Supabase tích hợp với nhiều nền tảng AI/ML:
| Integration | Công dụng |
|---|---|
| OpenAI | Embeddings (text-embedding-3-small/large), ChatGPT/GPT-4o completions |
| HuggingFace | 100,000+ ML models, Inference API, open-source embeddings |
| LangChain | Framework phát triển LLM apps với vector stores, chains, agents |
| LlamaIndex | Data framework cho LLM, indexing, querying |
| Amazon Bedrock | Foundation models từ AWS (Claude, Llama, Titan) |
| Google Colab | Notebooks cho AI development, prototyping |
| Roboflow | Computer vision, object detection integration |
Vector Buckets
Loại bucket mới trong Supabase Storage dành riêng cho vector operations:
- HNSW indexing: Fast approximate nearest neighbor search
- Distance metrics: Cosine, Euclidean, Inner Product
- Metadata filtering: Lọc kết quả theo metadata kèm vector
- Local development: Hỗ trợ dev local với Supabase CLI
Compute cho AI Workloads
Vector search production cần compute cao:
- Large hoặc XL compute cho vector search production
- Cần RAM cao để HNSW index nạp vào memory
- IOPS cao cho parallel similarity queries
- Nếu dataset > 1M vectors, cân nhắc 2XL hoặc cao hơn
Bài tập
Bài tập 5: Stripe Checkout Webhook Handler
Yêu cầu:
- Tạo Edge Function nhận webhook từ Stripe khi thanh toán hoàn tất
- Verify webhook signature để đảm bảo request từ Stripe
- Khi
checkout.session.completed: cập nhật subscription status trong database - Khi
customer.subscription.deleted: đánh dấu subscription canceled - Log tất cả events vào bảng
webhook_logsđể debug - Xử lý idempotency (tránh xử lý trùng lặp)
Gợi ý triển khai:
// supabase/functions/stripe-webhook/index.ts
import { createClient } from 'jsr:@supabase/supabase-js@2'
import Stripe from 'npm:stripe'
const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY')!)
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
Deno.serve(async (req: Request) => {
// 1. Verify signature
// 2. Handle event types
// 3. Log to webhook_logs table
// 4. Return success
})
Bài tập 6: Contact Form API với Email Notification
Yêu cầu:
- Tạo Edge Function xử lý form liên hệ từ website
- Validate input (tên, email, nội dung không được trống)
- Lưu submission vào database
- Gửi email thông báo cho admin qua Resend
- Gửi email xác nhận cho người gửi
- Rate limiting: mỗi IP chỉ được gửi 3 form/giờ
- Trả về CAPTCHA token verification (Turnstile)
Bài tập 7: Semantic Search cho Knowledge Base
Yêu cầu:
- Tạo bảng
articlesvới title, content, embedding (VECTOR) - Viết Edge Function để tạo và lưu embedding khi thêm/sửa article
- Viết hàm
match_articlestrong Postgres để semantic search - Hiển thị kết quả với similarity score, highlight đoạn text liên quan
- Hỗ trợ kết hợp keyword search + semantic search (hybrid)
Gợi ý triển khai:
// supabase/functions/index-article/index.ts
// Tự động tạo embedding khi article được tạo/cập nhật
Deno.serve(async (req: Request) => {
const { article_id, title, content } = await req.json()
// Tạo embedding từ title + content
const text = `${title}\n\n${content}`
const embedding = await generateEmbedding(text)
// Lưu embedding vào article
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
await supabase.from('articles')
.update({ embedding })
.eq('id', article_id)
return new Response(JSON.stringify({ success: true }))
})
Bài tập 8: RAG Chatbot với Document Context
Yêu cầu:
- Upload PDF/text documents, chunk thành các đoạn nhỏ (500-1000 tokens)
- Tạo embedding cho từng chunk, lưu vào pgvector
- Xây dựng chat UI: user gửi câu hỏi, system tìm context + gọi LLM
- Hiển thị sources (trích dẫn document + chunk) kèm câu trả lời
- Xử lý streaming response từ LLM (hiển thị từng chữ)
- Hỗ trợ follow-up questions (giữ conversation history)
Kiến trúc gợi ý:
Upload Doc -> Chunking -> Embedding -> pgvector
|
User Query -> Embedding -> Vector Search -> Top-K Chunks
|
Context + Query -> OpenAI -> Answer + Sources