RAG ตอบผิดทั้งที่คำตอบอยู่ในเอกสาร บ่อยครั้งเพราะข้อความเสียตั้งแต่ก่อนเข้าดัชนี เช่น PDF ภาษาไทยที่ดึงออกมาแล้วสระลอย ไฟล์สแกนที่ไม่มีชั้นข้อความ หรือ chunk (ชิ้นข้อความที่ระบบค้นคืนมาให้โมเดล) ที่ตัดกลางข้อของระเบียบ

การเตรียมเอกสารจึงต้องตรวจข้อความที่ดึงได้ทุกชุด ทำ OCR เฉพาะหน้าที่จำเป็น normalize (ปรับข้อความให้อยู่ในรูปมาตรฐานเดียวกัน) ข้อความไทยด้วยกฎที่ชัดเจน และแบ่ง chunk ตามโครงสร้างเอกสารพร้อม metadata ที่ใช้อ้างอิงและกรองสิทธิ์ได้

บทความนี้ลงรายละเอียดเฉพาะช่วงนำเอกสารเข้าระบบ (ingestion) สำหรับเอกสารขององค์กรและหน่วยงานภาครัฐที่เป็นภาษาไทย ถ้ายังไม่คุ้นกับ RAG หรือต้องการเรื่องการตัดคำสำหรับค้นหา hybrid search การวัดผล และสิทธิ์กับ PDPA อ่าน RAG คืออะไร: ทำ AI ค้นเอกสารภาษาไทยให้ตอบถูกและตรวจสอบได้ ก่อน ที่นี่จะไม่อธิบายเรื่องเหล่านั้นซ้ำ

สรุปสั้น: แยกเอกสารตามชนิดก่อนเลือกวิธีดึงข้อความ ตรวจข้อความที่ได้จาก PDF ด้วยการค้นหา (cid:x), U+FFFD, code point ใน Private Use Area (ช่วงรหัสที่ Unicode เว้นไว้ให้ใช้เอง ไม่มีความหมายมาตรฐาน) และเครื่องหมายไทยที่ไม่มีพยัญชนะนำ ถ้าพบมากให้เรนเดอร์หน้าแล้วทำ OCR ด้วยข้อมูลภาษา tha ใช้ NFC ตามด้วยกฎเฉพาะภาษาไทยเพราะ NFC แก้ลำดับวรรณยุกต์กับสระบนไม่ได้ แบ่ง chunk ตามหมวด ข้อ มาตรา หรือหัวข้อก่อน แล้วจึงใช้ตัวตัดประโยคภาษาไทยคุมขนาด และติด metadata ทุก chunk

ทำไม RAG ตอบผิดทั้งที่คำตอบอยู่ในเอกสาร?

เพราะโมเดลเห็นเฉพาะข้อความที่ระบบค้นส่งไปให้ ถ้าข้อความนั้นเพี้ยน ขาดหาย หรือถูกตัดผิดที่ตั้งแต่ขั้นนำเข้า การค้นจะหาไม่เจอหรือได้ชิ้นที่ไม่ครบ แม้คนเปิดไฟล์ต้นฉบับจะเห็นคำตอบชัดเจน

ต้นเหตุมักอยู่ที่ PDF ซึ่งไม่ได้เก็บข้อความเป็นย่อหน้าแบบไฟล์ Word เอกสารของ pdfminer.six อธิบายว่าไฟล์ PDF รู้แค่ตัวอักษรและตำแหน่งที่วาง ตัวอักษรในย่อหน้า ตาราง และท้ายกระดาษไม่ต่างกัน และต้องแทรกช่องว่างเองเพราะรูปแบบ PDF ไม่มีแนวคิดเรื่องอักขระเว้นวรรค เครื่องมือดึงข้อความจึงต้องเดาลำดับการอ่าน ขอบเขตบรรทัด และบางครั้งเดาว่ารหัสตัวอักษรหมายถึงอักษรใด

อาการที่พบบ่อยในระบบถามตอบภาษาไทยมีสามแบบ แบบแรกค้นคำที่มีอยู่ในเอกสารไม่เจอ เพราะคำในดัชนีสะกดด้วยลำดับอักขระต่างจากคำที่ผู้ใช้พิมพ์ แบบที่สองได้คำตอบครึ่งเดียว เพราะ chunk ตัดกลางข้อหรือกลางตาราง แบบที่สามอ้างฉบับเก่า เพราะไม่มีข้อมูลฉบับหรือวันที่มีผลติดไปกับ chunk ทั้งสามแบบแก้ที่ขั้นเตรียมเอกสาร ไม่ใช่ที่ prompt

แยกเอกสารตามชนิดก่อนเลือกวิธีดึงข้อความ

ทำรายการเอกสารตามชนิดก่อน เพราะแต่ละชนิดเสียคนละแบบและต้องใช้วิธีดึงข้อความต่างกัน การใช้วิธีเดียวกับทุกไฟล์ทำให้ปัญหาของไฟล์บางกลุ่มถูกกลบในผลรวม

ชนิดเอกสารกับวิธีดึงข้อความที่ควรเริ่ม
ชนิดเอกสารวิธีที่ควรเริ่มเหตุผล
PDF ที่สร้างจากโปรแกรม (born-digital) และผ่านการตรวจข้อความใช้ชั้นข้อความในไฟล์ได้อักขระตามต้นฉบับ ไม่มีความผิดพลาดจาก OCR
PDF ที่สร้างจากโปรแกรม แต่ตรวจแล้วพบ (cid:x), U+FFFD หรือ Private Use Area จำนวนมากเรนเดอร์หน้าเป็นภาพแล้วทำ OCRไฟล์ไม่มีข้อมูลพอให้แปลงกลับเป็น Unicode ได้ถูก
PDF สแกนหรือไฟล์ภาพOCR ด้วยข้อมูลภาษาไทย (และอังกฤษถ้ามีปน)ไม่มีชั้นข้อความให้ดึง
หน้าที่เป็นตารางหรือแบบฟอร์มดึงพร้อมตำแหน่ง แล้วแปลงเป็นตารางหรือประโยครายแถวข้อความเรียงเป็นบรรทัดทำให้แถวกับคอลัมน์ปนกัน
Word, Excel, HTMLแปลงโดยเก็บโครงสร้าง เช่น หัวข้อ รายการ ตารางโครงสร้างมีอยู่แล้วในไฟล์ ใช้เป็นจุดตัด chunk ได้
อีเมลแยกเนื้อความกับไฟล์แนบ แล้วจัดไฟล์แนบตามชนิดข้างบนไฟล์แนบมักเป็นเอกสารหลัก ส่วนผู้ส่งและวันที่ใช้เป็น metadata

