ในบทความนี้เราจะมาเจาะลึกเทคนิคการใช้ Metadata Filtering ใน Vector Database สำหรับระบบ RAG (Retrieval-Augmented Generation) ที่ช่วยให้การค้นหาข้อมูลแม่นยำมากขึ้น ลด hallucination และประหยัดค่าใช้จ่ายได้อย่างมีประสิทธิภาพ โดยเนื้อหาครอบคลุมตั้งแต่พื้นฐานจนถึงการนำไปใช้จริงใน Production

ทำไมต้องใช้ Metadata Filtering ใน RAG

ในระบบ RAG แบบดั้งเดิม ทุกครั้งที่ผู้ใช้ถามคำถาม ระบบจะทำ Semantic Search จาก Vector Database ทั้งหมด ทำให้ผลลัพธ์อาจมีเอกสารที่ไม่เกี่ยวข้องปนเข้ามา เช่น ถามเรื่องนโยบายปี 2024 แต่ได้เอกสารปี 2023 หรือถามเรื่องผลิตภัณฑ์ A แต่ได้เอกสารผลิตภัณฑ์ B

Metadata Filtering ช่วยให้เรากรองผลลัพธ์ตามข้อมูลเชิงบริบท เช่น วันที่ หมวดหมู่ ผู้เขียน หรือแผนก เพื่อให้ได้เอกสารที่ตรงใจมากที่สุด ลด Token Usage และเพิ่มความแม่นยำของคำตอบ

การเปรียบเทียบโครงสร้างข้อมูลแบบต่างๆ

ก่อนจะเริ่มต้นใช้งาน เรามาดูโครงสร้างข้อมูลที่เราจะใช้ในตัวอย่างนี้กัน โดยเราจะจัดเก็บเอกสารคู่มือการใช้งานผลิตภัณฑ์ 3 ประเภท ได้แก่ Hardware, Software และ Cloud Service

# โครงสร้างข้อมูลเอกสารที่จัดเก็บใน Vector Database
documents = [
    {
        "id": "doc_001",
        "content": "คู่มือการตั้งค่า Server แบบครบวงจร...",
        "metadata": {
            "category": "Hardware",
            "product": "Enterprise Server X500",
            "created_date": "2024-01-15",
            "department": "IT-Infrastructure",
            "version": "3.2"
        }
    },
    {
        "id": "doc_002",
        "content": "การใช้งาน Cloud Dashboard สำหรับผู้ดูแลระบบ...",
        "metadata": {
            "category": "Cloud Service",
            "product": "CloudDash Pro",
            "created_date": "2024-03-20",
            "department": "Cloud-Operations",
            "version": "2.1"
        }
    },
    {
        "id": "doc_003",
        "content": "การติดตั้งและใช้งาน IDE สำหรับนักพัฒนา...",
        "metadata": {
            "category": "Software",
            "product": "DevStudio Ultimate",
            "created_date": "2024-02-10",
            "department": "Development",
            "version": "5.0"
        }
    }
]

กรองเฉพาะเอกสารที่มี category = 'Software'

filtered = [doc for doc in documents if doc["metadata"]["category"] == "Software"] print(f"พบเอกสาร {len(filtered)} รายการ")

การตั้งค่า HolySheep AI API สำหรับ Embedding และ RAG

สำหรับการใช้งาน Vector Database ร่วมกับ LLM ในระบบ RAG เราจะใช้ HolySheep AI เป็น API Gateway เพราะให้บริการทั้ง Embedding และ Chat Completion ในราคาที่ประหยัดกว่ามาก คือเริ่มต้นเพียง $0.42/MTok สำหรับ DeepSeek V3.2 รวมถึงรองรับชำระเงินผ่าน WeChat และ Alipay อีกด้วย

import requests
import json
from datetime import datetime

การตั้งค่า HolySheep API

