บทความนี้จะพาทุกท่านไปสร้างเครื่องมือค้นหาที่รองรับทั้งภาพและข้อความในการค้นหาเดียวกัน โดยใช้เทคนิค 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']}")