pgvector 向量入門:在 Postgres 存 embedding | Supabase 完整教學

2026/10/24
pgvector 向量入門:在 Postgres 存 embedding | Supabase 完整教學

從這一篇開始,我們正式踏進 Supabase 的 AI 與向量 領域。核心主角是 pgvector——一個讓 Postgres 學會儲存與比對「向量(vector)」的擴充功能。你會先搞懂什麼是 embedding(把一段文字變成一串數字,讓「語意」變得可以計算)、如何用 create extension vector 啟用它、用 vector(1536) 這種欄位把 embedding 存進資料表,再用三個距離運算子(<-> L2、<=> cosine、<#> 內積)找出「意思最接近」的資料。這是 AI/向量段 的第一篇,替之後的向量搜尋與 RAG 打好地基。

前言

一句話定義本篇主角:pgvector 是 Postgres 的一個擴充功能,它新增了一種叫 vector 的欄位型別和一組「距離運算子」,讓你能把 AI 模型產生的 embedding(一串浮點數)存進資料庫,並用 <->、<=>、<#> 這些運算子算出「哪兩筆資料語意最接近」。

先接住上一篇《串流回應與 Webhooks 實戰》的脈絡。運算篇章的 Edge Functions 段收尾後,我們把視線從「運算層」轉回「資料庫層」——因為 AI 應用最核心的資料(向量)就住在這裡。

那到底什麼是 向量(vector) 和 embedding?先給一個直覺。傳統資料庫擅長「精確比對」:where title = 'Supabase 教學',一字不差才找得到。但人類的語言不是這樣運作的——「怎麼在 Postgres 存 AI 向量」和「pgvector 如何儲存 embedding」用字完全不同,意思卻幾乎一樣,關鍵字搜尋卻會判定它們無關。embedding 就是為了解決這件事:它透過一個 AI 模型,把一段文字(或圖片、音訊)轉換成一串固定長度的浮點數,例如 [0.021, -0.135, 0.087, ...](共 1536 個數字)。這串數字的神奇之處在於——意思越接近的內容,產生的數字串在「空間中的位置」就越靠近。於是「找語意相似的資料」這個模糊問題,就變成了「找空間中最接近的點」這個可以用數學精確計算的問題。

用一個類比。把每段文字想成地圖上的一個座標點:一般 2D 地圖用「經度、緯度」兩個數字定位一個城市;embedding 則是用「1536 個數字」在一個1536 維的空間裡定位一段文字的「意思」。維度多到人腦無法想像,但數學不在乎——它照樣能算「台北到高雄」的距離。語意相近的文字,就像地理位置相近的城市,彼此距離很短。而 pgvector 做的事,就是「把這些高維座標點存進 Postgres,並提供計算兩點距離的運算子」。

本篇你會學到:

  • 向量與 embedding 是什麼:為什麼「一串數字」能代表「意思」,以及維度(dimension)的概念
  • 啟用與建表:用 create extension vector 開啟 pgvector、用 vector(1536) 建立向量欄位、把 embedding 存進去
  • 三個距離運算子:<->(L2)、<=>(cosine)、<#>(負內積)分別量什麼、何時該用哪一個
  • 維度一致的鐵律:為什麼欄位維度必須和 embedding 模型完全一致,以及 Supabase 為何適合把向量放進 Postgres

本篇聚焦「向量入門與儲存」;embedding 的實際生成(呼叫 OpenAI/開源模型)、向量索引(HNSW/IVFFlat)、以及完整 RAG 流程,留給後續文章。以下擴充名稱、型別行為與運算子細節請以官方文件為準。

核心概念

要把 pgvector 用對,先建立三張心智圖:向量與 embedding「是什麼、維度是什麼」、vector 型別「怎麼在 Postgres 存這串數字」、距離運算子「怎麼算出誰最相似」。

一、向量與 embedding:把「意思」變成可計算的座標

embedding(嵌入向量) 是一個 AI 模型的產物。你餵它一段文字,它吐出一個固定長度的浮點數陣列:

"Supabase 是開源的 Firebase 替代方案"
        │
        ▼   (embedding 模型,例如 text-embedding-3-small)
        │
[0.021, -0.135, 0.087, 0.004, ..., -0.052]   ← 共 1536 個數字

這串數字就是這段文字在「語意空間」中的座標。它有兩個關鍵性質:

  • 固定長度:同一個模型,不管你餵多長的文字,輸出的維度都一樣(例如永遠是 1536 個數字)。這個長度就叫 維度(dimension)。
  • 語意即距離:意思相近的兩段文字,它們的向量在空間中「靠得近」;意思無關的則「離得遠」。這正是語意搜尋能運作的根本。

不同模型的維度不同,這是你之後建表時必須知道的關鍵數字:

模型維度備註
text-embedding-3-small(OpenAI)1536最常用,性價比高,可用參數縮短
text-embedding-3-large(OpenAI)3072品質更高,可縮短至 512
gte-small(Hugging Face)384開源免費,維度小、查詢快
voyage-3(Voyage AI)1024針對 RAG 任務最佳化

記住這張表最重要的一句話:你選哪個模型,就決定了向量欄位要開幾維。

二、vector 型別:Postgres 怎麼存這串數字

原生 Postgres 沒有「向量」這種東西——你頂多把它塞進 float[] 陣列,但那樣既不能高效比對距離、也不能建向量索引。pgvector 這個擴充功能補上了這一塊:啟用後,Postgres 就多了一個 vector(n) 型別,n 是維度數。

create extension vector          ← 啟用擴充,Postgres 從此認得 vector 型別
        │
        ▼
embedding vector(1536)           ← 宣告一個欄位,每筆存 1536 個浮點數
        │
        ▼
'[0.021, -0.135, ...]'::vector   ← 一個向量字面值長這樣(中括號包起來)

有一個容易搞混的小細節先講清楚:擴充功能的名稱是 vector,但這個「專案」叫 pgvector。所以你在 SQL 裡寫的是 create extension vector,而在文件、社群裡聽到的都是「pgvector」——兩個是同一件事的兩個名字。

除了標準的 vector(n),pgvector 還有一個省記憶體的變體 halfvec(n):它用 16-bit 半精度浮點數存,約省一半記憶體,適合資料量很大時。入門階段先用 vector(n) 就好,halfvec 是之後最佳化的選項。

三、距離運算子:怎麼算出「誰最相似」

向量存進去之後,「找相似」就是「找距離最近的向量」。pgvector 提供三個距離運算子,各對應一種數學度量:

運算子名稱量什麼值域適用情境
<->L2 / 歐幾里得距離兩點的直線距離0 ~ ∞向量未正規化、在意絕對位置
<=>cosine / 餘弦距離兩向量「方向」的差異(忽略長度)0 ~ 2最安全的預設,語意搜尋常用
<#>negative inner product / 負內積內積的負值-∞ ~ ∞已正規化向量,速度最快

三者的白話理解:

  • <->(L2) 像量「地圖上兩城市的直線公里數」——量的是絕對位置差。
  • <=>(cosine) 只在乎兩個向量指的方向像不像,不管它們有多長。這在語意搜尋裡最直覺(我們在意「意思方向」而非「文字長短」),而且不管向量有沒有正規化都能用,所以是最安全的預設值。
  • <#>(負內積) 是三者裡計算最快的,但它只在向量已經正規化(長度為 1)時才等價於餘弦距離。它回傳「內積取負」而非內積本身——這是 pgvector 刻意設計的,好讓三個運算子都遵守同一個排序邏輯:值越小 = 越相似,一律 order by ... asc 就對了。

還有一個實務上一定會用到的換算——餘弦相似度(cosine similarity)= 1 − 餘弦距離。距離越小越相似,但一般人更習慣「相似度越大越相似」的直覺,所以查詢時常會寫 1 - (embedding <=> query) 把距離換算成 0~1 的相似度分數。

實作範例

下面用一段可直接在 Supabase SQL Editor 執行的 SQL,走一遍「啟用 pgvector → 建向量表 → 插入 embedding → 用 <=> 找最相似」的完整最小流程。為了讓你能親手跑通、看得懂數字,範例刻意用 3 維 的迷你向量(真實世界是 1536 維,原理完全相同,只是數字多而已)。

一、啟用 pgvector 擴充功能

第一步是讓 Postgres 認得 vector 型別。Supabase 建議把擴充裝在 extensions schema 下(保持 public 乾淨):

-- 啟用 pgvector 擴充功能(擴充名叫 vector,專案名才叫 pgvector)
create extension if not exists vector with schema extensions;

if not exists 讓這行可重複執行不報錯。在 Supabase Dashboard,你也可以走 Database → Extensions,搜尋「vector」再按啟用,效果一樣。

二、建立含向量欄位的資料表

接著建一張文件表,重點在 embedding 欄位——這裡先用 3 維方便示範,正式用 OpenAI text-embedding-3-small 時就把 vector(3) 改成 vector(1536):

-- 建立文件表,embedding 欄位用來存向量
create table documents (
  id bigint primary key generated always as identity,
  content text not null,                 -- 原始文字
  embedding vector(3),                    -- 向量欄位:示範用 3 維
  created_at timestamptz default now()
);

正式環境請把 vector(3) 換成 vector(1536)(對應 text-embedding-3-small)。欄位維度必須等於你所用 embedding 模型的輸出維度,這是最重要的一條規則。

三、插入 embedding

真實情境下,embedding 是呼叫 embedding 模型(下一篇會講)得到的 number[] 陣列。這裡我們手動塞幾筆已算好的 3 維向量進去。注意向量字面值的寫法:用中括號包住、單引號成字串、再 ::vector 轉型:

-- 插入幾筆文件與它們的(示範用)向量
insert into documents (content, embedding) values
  ('貓咪是一種可愛的寵物',       '[0.9, 0.1, 0.0]'::vector),
  ('小狗喜歡出門散步',           '[0.8, 0.2, 0.0]'::vector),
  ('股票市場今天大跌',           '[0.0, 0.1, 0.9]'::vector),
  ('央行宣布升息半碼',           '[0.0, 0.2, 0.8]'::vector);

觀察一下這些數字:前兩筆(寵物)的向量彼此靠近、後兩筆(財經)也彼此靠近,而寵物與財經之間離得遠——這就模擬了「語意相近則向量相近」。

四、用 <=> 找最相似的資料

現在來查詢:假設使用者的問題經過 embedding 模型算出來是 [0.85, 0.15, 0.0](偏向「寵物」那一側),我們用餘弦距離 <=> 找出最接近的文件,並把距離換算成相似度分數:

-- 用餘弦距離 <=> 找出與查詢向量最相似的前 3 筆
select
  content,
  embedding <=> '[0.85, 0.15, 0.0]'::vector          as distance,     -- 距離:越小越相似
  1 - (embedding <=> '[0.85, 0.15, 0.0]'::vector)     as similarity    -- 相似度:越大越相似
from documents
order by embedding <=> '[0.85, 0.15, 0.0]'::vector    -- 依距離由小到大排序
limit 3;

預期結果會是兩筆「寵物」文件排在最前面(距離最小、相似度最高),財經文件則排在後面——即使查詢字串 [0.85, 0.15, 0.0] 跟任何一筆都不完全相等,pgvector 依然靠「距離」找出了語意最接近的資料。這就是語意搜尋的雛形。

想改用其他運算子也只要換符號。例如向量已正規化、追求速度時可改用負內積 <#>:

-- 已正規化向量可用負內積 <#>(最快);排序邏輯一樣是「越小越前面」
select content, embedding <#> '[0.85, 0.15, 0.0]'::vector as neg_ip
from documents
order by embedding <#> '[0.85, 0.15, 0.0]'::vector
limit 3;

注意:本篇為了聚焦入門,查詢用的是全表掃描(線性比對每一筆)。資料量小時這樣完全沒問題;當資料成長到十萬、百萬筆,就需要**向量索引(HNSW/IVFFlat)**來加速——那是後續文章的主題。

常見錯誤與最佳實踐

坑一:欄位維度和 embedding 模型不一致。 建表時寫了 vector(1536),卻插入一個 384 維(gte-small)的向量,Postgres 會直接報 expected 1536 dimensions, not 384 而插入失敗。正確做法:先確定要用哪個模型、查清它的輸出維度,再據此宣告欄位——OpenAI text-embedding-3-small → vector(1536)、gte-small → vector(384)、voyage-3 → vector(1024)。若用 OpenAI dimensions 參數縮短過向量,欄位維度也要跟著改。

坑二:同一欄混用不同模型產生的向量。 就算兩個模型維度湊巧相同,它們的向量空間座標系也不一樣,算出的距離毫無意義,搜尋結果等於亂數。正確做法:同一個向量欄位裡的所有向量,必須來自同一個模型;查詢用的 query embedding 也要用同一個模型產生。若決定換模型,就把整批資料重新 embed 一次,不能新舊混放。

坑三:混用或搞錯距離運算子。 <->、<=>、<#> 量的東西不同,隨便挑會得到怪結果;更隱蔽的是建索引時用的運算子類別和查詢時用的運算子不一致(例如索引用 vector_l2_ops、查詢卻用 <=>),這會讓索引完全不生效、悄悄退回全表掃描。正確做法:不確定向量是否正規化就一律用 <=>(cosine,最安全);之後建索引時,運算子類別要和查詢運算子對齊(<=> 配 vector_cosine_ops、<-> 配 vector_l2_ops、<#> 配 vector_ip_ops)。

坑四:以為 <#> 回傳的是正的內積、或忘了向量沒正規化。 <#> 回傳的是內積的負值,且只有在向量已正規化(長度為 1)時才等價於餘弦距離。若你的向量沒正規化就用 <#>,結果會被向量長度干擾而失準。正確做法:不確定有沒有正規化,就用 <=>(它天生不受長度影響);確定已正規化、且要壓榨效能,才用 <#>。

坑五:向量字面值格式寫錯。 把向量寫成 (0.1, 0.2, 0.3)(小括號)或忘了轉型,會遇到 operator does not exist: vector <=> unknown 之類的錯誤。正確做法:向量字面值用中括號、包成字串、明確轉型——'[0.1, 0.2, 0.3]'::vector。從程式端(Supabase JS/Python SDK)插入時,直接傳 number[] 陣列即可,SDK 會處理轉換。

最佳實踐總結:

  • 維度對齊模型:先定模型、查維度,再宣告 vector(n);縮短維度就同步改欄位
  • 一欄一模型:同欄向量只能來自同一模型,換模型就整批重 embed
  • 預設用 <=>:不確定正規化與否就用餘弦距離,最安全;索引運算子類別務必和查詢運算子對齊
  • 相似度換算:1 - (embedding <=> query) 把餘弦距離換成 0~1 的相似度分數
  • 入門先不建索引:小資料集全表掃描即可;十萬筆以上再上 HNSW/IVFFlat
  • 善用 Postgres 既有能力:向量和關聯資料同庫,where 過濾 + 向量排序一次搞定,並可用 RLS 做多租戶隔離
  • 細節以官方為準:vector/halfvec 行為、運算子值域、擴充安裝方式都可能調整,正式決策前查官方文件

小結

這是 Supabase 系列教學的第 035 篇,也是 SB-4 運算與 AI 篇章中 AI/向量段 的開篇。上一篇《串流回應與 Webhooks 實戰》我們收尾了 Edge Functions 段,讓函式學會串流與被事件驅動;這一篇則把視線帶回資料庫層,認識讓 Postgres 具備 AI 能力的關鍵——pgvector。

核心觀念濃縮成一句話:embedding 是把「一段文字的意思」轉成「一串固定長度的浮點數座標」,讓「找語意相似」變成「找空間中最近的點」;pgvector 用 create extension vector 啟用後,就能用 vector(n) 欄位儲存 embedding(維度 n 必須等於模型輸出),並用三個距離運算子做相似度搜尋——<-> 是 L2、<=> 是 cosine(最安全的預設)、<#> 是負內積(已正規化時最快),三者一律「值越小越相似」。

  • 向量與 embedding:固定維度的浮點數陣列,語意相近則向量相近
  • 啟用與建表:create extension vector、embedding vector(1536)、'[...]'::vector 插入
  • 距離運算子:<->(L2)、<=>(cosine,預設)、<#>(負內積,最快)
  • 維度鐵律:欄位維度必須等於模型輸出維度,且同欄不可混用不同模型

Supabase 之所以適合把向量放進 Postgres,關鍵在於統一管理:向量和它的關聯資料、交易、備份、RLS 全都在同一個資料庫裡,一次 SQL 就能「先過濾再排序」,還能靠 RLS 做多租戶隔離——這是外掛式向量庫難以乾淨達成的。

有了向量的儲存與比對基礎,下一篇《embeddings 與相似度搜尋》,我們要補上真正「產生 embedding」的那一步——如何呼叫 OpenAI 或開源模型把文字變成向量、如何把相似度查詢封裝成可從前端呼叫的函式,讓語意搜尋真正跑起來。AI/向量段,才剛開始。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。

本週主打

AI 自動化入門包

你每天手動在做的那些煩事,其實 AI 可以自己跑。這份給你 10 個照著做就會的自動化工作流 + 50 個複製即用的提示詞,不用會寫程式。

看看這個產品 →