HOLYSHEEP_API_KEY = "YOUR_HOLYSHEEP_API_KEY" HOLYSHEEP_BASE_URL = "https://api.holysheep.ai/v1" class HolySheepRAG: def __init__(self, api_key: str): self.api_key = api_key self.base_url = HOLYSHEEP_BASE_URL self.headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } def create_embedding(self, text: str, model: str = "text-embedding-3-small"): """สร้าง Embedding vector สำหรับข้อความ""" response = requests.post( f"{self.base_url}/embeddings", headers=self.headers, json={"input": text, "model": model} ) if response.status_code == 200: return response.json()["data"][0]["embedding"] else: raise Exception(f"Embedding Error: {response.text}") def chat_completion(self, messages: list, model: str = "gpt-4o-mini", temperature: float = 0.3, max_tokens: int = 1000): """ส่งคำถามไปยัง LLM พร้อมบริบทจาก RAG""" response = requests.post( f"{self.base_url}/chat/completions", headers=self.headers, json={ "model": model, "messages": messages, "temperature": temperature, "max_tokens": max_tokens } ) if response.status_code == 200: return response.json()["choices"][0]["message"]["content"] else: raise Exception(f"Chat Error: {response.text}")

ทดสอบการเชื่อมต่อ

rag = HolySheepRAG(HOLYSHEEP_API_KEY) print("✅ เชื่อมต่อ HolySheep API สำเร็จ")

การสร้างระบบ Metadata Filtering แบบละเอียด

ต่อไปเราจะสร้าง Class สำหรับจัดการ Vector Database พร้อมระบบกรองข้อมูลตาม Metadata ที่ซับซ้อน รองรับการกรองหลายเงื่อนไขพร้อมกัน

import numpy as np
from typing import List, Dict, Any, Optional
from dataclasses import dataclass
from enum import Enum

class FilterOperator(Enum):
    EQ = "eq"           # เท่ากับ
    NE = "ne"           # ไม่เท่ากับ
    GT = "gt"           # มากกว่า
    GTE = "gte"         # มากกว่าหรือเท่ากับ
    LT = "lt"           # น้อยกว่า
    LTE = "lte"         # น้อยกว่าหรือเท่ากับ
    IN = "in"           # อยู่ใน list
    NOT_IN = "not_in"   # ไม่อยู่ใน list
    CONTAINS = "contains"  # มี substring

@dataclass
class FilterCondition:
    field: str
    operator: FilterOperator
    value: Any

class VectorStoreWithFilter:
    def __init__(self, rag_client: HolySheepRAG):
        self.rag = rag_client
        self.vectors = []  # เก็บ vector embeddings
        self.documents = []  # เก็บเอกสารต้นฉบับพร้อม metadata
    
    def add_document(self, content: str, metadata: Dict[str, Any]):
        """เพิ่มเอกสารพร้อมสร้าง embedding"""
        embedding = self.rag.create_embedding(content)
        doc_id = f"doc_{len(self.documents):04d}"
        
        self.vectors.append(embedding)
        self.documents.append({
            "id": doc_id,
            "content": content,
            "metadata": metadata
        })
        print(f"✅ เพิ่มเอกสาร {doc_id} แล้ว")
    
    def apply_filter(self, documents: List[Dict], 
                     conditions: List[FilterCondition]) -> List[Dict]:
        """กรองเอกสารตามเงื่อนไข metadata"""
        filtered = []
        for doc in documents:
            match = True
            for cond in conditions:
                value = doc["metadata"].get(cond.field)
                if value is None:
                    match = False
                    break
                
                if cond.operator == FilterOperator.EQ:
                    if value != cond.value:
                        match = False
                        break
                elif cond.operator == FilterOperator.GT:
                    if not (value > cond.value):
                        match = False
                        break
                elif cond.operator == FilterOperator.IN:
                    if value not in cond.value:
                        match = False
                        break
                elif cond.operator == FilterOperator.CONTAINS:
                    if cond.value not in str(value):
                        match = False
                        break
            if match:
                filtered.append(doc)
        return filtered
    
    def search(self, query: str, top_k: int = 5, 
               filters: Optional[List[FilterCondition]] = None) -> List[Dict]:
        """ค้นหาด้วย semantic search พร้อมกรอง metadata"""
        # สร้าง query embedding
        query_embedding = self.rag.create_embedding(query)
        
        # กรองเอกสารตาม metadata ก่อน (ถ้ามีเงื่อนไข)
        candidates = self.documents
        if filters:
            candidates = self.apply_filter(self.documents, filters)
            print(f"🔍 กรองเหลือ {len(candidates)} เอกสารจาก {len(self.documents)} รายการ")
        
        if not candidates:
            return []
        
        # คำนวณ cosine similarity
        scores = []
        for i, doc in enumerate(candidates):
            vec = self.vectors[self.documents.index(doc)]
            similarity = np.dot(query_embedding, vec) / (
                np.linalg.norm(query_embedding) * np.linalg.norm(vec)
            )
            scores.append((similarity, doc))
        
        # เรียงลำดับและเลือก top_k
        scores.sort(reverse=True)
        results = []
        for score, doc in scores[:top_k]:
            doc_copy = doc.copy()
            doc_copy["similarity_score"] = round(score, 4)
            results.append(doc_copy)
        
        return results

