RAG ตอบผิดทั้งที่คำตอบอยู่ในเอกสาร บ่อยครั้งเพราะข้อความเสียตั้งแต่ก่อนเข้าดัชนี เช่น PDF ภาษาไทยที่ดึงออกมาแล้วสระลอย ไฟล์สแกนที่ไม่มีชั้นข้อความ หรือ chunk (ชิ้นข้อความที่ระบบค้นคืนมาให้โมเดล) ที่ตัดกลางข้อของระเบียบ
การเตรียมเอกสารจึงต้องตรวจข้อความที่ดึงได้ทุกชุด ทำ OCR เฉพาะหน้าที่จำเป็น normalize (ปรับข้อความให้อยู่ในรูปมาตรฐานเดียวกัน) ข้อความไทยด้วยกฎที่ชัดเจน และแบ่ง chunk ตามโครงสร้างเอกสารพร้อม metadata ที่ใช้อ้างอิงและกรองสิทธิ์ได้
บทความนี้ลงรายละเอียดเฉพาะช่วงนำเอกสารเข้าระบบ (ingestion) สำหรับเอกสารขององค์กรและหน่วยงานภาครัฐที่เป็นภาษาไทย ถ้ายังไม่คุ้นกับ RAG หรือต้องการเรื่องการตัดคำสำหรับค้นหา hybrid search การวัดผล และสิทธิ์กับ PDPA อ่าน RAG คืออะไร: ทำ AI ค้นเอกสารภาษาไทยให้ตอบถูกและตรวจสอบได้ ก่อน ที่นี่จะไม่อธิบายเรื่องเหล่านั้นซ้ำ
ทำไม 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 นอกมาตรฐาน ข้อความที่ดึงได้จะมีเครื่องหมายหาย ลอย หรือสลับลำดับ
| อาการ | สาเหตุตามเอกสารต้นทาง | ตรวจอย่างไร |
|---|---|---|
มีข้อความ (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 ใช้ได้?
ตรวจทุกไฟล์ด้วยโปรแกรมก่อน แล้วสุ่มเทียบกับหน้าต้นฉบับด้วยตา เกณฑ์อัตโนมัติช่วยคัดหน้าที่มีปัญหาชัดเจนออกมา ส่วนการเทียบด้วยตาจับปัญหาลำดับการอ่านที่โปรแกรมตรวจไม่ได้
- ดูฟอนต์ในไฟล์: pdffonts แสดงคอลัมน์
uniเป็น yes เมื่อฟอนต์มี ToUnicode map ชัดเจน ค่า no ไม่ได้แปลว่าดึงข้อความไม่ได้เสมอ เพราะมาตรฐาน PDF มีวิธีสำรองอื่น แต่เป็นสัญญาณให้ตรวจไฟล์นั้นละเอียดขึ้น - ทดสอบคัดลอก: FAQ ของ pdfminer.six แนะนำให้คัดลอกข้อความจากโปรแกรมอ่าน PDF ไปวางใน text editor ถ้าได้ข้อความถูก เครื่องมือดึงข้อความก็ควรได้เช่นกัน ถ้าได้ข้อความเพี้ยน ปัญหาอยู่ที่ไฟล์
- นับอักขระผิดปกติต่อหน้า: (cid:x), U+FFFD, Private Use Area, เครื่องหมายไทยที่ไม่มีพยัญชนะนำ วรรณยุกต์ซ้อน วรรณยุกต์ก่อนสระบน และนิคหิตตามด้วยสระอา (ํา ซึ่งควรเป็นสระอำ)
- ตั้งเกณฑ์ส่งหน้าไป OCR: Apache Tika ใช้หลักการเดียวกันใน ocrStrategy แบบ
autoคือทำ OCR เมื่อดึงข้อความได้น้อยกว่า 10 ตัวอักษร หรือมีอักขระที่ไม่มีค่า Unicode เกิน 10 ตัว เกณฑ์ของคุณควรตั้งจากผลตรวจเอกสารจริง - สุ่มเทียบด้วยตา: เลือกหน้าจากทุกกลุ่มเอกสาร ทั้งหน้าที่ผ่านและไม่ผ่านเกณฑ์ เทียบลำดับย่อหน้า ตาราง และตัวเลขกับต้นฉบับ
ตัวอย่าง 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ค่าเริ่มต้น falsecrawler.document.ocr.languageค่าเริ่มต้น engcrawler.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 แนะนำว่าไม่ต้องระบุรหัสภาษาถ้าไม่แน่ใจ เพราะการบังคับภาษาผิดอาจได้ข้อความไม่ครบหรือไม่ถูก
| เครื่องมือ | ภาษาไทยในเอกสาร | หมายเหตุ |
|---|---|---|
| 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 ข้อความไทยที่ไม่มีการขึ้นย่อหน้าจะตกไปถึงตัวคั่นสตริงว่าง ซึ่งคือการตัดกลางคำ
- ตัดตามโครงสร้างก่อน: ใช้หัวข้อจาก Word หรือ HTML และในระเบียบ ประกาศ หรือกฎหมาย ใช้หมวด ส่วน มาตรา และข้อเป็นจุดตัด หนึ่งข้อหรือหนึ่งมาตราที่ไม่ยาวเกินควรอยู่ใน chunk เดียว ตารางหนึ่งตารางไม่ควรถูกตัดกลาง
- ช่วงที่ยาวเกิน ตัดที่ขอบประโยค:
sent_tokenize()ของ PyThaiNLP ใช้ enginecrfcutเป็นค่าเริ่มต้น ซึ่งเอกสารระบุว่าเป็น CRF ที่ฝึกจากชุดข้อมูล TED ส่วนparagraph_tokenize()ใช้แบ่งย่อหน้า ข้อความราชการเขียนต่างจากภาษาพูด จึงควรตรวจจุดตัดกับเอกสารจริง - คุมขนาดด้วย token ของ embedding model: นับขนาด chunk ด้วย tokenizer ของโมเดลที่ใช้จริง ไม่ใช่จำนวนตัวอักษร ถ้าต้องตัดในประโยคที่ยาวมาก ให้ตัดที่ขอบคำจาก
word_tokenize()(engine เริ่มต้นnewmm) ไม่ใช่ตำแหน่งตัวอักษร - 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 ทีละชิ้น
| ฟิลด์ | ใช้ทำอะไร |
|---|---|
| รหัสเอกสาร ชื่อเอกสาร และที่อยู่ไฟล์ต้นฉบับ | แสดงแหล่งอ้างอิงและลิงก์กลับไปที่ไฟล์ |
| ลำดับหัวข้อ เช่น หมวด › ข้อ | ใส่ในคำอ้างอิง และเติมบริบทให้ 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 ให้สุ่มตรวจเป็นกิจวัตร ไม่ต้องรอชุดประเมินเต็มรูปแบบ:
- ค้นวลีที่รู้ว่ามีอยู่: เลือกวลีจากเอกสารแต่ละกลุ่ม โดยเฉพาะคำที่มีสระบน วรรณยุกต์ และสระอำ แล้วค้นในดัชนีโดยตรง ถ้าไม่เจอ ให้ดูข้อความใน chunk ว่าสะกดต่างจากที่พิมพ์อย่างไร
- เปิด chunk เทียบหน้าต้นฉบับ: ใช้เลขหน้าใน metadata เปิดหน้าต้นฉบับ แล้วเทียบว่าเนื้อหาครบ ลำดับถูก และตัวเลขในตารางตรงกัน
- ถามคำถามที่คำตอบอยู่ในตาราง หรือข้อที่ยาวหลายย่อหน้า: สองแบบนี้มักเสียจากการแบ่ง chunk ก่อน
- บันทึกผลเป็นรายการคงที่: เก็บวลีและคำถามชุดนี้ไว้ รันซ้ำเมื่อเปลี่ยนเครื่องมือหรือกฎ normalize แล้วนำไปรวมกับชุดประเมินตามวิธีวัดความถูกต้องของ RAG
UNIXDEV ช่วยเตรียมเอกสารสำหรับ RAG
UNIXDEV ออกแบบและพัฒนา RAG และ document AI สำหรับเอกสารภาษาไทย ภายใต้บริการ AI Agent และ RAG สำหรับองค์กร งานที่เกี่ยวกับบทความนี้คือ สำรวจชนิดเอกสาร ทดสอบวิธีดึงข้อความและ OCR กับหน้าตัวอย่างจริง วางกฎ normalize ภาษาไทยและกลยุทธ์แบ่ง chunk ตามโครงสร้างเอกสาร และวาง metadata สำหรับการอ้างอิงและการกรองสิทธิ์ ถ้าองค์กรใช้หรือกำลังพิจารณา Fess เป็นระบบค้นหา เราติดตั้งและตั้งค่า crawler ให้เข้ากับแหล่งเอกสารขององค์กรได้
ขั้นแรกที่แนะนำคือส่งตัวอย่างเอกสารแต่ละชนิดมาประเมินว่าดึงข้อความได้ดีแค่ไหน และต้องใช้ OCR กับส่วนใด ก่อนตัดสินใจเรื่องโมเดลหรือระบบค้น
แหล่งอ้างอิงและอ่านต่อ
- ISO 32000-1:2008 — Document management, Portable document format, PDF 1.7 (Adobe)
- The Unicode Standard 18.0 — Chapter 16, Southeast Asia (Thai) · Chapter 23, Special Areas and Format Characters
- Unicode Character Database — UnicodeData.txt · UAX #15 Unicode Normalization Forms · UAX #29 Unicode Text Segmentation
- Microsoft Typography — Developing OpenType fonts for Thai script
- pdfminer.six — Converting a PDF file to text · FAQ
- poppler-utils — pdftotext(1) · pdffonts(1)
- PyMuPDF — Appendix 1: Details on text extraction · Text recipes · Page
- Apache Tika — PDFParser (Apache PDFBox) · TikaOCR
- Tesseract — Improving the quality of the output · Command line usage · Data files · Data files in different versions · Releases
- OCRmyPDF — Cookbook · Common error messages
- Fess 15.9.0 release notes · Fess pull request #3520 (OCR settings)
- Azure Document Intelligence — Language support: OCR
- Google Cloud Vision — OCR language support
- Amazon Textract — FAQs
- PyThaiNLP 5.4.0 — pythainlp.tokenize · pythainlp.util
- LangChain — Recursive text splitter
- bact/constitution — Thai constitution draft PDF to text (GitHub)
- บทความที่เกี่ยวข้อง: RAG คืออะไร: ทำ AI ค้นเอกสารภาษาไทยให้ตอบถูกและตรวจสอบได้
- บทความที่เกี่ยวข้อง: ย้ายข้อมูลจาก Legacy System ไม่ให้ภาษาไทยเพี้ยน
- บทความที่เกี่ยวข้อง: นำ AI/LLM ไปใช้จริงในองค์กรไทย
- ความสามารถด้าน Fess ของ UNIXDEV
- บทความทั้งหมดในหมวด AI Agent และ RAG สำหรับองค์กร
ตรวจข้อมูล ณ วันที่ 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 ใหม่ไหม?
ต้องประมวลผลเอกสารกลุ่มที่ได้รับผลใหม่ และใช้กฎชุดใหม่กับคำค้นไปพร้อมกัน ไม่อย่างนั้นคำเดียวกันจะสะกดต่างกันระหว่างดัชนีกับคำค้น จากนั้นรันรายการวลีและคำถามสุ่มตรวจชุดเดิมซ้ำเพื่อเทียบผลกับก่อนเปลี่ยน