---
title: "แผนการนำเข้าข้อมูล (Data Ingestion Plan)"
titleEn: "Data Ingestion Plan"
clauses: ["4.5.1", "7.2.4"]
phase: 2
category: "ออกแบบ"
generated: false
---

# แผนการนำเข้าข้อมูล (Data Ingestion Plan)

**อ้างอิง TOR:** ข้อ 4.5.1 (จัดทำแผนการนำเข้าข้อมูล) และข้อ 7.2.4 (เอกสารส่งมอบงวดที่ 2) ประกอบภาคผนวก ก ข้อ 4.6–4.8 และภาคผนวก ข.1
**ฉบับ:** 1.0 — วันที่ 3 ตุลาคม พ.ศ. 2569

> แผนนี้อธิบายกระบวนการนำเข้าเอกสารเข้าสู่ฐานองค์ความรู้ตามที่ implement จริงในระบบ (`src/server/ingestion/*`, `src/domain/ingestion/*`) สถานะความครอบคลุมล่าสุดที่วัดจากฐานข้อมูลดูได้ที่ `/documents/ingestion-report` (สร้างอัตโนมัติด้วย `npm run report:ingestion`) และแบบสดที่ `/admin/knowledge-base`

---

## 1. วัตถุประสงค์และขอบเขต

1. นำเข้าพระไตรปิฎกและอรรถกถาแปล ฉบับมหามกุฏราชวิทยาลัย 91 เล่ม (ภาคผนวก ข.1: พระวินัยปิฎก เล่ม 1–10, พระสุตตันตปิฎก เล่ม 11–74, พระอภิธรรมปิฎก เล่ม 75–91) และเอกสารประกอบ (ฎีกา ตำราร่วมสมัย ธรรมะพ่อแม่ครูอาจารย์)
2. ทุก chunk ต้องมี metadata ครบพอสร้าง Citation รูปแบบมาตรฐานและตรวจสอบย้อนกลับถึงเล่ม/ข้อ/หน้าได้ (ก.3.6.3, ก.4.8.3)
3. ห้ามตัดตอนเนื้อหาพระไตรปิฎกกลางข้อ (ก.4.6.2, ก.4.8.2)
4. ป้องกันคำสั่งแฝงในเอกสาร (indirect prompt injection) ก่อนเข้าดัชนี (ก.4.4.1)
5. ปรับดัชนีได้อัตโนมัติผ่าน pipeline โดยไม่ต้อง redeploy (4.5.3, ก.4.7.3, ก.6.3.2)

**แนวทางตาม TOR ภาคผนวก ข:** ระยะแรกใช้พระไตรปิฎกฉบับออนไลน์ แล้วสลับเป็นฉบับ มมร. 91 เล่มเมื่อมหาวิทยาลัยส่งมอบไฟล์ (หรือเมื่อเว็บไซต์คลังปัญญา Dhammaverse พร้อมเป็นแหล่งข้อมูล)

---

## 2. รูปแบบไฟล์ที่รองรับ (Parsers)

ตัวแยกวิเคราะห์อยู่ที่ `src/server/ingestion/parsers/index.ts` (`parseFile` เลือกตามนามสกุลไฟล์) ขนาดไฟล์อัปโหลดสูงสุด 20MB (`MAX_UPLOAD_BYTES` ใน `src/application/ingestion/kb-admin.ts`)

| นามสกุล | MIME | ไลบรารี | พฤติกรรม |
|---|---|---|---|
| `.pdf` | `application/pdf` | `unpdf` (`extractText`, แยกหน้า) | ดึงข้อความรายหน้าแล้วต่อกัน; ถ้าไม่มีชั้นข้อความเลยคืนคำเตือน “PDF ไม่มีชั้นข้อความ (อาจเป็นไฟล์สแกน ต้อง OCR ก่อนนำเข้า)” และงานล้มเหลวพร้อมข้อความนี้ |
| `.docx` | `application/vnd.openxmlformats-officedocument.wordprocessingml.document` | `mammoth.extractRawText` | ข้อความล้วน + คำเตือนของ mammoth (สูงสุด 5 รายการ) |
| `.xml` (TEI) | `application/xml` | `fast-xml-parser` (preserveOrder) | `<div type="volume" n>` → เล่ม, `<div type="item" n>` → ข้อ, `<pb n>` → หน้า แปลงเป็น marker `[เล่ม N][ข้อ N][หน้า N]`; `<pb>` กลางข้อสร้าง marker ข้อเดิมหน้าใหม่ (ChunkingPolicy รวมเป็น chunk เดียวและเก็บหน้าเริ่ม–หน้าจบ); ข้าม `<note>` และ `<teiHeader>`; ถ้าไม่พบ `<div type="item">` นำเข้าแบบข้อความทั่วไปพร้อมคำเตือน |
| `.txt`, `.md` | `text/plain`, `text/markdown` | อ่าน UTF-8 (ตัด BOM) | ไฟล์ seed `.md` ถูกตัด frontmatter ด้วย `stripFrontmatter()` ก่อนนำเข้า |

