前言

自從 ChatGPT 和各種大型語言模型(LLM)爆紅以來,「向量資料庫」這個詞突然變成後端工程師的必修課。以前我們把資料存成行和列,現在卻要存成一串浮點數組成的向量?聽起來很玄,但其實原理並不複雜。

這篇文章我會從「為什麼需要向量搜尋」開始,帶你認識三個主流的向量資料庫方案:pgvector、Milvus 和 Qdrant。如果你正在建構 RAG(Retrieval Augmented Generation)系統、推薦引擎或語意搜尋功能,這篇應該對你有幫助。


什麼是向量搜尋?

從文字到向量

傳統的資料庫搜尋是「精確匹配」或「模糊匹配」——你搜 “咖啡”,資料庫找包含 “咖啡” 這兩個字的資料。但如果使用者搜 “提神飲料” 呢?傳統搜尋就找不到了。

向量搜尋的做法不同:

  1. Embedding 模型(如 OpenAI 的 text-embedding-ada-002)把文字轉成一個高維度的向量(例如 1536 維的浮點數陣列)
  2. 語意相近的文字,在向量空間中的距離也會比較近
  3. 搜尋時,把查詢文字也轉成向量,然後找最近的鄰居(Nearest Neighbor)
# 概念示意
"咖啡"   -> [0.12, -0.34, 0.56, ..., 0.78]  # 1536 維
"提神飲料" -> [0.11, -0.32, 0.55, ..., 0.79]  # 很接近!
"資料庫"  -> [0.89, 0.23, -0.67, ..., -0.12]  # 很遠

距離計算方式

  • 餘弦相似度(Cosine Similarity):最常用,衡量方向的相似性
  • 歐氏距離(L2 Distance):直線距離
  • 內積(Inner Product):適合已正規化的向量

近似最近鄰(ANN)演算法

當你有一百萬個向量,暴力計算距離太慢了。常見的 ANN 演算法包括:

  • HNSW(Hierarchical Navigable Small World):查詢快、記憶體用量大
  • IVFFlat:先分群再搜尋、記憶體用量適中
  • PQ(Product Quantization):壓縮向量、犧牲一點精度換空間

pgvector — PostgreSQL 的向量擴充

如果你已經在用 PostgreSQL,pgvector 是最低門檻的選擇。

安裝與設定

# docker-compose.yml
version: '3.8'
services:
  postgres:
    image: pgvector/pgvector:pg16
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: vecadmin
      POSTGRES_PASSWORD: secret123
      POSTGRES_DB: vector_db
    volumes:
      - pg_data:/var/lib/postgresql/data

volumes: pg_data:

-- 啟用擴充
CREATE EXTENSION IF NOT EXISTS vector;

-- 建立文章表,embedding 欄位存 1536 維向量 CREATE TABLE articles ( id SERIAL PRIMARY KEY, title TEXT NOT NULL, content TEXT NOT NULL, embedding vector(1536), created_at TIMESTAMPTZ DEFAULT NOW() );

-- 建立 HNSW 索引(推薦,查詢較快) CREATE INDEX ON articles USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 200);

-- 或者建立 IVFFlat 索引(較省記憶體) -- CREATE INDEX ON articles -- USING ivfflat (embedding vector_cosine_ops) -- WITH (lists = 100);

用 Python 寫入和查詢

import psycopg2
from openai import OpenAI

# 初始化 client = OpenAI(api_key="your-api-key") conn = psycopg2.connect( host="localhost", database="vector_db", user="vecadmin", password="secret123" )

def get_embedding(text: str) -> list[float]: """用 OpenAI API 產生文字向量""" response = client.embeddings.create( model="text-embedding-ada-002", input=text ) return response.data[0].embedding

# 寫入文章及其向量 def insert_article(title: str, content: str): embedding = get_embedding(f"{title} {content}") with conn.cursor() as cur: cur.execute( """ INSERT INTO articles (title, content, embedding) VALUES (%s, %s, %s) """, (title, content, embedding) ) conn.commit()

