ในโลกของ AI ที่ต้องการความแม่นยำสูง การค้นหาข้อมูลจากตาราง (Structured Data) และเอกสาร (Unstructured Data) พร้อมกันเป็นความท้าทายที่ใหญ่หลวง บทความนี้จะพาคุณเข้าใจวิธีการสร้างระบบ RAG แบบ Hybrid ที่รวมข้อมูลทั้งสองประเภทเข้าด้วยกัน พร้อมแบ่งปันประสบการณ์การย้ายจากระบบเดิมมาสู่ HolySheep AI ที่ประหยัดค่าใช้จ่ายได้ถึง 85%

ทำไมต้อง Hybrid RAG สำหรับข้อมูลตาราง?

จากประสบการณ์ที่พัฒนาระบบ Q&A สำหรับฐานข้อมูลผลิตภัณฑ์ของบริษัท พบว่าการใช้ RAG แบบเดียวไม่เพียงพอ ตัวอย่างเช่น ผู้ใช้ถามว่า "สินค้า X มีราคาเท่าไหร่ และมีรีวิวที่ดีที่สุดอะไรบ้าง" — คำตอบต้องดึงข้อมูลจากตารางราคา (Structured) และบทวิจารณ์ (Unstructured) พร้อมกัน

สถาปัตยกรรมระบบ Hybrid RAG

┌─────────────────────────────────────────────────────────────┐
│                    Hybrid RAG Architecture                    │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│   ┌──────────────┐    ┌──────────────┐                      │
│   │   Structured │    │ Unstructured │                      │
│   │    (Table)   │    │   (Docs)     │                      │
│   └──────┬───────┘    └──────┬───────┘                      │
│          │                   │                              │
│          ▼                   ▼                              │
│   ┌──────────────┐    ┌──────────────┐                      │
│   │  Table Chunk │    │  Text Chunk  │                      │
│   │   + Schema   │    │  + Metadata  │                      │
│   └──────┬───────┘    └──────┬───────┘                      │
│          │                   │                              │
│          └────────┬──────────┘                              │
│                   ▼                                         │
│          ┌──────────────┐                                   │
│          │  Vector Index │                                   │
│          │   (Unified)   │                                   │
│          └──────┬───────┘                                   │
│                 │                                           │
│                 ▼                                           │
│          ┌──────────────┐                                   │
│          │  Reranker    │                                   │
│          │   (Cross     │                                   │
│          │   Encoder)   │                                   │
│          └──────┬───────┘                                   │
│                 │                                           │
│                 ▼                                           │
│          ┌──────────────┐                                   │
│          │ LLM Response │                                   │
│          │  Generator   │                                   │
│          └──────────────┘                                   │
└─────────────────────────────────────────────────────────────┘

ขั้นตอนการติดตั้งและย้ายระบบ

1. ติดตั้ง Dependencies ที่จำเป็น

npm install @holysheepai/sdk langchain langchain-community \
  pg vectorize-types zod exceljs papaparse

สำหรับ TypeScript ต้องติดตั้ง types ด้วย:

npm install -D @types/papaparse @types/exceljs

2. โค้ดสำหรับโหลดและประมวลผลข้อมูลตาราง

ด้านล่างคือโค้ดที่ทีมใช้ในการประมวลผลข้อมูลจาก Excel และ CSV เพื่อสร้าง Embedding สำหรับ Hybrid Search:

import { HolySheepAI } from '@holysheepai/sdk';
import * as Papa from 'papaparse';
import * as ExcelJS from 'exceljs';

const client = new HolySheepAI({
  apiKey: process.env.HOLYSHEEP_API_KEY!,
  baseURL: 'https://api.holysheep.ai/v1'
});

interface TableRow {
  [key: string]: string | number | boolean;
}

interface ProcessedChunk {
  content: string;
  metadata: {
    source: string;
    rowIndex: number;
    schema: string;
    dataType: 'structured';
  };
  embedding?: number[];
}