นามสกุลอื่นถูกปฏิเสธด้วยข้อความ “ไม่รองรับไฟล์ชนิด .xxx (รองรับ PDF, TXT, TEI-XML, DOCX)” ไฟล์ตัวอย่างสำหรับทดสอบทั้ง 4 ชนิดอยู่ที่ `tests/fixtures/kb/` (รวม `no-text.pdf` สำหรับกรณีไฟล์สแกน)

---

## 3. ขั้นตอนของ Pipeline

ไฟล์หลัก: `src/server/ingestion/pipeline.ts`

```mermaid
flowchart LR
  U["อัปโหลด / seed / reindex"] --> J["IngestionJob + IngestionJobItem (queued)"]
  J --> P1["parse<br/>parseFile + stripFrontmatter"]
  P1 --> P2["sanitize<br/>sanitizeText"]
  P2 --> UW["unwrapLines<br/>(tipitaka/atthakatha)"]
  UW --> P3["chunk<br/>chunkText → ChunkingPolicy"]
  P3 --> P4["embed<br/>pseudo-embedding 512 มิติ"]
  P4 --> P5["index<br/>transaction Chunk + reindexDocument"]
  P5 --> D["done (status indexed)"]
  P1 -. error .-> F["failed + errorMsg"]
  P3 -. error .-> F
```

| Stage (`IngestionJobItem.stage`) | การทำงาน |
|---|---|
| `queued` | สร้างงานด้วย `createJob(type, requestedBy, docs)` ชนิด `ingest` / `reindex_all` / `reindex_selected` / `delete` |
| `parse` | อ่านไฟล์ต้นฉบับจาก `sourceFile` (ใต้ `STORAGE_DIR` ค่าเริ่มต้น `storage/uploads` หรือ `seed/knowledge/`) แล้วแยกข้อความ |
| `sanitize` | `sanitizeText()` (หัวข้อ 7) และนับ `removedLines` |
| `chunk` | ข้อความพระไตรปิฎกออนไลน์ที่ตัดบรรทัดกลางประโยคถูกต่อกลับด้วย `unwrapLines()` (ใช้ความกว้างบรรทัด p90) แล้วแบ่งด้วย `chunkText()` → `chunkDocument()`; ไฟล์ที่ marker ซ้ำหลายบล็อก (อรรถกถา/ฉบับแปลที่ผูกกับข้อเดียวกัน) แยกแต่ละบล็อกเป็น chunk ที่อ้างข้อเดียวกัน; ไม่มีเนื้อหา → ล้มเหลว “ไม่พบเนื้อหาที่นำเข้าได้ในไฟล์” |
| `embed` | คำนวณ pseudo-embedding 512 มิติ (feature hashing FNV-1a, ปัดทศนิยม 4 ตำแหน่ง) เก็บเป็น JSON ใน `Chunk.embedding` เป็น cache |
| `index` | ใน transaction เดียว: chunk ที่ลำดับและเนื้อหาเหมือนเดิม **คง id เดิม** (อัปเดตเฉพาะ metadata) เพื่อไม่ให้ Citation ในประวัติและ `expectedRefs` ของชุดทดสอบหลุด; ลบเฉพาะ chunk ที่เนื้อหาเปลี่ยน (พร้อม Citation ที่อ้างถึง) แล้วสร้างใหม่; ตั้งเอกสารเป็น `indexed` พร้อม `chunkCount`, `sanitizedLines`; อัปเดตดัชนีในหน่วยความจำด้วย `reindexDocument()` |
| `done` / `failed` | บันทึก `chunkCount`, `durationMs` หรือ `errorMsg` |

