ข้ามไปยังเนื้อหาหลัก
ProgrammerThailand

AI & Vibe Coding

Embeddings และ Vector Search อธิบายแบบนักพัฒนา

Embeddings และ Vector Search อธิบายแบบนักพัฒนา

ถ้าเคยทำช่องค้นหาด้วย LIKE '%คำค้น%' แล้วเจอลูกค้าพิมพ์ "รองเท้าใส่วิ่ง" แต่สินค้าในระบบชื่อ "running shoes" ก็จะรู้ว่าการค้นด้วยคำตรงตัวมีข้อจำกัด Embeddings และ Vector Search ช่วยให้ค้นหาตาม ความหมาย ได้ และยังเป็นหัวใจของระบบ RAG ด้วย บทความนี้อธิบายแบบที่นักพัฒนาเข้าใจได้โดยไม่ต้องลงคณิตศาสตร์ลึก ว่าเวกเตอร์คืออะไร cosine similarity วัดอะไร ต่างจาก full-text search อย่างไร และทำไมระบบจริงมักใช้ทั้งสองแบบร่วมกัน

เวกเตอร์คืออะไรในมุมนักพัฒนา

ในโค้ด เวกเตอร์ก็คืออาร์เรย์ของตัวเลขทศนิยมที่มีความยาวคงที่ เช่น [0.12, -0.53, 0.08, ...] ความยาวนี้เรียกว่า dimension ซึ่งขึ้นกับ embedding model ที่ใช้ โดยทั่วไปมีตั้งแต่หลักร้อยถึงหลักพันมิติ

Embedding model คือโมเดลที่รับข้อความเข้าไปแล้วคืนเวกเตอร์ออกมา โมเดลถูกฝึกให้ข้อความที่มีความหมายใกล้กันได้เวกเตอร์ที่ "ชี้ไปทางเดียวกัน" ในพื้นที่หลายมิติ ลองนึกถึงแผนที่สองมิติที่วางคำไว้ คำว่า "แมว" กับ "สุนัข" อยู่ใกล้กันในโซนสัตว์เลี้ยง ส่วน "ใบแจ้งหนี้" อยู่อีกมุมหนึ่ง embedding ทำแบบเดียวกันแต่ใช้หลายร้อยมิติ จึงจับความสัมพันธ์ได้ละเอียดกว่ามาก

สิ่งที่ต้องจำในทางปฏิบัติ

  • เวกเตอร์จาก โมเดลต่างกันเอามาเทียบกันไม่ได้ ถ้าเปลี่ยน embedding model ต้อง embed ข้อมูลทั้งหมดใหม่
  • ตัวเลขแต่ละมิติไม่มีความหมายที่มนุษย์อ่านออก อย่าพยายามตีความว่ามิติที่ 5 แปลว่าอะไร
  • การเรียก embedding API ทำแบบเดียวกับเรียก LLM API คือ POST ข้อความไปแล้วได้อาร์เรย์ตัวเลขกลับมา รูปแบบ JSON ขึ้นกับผู้ให้บริการ หรือจะรันโมเดลเองในเครื่องด้วยเครื่องมืออย่าง Ollama ก็ได้
<?php
// เรียก embedding API แบบ vendor-neutral: endpoint และโมเดลมาจาก env
// ชื่อ field ใน request/response ให้ตรวจตามเอกสารของผู้ให้บริการ
function embed(\GuzzleHttp\Client $http, string $text): array
{
    $res = $http->post(getenv('EMBEDDING_API_URL'), [
        'headers' => ['Authorization' => 'Bearer ' . getenv('EMBEDDING_API_KEY')],
        'json'    => ['model' => getenv('EMBEDDING_MODEL'), 'input' => $text],
        'timeout' => 30,
    ]);
    $data = json_decode((string)$res->getBody(), true, 512, JSON_THROW_ON_ERROR);
    return $data['data'][0]['embedding']; // ปรับตามรูปแบบ response จริง
}

Cosine similarity วัดอะไร

เมื่อมีเวกเตอร์สองตัว เราต้องการตัวเลขบอกว่ามัน "ใกล้กัน" แค่ไหน วิธีที่นิยมที่สุดสำหรับ embedding ข้อความคือ cosine similarity ซึ่งวัดมุมระหว่างเวกเตอร์สองตัว ไม่สนความยาว

  • ค่าใกล้ 1 แปลว่าชี้ไปทางเดียวกัน ความหมายใกล้กันมาก
  • ค่าใกล้ 0 แปลว่าไม่ค่อยเกี่ยวข้องกัน
  • ค่าติดลบแปลว่าชี้ไปคนละทาง (ในทางปฏิบัติกับ embedding ข้อความ ค่ามักไม่ต่ำมาก)

