歡迎來到第十天!你應該多少有發現第二週開始之後每天文章的內容變少了些,原因也相當簡單,整個專案我只有第一週是我熟悉的領域,後面開始我就是跟你們一樣邊做邊學了,即便在多個 AI 模型的協助下,研究跟文獻參考還是相當費時的工作,更遑論在這個領域中資訊變化得太快, AI 模型根本跟不上,比方說前兩天的腳本的內容都是請 AI 先產出初稿,我再研讀後對照官方文件修正,否則它堅持一些過時的資訊與語法導致錯誤百出,由於也是第一次深入的領域,我並沒有能力光頻閱讀就判斷它們是否產生幻覺,也只能在測試與對照文件的過程中慢慢揀起來我需要的觀念,因此每天的文章我都需要預留很大一部分的時間做內容的驗證,以防有一些我預期之外的意外發生(像是昨天的餘弦相似度不知道為什麼那個模型就是一直出錯),自然在內容上我就會有所縮減當作緩衝囉!
回到我們的進度,昨天我們在本地用一個 Node.js 腳本,成功驗證了可以透過「餘弦相似度」來量化語意的接近程度。這很酷,但我們心裡都清楚,昨天的作法有一個致命傷:每一次比較,都需要重新呼叫 API 產生所有文字的 Embedding。如果我們的知識庫只有 5 條 keyPoints,這不是問題。但如果我們有 500、甚至 5000 條呢?這會讓我們的應用變得極度緩慢且昂貴。這就好像每次你想找書,都要先把整座圖書館的書名都讀一遍一樣,你肯定不想為了這樣的東西付費
我們需要一個更專業的方案:一個能「預先儲存」所有 Embedding 向量,並且能「高速查詢」相似向量的「向量資料庫」。
市面上有 Pinecone, Weaviate 等許多專業的向量資料庫,但它們對新手來說可能有點複雜。因此,我選擇了一個對前端工程師極其友善的工具:Supabase。
老實說,Supabase 我也是第一次使用。但它吸引我的地方在於:
今天,就讓我們一起從零開始,踏出建立 AI 知識庫的第一步,為我們的 AI 面試官打造一個可用的向量資料庫吧,會帶到一點後端的東西,如果你是純前端仔,那現在正是好機會一起多多練習!多掌握一些能力對你來說不會吃虧的!
首先,前往 Supabase 官網 並用你的 GitHub 帳號登入。對工程師來說,這應該是最快的方式了。
登入後,點擊首頁的「Start Your Project」=>「New Project」進入專案新增畫面。
你需要設定幾項資訊:
![]() |
---|
圖1 :建立你的 Supabase 專案 |
按下「Create new project」後,稍等幾分鐘,Supabase 就會為你準備好一個功能齊全的後端資料庫,現在的平台真是不簡單,一個比一個方便。
專案建立完成後,從左側導覽列找到資料庫圖示的「Database」,然後點擊「Extensions」。它就像是 PostgreSQL 的 App Store,可以為你的資料庫增加各種強大的外掛。
在搜尋框中輸入 vector,你應該就能看到 pgvector。它就是我們今天的主角,負責處理所有向量相關的儲存與計算。勇敢地點下開關啟用它吧,右上角跳出成功的通知就表示完成囉!
![]() |
---|
圖2 :在 Extensions 中啟用 pgvector |
啟用成功後,我們的資料庫就正式擁有了處理向量的能力!
有了向量能力,我們還需要一個地方來存放資料。讓我們點擊左側導覽列的「SQL Editor」,我們要用最經典的 SQL 語法來建立我們的表格。
點擊「New query」,將下方的 SQL 語法貼上:
-- 建立一個名為 documents 的表格來儲存我們的知識點
create table documents (
id bigserial primary key, -- 自動增長的唯一 ID
content text, -- 儲存原始的 keyPoint 文字內容
embedding vector(768), -- 儲存語意向量,768 必須和我們 Embedding 模型的維度一致!
question_id text -- 關鍵欄位:用來連結回原始 questions.json 中的問題 ID
);
這段 SQL 做了幾件重要的事:
pgvector
擴充提供的!768 這個數字必須與我們 Day 8 和 Day 9 使用的 Gemini text-embedding-004
& gemini-embedding-001
模型輸出的維度完全一致。keyPoint
) 與其所屬的原始問題綁定。它確保了 AI 在評價時,檢索的參考資料永遠被限定在當前題目的範圍內,實現精準、公平的評測。確認無誤後,點擊右下角的「RUN」按鈕。很快地,我們的第一張知識庫表格就誕生了!
![]() |
---|
圖3 :建立我們的向量資料表 |
表格建好了,現在該把 questions.json 裡的知識點放進去了。我們來寫一個獨立的 Node.js 腳本,專門用來處理這件事。
在你的專案終端機中,執行:
npm install @supabase/supabase-js dotenv
dotenv 可以讓我們的 Node.js 腳本讀取 .env.local 檔案,方便管理金鑰,雖然是本地一次性的腳本,就算用明碼也沒什麼關係,但我反覆橫跳後還是覺得裝一下會比較方便。
先回到 Supabase 專案主畫面,往下拉會看到專案的Url 與 API key,我們首先需要專案url。
![]() |
---|
圖4 :取得專案Url |
接著,進入左側的「Project Settings」->「API Keys」,他們最近似乎更新了介面,請你點選右邊的 API Keys 的 Tab 並點擊「Create new API Keys」的按鈕,完成後你應該會看到這樣的畫面。
![]() |
---|
圖5 :取得專案API Keys |
將這兩個值複製後,加入到你專案根目錄的 .env.local 檔案中:
# .env.local
GEMINI_API_KEY=...
SUPABASE_URL=YOUR_SUPABASE_PROJECT_URL
SUPABASE_SERVICE_KEY=YOUR_SUPABASE_SERVICE_ROLE_KEY
在專案根目錄建立一個新資料夾 scripts,並在其中建立一個檔案 seed-vector.js
。貼上以下程式碼:
// scripts/seed-vector.js
import { createClient } from '@supabase/supabase-js';
import { GoogleGenAI } from '@google/genai';
import dotenv from 'dotenv';
import questions from '../data/questions.json' with { type: "json" };
// 讓 node.js 腳本可以讀取 .env.local
dotenv.config({ path: './.env.local' });
async function seed() {
// 1. 初始化客戶端
const gemini = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_KEY
);
// 2. 準備要上傳的資料 (以 hoisting 問題的第一個 keyPoint 為例)
const firstQuestion = questions.find((q) => q.id === 'js-con-001');
if (!firstQuestion || !firstQuestion.keyPoints.length) {
console.error('找不到範例題目或 KeyPoint');
return;
}
const keyPointToSeed = {
questionId: firstQuestion.id,
content: firstQuestion.keyPoints[0],
};
console.log(`準備處理 KeyPoint: "${keyPointToSeed.content}"`);
try {
// 3. 產生 Embedding
console.log('正在產生 Embedding...');
const response = await gemini.models.embedContent({
model: 'gemini-embedding-001',
contents: keyPointToSeed.content,
config: {
outputDimensionality: 768,
},
});
const embedding = response.embeddings[0].values;
console.log(`Embedding 產生成功,維度: ${embedding.length}`);
// 4. 將資料寫入 Supabase
console.log('正在寫入 Supabase...');
const { data, error } = await supabase
.from('documents')
.insert({
content: keyPointToSeed.content,
embedding: embedding,
question_id: keyPointToSeed.questionId,
})
.select(); // .select() 會將插入的資料回傳
if (error) {
throw new Error(`寫入 Supabase 失敗: ${error.message}`);
}
console.log('🎉 成功將第一筆向量資料寫入 Supabase!');
console.log('寫入的資料:', data);
} catch (error) {
console.error('處理過程中發生錯誤:', error.message);
}
}
seed();
這個腳本的流程非常清晰:初始化客戶端 -> 從 questions.json 讀取範例資料 -> 呼叫 Gemini API 產生向量 -> 呼叫 Supabase Client 將原文、向量和問題 ID 一起寫入 documents 表格。
最後,在終端機中執行:
node scripts/seed-vector.js
如果一切順利,你應該會看到成功訊息!
準備處理 KeyPoint: "變數和函數宣告會被提升到其作用域的頂部"
正在產生 Embedding...
Embedding 產生成功,維度: 768
正在寫入 Supabase...
🎉 成功將第一筆向量資料寫入 Supabase!
現在回到 Supabase 的「Table Editor」,點開你的 documents 表格,你就會看到我們第一筆 AI 知識庫的紀錄已經靜靜地躺在那裡了!
![]() |
---|
圖6 :第一筆向量資料 |
補充說明:
在測試時,我發現我們昨天選用的模型gemini-embedding-001
實際上預設的維度是3072,根據官方的說法截斷維度大小不太會影響品質且可以維持較低的成本,因此你會看到今天的腳本中有一段設定輸出維度的部分。
順帶一提,官方給的腳本是錯誤的,你照下方的寫法是無法指定輸出維度的,真有你的估狗!
import { GoogleGenAI } from "@google/genai";
async function main() {
const ai = new GoogleGenAI({});
const response = await ai.models.embedContent({
model: 'gemini-embedding-001',
content: 'What is the meaning of life?',
outputDimensionality: 768,
});
const embeddingLength = response.embedding.values.length;
console.log(`Length of embedding: ${embeddingLength}`);
}
main();
呼!今天終於也是撐過去了,測試時出了一堆問題,把短短的文章拖了超多時間才寫完, supabase 你不要一口氣大改介面啊,AI 助理會混亂的! 回顧一下今天幹了什麼吧!
✅ 我們成功建立了 Supabase 專案並啟用了 pgvector。
✅ 我們學會了如何用 SQL 語法定義一個包含 vector 型別與 question_id 的表格。
✅ 我們透過 Node.js 腳本,成功將 Gemini 產生的 Embedding 寫入了雲端資料庫。
✅ 我們完美達成了 RAG 藍圖中「事前準備」階段的第一步!
資料存進去只是第一步,真正的魔法在於如何「取出來」。明天(Day 11),我們將深入 Supabase 的另一個強大功能:資料庫函式 (Database Functions)。我們將會撰寫一個專門的 SQL 函式來執行向量的「相似度搜尋」,完成 RAG 流程中最關鍵的檢索 (Retrieval) 步驟!
我們明天見!🚀
今日程式碼: https://github.com/windate3411/Itiron-2025-code/tree/day-10