**Job runner:** งานถูกต่อคิวแบบ promise chain ใน process เดียว (`enqueueJob`) ประมวลผลทีละ item; item ที่ล้มเหลวไม่หยุดทั้งงาน สถานะงานสุดท้ายเป็น `completed` (ไม่มีล้มเหลว), `partial` (สำเร็จบางส่วน) หรือ `failed` (ล้มเหลวทั้งหมด) และเมื่อมีรายการล้มเหลวระบบส่งแจ้งเตือนชนิด `ingestion_failed` (บันทึก `AlertEmailLog` หรือส่ง SMTP) — ใน Production ใช้ BullMQ + worker แยก (ingestion service ใน Helm chart)

**การลบ:** งานชนิด `delete` ถอดเอกสารออกจากดัชนีค้นหาและตั้งสถานะ `deleted` + `deletedAt` (soft delete) เอกสารที่ถูกลบแล้วไม่สามารถนำเข้า/ทำดัชนีซ้ำได้

---

## 4. นโยบายการแบ่ง Chunk (Chunking Policy)

ไฟล์: `src/domain/ingestion/chunking-policy.ts` (pure TypeScript ทดสอบใน `tests/unit/chunking-policy.test.ts`)

| ค่าคงที่ (`CHUNKING`) | ค่า | ความหมาย |
|---|---|---|
| `oversizedTokens` | 3,000 | เกินค่านี้ติดธง `oversized` แต่ **ไม่แบ่ง** ข้อพระไตรปิฎก |
| `targetTokens` | 400 | ขนาดเป้าหมายของ semantic chunk |
| `minTokensForTopicCut` | 120 | ต้องสะสมอย่างน้อยเท่านี้ก่อนตัดเพราะหัวข้อเปลี่ยน |
| `topicShiftCosine` | 0.35 | cosine ระหว่างย่อหน้าติดกันต่ำกว่านี้ = หัวข้อเปลี่ยน |
| `maxHeadingChars` | 200 | ข้อความก่อน marker แรกที่สั้นกว่านี้ใช้เป็นหัวข้อของ chunk แรก |

การประมาณโทเคน: `ceil(จำนวนอักขระ / 3)` (เหมาะกับภาษาไทย deterministic)

### 4.1 โหมด marker (docType `tipitaka` และ `atthakatha`)

- ค้นหา marker `[เล่ม N][ข้อ N][หน้า N]` (รองรับเลขไทย ๐–๙ และหน้าเว้นว่าง)
- **1 ข้อ = 1 chunk:** block ติดกันที่เป็น (เล่ม, ข้อ) เดียวกันถูกรวม — ข้อที่คร่อมหลายหน้าเก็บ `page` (หน้าแรก) และ `pageEnd` (หน้าสุดท้าย)
- หน้า 0 หรือไม่มีหน้า → `page = null`
- บรรทัดหัวข้อ Markdown ท้าย block กลายเป็น `heading` ของข้อถัดไป
- ข้อความนำหน้า marker แรกที่ยาวเกิน 200 อักขระเป็น chunk ของตัวเอง (`item = null`)
- ถ้าเอกสารประเภทนี้ไม่มี marker เลย ใช้โหมด semantic แทน

### 4.2 โหมด semantic (docType อื่น)

- แยก section ตามหัวข้อ Markdown (heading เป็นลำดับชั้น `ก > ข`)
- แยกย่อหน้า → ย่อหน้าที่ยาวเกิน 400 tokens แบ่งตามประโยค (`Intl.Segmenter("th", sentence)`) → วรรค → อักขระ ตามลำดับ
- สะสมหน่วยจนเกิน 400 tokens หรือ **ตัดเมื่อหัวข้อเปลี่ยน** (cosine ของ pseudo-embedding < 0.35 และสะสม ≥120 tokens) — ไม่ได้ตัดตามจำนวนโทเคนล้วน

### 4.3 สถิติขนาด chunk ปัจจุบัน