ไฟล์จากระบบเก่าที่บันทึกด้วย encoding อื่นอย่าง TIS-620 หรือ Windows-874 จะเพี้ยนอีกแบบหนึ่ง วิธีตรวจและแปลงอยู่ในบทความ ย้ายข้อมูลจาก Legacy System ไม่ให้ภาษาไทยเพี้ยน หัวข้อ "ภาษาไทยในระบบเก่าเก็บด้วย encoding อะไรได้บ้าง?"

PDF ภาษาไทยดึงข้อความแล้วสระลอยหรือตัวอักษรเพี้ยน เกิดจากอะไร?

สาเหตุหลักคือ PDF เก็บรหัส glyph (รูปตัวอักษรที่ฟอนต์ใช้วาด) สำหรับวาดตัวอักษร ส่วนข้อมูลว่า glyph นั้นคืออักขระ Unicode ตัวไหนเป็นข้อมูลเสริมที่อาจไม่ครบ ตามมาตรฐาน PDF (ISO 32000-1) หัวข้อ 9.10 ข้อความเทียบ Unicode มาจาก ToUnicode CMap (ตารางเทียบรหัส glyph กับอักขระ Unicode ในไฟล์) ในพจนานุกรมฟอนต์ หรือวิธีสำรองอื่น และถ้าวิธีเหล่านี้ให้ค่า Unicode ไม่ได้ ก็ไม่มีทางรู้ว่ารหัสนั้นหมายถึงอักขระใด

ภาษาไทยมีเงื่อนไขเพิ่ม เพราะลำดับที่เก็บในหน่วยความจำต่างจากตำแหน่งที่เห็นบนหน้า Unicode บทที่ 16.1 กำหนดว่าสระบน สระล่าง และวรรณยุกต์เก็บต่อหลังพยัญชนะที่กำกับ ส่วนสระหน้า (เ แ โ ใ ไ) เก็บก่อนพยัญชนะตามลำดับที่มองเห็น ฟอนต์บางแบบวาดวรรณยุกต์และสระบนด้วย glyph หลายตำแหน่ง ถ้า glyph เหล่านั้นไม่มี mapping หรือ map ไปที่ code point นอกมาตรฐาน ข้อความที่ดึงได้จะมีเครื่องหมายหาย ลอย หรือสลับลำดับ