สูตรคือผลคูณจุด (dot product) หารด้วยผลคูณของความยาวเวกเตอร์ทั้งสอง เขียนเป็น PHP ได้สั้น ๆ

<?php
function cosineSimilarity(array $a, array $b): float
{
    if (count($a) !== count($b)) {
        throw new InvalidArgumentException('เวกเตอร์ต้องมีจำนวนมิติเท่ากัน');
    }
    $dot = $normA = $normB = 0.0;
    foreach ($a as $i => $x) {
        $y = $b[$i];
        $dot   += $x * $y;
        $normA += $x * $x;
        $normB += $y * $y;
    }
    if ($normA == 0.0 || $normB == 0.0) {
        return 0.0;
    }
    return $dot / (sqrt($normA) * sqrt($normB));
}

// ทดลองกับเวกเตอร์ของเล่น 3 มิติ
echo cosineSimilarity([1, 2, 3], [2, 4, 6]);  // 1 (ทิศเดียวกัน)
echo PHP_EOL;
echo cosineSimilarity([1, 0, 0], [0, 1, 0]);  // 0 (ตั้งฉาก)

ในฐานข้อมูลบางตัวจะใช้ cosine distance แทน ซึ่งเท่ากับ 1 ลบ similarity ค่ายิ่งน้อยยิ่งใกล้ เช่น operator <=> ของ pgvector คืน cosine distance ดังนั้นต้อง ORDER BY ... ASC ไม่ใช่ DESC จุดนี้สับสนกันบ่อย

ตัวอย่างเทียบประโยค

สมมติเรา embed ประโยคต่อไปนี้แล้วเทียบกับคำถาม "ขอคืนเงินได้ไหมคะ" ผลที่คาดหวังเชิงคุณภาพจะเป็นประมาณนี้

ประโยคในฐานข้อมูลความใกล้เชิงความหมายมีคำซ้ำกับคำถาม
นโยบายการคืนเงินภายใน 7 วันสูงมี (คืนเงิน)
Refund policy for online ordersสูง (ถ้าโมเดลรองรับหลายภาษา)ไม่มี
ยกเลิกคำสั่งซื้อและรับเงินกลับค่อนข้างสูงไม่มีตรงตัว
วิธีเปลี่ยนรหัสผ่านต่ำไม่มี

นี่คือจุดแข็งของ vector search คือเจอ "รับเงินกลับ" และ "Refund" ทั้งที่ไม่มีคำว่า "คืนเงิน" ตัวเลข similarity จริงขึ้นกับโมเดลที่ใช้ ไม่ควรตั้ง threshold ตายตัวจากบทความไหน ให้ทดลองกับข้อมูลของตัวเองแล้วดูการกระจายของคะแนน

ต่างจาก Full-Text Search อย่างไร

Full-text search (เช่น tsvector ใน PostgreSQL หรือ search engine ทั่วไป) ทำงานด้วยการตัดคำ ทำดัชนีคำ แล้วให้คะแนนจากการที่คำในเอกสารตรงกับคำค้น ทั้งสองแบบเก่งคนละเรื่อง

ประเด็นFull-text searchVector search
หลักการจับคู่คำความใกล้ของความหมาย
คำพ้องความหมาย ข้ามภาษาต้องทำ synonym เองจับได้ค่อนข้างดี
รหัสสินค้า ชื่อเฉพาะ เลขเอกสารแม่นยำมักพลาด เพราะโมเดลไม่เข้าใจรหัส
อธิบายได้ว่าทำไมเจอได้ (highlight คำ)ยาก
ค่าใช้จ่ายต่ำต้องเรียก embedding และเก็บเวกเตอร์
ภาษาไทยต้องจัดการเรื่องตัดคำขึ้นกับว่าโมเดลรองรับภาษาไทยดีแค่ไหน

ตัวอย่างที่ vector search พลาด เช่น ลูกค้าค้น "SKU-A1029" แต่ระบบคืนสินค้า "SKU-A1092" ที่หน้าตาใกล้กัน เพราะในมุมของ embedding สองรหัสนี้แทบไม่ต่างกัน งานแบบนี้ full-text หรือ WHERE sku = :sku ธรรมดาชนะขาด

Hybrid Search: ใช้ทั้งสองแบบร่วมกัน