จากฐานข้อมูลสาธิต ณ วันที่จัดทำ (อ่านแบบ read-only): chunk ทั้งหมด 777 รายการ, ติดธง oversized 5 รายการ, ขนาดสูงสุด 17,848 tokens (ข้อพระไตรปิฎกที่ยาวมาก ยังคงเป็น chunk เดียวตาม ก.4.6.2) — ตัวเลขทางการที่สร้างอัตโนมัติอยู่ใน `/documents/ingestion-report`

เมื่อส่ง chunk ยาวมากเป็นบริบทให้ LLM ระบบย่อเฉพาะตอนสร้าง prompt (`bestWindow()` เลือกช่วงที่ตรงคำถามมากที่สุด 2,400 อักขระสำหรับถาม-ตอบ / 4,500 สำหรับ Dhamma Talk) ส่วนดัชนีและ Citation ยังอ้างข้อเต็ม

---

## 5. Metadata และรูปแบบ Citation

### 5.1 ฟิลด์ metadata (`KnowledgeDocument` / `Chunk`)

| ฟิลด์ | ระดับ | ตัวอย่าง | หมายเหตุ |
|---|---|---|---|
| `title` | เอกสาร | มงคลสูตร | บังคับ |
| `docType` | เอกสาร | `tipitaka` | `tipitaka` \| `atthakatha` \| `tika` \| `contemporary` \| `teacher_dhamma` (`DOC_TYPES`) |
| `edition` | เอกสาร | `ไทย`, `บาลี`, `มมร.`, `English (Sujato, CC0)` | ใช้ในวงเล็บของ citation |
| `pitaka` | เอกสาร | `vinaya` \| `sutta` \| `abhidhamma` | |
| `nikaya`, `sutta` | เอกสาร | ขุททกนิกาย ขุททกปาฐะ / มงคลสูตร | |
| `volume` | เอกสาร/chunk | 25 | เล่มของฉบับที่นำเข้า (ฉบับหลวง 45 เล่ม สำหรับข้อมูลออนไลน์) |
| `mbuVolume` | เอกสาร | 39 | เล่มในฉบับ มมร. 91 เล่ม — ใช้คำนวณ coverage ภาคผนวก ข |
| `abbreviation` | เอกสาร | `ขุ.ขุ.` | ตัวย่อคัมภีร์สำหรับ citation |
| `item`, `page`, `pageEnd`, `heading` | chunk | 5 / 3 / 3 | จาก marker |
| `paliAliases` | เอกสาร/chunk | `maṅgala\|mangala sutta` | คำบาลี/อังกฤษ ช่วยค้นข้ามภาษา (น้ำหนักสูงใน BM25) |
| `precedenceScore` | เอกสาร | 1.0 | 0–2 ปรับได้ใน admin |
| `sourceType`, `sourceFile`, `sourceUrl`, `sourceNote` | เอกสาร | `seed` / `online` / `upload` | ที่มาและหมายเหตุสิทธิ์ |
| `oversized`, `tokenCount` | chunk | | |

### 5.2 รูปแบบ Citation

สร้างโดย `formatCitationLabel()` ใน `src/domain/ai/citation.ts`:

```
{abbreviation} ({edition}) {volume}/{item}/{page}
ตัวอย่าง: ขุ.ขุ. (ไทย) 25/5/3
```

- ส่วนท้ายที่ไม่มีค่าถูกตัด (ไม่มีหน้า → `25/5`); ส่วนกลางที่ไม่มีค่าแสดง `-`
- ไม่มี `abbreviation` ใช้ `title` แทน
- LLM อ้างด้วย `[[cite:<chunkId>]]` หรือ `[[cite:<n>]]` แล้วโหนด `outputGuardrail` แปลงเป็น label และตัดการอ้างที่ไม่อยู่ในรายการอ้างอิง

### 5.3 ประเภทเอกสารและลำดับความสำคัญ (Precedence)

ค่าเริ่มต้นใน `DEFAULT_PRECEDENCE` (`src/domain/retrieval/relevance-scoring.ts`) ใช้คูณคะแนนหลัง rerank (ก.4.8.3(3)):

