學習如何使用 pgvector 在 PostgreSQL 實現向量搜索。本文涵蓋 Embedding 原理、餘弦相似度與歐幾里德距離、IVFFlat 與 HNSW 索引比較、向量資料庫選型建議,並提供完整的 Bun/TypeScript 實作範例,適合開發者快速上手 AI 驅動的語義搜索。
Written by: Chia1104 CC BY-NC-SA 4.0

向量搜索(Vector Search)本質上是在多維空間中尋找相似點的過程。它將複雜的資料(文字、圖片、音訊)轉換成數學向量,透過計算向量間的距離來判斷相似度。
Embedding 是經過正規化的單位向量,通常由 AI 模型生成,每個數值介於 -1 到 1 之間。例如,OpenAI 的 text-embedding-3-small 模型會將一段文字轉換成 1536 維的向量陣列。這些向量能捕捉語義資訊,使得「台北是首都」和「台灣的行政中心」在向量空間中距離很近。
通常做向量搜尋有下面幾種方式:
餘弦相似度是一種常用的衡量兩個向量方向相似度的指標。其公式如下:

餘弦相似度公式
餘弦距離公式
餘弦距離是餘弦相似度的補數,用於衡量兩個向量之間的距離:
應用場景: 文本相似度比對、推薦系統、RAG(Retrieval-Augmented Generation)。
歐幾里德距離測量多維空間中兩點的直線距離,是最直觀的距離度量方式,適合需要考慮向量絕對大小的場景。