ระบบค้นหาจริงจำนวนมากจึงใช้ hybrid search คือค้นทั้งสองแบบแล้วรวมผล วิธีรวมที่ง่ายและได้ผลดีคือ Reciprocal Rank Fusion (RRF) ซึ่งใช้แค่ลำดับ ไม่ต้องปรับคะแนนสองระบบให้อยู่สเกลเดียวกัน แต่ละเอกสารได้คะแนน 1 / (k + rank) จากแต่ละรายการ แล้วนำมารวมกัน โดย k เป็นค่าคงที่ (นิยมใช้ประมาณ 60)

<?php
/**
 * @param array $rankedLists เช่น [[id ที่ได้จาก full-text เรียงตามอันดับ], [id จาก vector]]
 * @return array id => score เรียงจากมากไปน้อย
 */
function reciprocalRankFusion(array $rankedLists, int $k = 60): array
{
    $scores = [];
    foreach ($rankedLists as $list) {
        foreach (array_values($list) as $rank => $id) {
            $scores[$id] = ($scores[$id] ?? 0) + 1 / ($k + $rank + 1);
        }
    }
    arsort($scores);
    return $scores;
}

$fullText = [12, 5, 33];
$vector   = [5, 41, 12, 7];
print_r(reciprocalRankFusion([$fullText, $vector])); // 5 และ 12 ขึ้นนำ

แนวทางเพิ่มเติมที่ใช้คู่กันได้

  • ถ้าคำค้นหน้าตาเป็นรหัส (ตรงกับ regex ของ SKU) ให้ค้นแบบ exact match ก่อนเลย
  • ใช้ filter จาก metadata เช่น หมวดหมู่ สถานะเผยแพร่ สิทธิ์ผู้ใช้ ร่วมกับทั้งสองแบบ
  • เก็บ log คำค้นที่ไม่มีใครคลิกผลลัพธ์ แล้วนำมาสร้างชุดทดสอบ

ข้อควรระวังก่อนใช้จริง

  • embed ข้อความก่อนบันทึกและเมื่อแก้ไข อย่าลืมกรณีแก้ไขข้อมูล ไม่อย่างนั้นเวกเตอร์จะไม่ตรงกับเนื้อหา
  • เก็บชื่อ embedding model ไว้คู่กับเวกเตอร์ จะได้รู้ว่าแถวไหนต้อง embed ใหม่เมื่อเปลี่ยนโมเดล
  • ข้อความที่ส่งไป embed ถูกส่งออกนอกระบบ ถ้าเป็นข้อมูลส่วนบุคคลต้องพิจารณาเรื่องความเป็นส่วนตัวและข้อตกลงกับผู้ให้บริการ หรือใช้โมเดลที่รันในเครื่องเอง
  • คำนวณ cosine ใน PHP แบบตัวอย่างใช้ได้กับข้อมูลหลักร้อยแถว ถ้าหลักหมื่นขึ้นไปให้ย้ายไปทำในฐานข้อมูลที่มี index สำหรับเวกเตอร์

สรุป

  • Embedding คือการแปลงข้อความเป็นอาร์เรย์ตัวเลข โดยข้อความที่ความหมายใกล้กันจะได้เวกเตอร์ที่ใกล้กัน
  • Cosine similarity วัดมุมระหว่างเวกเตอร์ ค่าใกล้ 1 คือใกล้กัน ส่วน cosine distance ยิ่งน้อยยิ่งใกล้
  • Vector search เก่งเรื่องความหมายและคำพ้อง full-text search เก่งเรื่องคำตรงตัวและรหัส
  • Hybrid search ด้วย RRF เป็นวิธีง่ายที่ได้ทั้งสองข้อดี

ลองทำ

เขียนสคริปต์ PHP ที่ embed ประโยค 8 ประโยคเกี่ยวกับร้านค้าของคุณ (เช่น การจัดส่ง การคืนสินค้า การชำระเงิน) ด้วย embedding API ที่มีอยู่ หรือโมเดลที่รันในเครื่อง จากนั้นรับคำถามจาก command line แล้วใช้ cosineSimilarity() เรียงประโยคจากใกล้ไปไกล ทดลองคำถามสามแบบ คือ คำถามที่ใช้คำตรงกัน คำถามที่ใช้คำพ้องความหมาย และคำถามที่มีรหัสสินค้า แล้วสังเกตว่าแบบไหนที่ vector search ทำได้ดีและแบบไหนที่พลาด

ติดปัญหาตอนทำตาม? ถามในชุมชน

ความคิดเห็น (0)

ยังไม่มีความคิดเห็น — เริ่มคุยเป็นคนแรก ถามสิ่งที่สงสัย หรือแชร์ประสบการณ์ของคุณ