| docType | ความหมาย | precedence เริ่มต้น |
|---|---|---:|
| `tipitaka` | พระไตรปิฎก | 1.0 |
| `atthakatha` | อรรถกถา | 0.9 |
| `tika` | ฎีกา | 0.8 |
| `contemporary` | ตำรา/พระนิพนธ์ร่วมสมัย | 0.7 |
| `teacher_dhamma` | ธรรมะพ่อแม่ครูอาจารย์ | 0.6 |

ผู้ดูแลปรับรายเอกสารได้ (0–2) ที่ `/admin/knowledge-base` → แก้ metadata (`PATCH /api/admin/knowledge-base/[id]`) ระบบอัปเดตดัชนีในหน่วยความจำทันทีและบันทึก audit before/after

---

## 6. รูปแบบ marker และไฟล์ต้นฉบับ

ไฟล์ seed ทุกไฟล์ใน `seed/knowledge/` เป็น Markdown ที่มี frontmatter บรรทัดเดียวต่อคีย์ (รายละเอียดใน `scripts/data/README.md`):

```
---
title: "มงคลสูตร"
docType: tipitaka
edition: "ไทย"
pitaka: sutta
nikaya: "ขุททกนิกาย ขุททกปาฐะ"
sutta: "มงคลสูตร"
volume: 25
mbuVolume: 39
abbreviation: "ขุ.ขุ."
sourceUrl: "https://84000.org/tipitaka/read/byitem.php?book=25&item=5"
sourceNote: "..."
paliAliases: "maṅgala|mangala sutta|..."
---
[เล่ม 25][ข้อ 5][หน้า 3]
<ข้อความทั้งข้อ>
```

กติกา:
- ฉบับหลวง/บาลี: 1 บล็อก = 1 ข้อ; `หน้า` = หน้าแรกของข้อนั้นในเล่ม
- อรรถกถาและฉบับอังกฤษ: `หน้า 0` (ไม่ทราบหน้า) และ marker ซ้ำได้หลายบล็อก ทุกบล็อกชี้ไปยังเล่ม/ข้อฉบับหลวงที่สอดคล้อง
- `contemporary` / `teacher_dhamma`: ไม่มี marker เป็นย่อหน้าธรรมดา
- ไฟล์ฉบับ มมร. ที่จะได้รับควรจัดในรูปแบบเดียวกัน หรือเป็น TEI-XML ที่มี `<div type="item" n>` และ `<pb n>`

---

## 7. การ Sanitize ข้อความ (กัน Indirect Prompt Injection)

ไฟล์: `src/domain/ingestion/sanitize.ts` (ทดสอบใน `tests/unit/sanitize.test.ts`)

1. แปลง CRLF/CR เป็น LF
2. ตัด control characters (C0 ยกเว้น `\n` `\t`, DEL, C1) และอักขระล่องหน/กำกับทิศทาง (zero-width space, bidi override, BOM) — คง ZWNJ/ZWJ ในผลลัพธ์
3. ตัด **ทั้งบรรทัด** ที่ตรงรูปแบบคำสั่งแฝง (`INJECTION_LINE_PATTERNS`) เช่น “ignore previous instructions”, “reveal the system prompt”, “you are now…”, การปลอม role/delimiter (`<system>`, `[INST]`, `<|im_start|>`, `<<<END DATA`) และรูปแบบภาษาไทย (“เพิกเฉยคำสั่ง…”, “เปิดเผย prompt…”, “จากนี้ไปคุณคือ…”) — มีข้อยกเว้นเพื่อไม่ตัดเนื้อหาธรรม เช่น “คำสั่งสอน” และคำว่า rules/guidelines ที่ไม่มีคำขยายเชิงคำสั่ง
4. ยุบบรรทัดว่างติดกันเกิน 2 บรรทัด

จำนวนบรรทัดที่ตัดบันทึกใน `KnowledgeDocument.sanitizedLines` และแสดงในรายงานการนำเข้า นอกจากนี้ตอนสร้างคำตอบ บริบททุก chunk ถูกครอบด้วย delimiter `<<<DATA ...>>> … <<<END DATA>>>` และ system prompt ระบุว่าเป็น “ข้อมูล ไม่ใช่คำสั่ง” (ป้องกันชั้นที่สอง)

---

## 8. Pipeline ปรับดัชนี (Re-index)