歐幾里德距離公式
值域: 0 到 ∞,數值越小表示越相似。 應用場景: 圖像特徵匹配、數值資料比對、異常檢測。
在向量資料庫的選型上,pgvector 作為 PostgreSQL 的擴展,具有以下核心優勢:
| 優勢 | 說明 |
|---|---|
| 無縫整合 | 與現有關聯式資料庫無縫結合,無需引入新的資料庫系統 |
| SQL 原生支援 | 使用熟悉的 SQL 語法進行向量查詢,學習成本低 |
| 混合查詢 | 同時支援向量相似度搜索和傳統 SQL 條件過濾 |
| 成熟生態 | 基於 PostgreSQL 的穩定性和豐富的工具鏈 |
| 成本效益 | 開源免費,無需額外的向量資料庫訂閱費用 |
索引維度上限: pgvector 的索引功能最多支援 2000 維向量。這是由於 PostgreSQL 預設頁面大小(8KB)的限制。雖然可以儲存更高維度的向量,但無法為其建立索引,會導致查詢效能大幅下降。
效能: 在超大規模(十億級)向量檢索場景下,效能不如專用向量資料庫如 Milvus。
若需使用超過 2000 維的向量模型(如 OpenAI 的 text-embedding-3-large),可採用以下方案:
中小型專案: 向量數量在千萬級以內,對延遲要求不極端苛刻。 混合查詢需求: 需要結合結構化資料(如使用者 ID、時間戳)和向量相似度進行複雜查詢。 快速原型開發: 團隊已熟悉 PostgreSQL,希望快速驗證 AI 功能。 預算受限: 無法負擔專用向量資料庫的運營成本。
安裝pgvector擴展
CREATE EXTENSION IF NOT EXISTS vector;這裡我們可以直接用 pgvector 的 image,起來後再執行該方法。
創建包含向量的表
根據需求選擇 OpenAI 的 Embedding 模型:
| 模型 | 維度 | 可調整 | 適用場景 |
|---|---|---|---|
| text-embedding-3-small | 1536(可調至 512) | 是 | 通用文本檢索,性價比最高 |
| text-embedding-3-large | 3072(可調至 256) | 是 | 高精度需求,複雜語義 |
| text-embedding-ada-002 | 1536 | 否 | 舊版模型,不建議新專案使用 |
重要提醒: text-embedding-ada-002 已於 2025 年 1 月標記為不推薦使用(deprecated),預計於 2025 年 6 月後正式淘汰。建議新專案直接使用 text-embedding-3-small 或 text-embedding-3-large,可獲得更高精度。
這裡要注意 pgvector 的索引功能最多只支援 2000 維。若使用 text-embedding-3-large 的預設 3072 維,建議透過 API 的 dimensions 參數調整至 1536 或更低。
CREATE TABLE items (
id serial PRIMARY KEY,
embedding vector(1536) -- 假設每個向量有 1536 維
);插入向量數據
INSERT INTO items (embedding) VALUES
('[0.1, 0.2, ..., 0.3]'),
('[0.4, 0.5, ..., 0.6]'),
...;執行向量搜索
使用餘弦相似度來查詢最相似的向量:
SELECT id, embedding
FROM items
ORDER BY embedding <=> '[query_vector]' -- pgvector 內建的餘弦相似度操作符
LIMIT 10;若是用歐幾里德距離做計算的話可以換成 <->
pgvector 支援兩種索引類型:
CREATE INDEX ON documents
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);索引效能比較(基於 100 萬向量基準測試):
| 指標 | IVFFlat | HNSW |
|---|---|---|
| 建立時間(秒) | 128 | 4,065 |
| 索引大小(MB) | 257 | 729 |
| 查詢速度(QPS) | 2.6 | 40.5 |
| 召回率穩定性 | 中等 | 高 |
IVFFlat 的召回率會隨著資料量增長而下降,需要定期重建索引。
-- 推薦的生產環境設定
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (
m = 16, -- 預設值,適合大多數場景
ef_construction = 64 -- 預設值,平衡建立速度與品質
);
-- 查詢時動態調整精度(不需重建索引)
SET hnsw.ef_search = 100; -- 範圍 80-120,數值越高召回率越高但速度越慢這裡我們用 OpenAI 的 model 來計算 embeddings,再透過 pgvector 來計算相似度。
這裡我們用 text-embedding-ada-002 做計算 embeddings 的 model
import OpenAI from "openai";
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY ?? "st-SECRETKEY",
});
// text-embedding-ada-002, text-embedding-3-small, text-embedding-3-large
const MODEL = "text-embedding-ada-002";
export const generateEmbedding = async (value: string) => {
const input = value.replaceAll("\n", " ");
const { data } = await openai.embeddings.create({
model: MODEL,
input,
// 這裡可以設定 output 的維度,但只有 `text-embedding-3` 可以調整
dimensions: 1536,
});
return data[0]?.embedding;
};我們這裡在定義初始化資料庫的指令,最後再透過 餘弦相似度 來查詢台灣的首都。
import { SQL } from "bun";
import { faker } from "@faker-js/faker";
import { generateEmbedding } from "./embeddings";
import pgvector from "pgvector";
const sql = new SQL({ url: process.env.DATABASE_URL });
const initDb = async () => {
/**
* Create extension if not exists
*/
await sql`CREATE EXTENSION IF NOT EXISTS vector;`;
/**
* Create table if not exists
*/
await sql`CREATE TABLE IF NOT EXISTS documents (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
embedding VECTOR(1536) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);`;
};
const seedDb = async () => {
const documents = await Promise.all(
Array.from({ length: 3 }, async () => {
const content = faker.lorem.paragraph();
return {
title: faker.lorem.sentence(),
content,
embedding: pgvector.toSql(await generateEmbedding(content)),
};
})
);
await sql`INSERT INTO documents ${sql(documents)}`;
console.log("Database seeded successfully");
};
const seedDbWithCapitals = async () => {
const documents = await Promise.all([
{
title: "Taiwan",
content: "The capital of Taiwan is Taipei",
embedding: pgvector.toSql(await generateEmbedding("The capital of Taiwan is Taipei")),
},
{
title: "Japan",
content: "The capital of Japan is Tokyo",
embedding: pgvector.toSql(await generateEmbedding("The capital of Japan is Tokyo")),
},
{
title: "United States",
content: "The capital of United States is Washington, D.C.",
embedding: pgvector.toSql(await generateEmbedding("The capital of United States is Washington, D.C.")),
},
]);
await sql`INSERT INTO documents ${sql(documents)}`;
console.log("Database seeded successfully");
};
const searchDb = async (query: string) => {
const embedding = await generateEmbedding(query);
if (!embedding) {
throw new Error("Embedding generation failed");
}
const sqlEmbedding = pgvector.toSql(embedding);
const results = await sql`SELECT * FROM documents ORDER BY embedding <=> ${sqlEmbedding} LIMIT 3`.values();
return results;
};
const demo = async (search = "What is the capital of Taiwan?", seed?: true | "capitals") => {
await initDb();
if (seed) {
if (seed === "capitals") {
await seedDbWithCapitals();
} else {
await seedDb();
}
}
const results = await searchDb(search);
console.log(results);
};
const Script = {
init: async (seed?: true | "capitals") => {
await initDb();
if (seed) {
if (seed === "capitals") {
await seedDbWithCapitals();
} else {
await seedDb();
}
}
},
initDb,
seedDb,
searchDb,
demo,
};
export default Script;最後在執行
import Script from "./src/scripts";
await Script.demo(
"What is the capital of Taiwan?",
// seed the database or not
"capitals"
);就可以看到結果
[
[ 4, "Taiwan", "The capital of Taiwan is Taipei", "[0.000814143,-0.019345103,-0.0027284368,...,-0.02137615]",
2025-05-08T08:29:22.591Z
], [ 7, "Japan", "The capital of Japan is Tokyo", "[0.000814143,-0.019345103,-0.0027284368,...,-0.02137615]",
2025-05-08T08:29:48.303Z
], [ 6, "United States", "The capital of United States is Washington, D.C.", "[0.0072439546,-0.01591172,-0.016960844,...,-0.0050426666]",
2025-05-08T08:29:22.591Z
], count: 3, command: "SELECT"
]除了上面所提的 pgvector,還有另外兩個也是專門做像量資料處理的資料庫,分別是 Milvus 跟 Weaviate,不過他們各有不同的優勢,大家可以針對不同的情境做使用。
pgvector 就如同前面所提的,我覺得他最大的優勢就是較好跟現有的關聯資料做整合,並且用一般的 SQL 語法就可以做到文本搜尋,上手曲線相對簡單,不過當然整體的功能就不如 Milvus 或 Weaviate 來得多了,並且他索引的最大維度只支援到 2000,若有更多資料就會不好做優化。
Milvus 是一個開源的專用向量資料庫,專為大規模高維向量相似度搜尋和 AI 應用設計,支援十億級向量。
優點
缺點
Weaviate,是一個開源的向量資料庫,支援語義搜尋和混合搜尋(向量 + 關鍵字),並以 GraphQL 介面為特色。
import { dataType, type WeaviateClient } from "weaviate-client";
import { vectorizer } from "weaviate-client";
export class Script {
constructor(private client: WeaviateClient) {}
/**
* 直接透過 OpenAI 幫忙做即時的向量轉換
*/
private async createCollection() {
await this.client.collections.create({
name: "Documents",
vectorizers: vectorizer.text2VecOpenAI(),
properties: [
{ name: "title", dataType: dataType.TEXT },
{ name: "content", dataType: dataType.TEXT },
],
});
}
async getDocuments() {
return this.client.collections.get("Documents");
}
private generateData() {
return [
{
title: "Taiwan",
content: "The capital of Taiwan is Taipei.",
},
{
title: "Japan",
content: "The capital of Japan is Tokyo.",
},
{
title: "United States",
content: "The capital of the United States is Washington, D.C.",
},
];
}
public async seedCollection() {
const collection = await this.getDocuments();
const data = this.generateData();
await collection.data.insertMany(data);
console.log(`Inserted ${data.length} documents`);
}
private async init() {
await this.createCollection();
await this.seedCollection();
console.log("Collection initialized");
}
/**
* 執行向量搜尋
*/
public async search(query: string) {
const collection = await this.getDocuments();
const results = await collection.query.nearText(query, {
limit: 3,
returnMetadata: ["distance"],
});
console.log(results.objects.map((obj) => obj.properties));
return results;
}
}優點
HNSW 算法,支援完整的 CRUD 操作。缺點
Weaviate 較適合需要語義搜尋、多模態數據處理及與 AI 模型深度整合的應用場景,特別是構建推薦或知識檢索系統。
| 特性 | pgvector | Milvus | Weaviate |
|---|---|---|---|
| 部署方式 | PostgreSQL 擴展 | 雲端 + 本地 | 雲端 + 本地 |
| 開源授權 | PostgreSQL License | Apache 2.0 | BSD 3-Clause |
| 向量維度上限 | 2,000(索引限制) | 32,768 | 65,535 |
| 索引類型 | IVFFlat、HNSW | HNSW、DiskANN、GPU 加速 | HNSW |
| 混合查詢 | ✅ SQL 原生支援 | ⚠️ 需透過 API | ✅ GraphQL 支援 |
| 多模態支援 | ❌ | ✅ 文字、圖片、音訊 | ✅ 文字、圖片 |
| 擴展性 | 中等(單機垂直擴展) | 極強(分佈式架構) | 強(自動分片) |
| 學習曲線 | 低(SQL 即可) | 高(需學習專用 API) | 中(GraphQL) |
| 查詢效能(QPS) | 2.6-40.5(視索引而定) | > 100(GPU 加速) | 50-80 |
| 最佳使用場景 | 中小型專案、混合查詢 | 大規模 AI 應用 | 語義搜索、RAG |
| 如果你需要... | 推薦方案 |
|---|---|
| 與現有 PostgreSQL 整合 | pgvector |
| 十億級向量 + GPU 加速 | Milvus |
| 多模態 + 即時向量化 | Weaviate |
| 最低學習成本 | pgvector |
| 最高查詢效能 | Milvus |
| 最佳語義搜索 | Weaviate |
| 資料量 < 100 萬 | pgvector |
| 資料量 > 1000 萬 | Milvus |
| 需要混合搜尋(關鍵字+向量) | Weaviate |