async function processCSVTable(filePath: string): Promise<ProcessedChunk[]> {
  const fileContent = await Bun.file(filePath).text();
  
  return new Promise((resolve, reject) => {
    const chunks: ProcessedChunk[] = [];
    
    Papa.parse(fileContent, {
      header: true,
      dynamicTyping: true,
      complete: async (results) => {
        const headers = results.meta.fields || [];
        
        for (let i = 0; i < results.data.length; i++) {
          const row = results.data[i] as TableRow;
          const schemaDescription = headers.map(h => ${h}: ${typeof row[h]}).join(', ');
          
          // สร้าง text representation ของแถว
          const rowText = headers
            .map(h => ${h} = ${row[h]})
            .join(' | ');
          
          chunks.push({
            content: Row ${i + 1}: ${rowText},
            metadata: {
              source: filePath,
              rowIndex: i,
              schema: schemaDescription,
              dataType: 'structured'
            }
          });
        }
        
        resolve(chunks);
      },
      error: reject
    });
  });
}

async function processExcelTable(filePath: string): Promise<ProcessedChunk[]> {
  const workbook = new ExcelJS.Workbook();
  await workbook.xlsx.readFile(filePath);
  
  const chunks: ProcessedChunk[] = [];
  
  workbook.eachSheet((worksheet, sheetId) => {
    const headers: string[] = [];
    
    worksheet.eachRow((row, rowNumber) => {
      const values = row.values as (string | number | null)[];
      
      if (rowNumber === 1) {
        headers.push(...values.map(v => String(v ?? 'unnamed')));
        return;
      }
      
      const rowData: TableRow = {};
      headers.forEach((h, idx) => {
        rowData[h] = values[idx] ?? '';
      });
      
      const rowText = headers
        .map((h, idx) => ${h} = ${rowData[h]})
        .join(' | ');
      
      chunks.push({
        content: Sheet ${sheetId}, Row ${rowNumber}: ${rowText},
        metadata: {
          source: ${filePath}#sheet${sheetId},
          rowIndex: rowNumber,
          schema: headers.map(h => ${h}: string).join(', '),
          dataType: 'structured'
        }
      });
    });
  });
  
  return chunks;
}

// ฟังก์ชันหลักสำหรับ embedding ข้อมูลตาราง
async function embedTableData(chunks: ProcessedChunk[]): Promise<void> {
  const texts = chunks.map(c => c.content);
  
  // ใช้ HolySheep API สำหรับ batch embedding
  const embeddings = await client.embeddings.create({
    model: 'text-embedding-3-small',
    input: texts,
    batchSize: 100
  });
  
  // เก็บ embeddings พร้อม metadata ลงใน vector store
  // (ใช้ Pinecone, Weaviate, หรือ Qdrant ตามความเหมาะสม)
  for (let i = 0; i < chunks.length; i++) {
    chunks[i].embedding = embeddings.data[i].embedding;
    await saveToVectorStore(chunks[i]);
  }
  
  console.log(✅ Embed เสร็จสิ้น: ${chunks.length} รายการ);
}

// ตัวอย่างการใช้งาน
async function main() {
  const csvChunks = await processCSVTable('./data/products.csv');
  const excelChunks = await processExcelTable('./data/inventory.xlsx');
  
  const allChunks = [...csvChunks, ...excelChunks];
  await embedTableData(allChunks);
}

main().catch(console.error);

3. การค้นหาแบบ Hybrid พร้อม Reranking

หลังจากประมวลผลข้อมูลเสร็จ ต่อไปคือการสร้างระบบค้นหาที่รวม Structured และ Unstructured เข้าด้วยกัน:

import { HolySheepAI } from '@holysheepai/sdk';

interface SearchResult {
  content: string;
  score: number;
  metadata: Record<string, any>;
  dataType: 'structured' | 'unstructured';
}

interface HybridQueryResult {
  structuredResults: SearchResult[];
  unstructuredResults: SearchResult[];
  mergedResults: SearchResult[];
}

class HybridRAGRetriever {
  private client: HolySheepAI;
  
  constructor(apiKey: string) {
    this.client = new HolySheepAI({
      apiKey,
      baseURL: 'https://api.holysheep.ai/v1'
    });
  }
  
  async semanticSearch(
    query: string, 
    filterType?: 'structured' | 'unstructured'
  ): Promise<{embedding: number[], query: string}> {
    // สร้าง query embedding
    const response = await this.client.embeddings.create({
      model: 'text-embedding-3-small',
      input: query
    });
    
    return {
      embedding: response.data[0].embedding,
      query
    };
  }
  