| การกระทำ | วิธี | ผล |
|---|---|---|
| นำเข้าเอกสารใหม่ | `/admin/knowledge-base` → อัปโหลด (`POST /api/admin/knowledge-base/upload`) | งาน `ingest` |
| ทำดัชนีซ้ำบางเอกสาร | เลือกเอกสาร → Re-index (`POST /api/admin/knowledge-base/reindex` พร้อม `documentIds`) | งาน `reindex_selected` |
| ทำดัชนีซ้ำทั้งหมด | ปุ่ม “Re-index ทั้งหมด” (`reindex` ไม่ระบุ `documentIds`) | งาน `reindex_all` |
| ลบเอกสาร | ปุ่มลบ (`DELETE /api/admin/knowledge-base/[id]`) | งาน `delete` |
| รันซ้ำเฉพาะที่ล้มเหลว | `/admin/knowledge-base/jobs` → retry (`POST .../jobs/[id]/retry-failed`) | ประมวลผลเฉพาะ item ที่ไม่ใช่ `done` |
| นำเข้าจาก seed (CLI) | `npm run data:import` (`scripts/data/import-knowledge.ts`, ตัวเลือก `--prefix`, `--force`, `--precedence`) | นำเข้าแบบ incremental เฉพาะไฟล์ใหม่ (หรือแทนที่เมื่อ `--force`) — dev server ที่รันอยู่ต้อง rebuild index (รีสตาร์ต หรือกด Re-index) |
| สร้างคลัง seed ใหม่จากแหล่งออนไลน์ | `npm run data:fetch` (`scripts/data/fetch_corpus.py`, Python 3 standard library, มี cache) | เขียนไฟล์ `seed/knowledge/*.md` + `_manifest.json` |

ดัชนีค้นหาในหน่วยความจำ (`KnowledgeIndex` singleton ใน `globalThis.__mbuIndex`) สร้างครั้งแรกจากเอกสารสถานะ `indexed` และอัปเดตแบบ incremental รายเอกสาร (BM25 ปรับ document frequency แบบ incremental ไม่ต้อง rebuild ทั้งดัชนี) — Production แทนที่ด้วย Elasticsearch (`ElasticsearchSearchAdapter`)

---

## 9. การตรวจสอบความครบถ้วน (Completeness Verification)

| ระดับ | วิธีตรวจ | หลักฐาน |
|---|---|---|
| ไฟล์ → เอกสาร | ทุกไฟล์มีระเบียน `KnowledgeDocument` สถานะ `indexed`; เทียบจำนวนไฟล์ใน `seed/knowledge/_manifest.json` กับจำนวนเอกสาร | `/documents/ingestion-report` |
| เอกสาร → chunk | `KnowledgeDocument.chunkCount` เท่ากับจำนวนแถวใน `Chunk`; ไม่มีเอกสาร `pending`/`failed` ค้าง | การ์ด coverage ที่ `/admin/knowledge-base`, `GET /api/admin/knowledge-base/coverage` |
| ข้อ → chunk | จำนวน marker ข้อในไฟล์ต้นฉบับ (ฉบับหลวง/บาลี) ≈ จำนวน chunk ของเอกสาร (marker ซ้ำของข้อเดียวกันถูกรวม) | ตรวจได้จาก `/admin/knowledge-base/[id]` (รายการ chunk พร้อมเล่ม/ข้อ/หน้า) |
| เล่ม → 91 เล่ม | นับ `mbuVolume` ที่ไม่ซ้ำของเอกสารที่ทำดัชนีแล้ว เทียบ 91 เล่ม แยกตามปิฎก และเทียบเป้า 20% ของงวดที่ 2 (`TOR_PHASE2_TARGET_PERCENT`) | `src/application/ingestion/coverage.ts` |
| ฉบับหลวง 45 เล่ม | นับ `volume` ของเอกสารพระไตรปิฎกที่ไม่ใช่ฉบับ มมร. | coverage report (ส่วน `royal`) |
| คุณภาพการค้นคืน | Recall@5 ของชุด RET-1/2/3 (300 คำถามที่สร้างจากข้อความจริงในคลัง) | `/admin/evaluation`, `/documents/baseline-evaluation-report` |
| ความถูกต้องของเนื้อหา | สุ่มเปิดเนื้อหาเต็มผ่าน Citation Modal เทียบต้นฉบับ (`sourceUrl`) | ขั้นตอน UAT กับผู้เชี่ยวชาญของมหาวิทยาลัย |