# 語意搜尋 def search_articles(query: str, limit: int = 5): query_embedding = get_embedding(query) with conn.cursor() as cur: cur.execute( """ SELECT id, title, 1 - (embedding <=> %s::vector) AS similarity FROM articles ORDER BY embedding <=> %s::vector LIMIT %s """, (query_embedding, query_embedding, limit) ) return cur.fetchall()

# 使用範例 insert_article("Docker 入門教學", "Docker 是一個容器化平台...") results = search_articles("如何使用容器技術部署應用程式") for article_id, title, similarity in results: print(f"[{similarity:.4f}] {title}")

注意 <=> 是 pgvector 的餘弦距離運算子。其他還有:

  • <-> L2 距離
  • <#> 負內積

Milvus — 分散式向量資料庫

Milvus 是一個專門為向量搜尋設計的分散式資料庫,適合大規模場景。

安裝(Standalone 模式)

# docker-compose.yml
version: '3.8'
services:
  etcd:
    image: quay.io/coreos/etcd:v3.5.5
    environment:
      ETCD_AUTO_COMPACTION_MODE: revision
      ETCD_AUTO_COMPACTION_RETENTION: "1000"
    command: etcd --advertise-client-urls=http://127.0.0.1:2379 --listen-client-urls http://0.0.0.0:2379

minio: image: minio/minio:RELEASE.2023-03-20T20-16-18Z environment: MINIO_ACCESS_KEY: minioadmin MINIO_SECRET_KEY: minioadmin command: minio server /minio_data

milvus: image: milvusdb/milvus:v2.3.3 depends_on: - etcd - minio ports: - "19530:19530" - "9091:9091" environment: ETCD_ENDPOINTS: etcd:2379 MINIO_ADDRESS: minio:9000

Python 操作 Milvus

from pymilvus import (
    connections, Collection, CollectionSchema,
    FieldSchema, DataType, utility
)

# 連線 connections.connect("default", host="localhost", port="19530")

# 定義 Schema fields = [ FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True), FieldSchema(name="title", dtype=DataType.VARCHAR, max_length=500), FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=1536), ] schema = CollectionSchema(fields, description="Article embeddings")

# 建立 Collection collection = Collection("articles", schema)

# 建立索引 index_params = { "index_type": "HNSW", "metric_type": "COSINE", "params": {"M": 16, "efConstruction": 200} } collection.create_index("embedding", index_params)

# 寫入資料 import random data = [ ["Docker 入門", "Kubernetes 教學", "PostgreSQL 調優"], [[random.random() for _ in range(1536)] for _ in range(3)], ] collection.insert([data[0], data[1]])

# 搜尋 collection.load() search_params = {"metric_type": "COSINE", "params": {"ef": 100}} query_vector = [[random.random() for _ in range(1536)]]

results = collection.search( data=query_vector, anns_field="embedding", param=search_params, limit=5, output_fields=["title"] )

for hits in results: for hit in hits: print(f"ID: {hit.id}, Title: {hit.entity.get('title')}, " f"Score: {hit.score:.4f}")


Qdrant — Rust 打造的高效能向量搜尋

Qdrant 用 Rust 寫成,以高效能和易用的 API 著稱。

安裝

# docker-compose.yml
version: '3.8'
services:
  qdrant:
    image: qdrant/qdrant:v1.7.3
    ports:
      - "6333:6333"   # REST API
      - "6334:6334"   # gRPC
    volumes:
      - qdrant_data:/qdrant/storage

volumes: qdrant_data:

Python 操作 Qdrant

from qdrant_client import QdrantClient
from qdrant_client.models import (
    Distance, VectorParams, PointStruct, Filter,
    FieldCondition, MatchValue
)

client = QdrantClient(host="localhost", port=6333)

# 建立 Collection client.create_collection( collection_name="articles", vectors_config=VectorParams( size=1536, distance=Distance.COSINE ) )

