ถ้าคุณกำลังสร้างระบบ RAG หรือแอปพลิเคชันที่ต้องการ Semantic Search คำถามแรกที่ต้องตัดสินใจคือจะเก็บ Embedding ไว้ที่ไหน Vector Database เฉพาะทางอย่าง Pinecone, Qdrant หรือ Weaviate เป็นตัวเลือกหนึ่ง แต่สำหรับทีมที่ใช้ PostgreSQL อยู่แล้ว pgvector คือทางเลือกที่เร็วกว่า ถูกกว่า และดูแลง่ายกว่า
pgvector คือ Extension แบบ Open Source สำหรับ PostgreSQL ที่เพิ่ม Type ข้อมูล vector พร้อม Similarity Search Operator และ Index แบบ HNSW/IVFFlat เข้าไปใน Database ที่มีอยู่เดิม Embedding ของคุณอยู่ใน Database เดียวกับข้อมูลแอปพลิเคชัน Query ด้วย SQL มาตรฐาน รองรับ ACID ของ Postgres เต็มรูปแบบ
บทความนี้ครอบคลุมตั้งแต่การติดตั้งจนถึง RAG Query ระดับ Production พร้อมโค้ดจริงทุกขั้นตอน
สิ่งที่คุณจะได้: ระบบ Semantic Search ที่เก็บ Document Embedding ใน PostgreSQL และดึงข้อมูลที่เกี่ยวข้องที่สุดสำหรับ Query ที่กำหนด — เป็น Retrieval Layer ของ RAG Pipeline
Stack ที่ใช้:
- PostgreSQL 15+ พร้อม pgvector 0.7+
- Python 3.11
openaiหรือollamaสำหรับ Embeddingpsycopg2/asyncpgสำหรับเชื่อมต่อ Database
pgvector คืออะไร และทำไมถึงควรใช้?
Vector Embedding คือลิสต์ของตัวเลข Floating-Point — โดยทั่วไป 768 ถึง 3,072 มิติ — ที่เข้ารหัสความหมายเชิงความหมายของข้อความ รูปภาพ หรือข้อมูลอื่นๆ ข้อความสองชิ้นที่มีความหมายใกล้เคียงกันจะให้ Vector ที่อยู่ใกล้กันในพื้นที่หลายมิตินั้น
pgvector เพิ่มความสามารถในการเก็บ Vector เหล่านี้ใน Postgres และรันการคำนวณระยะทางได้อย่างมีประสิทธิภาพ:
| การดำเนินการ | SQL |
|---|---|
| Cosine Similarity | embedding <=> query_vector |
| L2 Distance | embedding <-> query_vector |
| Inner Product | embedding <#> query_vector |
เมื่อไหร่ที่ pgvector เหมาะสม:
- คุณใช้ PostgreSQL อยู่แล้ว
- จำนวน Vector น้อยกว่า ~5 ล้าน
- ต้องการ JOIN Embedding กับข้อมูล Relational (User ID, Document Metadata, Access Control)
- ต้องการ ACID Transaction ครอบคลุมทั้งข้อมูลแอปและ Embedding
- ไม่ต้องการจัดการ Infrastructure เพิ่มเติม
เมื่อไหร่ที่ควรใช้ Vector DB เฉพาะทางแทน:
- มี Vector มากกว่า 10 ล้านรายการ และต้องการ Latency ต่ำกว่า 10ms
- ต้องการ Advanced Filtering ที่ Scale ขนาดใหญ่
- ทีมไม่มี Infrastructure ของ Postgres อยู่เดิม
สำหรับโปรเจกต์ RAG ขององค์กรในไทยและอาเซียน — การค้นหาเอกสารภายใน ระบบสัญญา ระบบปฏิบัติตาม PDPA — pgvector เพียงพอและดูแลง่ายกว่ามาก
ภาพรวมสถาปัตยกรรม
flowchart TD
A["เอกสาร\n(PDF, ข้อความ, ฐานข้อมูล)"] --> B["การแบ่ง Chunk\n(ประมาณ 500 Token ต่อส่วน)"]
B --> C["Embedding Model\n(OpenAI หรือ Ollama หรือ Local)"]
C --> D["pgvector\n(PostgreSQL)"]
E["คำถามของผู้ใช้"] --> F["แปลงคำถามเป็น Embedding"]
F --> G["Similarity Search\n(Cosine หรือ L2)"]
D --> G
G --> H["Chunk ที่เกี่ยวข้อง Top-k"]
H --> I["LLM\n(GPT หรือ Claude หรือ Local)"]
I --> J["คำตอบพร้อมแหล่งอ้างอิง"]
ขั้นตอนที่ 1 — ติดตั้ง pgvector
ตัวเลือก A: Docker (แนะนำสำหรับ Development)
docker run -d \
--name pgvector-dev \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=ragdb \
-p 5432:5432 \
pgvector/pgvector:pg16
Image นี้ติดตั้ง pgvector ไว้ล่วงหน้าแล้ว ไม่ต้อง Build Extension เอง
ตัวเลือก B: ติดตั้งบน PostgreSQL ที่มีอยู่
Ubuntu / Debian:
sudo apt install postgresql-16-pgvector
macOS (Homebrew):
brew install pgvector
เปิดใช้งาน Extension ใน Database
CREATE EXTENSION IF NOT EXISTS vector;
-- ตรวจสอบการติดตั้ง
SELECT * FROM pg_extension WHERE extname = 'vector';
ขั้นตอนที่ 2 — สร้าง Schema
-- ตาราง Documents: เก็บข้อความต้นฉบับและ Metadata
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
source_url TEXT,
lang VARCHAR(5) DEFAULT 'th',
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ตาราง Chunks: เก็บส่วนข้อความและ Embedding
CREATE TABLE document_chunks (
id BIGSERIAL PRIMARY KEY,
document_id BIGINT REFERENCES documents(id) ON DELETE CASCADE,
chunk_index INTEGER NOT NULL,
content TEXT NOT NULL,
token_count INTEGER,
embedding vector(1536), -- ต้องตรงกับ Embedding Model ที่ใช้
created_at TIMESTAMPTZ DEFAULT NOW()
);
มิติของ Embedding ตาม Model:
| Model | มิติ |
|---|---|
| OpenAI text-embedding-3-small | 1536 |
| OpenAI text-embedding-3-large | 3072 |
| Ollama nomic-embed-text | 768 |
| Ollama mxbai-embed-large | 1024 |
| BGE-M3 (รองรับหลายภาษา รวมถึงภาษาไทย) | 1024 |
Column vector(1536) ต้องตรงกันทุกครั้ง ไม่สามารถผสมมิติใน Column เดียวกันได้
ขั้นตอนที่ 3 — นำเข้าเอกสารและสร้าง Embedding
import os
import psycopg2
from openai import OpenAI
import tiktoken
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
DB_URL = "postgresql://postgres:secret@localhost:5432/ragdb"
def chunk_text(text: str, max_tokens: int = 500, overlap: int = 50) -> list[str]:
"""แบ่งข้อความเป็น Chunk ที่ทับซ้อนกันตามจำนวน Token"""
enc = tiktoken.get_encoding("cl100k_base")
tokens = enc.encode(text)
chunks = []
start = 0
while start < len(tokens):
end = min(start + max_tokens, len(tokens))
chunks.append(enc.decode(tokens[start:end]))
start += max_tokens - overlap
return chunks
def get_embedding(text: str) -> list[float]:
"""ดึง Embedding Vector จาก OpenAI"""
response = client.embeddings.create(
model="text-embedding-3-small",
input=text.replace("\n", " ")
)
return response.data[0].embedding
def ingest_document(title: str, content: str, source_url: str = None, lang: str = "th"):
"""แบ่ง Chunk เอกสาร สร้าง Embedding และเก็บลง PostgreSQL"""
conn = psycopg2.connect(DB_URL)
cur = conn.cursor()
try:
cur.execute(
"INSERT INTO documents (title, source_url, lang) VALUES (%s, %s, %s) RETURNING id",
(title, source_url, lang)
)
doc_id = cur.fetchone()[0]
chunks = chunk_text(content)
print(f"กำลังนำเข้า '{title}': {len(chunks)} chunks")
for i, chunk in enumerate(chunks):
embedding = get_embedding(chunk)
cur.execute(
"""INSERT INTO document_chunks
(document_id, chunk_index, content, token_count, embedding)
VALUES (%s, %s, %s, %s, %s)""",
(doc_id, i, chunk, len(chunk.split()), embedding)
)
conn.commit()
print(f"เสร็จสิ้น: document_id={doc_id}")
return doc_id
except Exception as e:
conn.rollback()
raise e
finally:
cur.close()
conn.close()
# ตัวอย่างการใช้งาน
ingest_document(
title="นโยบายความปลอดภัยข้อมูลส่วนบุคคล (PDPA) ฉบับ 3.2",
content=open("pdpa_policy.txt").read(),
source_url="https://internal.company.co.th/policies/pdpa",
lang="th"
)
ใช้ Ollama สำหรับ Local Embedding (ไม่ต้องใช้ API Key)
import httpx
def get_embedding_ollama(text: str, model: str = "nomic-embed-text") -> list[float]:
"""ดึง Embedding จาก Ollama ที่รันในเครื่อง"""
response = httpx.post(
"http://localhost:11434/api/embeddings",
json={"model": model, "prompt": text}
)
return response.json()["embedding"]
เปลี่ยน get_embedding() เป็น get_embedding_ollama() และเปลี่ยนมิติ Vector เป็น 768 ใน Schema ถ้าใช้ nomic-embed-text
ขั้นตอนที่ 4 — สร้าง Index สำหรับ Similarity Search ที่รวดเร็ว
HNSW (แนะนำสำหรับกรณีทั่วไป)
CREATE INDEX ON document_chunks
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
| Parameter | ค่าเริ่มต้น | ผลกระทบ |
|---|---|---|
m |
16 | การเชื่อมต่อต่อ Node สูงกว่า = Recall ดีกว่า ใช้ Memory มากกว่า |
ef_construction |
64 | ความกว้างการค้นหาตอน Build สูงกว่า = Recall ดีกว่า Build ช้ากว่า |
IVFFlat (สำหรับ Dataset ขนาดใหญ่มาก)
CREATE INDEX ON document_chunks
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
เปรียบเทียบ:
| HNSW | IVFFlat | |
|---|---|---|
| Recall | สูงกว่า | ต่ำกว่า (ปรับได้) |
| เวลา Build | ช้ากว่า | เร็วกว่า |
| Memory | สูงกว่า | ต่ำกว่า |
| เหมาะสำหรับ | Vector < 2 ล้าน | Vector > 2 ล้าน |
ขั้นตอนที่ 5 — รัน Similarity Search
def semantic_search(
query: str,
top_k: int = 5,
lang_filter: str = None,
min_similarity: float = 0.7
) -> list[dict]:
"""ค้นหา Chunk ที่มีความหมายใกล้เคียงกับคำถามมากที่สุด"""
query_embedding = get_embedding(query)
conn = psycopg2.connect(DB_URL)
cur = conn.cursor()
try:
cur.execute("SET hnsw.ef_search = 100")
sql = """
SELECT
dc.id, dc.content, dc.chunk_index,
d.title, d.source_url, d.lang,
1 - (dc.embedding <=> %s::vector) AS similarity
FROM document_chunks dc
JOIN documents d ON d.id = dc.document_id
WHERE 1 - (dc.embedding <=> %s::vector) >= %s
{lang_clause}
ORDER BY dc.embedding <=> %s::vector
LIMIT %s
""".format(lang_clause="AND d.lang = %s" if lang_filter else "")
params = [query_embedding, query_embedding, min_similarity]
if lang_filter:
params.append(lang_filter)
params.extend([query_embedding, top_k])
cur.execute(sql, params)
return [
{"id": r[0], "content": r[1], "chunk_index": r[2],
"title": r[3], "source_url": r[4], "lang": r[5],
"similarity": float(r[6])}
for r in cur.fetchall()
]
finally:
cur.close()
conn.close()
# ตัวอย่าง
results = semantic_search(
query="นโยบายรหัสผ่านสำหรับบัญชีผู้ดูแลระบบคืออะไร",
top_k=5,
lang_filter="th",
min_similarity=0.75
)
for r in results:
print(f"[{r['similarity']:.3f}] {r['title']} — chunk {r['chunk_index']}")
ขั้นตอนที่ 6 — สร้าง RAG Pipeline ครบวงจร
def rag_query(question: str, lang: str = "th", top_k: int = 5) -> dict:
"""RAG Pipeline ครบวงจร: ดึง Chunk และสร้างคำตอบที่มีการอ้างอิง"""
chunks = semantic_search(question, top_k=top_k, lang_filter=lang)
if not chunks:
return {"answer": "ไม่พบเอกสารที่เกี่ยวข้องกับคำถามนี้", "sources": []}
context = "\n\n".join([f"[{i+1}] {c['content']}" for i, c in enumerate(chunks)])
sources = [{"title": c["title"], "url": c["source_url"], "similarity": c["similarity"]}
for c in chunks]
system_prompt = """คุณเป็นผู้ช่วยที่เป็นประโยชน์ ตอบคำถามโดยใช้เฉพาะ Context ที่ให้มาเท่านั้น
หากไม่มีคำตอบใน Context ให้ระบุว่า "ไม่พบข้อมูลนี้ในเอกสารที่มี"
อ้างอิงหมายเลขแหล่งข้อมูล [1], [2] ฯลฯ เสมอเมื่ออ้างอิงข้อมูล"""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"Context:\n{context}\n\nคำถาม: {question}\n\nคำตอบ:"}
],
temperature=0.1
)
return {"answer": response.choices[0].message.content, "sources": sources}
# ตัวอย่าง
result = rag_query("ข้อกำหนด PDPA สำหรับการเก็บข้อมูลลูกค้าคืออะไร", lang="th")
print(result["answer"])
ขั้นตอนที่ 7 — Hybrid Search (Vector + Full-Text)
การค้นหาด้วย Vector อย่างเดียวบางครั้งพลาด Keyword ที่ตรงกัน Hybrid Search รวม Vector Similarity กับ PostgreSQL Full-Text Search เพื่อ Recall ที่ดีกว่า:
ALTER TABLE document_chunks ADD COLUMN content_tsv tsvector
GENERATED ALWAYS AS (to_tsvector('english', content)) STORED;
CREATE INDEX ON document_chunks USING gin(content_tsv);
def hybrid_search(query: str, top_k: int = 10, rrf_k: int = 60) -> list[dict]:
"""
Reciprocal Rank Fusion: รวมการจัดอันดับ Vector Search และ Full-Text Search
RRF Score = 1/(k + rank_vector) + 1/(k + rank_fts)
"""
query_embedding = get_embedding(query)
conn = psycopg2.connect(DB_URL)
cur = conn.cursor()
try:
cur.execute("SET hnsw.ef_search = 100")
cur.execute("""
WITH vector_ranked AS (
SELECT id, content, document_id,
ROW_NUMBER() OVER (ORDER BY embedding <=> %s::vector) AS rank
FROM document_chunks ORDER BY embedding <=> %s::vector LIMIT 20
),
fts_ranked AS (
SELECT id, content, document_id,
ROW_NUMBER() OVER (ORDER BY ts_rank(content_tsv, query) DESC) AS rank
FROM document_chunks, plainto_tsquery('english', %s) query
WHERE content_tsv @@ query LIMIT 20
),
rrf AS (
SELECT COALESCE(v.id, f.id) AS id,
COALESCE(v.content, f.content) AS content,
COALESCE(v.document_id, f.document_id) AS document_id,
COALESCE(1.0/(%s+v.rank),0) + COALESCE(1.0/(%s+f.rank),0) AS rrf_score
FROM vector_ranked v FULL OUTER JOIN fts_ranked f ON v.id = f.id
)
SELECT r.id, r.content, d.title, d.source_url, r.rrf_score
FROM rrf r JOIN documents d ON d.id = r.document_id
ORDER BY rrf_score DESC LIMIT %s
""", [query_embedding, query_embedding, query, rrf_k, rrf_k, top_k])
return [{"id": r[0], "content": r[1], "title": r[2],
"source_url": r[3], "rrf_score": float(r[4])}
for r in cur.fetchall()]
finally:
cur.close()
conn.close()
Hybrid Search ปรับปรุง Recall ได้ 15–30% สำหรับ Corpus เฉพาะทางที่ผู้ใช้ผสม Keyword และ Semantic Query เหมาะมากสำหรับเอกสารที่มีรหัสผลิตภัณฑ์ หมายเลขกฎระเบียบ หรือชื่อเฉพาะ
การปรับแต่ง Performance
-- เพิ่ม Memory สำหรับ Build Index เร็วขึ้น
SET maintenance_work_mem = '1GB';
CREATE INDEX ON document_chunks USING hnsw (embedding vector_cosine_ops);
-- ปรับ ef_search สูงขึ้นสำหรับ Recall ที่ดีกว่า (ค่าเริ่มต้น 40)
SET hnsw.ef_search = 100;
-- Index แยกตามภาษาสำหรับระบบ Multi-Language
CREATE INDEX ON document_chunks USING hnsw (embedding vector_cosine_ops)
WHERE document_id IN (SELECT id FROM documents WHERE lang = 'th');
-- ตรวจสอบ Query Plan ว่าใช้ Index หรือไม่
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, content, embedding <=> '[0.1, 0.2, ...]'::vector AS distance
FROM document_chunks ORDER BY distance LIMIT 5;
คำถามที่พบบ่อย
pgvector รองรับกี่ Vector?
ในทางปฏิบัติ pgvector จัดการ 1–5 ล้าน Vector ได้ดีบน Hardware มาตรฐาน (RAM 16 GB, SSD) เกินนั้น Latency จะเริ่มสูงขึ้น หากมีมากกว่า 10 ล้าน Vector ควรพิจารณา Qdrant หรือ Weaviate
ควรใช้ Cosine Similarity หรือ L2 Distance?
Cosine Similarity (<=>) เหมาะกว่าสำหรับ Text Embedding เพราะวัดมุมระหว่าง Vector โดยไม่สนขนาด L2 Distance (<->) เหมาะกับ Image Embedding สำหรับ RAG กับข้อความ ให้ใช้ Cosine เสมอ
เก็บ Embedding หลายภาษาใน Table เดียวได้ไหม?
ได้ ถ้า Embedding Model รองรับหลายภาษา BGE-M3 และ multilingual-e5-large ทำงานได้ดีกับภาษาไทย ญี่ปุ่น และจีน เก็บภาษาใน Column lang และ Filter ตอน Query
ขนาด Chunk ที่เหมาะสมคือเท่าไหร่?
500 Token พร้อม Overlap 50 Token เป็นจุดเริ่มต้นที่ดี เล็กเกินไป (< 100 Token) สูญเสีย Context ใหญ่เกินไป (> 1,000 Token) ลด Relevance สำหรับเอกสารที่มีโครงสร้าง เช่น สัญญา หรือเอกสารกฎหมาย PDPA ควร Chunk ตามขอบเขตหัวข้อแทนการนับ Token ตายตัว
pgvector กับ Pinecone ต่างกันอย่างไร?
ถ้าใช้ Postgres อยู่แล้ว ใช้ pgvector ได้เลย คุณได้ SQL JOIN, Transaction, ไม่ต้องจัดการ Infrastructure เพิ่ม และไม่มีค่าใช้จ่าย Per-Query สำหรับโปรเจกต์ RAG ขององค์กรในช่วง < 5 ล้าน Vector pgvector คือทางเลือกที่คุ้มค่าที่สุด
Checklist ก่อน Production
- [ ] สร้าง HNSW หรือ IVFFlat Index บน Column Embedding แล้ว
- [ ] ตั้ง
maintenance_work_memอย่างน้อย 512MB สำหรับ Build Index - [ ] ปรับ
hnsw.ef_search(เริ่มที่ 100 แล้ว Benchmark Recall vs Latency) - [ ] สร้าง Partial Index แยกตามภาษาหรือ Tenant ถ้า Filter หนัก
- [ ] ใช้ Connection Pooling (PgBouncer) หน้า Postgres
- [ ] การสร้าง Embedding เป็นแบบ Async ผ่าน Queue ไม่ใช่ Inline กับ Request ผู้ใช้
- [ ] ตั้ง
min_similarityThreshold (0.7–0.8) เพื่อกรอง Match คุณภาพต่ำ - [ ] Monitor
pg_stat_activityสำหรับ Vector Query ที่ช้า - [ ] รัน
VACUUM ANALYZEบนdocument_chunksหลัง Bulk Ingest
ขั้นตอนถัดไป
บทความนี้ครอบคลุม Retrieval Layer สำหรับ RAG Stack ที่สมบูรณ์:
- สำหรับการนำเข้าเอกสารภาษาไทยและญี่ปุ่นด้วย Multilingual Embedding — ดูคู่มือ: LlamaIndex + pgvector: Production RAG for Thai and Japanese Business Documents
- สำหรับการเลือก Hardware รัน Local Embedding Model โดยไม่พึ่ง OpenAI — ดู: Choosing Hardware for Local LLMs in 2026
- สำหรับความเข้าใจว่า RAG เข้ากับ Private AI Architecture อย่างไร — ดู: Private AI vs ChatGPT: ต่างกันอย่างไรและธุรกิจของคุณต้องการอะไร
กำลังสร้างระบบ RAG สำหรับเอกสารภายใน Workflow ด้านการปฏิบัติตาม PDPA หรือระบบ Support ลูกค้า? ติดต่อเราได้ที่ hello@simplico.net — เราให้บริการ Deploy Production RAG Stack สำหรับองค์กรในไทยและญี่ปุ่น