  async structuredQuery(
    tableName: string,
    conditions: Record<string, any>
  ): Promise<SearchResult[]> {
    // ค้นหาจาก structured data ด้วย SQL-like query
    const whereClause = Object.entries(conditions)
      .map(([key, value]) => ${key} = '${value}')
      .join(' AND ');
    
    // ดึงข้อมูลจาก PostgreSQL หรือ database ที่รองรับ
    const results = await queryPostgres(
      SELECT * FROM ${tableName} WHERE ${whereClause}
    );
    
    return results.map(row => ({
      content: Object.entries(row)
        .map(([k, v]) => ${k}: ${v})
        .join(' | '),
      score: 1.0, // exact match = score 1.0
      metadata: row,
      dataType: 'structured' as const
    }));
  }
  
  async hybridSearch(
    query: string,
    topK: number = 10,
    structuredWeight: number = 0.4,
    unstructuredWeight: number = 0.6
  ): Promise<HybridQueryResult> {
    // 1. Semantic search สำหรับทั้งสองประเภท
    const { embedding, query: processedQuery } = await this.semanticSearch(query);
    
    // 2. Vector search จาก Pinecone/Weaviate
    const [structuredVectors, unstructuredVectors] = await Promise.all([
      vectorStore.search(embedding, { 
        filter: { dataType: 'structured' }, 
        topK: topK * 2 
      }),
      vectorStore.search(embedding, { 
        filter: { dataType: 'unstructured' }, 
        topK: topK * 2 
      })
    ]);
    
    // 3. Cross-encoder reranking ด้วย HolySheep
    const allCandidates = [
      ...structuredVectors.map(v => ({ ...v, dataType: 'structured' as const })),
      ...unstructuredVectors.map(v => ({ ...v, dataType: 'unstructured' as const }))
    ];
    
    const reranked = await this.rerankWithCrossEncoder(
      processedQuery,
      allCandidates
    );
    
    // 4. รวมผลลัพธ์ตามน้ำหนักที่กำหนด
    const mergedResults = this.mergeByWeight(
      structuredVectors,
      unstructuredVectors,
      structuredWeight,
      unstructuredWeight,
      topK
    );
    
    return {
      structuredResults: structuredVectors,
      unstructuredResults: unstructuredVectors,
      mergedResults
    };
  }
  
  private async rerankWithCrossEncoder(
    query: string,
    candidates: SearchResult[]
  ): Promise<SearchResult[]> {
    // ใช้ HolySheep สำหรับ reranking
    const response = await this.client.chat.completions.create({
      model: 'gpt-4o-mini',
      messages: [
        {
          role: 'system',
          content: `คุณคือตัวจัดลำดับความสำคัญ จัดลำดับผลลัพธ์จากความเกี่ยวข้องกับคำถาม
มี format output: JSON array ของ index ที่เรียงจาก relevant มากไปน้อย
ตัวอย่าง: [0, 2, 1, 3]`
        },
        {
          role: 'user',
          content: `คำถาม: ${query}\n\nผลลัพธ์:\n${
            candidates.map((c, i) => ${i}: ${c.content}).join('\n')
          }`
        }
      ],
      temperature: 0.1
    });
    
    // parse ranking...
    return candidates;
  }
  
  private mergeByWeight(
    structured: SearchResult[],
    unstructured: SearchResult[],
    sWeight: number,
    uWeight: number,
    topK: number
  ): SearchResult[] {
    const scored = [
      ...structured.map(r => ({ ...r, weightedScore: r.score * sWeight })),
      ...unstructured.map(r => ({ ...r, weightedScore: r.score * uWeight }))
    ];
    
    return scored
      .sort((a, b) => b.weightedScore - a.weightedScore)
      .slice(0, topK);
  }
}

// ตัวอย่างการใช้งาน
async function example() {
  const retriever = new HybridRAGRetriever(process.env.HOLYSHEEP_API_KEY!);
  
  const result = await retriever.hybridSearch(
    'สินค้าที่มีราคาต่ำกว่า 500 บาท และมี rating สูง พร้อมรีวิวจากลูกค้า',
    10,
    0.5, // 50% น้ำหนักสำหรับ structured
    0.5  // 50% น้ำหนักสำหรับ unstructured
  );
  
  console.log('ผลลัพธ์แบบผสม:', result.mergedResults);
}

example();

4. สร้าง Context และ Generate คำตอบ

import { HolySheepAI } from '@holysheepai/sdk';

interface RAGResponse {
  answer: string;
  sources: Array<{content: string, source: string, score: number}>;