# 寫入資料(帶 metadata payload) points = [ PointStruct( id=1, vector=[0.1] * 1536, # 實際應用中用 embedding 模型產生 payload={ "title": "Docker 入門教學", "category": "devops", "tags": ["docker", "container"] } ), PointStruct( id=2, vector=[0.2] * 1536, payload={ "title": "PostgreSQL 效能調優", "category": "database", "tags": ["postgresql", "performance"] } ), ]

client.upsert(collection_name="articles", points=points)

# 語意搜尋 results = client.search( collection_name="articles", query_vector=[0.15] * 1536, limit=5 )

for result in results: print(f"ID: {result.id}, Score: {result.score:.4f}") print(f" Title: {result.payload['title']}")

# 帶過濾條件的搜尋 results = client.search( collection_name="articles", query_vector=[0.15] * 1536, query_filter=Filter( must=[ FieldCondition( key="category", match=MatchValue(value="database") ) ] ), limit=5 )

Qdrant 的一個亮點是它的 payload 過濾 做得非常好,可以在向量搜尋的同時加上結構化的過濾條件,而且效能幾乎不受影響。


三者比較

| 面向 | pgvector | Milvus | Qdrant |
|——|———-|——–|——–|
| 語言 | C(PG 擴充) | Go / C++ | Rust |
| 部署難度 | 極低 | 中高(需 etcd + MinIO) | 低 |
| 最大向量數 | 數百萬 | 數十億 | 數億 |
| 分散式 | 不支援 | 支援 | 支援(Qdrant Cloud) |
| SQL 支援 | 完整 | 不支援 | 不支援 |
| 過濾能力 | SQL WHERE | 屬性過濾 | Payload 過濾(強) |
| 適合場景 | 中小規模、已用 PG | 超大規模 | 中大規模、快速開發 |
| 社群活躍度 | 高 | 高 | 中高 |

選擇建議

  • 已有 PostgreSQL,向量數 < 500 萬 → pgvector。零額外維護成本,SQL 整合最方便
  • 需要處理數十億向量、分散式需求 → Milvus。它就是為這種規模設計的
  • 重視 API 易用性和開發速度 → Qdrant。REST API 設計最直覺,payload 過濾很強
  • 建構 RAG 系統的 MVP → 先用 pgvector,規模大了再換

實戰:用 pgvector 建一個簡易 RAG 系統

from openai import OpenAI
import psycopg2

openai_client = OpenAI(api_key="your-key") conn = psycopg2.connect("dbname=vector_db user=vecadmin password=secret123")

def rag_query(user_question: str) -> str: """RAG 流程:搜尋相關文件 → 餵給 LLM 產生回答"""

# Step 1: 把問題轉成向量 q_embedding = openai_client.embeddings.create( model="text-embedding-ada-002", input=user_question ).data[0].embedding

# Step 2: 在資料庫中搜尋最相關的 3 篇文章 with conn.cursor() as cur: cur.execute(""" SELECT title, content, 1 - (embedding <=> %s::vector) AS score FROM articles ORDER BY embedding <=> %s::vector LIMIT 3 """, (q_embedding, q_embedding)) docs = cur.fetchall()

# Step 3: 組合 context context = "\n\n".join( f"### {title}\n{content}" for title, content, _ in docs )

# Step 4: 送給 LLM response = openai_client.chat.completions.create( model="gpt-4", messages=[ {"role": "system", "content": f"根據以下參考資料回答問題:\n\n{context}"}, {"role": "user", "content": user_question} ] )

return response.choices[0].message.content

# 使用 answer = rag_query("Docker 和虛擬機有什麼差別?") print(answer)


小結

向量資料庫的核心價值在於「語意搜尋」——讓機器理解意思,而不只是比對字串。在 LLM 應用爆發的時代,掌握向量搜尋技術已經是後端工程師的基本功。

我的實際經驗是:大多數專案用 pgvector 就夠了。它的優勢在於你不需要多維護一個服務,所有資料都在同一個 PostgreSQL 裡,SQL 的靈活性也無可取代。只有當你的向量數量真的超過千萬級別、或者需要分散式部署時,才需要考慮 Milvus 或 Qdrant。

延伸閱讀