ตัวอย่างการใช้งาน

store = VectorStoreWithFilter(rag)

เพิ่มเอกสารตัวอย่าง

store.add_document( "คู่มือการตั้งค่า Server แบบครบวงจรสำหรับองค์กร...", {"category": "Hardware", "product": "Server X500", "price": 150000} ) store.add_document( "การใช้งาน Cloud Dashboard สำหรับผู้ดูแลระบบ...", {"category": "Cloud", "product": "CloudDash", "price": 5000} ) store.add_document( "การติดตั้ง IDE สำหรับนักพัฒนา Python...", {"category": "Software", "product": "DevStudio", "price": 2990} ) print(f"📦 จำนวนเอกสารทั้งหมด: {len(store.documents)}")

การค้นหาแบบละเอียดพร้อมตัวอย่างการใช้งานจริง

ต่อไปเราจะมาดูตัวอย่างการค้นหาที่ซับซ้อนขึ้น เช่น การค้นหาสินค้าที่มีราคาในช่วงที่กำหนด หรือการกรองตามหลายหมวดหมู่พร้อมกัน

# ตัวอย่างการค้นหาหลายรูปแบบ

1. ค้นหาสินค้าเฉพาะหมวด Software เท่านั้น

results = store.search( query="การติดตั้งและใช้งานโปรแกรม", filters=[FilterCondition("category", FilterOperator.EQ, "Software")] ) print("📌 ผลการค้นหาหมวด Software:") for r in results: print(f" - {r['id']}: {r['metadata']['product']} (score: {r['similarity_score']})")

2. ค้นหาสินค้าที่มีราคาต่ำกว่า 10,000 บาท

results = store.search( query="เครื่องมือสำหรับทำงาน", filters=[FilterCondition("price", FilterOperator.LT, 10000)] ) print("\n📌 ผลการค้นหาราคาต่ำกว่า 10,000:") for r in results: print(f" - {r['metadata']['product']}: {r['metadata']['price']} บาท")

3. ค้นหาจากหลายหมวดหมู่

results = store.search( query="คู่มือการใช้งาน", filters=[FilterCondition("category", FilterOperator.IN, ["Software", "Cloud"])] ) print("\n📌 ผลการค้นหาหมวด Software และ Cloud:") for r in results: print(f" - {r['metadata']['product']} ({r['metadata']['category']})")

การสร้าง RAG Pipeline สมบูรณ์