---

## 10. สถานะคลังข้อมูลปัจจุบัน (ฉบับออนไลน์ระยะแรก)

นับจากไฟล์ใน `seed/knowledge/` ณ วันที่จัดทำเอกสาร (ไม่รวม `_manifest.json`):

| รายการ | จำนวน |
|---|---:|
| ไฟล์เอกสาร | 169 |
| — `tipitaka` (ไทย 138, บาลี 6, English Sujato CC0 7) | 151 |
| — `atthakatha` (ฉบับ มมร.) | 4 |
| — `contemporary` (นวโกวาท ฉบับ มมร. พ.ศ. ๒๕๑๓) | 11 |
| — `teacher_dhamma` (เว็บไซต์ มมร.) | 3 |
| เอกสารแยกตามปิฎก: วินัย / สุตตันตะ / อภิธรรม | 13 / 131 / 7 |
| เล่มฉบับหลวงที่มีข้อมูล | 1, 4, 10, 11, 12, 16, 17, 19, 20, 25, 34, 35 (12 เล่ม บางข้อ) |
| เล่มฉบับ มมร. ที่ระบุใน metadata (`mbuVolume`) | 6, 14, 16, 34, 39, 40 (6 จาก 91 เล่ม) |
| บล็อก marker ข้อในไฟล์ | 721 |
| ขนาดข้อความรวม | ประมาณ 0.97 ล้านอักขระ (ไฟล์รวม 3.0 MB) |
| ตัวย่อคัมภีร์ที่ใช้ | 24 ค่า (เช่น `วิ.มหา.`, `วิ.ม.`, `ที.ม.`, `ม.มู.`, `สํ.นิ.`, `องฺ.ติก.`, `ขุ.ขุ.`, `ขุ.ธ.`, `อภิ.สํ.`, `อภิ.วิ.`, `DN 22`, `SN 56.11`) |

แหล่งข้อมูลและสิทธิ์ (จาก `scripts/data/README.md`): พระไตรปิฎกฉบับหลวงและบาลีฉบับสยามรัฐจาก 84000.org, อรรถกถาแปลฉบับ มมร. จาก 84000.org, คำแปลอังกฤษของภิกษุสุชาโต (CC0) จาก SuttaCentral, นวโกวาท (สาธารณสมบัติ) จากวิกิซอร์ซ และเนื้อหาจากเว็บไซต์ www.mbu.ac.th

> ความครอบคลุมเทียบ 91 เล่มที่วัดจากฐานข้อมูลจริงอยู่ที่ `/documents/ingestion-report` (สถานะ ณ การสร้างรายงานล่าสุด: 6 จาก 91 เล่ม = 6.6% ซึ่ง **ยังต่ำกว่าเป้าหมาย ≥20% ของงวดที่ 2 ตาม TOR 7.2.5** — ข้อ 7.2.5, 4.5.2, ข.1 จึงมีสถานะ partial ในหน้า `/tor`)

---

## 11. แผนนำเข้าพระไตรปิฎกและอรรถกถาฉบับ มมร. 91 เล่ม