อาการของข้อความที่ดึงจาก PDF ภาษาไทย สาเหตุ และวิธีตรวจ
อาการสาเหตุตามเอกสารต้นทางตรวจอย่างไร
มีข้อความ (cid:12) ปนFAQ ของ pdfminer.six: PDF บางไฟล์มี unicode mapping ไม่ครบ เครื่องมือจึงแสดงรหัสดิบค้นรูปแบบ (cid: ในผลลัพธ์
มี U+FFFD (�)Unicode 23: อักขระแทนตัวที่ map เป็น Unicode ไม่ได้ ส่วน PyMuPDF มี flag ให้คืนรหัส CID แทน U+FFFDนับ U+FFFD ต่อหน้า
สระหรือวรรณยุกต์หาย หรือได้อักขระที่มองไม่เห็นcode point ใน Private Use Area (U+E000–U+F8FF) ซึ่ง Unicode 23 ระบุว่าไม่มีความหมายมาตรฐานนับอักขระในช่วง U+E000–U+F8FF
สระลอย วรรณยุกต์อยู่ต้นคำหรือหลังช่องว่างเครื่องหมายที่ไม่มีพยัญชนะนำ ซึ่งข้อกำหนดฟอนต์ไทยของ Microsoft ถือว่าไม่ถูกต้องหาเครื่องหมายที่อยู่ต้นข้อความหรือหลังอักขระที่ไม่ใช่พยัญชนะหรือเครื่องหมายไทย
วรรณยุกต์มาก่อนสระบน หรือวรรณยุกต์ซ้อนสองตัวลำดับเก็บผิดจากที่ Unicode กำหนด ข้อกำหนดของ Microsoft ตรวจไม่ให้มีเครื่องหมายกลุ่มเดียวกันสองตัวบนพยัญชนะเดียวหาวรรณยุกต์ที่ตามด้วยสระบน และวรรณยุกต์สองตัวติดกัน
ประโยคสลับบรรทัด ท้ายกระดาษแทรกกลางย่อหน้า คอลัมน์อ่านข้ามข้อความเรียงตามลำดับที่โปรแกรมสร้างไฟล์บันทึก ไม่ใช่ลำดับการอ่าน (PyMuPDF)เทียบกับหน้าต้นฉบับด้วยตา ลอง sort=True ของ PyMuPDF หรือ -layout ของ pdftotext

ตัวอย่างจริงที่บันทึกไว้สาธารณะคือ repository bact/constitution ซึ่งแปลง PDF ร่างรัฐธรรมนูญเป็นข้อความ และพบทั้งสระและวรรณยุกต์ที่เก็บเป็น code point ใน Private Use Area และลำดับอักขระผิดแบบวรรณยุกต์มาก่อนสระ

เครื่องมือดึงข้อความแต่ละตัวมีตัวเลือกช่วยเรื่องลำดับ แต่ไม่มีตัวไหนสร้างข้อมูลที่ไฟล์ไม่มีขึ้นมาได้ pdftotext มี -layout ที่พยายามคงตำแหน่งตามหน้า และ -raw ที่เรียงตามลำดับใน content stream (คำสั่งวาดหน้าที่เก็บในไฟล์) ส่วน PyMuPDF มี sort=True ที่เรียงจากบนลงล่างและซ้ายไปขวา

ตรวจอย่างไรว่าข้อความที่ดึงจาก PDF ใช้ได้?

ตรวจทุกไฟล์ด้วยโปรแกรมก่อน แล้วสุ่มเทียบกับหน้าต้นฉบับด้วยตา เกณฑ์อัตโนมัติช่วยคัดหน้าที่มีปัญหาชัดเจนออกมา ส่วนการเทียบด้วยตาจับปัญหาลำดับการอ่านที่โปรแกรมตรวจไม่ได้

  1. ดูฟอนต์ในไฟล์: pdffonts แสดงคอลัมน์ uni เป็น yes เมื่อฟอนต์มี ToUnicode map ชัดเจน ค่า no ไม่ได้แปลว่าดึงข้อความไม่ได้เสมอ เพราะมาตรฐาน PDF มีวิธีสำรองอื่น แต่เป็นสัญญาณให้ตรวจไฟล์นั้นละเอียดขึ้น
  2. ทดสอบคัดลอก: FAQ ของ pdfminer.six แนะนำให้คัดลอกข้อความจากโปรแกรมอ่าน PDF ไปวางใน text editor ถ้าได้ข้อความถูก เครื่องมือดึงข้อความก็ควรได้เช่นกัน ถ้าได้ข้อความเพี้ยน ปัญหาอยู่ที่ไฟล์
  3. นับอักขระผิดปกติต่อหน้า: (cid:x), U+FFFD, Private Use Area, เครื่องหมายไทยที่ไม่มีพยัญชนะนำ วรรณยุกต์ซ้อน วรรณยุกต์ก่อนสระบน และนิคหิตตามด้วยสระอา (ํา ซึ่งควรเป็นสระอำ)
  4. ตั้งเกณฑ์ส่งหน้าไป OCR: Apache Tika ใช้หลักการเดียวกันใน ocrStrategy แบบ auto คือทำ OCR เมื่อดึงข้อความได้น้อยกว่า 10 ตัวอักษร หรือมีอักขระที่ไม่มีค่า Unicode เกิน 10 ตัว เกณฑ์ของคุณควรตั้งจากผลตรวจเอกสารจริง
  5. สุ่มเทียบด้วยตา: เลือกหน้าจากทุกกลุ่มเอกสาร ทั้งหน้าที่ผ่านและไม่ผ่านเกณฑ์ เทียบลำดับย่อหน้า ตาราง และตัวเลขกับต้นฉบับ

ตัวอย่าง Python ด้านล่างเราเขียนขึ้นเองจาก code point ใน UnicodeData ไม่ได้มาจากเอกสารของเครื่องมือใด ใช้นับสัญญาณผิดปกติในข้อความของแต่ละหน้า ไม่ได้แก้ข้อความ

import re
MARK = ("\u0e31\u0e34-\u0e3a"
        "\u0e47-\u0e4e")
BASE = "\u0e01-\u0e2e"
BAD = re.compile(
  r"\(cid:\d+\)|\ufffd|[\ue000-\uf8ff]"
  rf"|(?:^|[^{BASE}{MARK}])[{MARK}]"
  r"|[\u0e48-\u0e4b]{2}"
  r"|[\u0e48-\u0e4b]"
  r"[\u0e31\u0e34-\u0e37]"
  r"|\u0e4d\u0e32")

def bad_count(page_text):
    return len(BAD.findall(page_text))

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

PDF สแกนและรูปภาพ: OCR ภาษาไทยมีทางเลือกอะไรบ้าง?

ทางเลือกที่ติดตั้งในองค์กรได้และมีเอกสารรองรับภาษาไทย เช่น Tesseract ซึ่งมีข้อมูลภาษาไทย และเครื่องมือที่เรียก Tesseract อีกชั้น เช่น OCRmyPDF, Apache Tika, PyMuPDF และ Fess ส่วนบริการ cloud ให้ตรวจรายการภาษาในเอกสารของผู้ให้บริการก่อน เพราะไม่ใช่ทุกบริการที่รองรับภาษาไทย

Tesseract

ตามตารางข้อมูลภาษาของ Tesseract ภาษาไทยใช้รหัส tha และมีข้อมูลระดับอักษร (script) ชื่อ Thai ในชุดข้อมูลของรุ่น 4.0.0 ชุดข้อมูลมีสามแบบตามเอกสาร Data Files: tessdata_fast เร็วที่สุดแต่แม่นน้อยที่สุด tessdata_best ช้าที่สุดแต่แม่นที่สุดและใช้ได้กับ LSTM เท่านั้น ส่วน tessdata เป็นชุดเดียวที่รองรับ engine แบบ legacy

ตามคู่มือ command line ใช้ -l เลือกภาษา รวมหลายภาษาด้วย + และผลอาจต่างกันตามลำดับภาษา ใช้ --oem 1 สำหรับ LSTM และส่งออกได้ทั้ง txt, PDF ที่ค้นได้, hOCR และ TSV ซึ่ง hOCR กับ TSV มีตำแหน่งของคำ ใช้ต่อกับงานตารางได้

tesseract page-01.png page-01 \
  -l tha+eng --oem 1 --psm 4

ข้อควรรู้จากเอกสาร Improving the quality of the output:

  • ความละเอียด: Tesseract ทำงานได้ดีที่สุดกับภาพอย่างน้อย 300 DPI
  • ขอบดำและขอบกว้าง: ขอบดำจากเครื่องสแกนอาจถูกอ่านเป็นตัวอักษรเกิน ส่วนขอบที่กว้างมากอาจทำให้ได้ผลเป็นหน้าว่าง
  • การแปลงเป็นขาวดำ: Tesseract ทำเองด้วยวิธี Otsu แต่ผลอาจไม่ดีกับพื้นหลังไม่สม่ำเสมอ รุ่น 5.0.0 เพิ่ม Adaptive Otsu และ Sauvola ให้เลือก
  • การแบ่งหน้า (--psm): ค่าเริ่มต้น 3 แบ่งหน้าอัตโนมัติ 4 สมมติว่าเป็นคอลัมน์เดียว 6 สมมติว่าเป็นข้อความก้อนเดียว และ 11 หาข้อความกระจายโดยไม่สนลำดับ เอกสารไม่มีคำแนะนำเฉพาะภาษาไทย จึงควรลองกับหน้าตัวอย่างของแต่ละกลุ่มเอกสาร
  • ตาราง: เอกสารระบุว่า Tesseract มีปัญหาในการอ่านข้อมูลจากตารางถ้าไม่ได้แบ่งโครงหน้าเอง

เครื่องมือที่เรียก Tesseract

  • OCRmyPDF: เพิ่มชั้นข้อความให้ PDF ตามเอกสารของ OCRmyPDF ตัวเลือก --mode skip (ชื่อเดิม --skip-text) ข้ามหน้าที่มีข้อความอยู่แล้ว จึงทำ OCR เฉพาะหน้าที่ยังไม่มีข้อความ ส่วน --mode force (ชื่อเดิม --force-ocr) แปลงทุกหน้าเป็นภาพแล้วทำ OCR ซึ่ง cookbook เตือนว่าอาจลดคุณภาพหรือทำให้เนื้อหาแบบ vector หาย cookbook ระบุว่าชื่อเดิมยังใช้ได้ในฐานะชื่อแทน --deskew หมุนหน้าที่สแกนเอียงกลับ และ --rotate-pages แก้หน้าที่กลับด้าน
  • OCRmyPDF กับ RapidOCR: cookbook ระบุว่า OCRmyPDF มี engine เสริมชื่อ RapidOCR ติดตั้งผ่าน extra ชื่อ rapidocr และเลือกด้วย --ocr-engine rapidocr ภาษาไทยใช้โมเดลแยกที่ RapidOCR ดาวน์โหลดเมื่อใช้ครั้งแรก
  • Apache Tika: ใช้ Tesseract ทำ OCR ตั้งภาษาด้วย header X-Tika-OCRLanguage และเลือก X-Tika-PDFOcrStrategy: ocr_only เพื่อเรนเดอร์ทุกหน้าแล้วทำ OCR ค่า ocrStrategy อื่นได้แก่ no_ocr, ocr_and_text และ auto
  • PyMuPDF: Page.get_textpage_ocr() รับ language, dpi และ full (OCR ทั้งหน้า หรือเฉพาะส่วนที่ไม่มีข้อความอ่านได้) ค่า dpi เริ่มต้นคือ 72 ซึ่งต่ำกว่าระดับที่ Tesseract แนะนำมาก ควรตั้งเองเมื่อใช้กับภาษาไทย
  • Fess: release notes ของ Fess 15.9.0 ระบุว่าเพิ่มการตั้งค่าให้ทำ OCR กับรูปภาพและ PDF สแกนระหว่าง crawl pull request #3520 ระบุว่าใช้ Tesseract ผ่าน Tika และมีค่าใน fess_config.properties สามตัว:
    • crawler.document.ocr.enabled ค่าเริ่มต้น false
    • crawler.document.ocr.language ค่าเริ่มต้น eng
    • crawler.document.ocr.timeout ค่าเริ่มต้น 120 วินาทีต่อการรัน Tesseract หนึ่งครั้ง

Fess ไม่ได้ระบุเรื่องภาษาไทยในเอกสารทั้งสองฉบับ การตั้ง tha ด้านล่างจึงมาจากการนำสองแหล่งมาประกอบกัน คือค่า language ของ Fess รับรหัสภาษาแบบ Tesseract และรหัสภาษาไทยของ Tesseract คือ tha ต้องติดตั้งข้อมูลภาษาไทยของ Tesseract บนเครื่องที่รัน Fess และทดสอบกับเอกสารตัวอย่างก่อนเปิดใช้กับทั้งคลัง

crawler.document.ocr.enabled=true
crawler.document.ocr.language=tha+eng

บริการ OCR บน cloud

ถ้าเอกสารส่งออกนอกองค์กรได้ บริการ cloud เป็นอีกทางเลือก แต่ต้องตรวจรายการภาษาและเงื่อนไขข้อมูลก่อน (ดูเรื่องสิทธิ์เข้าถึงเอกสารและ PDPA ในบทความ RAG ภาษาไทย) เอกสารของ Azure Document Intelligence แนะนำว่าไม่ต้องระบุรหัสภาษาถ้าไม่แน่ใจ เพราะการบังคับภาษาผิดอาจได้ข้อความไม่ครบหรือไม่ถูก

สถานะ ณ 2026-11-09: เครื่องมือ OCR กับภาษาไทย ตามเอกสารของผู้พัฒนาแต่ละราย
เครื่องมือภาษาไทยในเอกสารหมายเหตุ
Tesseract 5.xมี tha และ script Thaiติดตั้งในองค์กรได้ ใช้ --oem 1 กับ tessdata_best หรือ tessdata_fast
Apache Tikaใช้ข้อมูลภาษาของ Tesseractตั้งภาษาต่อคำขอผ่าน HTTP header
Fess 15.9.0 (15 ตุลาคม 2026)ไม่ได้ระบุปิด OCR เป็นค่าเริ่มต้น ภาษาเริ่มต้น eng
Azure Document Intelligence v4.0 (Read, Layout)ระบุ Thai (th) ทั้งตัวพิมพ์และลายมือบริการ cloud
Google Cloud Visionระบุ Thai (th) สำหรับ OCRตารางอักษรลายมือไม่มีอักษรไทย
Amazon Textractรายการภาษาใน FAQ ไม่มีภาษาไทยFAQ ระบุว่าลายมือ ใบแจ้งหนี้ ใบเสร็จ บัตรประจำตัว และ Queries รองรับแค่ภาษาอังกฤษ

เอกสารที่อ้างอิงข้างต้นไม่มีตัวเลขความแม่นยำสำหรับภาษาไทย การเลือกจึงต้องวัดเองกับหน้าตัวอย่างขององค์กร

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

ถ้าเอกสารสแกนต้องใช้แทนต้นฉบับทางกฎหมายด้วย เรื่องนั้นเป็นอีกประเด็นหนึ่ง ดูประเด็นกฎหมายไทยในบทความงานเอกสารและการอนุมัติแบบดิจิทัล

ตาราง แบบฟอร์ม และเอกสารราชการ เก็บโครงสร้างไว้อย่างไร?

ตารางต้องดึงพร้อมตำแหน่งแล้วประกอบแถวและคอลัมน์กลับ ถ้าดึงเป็นข้อความเรียงบรรทัด ค่าในคอลัมน์หนึ่งจะไปต่อกับค่าในอีกคอลัมน์ และ chunk ที่ได้จะตอบคำถามแบบ "แถวนี้มีค่าเท่าไร" ไม่ได้

  • ดึงพร้อมตำแหน่ง: pdftotext มี -bbox-layout ที่ส่งออก XHTML พร้อมกรอบตำแหน่งของ block บรรทัด และคำ ส่วนผล OCR ของ Tesseract ส่งออกเป็น TSV หรือ hOCR ที่มีตำแหน่งคำ ใช้ประกอบแถวและคอลัมน์ได้
  • เลือกรูปแบบที่ index: ตารางเล็กแปลงเป็นตาราง Markdown หรือ HTML ทั้งตาราง ตารางยาวแปลงเป็นประโยครายแถวที่มีชื่อคอลัมน์กำกับ เช่น "รายการ: ค่าเดินทาง · อัตรา: ตามที่ระเบียบกำหนด · หมายเหตุ: …" เพื่อให้แต่ละแถวเข้าใจได้แม้ถูกค้นมาเดี่ยว ๆ
  • หัวตารางที่ซ้ำทุกหน้า: ตารางที่ต่อหลายหน้าควรรวมเป็นตารางเดียวและเก็บหัวตารางชุดเดียว แต่ใส่ชื่อคอลัมน์ไว้ในทุกแถวที่แปลงเป็นประโยค
  • แบบฟอร์ม: เก็บเป็นคู่ชื่อช่องกับค่า ถ้าเป็นแบบฟอร์มเปล่าที่ใช้เป็นแม่แบบ ให้ index คำอธิบายช่องแทนช่องว่าง

เลขไทย (๐–๙) ในเอกสารราชการเป็นตัวเลขฐานสิบจริงตาม UnicodeData (หมวด Nd) แต่ผู้ใช้มักพิมพ์ค้นด้วยเลขอารบิก ให้เก็บข้อความต้นฉบับไว้แสดง และสร้างสำเนาสำหรับ index ที่แปลงเลขไทยเป็นเลขอารบิก เช่นด้วย thai_digit_to_arabic_digit() ของ PyThaiNLP

ส่วนปี พ.ศ. ในวันที่ ให้แปลงเป็นฟิลด์วันที่ใน metadata ด้วยวิธีที่อธิบายไว้ในบทความ ย้ายข้อมูลจาก Legacy System หัวข้อ "ปี พ.ศ. ในช่องวันที่ ตรวจและแปลงเป็น ค.ศ. อย่างไร?" และระวังว่า convert_years() ของ PyThaiNLP ระบุว่าทำงานถูกต้องเฉพาะปีหลัง 1941

Normalize ข้อความไทยก่อน index: ทำไม NFC อย่างเดียวไม่พอ?

NFC ทำให้ลำดับอักขระที่ Unicode ถือว่าเทียบเท่ากันเป็นแบบเดียว แต่ความเพี้ยนของข้อความไทยส่วนใหญ่ไม่อยู่ในขอบเขตนั้น จึงต้องมีกฎเฉพาะภาษาไทยต่อจาก NFC และใช้กฎชุดเดียวกันทั้งกับเอกสารและคำถามของผู้ใช้ ไม่อย่างนั้นคำเดียวกันจะเป็นคนละสตริงในดัชนีกับในคำค้น

ข้อจำกัดของ NFC ดูได้จาก UnicodeData และ UAX #15:

  • สระอำ: U+0E33 (ำ) มีแค่ compatibility decomposition เป็น U+0E4D นิคหิต + U+0E32 สระอา NFC จึงไม่รวม ำ กับ ํา เป็นแบบเดียวกัน ส่วน NFKC แยก ำ เป็น ํา ซึ่งกลับทิศกับที่ต้องการ และ UAX #15 เตือนว่าไม่ควรใช้ NFKC กับข้อความทั่วไปโดยไม่ไตร่ตรอง
  • ลำดับเครื่องหมาย: สระล่าง (ุ ู) มี combining class (ค่าที่ Unicode ใช้กำหนดลำดับเครื่องหมายบนพยัญชนะ) 103 และวรรณยุกต์มี 107 NFC จึงเรียงสระล่างก่อนวรรณยุกต์ให้ได้ แต่สระบน (ิ ี ึ ื) ไม้หันอากาศ และทัณฑฆาตมี combining class 0 NFC จึงไม่สลับวรรณยุกต์ที่มาก่อนสระบน

กฎเฉพาะภาษาไทยที่มีเอกสารรองรับคือ normalize() ของ PyThaiNLP 5.4.0 ซึ่งตามเอกสารจะลบ zero-width space ลบช่องว่างซ้ำ ลบช่องว่างก่อนวรรณยุกต์ จัดลำดับวรรณยุกต์และสระให้เป็นลำดับมาตรฐาน ลบสระและวรรณยุกต์ที่ซ้ำ และลบเครื่องหมายที่ไม่มีพยัญชนะนำที่ต้นข้อความ ส่วน reorder_vowels() มีกฎ "Nikhahit + Sara Aa -> Sara Am" ที่รวม ํา เป็น ำ

import unicodedata
from pythainlp.util import normalize

def clean(text):
    return normalize(unicodedata.normalize("NFC", text))

normalize() เรียก remove_dup_spaces() ซึ่งตามเอกสาร PyThaiNLP 5.4.0 ยุบการขึ้นบรรทัดหลายครั้งและบรรทัดว่างให้เหลือการขึ้นบรรทัดเดียว เส้นแบ่งย่อหน้าจึงหายไปด้วย ควรเรียก clean() กับแต่ละ chunk หลังแบ่งตามโครงสร้างแล้ว หรือกับสำเนาที่ใช้ index ไม่ใช่กับทั้งเอกสารก่อนแบ่ง chunk

เรื่อง zero-width space มีสองแนวทางที่ขัดกัน Unicode แนะนำให้ใช้ U+200B เป็นเครื่องหมายแบ่งคำที่มองไม่เห็นในภาษาไทย แต่ normalize() และ remove_zw() ของ PyThaiNLP ลบทิ้งเพื่อให้เทียบสตริงได้คงที่

แนวทางที่เราใช้คือ ถ้าต้นฉบับมี zero-width space ให้ใช้เป็นจุดตัดช่วยตอนแบ่ง chunk ก่อน แล้วจึงลบออกจากสำเนาที่ index และสร้าง embedding เพราะคำค้นที่ผู้ใช้พิมพ์แทบไม่มีอักขระนี้ และเก็บข้อความต้นฉบับไว้แสดงผลเสมอ

งานทำความสะอาดอื่นก่อน index:

  • หัวกระดาษ ท้ายกระดาษ และเลขหน้า: ตัดออกจากเนื้อหา แต่เก็บเลขหน้าไว้ใน metadata เอกสาร PyMuPDF ระบุว่าหัวกระดาษอาจถูกเขียนลงไฟล์ภายหลัง จึงไปโผล่ท้ายข้อความของหน้าได้ ตัวตรวจจึงควรหาข้อความที่ซ้ำกันทุกหน้า ไม่ใช่ดูแค่บรรทัดแรกและบรรทัดสุดท้าย
  • การขึ้นบรรทัดกลางคำ: ภาษาไทยไม่มีช่องว่างระหว่างคำ การต่อบรรทัดที่ถูกตัดตามความกว้างหน้าจึงไม่ควรใส่ช่องว่างแทนการขึ้นบรรทัดทุกครั้ง ให้ต่อโดยไม่เว้นวรรคเมื่อทั้งท้ายบรรทัดและต้นบรรทัดถัดไปเป็นอักษรไทย
  • ฉบับซ้ำ: เอกสารเดียวกันหลายฉบับในคลังทำให้ระบบค้นได้ฉบับเก่า เลือกฉบับที่มีผลเป็นหลัก ฉบับที่ยกเลิกแล้วให้ตัดออกหรือติด metadata สถานะไว้กรอง และตัดไฟล์ที่เนื้อหาเหมือนกันทุกตัวอักษรออกด้วย checksum

แบ่ง chunk ภาษาไทยอย่างไรให้ไม่ตัดกลางความหมาย?

ให้แบ่งตามโครงสร้างของเอกสารก่อน แล้วจึงใช้ตัวตัดประโยคหรือตัดคำภาษาไทยคุมขนาดเฉพาะช่วงที่ยาวเกิน การนับตัวอักษรแล้วตัดตรงตำแหน่งคงที่ใช้กับภาษาไทยไม่ได้ เพราะUAX #29 ระบุว่าการหาขอบเขตคำในภาษาไทยให้น่าเชื่อถือต้องใช้พจนานุกรม และ Unicode บทที่ 16 ระบุว่าคำภาษาไทยไม่คั่นด้วยช่องว่าง

ตัวแบ่งข้อความทั่วไปมักไม่รู้เรื่องนี้ RecursiveCharacterTextSplitter ของ LangChain มีตัวคั่นเริ่มต้นเป็นย่อหน้า บรรทัด ช่องว่าง และสตริงว่าง ซึ่งเอกสารเตือนเองว่ากับภาษาที่ไม่มีขอบเขตคำอย่างภาษาไทย อาจทำให้คำถูกตัดคร่อมสอง chunk ข้อความไทยที่ไม่มีการขึ้นย่อหน้าจะตกไปถึงตัวคั่นสตริงว่าง ซึ่งคือการตัดกลางคำ

  1. ตัดตามโครงสร้างก่อน: ใช้หัวข้อจาก Word หรือ HTML และในระเบียบ ประกาศ หรือกฎหมาย ใช้หมวด ส่วน มาตรา และข้อเป็นจุดตัด หนึ่งข้อหรือหนึ่งมาตราที่ไม่ยาวเกินควรอยู่ใน chunk เดียว ตารางหนึ่งตารางไม่ควรถูกตัดกลาง
  2. ช่วงที่ยาวเกิน ตัดที่ขอบประโยค: sent_tokenize() ของ PyThaiNLP ใช้ engine crfcut เป็นค่าเริ่มต้น ซึ่งเอกสารระบุว่าเป็น CRF ที่ฝึกจากชุดข้อมูล TED ส่วน paragraph_tokenize() ใช้แบ่งย่อหน้า ข้อความราชการเขียนต่างจากภาษาพูด จึงควรตรวจจุดตัดกับเอกสารจริง
  3. คุมขนาดด้วย token ของ embedding model: นับขนาด chunk ด้วย tokenizer ของโมเดลที่ใช้จริง ไม่ใช่จำนวนตัวอักษร ถ้าต้องตัดในประโยคที่ยาวมาก ให้ตัดที่ขอบคำจาก word_tokenize() (engine เริ่มต้น newmm) ไม่ใช่ตำแหน่งตัวอักษร
  4. overlap เฉพาะที่จำเป็น: chunk ที่ตัดตามข้อหรือมาตรามักไม่ต้องซ้อนกัน chunk ที่ตัดกลางเนื้อความยาวควรซ้อนกันหนึ่งถึงสองประโยค และใส่ชื่อหัวข้อแม่ไว้ต้น chunk เพื่อให้ chunk เข้าใจได้เมื่อถูกค้นมาเดี่ยว ๆ

ตัวอย่างด้านล่างตัดข้อความที่ต้นหมวด ส่วน มาตรา และข้อ ด้วย regular expression ที่เราเขียนเป็นตัวอย่าง ต้องปรับตามรูปแบบเอกสารจริง แล้วจึงตัดประโยคในช่วงที่ยาวเกินด้วย PyThaiNLP ตามเอกสาร

import re
from pythainlp.tokenize import (
    sent_tokenize)

HEAD = re.compile(
  r"^(หมวด|ส่วนที่|มาตรา|ข้อ)\s*[0-9๐-๙]+",
  re.M)

def sections(text, head=HEAD):
    cut = [m.start() for m
           in head.finditer(text)]
    cut = [0] + cut + [len(text)]
    return [text[a:b] for a, b
            in zip(cut, cut[1:])
            if text[a:b].strip()]

def split_long(section_text):
    return sent_tokenize(
        section_text, engine="crfcut")

การตัดคำเพื่อทำดัชนีค้นหา (เช่น thai tokenizer ของ OpenSearch หรือ ICU) เป็นอีกเรื่องหนึ่งจากการหาจุดตัด chunk และอธิบายไว้แล้วในเรื่องการตัดคำและ hybrid search ในบทความ RAG ภาษาไทย

ทุก chunk ต้องมี metadata อะไรบ้าง?

ทุก chunk ต้องมีข้อมูลพอให้ระบบอ้างอิงที่มา แยกฉบับ และกรองสิทธิ์ได้โดยไม่ต้องย้อนไปเปิดไฟล์ต้นฉบับ เพราะตอนค้นและตอนส่งให้โมเดล ระบบเห็น chunk ทีละชิ้น

metadata ที่ควรติดกับทุก chunk
ฟิลด์ใช้ทำอะไร
รหัสเอกสาร ชื่อเอกสาร และที่อยู่ไฟล์ต้นฉบับแสดงแหล่งอ้างอิงและลิงก์กลับไปที่ไฟล์
ลำดับหัวข้อ เช่น หมวด › ข้อใส่ในคำอ้างอิง และเติมบริบทให้ chunk
เลขหน้าให้ผู้ใช้เปิดหน้าต้นฉบับมาเทียบได้
ฉบับ วันที่มีผล และสถานะ (มีผล ยกเลิก ร่าง)กรองฉบับเก่าออก หรือแสดงว่าคำตอบมาจากฉบับใด
กลุ่มผู้มีสิทธิ์เห็นกรองสิทธิ์ตั้งแต่ขั้นค้น ก่อนส่งเนื้อหาให้โมเดล
วิธีดึงข้อความ (ชั้นข้อความ หรือ OCR) และผลตรวจคุณภาพย้อนหาต้นเหตุเมื่อคำตอบผิด และเลือกหน้าที่ต้องให้คนตรวจ

วิธีออกแบบการกรองสิทธิ์และข้อกำหนดของ PDPA อยู่ในหัวข้อสิทธิ์เข้าถึงเอกสารและ PDPA ของบทความ RAG ภาษาไทยแล้ว ที่นี่สำคัญแค่ว่าข้อมูลสิทธิ์ต้องถูกคัดลอกลงทุก chunk ตั้งแต่ขั้นนำเข้า ถ้าเก็บไว้ที่ระดับไฟล์อย่างเดียว ระบบค้นจะกรองไม่ได้

เช็กลิสต์ก่อน index และการสุ่มตรวจหลัง index

ใช้เช็กลิสต์นี้กับเอกสารทุกชุดก่อนนำเข้าครั้งแรก และทุกครั้งที่เปลี่ยนเครื่องมือดึงข้อความ ตัว OCR หรือวิธีแบ่ง chunk

  • มีรายการเอกสารแยกตามชนิด และรู้ว่าแต่ละกลุ่มใช้วิธีดึงข้อความแบบใด
  • PDF ทุกไฟล์ผ่านการนับ (cid:x), U+FFFD, Private Use Area และเครื่องหมายไทยผิดลำดับต่อหน้า และหน้าที่เกินเกณฑ์ถูกส่งไป OCR
  • หน้าตัวอย่างจากทุกกลุ่มถูกเทียบกับต้นฉบับด้วยตา ทั้งลำดับย่อหน้า ตาราง ตัวเลข และวันที่
  • ภาพที่ส่งเข้า OCR มีความละเอียดตามที่เครื่องมือแนะนำ ตัดขอบดำ และแก้หน้าที่เอียงหรือกลับด้านแล้ว
  • หน้าที่มีตราประทับ ลายมือ หรือลายเซ็นทับข้อความถูกติดป้ายให้คนตรวจ
  • ตารางถูกแปลงเป็นตารางหรือประโยครายแถว และตารางหลายหน้าถูกรวมเป็นตารางเดียว
  • แต่ละ chunk ผ่าน NFC และกฎเฉพาะภาษาไทยชุดเดียวกับที่ใช้กับคำค้น โดยทำหลังแบ่งตามโครงสร้างแล้ว
  • หัวกระดาษ ท้ายกระดาษ และเลขหน้าถูกตัดออกจากเนื้อหา และเก็บเลขหน้าไว้ใน metadata
  • เลขไทยและปี พ.ศ. มีสำเนาที่แปลงแล้วสำหรับ index โดยยังเก็บต้นฉบับไว้แสดง
  • ฉบับซ้ำและฉบับที่ยกเลิกถูกตัดออกหรือติดสถานะไว้กรอง
  • chunk ไม่ตัดกลางคำ กลางข้อ หรือกลางตาราง และมี metadata ครบทุกฟิลด์

หลัง index ให้สุ่มตรวจเป็นกิจวัตร ไม่ต้องรอชุดประเมินเต็มรูปแบบ:

  1. ค้นวลีที่รู้ว่ามีอยู่: เลือกวลีจากเอกสารแต่ละกลุ่ม โดยเฉพาะคำที่มีสระบน วรรณยุกต์ และสระอำ แล้วค้นในดัชนีโดยตรง ถ้าไม่เจอ ให้ดูข้อความใน chunk ว่าสะกดต่างจากที่พิมพ์อย่างไร
  2. เปิด chunk เทียบหน้าต้นฉบับ: ใช้เลขหน้าใน metadata เปิดหน้าต้นฉบับ แล้วเทียบว่าเนื้อหาครบ ลำดับถูก และตัวเลขในตารางตรงกัน
  3. ถามคำถามที่คำตอบอยู่ในตาราง หรือข้อที่ยาวหลายย่อหน้า: สองแบบนี้มักเสียจากการแบ่ง chunk ก่อน
  4. บันทึกผลเป็นรายการคงที่: เก็บวลีและคำถามชุดนี้ไว้ รันซ้ำเมื่อเปลี่ยนเครื่องมือหรือกฎ normalize แล้วนำไปรวมกับชุดประเมินตามวิธีวัดความถูกต้องของ RAG

UNIXDEV ช่วยเตรียมเอกสารสำหรับ RAG

UNIXDEV ออกแบบและพัฒนา RAG และ document AI สำหรับเอกสารภาษาไทย ภายใต้บริการ AI Agent และ RAG สำหรับองค์กร งานที่เกี่ยวกับบทความนี้คือ สำรวจชนิดเอกสาร ทดสอบวิธีดึงข้อความและ OCR กับหน้าตัวอย่างจริง วางกฎ normalize ภาษาไทยและกลยุทธ์แบ่ง chunk ตามโครงสร้างเอกสาร และวาง metadata สำหรับการอ้างอิงและการกรองสิทธิ์ ถ้าองค์กรใช้หรือกำลังพิจารณา Fess เป็นระบบค้นหา เราติดตั้งและตั้งค่า crawler ให้เข้ากับแหล่งเอกสารขององค์กรได้

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

แหล่งอ้างอิงและอ่านต่อ

ตรวจข้อมูล ณ วันที่ 2026-11-09

วิธีจัดทำ: อ้างอิงจากเอกสารต้นทางข้างต้น ตรวจโดยทีม UNIXDEV

คำถามที่พบบ่อย

ดึงข้อความจาก PDF ภาษาไทยแล้วได้ (cid:12) หรือสี่เหลี่ยมแปลก ๆ ควรทำอย่างไร?

(cid:x) มักแปลว่า PDF มี Unicode mapping ไม่ครบ ตาม FAQ ของ pdfminer.six ส่วน U+FFFD คืออักขระแทนตัวที่ map เป็น Unicode ไม่ได้ และ code point ใน Private Use Area ไม่มีความหมายมาตรฐาน ลองคัดลอกข้อความจากโปรแกรมอ่าน PDF มาวางใน text editor ถ้ายังเพี้ยน ให้เรนเดอร์หน้านั้นเป็นภาพแล้วทำ OCR แทนการใช้ชั้นข้อความ

PDF ไฟล์เดียวมีทั้งหน้าสแกนและหน้าที่มีข้อความ ต้องทำ OCR ทั้งไฟล์ไหม?

ไม่ต้อง OCRmyPDF มีตัวเลือก --mode skip (ชื่อเดิม --skip-text) ที่ข้ามหน้าที่มีข้อความอยู่แล้ว และ Apache Tika มี ocrStrategy แบบ auto ที่ทำ OCR เฉพาะเมื่อดึงข้อความได้น้อยหรือมีอักขระที่ไม่มีค่า Unicode มาก ส่วน --mode force (ชื่อเดิม --force-ocr) แปลงทุกหน้าเป็นภาพ ซึ่งเอกสารของ OCRmyPDF เตือนว่าอาจลดคุณภาพหรือทำให้เนื้อหาแบบ vector หาย หน้าที่มีข้อความแต่ตรวจไม่ผ่านยังต้องส่งไป OCR ตามเกณฑ์ของคุณเอง

Chunk ภาษาไทยควรยาวเท่าไร?

ไม่มีตัวเลขเดียวที่ถูกสำหรับทุกงาน ภาษาไทยไม่เว้นวรรคระหว่างคำ จำนวนตัวอักษรกับจำนวน token จึงไม่สัมพันธ์กันแบบเดียวกับภาษาอังกฤษ ให้ตัดตามโครงสร้างเอกสารก่อน เช่น หมวด ข้อ มาตรา หรือหัวข้อ แล้วใช้ตัวตัดประโยคหรือตัดคำภาษาไทยคุมขนาดเฉพาะช่วงที่ยาวเกิน จากนั้นวัดผลการค้นด้วยชุดคำถามจริงเมื่อปรับขนาด

เปลี่ยนตัว OCR หรือกฎ normalize แล้วต้อง index ใหม่ไหม?

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