ตอนนี้เราจะนำทุกอย่างมารวมกันเป็น RAG Pipeline ที่สมบูรณ์ ซึ่งจะทำงานดังนี้ รับคำถามจากผู้ใช้ กรองข้อมูลตามเงื่อนไขที่กำหนด ค้นหาเอกสารที่เกี่ยวข้อง และส่งไปยัง LLM เพื่อสร้างคำตอบ

class RAGPipeline:
    def __init__(self, vector_store: VectorStoreWithFilter, 
                 rag_client: HolySheepRAG):
        self.store = vector_store
        self.rag = rag_client
    
    def query(self, user_question: str, 
              category_filter: Optional[str] = None,
              max_price: Optional[float] = None,
              top_k: int = 3) -> str:
        """ประมวลผลคำถามผ่าน RAG Pipeline"""
        
        # สร้างเงื่อนไขการกรอง
        filters = []
        if category_filter:
            filters.append(
                FilterCondition("category", FilterOperator.EQ, category_filter)
            )
        if max_price:
            filters.append(
                FilterCondition("price", FilterOperator.LT, max_price)
            )
        
        # ค้นหาเอกสารที่เกี่ยวข้อง
        results = self.store.search(user_question, top_k=top_k, filters=filters)
        
        if not results:
            return "ไม่พบเอกสารที่เกี่ยวข้องกับคำถามของคุณ"
        
        # รวบรวมบริบทจากเอกสารที่พบ
        context_parts = []
        for i, doc in enumerate(results, 1):
            context_parts.append(
                f"[เอกสาร {i}] {doc['content']}\n"
                f"หมวดหมู่: {doc['metadata'].get('category', 'N/A')}\n"
                f"ผลิตภัณฑ์: {doc['metadata'].get('product', 'N/A')}"
            )
        context = "\n\n".join(context_parts)
        
        # สร้าง prompt สำหรับ LLM
        system_prompt = """คุณเป็นผู้ช่วยตอบคำถามจากคู่มือการใช้งานผลิตภัณฑ์ 
ให้ตอบคำถามโดยอิงจากบริบทที่ให้มาเท่านั้น 
ถ้าไม่แน่ใจให้ตอบว่าไม่มีข้อมูลในเอกสาร"""
        
        messages = [
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": f"บริบท:\n{context}\n\nคำถาม: {user_question}"}
        ]
        
        # ส่งไปยัง LLM
        answer = self.rag.chat_completion(messages, model="gpt-4o-mini")
        
        return answer

ทดสอบ RAG Pipeline

pipeline = RAGPipeline(store, rag)

ทดสอบการค้นหาแบบกรองหมวดหมู่

answer = pipeline.query( "วิธีการติดตั้งอย่างไร", category_filter="Software" ) print(f"💬 คำตอบ: {answer}")

การวัดผลและปรับปรุงประสิทธิภาพ

เพื่อให้ระบบ RAG ทำงานได้อย่างมีประสิทธิภาพ เราควรวัดผลอย่างสม่ำเสมอ โดยมีเมตริกสำคัญดังนี้

ข้อผิดพลาดที่พบบ่อยและวิธีแก้ไข

1. ปัญหา: ได้ผลลัพธ์ว่างเปล่าหลังการกรอง Metadata

สาเหตุ: เงื่อนไขการกรองเข้มเกินไปจนไม่มีเอกสารตรงกับเงื่อนไข หรือชื่อฟิลด์ใน FilterCondition ไม่ตรงกับ metadata จริง

# ❌ วิธีที่ผิด: ชื่อฟิลด์ไม่ตรง
filters = [FilterCondition("Category", FilterOperator.EQ, "Software")]

✅ วิธีที่ถูก: ตรวจสอบชื่อฟิลด์ให้ตรงกับ metadata

print("ชื่อฟิลด์ที่มี:", list(store.documents[0]["metadata"].keys()))

ผลลัพธ์: ['category', 'product', 'price']

filters = [FilterCondition("category", FilterOperator.EQ, "Software")]

✅ เพิ่ม fallback: ถ้ากรองแล้วว่าง ให้ค้นหาทั้งหมดแล้วแจ้งผู้ใช้