| ระยะ | กิจกรรม | ผลผลิต | เกณฑ์สำเร็จ |
|---|---|---|---|
| 0 (ดำเนินการแล้ว) | ใช้ฉบับออนไลน์บางส่วน + อรรถกถา มมร. จาก 84000.org เพื่อพัฒนา/ทดสอบ pipeline และชุด RET-1..3 | คลัง seed 169 เอกสาร | pipeline ผ่านชุดทดสอบ `tests/api/knowledge-base.test.ts` |
| 1 | รับไฟล์ต้นฉบับ 91 เล่มจากมหาวิทยาลัย (ต้องการ: PDF ที่มีชั้นข้อความ, DOCX, หรือ TEI-XML) และทำบัญชีไฟล์ (เล่ม/ปิฎก/จำนวนหน้า/checksum) | ทะเบียนไฟล์ | ครบ 91 เล่ม |
| 2 | ตรวจคุณภาพไฟล์: PDF มีชั้นข้อความหรือไม่ (ระบบแจ้ง “ไม่มีชั้นข้อความ” อัตโนมัติ), การเข้ารหัสอักษรไทย/บาลี, ตำแหน่งเลขข้อ/หน้า | รายงานคุณภาพไฟล์ | ระบุไฟล์ที่ต้อง OCR |
| 3 | แปลงเป็นรูปแบบมี marker `[เล่ม N][ข้อ N][หน้า N]` หรือ TEI `<div type="item">`/`<pb>` (สคริปต์แปลงเฉพาะรูปแบบไฟล์ที่ได้รับ) ตั้ง metadata `edition = มมร.`, `mbuVolume`, `abbreviation` | ไฟล์พร้อมนำเข้า | สุ่มตรวจ 5% ของข้อต่อเล่ม |
| 4 | นำเข้าทีละปิฎกผ่าน `/admin/knowledge-base` หรือสคริปต์; งวด 2 อย่างน้อย 19 เล่ม (≥20%), งวด 3 ครบ 91 เล่ม | งานนำเข้า (IngestionJob) | ไม่มี item ล้มเหลวค้าง |
| 5 | สลับแหล่งข้อมูล: ตั้ง precedence ฉบับ มมร. ให้สูงกว่าฉบับออนไลน์ หรือลบเอกสารฉบับออนไลน์ที่ซ้ำ (งาน `delete`) และปรับ `expectedRefs` ของชุด RET ให้อ้างฉบับ มมร. | ดัชนีฉบับ มมร. | Recall@5 ≥80% ทั้ง 3 ชุด |
| 6 | เมื่อเว็บไซต์ Dhammaverse พร้อม: เชื่อมเป็นแหล่งข้อมูล (`sourceType = online`, `sourceUrl`) ให้ Citation Modal ลิงก์ไปยังหน้าคลังปัญญา | ลิงก์ย้อนกลับ | ตรวจลิงก์ครบ |
| 7 | ออกรายงาน `npm run report:ingestion` ทุกครั้งที่นำเข้าเป็นชุด และแนบในรายงานงวด | `/documents/ingestion-report` | coverage ตามเป้า |

---

## 12. ความเสี่ยงและมาตรการ

| ความเสี่ยง | ผลกระทบ | มาตรการ |
|---|---|---|
| ไฟล์ 91 เล่มเป็นภาพสแกน (ไม่มีชั้นข้อความ) | นำเข้าไม่ได้/ข้อความผิด | parser ตรวจและแจ้งเตือนอัตโนมัติ; ทำ OCR ภาษาไทย-บาลีนอกระบบ (เช่น Tesseract tha) แล้วตรวจทานโดยผู้เชี่ยวชาญก่อนนำเข้า; จัดลำดับเล่มที่มีไฟล์ข้อความก่อน — ระบบสาธิตยังไม่มีขั้น OCR ในตัว |
| อักษรบาลี (นิคหิต/พินทุ) เข้ารหัสต่างกัน | ค้นไม่เจอ | `normalizeText` ใน tokenizer + char n-gram ใน pseudo-embedding + `paliAliases` + พจนานุกรม synonym |
| เลขข้อ/หน้าในต้นฉบับไม่สม่ำเสมอ | Citation ผิด | ตรวจ marker ก่อนนำเข้า, สุ่มตรวจผ่าน Citation Modal |
| ข้อยาวมาก (oversized) | บริบทเกินเพดานโทเคน | ไม่แบ่งข้อในดัชนี; ย่อเฉพาะตอนสร้าง prompt (`bestWindow`) + `fitContext` |
| คำสั่งแฝงในเอกสาร | AI ถูกชักนำ | sanitize + delimiter + guardrail |
| ไฟล์ซ้ำระหว่างฉบับออนไลน์กับฉบับ มมร. | ผลค้นซ้ำซ้อน | precedence/ลบฉบับออนไลน์เมื่อสลับ |
| ดัชนีในหน่วยความจำของระบบสาธิตจำกัดด้วย RAM ของเครื่องเดียว | รองรับ 91 เล่มได้ช้า | Production ใช้ Elasticsearch + bge-m3 ตาม Solution Architecture |
