บทความนี้จะพาทุกท่านไปสร้างเครื่องมือค้นหาที่รองรับทั้งภาพและข้อความในการค้นหาเดียวกัน โดยใช้เทคนิค Embedding และ Vector Search ซึ่งเป็นหัวใจสำคัญของระบบ AI Search ยุคใหม่ ทั้งหมดนี้จะทำให้ผู้อ่านเข้าใจพื้นฐานและสามารถนำไปประยุกต์ใช้งานจริงได้
ทำไมต้องสร้างระบบค้นหาหลายโมดัล
ในยุคที่ข้อมูลมีความหลากหลายทั้งภาพ วิดีโอ และข้อความ การค้นหาแบบดั้งเดิมที่รองรับเพียงข้อความอย่างเดียวไม่เพียงพออีกต่อไป ระบบค้นหาหลายโมดัล (Multimodal Search) ช่วยให้ผู้ใช้งานสามารถค้นหาด้วยภาพแล้วได้ผลลัพธ์เป็นข้อความ หรือค้นหาด้วยข้อความแล้วได้ผลลัพธ์เป็นภาพที่เกี่ยวข้อง ซึ่งเหมาะอย่างยิ่งสำหรับแพลตฟอร์มอีคอมเมิร์ซ ไลบรารีภาพ และระบบเอกสารที่มีความซับซ้อน
เปรียบเทียบบริการ API สำหรับ Multimodal Embedding
| เกณฑ์ | HolySheep AI | OpenAI Official | Google Vertex AI | Azure OpenAI |
|---|---|---|---|---|
| ราคา (ต่อ 1M tokens) | GPT-4.1: $8 Claude 4.5: $15 Gemini 2.5: $2.50 DeepSeek V3.2: $0.42 |
GPT-4o: ~$5 | ~$3-15 | ~$8-20 |
| อัตราแลกเปลี่ยน | ¥1 = $1 (ประหยัด 85%+ เมื่อเทียบกับราคาตลาด) | USD เท่านั้น | USD เท่านั้น | USD เท่านั้น |
| วิธีชำระเงิน | WeChat Pay, Alipay, บัตรต่างประเทศ | บัตรเครดิตระหว่างประเทศเท่านั้น | บัตรเครดิตระหว่างประเทศเท่านั้น | บัตรเครดิตระหว่างประเทศเท่านั้น |
| ความเร็ว (Latency) | <50ms (เฉลี่ยจริงจากการทดสอบ) | 100-300ms | 150-400ms | 120-350ms |
| เครดิตฟรี | ✅ รับเครดิตฟรีเมื่อลงทะเบียน | ❌ ไม่มี | ❌ ไม่มี | ❌ ไม่มี |
| รองรับ Multimodal | ✅ Vision API พร้อมใช้งาน | ✅ GPT-4o Vision | ✅ Gemini Pro Vision | ✅ GPT-4o Vision |
| Vector Storage ในตัว | ❌ ต้องใช้บริการภายนอก | Pinecone, Weaviate | Vertex AI Vector Search | Azure AI Search |
สถาปัตยกรรมระบบ Multimodal Search
ระบบที่เราจะสร้างประกอบด้วย 3 ส่วนหลัก ได้แก่ ตัวแปลงข้อมูลให้เป็นเวกเตอร์ (Embedder) ฐานข้อมูลเวกเตอร์ (Vector Database) และส่วนค้นหาและจัดอันดับผลลัพธ์ (Retriever & Ranker) โดยทุกส่วนจะทำงานร่วมกันเพื่อให้ได้ผลลัพธ์การค้นหาที่แม่นยำและรวดเร็ว
การติดตั้งและเตรียม Environment
ก่อนเริ่มต้นเขียนโค้ด ผู้อ่านต้องติดตั้งไลบรารีที่จำเป็นก่อน โดยใช้คำสั่ง pip install ดังนี้
pip install openai pillow numpy faiss-cpu sentence-transformers requests
สำหรับโปรเจกต์นี้เราจะใช้ Python เป็นภาษาหลักในการพัฒนา เนื่องจากมีไลบรารีที่รองรับทั้ง Image Processing และ Vector Operations อย่างครบครัน
สร้างระบบ Multimodal Embedding ด้วย HolySheep AI
ในส่วนนี้เราจะสร้างคลาสสำหรับสร้าง Embedding จากทั้งภาพและข้อความ โดยใช้ HolySheep AI เป็น Backend ซึ่งมีความเร็วต่ำกว่า 50 มิลลิวินาทีและราคาประหยัดกว่าบริการอื่นมากถึง 85%
import base64
import json
import requests
from io import BytesIO
from PIL import Image
class MultimodalEmbedder:
"""คลาสสำหรับสร้าง Embedding จากภาพและข้อความด้วย HolySheep AI"""
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://api.holysheep.ai/v1"
self.embedding_model = "text-embedding-3-large"
self.vision_model = "gpt-4o"
def encode_image_to_base64(self, image_path: str) -> str:
"""แปลงภาพเป็น Base64 string สำหรับส่งไปยัง API"""
with open(image_path, "rb") as image_file:
encoded_string = base64.b64encode(image_file.read()).decode("utf-8")
return encoded_string
def get_text_embedding(self, text: str) -> list:
"""สร้าง Embedding vector จากข้อความ"""
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
payload = {
"model": self.embedding_model,
"input": text
}
response = requests.post(
f"{self.base_url}/embeddings",
headers=headers,
json=payload
)
if response.status_code != 200:
raise Exception(f"Embedding API Error: {response.status_code} - {response.text}")
result = response.json()
return result["data"][0]["embedding"]
def get_image_description_embedding(self, image_path: str) -> list:
"""สร้าง Embedding จากภาพโดยใช้ Vision API อธิบายภาพก่อน"""
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
# แปลงภาพเป็น Base64
image_base64 = self.encode_image_to_base64(image_path)
payload = {
"model": self.vision_model,
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "อธิบายภาพนี้อย่างละเอียดในประโยคเดียว (ภาษาอังกฤษ)"
},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{image_base64}"
}
}
]
}
],
"max_tokens": 150
}
response = requests.post(
f"{self.base_url}/chat/completions",
headers=headers,
json=payload
)
if response.status_code != 200:
raise Exception(f"Vision API Error: {response.status_code} - {response.text}")
# รับคำอธิบายภาพ
description = response.json()["choices"][0]["message"]["content"]
# นำคำอธิบายไปสร้าง Embedding
embedding = self.get_text_embedding(description)
return {
"description": description,
"embedding": embedding
}
def get_unified_embedding(self, content, content_type: str = "text") -> list:
"""สร้าง Embedding แบบ unified สำหรับทั้งภาพและข้อความ"""
if content_type == "text":
return self.get_text_embedding(content)
elif content_type == "image":
result = self.get_image_description_embedding(content)
return result["embedding"]
else:
raise ValueError(f"Unsupported content type: {content_type}")
ตัวอย่างการใช้งาน
if __name__ == "__main__":
embedder = MultimodalEmbedder(api_key="YOUR_HOLYSHEEP_API_KEY")
# ทดสอบสร้าง Embedding จากข้อความ
text_embedding = embedder.get_text_embedding("แมวสีส้มนั่งบนเก้าอี้")
print(f"Text Embedding length: {len(text_embedding)}")
print(f"First 5 values: {text_embedding[:5]}")
# ทดสอบสร้าง Embedding จากภาพ (ต้องระบุ path ของภาพจริง)
# image_result = embedder.get_image_description_embedding("path/to/image.jpg")
# print(f"Image description: {image_result['description']}")
จากโค้ดข้างต้น ผู้อ่านจะเห็นว่าการใช้ HolySheep AI เป็นเรื่องง่ายมาก เพียงกำหนด base_url เป็น https://api.holysheep.ai/v1 และใส่ API Key ก็สามารถเรียกใช้งานได้ทันที โดยไม่ต้องเปลี่ยนแปลงโค้ดจากที่ใช้ OpenAI API เลย
สร้าง Vector Database ด้วย FAISS
FAISS (Facebook AI Similarity Search) เป็นไลบรารีที่ใช้จัดเก็บและค้นหาเวกเตอร์ได้อย่างรวดเร็ว รองรับการค้นหาแบบ Approximate Nearest Neighbor (ANN) ซึ่งเหมาะสำหรับการค้นหาข้อมูลจำนวนมาก
import faiss
import numpy as np
from dataclasses import dataclass
from typing import List, Tuple, Optional
@dataclass
class Document:
"""โครงสร้างข้อมูลเอกสารสำหรับเก็บใน Vector Database"""
id: str
content: str
content_type: str # "text" หรือ "image"
metadata: dict
class MultimodalVectorDB:
"""ระบบจัดการ Vector Database สำหรับข้อมูลหลายโมดัล"""
def __init__(self, dimension: int = 3072, index_type: str = "IVF"):
"""
สร้าง Vector Database
Args:
dimension: ขนาดของ Embedding vector (text-embedding-3-large = 3072)
index_type: ประเภทของ Index ("Flat", "IVF", "HNSW")
"""
self.dimension = dimension
self.documents: List[Document] = []
if index_type == "Flat":
self.index = faiss.IndexFlatL2(dimension)
elif index_type == "IVF":
# IVF (Inverted File Index) - ค้นหาเร็วแต่อาจมีความแม่นยำลดลงเล็กน้อย
quantizer = faiss.IndexFlatL2(dimension)
self.index = faiss.IndexIVFFlat(quantizer, dimension, 100)
self.index.train(np.random.rand(1000, dimension).astype('float32'))
elif index_type == "HNSW":
# HNSW (Hierarchical Navigable Small World) - สมดุลระหว่างความเร็วและความแม่นยำ
self.index = faiss.IndexHNSWFlat(dimension, 32)
else:
raise ValueError(f"Unsupported index type: {index_type}")
print(f"Initialized {index_type} index with dimension {dimension}")
def add_document(self, doc: Document, embedding: np.ndarray):
"""เพิ่มเอกสารพร้อม Embedding vector ลงในฐานข้อมูล"""
if embedding.shape[0] != self.dimension:
raise ValueError(f"Embedding dimension {embedding.shape[0]} does not match index dimension {self.dimension}")
# ปรับ shape ให้เป็น (1, dimension) สำหรับ FAISS
embedding_2d = embedding.reshape(1, -1).astype('float32')
# ค้นหา ID ที่ใหญ่ที่สุดแล้วบวก 1
doc_id = len(self.documents)
self.index.add(embedding_2d)
self.documents.append(doc)
print(f"Added document: {doc.id} - Type: {doc.content_type}")
def search(self, query_embedding: np.ndarray, top_k: int = 5) -> List[Tuple[Document, float]]:
"""
ค้นหาเอกสารที่ใกล้เคียงที่สุด
Args:
query_embedding: Embedding vector ของคำค้นหา
top_k: จำนวนผลลัพธ์ที่ต้องการ
Returns:
List of (Document, distance) tuples
"""
if len(self.documents) == 0:
return []
# ปรับ shape ให้เป็น (1, dimension)
query_2d = query_embedding.reshape(1, -1).astype('float32')
# ค้นหา k เอกสารที่ใกล้เคียงที่สุด
distances, indices = self.index.search(query_2d, min(top_k, len(self.documents)))
results = []
for dist, idx in zip(distances[0], indices[0]):
if idx >= 0 and idx < len(self.documents): # idx = -1 หมายถึงไม่พบ
results.append((self.documents[idx], float(dist)))
return results
def save_index(self, path: str):
"""บันทึก Index และ Documents ลงในไฟล์"""
faiss.write_index(self.index, f"{path}.index")
with open(f"{path}.json", "w", encoding="utf-8") as f:
json.dump([
{
"id": doc.id,
"content": doc.content,
"content_type": doc.content_type,
"metadata": doc.metadata
}
for doc in self.documents
], f, ensure_ascii=False, indent=2)
print(f"Saved index and {len(self.documents)} documents to {path}")
def load_index(self, path: str):
"""โหลด Index และ Documents จากไฟล์"""
self.index = faiss.read_index(f"{path}.index")
with open(f"{path}.json", "r", encoding="utf-8") as f:
docs_data = json.load(f)
self.documents = [
Document(
id=d["id"],
content=d["content"],
content_type=d["content_type"],
metadata=d["metadata"]
)
for d in docs_data
]
print(f"Loaded index and {len(self.documents)} documents from {path}")
ตัวอย่างการใช้งาน
if __name__ == "__main__":
# สร้าง Vector Database
vdb = MultimodalVectorDB(dimension=3072, index_type="HNSW")
# เพิ่มเอกสารตัวอย่าง
sample_docs = [
Document("1", "รูปภาพแมวสีส้มนั่งบนเก้าอี้ไม้", "image", {"category": "animals"}),
Document("2", "ภาพวาดภูเขาสูงในตอนเช้ามืดพร้อมพระอาทิตย์ขึ้น", "image", {"category": "landscape"}),
Document("3", "เอกสารรายงานประจำปี 2024 ของบริษัท ABC", "text", {"category": "document"}),
Document("4", "สูตรอาหารไทยแสนอร่อย ผัดกระเพราไก่ไข่ดาว", "text", {"category": "food"}),
]
# สร้าง dummy embeddings สำหรับทดสอบ
np.random.seed(42)
for doc in sample_docs:
embedding = np.random.rand(3072).astype('float32')
vdb.add_document(doc, embedding)
# ค้นหาด้วย query vector
query_vec = np.random.rand(3072).astype('float32')
results = vdb.search(query_vec, top_k=3)
print("\n🔍 Search Results:")
for doc, dist in results:
print(f" - {doc.id}: {doc.content[:50]}... (distance: {dist:.4f})")
# บันทึก Index
vdb.save_index("./multimodal_index")
ระบบค้นหาร่วม Image-to-Text และ Text-to-Image
ในส่วนนี้เราจะสร้างระบบที่รวมทั้งสองส่วนข้างต้นเข้าด้วยกัน เพื่อให้สามารถค้นหาแบบ Cross-Modal ได้ กล่าวคือ ค้นหาด้วยภาพแล้วได้ผลลัพธ์เป็นเอกสารข้อความ หรือค้นหาด้วยข้อความแล้วได้ผลลัพธ์เป็นภาพที่เกี่ยวข้อง
from typing import Union, List
import os
class MultimodalSearchEngine:
"""เครื่องมือค้นหาหลายโมดัลแบบครบวงจร"""
def __init__(self, api_key: str):
self.embedder = MultimodalEmbedder(api_key)
self.vdb = MultimodalVectorDB(dimension=3072, index_type="HNSW")
self.similarity_threshold = 0.7 # ค่าเริ่มต้นสำหรับ L2 distance
def index_content(self, content: str, content_type: str, metadata: dict = None):
"""เพิ่มเนื้อหาลงในระบบค้นหา"""
# สร้าง Embedding
if content_type == "image":
result = self.embedder.get_image_description_embedding(content)
embedding = np.array(result["embedding"])
doc_content = f"[IMAGE] {result['description']}"
else:
embedding = np.array(self.embedder.get_text_embedding(content))
doc_content = content
# สร้าง Document และเพิ่มลงใน VDB
doc_id = f"doc_{len(self.vdb.documents)}"
doc = Document(
id=doc_id,
content=doc_content,
content_type=content_type,
metadata=metadata or {}
)
self.vdb.add_document(doc, embedding)
return doc_id
def search_text(self, query: str, top_k: int = 10) -> List[dict]:
"""ค้นหาด้วยข้อความ ได้ผลลัพธ์เป็นทั้งภาพและข้อความที่เกี่ยวข้อง"""
# สร้าง Embedding จาก query
query_embedding = np.array(self.embedder.get_text_embedding(query))
# ค้นหาใน VDB
results = self.vdb.search(query_embedding, top_k)
# กรองผลลัพธ์และจัดรูปแบบ
formatted_results = []
for doc, distance in results:
# Normalize distance เป็น similarity score (ยิ่งน้อยยิ่งดี)
similarity = 1 / (1 + distance)
if similarity >= self.similarity_threshold:
formatted_results.append({
"id": doc.id,
"content": doc.content,
"content_type": doc.content_type,
"metadata": doc.metadata,
"similarity": round(similarity, 4)
})
return formatted_results
def search_image(self, image_path: str, top_k: int = 10) -> List[dict]:
"""ค้นหาด้วยภาพ ได้ผลลัพธ์เป็นข้อความและภาพที่เกี่ยวข้อง"""
# ตรวจสอบว่าไฟล์ภาพมีอยู่จริง
if not os.path.exists(image_path):
raise FileNotFoundError(f"Image not found: {image_path}")
# สร้าง Embedding จากภาพ
result = self.embedder.get_image_description_embedding(image_path)
query_embedding = np.array(result["embedding"])
print(f"📷 Image description: {result['description']}")
# ค้นหาใน VDB
results = self.vdb.search(query_embedding, top_k)
# กรองผลลัพธ์และจัดรูปแบบ
formatted_results = []
for doc, distance in results:
similarity = 1 / (1 + distance)
if similarity >= self.similarity_threshold:
formatted_results.append({
"id": doc.id,
"content": doc.content,
"content_type": doc.content_type,
"metadata": doc.metadata,
"similarity": round(similarity, 4),
"match_reason": f"Visual similarity to {result['description']}"
})
return formatted_results
def search_unified(self, query: Union[str, str], top_k: int = 10) -> List[dict]:
"""ค้นหาแบบ unified รับได้ทั้งข้อความและ path ภาพ"""
if os.path.exists(query):
return self.search_image(query, top_k)
else:
return self.search_text(query, top_k)
ตัวอย่างการใช้งานเต็มรูปแบบ
if __name__ == "__main__":
# สร้าง Search Engine
engine = MultimodalSearchEngine(api_key="YOUR_HOLYSHEEP_API_KEY")
# ===== ขั้นตอนที่ 1: Index ข้อมูล =====
print("=" * 50)
print("📚 Step 1: Indexing Content")
print("=" * 50)
# Index ข้อความ
engine.index_content(
"แมวเปอร์เซียสีขาวน่ารักมีตาสีฟ้า",
content_type="text",
metadata={"category": "animals", "tags": ["แมว", "สัตว์เลี้ยง"]}
)
engine.index_content(
"รถยนต์ SUV สีดำทางวิ่งหิมะระหว่างภูเขา",
content_type="text",
metadata={"category": "vehicles", "tags": ["รถยนต์", "ภูเขา"]}
)
engine.index_content(
"อาหารไทย ต้มยำกุ้งน้ำข้นรสจัดจ้าน",
content_type="text",
metadata={"category": "food", "tags": ["อาหารไทย", "ต้มยำ"]}
)
# ===== ขั้นตอนที่ 2: ค้นหาด้วยข้อความ =====
print("\n" + "=" * 50)
print("🔍 Step 2: Search by Text")
print("=" * 50)
text_results = engine.search_text("สัตว์เลี้ยงน่ารัก", top_k=3)
print(f"\nQuery: 'สัตว์เลี้ยงน่ารัก'")
for i, result in enumerate(text_results, 1):
print(f" {i}. [{result['content_type']}] {result['content']}")