results = store.search(query, filters=filters) if not results: results = store.search(query, filters=None) print("⚠️ ไม่พบเอกสารตรงเงื่อนไข ขยายการค้นหาเป็นทั้งหมด")

2. ปัญหา: Latency สูงเกินไป (>200ms)

สาเหตุ: การสร้าง Embedding ทีละข้อความทำให้เกิด Round Trip หลายครั้ง หรือเลือก model ที่ใหญ่เกินไป

# ❌ วิธีที่ผิด: สร้าง embedding ทีละข้อความในลูป
for doc in many_documents:
    embedding = self.rag.create_embedding(doc["content"])  # เพิ่ม latency

✅ วิธีที่ถูก: Batch embedding - ส่งหลายข้อความพร้อมกัน

def batch_create_embeddings(self, texts: List[str], batch_size: int = 100) -> List[List[float]]: all_embeddings = [] for i in range(0, len(texts), batch_size): batch = texts[i:i + batch_size] response = requests.post( f"{self.base_url}/embeddings", headers=self.headers, json={"input": batch, "model": "text-embedding-3-small"} ) embeddings = [item["embedding"] for item in response.json()["data"]] all_embeddings.extend(embeddings) return all_embeddings

✅ เปลี่ยน model เป็น embedding model ที่เล็กลงแต่เร็วขึ้น

embedding = self.rag.create_embedding(text, model="text-embedding-3-small")

แทน model="text-embedding-3-large"

3. ปัญหา: RAG ให้คำตอบที่ไม่ตรงกับบริบท (Hallucination)

สาเหตุ: ไม่ได้กรองเอกสารที่ไม่เกี่ยวข้องออกก่อนส่งให้ LLM หรือใช้ค่า Temperature สูงเกินไป

# ❌ วิธีที่ผิด: ใช้ temperature สูงทำให้ LLM แต่งเติมข้อมูล
answer = self.rag.chat_completion(messages, temperature=0.9)

✅ วิธีที่ถูก: กรองเอกสารด้วย similarity threshold

def search_with_threshold(self, query: str, similarity_threshold: float = 0.7) -> List[Dict]: results = self.search(query, top_k=10) # กรองเฉพาะเอกสารที่มีความคล้ายคลึงสูงพอ filtered = [r for r in results if r["similarity_score"] >= similarity_threshold] if not filtered: # ถ้าไม่มีผลลัพธ์ที่ดีพอ แจ้งผู้ใช้ raise ValueError("ไม่พบเอกสารที่เกี่ยวข้องเพียงพอ กรุณาปรับคำถาม") return filtered

✅ ลด temperature เพื่อความแม่นยำ

answer = self.rag.chat_completion(messages, model="gpt-4o-mini", temperature=0.2)

✅ เพิ่ม citation ในคำตอบ

def generate_with_citation(self, user_question: str, results: List[Dict]) -> str: # ... ค้นหาเอกสาร ... # สร้างคำตอบพร้อมอ้างอิงแหล่งที่มา answer = self.rag.chat_completion(messages, temperature=0.1) # เพิ่ม footnote แหล่งที่มา sources = "\n\n📚 แหล่งอ้างอิง:\n" for i, doc in enumerate(results, 1): sources += f"[{i}] {doc['id']} - {doc['metadata']['product']}\n" return answer + sources

สรุปและข้อแนะนำ

การใช้ Metadata Filtering ในระบบ RAG ช่วยให้เราสามารถค้นหาข้อมูลได้แม่นยำมากขึ้น ลดการใช้ Token โดยไม่จำเป็น และปรับปรุงคุณภาพคำตอบจาก LLM ได้อย่างมีนัยสำคัญ โดยปัจจัยสำคัญที่ควรพิจารณาคือ การออกแบบโครงสร้าง Metadata ให้ครอบคลุม การเลือกใช้ Operator การ