大多数 React Native 教程止步于 UI 层——展示如何渲染聊天气泡,然后用一句含糊的"直接从应用调用 OpenAI API"带过后端部分。
Continue reading “如何在 React Native 应用中添加 AI 聊天机器人(附 FastAPI 后端)”
大多数 React Native 教程止步于 UI 层——展示如何渲染聊天气泡,然后用一句含糊的"直接从应用调用 OpenAI API"带过后端部分。
Continue reading “如何在 React Native 应用中添加 AI 聊天机器人(附 FastAPI 后端)”
React Nativeのチュートリアルの多くはUI層で止まります。チャットバブルの描画やキーボード制御は丁寧に説明しながら、バックエンドについては「OpenAI APIをアプリから直接呼ぶ」とだけ説明して終わりにしてしまいます。
Continue reading “React NativeアプリにAIチャットボットを追加する方法(FastAPIバックエンド付き)”
บทความ React Native ส่วนใหญ่หยุดแค่ UI layer — แสดงวิธีทำ chat bubble แต่ข้ามเรื่อง backend ด้วยคำแนะนำคลุมเครือว่า "เรียก OpenAI API จากแอปโดยตรง"
Continue reading “วิธีเพิ่ม AI Chatbot เข้าแอป React Native (พร้อม FastAPI Backend)”
Most React Native tutorials stop at the UI layer. They show you how to render chat bubbles and handle keyboard offsets—then hand-wave the backend with a vague "call the OpenAI API from your app."
Continue reading “How to Add an AI Chatbot to Your React Native App (with FastAPI Backend)”
在构建RAG管道或需要语义搜索的应用时,首先需要决定的是将Embedding存储在哪里。Pinecone、Qdrant、Weaviate等专用向量数据库是一种选择,但对于已经运行PostgreSQL的团队而言,pgvector是更快、更经济、运维更简单的方案。
RAGパイプラインやセマンティック検索が必要なアプリケーションを構築する際、最初に決める必要があるのは「Embeddingをどこに保存するか」です。PineconeやQdrant、Weaviateといった専用ベクトルデータベースも選択肢の一つですが、すでにPostgreSQLを運用しているチームにとって、pgvectorはより速く、安価で、運用が簡単な方法です。
Continue reading “pgvectorチュートリアル:PostgreSQLにベクトル検索を追加してRAGとセマンティック検索を実現する”
ถ้าคุณกำลังสร้างระบบ 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 ที่ใช้:
openai หรือ ollama สำหรับ Embeddingpsycopg2 / asyncpg สำหรับเชื่อมต่อ DatabaseVector 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 เหมาะสม:
เมื่อไหร่ที่ควรใช้ Vector DB เฉพาะทางแทน:
สำหรับโปรเจกต์ 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["คำตอบพร้อมแหล่งอ้างอิง"]
docker run -d \
--name pgvector-dev \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=ragdb \
-p 5432:5432 \
pgvector/pgvector:pg16
Image นี้ติดตั้ง pgvector ไว้ล่วงหน้าแล้ว ไม่ต้อง Build Extension เอง
Ubuntu / Debian:
sudo apt install postgresql-16-pgvector
macOS (Homebrew):
brew install pgvector
CREATE EXTENSION IF NOT EXISTS vector;
-- ตรวจสอบการติดตั้ง
SELECT * FROM pg_extension WHERE extname = 'vector';
-- ตาราง 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 เดียวกันได้
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"
)
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
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 ช้ากว่า |
CREATE INDEX ON document_chunks
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
เปรียบเทียบ:
| HNSW | IVFFlat | |
|---|---|---|
| Recall | สูงกว่า | ต่ำกว่า (ปรับได้) |
| เวลา Build | ช้ากว่า | เร็วกว่า |
| Memory | สูงกว่า | ต่ำกว่า |
| เหมาะสำหรับ | Vector < 2 ล้าน | Vector > 2 ล้าน |
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']}")
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"])
การค้นหาด้วย 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 เหมาะมากสำหรับเอกสารที่มีรหัสผลิตภัณฑ์ หมายเลขกฎระเบียบ หรือชื่อเฉพาะ
-- เพิ่ม 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 คือทางเลือกที่คุ้มค่าที่สุด
maintenance_work_mem อย่างน้อย 512MB สำหรับ Build Indexhnsw.ef_search (เริ่มที่ 100 แล้ว Benchmark Recall vs Latency)min_similarity Threshold (0.7–0.8) เพื่อกรอง Match คุณภาพต่ำpg_stat_activity สำหรับ Vector Query ที่ช้าVACUUM ANALYZE บน document_chunks หลัง Bulk Ingestบทความนี้ครอบคลุม Retrieval Layer สำหรับ RAG Stack ที่สมบูรณ์:
กำลังสร้างระบบ RAG สำหรับเอกสารภายใน Workflow ด้านการปฏิบัติตาม PDPA หรือระบบ Support ลูกค้า? ติดต่อเราได้ที่ hello@simplico.net — เราให้บริการ Deploy Production RAG Stack สำหรับองค์กรในไทยและญี่ปุ่น
If you’re building a RAG pipeline or any application that needs semantic search, you’ll eventually need to decide where to store your embeddings. Dedicated vector databases (Pinecone, Qdrant, Weaviate) are one option. But for most teams — especially those already running PostgreSQL — pgvector is the faster, cheaper, and operationally simpler path.
Continue reading “pgvector Tutorial: Add Vector Search to PostgreSQL for RAG and Semantic Search”
大多数企业不会意识到自身存在身份管理问题——直到安全事故发生之后。
离职员工的账户仍在三个系统中保持活跃,因为没有人更新离职操作清单。某外包人员能够访问财务门户,因为六个月前申请了"临时"访问权限,工单从未关闭。一次网络钓鱼攻击得逞——不是因为安全防护薄弱,而是因为团队在管理24套独立的登录系统,没有人注意到其中一个根本没有启用MFA。
ほとんどの企業は、セキュリティ侵害が起きるまで自社のアイデンティティ問題に気づきません。